L4 was listed as "free -- H2 built all of this" and never actually tested,
which is a claim rather than a result. Tested now, and it holds:
ldlsd running ping 2/2; holds index 7 and DMA slots 3 and 4
killed interface DOWN, device released, bus mastering
cleared, DMA reclaimed -- back to vgpud's 3 blocks
ping fails honestly: sendto: -5
a second ldlsd ping 3/3, and the arena hands back the same addresses
pciuser_release_all() and netuser_release_all() sit beside each other in
exit() for the same reason, and that they apply unchanged to a foreign driver
is the whole point of having built them against our own.
With that, every milestone on ldls.md is done, so it leaves docs/plan/ for
docs/architecture/ -- the rule docs/plan/README.md states and the one that
keeps the two directories meaning different things. The new document describes
what is, not what was planned: the five rules, the four pieces, what it costs
(polling, no stack protector, locks that are no-ops), that it can be killed,
and a "what it does not do" that leads with one driver, one kernel version,
tried once.
Sixteen source files pointed at docs/plan/ldls.md and would have been dangling
the moment it moved. All repointed, and the milestone references -- which the
architecture doc deliberately does not have -- rewritten to name sections
instead, so a reader lands somewhere real.
The register row closes with it. hybrid.md freed a slot, ldls.md took it, and
it is now free again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
326 lines
11 KiB
C
326 lines
11 KiB
C
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// netecho -- a network, in a process.
|
|
//
|
|
// docs/architecture/ldls.md, "The class bridge". `/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
|
|
// 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);
|
|
}
|
|
}
|
|
}
|