nx_net.nx source
↩ module page · 268 lines · 9395 B
1// net.nx -- BSD-style socket primitives (Phase G2).
2//
3// Typed wrappers around Linux/NishiOS socket syscalls. Fallible
4// public calls return `*Result<i64, NetError>` -- the Ok payload is
5// an fd (for socket / accept) or a byte count (for send / recv) or
6// 0 (for bind / listen / connect). Raw `_raw` variants keep the
7// bare syscall return for callers that want the i64 directly (e.g.
8// tight send loops).
9//
10// Syscall numbers (Linux RV64, NishiOS matches):
11// 198 socket
12// 200 bind
13// 201 listen
14// 202 accept
15// 203 connect
16// 206 sendto (we use as send)
17// 207 recvfrom (we use as recv)
18// 208 setsockopt
19// 209 getsockopt
20//
21// Address families (SOL):
22// AF_INET = 2 (IPv4)
23// AF_INET6 = 10 (IPv6)
24//
25// Socket types:
26// SOCK_STREAM = 1 (TCP)
27// SOCK_DGRAM = 2 (UDP)
28//
29// The typed variant struct SockAddrIn matches Linux's `struct
30// sockaddr_in` exactly (16 bytes): sin_family + sin_port (big-
31// endian) + sin_addr + 8 bytes of padding.
32
33// nx_safety_envelope:
34// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
35// sil_target: SIL1
36// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
37// verdict: NOT_YET_EVALUATED
38
39import "nx_syscalls.nx"
40import "nx_stdlib.nx"
41
42const SYS_SOCKET: i64 = 198
43const SYS_BIND: i64 = 200
44const SYS_LISTEN: i64 = 201
45const SYS_ACCEPT: i64 = 202
46const SYS_CONNECT: i64 = 203
47const SYS_SENDTO: i64 = 206
48const SYS_RECVFROM: i64 = 207
49const SYS_SETSOCKOPT: i64 = 208
50
51const AF_INET: i64 = 2
52const SOCK_STREAM: i64 = 1
53const SOCK_DGRAM: i64 = 2
54
55// IPv4 socket address. sockaddr_in on Linux is 16 bytes with the
56// port in big-endian byte order. We expose it as an opaque byte
57// buffer via net_fill_sockaddr_in so callers don't reach into the
58// struct directly (Liskov-style: module boundary hides layout).
59struct SockAddrIn {
60 // family (2 bytes) + port (2 bytes) + addr (4 bytes) + pad (8)
61 // packed into two i64 slots; accessors below set/get the fields.
62 w0: i64,
63 w1: i64,
64}
65
66// ---- NetError ------------------------------------------------------
67//
68// Aligned with the subset of Linux errnos that sockets actually
69// surface. Keeps the enum small so match-arms stay exhaustive.
70enum NetError {
71 AddrInUse, // -EADDRINUSE = -98
72 AddrNotAvail, // -EADDRNOTAVAIL = -99
73 ConnRefused, // -ECONNREFUSED = -111
74 ConnReset, // -ECONNRESET = -104
75 TimedOut, // -ETIMEDOUT = -110
76 NetDown, // -ENETDOWN = -100
77 PermDenied, // -EACCES = -13
78 InvalidArg, // -EINVAL = -22
79 Unknown,
80}
81
82// Translate negative-errno to NetError discriminant.
83func net_errno_to(e: i64) -> i64 {
84 let err: i64 = 0 - e
85 if err == 98 { return NetError::AddrInUse }
86 if err == 99 { return NetError::AddrNotAvail }
87 if err == 111 { return NetError::ConnRefused }
88 if err == 104 { return NetError::ConnReset }
89 if err == 110 { return NetError::TimedOut }
90 if err == 100 { return NetError::NetDown }
91 if err == 13 { return NetError::PermDenied }
92 if err == 22 { return NetError::InvalidArg }
93 return NetError::Unknown
94}
95
96// ---- socket lifecycle ----------------------------------------------
97
98// Raw variants return the bare syscall rc (fd or -errno). Kept
99// public so tight I/O loops can avoid allocating a Result per call.
100func net_tcp_raw() -> i64 {
101 return __syscall(SYS_SOCKET, AF_INET, SOCK_STREAM, 0, 0, 0, 0)
102}
103
104func net_udp_raw() -> i64 {
105 return __syscall(SYS_SOCKET, AF_INET, SOCK_DGRAM, 0, 0, 0, 0)
106}
107
108// Typed socket constructors. Result payload is the fd.
109func net_tcp() -> *Result<i64, NetError> {
110 let rc: i64 = net_tcp_raw()
111 if rc < 0 { return Result::Err(net_errno_to(rc)) }
112 return Result::Ok(rc)
113}
114
115func net_udp() -> *Result<i64, NetError> {
116 let rc: i64 = net_udp_raw()
117 if rc < 0 { return Result::Err(net_errno_to(rc)) }
118 return Result::Ok(rc)
119}
120
121// Close a socket fd. Never fails observably in practice; returns
122// Result for symmetry with fs_close.
123func net_close(fd: i64) -> *Result<i64, NetError> {
124 let rc: i64 = sys_close(fd)
125 if rc < 0 { return Result::Err(net_errno_to(rc)) }
126 return Result::Ok(0)
127}
128
129// ---- address helpers -----------------------------------------------
130
131// Fill a sockaddr_in buffer with AF_INET + port + INADDR_ANY (0.0.0.0).
132// Port is passed in host byte order; we byte-swap to network order.
133func net_addr_any(sa: *SockAddrIn, port: i64) -> i64 {
134 // sin_family (2 bytes) = AF_INET = 2 LE, so low byte = 2, next = 0.
135 // sin_port (2 bytes, BE) — swap.
136 let port_be: i64 = ((port & 0xFF) << 8) | ((port >> 8) & 0xFF)
137 // Lay out as LE bytes: [family_lo, family_hi, port_hi, port_lo, ...]
138 // byte0 = 0x02
139 // byte1 = 0x00
140 // byte2 = port_be_hi (= port_lo in host order)
141 // byte3 = port_be_lo (= port_hi in host order)
142 // byte4..7 = 0 (INADDR_ANY)
143 sa.w0 = 0x02 | (port_be << 16)
144 sa.w1 = 0
145 return 0
146}
147
148// Fill sockaddr_in with an explicit IPv4 address. `ip` is four
149// bytes packed LSB first (matches inet_aton output on LE hosts).
150func net_addr(sa: *SockAddrIn, port: i64, ip: i64) -> i64 {
151 let port_be: i64 = ((port & 0xFF) << 8) | ((port >> 8) & 0xFF)
152 // bytes 0-1 family, 2-3 port_be, 4-7 ip
153 sa.w0 = 0x02 | (port_be << 16) | (ip << 32)
154 sa.w1 = 0
155 return 0
156}
157
158// ---- server side ---------------------------------------------------
159
160// Bind an already-opened socket to an address. `sa` points at a
161// SockAddrIn filled via net_addr_any / net_addr. addrlen = 16 for
162// IPv4. Ok payload is 0.
163func net_bind_raw(fd: i64, sa: *SockAddrIn) -> i64 {
164 return __syscall(SYS_BIND, fd, sa as i64, 16, 0, 0, 0)
165}
166
167func net_bind(fd: i64, sa: *SockAddrIn) -> *Result<i64, NetError> {
168 let rc: i64 = net_bind_raw(fd, sa)
169 if rc < 0 { return Result::Err(net_errno_to(rc)) }
170 return Result::Ok(0)
171}
172
173// Mark the socket as passive, ready to accept. backlog capped at
174// the kernel's somaxconn (typically 128-4096).
175func net_listen_raw(fd: i64, backlog: i64) -> i64 {
176 return __syscall(SYS_LISTEN, fd, backlog, 0, 0, 0, 0)
177}
178
179func net_listen(fd: i64, backlog: i64) -> *Result<i64, NetError> {
180 let rc: i64 = net_listen_raw(fd, backlog)
181 if rc < 0 { return Result::Err(net_errno_to(rc)) }
182 return Result::Ok(0)
183}
184
185// Accept a pending connection. Peer address written to `peer` (may
186// be null if caller doesn't care). Ok payload is the new fd.
187// addrlen_slot: caller allocates an i64 initialised to 16; kernel
188// writes back the actual size.
189func net_accept_raw(fd: i64, peer: *SockAddrIn,
190 addrlen_slot: *i64) -> i64 {
191 return __syscall(SYS_ACCEPT, fd, peer as i64,
192 addrlen_slot as i64, 0, 0, 0)
193}
194
195func net_accept(fd: i64, peer: *SockAddrIn,
196 addrlen_slot: *i64) -> *Result<i64, NetError> {
197 let rc: i64 = net_accept_raw(fd, peer, addrlen_slot)
198 if rc < 0 { return Result::Err(net_errno_to(rc)) }
199 return Result::Ok(rc)
200}
201
202// ---- client side ---------------------------------------------------
203
204// Initiate a connection to `sa`. Ok payload is 0.
205func net_connect_raw(fd: i64, sa: *SockAddrIn) -> i64 {
206 return __syscall(SYS_CONNECT, fd, sa as i64, 16, 0, 0, 0)
207}
208
209func net_connect(fd: i64, sa: *SockAddrIn) -> *Result<i64, NetError> {
210 let rc: i64 = net_connect_raw(fd, sa)
211 if rc < 0 { return Result::Err(net_errno_to(rc)) }
212 return Result::Ok(0)
213}
214
215// ---- I/O -----------------------------------------------------------
216
217// ---- hot-path I/O ------------------------------------------------
218//
219// send / recv run inside receive loops where a per-call allocation
220// is a real cost. They stay i64-returning; callers inside tight
221// loops check `< 0`, callers outside loops can wrap at the boundary.
222
223// Send bytes on a connected socket. Returns bytes sent or -errno.
224// Does NOT loop on partial send -- caller does.
225func net_send(fd: i64, buf: *u8, n: i64) -> i64 {
226 return __syscall(SYS_SENDTO, fd, buf, n, 0, 0, 0)
227}
228
229// Receive bytes. Returns bytes read (0 = peer closed cleanly) or
230// -errno.
231func net_recv(fd: i64, buf: *u8, n: i64) -> i64 {
232 return __syscall(SYS_RECVFROM, fd, buf, n, 0, 0, 0)
233}
234
235// Loop-until-complete send. Ok payload is total bytes sent.
236func net_send_all_raw(fd: i64, buf: *u8, n: i64) -> i64 {
237 var sent: i64 = 0
238 while sent < n {
239 let tail_addr: i64 = (buf as i64) + sent
240 let tail: *u8 = tail_addr as *u8
241 let got: i64 = net_send(fd, tail, n - sent)
242 if got <= 0 { return got }
243 sent = sent + got
244 }
245 return 0
246}
247
248func net_send_all(fd: i64, buf: *u8, n: i64) -> *Result<i64, NetError> {
249 let rc: i64 = net_send_all_raw(fd, buf, n)
250 if rc < 0 { return Result::Err(net_errno_to(rc)) }
251 return Result::Ok(0)
252}
253
254// ---- socket options ------------------------------------------------
255
256const SOL_SOCKET: i64 = 1
257const SO_REUSEADDR: i64 = 2
258const SO_REUSEPORT: i64 = 15
259
260// Enable SO_REUSEADDR so restarts don't hit "Address already in use"
261// on the bind call. Common pattern for servers.
262func net_set_reuseaddr(fd: i64) -> i64 {
263 let val_raw: *u8 = sys_mmap(16)
264 let val: *i64 = val_raw as *i64
265 *val = 1
266 return __syscall(SYS_SETSOCKOPT, fd, SOL_SOCKET, SO_REUSEADDR,
267 val as i64, 4, 0)
268}