code wiki / _hdl_build / nx_socks5_client.nx
nx_socks5_client.nx source
↩ module page · 192 lines · 9930 B
1// nx_socks5_client.nx -- SOVEREIGN SOCKS5 CLIENT (RFC 1928 CONNECT, no-auth method).
2// The FIRST pluggable-router backend for the privacy no-leak guard's nx_private_fetch
3// (knowledge/store/media-inventory/privacy_noleak_guard.DESIGN.txt): the guard opens an external
4// enrichment connection THROUGH a SOCKS5 proxy instead of a direct socket, so the destination is
5// reached via the proxy. When that proxy is a Tor client's SOCKS port (roadmap), the SAME code path
6// carries real anonymity -- the target sees the Tor exit, not us.
7//
8// HONEST SCOPE: a SOCKS5 proxy BY ITSELF is NOT anonymity -- the proxy sees origin IP + timing + dest.
9// This file is only the transport MECHANISM; nx_anon_transport.nx marks which backends actually hide
10// the origin. Do NOT mistake a working SOCKS5 tunnel for a working anonymizer.
11//
12// Sovereign: nx_syscalls only (sys_socket/connect/setsockopt/read/write/close). No libc, no shell.
13// license_tier: ORIGINAL expect_exit: 0
14import "nx_syscalls.nx"
15
16import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host
17// ---- RFC 1928 wire constants (rule 11: the protocol numbers are named, not buried) ----
18const S5_VER: i64 = 5 // SOCKS protocol version
19const S5_METHOD_NOAUTH: i64 = 0 // method 0x00 = NO AUTHENTICATION REQUIRED
20const S5_METHOD_REJECT: i64 = 0xff // method 0xFF = NO ACCEPTABLE METHODS (server rejects)
21const S5_CMD_CONNECT: i64 = 1 // CMD 0x01 = CONNECT (establish a TCP relay)
22const S5_RSV: i64 = 0 // reserved octet, must be 0x00
23const S5_ATYP_IPV4: i64 = 1 // address type 0x01 = IPv4 (4 bytes)
24const S5_ATYP_DOMAIN: i64 = 3 // address type 0x03 = domain name (1 len byte + name)
25const S5_ATYP_IPV6: i64 = 4 // address type 0x04 = IPv6 (16 bytes)
26const S5_REP_SUCCESS: i64 = 0 // reply REP 0x00 = succeeded
27
28// ---- structured error codes (negative; every boundary is fail-closed, rule 14/20) ----
29const S5_E_SHORT: i64 = 0 - 1 // reply shorter than the protocol minimum for its shape
30const S5_E_VERSION: i64 = 0 - 2 // server did not answer version 5
31const S5_E_NOAUTH: i64 = 0 - 3 // server did not select our no-auth method
32const S5_E_REPLY: i64 = 0 - 4 // CONNECT reply REP != 0 (proxy refused / failed)
33const S5_E_ATYP: i64 = 0 - 5 // reply bound-address type unrecognized
34const S5_E_HOSTLEN: i64 = 0 - 6 // domain name length outside [1,255]
35const S5_E_SOCKET: i64 = 0 - 7 // socket() failed
36const S5_E_CONNECT: i64 = 0 - 8 // connect() to the proxy failed
37const S5_E_IO: i64 = 0 - 9 // short / failed read or write on the wire
38
39// ================= PURE CODEC (no syscalls -- KAT-testable byte-for-byte) =================
40
41// Method-negotiation greeting: offer exactly ONE method (no-auth). out must hold >= 3 bytes.
42// wire = VER(0x05) NMETHODS(0x01) METHODS[0x00]. Returns 3.
43func s5_build_greeting(out: *u8) -> i64 {
44 out[0] = S5_VER as u8
45 out[1] = 1 as u8 // NMETHODS = 1
46 out[2] = S5_METHOD_NOAUTH as u8
47 return 3
48}
49
50// Parse the server METHOD selection: 2 bytes [VER][METHOD]. Returns the selected method (0) on
51// success, or a negative S5_E_* on any deviation. We only offered no-auth, so anything else is unusable.
52func s5_parse_method_reply(buf: *u8, n: i64) -> i64 {
53 if n < 2 { return S5_E_SHORT }
54 if (buf[0] as i64) != S5_VER { return S5_E_VERSION }
55 let m: i64 = buf[1] as i64
56 if m == S5_METHOD_REJECT { return S5_E_NOAUTH }
57 if m != S5_METHOD_NOAUTH { return S5_E_NOAUTH }
58 return S5_METHOD_NOAUTH
59}
60
61// Build a CONNECT request to a DOMAIN target (ATYP=3): the PROXY resolves the name, so the client
62// leaks no resolved IP -- and via a Tor SOCKS port the name is resolved anonymously (no local DNS leak).
63// wire = VER CMD RSV ATYP LEN host[LEN] PORT_hi PORT_lo. out >= 7+hlen bytes. Returns total length.
64func s5_build_connect_domain(host: *u8, hlen: i64, port: i64, out: *u8) -> i64 {
65 if hlen < 1 { return S5_E_HOSTLEN }
66 if hlen > 255 { return S5_E_HOSTLEN }
67 out[0] = S5_VER as u8
68 out[1] = S5_CMD_CONNECT as u8
69 out[2] = S5_RSV as u8
70 out[3] = S5_ATYP_DOMAIN as u8
71 out[4] = hlen as u8
72 var i: i64 = 0
73 while i < hlen { out[5 + i] = host[i]; i = i + 1 }
74 out[5 + hlen] = ((port >> 8) & 0xff) as u8 // port big-endian (network order)
75 out[6 + hlen] = (port & 0xff) as u8
76 return 7 + hlen
77}
78
79// Build a CONNECT request to an IPv4 target (ATYP=1). ip = 4 bytes. out >= 10 bytes. Returns 10.
80func s5_build_connect_ipv4(ip: *u8, port: i64, out: *u8) -> i64 {
81 out[0] = S5_VER as u8
82 out[1] = S5_CMD_CONNECT as u8
83 out[2] = S5_RSV as u8
84 out[3] = S5_ATYP_IPV4 as u8
85 out[4] = ip[0]; out[5] = ip[1]; out[6] = ip[2]; out[7] = ip[3]
86 out[8] = ((port >> 8) & 0xff) as u8
87 out[9] = (port & 0xff) as u8
88 return 10
89}
90
91// Expected TOTAL length of a CONNECT reply given its ATYP (header VER REP RSV ATYP = 4 bytes, then
92// bound addr, then 2-byte bound port). For a DOMAIN reply the addr length prefix is buf[4].
93// Returns the expected length, S5_E_SHORT if the header/len-prefix isn't present, or S5_E_ATYP.
94func s5_reply_len(buf: *u8, n: i64) -> i64 {
95 if n < 4 { return S5_E_SHORT }
96 let atyp: i64 = buf[3] as i64
97 if atyp == S5_ATYP_IPV4 { return 10 } // 4 + 4 + 2
98 if atyp == S5_ATYP_IPV6 { return 22 } // 4 + 16 + 2
99 if atyp == S5_ATYP_DOMAIN {
100 if n < 5 { return S5_E_SHORT }
101 return 7 + (buf[4] as i64) // 4 + 1 + LEN + 2
102 }
103 return S5_E_ATYP
104}
105
106// Parse a CONNECT reply from a COMPLETE buffer. Returns 0 (success) iff VER==5, REP==0, and the
107// buffer holds a full reply for its ATYP. Any deviation -> negative S5_E_* (fail-closed).
108func s5_parse_connect_reply(buf: *u8, n: i64) -> i64 {
109 if n < 4 { return S5_E_SHORT }
110 if (buf[0] as i64) != S5_VER { return S5_E_VERSION }
111 if (buf[1] as i64) != S5_REP_SUCCESS { return S5_E_REPLY }
112 let want: i64 = s5_reply_len(buf, n)
113 if want < 0 { return want }
114 if n < want { return S5_E_SHORT }
115 return S5_REP_SUCCESS
116}
117
118// ================= LIVE TRANSPORT (syscalls) =================
119
120// Read EXACTLY `want` bytes into buf (loops over short reads). Returns want on success, S5_E_IO if the
121// peer closes or the recv-timeout fires first. Bounds every SOCKS reply read so a stall fails closed.
122func s5_read_full(fd: i64, buf: *u8, want: i64) -> i64 {
123 var got: i64 = 0
124 while got < want {
125 let r: i64 = sys_read(fd, (buf as i64 + got) as *u8, want - got)
126 if r <= 0 { return S5_E_IO }
127 got = got + r
128 }
129 return got
130}
131
132// Set SO_RCVTIMEO (SOL_SOCKET=1, SO_RCVTIMEO=20) so a silent/hung proxy fails closed, never blocks
133// forever (rule 14). tv = struct timeval { i64 sec; i64 usec } = 16 bytes; sec < 256 here.
134func s5_set_rcv_timeout(fd: i64, secs: i64) -> i64 {
135 let tv: *u8 = sys_mmap(16)
136 var i: i64 = 0
137 while i < 16 { tv[i] = 0 as u8; i = i + 1 }
138 tv[0] = (secs & 0xff) as u8
139 return sys_setsockopt(fd, 1, 20, tv, 16)
140}
141
142// Open a TCP connection to the SOCKS5 proxy at proxy_ip(4 bytes)/proxy_port and run the RFC 1928
143// no-auth CONNECT handshake to the DOMAIN target (host,hlen,dport). On success returns the connected
144// fd -- a live byte tunnel to the target (caller does plain read/write). On ANY failure returns a
145// negative S5_E_* and leaves NO fd open (fail-closed, rule 20).
146func s5_connect(proxy_ip: *u8, proxy_port: i64, host: *u8, hlen: i64, dport: i64) -> i64 {
147 let fd: i64 = sys_socket(2, 1, 0) // AF_INET, SOCK_STREAM
148 if fd < 0 { return S5_E_SOCKET }
149 s5_set_rcv_timeout(fd, 8)
150
151 // sockaddr_in (16 bytes) for the proxy: family, port BE, IPv4, pad.
152 let sa: *u8 = sys_mmap(16)
153 sa[0] = 2 as u8; sa[1] = 0 as u8
154 sa[2] = ((proxy_port >> 8) & 0xff) as u8; sa[3] = (proxy_port & 0xff) as u8
155 sa[4] = proxy_ip[0]; sa[5] = proxy_ip[1]; sa[6] = proxy_ip[2]; sa[7] = proxy_ip[3]
156 var z: i64 = 8; while z < 16 { sa[z] = 0 as u8; z = z + 1 }
157 if nx_connect_bounded(fd, sa, 16, NX_CONN_DEFAULT_MS) != 0 { sys_close(fd); return S5_E_CONNECT }
158
159 // 1) method negotiation: offer no-auth, require the server to select it.
160 let greet: *u8 = sys_mmap(8)
161 let gn: i64 = s5_build_greeting(greet)
162 if sys_write(fd, greet, gn) != gn { sys_close(fd); return S5_E_IO }
163 let mrep: *u8 = sys_mmap(8)
164 if s5_read_full(fd, mrep, 2) < 0 { sys_close(fd); return S5_E_IO }
165 let ms: i64 = s5_parse_method_reply(mrep, 2)
166 if ms < 0 { sys_close(fd); return ms }
167
168 // 2) CONNECT request (domain form -- the proxy resolves the name).
169 let req: *u8 = sys_mmap(512)
170 let rn: i64 = s5_build_connect_domain(host, hlen, dport, req)
171 if rn < 0 { sys_close(fd); return rn }
172 if sys_write(fd, req, rn) != rn { sys_close(fd); return S5_E_IO }
173
174 // 3) CONNECT reply: read the 4-byte header, then drain the bound-address+port by ATYP so those
175 // bytes never pollute the app stream. VER + REP validated from the header.
176 let rep: *u8 = sys_mmap(512)
177 if s5_read_full(fd, rep, 4) < 0 { sys_close(fd); return S5_E_IO }
178 if (rep[0] as i64) != S5_VER { sys_close(fd); return S5_E_VERSION }
179 if (rep[1] as i64) != S5_REP_SUCCESS { sys_close(fd); return S5_E_REPLY }
180 let atyp: i64 = rep[3] as i64
181 var drain: i64 = 0 - 1
182 if atyp == S5_ATYP_IPV4 { drain = 6 } // 4 addr + 2 port
183 if atyp == S5_ATYP_IPV6 { drain = 18 } // 16 addr + 2 port
184 if atyp == S5_ATYP_DOMAIN {
185 if s5_read_full(fd, (rep as i64 + 4) as *u8, 1) < 0 { sys_close(fd); return S5_E_IO }
186 drain = (rep[4] as i64) + 2 // LEN bytes of name + 2 port
187 }
188 if drain < 0 { sys_close(fd); return S5_E_ATYP }
189 if s5_read_full(fd, (rep as i64 + 64) as *u8, drain) < 0 { sys_close(fd); return S5_E_IO }
190
191 return fd // live tunnel to (host:dport) via the proxy
192}