SchOS/kernel/net/lwip/lwipopts.h
Nexoniarz c362e308d0 net: the socket layer stops saying yes to things it did not do, and ping works
setsockopt was `return 0;` and getsockopt wrote a zero for whatever was asked.
The comment said it was so a server setting SO_REUSEADDR and treating failure as
fatal would not fail. That trade is the wrong way round: a caller told "no"
adapts, a caller told "yes" changes its behaviour on the strength of it -- sets
SO_RCVTIMEO and then blocks forever in a read it believes will time out, sets
TCP_NODELAY and reports latency numbers that measure Nagle. The failure is
silent, remote from the call, and indistinguishable from a bug in the stack.

Six options are honoured now and everything else is ENOPROTOOPT. SO_REUSEADDR,
SO_KEEPALIVE, SO_BROADCAST and TCP_NODELAY reach lwIP's pcb; SO_SNDBUF/SO_RCVBUF
are accepted and report the real size on read. SO_REUSEADDR is implemented rather
than refused precisely because of the original comment's concern -- it is the one
option a server really does set and really does check.

accept4 becomes its own table entry rather than an alias for accept. It has to
be: accept takes three arguments, so a shared handler reading a fourth register
reads whatever the caller last left there and invents a SOCK_NONBLOCK out of
stack litter. SOCK_CLOEXEC and SOCK_NONBLOCK are now honoured by socket(),
socketpair() and accept4() -- a dropped CLOEXEC leaks a listening socket across
every exec, and a dropped NONBLOCK has the caller block forever in what it
believes is a poll loop.

ICMP ping sockets: socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP), on lwIP's raw API,
with the callback stripping the variable-length IP header. Only echo replies are
eaten, and that restriction is load-bearing -- raw_input runs before lwIP's own
icmp_input, so consuming every ICMP message would swallow the echo *requests* and
this machine would silently stop answering pings from outside.

Loopback, so 127.0.0.1 is a real destination. LWIP_NETIF_LOOPBACK is the half
that is easy to miss: without it a packet addressed to an interface's own address
goes out of the wire and never comes back. With NO_SYS=1 nothing drains the
loopback queue, so the network thread calls netif_poll_all().

cmd/ping is first-party rather than toybox's, and the reason is a limitation not
a preference: toybox reads replies with recvmsg(2) and wants the TTL through an
IP_RECVTTL control message, and SchOS has neither. recvmsg is item 1 of
docs/plan/foundations.md, where it is wanted for SCM_RIGHTS anyway, and should
not be rushed to make one applet run.

Two bugs found on the way. Every AF_INET socket failed with ENFILE until DHCP
bound, because inet_new() demanded an *address* before handing out a socket and
reported it as "file table overflow"; net_is_ready() (lwIP up) is now distinct
from net_is_up() (DHCP bound). And that bug was hiding a bad test: socktest
asserted "AF_INET is refused, not faked", true when written, false since AF_INET
landed, and green the whole time because the suite runs before DHCP binds and the
ENFILE made the refusal look real. Fixing the kernel is what turned it red.

Verified: ping to 127.0.0.1, localhost, the gateway, 1.1.1.1 and 9.9.9.9, all
0% loss; socktest ALL CHECKS PASSED with ten new checks, including a setsockopt
round trip and an unknown option that must be refused -- a permissive stub cannot
pass both at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-12 17:14:35 +02:00

106 lines
4.6 KiB
C

// SPDX-License-Identifier: Apache-2.0
//
// lwIP configuration for SchOS.
//
// The shape of this file is one decision: NO_SYS=1.
//
// lwIP can run either as a set of threads of its own (NO_SYS=0), which needs a
// port layer of semaphores, mailboxes and thread creation and then hands you a
// blocking sockets API for free; or bare, with no threading assumptions
// (NO_SYS=1), where every call must come from one context and the blocking is
// yours to write. This kernel takes the second, for two reasons: the sys_arch
// a threaded lwIP wants is more code than the socket layer it would save, and
// the kernel already has a socket layer with its own shape (kernel/net/unix.c)
// that AF_INET has to fit into rather than replace.
//
// "One context" is enforced by a single lock rather than by literally one
// thread: kernel/net/net.c takes it around the network thread's work, and the
// socket syscalls take it around theirs. That is the entire concurrency model.
#ifndef LWIP_LWIPOPTS_H
#define LWIP_LWIPOPTS_H
#define NO_SYS 1
#define SYS_LIGHTWEIGHT_PROT 1
#define LWIP_NETCONN 0 // needs NO_SYS=0
#define LWIP_SOCKET 0 // ours lives in kernel/net/inet.c
// Memory. All of it is static: the kernel's allocator hands out whole pages
// and nothing else, so lwIP's own pools are a better fit than wrapping kalloc.
// These numbers are sized for a handful of connections, not for a server.
#define MEM_LIBC_MALLOC 0
#define MEMP_MEM_MALLOC 0
#define MEM_ALIGNMENT 4
#define MEM_SIZE (192 * 1024)
#define MEMP_NUM_PBUF 32
#define MEMP_NUM_UDP_PCB 8
#define MEMP_NUM_TCP_PCB 16
#define MEMP_NUM_TCP_PCB_LISTEN 8
#define MEMP_NUM_TCP_SEG 64
#define MEMP_NUM_ARP_QUEUE 16
#define MEMP_NUM_SYS_TIMEOUT 16
#define PBUF_POOL_SIZE 48
#define PBUF_POOL_BUFSIZE 1536 // one Ethernet frame plus room
// Protocols. IPv4 only: the driver is proven for it, the QEMU test network is
// v4, and every line of v6 here would be untested.
#define LWIP_IPV4 1
#define LWIP_IPV6 0
#define LWIP_ARP 1
#define LWIP_ETHERNET 1
#define LWIP_ICMP 1
#define LWIP_RAW 1
// A loopback interface, so 127.0.0.1 is a real destination rather than an
// address with no route. lwip_init() creates it; nothing else has to.
//
// LWIP_NETIF_LOOPBACK is the other half and is easy to miss: without it a
// packet addressed to an interface's *own* address goes out of the wire and
// never comes back. With NO_SYS=1 there is no thread to drain the loopback
// queue either, so netif_poll_all() is called from the network thread -- see
// kernel/net/net.c.
#define LWIP_HAVE_LOOPIF 1
#define LWIP_NETIF_LOOPBACK 1
#define LWIP_LOOPBACK_MAX_PBUFS 8
#define LWIP_UDP 1
#define LWIP_TCP 1
#define LWIP_DHCP 1
#define LWIP_DNS 1
#define LWIP_AUTOIP 0
#define LWIP_IGMP 0
#define LWIP_NETIF_HOSTNAME 1
// DHCP needs UDP checksums and a broadcast path; both are on. Requesting an
// address rather than hard-coding one matters because QEMU's user-mode network
// hands out 10.0.2.15 through DHCP and nothing else will route.
#define LWIP_NETIF_STATUS_CALLBACK 1
#define LWIP_NETIF_LINK_CALLBACK 1
#define TCP_MSS 1460
#define TCP_SND_BUF (8 * TCP_MSS)
#define TCP_WND (8 * TCP_MSS)
#define TCP_SND_QUEUELEN ((4 * TCP_SND_BUF) / TCP_MSS)
#define TCP_LISTEN_BACKLOG 1
// Checksums in software. The e1000 can offload them, but the driver does not
// ask it to, and a checksum computed by a device the stack has not been told
// about is the sort of mismatch that shows up only against a real peer.
#define CHECKSUM_GEN_IP 1
#define CHECKSUM_GEN_UDP 1
#define CHECKSUM_GEN_TCP 1
#define CHECKSUM_CHECK_IP 1
#define CHECKSUM_CHECK_UDP 1
#define CHECKSUM_CHECK_TCP 1
// Statistics are what /proc/net reports; they cost a few counters.
#define LWIP_STATS 1
#define LWIP_STATS_DISPLAY 0
// Debug output off by default -- turning LWIP_DEBUG on here makes the stack
// extremely chatty on the console, which is occasionally exactly what you want.
#define LWIP_DEBUG 0
// lwIP would otherwise provide its own errno, which would then disagree with
// the kernel's for every value a syscall returns.
#define LWIP_PROVIDE_ERRNO 1
#endif