code wiki / _hdl_build / nx_smtp_probe.nx
nx_smtp_probe.nx source
↩ module page · 134 lines · 6984 B
1// nx_smtp_probe.nx -- SOVEREIGN outbound-SMTP reachability probe (zero shell, zero curl).
2// THE make-or-break fact for full self-hosted MX (EMAIL-D3 first act): can this network open
3// TCP to a real Gmail MX on port 25 and receive the "220" SMTP banner? Many ISPs silently
4// block outbound 25 while 443/587 work -- in that case no amount of MTA code will ever
5// deliver to Gmail and the operator must get the port opened (or move the mail host).
6// Battery (default): gmail-smtp-in.l.google.com:25 + aspmx.l.google.com:25 (two independent
7// Google MXes = the real test) vs smtp.gmail.com:587 + :465 (submission CONTROLS -- normally
8// never ISP-blocked; they discriminate "port-25 block" from "no outbound at all").
9// Composes the proven substrate: nx_dns_resolve_default (sovereign DNS) + BSD sockets with
10// sys_set_socket_timeout(10s) so a blackholed port FAILS GRACEFULLY, never hangs.
11// usage: nx_smtp_probe [host port] (no args = the default battery)
12// verdict: exit 0 iff at least one :25 target returned an SMTP 220 banner (OPEN);
13// exit 1 otherwise (BLOCKED-OR-UNREACHABLE). Appends knowledge/status/smtp_probe.log.
14// license_tier: ORIGINAL
15import "nx_syscalls.nx"
16import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host
17import "nx_dns_resolve_a_record.nx"
18
19func sp_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
20func sp_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
21func sp_w(fd: i64, s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(fd, s, n); return 0 }
22func sp_putn(v: i64) -> i64 {
23 if v == 0 { sys_write(1, "0" as *u8, 1); return 0 }
24 var m: i64 = v
25 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m }
26 let t: *u8 = sys_mmap(24); var k: i64 = 0
27 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
28 let o: *u8 = sys_mmap(24); var w: i64 = 0; var q: i64 = k - 1
29 while q >= 0 { o[w] = t[q]; w = w + 1; q = q - 1 }
30 sys_write(1, o, w); return 0
31}
32func sp_ip(packed: i64) -> i64 {
33 sp_putn((packed >> 24) & 0xff); sp_puts("." as *u8)
34 sp_putn((packed >> 16) & 0xff); sp_puts("." as *u8)
35 sp_putn((packed >> 8) & 0xff); sp_puts("." as *u8)
36 sp_putn(packed & 0xff)
37 return 0
38}
39func sp_atoi(s: *u8) -> i64 {
40 var v: i64 = 0; var i: i64 = 0
41 while s[i] != (0 as u8) {
42 let c: i64 = s[i] as i64
43 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } }
44 i = i + 1
45 }
46 return v
47}
48// IPv4 sockaddr_in (16B): family=2 LE, port BE, addr BE, zero pad (mirrors nx_https_build_sockaddr).
49func sp_sockaddr(out: *u8, packed: i64, port: i64) -> i64 {
50 out[0] = 2 as u8; out[1] = 0 as u8
51 out[2] = ((port >> 8) & 0xff) as u8
52 out[3] = (port & 0xff) as u8
53 out[4] = ((packed >> 24) & 0xff) as u8
54 out[5] = ((packed >> 16) & 0xff) as u8
55 out[6] = ((packed >> 8) & 0xff) as u8
56 out[7] = (packed & 0xff) as u8
57 var i: i64 = 8
58 while i < 16 { out[i] = 0 as u8; i = i + 1 }
59 return 16
60}
61
62// probe one host:port. prints one line. returns: 2 = SMTP 220 banner; 1 = connected, no/odd
63// banner; 0 = connect failed; -1 = DNS failed.
64func sp_probe(host: *u8, port: i64) -> i64 {
65 sp_puts(" " as *u8); sp_puts(host); sp_puts(":" as *u8); sp_putn(port); sp_puts(" -> " as *u8)
66 let now: i64 = sys_now_realtime_sec()
67 let r: *DnsResolveResult = nx_dns_resolve_default(host, sp_slen(host), now)
68 if r.verdict != NX_DNS_R_OK { sp_puts("DNS-FAIL\n" as *u8); return 0 - 1 }
69 if r.ipv4_packed == 0 { sp_puts("NO-A-RECORD\n" as *u8); return 0 - 1 }
70 sp_ip(r.ipv4_packed); sp_puts(" " as *u8)
71 let fd: i64 = sys_socket(2, 1, 0)
72 if fd < 0 { sp_puts("SOCKET-FAIL\n" as *u8); return 0 }
73 sys_set_socket_timeout(fd, 10)
74 let sa: *u8 = sys_mmap(16)
75 sp_sockaddr(sa, r.ipv4_packed, port)
76 let cr: i64 = nx_connect_bounded(fd, sa, 16, NX_CONN_DEFAULT_MS)
77 if cr < 0 { sys_close(fd); sp_puts("CONNECT-FAIL(" as *u8); sp_putn(cr); sp_puts(") [blocked, filtered, or unreachable]\n" as *u8); return 0 }
78 let buf: *u8 = sys_mmap(256)
79 let rn: i64 = sys_read(fd, buf, 255)
80 var got220: i64 = 0
81 if rn >= 3 {
82 if (buf[0] as i64) == 50 { if (buf[1] as i64) == 50 { if (buf[2] as i64) == 48 { got220 = 1 } } }
83 }
84 if got220 == 1 {
85 // polite QUIT, best-effort
86 sys_write(fd, "QUIT\r\n" as *u8, 6)
87 sys_close(fd)
88 sp_puts("CONNECTED banner=" as *u8)
89 var i: i64 = 0
90 while i < rn { if (buf[i] as i64) == 13 { i = rn } else { if (buf[i] as i64) == 10 { i = rn } else { sys_write(1, ((buf as i64) + i) as *u8, 1); i = i + 1 } } }
91 sp_puts(" [SMTP 220 = PORT OPEN]\n" as *u8)
92 return 2
93 }
94 sys_close(fd)
95 if rn <= 0 { sp_puts("CONNECTED but NO BANNER (read timeout/reset -- likely a transparent proxy or filter)\n" as *u8); return 1 }
96 sp_puts("CONNECTED, non-220 first bytes\n" as *u8)
97 return 1
98}
99
100func main(argc: i64, argv: *i64) -> i64 {
101 sp_puts("=== nx_smtp_probe: outbound SMTP reachability (sovereign; 10s timeouts; vantage = THIS box's uplink) ===\n" as *u8)
102 if argc >= 3 {
103 let v: i64 = sp_probe(argv[1] as *u8, sp_atoi(argv[2] as *u8))
104 if v == 2 { sys_exit(0); return 0 }
105 sys_exit(1); return 1
106 }
107 sp_puts("real test (Gmail MX, port 25):\n" as *u8)
108 let a: i64 = sp_probe("gmail-smtp-in.l.google.com" as *u8, 25)
109 let b: i64 = sp_probe("aspmx.l.google.com" as *u8, 25)
110 sp_puts("controls (submission ports -- normally never ISP-blocked):\n" as *u8)
111 let c: i64 = sp_probe("smtp.gmail.com" as *u8, 587)
112 let d: i64 = sp_probe("smtp.gmail.com" as *u8, 465)
113 var open25: i64 = 0
114 if a == 2 { open25 = 1 }
115 if b == 2 { open25 = 1 }
116 var ctl: i64 = 0
117 if c >= 1 { ctl = 1 }
118 if d >= 1 { ctl = 1 }
119 sp_puts("----\nSMTP-PROBE port25_open=" as *u8); sp_putn(open25)
120 sp_puts(" controls_reachable=" as *u8); sp_putn(ctl)
121 let lg: i64 = sys_openat_append("knowledge/status/smtp_probe.log" as *u8, 0x1a4)
122 if lg >= 0 {
123 sp_w(lg, "SMTPPROBE port25_open=" as *u8)
124 if open25 == 1 { sp_w(lg, "1" as *u8) } else { sp_w(lg, "0" as *u8) }
125 sp_w(lg, " controls_reachable=" as *u8)
126 if ctl == 1 { sp_w(lg, "1" as *u8) } else { sp_w(lg, "0" as *u8) }
127 if open25 == 1 { sp_w(lg, " verdict=OPEN\n" as *u8) } else { sp_w(lg, " verdict=BLOCKED-OR-UNREACHABLE\n" as *u8) }
128 sys_close(lg)
129 }
130 if open25 == 1 { sp_puts(" verdict=OPEN -- full self-hosted MX outbound is POSSIBLE from this uplink\n" as *u8); sys_exit(0); return 0 }
131 if ctl == 1 { sp_puts(" verdict=BLOCKED -- outbound 25 filtered while 587/465 work: ISP port-25 block; ask provider or move the MTA\n" as *u8); sys_exit(1); return 1 }
132 sp_puts(" verdict=NO-OUTBOUND -- even the controls failed: check network/DNS before concluding anything about port 25\n" as *u8)
133 sys_exit(1); return 1
134}