// SPDX-License-Identifier: Apache-2.0 // // netecho -- a network, in a process. // // docs/plan/ldls.md L3a. `/dev/netdev` lets a process be a network interface; // this is the program that proves it, and it is deliberately *not* a driver. // It has no hardware, claims none, and answers every frame itself: // // DHCP so the interface gets an address at all -- lwIP asks the moment // the link comes up, and an interface with no address cannot be // pinged, so this is not optional // ARP so the stack can find us // ICMP so `ping 10.9.0.1` answers // // Why a fake network and not the Linux driver straight away: the two halves of // L3 fail differently. If `ping` does not work with this, the bridge is wrong. // If it works with this and not with a driver, the driver's side is wrong. A // test that can only fail in one lump tells you nothing about which half. // // Everything here is built by hand from the RFCs -- Ethernet II framing, // ARP (RFC 826), IPv4 (RFC 791), ICMP (RFC 792), DHCP (RFC 2131). All // published, all specifications, which is docs/licensing.md ยง5. #include "types.h" #include "user.h" #include "fcntl.h" #include "netuser.h" // Us, and the address we hand out. Numbers chosen not to collide with QEMU's // 10.0.2.0/24 user network, so both interfaces can be up at once. #define MY_IP 0x0A090001u /* 10.9.0.1, the "router" */ #define PEER_IP 0x0A090002u /* 10.9.0.2, offered to SchOS */ #define NETMASK 0xFFFFFF00u static const unsigned char my_mac[6] = { 0x02, 0x4c, 0x44, 0x4c, 0x53, 0x01 }; // --- output, since lib/ has no printf --------------------------------------- static void puts_(const char *s) { int n = 0; while (s[n]) n++; write(1, (void *)s, n); } static void putn(long long v) { char b[24]; int i = 0; if (v < 0) { write(1, "-", 1); v = -v; } if (v == 0) { write(1, "0", 1); return; } while (v > 0) { b[i++] = (char)('0' + (v % 10)); v /= 10; } while (i-- > 0) write(1, &b[i], 1); } static void putip(unsigned int ip) { putn((ip >> 24) & 0xff); puts_("."); putn((ip >> 16) & 0xff); puts_("."); putn((ip >> 8) & 0xff); puts_("."); putn(ip & 0xff); } // --- reading and writing the wire ------------------------------------------- static unsigned short rd16(const unsigned char *p) { return (unsigned short)((p[0] << 8) | p[1]); } static unsigned int rd32(const unsigned char *p) { return ((unsigned int)p[0] << 24) | ((unsigned int)p[1] << 16) | ((unsigned int)p[2] << 8) | p[3]; } static void wr16(unsigned char *p, unsigned short v) { p[0] = (unsigned char)(v >> 8); p[1] = (unsigned char)v; } static void wr32(unsigned char *p, unsigned int v) { p[0] = (unsigned char)(v >> 24); p[1] = (unsigned char)(v >> 16); p[2] = (unsigned char)(v >> 8); p[3] = (unsigned char)v; } // The one's-complement sum every IP protocol uses. RFC 1071. static unsigned short csum(const unsigned char *p, int n, unsigned int seed) { unsigned int s = seed; int i; for (i = 0; i + 1 < n; i += 2) s += (unsigned int)((p[i] << 8) | p[i + 1]); if (i < n) s += (unsigned int)(p[i] << 8); while (s >> 16) s = (s & 0xffff) + (s >> 16); return (unsigned short)(~s & 0xffff); } static void eth_header(unsigned char *f, const unsigned char *dst, unsigned short type) { int i; for (i = 0; i < 6; i++) f[i] = dst[i]; for (i = 0; i < 6; i++) f[6 + i] = my_mac[i]; wr16(f + 12, type); } // --- ARP (RFC 826) ----------------------------------------------------------- static int arp(const unsigned char *in, int len, unsigned char *out) { const unsigned char *a = in + 14; if (len < 14 + 28) return 0; if (rd16(a) != 1 || rd16(a + 2) != 0x0800) return 0; /* ethernet, ipv4 */ if (rd16(a + 6) != 1) return 0; /* request */ if (rd32(a + 24) != MY_IP) return 0; /* not for us */ eth_header(out, in + 6, 0x0806); { unsigned char *r = out + 14; int i; wr16(r, 1); wr16(r + 2, 0x0800); r[4] = 6; r[5] = 4; wr16(r + 6, 2); /* reply */ for (i = 0; i < 6; i++) r[8 + i] = my_mac[i]; wr32(r + 14, MY_IP); for (i = 0; i < 6; i++) r[18 + i] = a[8 + i]; /* their mac */ wr32(r + 24, rd32(a + 14)); /* their ip */ } return 14 + 28; } // --- ICMP echo (RFC 792) ------------------------------------------------------ static int icmp(const unsigned char *in, int len, unsigned char *out) { const unsigned char *ip = in + 14; int ihl, iplen, icmplen; if (len < 14 + 20) return 0; if ((ip[0] >> 4) != 4) return 0; ihl = (ip[0] & 0xf) * 4; if (ihl < 20 || len < 14 + ihl + 8) return 0; if (ip[9] != 1) return 0; /* not ICMP */ if (rd32(ip + 16) != MY_IP) return 0; /* not for us */ if (ip[ihl] != 8) return 0; /* not an echo */ iplen = rd16(ip + 2); if (iplen > len - 14 || iplen < ihl + 8) return 0; icmplen = iplen - ihl; eth_header(out, in + 6, 0x0800); { unsigned char *o = out + 14; int i; // The reply is the request with the addresses swapped and the type // changed, which is what RFC 792 asks for -- the payload has to come // back byte for byte so the sender can match it. for (i = 0; i < iplen; i++) o[i] = ip[i]; wr32(o + 12, MY_IP); wr32(o + 16, rd32(ip + 12)); o[10] = o[11] = 0; wr16(o + 10, csum(o, ihl, 0)); o[ihl] = 0; /* echo reply */ o[ihl + 2] = o[ihl + 3] = 0; wr16(o + ihl + 2, csum(o + ihl, icmplen, 0)); } return 14 + iplen; } // --- DHCP (RFC 2131) ---------------------------------------------------------- // // Only the two exchanges lwIP needs: DISCOVER answered with OFFER, REQUEST // answered with ACK. Everything else about DHCP -- leases expiring, renewal, // duplicate detection -- is real and is not what this program is for. static int dhcp(const unsigned char *in, int len, unsigned char *out) { const unsigned char *ip = in + 14; int ihl = (len >= 14 + 20) ? (ip[0] & 0xf) * 4 : 0; const unsigned char *udp, *bp; int msgtype = 0, i, blen; unsigned char *o, *b; if (ihl < 20 || len < 14 + ihl + 8) return 0; if (ip[9] != 17) return 0; /* not UDP */ udp = ip + ihl; if (rd16(udp) != 68 || rd16(udp + 2) != 67) return 0; /* not to a server */ bp = udp + 8; if (len < (int)(bp - in) + 240) return 0; if (rd32(bp + 236) != 0x63825363u) return 0; /* the magic cookie */ // Walk the options for the message type. TLV, terminated by 255. for (i = 240; (int)(bp - in) + i < len; ) { unsigned char t = bp[i]; if (t == 255) break; if (t == 0) { i++; continue; } if ((int)(bp - in) + i + 2 > len) break; if (t == 53) msgtype = bp[i + 2]; i += 2 + bp[i + 1]; } if (msgtype != 1 && msgtype != 3) return 0; /* DISCOVER/REQUEST */ // Broadcast the reply. A client with no address yet cannot be reached any // other way, which is exactly why DHCP is built on broadcast. { static const unsigned char bcast[6] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; eth_header(out, bcast, 0x0800); } o = out + 14; b = o + 20 + 8; for (i = 0; i < 240; i++) b[i] = 0; b[0] = 2; /* BOOTREPLY */ b[1] = 1; b[2] = 6; for (i = 0; i < 4; i++) b[4 + i] = bp[4 + i]; /* xid, echoed */ wr32(b + 16, PEER_IP); /* yiaddr */ wr32(b + 20, MY_IP); /* siaddr */ for (i = 0; i < 6; i++) b[28 + i] = bp[28 + i]; /* chaddr */ wr32(b + 236, 0x63825363u); blen = 240; b[blen++] = 53; b[blen++] = 1; b[blen++] = (msgtype == 1) ? 2 : 5; b[blen++] = 54; b[blen++] = 4; wr32(b + blen, MY_IP); blen += 4; b[blen++] = 51; b[blen++] = 4; wr32(b + blen, 86400); blen += 4; b[blen++] = 1; b[blen++] = 4; wr32(b + blen, NETMASK); blen += 4; b[blen++] = 3; b[blen++] = 4; wr32(b + blen, MY_IP); blen += 4; b[blen++] = 255; while (blen < 300) b[blen++] = 0; /* pad to a sane size */ wr16(o + 20, 67); wr16(o + 20 + 2, 68); wr16(o + 20 + 4, (unsigned short)(8 + blen)); o[20 + 6] = o[20 + 7] = 0; /* UDP checksum: optional over IPv4 */ o[0] = 0x45; o[1] = 0; wr16(o + 2, (unsigned short)(20 + 8 + blen)); wr16(o + 4, 0); wr16(o + 6, 0); o[8] = 64; o[9] = 17; o[10] = o[11] = 0; wr32(o + 12, MY_IP); wr32(o + 16, 0xFFFFFFFFu); wr16(o + 10, csum(o, 20, 0)); return 14 + 20 + 8 + blen; } // --- main --------------------------------------------------------------------- int main(void) { struct netuser_create c; unsigned char in[NETUSER_MTU], out[NETUSER_MTU]; int fd, i, n; long arps = 0, pings = 0, dhcps = 0; fd = open("/dev/netdev", O_RDWR); if (fd < 0) { puts_("netecho: cannot open /dev/netdev\n"); exit(); } for (i = 0; i < (int)sizeof(c.name); i++) c.name[i] = 0; for (i = 0; i < 6; i++) c.mac[i] = my_mac[i]; c.pad[0] = c.pad[1] = 0; c.name[0] = 'l'; c.name[1] = 'd'; c.name[2] = 'l'; c.name[3] = 's'; if (ioctl(fd, NETUSERIOC_CREATE, &c) < 0) { puts_("netecho: /dev/netdev is already somebody else's\n"); exit(); } puts_("netecho: this process is now an interface. I am "); putip(MY_IP); puts_(" and will offer "); putip(PEER_IP); puts_("\n"); for (;;) { n = read(fd, (char *)in, sizeof(in)); if (n <= 0) { // The kernel took the interface back, which happens when the // descriptor is released -- there is nothing left to do. puts_("netecho: the interface went away\n"); exit(); } if (n < 14) continue; { unsigned short type = rd16(in + 12); int r = 0; if (type == 0x0806) { r = arp(in, n, out); if (r) { arps++; if (arps == 1) puts_("netecho: answered an ARP\n"); } } else if (type == 0x0800) { r = dhcp(in, n, out); if (r) { dhcps++; puts_("netecho: answered DHCP, offering an address\n"); } if (!r) { r = icmp(in, n, out); if (r) { pings++; if (pings == 1) puts_("netecho: answered a ping\n"); } } } if (r > 0) write(fd, (char *)out, r); } } }