SchOS/cmd/netecho/main.c
Nexoniarz f38eb8a7fd ldls: L4 was free after all, and the plan closes
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
2026-09-14 13:37:04 +02:00

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);
}
}
}