diff --git a/cmd/ping/main.c b/cmd/ping/main.c new file mode 100644 index 0000000..f1dbae8 --- /dev/null +++ b/cmd/ping/main.c @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// ping -- ICMP echo, over a SchOS ping socket. +// +// This is a first-party program rather than toybox's ping, and the reason is +// worth recording because it is not "ours is better". toybox's ping reads its +// replies with recvmsg(2) and asks for the TTL through an IP_RECVTTL control +// message, and SchOS has neither: no recvmsg, no ancillary data. Both are owed +// -- recvmsg is item 1 of docs/plan/foundations.md, where it is wanted for +// SCM_RIGHTS -- and neither should be rushed to make one applet run. +// +// So this uses sendto/recvfrom, which the kernel does have, and proves the ICMP +// socket end to end in the meantime. When recvmsg lands, toybox's ping will +// work and this can go. +// +// A "ping socket" is socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP): the kernel owns +// the IP header and the caller reads and writes the ICMP message alone. See +// inet_icmp_recv_cb in kernel/net/inet.c. + +#include "types.h" +#include "user.h" +#include "socket.h" + +#define ICMP_ECHO 8 +#define ICMP_ECHOREPLY 0 +#define DEFAULT_COUNT 4 +#define PAYLOAD 56 // what every other ping sends + +struct icmphdr { + uchar type; + uchar code; + ushort checksum; + ushort id; + ushort seq; +}; + +// lib/ is raw syscall stubs -- no libc, so not even memset. Everything here is +// hand-rolled for the same reason cmd/schinit is. +static void +zero(void *p, int n) +{ + char *b = p; + while (n-- > 0) *b++ = 0; +} + +static void +puts_(const char *s) +{ + int n = 0; + while (s[n]) n++; + write(1, (void *)s, n); +} + +static void +putn_(int v) +{ + char buf[12]; + int i = 0, neg = 0; + if (v < 0) { neg = 1; v = -v; } + if (v == 0) buf[i++] = '0'; + while (v) { buf[i++] = (char)('0' + v % 10); v /= 10; } + if (neg) buf[i++] = '-'; + while (i--) write(1, &buf[i], 1); +} + +// The Internet checksum: one's complement of the one's complement sum of every +// 16-bit word. Computed over the ICMP message with the checksum field zeroed, +// which is why it is filled in last. +static ushort +cksum(const void *p, int n) +{ + const uchar *b = p; + uint sum = 0; + int i; + + for (i = 0; i + 1 < n; i += 2) + sum += (uint)((b[i] << 8) | b[i + 1]); + if (i < n) + sum += (uint)(b[i] << 8); + while (sum >> 16) + sum = (sum & 0xFFFF) + (sum >> 16); + return (ushort)(~sum & 0xFFFF); +} + +static ushort +hton16(ushort v) +{ + return (ushort)((v >> 8) | (v << 8)); +} + +// Dotted quad to a network-byte-order address. Returns 0 on a malformed one, +// which is indistinguishable from 0.0.0.0 and is fine: nobody pings that. +static uint +parse_ip(const char *s, int *ok) +{ + uint out = 0; + int part, i, digits; + + *ok = 0; + for (i = 0; i < 4; i++) { + part = 0; digits = 0; + while (*s >= '0' && *s <= '9') { + part = part * 10 + (*s - '0'); + if (part > 255) return 0; + s++; digits++; + } + if (digits == 0) return 0; + out |= (uint)part << (8 * i); // little-endian host, so LSB first + if (i < 3) { + if (*s != '.') return 0; + s++; + } + } + if (*s != '\0') return 0; + *ok = 1; + return out; +} + +static void +print_ip(uint addr) +{ + for (int i = 0; i < 4; i++) { + putn_((int)((addr >> (8 * i)) & 0xFF)); + if (i < 3) puts_("."); + } +} + +int +main(int argc, char *argv[]) +{ + char target[64]; + int count = DEFAULT_COUNT; + int ok, sent = 0, recvd = 0; + + if (argc < 2) { + puts_("usage: ping [count]\n"); + exit(); + } + + // "localhost" without a resolver. The kernel has no DNS client reachable + // from userland, and the one name worth special-casing is the one that + // always means the same thing. + const char *host = argv[1]; + if (host[0] == 'l' && host[1] == 'o' && host[2] == 'c' && + host[3] == 'a' && host[4] == 'l' && host[5] == 'h' && + host[6] == 'o' && host[7] == 's' && host[8] == 't' && host[9] == 0) + host = "127.0.0.1"; + + { int i = 0; while (host[i] && i < 63) { target[i] = host[i]; i++; } target[i] = 0; } + + uint addr = parse_ip(target, &ok); + if (!ok) { + puts_("ping: not a dotted-quad address: "); + puts_(argv[1]); + puts_("\nping: there is no resolver yet; use an address\n"); + exit(); + } + + if (argc > 2) { + count = 0; + for (const char *p = argv[2]; *p >= '0' && *p <= '9'; p++) + count = count * 10 + (*p - '0'); + if (count <= 0) count = DEFAULT_COUNT; + } + + int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP); + if (fd < 0) { + puts_("ping: socket: "); + putn_(fd); + puts_("\n"); + exit(); + } + + puts_("PING "); + print_ip(addr); + puts_(" "); + putn_(PAYLOAD); + puts_(" bytes of data.\n"); + + ushort id = (ushort)getpid(); + + for (int seq = 1; seq <= count; seq++) { + char out[sizeof(struct icmphdr) + PAYLOAD]; + struct icmphdr *ih = (struct icmphdr *)out; + + zero(out, (int)sizeof(out)); + ih->type = ICMP_ECHO; + ih->code = 0; + ih->id = hton16(id); + ih->seq = hton16((ushort)seq); + for (int i = 0; i < PAYLOAD; i++) + out[sizeof(*ih) + i] = (char)('a' + (i % 26)); + ih->checksum = 0; + // The checksum is stored as it is computed: cksum() already works in + // network order, so byte-swapping it here would send a wrong one. + ih->checksum = hton16(cksum(out, (int)sizeof(out))); + + struct sockaddr_in sa; + zero(&sa, (int)sizeof(sa)); + sa.sin_family = AF_INET; + sa.sin_port = 0; + sa.sin_addr = addr; + + int t0 = uptime(); + int n = sendto(fd, out, (int)sizeof(out), 0, + (struct sockaddr *)&sa, sizeof(sa)); + if (n < 0) { + puts_("ping: sendto: "); + putn_(n); + puts_("\n"); + break; + } + sent++; + + // Wait for a reply, but not forever. uptime() counts 10 ms ticks, so + // 100 of them is one second -- the same timeout every ping uses. + char in[256]; + int got = -1, waited = 0; + struct sockaddr_in from; + int fromlen = sizeof(from); + + while (waited < 100) { + got = recvfrom(fd, in, (int)sizeof(in), 0, + (struct sockaddr *)&from, &fromlen); + if (got > 0) break; + sleep(1); + waited = uptime() - t0; + } + + if (got <= 0) { + puts_("Request timeout for icmp_seq "); + putn_(seq); + puts_("\n"); + continue; + } + + struct icmphdr *rh = (struct icmphdr *)in; + if (rh->type != ICMP_ECHOREPLY || hton16(rh->id) != id) { + puts_("ping: reply for somebody else, ignored\n"); + continue; + } + + recvd++; + putn_(got); + puts_(" bytes from "); + print_ip(from.sin_addr); + puts_(": icmp_seq="); + putn_((int)hton16(rh->seq)); + puts_(" time="); + putn_((uptime() - t0) * 10); + puts_(" ms\n"); + + if (seq < count) + sleep(100); // one second between packets + } + + close(fd); + + puts_("--- "); + print_ip(addr); + puts_(" ping statistics ---\n"); + putn_(sent); + puts_(" packets transmitted, "); + putn_(recvd); + puts_(" received, "); + putn_(sent ? (sent - recvd) * 100 / sent : 0); + puts_("% packet loss\n"); + + exit(); +} diff --git a/cmd/socktest/main.c b/cmd/socktest/main.c index 9c73530..6be9122 100644 --- a/cmd/socktest/main.c +++ b/cmd/socktest/main.c @@ -150,7 +150,23 @@ test_errors(void) "connect() to a name nobody bound fails"); close(s); - check(socket(AF_INET, SOCK_STREAM, 0) < 0, "AF_INET is refused, not faked"); + // This used to assert that AF_INET was *refused*, which was true when this + // file was written and stopped being true the day AF_INET landed. It kept + // passing anyway, and the reason is worth keeping: socktest runs seconds + // after boot, DHCP had not bound yet, and inet_new() refused every socket + // until it did -- reported as ENFILE. So the check saw "< 0" and called it + // a pass for a reason that had nothing to do with what it was testing. + // + // Two lessons, both already written down elsewhere in this tree and both + // learned again here: a test that asserts a *failure* has to say which + // failure, and a green suite is not evidence until something has checked + // that it can go red. + // + // What is left are the refusals that are unconditionally true, whatever the + // link is doing. + check(socket(99, SOCK_STREAM, 0) < 0, "an unknown address family is refused"); + check(socket(AF_INET, SOCK_STREAM, 99) < 0, + "AF_INET with a nonsense protocol is refused"); check(socket(AF_UNIX, SOCK_DGRAM, 0) < 0, "SOCK_DGRAM is refused, not faked"); // read/write on a plain file descriptor must not be mistaken for a socket, @@ -283,6 +299,109 @@ test_big(void) check(ok, "and in the right order"); } + +// --------------------------------------------------------------------------- +// Socket options. +// +// Both calls used to succeed unconditionally: setsockopt was `return 0;` and +// getsockopt wrote a zero for whatever was asked. So the only check that proves +// anything is a *round trip* -- set a value, read it back, and see the value +// rather than a zero -- plus one option that must be refused. A stub passes +// "does setsockopt return 0"; it cannot pass both of these at once. +// --------------------------------------------------------------------------- + +#define T_SOL_SOCKET 1 +#define T_SO_REUSEADDR 2 +#define T_SO_TYPE 3 +#define T_SO_RCVBUF 8 +#define T_SO_NOSUCH 0x7ffe // not an option Linux defines either + +static void +test_sockopt(void) +{ + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + int val, len; + + puts_("-- socket options\n"); + if (fd < 0) { check(0, "socket()"); return; } + + // SO_TYPE must report what this socket actually is, not zero. + val = -1; len = sizeof(val); + check(getsockopt(fd, T_SOL_SOCKET, T_SO_TYPE, &val, &len) == 0 && + val == SOCK_STREAM, "getsockopt SO_TYPE says SOCK_STREAM"); + + // The round trip. A stub that ignores the set and zeroes the get fails here. + val = 1; + check(setsockopt(fd, T_SOL_SOCKET, T_SO_REUSEADDR, &val, sizeof(val)) == 0, + "setsockopt SO_REUSEADDR accepted"); + val = -1; len = sizeof(val); + check(getsockopt(fd, T_SOL_SOCKET, T_SO_REUSEADDR, &val, &len) == 0 && + val == 1, "getsockopt SO_REUSEADDR reads back 1"); + + // And back off again, so the answer is not just "always non-zero". + val = 0; + setsockopt(fd, T_SOL_SOCKET, T_SO_REUSEADDR, &val, sizeof(val)); + val = -1; len = sizeof(val); + check(getsockopt(fd, T_SOL_SOCKET, T_SO_REUSEADDR, &val, &len) == 0 && + val == 0, "and reads back 0 once cleared"); + + // A real buffer size, not zero. + val = -1; len = sizeof(val); + check(getsockopt(fd, T_SOL_SOCKET, T_SO_RCVBUF, &val, &len) == 0 && val > 0, + "getsockopt SO_RCVBUF is a real size"); + + // The refusal. This is the half that a permissive stub cannot fake. + val = 1; + check(setsockopt(fd, T_SOL_SOCKET, T_SO_NOSUCH, &val, sizeof(val)) < 0, + "setsockopt of an unknown option is refused"); + len = sizeof(val); + check(getsockopt(fd, T_SOL_SOCKET, T_SO_NOSUCH, &val, &len) < 0, + "getsockopt of an unknown option is refused"); + + close(fd); +} + +// --------------------------------------------------------------------------- +// SOCK_NONBLOCK, end to end. +// +// Checking that socket() accepts the flag proves nothing -- it was accepted +// before, and discarded. What proves it is a read that *returns* on a socket +// with nothing in it, where a blocking socket would sleep forever. +// --------------------------------------------------------------------------- + +#define T_SOCK_NONBLOCK 0x800 +#define T_F_GETFL 3 +#define T_O_NONBLOCK 04000 + +static void +test_nonblock(void) +{ + int sv[2]; + char c; + + puts_("-- SOCK_NONBLOCK\n"); + + if (socketpair(AF_UNIX, SOCK_STREAM | T_SOCK_NONBLOCK, 0, sv) < 0) { + check(0, "socketpair(SOCK_NONBLOCK)"); + return; + } + + check((fcntl(sv[0], T_F_GETFL, 0) & T_O_NONBLOCK) != 0, + "fcntl(F_GETFL) reports O_NONBLOCK"); + + // Nothing has been written, so a blocking read would never return. If this + // test hangs, the flag was dropped -- which is exactly what it used to be. + check(read(sv[0], &c, 1) < 0, "read on an empty non-blocking socket returns"); + + // And it still carries data, so the flag did not break the socket. + write(sv[1], "x", 1); + c = 0; + check(read(sv[0], &c, 1) == 1 && c == 'x', "and still delivers a byte"); + + close(sv[0]); + close(sv[1]); +} + int main(void) { @@ -293,6 +412,8 @@ main(void) test_eof(); test_socketpair(); test_big(); + test_sockopt(); + test_nonblock(); if (failures == 0) puts_("socktest: ALL CHECKS PASSED\n"); diff --git a/include/socket.h b/include/socket.h index 959cc68..de9ae41 100644 --- a/include/socket.h +++ b/include/socket.h @@ -22,6 +22,32 @@ #define SOCK_STREAM 1 #define SOCK_DGRAM 2 +// Flags ORed into socket()'s and accept4()'s `type`/`flags` argument. Linux's +// values, because a musl program passes Linux's constants. +#define SOCK_NONBLOCK 0x800 // O_NONBLOCK +#define SOCK_CLOEXEC 0x80000 // O_CLOEXEC + +// setsockopt/getsockopt levels and names, Linux's numbering. +// +// Only what is listed here is accepted; anything else is answered with +// ENOPROTOOPT rather than a success it did not earn. See the comment above +// sys_setsockopt for why that is worth the risk of failing a caller that never +// checked before. +#define SOL_SOCKET 1 +#define IPPROTO_ICMP 1 +#define IPPROTO_TCP 6 + +#define SO_REUSEADDR 2 +#define SO_TYPE 3 +#define SO_ERROR 4 +#define SO_BROADCAST 6 +#define SO_SNDBUF 7 +#define SO_RCVBUF 8 +#define SO_KEEPALIVE 9 +#define SO_ACCEPTCONN 30 + +#define TCP_NODELAY 1 + #define UNIX_PATH_MAX 108 #define SHUT_RD 0 @@ -93,6 +119,15 @@ struct socket { // Connections waiting to be accepted, on a listener. struct socket *queue[NBACKLOG]; int nqueue; + + // Socket options that are genuinely honoured. Kept here rather than only in + // lwIP's pcb because getsockopt has to answer for an AF_UNIX socket too, and + // because a flag set before bind() has to survive until there is a pcb to + // apply it to. + int o_reuseaddr; + int o_keepalive; + int o_broadcast; + int o_nodelay; }; #endif diff --git a/include/user.h b/include/user.h index 61ad465..77f058e 100644 --- a/include/user.h +++ b/include/user.h @@ -134,6 +134,7 @@ int socket(int domain, int type, int protocol); int bind(int fd, const struct sockaddr *addr, int addrlen); int listen(int fd, int backlog); int accept(int fd, struct sockaddr *addr, int *addrlen); +int accept4(int fd, struct sockaddr *addr, int *addrlen, int flags); int connect(int fd, const struct sockaddr *addr, int addrlen); int socketpair(int domain, int type, int protocol, int sv[2]); int sendto(int fd, const void *buf, int len, int flags, const struct sockaddr*, int); diff --git a/kernel/core/string.c b/kernel/core/string.c index 91f50ce..cd68751 100644 --- a/kernel/core/string.c +++ b/kernel/core/string.c @@ -70,6 +70,18 @@ strncmp(const char *p, const char *q, uint n) return (uchar)*p - (uchar)*q; } +// Added when lwIP's DNS resolver started needing it: with LWIP_HAVE_LOOPIF on, +// dns.c short-circuits the name "localhost" with a strcmp. The kernel had only +// strncmp until then, deliberately -- see the note in kernel/net/lwip/shim/ +// string.h about not growing that shim into a libc. +int +strcmp(const char *p, const char *q) +{ + while(*p && *p == *q) + p++, q++; + return (uchar)*p - (uchar)*q; +} + char* strncpy(char *s, const char *t, int n) { diff --git a/kernel/net/inet.c b/kernel/net/inet.c index df64710..8fbb977 100644 --- a/kernel/net/inet.c +++ b/kernel/net/inet.c @@ -62,6 +62,9 @@ #include "lwip/opt.h" #include "lwip/tcp.h" #include "lwip/udp.h" +#include "lwip/raw.h" +#include "lwip/icmp.h" +#include "lwip/prot/ip4.h" #include "lwip/ip_addr.h" #include "lwip/pbuf.h" @@ -84,9 +87,11 @@ struct dgram { struct inet_sock { int inuse; int type; // SOCK_STREAM or SOCK_DGRAM + int proto; // 0, or IPPROTO_ICMP for a ping socket struct tcp_pcb *tpcb; struct udp_pcb *upcb; + struct raw_pcb *rpcb; // SOCK_DGRAM + IPPROTO_ICMP // Receive ring. lwIP hands over pbufs from the network thread; a reader // drains bytes. Full means the sender is told to back off, which for TCP is @@ -399,6 +404,77 @@ inet_udp_recv_cb(void *arg, struct udp_pcb *pcb, struct pbuf *p, net_wake(); } +// ICMP, for ping sockets. +// +// Linux calls these "ping sockets": SOCK_DGRAM with IPPROTO_ICMP, giving an +// unprivileged process the one thing raw sockets were used for. The contract is +// that the caller reads and writes the ICMP message alone, and the kernel deals +// with the IP header -- which is why this strips it on the way in, where +// lwIP's raw layer hands over a pbuf that starts at the IP header. +// +// **Only echo replies are eaten.** raw_input runs before lwIP's own +// icmp_input, so a callback that returned 1 for everything would swallow the +// echo *requests* too and this machine would silently stop answering pings +// from outside. Returning 0 leaves the packet for lwIP, which is exactly what +// should happen to every ICMP message we did not ask for. +// +// No matching against the echo id: a reply goes to every open ICMP socket. +// Linux rewrites the id per socket and filters on it, which matters when two +// processes ping at once; with one ping at a time the difference is invisible, +// and a socket that sees a reply to somebody else's request will read a +// sequence number it never sent and ignore it. +static u8_t +inet_icmp_recv_cb(void *arg, struct raw_pcb *pcb, struct pbuf *p, + const ip_addr_t *addr) +{ + struct inet_sock *is = arg; + (void)pcb; + + if (is == 0 || p == NULL) + return 0; + + // The IP header is variable length: IHL counts 32-bit words. + if (p->tot_len < sizeof(struct ip_hdr)) + return 0; + struct ip_hdr *ih = (struct ip_hdr *)p->payload; + u16_t hlen = (u16_t)(IPH_HL(ih) * 4); + if (hlen < sizeof(struct ip_hdr) || p->tot_len < hlen + 8) + return 0; + + // Type 0 is ICMP Echo Reply. Everything else -- including the requests this + // host must keep answering -- is left for lwIP. + u8_t type; + if (pbuf_copy_partial(p, &type, 1, hlen) != 1 || type != 0) + return 0; + + u16_t icmplen = (u16_t)(p->tot_len - hlen); + + if (icmplen <= rx_space(is) && dq_used(is) < NDGRAM) { + struct dgram *d = &is->dq[is->dhead % NDGRAM]; + d->addr = addr ? ip4_addr_get_u32(ip_2_ip4(addr)) : 0; + d->port = 0; // ICMP has no ports + d->len = icmplen; + is->dhead++; + + // Copy the ICMP message out, skipping the IP header, across whatever pbuf + // chain it arrived in. + u16_t off = hlen; + while (off < p->tot_len) { + char tmp[64]; + u16_t want = (u16_t)(p->tot_len - off); + if (want > sizeof(tmp)) want = sizeof(tmp); + if (pbuf_copy_partial(p, tmp, want, off) != want) + break; + rx_put(is, tmp, want); + off = (u16_t)(off + want); + } + } + + pbuf_free(p); + net_wake(); + return 1; // eaten +} + /* ------------------------------------------------------------------ */ /* Allocation */ /* ------------------------------------------------------------------ */ @@ -422,11 +498,13 @@ inet_alloc(int type) /* ------------------------------------------------------------------ */ void * -inet_new(int type) +inet_new(int type, int proto) { if (type != SOCK_STREAM && type != SOCK_DGRAM) return 0; - if (!net_is_up()) + // Ready, not *up*: see net_is_ready() in kernel/net/net.c. Requiring an + // address here made every socket() fail until DHCP answered. + if (!net_is_ready()) return 0; lwip_sys_lock(); @@ -435,8 +513,21 @@ inet_new(int type) lwip_sys_unlock(); return 0; } + is->proto = proto; - if (type == SOCK_STREAM) { + if (proto == IPPROTO_ICMP) { + // A ping socket. raw_new registers for one IP protocol number; the + // callback below filters to echo replies so lwIP keeps answering the + // requests this host receives. See inet_icmp_recv_cb. + is->rpcb = raw_new(IP_PROTO_ICMP); + if (is->rpcb == 0) { + is->inuse = 0; + lwip_sys_unlock(); + return 0; + } + raw_recv(is->rpcb, inet_icmp_recv_cb, is); + raw_bind(is->rpcb, IP_ANY_TYPE); + } else if (type == SOCK_STREAM) { is->tpcb = tcp_new(); if (is->tpcb == 0) { is->inuse = 0; @@ -480,6 +571,58 @@ inet_bind(void *h, uint32 addr, uint16 port) return e == ERR_OK ? 0 : -EINVAL; } +// Apply a socket option to the lwIP pcb behind this handle. +// +// Only the four that lwIP can genuinely honour. Everything else is refused by +// the caller (kernel/sys/sys_net.c) before it reaches here, so this never has +// to pretend. +// +// Called with the option already recorded on the struct socket, so a flag set +// before bind() is not lost: lwIP's pcb exists from inet_new() onwards, which +// is early enough for all four, but recording it there as well means +// getsockopt has one answer and not two. +int +inet_setopt(void *h, int level, int name, int val) +{ + struct inet_sock *is = h; + + lwip_sys_lock(); + if (level == SOL_SOCKET) { + struct ip_pcb *p = (is->type == SOCK_STREAM) ? (struct ip_pcb *)is->tpcb + : (struct ip_pcb *)is->upcb; + if (p == 0) { lwip_sys_unlock(); return -EINVAL; } + switch (name) { + case SO_REUSEADDR: + if (val) ip_set_option(p, SOF_REUSEADDR); + else ip_reset_option(p, SOF_REUSEADDR); + break; + case SO_KEEPALIVE: + if (val) ip_set_option(p, SOF_KEEPALIVE); + else ip_reset_option(p, SOF_KEEPALIVE); + break; + case SO_BROADCAST: + if (val) ip_set_option(p, SOF_BROADCAST); + else ip_reset_option(p, SOF_BROADCAST); + break; + default: + lwip_sys_unlock(); + return -ENOPROTOOPT; + } + } else if (level == IPPROTO_TCP && name == TCP_NODELAY) { + if (is->type != SOCK_STREAM || is->tpcb == 0) { + lwip_sys_unlock(); + return -EINVAL; + } + if (val) tcp_nagle_disable(is->tpcb); + else tcp_nagle_enable(is->tpcb); + } else { + lwip_sys_unlock(); + return -ENOPROTOOPT; + } + lwip_sys_unlock(); + return 0; +} + int inet_listen(void *h, int backlog) { @@ -653,6 +796,38 @@ inet_sendto(void *h, char *buf, int n, uint addr, ushort port) if (n <= 0) return 0; + // ICMP. The caller hands over the ICMP message alone -- header, checksum and + // payload -- and lwIP puts the IP header on the front, which is what makes + // this a ping socket rather than a raw one. + // + // PBUF_IP rather than PBUF_TRANSPORT for the headroom: the IP header is what + // goes in front here, and asking for transport headroom would have lwIP + // reallocate to make room for it. + if (is->proto == IPPROTO_ICMP) { + if (is->rpcb == 0) + return -ENOTCONN; + if (addr == 0) + return -EDESTADDRREQ; + + lwip_sys_lock(); + struct pbuf *p = pbuf_alloc(PBUF_IP, (u16_t)n, PBUF_RAM); + if (p == 0) { + lwip_sys_unlock(); + return -ENOBUFS; + } + pbuf_take(p, buf, (u16_t)n); + + ip_addr_t ip; + ip_addr_set_ip4_u32(&ip, addr); + err_t e = raw_sendto(is->rpcb, p, &ip); + + pbuf_free(p); + lwip_sys_unlock(); + if (e == ERR_OK) + return n; + return e == ERR_RTE ? -ENETUNREACH : -EIO; + } + if (is->type == SOCK_DGRAM) { lwip_sys_lock(); struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, (u16_t)n, PBUF_RAM); @@ -793,6 +968,11 @@ inet_close(void *h) udp_remove(is->upcb); is->upcb = 0; } + if (is->rpcb) { + raw_recv(is->rpcb, 0, 0); + raw_remove(is->rpcb); + is->rpcb = 0; + } is->inuse = 0; lwip_sys_unlock(); } diff --git a/kernel/net/lwip/lwipopts.h b/kernel/net/lwip/lwipopts.h index fe395e7..0a5cf8f 100644 --- a/kernel/net/lwip/lwipopts.h +++ b/kernel/net/lwip/lwipopts.h @@ -49,6 +49,18 @@ #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 diff --git a/kernel/net/lwip/shim/string.h b/kernel/net/lwip/shim/string.h index d4874f8..7d48318 100644 --- a/kernel/net/lwip/shim/string.h +++ b/kernel/net/lwip/shim/string.h @@ -25,6 +25,7 @@ int memcmp(const void *, const void *, unsigned long); void *memmove(void *, const void *, unsigned long); void *memset(void *, int, unsigned long); int strlen(const char *); +int strcmp(const char *, const char *); int strncmp(const char *, const char *, unsigned long); char *strncpy(char *, const char *, int); diff --git a/kernel/net/net.c b/kernel/net/net.c index 2073a9f..ef95bad 100644 --- a/kernel/net/net.c +++ b/kernel/net/net.c @@ -55,12 +55,29 @@ static int net_started; // something true to say rather than reporting 0.0.0.0 forever. static int net_bound; +// Set once lwIP is initialised and this thread is running its timers. Distinct +// from net_bound, and the distinction is not academic: inet_new() used to +// require an *address* before it would hand out a socket, so every AF_INET +// socket() failed until DHCP answered -- reported as ENFILE, which sends you to +// look at the file table. It is also simply wrong for 127.0.0.1, which needs no +// lease and no NIC. +// +// Creating a socket is not the same as having somewhere to send: a bind or a +// connect with no route fails on its own, with an error that says so. +static int net_ready; + int net_is_up(void) { return net_bound; } +int +net_is_ready(void) +{ + return net_ready; +} + static void net_thread(void) { @@ -69,6 +86,9 @@ net_thread(void) // before there is a process context means doing it somewhere that cannot // sleep if the allocator ever needs to. lwip_sys_init(); + // From here a socket can be created: the pools exist and the timers are + // about to run. An address may still be minutes away, or never arrive. + net_ready = 1; lwip_sys_lock(); lwip_init(); @@ -111,6 +131,10 @@ net_thread(void) lwip_sys_lock(); moved = net_eth_poll(); sys_check_timeouts(); + // Loopback has no driver and no interrupt: a packet sent to 127.0.0.1 + // sits on the loopif's queue until something drains it. With NO_SYS=1 + // that something is this thread. + netif_poll_all(); // Report each address once, when it first arrives. Anyone who has spent // an evening wondering whether the guest has an address at all wants diff --git a/kernel/net/unix.c b/kernel/net/unix.c index 781309e..6827d63 100644 --- a/kernel/net/unix.c +++ b/kernel/net/unix.c @@ -159,6 +159,88 @@ sockdup(struct socket *s) release(&stab.lock); } +// --------------------------------------------------------------------------- +// Socket options +// +// Only the options that are genuinely honoured appear here; everything else is +// answered ENOPROTOOPT by the switch's default. That is deliberate and it is a +// change of policy: both calls used to succeed unconditionally, which is the +// worst of the three possible answers. A caller told "no" adapts; a caller told +// "yes" falsely proceeds on the strength of it -- setting SO_RCVTIMEO and then +// blocking forever in a read it believes will time out. +// +// The flags are recorded here as well as pushed into lwIP so that getsockopt +// has one answer rather than two, and so that an AF_UNIX socket -- which has no +// pcb and for which none of these mean anything -- can still give back what was +// set, as POSIX requires. +int +sockopt_set(struct socket *s, int level, int name, int val) +{ + int r = 0; + + acquire(&stab.lock); + if (level == SOL_SOCKET) { + switch (name) { + case SO_REUSEADDR: s->o_reuseaddr = !!val; break; + case SO_KEEPALIVE: s->o_keepalive = !!val; break; + case SO_BROADCAST: s->o_broadcast = !!val; break; + // Accepted and left at the size we have. Not the same lie as before: + // Linux is explicitly free to clamp these, and sockopt_get below reports + // the real figure rather than the requested one. + case SO_SNDBUF: + case SO_RCVBUF: release(&stab.lock); return 0; + default: release(&stab.lock); return -ENOPROTOOPT; + } + } else if (level == IPPROTO_TCP && name == TCP_NODELAY) { + if (s->type != SOCK_STREAM) { release(&stab.lock); return -EINVAL; } + s->o_nodelay = !!val; + } else { + release(&stab.lock); + return -ENOPROTOOPT; + } + int inet = (s->domain == AF_INET); + void *h = s->inet; + release(&stab.lock); + + // Outside the table lock: inet_setopt takes the lwIP lock, which is a + // sleeplock, and the same ordering rule the rest of this file follows. + if (inet && h) + r = inet_setopt(h, level, name, val); + return r; +} + +int +sockopt_get(struct socket *s, int level, int name, int *out) +{ + int r = 0; + + acquire(&stab.lock); + if (level == IPPROTO_TCP && name == TCP_NODELAY) { + *out = s->o_nodelay; + } else if (level != SOL_SOCKET) { + r = -ENOPROTOOPT; + } else { + switch (name) { + case SO_TYPE: *out = s->type; break; + case SO_ACCEPTCONN: *out = (s->state == SS_LISTENING); break; + case SO_REUSEADDR: *out = s->o_reuseaddr; break; + case SO_KEEPALIVE: *out = s->o_keepalive; break; + case SO_BROADCAST: *out = s->o_broadcast; break; + // The real buffer, not a number somebody asked for. + case SO_SNDBUF: + case SO_RCVBUF: *out = SOCKBUF; break; + // An honest zero rather than the blanket one it replaces: connect() here + // is blocking and reports failure as its own return value, so there is no + // deferred error for SO_ERROR to carry. It becomes a real field the day a + // non-blocking connect exists. + case SO_ERROR: *out = 0; break; + default: r = -ENOPROTOOPT; break; + } + } + release(&stab.lock); + return r; +} + const char * sockname(struct socket *s) { @@ -254,7 +336,7 @@ sockwrite(struct socket *s, char *addr, int n) // --------------------------------------------------------------------------- struct socket * -socknew(int domain, int type) +socknew(int domain, int type, int proto) { struct socket *s; @@ -276,7 +358,7 @@ socknew(int domain, int type) if(domain == AF_INET){ // Outside the table lock: inet_new takes the lwIP lock, which is a // sleeplock, and sleeping while holding a spinlock is a panic. - s->inet = inet_new(type); + s->inet = inet_new(type, proto); if(s->inet == 0){ sockclose(s); return 0; diff --git a/kernel/sys/sys_net.c b/kernel/sys/sys_net.c index bbc5750..d3af45f 100644 --- a/kernel/sys/sys_net.c +++ b/kernel/sys/sys_net.c @@ -67,8 +67,22 @@ fetch_sun_path(addr_t uaddr, int ulen, char *out) // Wrap an established socket in a file and a descriptor. Consumes the // socket's reference on success; on failure the caller still owns it. +// +// `sflags` carries SOCK_CLOEXEC and SOCK_NONBLOCK, which socket(), socketpair() +// and accept4() all take and all used to discard. +// +// Discarding them is not harmless in either direction. A dropped SOCK_CLOEXEC +// leaks the descriptor across every exec the process makes -- so a server that +// forks a child holds its listening socket open in it, and the port stays bound +// after the server dies. A dropped SOCK_NONBLOCK is worse: the caller believes +// it has a non-blocking socket, does not select() first, and blocks forever in +// what it thinks is a poll loop. +// +// Both are one line each, and they were masked off with a comment saying they +// were not honoured -- which is exactly the class of quiet lie the rest of this +// file has just been cleaned of. static int -sock_to_fd(struct socket *s) +sock_to_fd(struct socket *s, int sflags) { struct file *f; int fd; @@ -84,6 +98,9 @@ sock_to_fd(struct socket *s) f->readable = 1; f->writable = 1; f->off = 0; + f->nonblock = (sflags & SOCK_NONBLOCK) != 0; + if (sflags & SOCK_CLOEXEC) + myproc()->ofile_flags[fd] |= FD_CLOEXEC; return fd; } @@ -117,21 +134,40 @@ SYSCALL_DEFINE(socket) if (domain != AF_UNIX && domain != AF_INET) return -EAFNOSUPPORT; - // SOCK_CLOEXEC / SOCK_NONBLOCK ride in the high bits of `type`. Neither - // is honoured yet, but masking them off means a musl program that always - // sets SOCK_CLOEXEC still gets a socket instead of EINVAL. + // SOCK_CLOEXEC / SOCK_NONBLOCK ride in the high bits of `type`. Split them + // off rather than discarding them: the type check below wants the low byte, + // and sock_to_fd wants the flags. + int sflags = type & (SOCK_CLOEXEC | SOCK_NONBLOCK); + if (type & ~(0xFF | SOCK_CLOEXEC | SOCK_NONBLOCK)) + return -EINVAL; type &= 0xFF; if (domain == AF_UNIX && type != SOCK_STREAM) return -EPROTOTYPE; if (domain == AF_INET && type != SOCK_STREAM && type != SOCK_DGRAM) return -EPROTOTYPE; - if (protocol != 0) - return -EPROTONOSUPPORT; - if ((s = socknew(domain, type)) == 0) + // Protocol 0 means "the default for this type". The one non-default we + // support is a ping socket -- SOCK_DGRAM with IPPROTO_ICMP, Linux's way of + // letting an unprivileged process do the one thing raw sockets were used + // for. Anything else is still refused rather than quietly given a socket + // that speaks a different protocol than the caller asked for. + if (protocol != 0) { + if (!(domain == AF_INET && type == SOCK_DGRAM && + protocol == IPPROTO_ICMP)) + return -EPROTONOSUPPORT; + } + + // ENETDOWN rather than ENFILE when the stack itself is not running: the + // old blanket ENFILE said "file table overflow" for a machine with no + // network, which is a long way from the truth and a longer way from the + // fix. + if (domain == AF_INET && !net_is_ready()) + return -ENETDOWN; + + if ((s = socknew(domain, type, protocol)) == 0) return -ENFILE; - if ((fd = sock_to_fd(s)) < 0) { + if ((fd = sock_to_fd(s, sflags)) < 0) { sockclose(s); return fd; } @@ -229,7 +265,16 @@ SYSCALL_DEFINE(connect) return sockconnect(s, path); } -SYSCALL_DEFINE(accept) +// accept(2) and accept4(2) share everything but the flags word. +// +// They are two table entries rather than one with an alias, and the reason is +// the argument count: accept takes three, so a handler that read a fourth +// register would be reading whatever the caller happened to leave there and +// would invent a SOCK_NONBLOCK out of stack litter. That is precisely the bug +// the old `SYSCALL_ALIAS(288, accept)` would have grown the moment the flags +// started being honoured. +static addr_t +do_accept(int sflags) { struct socket *s, *c; addr_t uaddr, ulenp; @@ -243,7 +288,7 @@ SYSCALL_DEFINE(accept) if ((c = sockaccept(s)) == 0) return -EINVAL; - if ((fd = sock_to_fd(c)) < 0) { + if ((fd = sock_to_fd(c, sflags)) < 0) { sockclose(c); return fd; } @@ -273,6 +318,18 @@ SYSCALL_DEFINE(accept) return fd; } +SYSCALL_DEFINE(accept) { return do_accept(0); } + +SYSCALL_DEFINE(accept4) +{ + int flags = 0; + + argint(3, &flags); + if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK)) + return -EINVAL; + return do_accept(flags); +} + SYSCALL_DEFINE(socketpair) { int domain, type, protocol; @@ -287,6 +344,7 @@ SYSCALL_DEFINE(socketpair) if (domain != AF_UNIX) return -EAFNOSUPPORT; + int sflags = type & (SOCK_CLOEXEC | SOCK_NONBLOCK); type &= 0xFF; if (type != SOCK_STREAM) return -EPROTOTYPE; @@ -294,12 +352,12 @@ SYSCALL_DEFINE(socketpair) if ((r = sockpair(&a, &b)) < 0) return r; - if ((fd0 = sock_to_fd(a)) < 0) { + if ((fd0 = sock_to_fd(a, sflags)) < 0) { sockclose(a); sockclose(b); return fd0; } - if ((fd1 = sock_to_fd(b)) < 0) { + if ((fd1 = sock_to_fd(b, sflags)) < 0) { myproc()->ofile[fd0] = 0; sockclose(a); sockclose(b); @@ -431,23 +489,90 @@ SYSCALL_DEFINE(sockshutdown) return sock_shutdown(s, how); } -// getsockopt/setsockopt: accepted and ignored. A server that sets SO_REUSEADDR -// and treats failure as fatal is common enough to be worth not failing. -SYSCALL_DEFINE(setsockopt) { return 0; } +// setsockopt / getsockopt. +// +// These both used to succeed unconditionally -- setsockopt was `return 0;` and +// getsockopt wrote a zero for every option asked about. The comment said it was +// so that a server setting SO_REUSEADDR and treating failure as fatal would not +// fail. +// +// That trade is the wrong way round, and it is worth being precise about why, +// because "accept and ignore" looks like the kind thing to do. A caller that +// gets ENOPROTOOPT knows the option did not take effect and can decide what to +// do. A caller that gets success *changes its behaviour on the strength of it*: +// it sets SO_RCVTIMEO and then blocks forever in a read it believes will time +// out; it 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 protocol stack. +// +// So the rule here is the one the rest of this kernel already follows: do it, +// or say you did not. The four options below are genuinely applied to lwIP's +// pcb; the readable ones report the truth; everything else is ENOPROTOOPT. +// +// SO_REUSEADDR is honoured 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. + +// Fetch the int a setsockopt call is passing. Every option handled here is an +// int; anything wanting a struct (SO_RCVTIMEO and friends) is refused above. +static int +opt_in(addr_t uval, int ulen, int *out) +{ + if (!uval || ulen < (int)sizeof(int)) + return -EINVAL; + return get_user(*out, uval) < 0 ? -EFAULT : 0; +} + +// Hand one int back, and report how many bytes that was. +static int +opt_out(addr_t uval, addr_t ulenp, int val) +{ + int want = 0; + + if (!uval || !ulenp) + return -EFAULT; + if (get_user(want, ulenp) < 0) + return -EFAULT; + if (want < (int)sizeof(int)) + return -EINVAL; + if (put_user(val, uval) < 0) + return -EFAULT; + want = sizeof(int); + return put_user(want, ulenp) < 0 ? -EFAULT : 0; +} + +SYSCALL_DEFINE(setsockopt) +{ + struct socket *s; + addr_t uval; + int level, name, ulen, val, r; + + if ((r = argsock(0, &s)) < 0) + return r; + argint(1, &level); + argint(2, &name); + argaddr(3, &uval); + argint(4, &ulen); + + if ((r = opt_in(uval, ulen, &val)) < 0) + return r; + return sockopt_set(s, level, name, val); +} SYSCALL_DEFINE(getsockopt) { - addr_t uval, ulen; - int zero = 0, len = 0; + struct socket *s; + addr_t uval, ulenp; + int level, name, val = 0, r; + if ((r = argsock(0, &s)) < 0) + return r; + argint(1, &level); + argint(2, &name); argaddr(3, &uval); - argaddr(4, &ulen); - if (uval && ulen) { - if (get_user(len, ulen) == 0 && len >= (int)sizeof(int)) { - put_user(zero, uval); - len = sizeof(int); - put_user(len, ulen); - } - } - return 0; + argaddr(4, &ulenp); + + if ((r = sockopt_get(s, level, name, &val)) < 0) + return r; + return opt_out(uval, ulenp, val); } diff --git a/kernel/sys/table.inc b/kernel/sys/table.inc index 0535b48..722a4d1 100644 --- a/kernel/sys/table.inc +++ b/kernel/sys/table.inc @@ -134,6 +134,12 @@ SYSCALL(189, 48, sockshutdown) SYSCALL(190, 53, socketpair) SYSCALL(191, 54, setsockopt) SYSCALL(192, 55, getsockopt) +// accept4() differs from accept() only by a flags argument carrying +// SOCK_CLOEXEC/SOCK_NONBLOCK. It was an alias for accept, which silently +// dropped both -- and it cannot be an alias now that they are honoured: accept +// takes three arguments, so reading a fourth register from an accept() caller +// reads whatever it last left there and invents a flag out of stack litter. +SYSCALL(198, 288, accept4) // --- time --- SYSCALL( 28, 37, alarm) @@ -288,6 +294,3 @@ SYSCALL_ALIAS( 289, signalfd4) SYSCALL_ALIAS( 290, eventfd2) SYSCALL_ALIAS( 295, readv) SYSCALL_ALIAS( 296, writev) -// accept4() differs from accept() only in a flags argument naming -// SOCK_CLOEXEC/SOCK_NONBLOCK, neither of which is honoured yet. -SYSCALL_ALIAS( 288, accept) diff --git a/lib/usys.S b/lib/usys.S index e69428f..08b3ce8 100644 --- a/lib/usys.S +++ b/lib/usys.S @@ -100,6 +100,7 @@ SYSCALL(socket) SYSCALL(bind) SYSCALL(listen) SYSCALL(accept) +SYSCALL(accept4) SYSCALL(connect) SYSCALL(sendto) SYSCALL(recvfrom)