feat(cyw43): virtual DHCP/ARP net — WiFi reaches LINK_UP (isconnected)
The Pico W now connects end to end with NO backend: status reaches
CYW43_LINK_UP (3) and network.WLAN().isconnected() returns True.
After association the STA's lwIP broadcasts DHCP DISCOVER and ARPs the
gateway over the cyw43 DATA channel. A self-contained virtual network
(new virtualNet.ts) answers them:
- DHCP DISCOVER -> OFFER, REQUEST -> ACK (Ethernet+IPv4+UDP+BOOTP, valid
IPv4 header checksum, UDP checksum 0), leasing 192.168.4.2 with gateway
192.168.4.1.
- ARP who-has the gateway -> is-at the AP MAC.
On by default (Cyw43EmulatorOptions.virtualNet); pass null when an
external packet bridge owns the network.
Also fixes injectPacket to prepend the 4-byte BDC header that chip->host
DATA frames need (same as the event-frame fix), so injected packets parse.
Boot harness now reports: STEP_CONNECT_CALLED status=3 / POLL 0 status 3
conn True / HARNESS_DONE.
Known: receiving packets triggers ~30 rp2040js unaligned-read warnings
(lwIP reads the IPv4 header at ethernet offset 14); non-fatal here, but
the production RP2040Simulator must use a non-throwing logger.
This commit is contained in:
parent
74365d4ae9
commit
4d631b3a98
|
|
@ -0,0 +1,115 @@
|
|||
/**
|
||||
* cyw43-virtualnet.test.ts
|
||||
*
|
||||
* Unit tests for the self-contained virtual network (DHCP + ARP responder)
|
||||
* that lets a joined Pico W STA reach CYW43_LINK_UP with no backend.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DEFAULT_VNET, virtualNetReply } from '../simulation/cyw43/virtualNet';
|
||||
|
||||
const CLIENT_MAC = new Uint8Array([0x28, 0xcd, 0xc1, 0x01, 0x02, 0x03]);
|
||||
|
||||
function ip16(sum: number): number {
|
||||
while (sum >> 16) sum = (sum & 0xffff) + (sum >>> 16);
|
||||
return (~sum) & 0xffff;
|
||||
}
|
||||
|
||||
/** Minimal DHCP DISCOVER/REQUEST the way lwIP frames it. */
|
||||
function buildDhcpRequest(msgType: number): Uint8Array {
|
||||
const ETH = 14, IP = 20, UDP = 8;
|
||||
const dhcp = new Uint8Array(240 + 7);
|
||||
dhcp[0] = 1; // BOOTREQUEST
|
||||
dhcp[1] = 1; dhcp[2] = 6;
|
||||
dhcp.set([0x11, 0x22, 0x33, 0x44], 4); // xid
|
||||
dhcp.set(CLIENT_MAC, 28); // chaddr
|
||||
dhcp.set([0x63, 0x82, 0x53, 0x63], 236); // magic cookie
|
||||
dhcp.set([53, 1, msgType, 0xff], 240); // option 53 + end
|
||||
const total = ETH + IP + UDP + dhcp.length;
|
||||
const buf = new Uint8Array(total);
|
||||
const dv = new DataView(buf.buffer);
|
||||
for (let i = 0; i < 6; i++) buf[i] = 0xff; // dst broadcast
|
||||
buf.set(CLIENT_MAC, 6);
|
||||
dv.setUint16(12, 0x0800, false);
|
||||
buf[ETH] = 0x45;
|
||||
dv.setUint16(ETH + 2, IP + UDP + dhcp.length, false);
|
||||
buf[ETH + 9] = 17; // UDP
|
||||
let s = 0;
|
||||
for (let i = 0; i < IP; i += 2) s += dv.getUint16(ETH + i, false);
|
||||
dv.setUint16(ETH + 10, ip16(s), false);
|
||||
dv.setUint16(ETH + IP + 0, 68, false); // src port
|
||||
dv.setUint16(ETH + IP + 2, 67, false); // dst port (DHCP server)
|
||||
dv.setUint16(ETH + IP + 4, UDP + dhcp.length, false);
|
||||
buf.set(dhcp, ETH + IP + UDP);
|
||||
return buf;
|
||||
}
|
||||
|
||||
function dhcpReplyType(reply: Uint8Array): number {
|
||||
// reply = ETH(14)+IP(20)+UDP(8)+DHCP; option 53 is right after the magic cookie.
|
||||
const dhcp = 14 + 20 + 8;
|
||||
let i = dhcp + 236 + 4;
|
||||
while (i < reply.length) {
|
||||
if (reply[i] === 0xff) break;
|
||||
if (reply[i] === 0) { i++; continue; }
|
||||
if (reply[i] === 53) return reply[i + 2];
|
||||
i += 2 + reply[i + 1];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
describe('cyw43 virtual network', () => {
|
||||
it('answers DHCP DISCOVER with an OFFER carrying the client IP', () => {
|
||||
const reply = virtualNetReply(DEFAULT_VNET, buildDhcpRequest(1))!;
|
||||
expect(reply).not.toBeNull();
|
||||
expect(dhcpReplyType(reply)).toBe(2); // DHCPOFFER
|
||||
// yiaddr (offered address) sits at DHCP offset 16.
|
||||
const yi = reply.subarray(14 + 20 + 8 + 16, 14 + 20 + 8 + 20);
|
||||
expect(Array.from(yi)).toEqual([...DEFAULT_VNET.clientIp]);
|
||||
// Unicast back to the requesting STA.
|
||||
expect(Array.from(reply.subarray(0, 6))).toEqual([...CLIENT_MAC]);
|
||||
});
|
||||
|
||||
it('answers DHCP REQUEST with an ACK', () => {
|
||||
const reply = virtualNetReply(DEFAULT_VNET, buildDhcpRequest(3))!;
|
||||
expect(dhcpReplyType(reply)).toBe(5); // DHCPACK
|
||||
});
|
||||
|
||||
it('answers an ARP who-has the gateway with the AP MAC', () => {
|
||||
const a = 14;
|
||||
const req = new Uint8Array(a + 28);
|
||||
const dv = new DataView(req.buffer);
|
||||
for (let i = 0; i < 6; i++) req[i] = 0xff;
|
||||
req.set(CLIENT_MAC, 6);
|
||||
dv.setUint16(12, 0x0806, false); // ARP
|
||||
dv.setUint16(a + 0, 1, false); // htype
|
||||
dv.setUint16(a + 2, 0x0800, false);
|
||||
req[a + 4] = 6; req[a + 5] = 4;
|
||||
dv.setUint16(a + 6, 1, false); // request
|
||||
req.set(CLIENT_MAC, a + 8);
|
||||
req.set(DEFAULT_VNET.clientIp, a + 14);
|
||||
req.set(DEFAULT_VNET.serverIp, a + 24); // who-has the gateway
|
||||
|
||||
const reply = virtualNetReply(DEFAULT_VNET, req)!;
|
||||
expect(reply).not.toBeNull();
|
||||
expect(dv.getUint16(12, false)).toBe(0x0806);
|
||||
const rdv = new DataView(reply.buffer);
|
||||
expect(rdv.getUint16(a + 6, false)).toBe(2); // ARP reply
|
||||
// sender HW = AP MAC, sender IP = gateway
|
||||
expect(Array.from(reply.subarray(a + 8, a + 14))).toEqual([...DEFAULT_VNET.apMac]);
|
||||
expect(Array.from(reply.subarray(a + 14, a + 18))).toEqual([...DEFAULT_VNET.serverIp]);
|
||||
});
|
||||
|
||||
it('ignores non-DHCP UDP and ARP for other addresses', () => {
|
||||
// ARP for some other IP -> no reply.
|
||||
const a = 14;
|
||||
const req = new Uint8Array(a + 28);
|
||||
const dv = new DataView(req.buffer);
|
||||
dv.setUint16(12, 0x0806, false);
|
||||
dv.setUint16(a + 0, 1, false);
|
||||
dv.setUint16(a + 2, 0x0800, false);
|
||||
req[a + 4] = 6; req[a + 5] = 4;
|
||||
dv.setUint16(a + 6, 1, false);
|
||||
req.set([8, 8, 8, 8], a + 24); // not the gateway
|
||||
expect(virtualNetReply(DEFAULT_VNET, req)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -124,8 +124,18 @@ describe.skipIf(!process.env.CYW43_HARNESS)('Pico W cyw43 boot harness (investig
|
|||
const funcHist = [0, 0, 0, 0]; // count of decoded gSPI functions F0..F3
|
||||
const cmdCounts = new Map<string, number>();
|
||||
let ledOn = false;
|
||||
let packetsOut = 0;
|
||||
const packetLog: string[] = [];
|
||||
chip.onLed((e) => { ledOn = e.on; trace.push(`** LED ${e.on ? 'ON' : 'OFF'} **`); });
|
||||
chip.onConnect((e) => trace.push(`** WIFI CONNECT ssid=${e.ssid} **`));
|
||||
chip.onPacketOut((e) => {
|
||||
packetsOut++;
|
||||
if (packetLog.length < 12) {
|
||||
const et = e.ether.length >= 14 ? ((e.ether[12] << 8) | e.ether[13]) : 0;
|
||||
packetLog.push(`pkt ${e.ether.length}B ethertype=0x${et.toString(16)}` +
|
||||
(et === 0x0806 ? ' ARP' : et === 0x0800 ? ' IPv4' : ''));
|
||||
}
|
||||
});
|
||||
|
||||
const key = (c: Cyw43Cmd) =>
|
||||
`${c.write ? 'WR' : 'RD'} F${c.function} 0x${c.address.toString(16)} len=${c.length}`;
|
||||
|
|
@ -412,6 +422,7 @@ describe.skipIf(!process.env.CYW43_HARNESS)('Pico W cyw43 boot harness (investig
|
|||
`===== IOCTL ${chip.debugIoctlStats()} =====\n` +
|
||||
'===== IOCTL SEQUENCE =====\n' + chip.debugIoctlLog().map((s, i) => ` ${i}: ${s}`).join('\n') + '\n' +
|
||||
`===== restartsAtClm=${restartsAtClm} restartsFinal=${restartCount} rxPulled=${rxPulled} =====\n` +
|
||||
`===== packetsOut=${packetsOut} =====\n` + packetLog.map((s) => ` ${s}`).join('\n') + '\n' +
|
||||
`===== CPU FAULTS faultCount=${faultCount} =====\n` + faultLog.join('\n') + '\n' +
|
||||
'===== HOT PCs (busy-wait localization) =====\n' +
|
||||
[...pcHist.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12)
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ import {
|
|||
DEFAULT_STA_MAC,
|
||||
type VirtualAp,
|
||||
} from './virtual-ap';
|
||||
import { DEFAULT_VNET, virtualNetReply, type VirtualNetConfig } from './virtualNet';
|
||||
import type { Cyw43Cmd } from './PioBusSniffer';
|
||||
|
||||
// ── Public event surface ────────────────────────────────────────────
|
||||
|
|
@ -80,6 +81,12 @@ export interface Cyw43EmulatorOptions {
|
|||
staMac?: Uint8Array;
|
||||
/** Optional clock for tests. */
|
||||
now?: () => number;
|
||||
/**
|
||||
* Self-contained virtual network (DHCP/ARP responder). Defaults to on so a
|
||||
* joined STA gets an IP with no backend. Pass `null` when an external bridge
|
||||
* owns the network, or an object to override the addressing.
|
||||
*/
|
||||
virtualNet?: VirtualNetConfig | null;
|
||||
}
|
||||
|
||||
type Listener<T> = (ev: T) => void;
|
||||
|
|
@ -130,6 +137,7 @@ export class Cyw43Emulator {
|
|||
private currentSsid = '';
|
||||
private staMac: Uint8Array;
|
||||
private ap: VirtualAp;
|
||||
private virtualNet: VirtualNetConfig | null;
|
||||
private eventMask = new Uint8Array(32); // up to 256 event types
|
||||
private inboundEvents: Uint8Array[] = [];
|
||||
// Async events raised WHILE handling an IOCTL are deferred until just after
|
||||
|
|
@ -172,6 +180,7 @@ export class Cyw43Emulator {
|
|||
this.bootMs = this.now();
|
||||
this.ap = opts.ap ?? DEFAULT_AP;
|
||||
this.staMac = opts.staMac ?? DEFAULT_STA_MAC;
|
||||
this.virtualNet = opts.virtualNet === undefined ? DEFAULT_VNET : opts.virtualNet;
|
||||
// F2 ready at boot — driver tolerates this being true early.
|
||||
this.f0Regs[F0.F2_INFO >> 2] = 0x01;
|
||||
// Send an initial SDPCM control frame so the host's first F2 poll grants it
|
||||
|
|
@ -263,10 +272,15 @@ export class Cyw43Emulator {
|
|||
* receives bytes destined for the simulated STA's IP address.
|
||||
*/
|
||||
injectPacket(ether: Uint8Array): void {
|
||||
// Chip -> host DATA frames carry the same 4-byte BDC header as events: the
|
||||
// driver reads bdc at SDPCM header_length, then the ethernet at bdc + 4 +
|
||||
// (data_offset<<2). data_offset 0 => ethernet immediately follows.
|
||||
const bdc = new Uint8Array(4 + ether.length);
|
||||
bdc.set(ether, 4);
|
||||
const sdpcm = encodeSdpcm({
|
||||
channel: SdpcmChannel.DATA,
|
||||
sequence: this.chipToHostSeq++ & 0xff,
|
||||
payload: ether,
|
||||
payload: bdc,
|
||||
});
|
||||
this.pushFrame(sdpcm);
|
||||
}
|
||||
|
|
@ -477,11 +491,18 @@ export class Cyw43Emulator {
|
|||
if (channel === SdpcmChannel.CONTROL) {
|
||||
this.handleIoctl(payload);
|
||||
} else if (channel === SdpcmChannel.DATA) {
|
||||
// Outbound Ethernet frame. Strip BDC header (4 bytes) if present;
|
||||
// for the test harness we forward raw payload.
|
||||
// Outbound Ethernet frame. Strip the 4-byte BDC header the driver
|
||||
// prepends, then forward to any external bridge.
|
||||
const BDC = 4;
|
||||
const ether = payload.length >= BDC ? payload.subarray(BDC) : payload;
|
||||
const ether = payload.length >= BDC ? new Uint8Array(payload.subarray(BDC)) : payload;
|
||||
this.firePacketOut(ether);
|
||||
// Self-contained virtual network: answer DHCP / ARP so a freshly-joined
|
||||
// STA gets an IP and the link advances NOIP -> UP (isconnected == True).
|
||||
// Disabled when an external bridge owns the network.
|
||||
if (this.virtualNet) {
|
||||
const reply = virtualNetReply(this.virtualNet, ether);
|
||||
if (reply) this.injectPacket(reply);
|
||||
}
|
||||
}
|
||||
// Channel 1 (events) is chip → host only.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,218 @@
|
|||
/**
|
||||
* virtualNet.ts
|
||||
*
|
||||
* A tiny self-contained network for the CYW43 emulator. The real chip just
|
||||
* bridges Ethernet frames to/from the air; for a fully local emulation (no
|
||||
* backend) the virtual AP also has to answer the handful of L2/L3 exchanges a
|
||||
* freshly-joined STA needs before its `network.WLAN` reports a usable link:
|
||||
*
|
||||
* - DHCP (DISCOVER -> OFFER, REQUEST -> ACK) so lwIP gets an IP and the link
|
||||
* advances CYW43_LINK_NOIP -> CYW43_LINK_UP (isconnected() == True).
|
||||
* - ARP (who-has the gateway -> is-at) so the stack can resolve the router.
|
||||
*
|
||||
* Everything is built by hand (Ethernet + IPv4 + UDP + DHCP/BOOTP, or ARP) with
|
||||
* a correct IPv4 header checksum; the UDP checksum is left 0 (legal, and what
|
||||
* most embedded DHCP servers emit). All addresses are link-local to the virtual
|
||||
* AP and never leave the browser.
|
||||
*/
|
||||
|
||||
export interface VirtualNetConfig {
|
||||
serverIp: readonly [number, number, number, number]; // AP / DHCP server / gateway
|
||||
clientIp: readonly [number, number, number, number]; // address leased to the STA
|
||||
netmask: readonly [number, number, number, number];
|
||||
dnsIp: readonly [number, number, number, number];
|
||||
apMac: Uint8Array; // 6 bytes — the virtual AP's MAC
|
||||
leaseSecs: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_VNET: VirtualNetConfig = {
|
||||
serverIp: [192, 168, 4, 1],
|
||||
clientIp: [192, 168, 4, 2],
|
||||
netmask: [255, 255, 255, 0],
|
||||
dnsIp: [192, 168, 4, 1],
|
||||
apMac: new Uint8Array([0x02, 0x56, 0x45, 0x4c, 0x58, 0x00]), // locally-administered "VELX"
|
||||
leaseSecs: 86400,
|
||||
};
|
||||
|
||||
const ETH_IPV4 = 0x0800;
|
||||
const ETH_ARP = 0x0806;
|
||||
const IP_PROTO_UDP = 17;
|
||||
|
||||
function ip16(sum: number): number {
|
||||
while (sum >> 16) sum = (sum & 0xffff) + (sum >>> 16);
|
||||
return (~sum) & 0xffff;
|
||||
}
|
||||
|
||||
/** Build an Ethernet+IPv4+UDP frame carrying `udpPayload` (server -> client). */
|
||||
function buildUdp(
|
||||
cfg: VirtualNetConfig,
|
||||
dstMac: Uint8Array,
|
||||
srcPort: number,
|
||||
dstPort: number,
|
||||
udpPayload: Uint8Array,
|
||||
): Uint8Array {
|
||||
const ETH = 14;
|
||||
const IP = 20;
|
||||
const UDP = 8;
|
||||
const total = ETH + IP + UDP + udpPayload.length;
|
||||
const buf = new Uint8Array(total);
|
||||
const dv = new DataView(buf.buffer);
|
||||
|
||||
// Ethernet
|
||||
buf.set(dstMac.subarray(0, 6), 0);
|
||||
buf.set(cfg.apMac.subarray(0, 6), 6);
|
||||
dv.setUint16(12, ETH_IPV4, false);
|
||||
|
||||
// IPv4
|
||||
let o = ETH;
|
||||
buf[o] = 0x45; // version 4, IHL 5
|
||||
buf[o + 1] = 0; // DSCP/ECN
|
||||
dv.setUint16(o + 2, IP + UDP + udpPayload.length, false); // total length
|
||||
dv.setUint16(o + 4, 0, false); // id
|
||||
dv.setUint16(o + 6, 0, false); // flags/frag
|
||||
buf[o + 8] = 64; // TTL
|
||||
buf[o + 9] = IP_PROTO_UDP;
|
||||
dv.setUint16(o + 10, 0, false); // checksum (filled below)
|
||||
buf.set(cfg.serverIp, o + 12);
|
||||
buf.set(dstMac === BROADCAST_MAC ? [255, 255, 255, 255] : cfg.clientIp, o + 16);
|
||||
// IPv4 header checksum
|
||||
let sum = 0;
|
||||
for (let i = 0; i < IP; i += 2) sum += dv.getUint16(o + i, false);
|
||||
dv.setUint16(o + 10, ip16(sum), false);
|
||||
|
||||
// UDP (checksum 0 = not computed, legal for IPv4)
|
||||
o += IP;
|
||||
dv.setUint16(o + 0, srcPort, false);
|
||||
dv.setUint16(o + 2, dstPort, false);
|
||||
dv.setUint16(o + 4, UDP + udpPayload.length, false);
|
||||
dv.setUint16(o + 6, 0, false);
|
||||
buf.set(udpPayload, o + UDP);
|
||||
return buf;
|
||||
}
|
||||
|
||||
const BROADCAST_MAC = new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);
|
||||
const DHCP_MAGIC = [0x63, 0x82, 0x53, 0x63];
|
||||
|
||||
interface DhcpReq {
|
||||
xid: Uint8Array; // 4 bytes
|
||||
chaddr: Uint8Array; // 6 bytes (client MAC)
|
||||
msgType: number; // 1 = DISCOVER, 3 = REQUEST
|
||||
broadcast: boolean; // client wants the reply broadcast
|
||||
}
|
||||
|
||||
/** Parse a DHCP request out of an Ethernet frame; null if it isn't one. */
|
||||
function parseDhcp(ether: Uint8Array): DhcpReq | null {
|
||||
if (ether.length < 14 + 20 + 8 + 240) return null;
|
||||
const dv = new DataView(ether.buffer, ether.byteOffset, ether.byteLength);
|
||||
if (dv.getUint16(12, false) !== ETH_IPV4) return null;
|
||||
const ihl = (ether[14] & 0x0f) * 4;
|
||||
if (ether[14 + 9] !== IP_PROTO_UDP) return null;
|
||||
const udp = 14 + ihl;
|
||||
const dstPort = dv.getUint16(udp + 2, false);
|
||||
if (dstPort !== 67) return null; // DHCP server port
|
||||
const dhcp = udp + 8;
|
||||
if (ether[dhcp] !== 1) return null; // BOOTREQUEST
|
||||
const flags = dv.getUint16(dhcp + 10, false);
|
||||
const xid = ether.slice(dhcp + 4, dhcp + 8);
|
||||
const chaddr = ether.slice(dhcp + 28, dhcp + 34);
|
||||
// Verify + skip the magic cookie, then walk options for type 53 (msg type).
|
||||
const optStart = dhcp + 236;
|
||||
if (
|
||||
ether[optStart] !== DHCP_MAGIC[0] || ether[optStart + 1] !== DHCP_MAGIC[1] ||
|
||||
ether[optStart + 2] !== DHCP_MAGIC[2] || ether[optStart + 3] !== DHCP_MAGIC[3]
|
||||
) return null;
|
||||
let msgType = 0;
|
||||
let i = optStart + 4;
|
||||
while (i < ether.length) {
|
||||
const opt = ether[i];
|
||||
if (opt === 0xff) break; // end
|
||||
if (opt === 0x00) { i++; continue; } // pad
|
||||
const len = ether[i + 1];
|
||||
if (opt === 53 && len >= 1) msgType = ether[i + 2];
|
||||
i += 2 + len;
|
||||
}
|
||||
if (msgType !== 1 && msgType !== 3) return null; // only DISCOVER / REQUEST
|
||||
return { xid, chaddr, msgType, broadcast: (flags & 0x8000) !== 0 };
|
||||
}
|
||||
|
||||
/** Build the DHCP OFFER/ACK reply Ethernet frame for a parsed request. */
|
||||
function buildDhcpReply(cfg: VirtualNetConfig, req: DhcpReq): Uint8Array {
|
||||
// BOOTP fixed area (236) + magic (4) + options.
|
||||
const replyType = req.msgType === 1 ? 2 : 5; // DISCOVER->OFFER, REQUEST->ACK
|
||||
const fixed = new Uint8Array(236);
|
||||
const dv = new DataView(fixed.buffer);
|
||||
fixed[0] = 2; // BOOTREPLY
|
||||
fixed[1] = 1; // htype ethernet
|
||||
fixed[2] = 6; // hlen
|
||||
fixed.set(req.xid, 4);
|
||||
dv.setUint16(10, req.broadcast ? 0x8000 : 0, false); // flags
|
||||
fixed.set(cfg.clientIp, 16); // yiaddr (leased address)
|
||||
fixed.set(cfg.serverIp, 20); // siaddr (server)
|
||||
fixed.set(req.chaddr.subarray(0, 6), 28); // chaddr
|
||||
|
||||
const opts: number[] = [...DHCP_MAGIC];
|
||||
opts.push(53, 1, replyType); // DHCP message type
|
||||
opts.push(54, 4, ...cfg.serverIp); // server identifier
|
||||
opts.push(51, 4, ...le32be(cfg.leaseSecs)); // lease time
|
||||
opts.push(1, 4, ...cfg.netmask); // subnet mask
|
||||
opts.push(3, 4, ...cfg.serverIp); // router (gateway)
|
||||
opts.push(6, 4, ...cfg.dnsIp); // DNS server
|
||||
opts.push(0xff); // end
|
||||
while (opts.length < 60) opts.push(0); // pad to a sane minimum
|
||||
|
||||
const dhcp = new Uint8Array(fixed.length + opts.length);
|
||||
dhcp.set(fixed, 0);
|
||||
dhcp.set(opts, fixed.length);
|
||||
|
||||
const dstMac = req.broadcast ? BROADCAST_MAC : req.chaddr;
|
||||
return buildUdp(cfg, dstMac, 67, 68, dhcp);
|
||||
}
|
||||
|
||||
function le32be(v: number): number[] {
|
||||
return [(v >>> 24) & 0xff, (v >>> 16) & 0xff, (v >>> 8) & 0xff, v & 0xff];
|
||||
}
|
||||
|
||||
/** Parse an ARP request for the gateway; build the is-at reply, else null. */
|
||||
function buildArpReply(cfg: VirtualNetConfig, ether: Uint8Array): Uint8Array | null {
|
||||
if (ether.length < 14 + 28) return null;
|
||||
const dv = new DataView(ether.buffer, ether.byteOffset, ether.byteLength);
|
||||
if (dv.getUint16(12, false) !== ETH_ARP) return null;
|
||||
const a = 14;
|
||||
if (dv.getUint16(a + 0, false) !== 1) return null; // htype ethernet
|
||||
if (dv.getUint16(a + 2, false) !== ETH_IPV4) return null; // ptype IPv4
|
||||
if (dv.getUint16(a + 6, false) !== 1) return null; // opcode request
|
||||
const targetIp = ether.slice(a + 24, a + 28);
|
||||
// Only answer for the gateway/server address.
|
||||
if (!ipEq(targetIp, cfg.serverIp)) return null;
|
||||
const senderMac = ether.slice(a + 8, a + 14);
|
||||
const senderIp = ether.slice(a + 14, a + 18);
|
||||
|
||||
const out = new Uint8Array(14 + 28);
|
||||
const odv = new DataView(out.buffer);
|
||||
out.set(senderMac, 0); // dst = requester
|
||||
out.set(cfg.apMac.subarray(0, 6), 6); // src = AP
|
||||
odv.setUint16(12, ETH_ARP, false);
|
||||
odv.setUint16(a + 0, 1, false); // htype
|
||||
odv.setUint16(a + 2, ETH_IPV4, false);
|
||||
out[a + 4] = 6; out[a + 5] = 4;
|
||||
odv.setUint16(a + 6, 2, false); // opcode reply
|
||||
out.set(cfg.apMac.subarray(0, 6), a + 8); // sender HW = AP
|
||||
out.set(cfg.serverIp, a + 14); // sender IP = gateway
|
||||
out.set(senderMac, a + 18); // target HW = requester
|
||||
out.set(senderIp, a + 24); // target IP = requester
|
||||
return out;
|
||||
}
|
||||
|
||||
function ipEq(a: Uint8Array, b: readonly number[]): boolean {
|
||||
return a.length >= 4 && a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an Ethernet frame the STA transmitted, return the AP's reply frame to
|
||||
* inject back (DHCP OFFER/ACK or ARP is-at), or null if no reply is warranted.
|
||||
*/
|
||||
export function virtualNetReply(cfg: VirtualNetConfig, ether: Uint8Array): Uint8Array | null {
|
||||
const dhcpReq = parseDhcp(ether);
|
||||
if (dhcpReq) return buildDhcpReply(cfg, dhcpReq);
|
||||
return buildArpReply(cfg, ether);
|
||||
}
|
||||
Loading…
Reference in New Issue