The trace settled it: our reply satisfies EVERY check fsplib makes --
fsp_pkt_read's checksum (0x9a computed, 0x9a present), len <= recv_len,
(reply.seq & 0xfff8) == s->seq, cmd match, and fsp_getpro's pos ==
FSP_PRO_BYTES with the flags byte at buf[len]. Yet the console retried the
same transaction (seq 0x8ccb -> 0x8ccc, txn 0x8cc8 try 3 -> try 4).
The decisive evidence is the key: fsplib calls client_set_key(rpkt->key) on any
ACCEPTED reply, so the next request would carry our 0x1234. Every request
still carries 0x8022. No reply has ever been accepted -- so they are not
arriving, and the problem was never the protocol.
What differs from the thing that worked: mini-dhcpd.py replies over AF_PACKET
and got a DHCP lease immediately, while this server used a normal UDP socket,
whose reply depends on the kernel having a route to the console, an ARP entry
for it, and choosing the right source address. (AF_PACKET also never populates
the kernel ARP table, so DHCP succeeding tells us nothing about those.)
--iface <dev> now replies with raw ethernet frames that MIRROR the request's
own addresses -- MACs swapped, IPs swapped, ports swapped -- which removes
routing, ARP and source-address selection in one go. The UDP path is kept for
when it is not needed.
Frame construction is tested: the reply carries the console's MAC as
destination and ours as source, source IP 10.0.0.1 and destination 10.0.0.64
taken from the request, ports 21 -> 4096, a valid IP checksum, correct UDP
length, and padding to the 60-byte ethernet minimum.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>