nx_stun_client.nx source
↩ module page · 133 lines · 6074 B
1// nx_stun_client.nx -- SOVEREIGN STUN client (server-reflexive address discovery), NishiLang, composing
2// nx_stun. The gather side of ICE: send a Binding Request to a STUN server, read the Binding Response, and
3// extract the XOR-MAPPED-ADDRESS -- your PUBLIC reflexive IP:port as the server saw it. Works against ANY
4// standard STUN server (ours OR a third-party one), proving byte-level interop, with ZERO third-party code.
5//
6// sc_query(...) is the reusable primitive (real UDP socket round-trip, bounded by a timeout so it never
7// hangs). main() is a CLI that prints the reflexive address as JSON -- registered as the MCP tool
8// `nx_stun_probe` so an agent/operator can discover a public address over the wire.
9// license_tier: INDEPENDENT_REDERIVE genealogy_id: international-research-sources/ietf/rfc_5389
10import "nx_syscalls.nx"
11import "nx_stun.nx"
12const K_MAGIC_2048: i64 = 2048
13
14// walk the response attributes looking for XOR-MAPPED-ADDRESS (0x0020). Returns the byte offset of its
15// VALUE (family/port/addr), or -1 if absent.
16func sc_find_xma(resp: *u8, n: i64) -> i64 {
17 let mlen: i64 = st_len(resp)
18 var end: i64 = 20 + mlen
19 if end > n { end = n }
20 var off: i64 = 20
21 while off + 4 <= end {
22 let atype: i64 = st_get16(resp, off)
23 let alen: i64 = st_get16(resp, off+2)
24 if atype == STUN_ATTR_XOR_MAPPED_ADDRESS { return off + 4 }
25 off = off + 4 + ((alen+3)/4)*4
26 }
27 return 0 - 1
28}
29
30// build a 16-byte sockaddr_in for i0.i1.i2.i3:port (network byte order).
31func sc_sockaddr(sa: *u8, i0: i64, i1: i64, i2: i64, i3: i64, port: i64) -> i64 {
32 sa[0]=2 as u8; sa[1]=0 as u8
33 sa[2]=((port>>8)&0xff) as u8; sa[3]=(port&0xff) as u8
34 sa[4]=i0 as u8; sa[5]=i1 as u8; sa[6]=i2 as u8; sa[7]=i3 as u8
35 var k: i64=8; while k<16 { sa[k]=0 as u8; k=k+1 }
36 return 0
37}
38
39// QUERY a STUN server. On success writes the reflexive addr (u32) + port into out_ip/out_port and returns 0.
40// Negative return: -1 timeout/no-response, -2 not a Binding Response, -3 no XOR-MAPPED-ADDRESS, -9 socket.
41func sc_query(i0: i64, i1: i64, i2: i64, i3: i64, port: i64, out_ip: *i64, out_port: *i64) -> i64 {
42 let dest: *u8 = sys_mmap(16)
43 sc_sockaddr(dest, i0, i1, i2, i3, port)
44 let fd: i64 = sys_socket(AF_INET, SOCK_DGRAM, 0)
45 if fd < 0 { return 0 - 9 }
46 sys_set_socket_timeout(fd, 3)
47 // Binding Request with a time-seeded transaction id (fresh socket, so matching is trivial).
48 let txid: *u8 = sys_mmap(16)
49 var seed: i64 = sys_now_realtime_ms()
50 var ti: i64 = 0
51 while ti < 12 { txid[ti] = (seed & 0xff) as u8; seed = seed / 7 + 131; ti = ti + 1 }
52 let req: *u8 = sys_mmap(64)
53 st_write_header(req, STUN_BINDING_REQUEST, 0, txid)
54 sys_sendto(fd, req, 20, 0, dest, 16)
55 let resp: *u8 = sys_mmap(K_MAGIC_2048)
56 let n: i64 = sys_recvfrom(fd, resp, K_MAGIC_2048, 0, 0 as *u8, 0 as *i64)
57 sys_close(fd)
58 if n < 20 { return 0 - 1 }
59 if st_type(resp) != STUN_BINDING_RESPONSE { return 0 - 2 }
60 let xoff: i64 = sc_find_xma(resp, n)
61 if xoff < 0 { return 0 - 3 }
62 let xma: *u8 = (resp as i64 + xoff) as *u8
63 out_ip[0] = st_xma_addr(xma)
64 out_port[0] = st_xma_port(xma)
65 return 0
66}
67
68// ------------------------------------------------------------------ CLI (MCP tool nx_stun_probe)
69func cp(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
70func cpn(v: i64) -> i64 {
71 let b: *u8=sys_mmap(28); var m: i64=v; if m<0 {sys_write(1,"-" as *u8,1); m=0-m}
72 let t: *u8=sys_mmap(28); var k: i64=0; if m==0 {t[0]=48 as u8;k=1} while m>0 {t[k]=(48+(m%10)) as u8;m=m/10;k=k+1}
73 var i: i64=0; while i<k {b[i]=t[k-1-i];i=i+1} sys_write(1,b,k); return 0
74}
75// parse a dotted-quad "a.b.c.d" from a z-string into oct[0..4]. Returns 1 ok, 0 malformed.
76func cp_parse_ip(s: *u8, oct: *i64) -> i64 {
77 var idx: i64=0
78 var cur: i64=0
79 var have: i64=0
80 var i: i64=0
81 var go: i64=1
82 while go==1 {
83 let c: i64 = s[i] as i64 & 0xff
84 if c==0 {
85 if idx>3 { return 0 }
86 oct[idx]=cur; idx=idx+1; go=0
87 } else {
88 if c==46 { // '.'
89 if idx>3 { return 0 }
90 if have==0 { return 0 }
91 oct[idx]=cur; idx=idx+1; cur=0; have=0
92 } else {
93 if c<48 { return 0 }
94 if c>57 { return 0 }
95 cur=cur*10+(c-48); have=1
96 if cur>255 { return 0 }
97 }
98 i=i+1
99 }
100 }
101 if idx!=4 { return 0 }
102 return 1
103}
104func cp_atoi(s: *u8) -> i64 { var v: i64=0; var i: i64=0; while s[i]!=(0 as u8){ let c: i64=s[i] as i64 & 0xff; if c>=48 { if c<=57 { v=v*10+(c-48) } } i=i+1 } return v }
105
106func main(argc: i64, argv: *i64) -> i64 {
107 if argc < 3 {
108 cp("{\"v\":1,\"ok\":0,\"error\":\"usage: nx_stun_probe <server-ipv4> <port>\"}\n" as *u8)
109 sys_exit(2); return 2
110 }
111 let host: *u8 = argv[1] as *u8
112 let ports: *u8 = argv[2] as *u8
113 let oct: *i64 = sys_mmap(64) as *i64
114 if cp_parse_ip(host, oct)==0 {
115 cp("{\"v\":1,\"ok\":0,\"error\":\"server must be a dotted-quad IPv4 (hostname resolution is a follow-on)\"}\n" as *u8)
116 sys_exit(2); return 2
117 }
118 let port: i64 = cp_atoi(ports)
119 let oip: *i64 = sys_mmap(16) as *i64
120 let oport: *i64 = sys_mmap(16) as *i64
121 let rc: i64 = sc_query(oct[0], oct[1], oct[2], oct[3], port, oip, oport)
122 if rc != 0 {
123 cp("{\"v\":1,\"ok\":0,\"rc\":" as *u8); cpn(rc); cp(",\"error\":\"no XOR-MAPPED-ADDRESS (timeout / not a STUN server / unreachable)\"}\n" as *u8)
124 sys_exit(1); return 1
125 }
126 let a: i64 = oip[0]
127 cp("{\"v\":1,\"ok\":1,\"server\":\"" as *u8); cpn(oct[0]); cp("." as *u8); cpn(oct[1]); cp("." as *u8); cpn(oct[2]); cp("." as *u8); cpn(oct[3]); cp(":" as *u8); cpn(port)
128 cp("\",\"reflexive\":\"" as *u8)
129 cpn((a>>24)&0xff); cp("." as *u8); cpn((a>>16)&0xff); cp("." as *u8); cpn((a>>8)&0xff); cp("." as *u8); cpn(a&0xff); cp(":" as *u8); cpn(oport[0])
130 cp("\"}\n" as *u8)
131 sys_exit(0)
132 return 0
133}