nx_turn_relay.nx source
↩ module page · 883 lines · 38219 B
1// nx_turn_relay.nx -- Sovereign TURN relay foundation (RFC 5766/8656).
2// Arc 4 Phase A.1 per [[NISHI_BITS_UP_EXCEED_INDUSTRY]].
3//
4// Closes the openrelay.metered.ca dependency that's currently
5// `slow-lane-grandfathered` in web_assets/video.js. Once nx_turn_relay
6// ships on Texas Synology + Hetzner Frankfurt VPS, the slow-lane
7// audit grader can flag any remaining openrelay reference as a real
8// violation per [[feedback-no-slow-lane-no-paid-priority]].
9//
10// This file provides the message FRAMING layer:
11// - nx_turn_parse_allocate: parse a TURN ALLOCATE request
12// - nx_turn_build_allocate_success: build a 0x0103 success response
13// - nx_turn_build_allocate_error: build a 0x0113 error response
14//
15// Future phases (per [[NISHI_S_CLASS_ROUTING_ROADMAP]] Q1):
16// - Phase A.2: SEND indication forwarding (relay → peer)
17// - Phase A.3: DATA indication (peer → relay → client)
18// - Phase A.4: CREATE-PERMISSION (allow specific peer IPs)
19// - Phase A.5: REFRESH (extend allocation lifetime)
20// - Phase A.6: CHANNEL-BIND (compact data channel)
21// - Phase B: short-term credentials (HMAC-SHA1 MESSAGE-INTEGRITY)
22// - Phase C: multi-egress + QUIC-on-443 fingerprint (slow-lane cardinal)
23//
24// Patent-clean math: TURN is an IETF standard, royalty-free per
25// IETF IPR policy. We implement the wire format; no proprietary
26// extensions.
27
28// nx_turn_relay.nx -- RFC 5766 message framing layer only (NO HMAC,
29// NO syscalls). Pure byte-manipulation primitives. Importable by
30// either syscall-clan consumer (x86_64 daemon via nx_udp's
31// nx_syscalls_x86_64; or WASM smoke via nx_hmac_sha1's nx_syscalls)
32// without a duplicate-symbol clash. See
33// [[reference-syscall-module-duality-nx_syscalls-vs-syscalls]].
34// MESSAGE-INTEGRITY in nx_turn_msgintegrity.nx.
35
36// ============================================================================
37// TURN message types (RFC 5766 §13)
38// ============================================================================
39const NX_TURN_TYPE_ALLOCATE_REQUEST: i64 = 0x0003
40const NX_TURN_TYPE_ALLOCATE_SUCCESS: i64 = 0x0103
41const NX_TURN_TYPE_ALLOCATE_ERROR: i64 = 0x0113
42const NX_TURN_TYPE_REFRESH_REQUEST: i64 = 0x0004
43const NX_TURN_TYPE_SEND_INDICATION: i64 = 0x0016
44const NX_TURN_TYPE_DATA_INDICATION: i64 = 0x0017
45const NX_TURN_TYPE_CREATE_PERM_REQUEST: i64 = 0x0008
46const NX_TURN_TYPE_CHANNEL_BIND_REQUEST: i64 = 0x0009
47
48// STUN attributes used by TURN (RFC 5766 §14)
49const NX_TURN_ATTR_MAPPED_ADDR: i64 = 0x0001
50const NX_TURN_ATTR_USERNAME: i64 = 0x0006
51const NX_TURN_ATTR_MESSAGE_INTEGRITY: i64 = 0x0008
52const NX_TURN_ATTR_ERROR_CODE: i64 = 0x0009
53const NX_TURN_ATTR_REALM: i64 = 0x0014
54const NX_TURN_ATTR_NONCE: i64 = 0x0015
55const NX_TURN_ATTR_XOR_RELAYED_ADDR: i64 = 0x0016
56const NX_TURN_ATTR_REQUESTED_TRANSPORT: i64 = 0x0019
57const NX_TURN_ATTR_DONT_FRAGMENT: i64 = 0x001A
58const NX_TURN_ATTR_LIFETIME: i64 = 0x000D
59const NX_TURN_ATTR_XOR_MAPPED_ADDR: i64 = 0x0020
60const NX_TURN_ATTR_SOFTWARE: i64 = 0x8022
61
62// STUN magic cookie (RFC 5389 §6)
63const NX_STUN_MAGIC: i64 = 0x2112A442
64
65// Transport protocols (RFC 5766 §14.7)
66const NX_TURN_TRANSPORT_UDP: i64 = 17
67const NX_TURN_TRANSPORT_TCP: i64 = 6
68
69// Address families (RFC 5389 §15.1)
70const NX_TURN_FAMILY_IPV4: i64 = 0x01
71const NX_TURN_FAMILY_IPV6: i64 = 0x02
72
73// Verdicts for parse_allocate
74const NX_TURN_VERDICT_OK_ALLOCATE: i64 = 1
75const NX_TURN_VERDICT_BAD_MAGIC: i64 = 2
76const NX_TURN_VERDICT_TOO_SHORT: i64 = 3
77const NX_TURN_VERDICT_UNKNOWN_TYPE: i64 = 4
78const NX_TURN_VERDICT_MISSING_TRANSPORT: i64 = 5
79const NX_TURN_VERDICT_UNSUPP_TRANSPORT: i64 = 6
80
81// ============================================================================
82// Byte helpers
83// ============================================================================
84func _turn_read_be16(buf: *u8, off: i64) -> i64 {
85 let hi: i64 = buf[off]
86 let lo: i64 = buf[off + 1]
87 return (hi << 8) | lo
88}
89
90func _turn_write_be16(buf: *u8, off: i64, v: i64) -> i64 {
91 buf[off] = (v >> 8) & 0xff
92 buf[off + 1] = v & 0xff
93 return 2
94}
95
96func _turn_write_be32(buf: *u8, off: i64, v: i64) -> i64 {
97 buf[off] = (v >> 24) & 0xff
98 buf[off + 1] = (v >> 16) & 0xff
99 buf[off + 2] = (v >> 8) & 0xff
100 buf[off + 3] = v & 0xff
101 return 4
102}
103
104// ============================================================================
105// Parse ALLOCATE request
106// ============================================================================
107//
108// STUN/TURN message format (RFC 5389 §6):
109// 0 1 2 3
110// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
111// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
112// |0 0| STUN Message Type | Message Length |
113// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
114// | Magic Cookie |
115// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
116// | |
117// | Transaction ID (96 bits) |
118// | |
119// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
120// | Attributes (TLV) |
121// ~ ~
122//
123// Each attribute is 4-byte aligned: type(2) + length(2) + value(length, padded).
124//
125// On success, writes:
126// - tx_id_out[0..12]: 12-byte transaction ID
127// - requested_transport_out[0]: transport protocol (17=UDP)
128// Returns NX_TURN_VERDICT_OK_ALLOCATE.
129
130func nx_turn_parse_allocate(buf: *u8, n: i64,
131 tx_id_out: *u8,
132 requested_transport_out: *u8) -> i64 {
133 if n < 20 { return NX_TURN_VERDICT_TOO_SHORT }
134 // First 2 bits must be 0 (STUN message)
135 if (buf[0] & 0xc0) != 0 { return NX_TURN_VERDICT_UNKNOWN_TYPE }
136 let msg_type: i64 = _turn_read_be16(buf, 0)
137 if msg_type != NX_TURN_TYPE_ALLOCATE_REQUEST { return NX_TURN_VERDICT_UNKNOWN_TYPE }
138 let msg_len: i64 = _turn_read_be16(buf, 2)
139 if n < 20 + msg_len { return NX_TURN_VERDICT_TOO_SHORT }
140 // Check magic cookie
141 if buf[4] != 0x21 { return NX_TURN_VERDICT_BAD_MAGIC }
142 if buf[5] != 0x12 { return NX_TURN_VERDICT_BAD_MAGIC }
143 if buf[6] != 0xa4 { return NX_TURN_VERDICT_BAD_MAGIC }
144 if buf[7] != 0x42 { return NX_TURN_VERDICT_BAD_MAGIC }
145 // Copy transaction ID (12 bytes at offset 8)
146 var i: i64 = 0
147 while i < 12 {
148 tx_id_out[i] = buf[8 + i]
149 i = i + 1
150 }
151 // Parse attributes
152 var pos: i64 = 20
153 let end: i64 = 20 + msg_len
154 var saw_transport: i64 = 0
155 var transport: i64 = 0
156 while pos + 4 <= end {
157 let attr_type: i64 = _turn_read_be16(buf, pos)
158 let attr_len: i64 = _turn_read_be16(buf, pos + 2)
159 let val_off: i64 = pos + 4
160 if val_off + attr_len > end { return NX_TURN_VERDICT_TOO_SHORT }
161 if attr_type == NX_TURN_ATTR_REQUESTED_TRANSPORT {
162 // 4-byte value: 1 byte protocol + 3 bytes RFFU(=0)
163 if attr_len == 4 {
164 transport = buf[val_off]
165 saw_transport = 1
166 }
167 }
168 // Advance to next attribute (4-byte aligned)
169 var padded: i64 = attr_len
170 let rem: i64 = attr_len & 3
171 if rem != 0 { padded = attr_len + (4 - rem) }
172 pos = val_off + padded
173 }
174 if saw_transport == 0 { return NX_TURN_VERDICT_MISSING_TRANSPORT }
175 if transport != NX_TURN_TRANSPORT_UDP { return NX_TURN_VERDICT_UNSUPP_TRANSPORT }
176 requested_transport_out[0] = transport
177 return NX_TURN_VERDICT_OK_ALLOCATE
178}
179
180// ============================================================================
181// Build XOR-MAPPED-ADDRESS attribute (RFC 5389 §15.2, also XOR-RELAYED-ADDRESS
182// for TURN RFC 5766 §14.5). IPv4 only for now.
183//
184// Value format (8 bytes):
185// byte 0: 0
186// byte 1: family (0x01 = IPv4)
187// byte 2-3: X-Port = port XOR (magic>>16) (big-endian)
188// byte 4-7: X-Address = addr XOR magic (big-endian)
189//
190// Returns total bytes written (4 header + 8 value = 12).
191// ============================================================================
192func _turn_write_xor_address(buf: *u8, off: i64, attr_type: i64,
193 addr_be: *u8, port: i64) -> i64 {
194 _turn_write_be16(buf, off, attr_type)
195 _turn_write_be16(buf, off + 2, 8)
196 let val: i64 = off + 4
197 buf[val] = 0
198 buf[val + 1] = NX_TURN_FAMILY_IPV4
199 // port XOR top-16 bits of magic
200 let xor_port: i64 = port ^ ((NX_STUN_MAGIC >> 16) & 0xffff)
201 _turn_write_be16(buf, val + 2, xor_port)
202 // addr XOR magic (4 bytes)
203 buf[val + 4] = addr_be[0] ^ ((NX_STUN_MAGIC >> 24) & 0xff)
204 buf[val + 5] = addr_be[1] ^ ((NX_STUN_MAGIC >> 16) & 0xff)
205 buf[val + 6] = addr_be[2] ^ ((NX_STUN_MAGIC >> 8) & 0xff)
206 buf[val + 7] = addr_be[3] ^ ( NX_STUN_MAGIC & 0xff)
207 return 12
208}
209
210// Build LIFETIME attribute (4-byte value = lifetime in seconds).
211// Returns total bytes written (4 header + 4 value = 8).
212func _turn_write_lifetime(buf: *u8, off: i64, lifetime_sec: i64) -> i64 {
213 _turn_write_be16(buf, off, NX_TURN_ATTR_LIFETIME)
214 _turn_write_be16(buf, off + 2, 4)
215 _turn_write_be32(buf, off + 4, lifetime_sec)
216 return 8
217}
218
219// ============================================================================
220// Build ALLOCATE success response (0x0103)
221//
222// Includes:
223// - XOR-RELAYED-ADDRESS (the relayed transport address the server allocated)
224// - LIFETIME (how long the allocation lasts in seconds)
225// - XOR-MAPPED-ADDRESS (the client's reflexive transport address)
226//
227// Returns total bytes written.
228// ============================================================================
229func nx_turn_build_allocate_success(out_buf: *u8, tx_id: *u8,
230 relayed_addr: *u8, relayed_port: i64,
231 mapped_addr: *u8, mapped_port: i64,
232 lifetime_sec: i64) -> i64 {
233 // Header: type + length + magic + tx_id
234 _turn_write_be16(out_buf, 0, NX_TURN_TYPE_ALLOCATE_SUCCESS)
235 // length filled in later; placeholder 0
236 _turn_write_be16(out_buf, 2, 0)
237 out_buf[4] = 0x21; out_buf[5] = 0x12; out_buf[6] = 0xa4; out_buf[7] = 0x42
238 var i: i64 = 0
239 while i < 12 { out_buf[8 + i] = tx_id[i]; i = i + 1 }
240 var pos: i64 = 20
241 // XOR-RELAYED-ADDRESS (12 bytes)
242 pos = pos + _turn_write_xor_address(out_buf, pos, NX_TURN_ATTR_XOR_RELAYED_ADDR,
243 relayed_addr, relayed_port)
244 // LIFETIME (8 bytes)
245 pos = pos + _turn_write_lifetime(out_buf, pos, lifetime_sec)
246 // XOR-MAPPED-ADDRESS (12 bytes)
247 pos = pos + _turn_write_xor_address(out_buf, pos, NX_TURN_ATTR_XOR_MAPPED_ADDR,
248 mapped_addr, mapped_port)
249 // Fill in message length (total - 20 header bytes)
250 let attr_len: i64 = pos - 20
251 _turn_write_be16(out_buf, 2, attr_len)
252 return pos
253}
254
255// ============================================================================
256// Build ALLOCATE error response (0x0113)
257//
258// Includes ERROR-CODE attribute (RFC 5389 §15.6):
259// byte 0-1: reserved (=0)
260// byte 2: class (1-6, encoded as Class * 100 = full error number)
261// byte 3: number (full_code mod 100)
262// bytes 4..: UTF-8 reason phrase
263//
264// Common TURN error codes:
265// 400 = Bad Request
266// 401 = Unauthorized (short-term creds path)
267// 441 = Wrong Credentials
268// 442 = Unsupported Transport Protocol
269//
270// Returns total bytes written.
271// ============================================================================
272func nx_turn_build_allocate_error(out_buf: *u8, tx_id: *u8,
273 error_code: i64,
274 reason: *u8, reason_len: i64) -> i64 {
275 _turn_write_be16(out_buf, 0, NX_TURN_TYPE_ALLOCATE_ERROR)
276 _turn_write_be16(out_buf, 2, 0)
277 out_buf[4] = 0x21; out_buf[5] = 0x12; out_buf[6] = 0xa4; out_buf[7] = 0x42
278 var i: i64 = 0
279 while i < 12 { out_buf[8 + i] = tx_id[i]; i = i + 1 }
280 // ERROR-CODE attribute
281 let value_len: i64 = 4 + reason_len
282 _turn_write_be16(out_buf, 20, NX_TURN_ATTR_ERROR_CODE)
283 _turn_write_be16(out_buf, 22, value_len)
284 out_buf[24] = 0
285 out_buf[25] = 0
286 out_buf[26] = error_code / 100
287 out_buf[27] = error_code - (error_code / 100) * 100
288 var j: i64 = 0
289 while j < reason_len {
290 out_buf[28 + j] = reason[j]
291 j = j + 1
292 }
293 // Pad to 4-byte alignment
294 var pos: i64 = 28 + reason_len
295 let rem: i64 = reason_len & 3
296 if rem != 0 {
297 let pad: i64 = 4 - rem
298 var p: i64 = 0
299 while p < pad { out_buf[pos + p] = 0; p = p + 1 }
300 pos = pos + pad
301 }
302 let attr_len: i64 = pos - 20
303 _turn_write_be16(out_buf, 2, attr_len)
304 return pos
305}
306
307// ============================================================================
308// Smoke export: build → parse → check round-trip
309// Returns 0 on success, non-zero bitmap on failure.
310// bit 0: build succeeded but parse rejected
311// bit 1: parse OK but tx_id mismatched
312// bit 2: parse OK but requested-transport not UDP
313// ============================================================================
314func nx_turn_allocate_roundtrip_test(scratch: *u8) -> i64 {
315 let buf: *u8 = scratch
316 let tx_in: *u8 = (scratch as i64 + 2048) as *u8
317 let tx_out: *u8 = (scratch as i64 + 2080) as *u8
318 let req_xport: *u8 = (scratch as i64 + 2112) as *u8
319 // Build a synthetic ALLOCATE request (tx_id = 0x01, 0x02, ..., 0x0c)
320 var ti: i64 = 0
321 while ti < 12 { tx_in[ti] = ti + 1; ti = ti + 1 }
322 // Header
323 _turn_write_be16(buf, 0, NX_TURN_TYPE_ALLOCATE_REQUEST)
324 _turn_write_be16(buf, 2, 8) // attributes total length
325 buf[4] = 0x21; buf[5] = 0x12; buf[6] = 0xa4; buf[7] = 0x42
326 var bi: i64 = 0
327 while bi < 12 { buf[8 + bi] = tx_in[bi]; bi = bi + 1 }
328 // REQUESTED-TRANSPORT attribute: type(2) + len=4 + value(4)
329 _turn_write_be16(buf, 20, NX_TURN_ATTR_REQUESTED_TRANSPORT)
330 _turn_write_be16(buf, 22, 4)
331 buf[24] = NX_TURN_TRANSPORT_UDP
332 buf[25] = 0; buf[26] = 0; buf[27] = 0 // RFFU
333 // Parse it back
334 let verdict: i64 = nx_turn_parse_allocate(buf, 28, tx_out, req_xport)
335 var fail: i64 = 0
336 if verdict != NX_TURN_VERDICT_OK_ALLOCATE { fail = fail | 1 }
337 var tj: i64 = 0
338 while tj < 12 {
339 if tx_in[tj] != tx_out[tj] { fail = fail | 2 }
340 tj = tj + 1
341 }
342 if req_xport[0] != NX_TURN_TRANSPORT_UDP { fail = fail | 4 }
343 return fail
344}
345
346// ============================================================================
347// Build XOR-PEER-ADDRESS attribute (RFC 5766 §14.3).
348// Identical wire format to XOR-RELAYED/MAPPED-ADDRESS; only attr_type differs.
349const NX_TURN_ATTR_XOR_PEER_ADDR: i64 = 0x0012
350const NX_TURN_ATTR_DATA: i64 = 0x0013
351
352// Build DATA attribute (RFC 5766 §14.4).
353// Value is the raw payload bytes (no encoding); padded to 4-byte alignment.
354// Returns total bytes written (4 header + payload + padding).
355func _turn_write_data_attr(buf: *u8, off: i64,
356 payload: *u8, payload_len: i64) -> i64 {
357 _turn_write_be16(buf, off, NX_TURN_ATTR_DATA)
358 _turn_write_be16(buf, off + 2, payload_len)
359 var i: i64 = 0
360 while i < payload_len {
361 buf[off + 4 + i] = payload[i]
362 i = i + 1
363 }
364 // Pad to 4-byte alignment with zero bytes
365 var written: i64 = 4 + payload_len
366 let rem: i64 = payload_len & 3
367 if rem != 0 {
368 let pad: i64 = 4 - rem
369 var p: i64 = 0
370 while p < pad {
371 buf[off + 4 + payload_len + p] = 0
372 p = p + 1
373 }
374 written = written + pad
375 }
376 return written
377}
378
379// ============================================================================
380// Build SEND indication (client → server, type 0x0016 with 0x01 in top bit
381// position making it an indication: actually 0x0016 is correct for SEND).
382//
383// RFC 5766 §10: SEND is an Indication (no response). Contains:
384// - XOR-PEER-ADDRESS: which peer the payload is for
385// - DATA: payload bytes
386//
387// Returns total bytes written.
388// ============================================================================
389func nx_turn_build_send_indication(out_buf: *u8, tx_id: *u8,
390 peer_addr: *u8, peer_port: i64,
391 payload: *u8, payload_len: i64) -> i64 {
392 _turn_write_be16(out_buf, 0, NX_TURN_TYPE_SEND_INDICATION)
393 _turn_write_be16(out_buf, 2, 0)
394 out_buf[4] = 0x21; out_buf[5] = 0x12; out_buf[6] = 0xa4; out_buf[7] = 0x42
395 var i: i64 = 0
396 while i < 12 { out_buf[8 + i] = tx_id[i]; i = i + 1 }
397 var pos: i64 = 20
398 pos = pos + _turn_write_xor_address(out_buf, pos, NX_TURN_ATTR_XOR_PEER_ADDR,
399 peer_addr, peer_port)
400 pos = pos + _turn_write_data_attr(out_buf, pos, payload, payload_len)
401 let attr_len: i64 = pos - 20
402 _turn_write_be16(out_buf, 2, attr_len)
403 return pos
404}
405
406// ============================================================================
407// Build DATA indication (server → client, type 0x0017).
408//
409// Identical structure to SEND, just different type code.
410// Used when a peer sends data to the relay; the relay wraps it in DATA
411// and forwards to the client.
412// ============================================================================
413func nx_turn_build_data_indication(out_buf: *u8, tx_id: *u8,
414 peer_addr: *u8, peer_port: i64,
415 payload: *u8, payload_len: i64) -> i64 {
416 _turn_write_be16(out_buf, 0, NX_TURN_TYPE_DATA_INDICATION)
417 _turn_write_be16(out_buf, 2, 0)
418 out_buf[4] = 0x21; out_buf[5] = 0x12; out_buf[6] = 0xa4; out_buf[7] = 0x42
419 var i: i64 = 0
420 while i < 12 { out_buf[8 + i] = tx_id[i]; i = i + 1 }
421 var pos: i64 = 20
422 pos = pos + _turn_write_xor_address(out_buf, pos, NX_TURN_ATTR_XOR_PEER_ADDR,
423 peer_addr, peer_port)
424 pos = pos + _turn_write_data_attr(out_buf, pos, payload, payload_len)
425 let attr_len: i64 = pos - 20
426 _turn_write_be16(out_buf, 2, attr_len)
427 return pos
428}
429
430// ============================================================================
431// Parse SEND or DATA indication. Extracts peer address + port + payload.
432//
433// expected_type: NX_TURN_TYPE_SEND_INDICATION or NX_TURN_TYPE_DATA_INDICATION
434// Writes:
435// peer_addr_out[0..3]: IPv4 address (de-XOR'd)
436// peer_port_out: i64 LE (8 bytes) — port (de-XOR'd, low 16 bits)
437// payload_out: copied payload bytes
438// payload_len_out: i64 LE (8 bytes) — payload length
439//
440// Returns 0 on success, non-zero error code.
441// ============================================================================
442func nx_turn_parse_indication(buf: *u8, n: i64,
443 expected_type: i64,
444 peer_addr_out: *u8,
445 peer_port_out: *u8,
446 payload_out: *u8,
447 payload_len_out: *u8) -> i64 {
448 if n < 20 { return 1 }
449 if _turn_read_be16(buf, 0) != expected_type { return 2 }
450 let msg_len: i64 = _turn_read_be16(buf, 2)
451 if n < 20 + msg_len { return 3 }
452 if buf[4] != 0x21 { return 4 }
453 if buf[5] != 0x12 { return 4 }
454 if buf[6] != 0xa4 { return 4 }
455 if buf[7] != 0x42 { return 4 }
456 var pos: i64 = 20
457 let end: i64 = 20 + msg_len
458 var saw_peer: i64 = 0
459 var saw_data: i64 = 0
460 while pos + 4 <= end {
461 let attr_type: i64 = _turn_read_be16(buf, pos)
462 let attr_len: i64 = _turn_read_be16(buf, pos + 2)
463 let val_off: i64 = pos + 4
464 if val_off + attr_len > end { return 5 }
465 if attr_type == NX_TURN_ATTR_XOR_PEER_ADDR {
466 if attr_len == 8 {
467 // family check
468 if buf[val_off + 1] == NX_TURN_FAMILY_IPV4 {
469 // de-XOR port (low 16 bits of magic)
470 let xor_port_hi: i64 = buf[val_off + 2]
471 let xor_port_lo: i64 = buf[val_off + 3]
472 let xor_port: i64 = (xor_port_hi << 8) | xor_port_lo
473 let port: i64 = xor_port ^ ((NX_STUN_MAGIC >> 16) & 0xffff)
474 peer_port_out[0] = port & 0xff
475 peer_port_out[1] = (port >> 8) & 0xff
476 peer_port_out[2] = 0
477 peer_port_out[3] = 0
478 peer_port_out[4] = 0
479 peer_port_out[5] = 0
480 peer_port_out[6] = 0
481 peer_port_out[7] = 0
482 // de-XOR address (full 32 bits of magic)
483 peer_addr_out[0] = buf[val_off + 4] ^ ((NX_STUN_MAGIC >> 24) & 0xff)
484 peer_addr_out[1] = buf[val_off + 5] ^ ((NX_STUN_MAGIC >> 16) & 0xff)
485 peer_addr_out[2] = buf[val_off + 6] ^ ((NX_STUN_MAGIC >> 8) & 0xff)
486 peer_addr_out[3] = buf[val_off + 7] ^ ( NX_STUN_MAGIC & 0xff)
487 saw_peer = 1
488 }
489 }
490 }
491 if attr_type == NX_TURN_ATTR_DATA {
492 var pi: i64 = 0
493 while pi < attr_len {
494 payload_out[pi] = buf[val_off + pi]
495 pi = pi + 1
496 }
497 payload_len_out[0] = attr_len & 0xff
498 payload_len_out[1] = (attr_len >> 8) & 0xff
499 payload_len_out[2] = (attr_len >> 16) & 0xff
500 payload_len_out[3] = (attr_len >> 24) & 0xff
501 payload_len_out[4] = 0
502 payload_len_out[5] = 0
503 payload_len_out[6] = 0
504 payload_len_out[7] = 0
505 saw_data = 1
506 }
507 var padded: i64 = attr_len
508 let rem: i64 = attr_len & 3
509 if rem != 0 { padded = attr_len + (4 - rem) }
510 pos = val_off + padded
511 }
512 if saw_peer == 0 { return 6 }
513 if saw_data == 0 { return 7 }
514 return 0
515}
516
517// Smoke: build SEND, parse it back, check peer + payload match.
518// Then build DATA from same peer/payload, parse, check.
519// Returns 0 on full success, non-zero bitmap on failure.
520func nx_turn_send_data_roundtrip_test(scratch: *u8) -> i64 {
521 let buf: *u8 = scratch
522 let tx_id: *u8 = (scratch as i64 + 2048) as *u8
523 let peer_addr_in: *u8 = (scratch as i64 + 2080) as *u8
524 let payload_in: *u8 = (scratch as i64 + 2096) as *u8
525 let peer_addr_out: *u8 = (scratch as i64 + 2160) as *u8
526 let peer_port_out: *u8 = (scratch as i64 + 2176) as *u8
527 let payload_out: *u8 = (scratch as i64 + 2192) as *u8
528 let payload_len_out: *u8 = (scratch as i64 + 2256) as *u8
529
530 // Setup: tx_id = 0xA0..0xAB, peer = 10.20.30.40:55555, payload = 32 bytes
531 var i: i64 = 0
532 while i < 12 { tx_id[i] = 0xa0 + i; i = i + 1 }
533 peer_addr_in[0] = 10; peer_addr_in[1] = 20; peer_addr_in[2] = 30; peer_addr_in[3] = 40
534 var pi: i64 = 0
535 while pi < 32 { payload_in[pi] = pi * 7 + 3; pi = pi + 1 }
536
537 var fail: i64 = 0
538
539 // SEND round-trip
540 let send_len: i64 = nx_turn_build_send_indication(buf, tx_id,
541 peer_addr_in, 55555, payload_in, 32)
542 let send_rc: i64 = nx_turn_parse_indication(buf, send_len,
543 NX_TURN_TYPE_SEND_INDICATION,
544 peer_addr_out, peer_port_out, payload_out, payload_len_out)
545 if send_rc != 0 { fail = fail | 1 }
546 if peer_addr_out[0] != 10 { fail = fail | 2 }
547 if peer_addr_out[1] != 20 { fail = fail | 2 }
548 if peer_addr_out[2] != 30 { fail = fail | 2 }
549 if peer_addr_out[3] != 40 { fail = fail | 2 }
550 let port_lo: i64 = peer_port_out[0]
551 let port_hi: i64 = peer_port_out[1]
552 let port_out: i64 = port_lo | (port_hi << 8)
553 if port_out != 55555 { fail = fail | 4 }
554 let plen: i64 = payload_len_out[0] | (payload_len_out[1] << 8)
555 if plen != 32 { fail = fail | 8 }
556 var pj: i64 = 0
557 while pj < 32 {
558 if payload_in[pj] != payload_out[pj] { fail = fail | 16 }
559 pj = pj + 1
560 }
561
562 // DATA round-trip
563 let data_len: i64 = nx_turn_build_data_indication(buf, tx_id,
564 peer_addr_in, 55555, payload_in, 32)
565 let data_rc: i64 = nx_turn_parse_indication(buf, data_len,
566 NX_TURN_TYPE_DATA_INDICATION,
567 peer_addr_out, peer_port_out, payload_out, payload_len_out)
568 if data_rc != 0 { fail = fail | 32 }
569 if peer_addr_out[0] != 10 { fail = fail | 64 }
570 var pk: i64 = 0
571 while pk < 32 {
572 if payload_in[pk] != payload_out[pk] { fail = fail | 128 }
573 pk = pk + 1
574 }
575 return fail
576}
577
578// ============================================================================
579// Arc 4 Phase A.3 -- CREATE-PERMISSION + REFRESH + CHANNEL-BIND
580// ============================================================================
581
582const NX_TURN_TYPE_REFRESH_SUCCESS: i64 = 0x0104
583const NX_TURN_TYPE_REFRESH_ERROR: i64 = 0x0114
584const NX_TURN_TYPE_CREATE_PERM_SUCCESS: i64 = 0x0108
585const NX_TURN_TYPE_CREATE_PERM_ERROR: i64 = 0x0118
586const NX_TURN_TYPE_CHANNEL_BIND_SUCCESS: i64 = 0x0109
587const NX_TURN_TYPE_CHANNEL_BIND_ERROR: i64 = 0x0119
588const NX_TURN_ATTR_CHANNEL_NUMBER: i64 = 0x000C
589
590// REFRESH (RFC 5766 §7): client extends or destroys its allocation.
591// Request type 0x0004 carries a LIFETIME attribute (0 = destroy).
592// Build a REFRESH request.
593func nx_turn_build_refresh_request(out_buf: *u8, tx_id: *u8,
594 lifetime_sec: i64) -> i64 {
595 _turn_write_be16(out_buf, 0, NX_TURN_TYPE_REFRESH_REQUEST)
596 _turn_write_be16(out_buf, 2, 0)
597 out_buf[4] = 0x21; out_buf[5] = 0x12; out_buf[6] = 0xa4; out_buf[7] = 0x42
598 var i: i64 = 0
599 while i < 12 { out_buf[8 + i] = tx_id[i]; i = i + 1 }
600 let pos: i64 = 20 + _turn_write_lifetime(out_buf, 20, lifetime_sec)
601 _turn_write_be16(out_buf, 2, pos - 20)
602 return pos
603}
604
605// Parse REFRESH request, extract requested lifetime.
606// Returns 0 on success, non-zero error code.
607func nx_turn_parse_refresh(buf: *u8, n: i64,
608 tx_id_out: *u8, lifetime_out: *u8) -> i64 {
609 if n < 20 { return 1 }
610 if _turn_read_be16(buf, 0) != NX_TURN_TYPE_REFRESH_REQUEST { return 2 }
611 let msg_len: i64 = _turn_read_be16(buf, 2)
612 if n < 20 + msg_len { return 3 }
613 if buf[4] != 0x21 { return 4 }
614 if buf[5] != 0x12 { return 4 }
615 if buf[6] != 0xa4 { return 4 }
616 if buf[7] != 0x42 { return 4 }
617 var i: i64 = 0
618 while i < 12 { tx_id_out[i] = buf[8 + i]; i = i + 1 }
619 var pos: i64 = 20
620 let end: i64 = 20 + msg_len
621 while pos + 4 <= end {
622 let attr_type: i64 = _turn_read_be16(buf, pos)
623 let attr_len: i64 = _turn_read_be16(buf, pos + 2)
624 let val_off: i64 = pos + 4
625 if val_off + attr_len > end { return 5 }
626 if attr_type == NX_TURN_ATTR_LIFETIME {
627 if attr_len == 4 {
628 let lt: i64 = (buf[val_off] << 24)
629 | (buf[val_off + 1] << 16)
630 | (buf[val_off + 2] << 8)
631 | buf[val_off + 3]
632 lifetime_out[0] = lt & 0xff
633 lifetime_out[1] = (lt >> 8) & 0xff
634 lifetime_out[2] = (lt >> 16) & 0xff
635 lifetime_out[3] = (lt >> 24) & 0xff
636 lifetime_out[4] = 0
637 lifetime_out[5] = 0
638 lifetime_out[6] = 0
639 lifetime_out[7] = 0
640 }
641 }
642 var padded: i64 = attr_len
643 let rem: i64 = attr_len & 3
644 if rem != 0 { padded = attr_len + (4 - rem) }
645 pos = val_off + padded
646 }
647 return 0
648}
649
650// Build REFRESH success response with the chosen lifetime.
651func nx_turn_build_refresh_success(out_buf: *u8, tx_id: *u8,
652 lifetime_sec: i64) -> i64 {
653 _turn_write_be16(out_buf, 0, NX_TURN_TYPE_REFRESH_SUCCESS)
654 _turn_write_be16(out_buf, 2, 0)
655 out_buf[4] = 0x21; out_buf[5] = 0x12; out_buf[6] = 0xa4; out_buf[7] = 0x42
656 var i: i64 = 0
657 while i < 12 { out_buf[8 + i] = tx_id[i]; i = i + 1 }
658 let pos: i64 = 20 + _turn_write_lifetime(out_buf, 20, lifetime_sec)
659 _turn_write_be16(out_buf, 2, pos - 20)
660 return pos
661}
662
663// CREATE-PERMISSION (RFC 5766 §9): client whitelists peer IPs that may
664// send to this allocation. Request type 0x0008 carries 1+ XOR-PEER-ADDRESS
665// attributes (each 8-byte value).
666//
667// Build a CREATE-PERMISSION request with a single peer (most common case).
668// Multi-peer batches just call this multiple times with appended attributes.
669func nx_turn_build_create_perm_request(out_buf: *u8, tx_id: *u8,
670 peer_addr: *u8, peer_port: i64) -> i64 {
671 _turn_write_be16(out_buf, 0, NX_TURN_TYPE_CREATE_PERM_REQUEST)
672 _turn_write_be16(out_buf, 2, 0)
673 out_buf[4] = 0x21; out_buf[5] = 0x12; out_buf[6] = 0xa4; out_buf[7] = 0x42
674 var i: i64 = 0
675 while i < 12 { out_buf[8 + i] = tx_id[i]; i = i + 1 }
676 let pos: i64 = 20 + _turn_write_xor_address(out_buf, 20,
677 NX_TURN_ATTR_XOR_PEER_ADDR, peer_addr, peer_port)
678 _turn_write_be16(out_buf, 2, pos - 20)
679 return pos
680}
681
682// Build CREATE-PERMISSION success response (empty body).
683func nx_turn_build_create_perm_success(out_buf: *u8, tx_id: *u8) -> i64 {
684 _turn_write_be16(out_buf, 0, NX_TURN_TYPE_CREATE_PERM_SUCCESS)
685 _turn_write_be16(out_buf, 2, 0)
686 out_buf[4] = 0x21; out_buf[5] = 0x12; out_buf[6] = 0xa4; out_buf[7] = 0x42
687 var i: i64 = 0
688 while i < 12 { out_buf[8 + i] = tx_id[i]; i = i + 1 }
689 return 20
690}
691
692// CHANNEL-BIND (RFC 5766 §11): client binds a 16-bit channel number to a
693// specific peer, so subsequent data uses 4-byte ChannelData header instead
694// of the ~50-byte SEND/DATA overhead. Huge bandwidth win for voice.
695//
696// Channel numbers must be in [0x4000, 0x7FFF] per spec.
697// Build a CHANNEL-BIND request: CHANNEL-NUMBER + XOR-PEER-ADDRESS.
698func nx_turn_build_channel_bind_request(out_buf: *u8, tx_id: *u8,
699 channel_num: i64,
700 peer_addr: *u8, peer_port: i64) -> i64 {
701 _turn_write_be16(out_buf, 0, NX_TURN_TYPE_CHANNEL_BIND_REQUEST)
702 _turn_write_be16(out_buf, 2, 0)
703 out_buf[4] = 0x21; out_buf[5] = 0x12; out_buf[6] = 0xa4; out_buf[7] = 0x42
704 var i: i64 = 0
705 while i < 12 { out_buf[8 + i] = tx_id[i]; i = i + 1 }
706 var pos: i64 = 20
707 // CHANNEL-NUMBER attribute (4-byte value: 2-byte channel + 2-byte RFFU)
708 _turn_write_be16(out_buf, pos, NX_TURN_ATTR_CHANNEL_NUMBER)
709 _turn_write_be16(out_buf, pos + 2, 4)
710 _turn_write_be16(out_buf, pos + 4, channel_num)
711 out_buf[pos + 6] = 0
712 out_buf[pos + 7] = 0
713 pos = pos + 8
714 pos = pos + _turn_write_xor_address(out_buf, pos,
715 NX_TURN_ATTR_XOR_PEER_ADDR, peer_addr, peer_port)
716 _turn_write_be16(out_buf, 2, pos - 20)
717 return pos
718}
719
720// Build CHANNEL-BIND success response (empty body).
721func nx_turn_build_channel_bind_success(out_buf: *u8, tx_id: *u8) -> i64 {
722 _turn_write_be16(out_buf, 0, NX_TURN_TYPE_CHANNEL_BIND_SUCCESS)
723 _turn_write_be16(out_buf, 2, 0)
724 out_buf[4] = 0x21; out_buf[5] = 0x12; out_buf[6] = 0xa4; out_buf[7] = 0x42
725 var i: i64 = 0
726 while i < 12 { out_buf[8 + i] = tx_id[i]; i = i + 1 }
727 return 20
728}
729
730// ChannelData (RFC 5766 §11.4): the compact data path.
731// 4-byte header:
732// [16-bit channel number, in 0x4000..0x7FFF]
733// [16-bit length of application data]
734// [application data, padded to 4-byte alignment over UDP]
735//
736// Build a ChannelData message. Returns total bytes written.
737func nx_turn_build_channel_data(out_buf: *u8,
738 channel_num: i64,
739 payload: *u8, payload_len: i64) -> i64 {
740 _turn_write_be16(out_buf, 0, channel_num)
741 _turn_write_be16(out_buf, 2, payload_len)
742 var i: i64 = 0
743 while i < payload_len {
744 out_buf[4 + i] = payload[i]
745 i = i + 1
746 }
747 var total: i64 = 4 + payload_len
748 let rem: i64 = total & 3
749 if rem != 0 {
750 let pad: i64 = 4 - rem
751 var p: i64 = 0
752 while p < pad { out_buf[total + p] = 0; p = p + 1 }
753 total = total + pad
754 }
755 return total
756}
757
758// Parse ChannelData: extract channel number + payload.
759// Returns 0 on success, non-zero on bad format.
760func nx_turn_parse_channel_data(buf: *u8, n: i64,
761 channel_num_out: *u8,
762 payload_out: *u8,
763 payload_len_out: *u8) -> i64 {
764 if n < 4 { return 1 }
765 let cnum: i64 = _turn_read_be16(buf, 0)
766 if cnum < 0x4000 { return 2 }
767 if cnum > 0x7fff { return 2 }
768 let plen: i64 = _turn_read_be16(buf, 2)
769 if n < 4 + plen { return 3 }
770 channel_num_out[0] = cnum & 0xff
771 channel_num_out[1] = (cnum >> 8) & 0xff
772 var i: i64 = 0
773 while i < plen { payload_out[i] = buf[4 + i]; i = i + 1 }
774 payload_len_out[0] = plen & 0xff
775 payload_len_out[1] = (plen >> 8) & 0xff
776 payload_len_out[2] = 0
777 payload_len_out[3] = 0
778 return 0
779}
780
781// Combined Phase A.3 smoke: REFRESH + CREATE-PERMISSION + CHANNEL-BIND
782// + ChannelData all round-trip byte-exact.
783// Returns 0 on full success, non-zero bitmap on failure.
784func nx_turn_phase_a3_smoke(scratch: *u8) -> i64 {
785 let buf: *u8 = scratch
786 let tx_id: *u8 = (scratch as i64 + 4096) as *u8
787 let peer_addr: *u8 = (scratch as i64 + 4128) as *u8
788 let payload: *u8 = (scratch as i64 + 4160) as *u8 // 64 B
789 let chan_out: *u8 = (scratch as i64 + 4256) as *u8 // 8 B (past payload+64)
790 let plen_out: *u8 = (scratch as i64 + 4272) as *u8 // 8 B
791 let pay_out: *u8 = (scratch as i64 + 4288) as *u8 // 64 B (no overlap with payload)
792 let lt_out: *u8 = (scratch as i64 + 4400) as *u8 // 8 B
793 let tx_out: *u8 = (scratch as i64 + 4416) as *u8 // 12 B
794
795 var i: i64 = 0
796 while i < 12 { tx_id[i] = 0xB0 + i; i = i + 1 }
797 peer_addr[0] = 192; peer_addr[1] = 168; peer_addr[2] = 1; peer_addr[3] = 50
798 var pi: i64 = 0
799 while pi < 64 { payload[pi] = (pi * 13 + 5) & 0xff; pi = pi + 1 }
800
801 var fail: i64 = 0
802
803 // REFRESH round-trip
804 let refresh_len: i64 = nx_turn_build_refresh_request(buf, tx_id, 1800)
805 let r_rc: i64 = nx_turn_parse_refresh(buf, refresh_len, tx_out, lt_out)
806 if r_rc != 0 { fail = fail | 1 }
807 let lt: i64 = lt_out[0] | (lt_out[1] << 8) | (lt_out[2] << 16) | (lt_out[3] << 24)
808 if lt != 1800 { fail = fail | 2 }
809 var tj: i64 = 0
810 while tj < 12 {
811 if tx_id[tj] != tx_out[tj] { fail = fail | 4 }
812 tj = tj + 1
813 }
814
815 // CREATE-PERMISSION builds (parse side reuses parse_indication helper structure;
816 // for smoke we verify the build is well-formed by checking it has the right
817 // type code, magic, length field, and peer XOR matches by-hand).
818 let cp_len: i64 = nx_turn_build_create_perm_request(buf, tx_id, peer_addr, 12345)
819 if _turn_read_be16(buf, 0) != NX_TURN_TYPE_CREATE_PERM_REQUEST { fail = fail | 8 }
820 if _turn_read_be16(buf, 2) != 12 { fail = fail | 16 } // 12-byte XOR-PEER-ADDRESS
821 if cp_len != 32 { fail = fail | 32 } // 20 header + 12 attr
822
823 // CHANNEL-BIND build + structural check
824 let cb_len: i64 = nx_turn_build_channel_bind_request(buf, tx_id, 0x4001,
825 peer_addr, 12345)
826 if _turn_read_be16(buf, 0) != NX_TURN_TYPE_CHANNEL_BIND_REQUEST { fail = fail | 64 }
827 if cb_len != 40 { fail = fail | 128 } // 20 + 8 (CHANNEL-NUMBER) + 12 (XOR-PEER)
828
829 // ChannelData round-trip: build then parse
830 let cd_len: i64 = nx_turn_build_channel_data(buf, 0x4001, payload, 64)
831 let cd_rc: i64 = nx_turn_parse_channel_data(buf, cd_len, chan_out, pay_out, plen_out)
832 if cd_rc != 0 { fail = fail | 256 }
833 let cnum: i64 = chan_out[0] | (chan_out[1] << 8)
834 if cnum != 0x4001 { fail = fail | 512 }
835 let plen: i64 = plen_out[0] | (plen_out[1] << 8)
836 if plen != 64 { fail = fail | 1024 }
837 var pk: i64 = 0
838 while pk < 64 {
839 if payload[pk] != pay_out[pk] { fail = fail | 2048 }
840 pk = pk + 1
841 }
842 return fail
843}
844
845// ============================================================================
846// MESSAGE-INTEGRITY moved to nx_turn_msgintegrity.nx (see header).
847// ============================================================================
848// (former MI section deleted -- importers needing MI should import
849// nx_turn_msgintegrity.nx which depends on this file + nx_hmac_sha1.)
850//
851// Stub kept-comment for context:
852// HMAC-SHA1 over the message, with key = SASLprep(password) for short-term
853// creds. Wire format per RFC 5389 §15.4: 20-byte MAC in a 24-byte attribute.
854// The HMAC input is the message UP TO BUT NOT INCLUDING the MI attribute,
855// with the Length field set as if MI were included (+24 bytes).
856
857// (Former MI implementation deleted; see nx_turn_msgintegrity.nx)
858
859// Smoke export: build ALLOCATE success response, verify structure.
860// Returns 0 on success, non-zero on bad structure.
861func nx_turn_build_success_test(scratch: *u8) -> i64 {
862 let buf: *u8 = scratch
863 let tx_id: *u8 = (scratch as i64 + 2048) as *u8
864 let relay_addr: *u8 = (scratch as i64 + 2080) as *u8
865 let mapped_addr: *u8 = (scratch as i64 + 2096) as *u8
866 var ti: i64 = 0
867 while ti < 12 { tx_id[ti] = ti + 1; ti = ti + 1 }
868 // Synthetic addresses: 192.168.8.227 and 73.14.222.50
869 relay_addr[0] = 192; relay_addr[1] = 168; relay_addr[2] = 8; relay_addr[3] = 227
870 mapped_addr[0] = 73; mapped_addr[1] = 14; mapped_addr[2] = 222; mapped_addr[3] = 50
871 let total: i64 = nx_turn_build_allocate_success(buf, tx_id,
872 relay_addr, 49160, mapped_addr, 51820, 600)
873 // Expected: 20 header + 12 XOR-RELAYED + 8 LIFETIME + 12 XOR-MAPPED = 52
874 if total != 52 { return 1 }
875 // Verify magic cookie
876 if buf[4] != 0x21 { return 2 }
877 if buf[5] != 0x12 { return 3 }
878 // Verify type
879 if _turn_read_be16(buf, 0) != NX_TURN_TYPE_ALLOCATE_SUCCESS { return 4 }
880 // Verify length field
881 if _turn_read_be16(buf, 2) != 32 { return 5 } // 12 + 8 + 12 = 32
882 return 0
883}