feat(core): home router NAT after Java, measured with Java FILIUS

The core translates before the IP thread, as Java's NAT threads sit
between card and Ethernet: out, the WAN address and the entry's port (ICMP
keeps its identifier); in, by remote end, port and protocol, else by the
port forwarding, which then makes a dynamic entry; a ping at the WAN
address is answered by the router, packets from outside for the LAN are
dropped, both sides pass the router's firewall, TTL 1 going out gets time
exceeded. Dynamic entries expire at Java's check each second, 300 s times
the delay factor old (55 minutes at 90 %), refreshed only by what comes in.
The home router forwards as a router does.

Measured and not taken: when a LAN endpoint sends to a second remote end,
Java gives it the entry's WAN port but no entry, and the answer stays at
the router; the core adds an entry for each new remote end on that port.

The oracle now records every card of a node (the second card of routers
was missing); the ARP and RIP runs are recorded anew and still match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 20:06:04 +00:00
co-authored by Claude Opus 5.5
parent 5505e8e8af
commit 7a77bc1b43
23 changed files with 2915 additions and 748 deletions
+238
View File
@@ -39,6 +39,7 @@ import {
type EthernetFrame,
type EthernetPayload,
type FirewallConfiguration,
type NatEntry,
type IPv4Packet,
type NodeSnapshot,
type RuntimeOutput,
@@ -2147,3 +2148,240 @@ describe('RIP', () => {
expect(engine.state.ripTablesByNodeId.size).toBe(0);
});
});
describe('Home router NAT', () => {
const [lanPcId, lanPcPort, gatewayId, wanPort, lanPort, wanPcId, wanPcPort] = [0x1c01, 0x1c02, 0x1c03, 0x1c04, 0x1c05, 0x1c06, 0x1c07].map(uuid) as [
string,
string,
string,
string,
string,
string,
string,
];
/** PC 192.168.0.10 — home router (WAN 42.0.0.10/8, LAN 192.168.0.1/24) — PC 42.0.0.20, each on its own cable. */
const home = (options: { forwarding?: [string, string, string, string][]; firewall?: FirewallConfiguration; speed?: number } = {}) => {
const engine = new NetworkRuntimeEngine(14);
if (options.speed !== undefined) engine.setSimulationSpeed(options.speed);
engine.handle({
type: 'start',
snapshot: topologySnapshot({
nodes: [
pc(lanPcId, lanPcPort),
{ id: gatewayId, kind: 'gateway', ports: [portSnapshot(wanPort, 'wan0'), portSnapshot(lanPort, 'lan0')] },
pc(wanPcId, wanPcPort),
],
links: [link(lanPcId, lanPcPort, gatewayId, lanPort), link(gatewayId, wanPort, wanPcId, wanPcPort)],
deviceConfigurations: new Map([
[lanPcId, device('192.168.0.10', { gateway: '192.168.0.1' })],
[wanPcId, device('42.0.0.20', { mask: '255.0.0.0', gateway: '42.0.0.10' })],
]),
interfaceConfigurations: new Map([
[interfaceKey(gatewayId, wanPort), { ipAddress: '42.0.0.10', subnetMask: '255.0.0.0' }],
[interfaceKey(gatewayId, lanPort), { ipAddress: '192.168.0.1', subnetMask: '255.255.255.0' }],
]),
portForwardingRowsByNodeId: new Map([
[gatewayId, (options.forwarding ?? []).map(([protocolValue, publicPortValue, lanIPAddress, lanPortValue]) => ({ protocolValue, publicPortValue, lanIPAddress, lanPortValue }))],
]),
...(options.firewall === undefined ? {} : { firewallConfigurationsByNodeId: new Map([[gatewayId, options.firewall]]) }),
}),
seed: 14,
initialTimeMilliseconds: 0,
});
return engine;
};
const udpOut = (engine: NetworkRuntimeEngine, localPort = 5000) => {
engine.sendUdp(engine.bindUdpSocket(lanPcId, { localPort, remoteIPAddress: '42.0.0.20', remotePort: 7 })!, new TextEncoder().encode('out'));
letFramesArrive(engine);
};
it("takes the port forwarding rows Java's input check takes, a later row for the same port replacing an earlier one", () => {
const engine = home({
forwarding: [
['tcp', '8080', '192.168.0.10', '80'],
['17', '53', '192.168.0.10', '53'],
['6', '8080', '192.168.0.11', '81'],
['ICMP', '1', '192.168.0.10', '1'],
['TCP', '080', '192.168.0.10', '80'],
['TCP', '65536', '192.168.0.10', '80'],
['UDP', '9', '192.168.0.300', '9'],
],
});
expect(engine.natEntries(gatewayId).map((entry) => `${entry.protocol} ${entry.port} ${entry.lan.ipAddress}:${entry.lan.port} ${entry.type}`)).toEqual([
'tcp 8080 192.168.0.11:81 static',
'udp 53 192.168.0.10:53 static',
]);
});
it('holds the WAN port of an entry against the router’s own sockets', () => {
const engine = home();
udpOut(engine);
const [entry] = engine.natEntries(gatewayId) as [NatEntry];
expect(engine.bindUdpSocket(gatewayId, { localPort: entry.port })).toBeUndefined();
});
it("drops an entry at the first of Java's checks, each second from the start, when it is 300 s times the delay factor old; answers refresh it", () => {
const engine = home({ speed: 100 });
udpOut(engine);
// At 100 %, the factor is 1: 300 s. The entry came at 15 ms (three cables of 5 ms); it is gone at the check at 301 s.
const [entry] = engine.natEntries(gatewayId) as [NatEntry];
expect(entry.updatedAtMilliseconds).toBe(15);
engine.handle({ type: 'advance', toMilliseconds: 300_999 });
expect(engine.natEntries(gatewayId)).toHaveLength(1);
engine.handle({ type: 'advance', toMilliseconds: 301_000 });
expect(engine.natEntries(gatewayId)).toHaveLength(0);
// Another, now refreshed by an answer, and not by what goes out.
udpOut(engine, 5001);
const [fresh] = engine.natEntries(gatewayId) as [NatEntry];
engine.handle({ type: 'advance', toMilliseconds: 400_000 });
udpOut(engine, 5001);
expect(engine.natEntries(gatewayId)[0]?.updatedAtMilliseconds).toBe(fresh.updatedAtMilliseconds);
const answer = engine.bindUdpSocket(wanPcId, { localPort: 7, remoteIPAddress: '42.0.0.10', remotePort: fresh.port })!;
engine.sendUdp(answer, new TextEncoder().encode('back'));
letFramesArrive(engine);
expect(engine.natEntries(gatewayId)[0]!.updatedAtMilliseconds).toBeGreaterThan(400_000);
});
it('passes both sides through the router’s firewall', () => {
const engine = home({ firewall: firewallConfiguration({ isActive: true, defaultPolicy: 'drop' }) });
udpOut(engine);
expect(engine.natEntries(gatewayId)).toEqual([]);
expect(engine.state.firewallDecisions.some((decision) => !decision.accepted && decision.nodeId === gatewayId)).toBe(true);
});
it('answers a packet going out with TTL 1 with time exceeded, and drops what comes from the WAN for the LAN', () => {
const engine = home();
engine.sendIPv4Packet(lanPcId, {
identity: engine.allocatePacketIdentity(),
senderIPAddress: '192.168.0.10',
receiverIPAddress: '42.0.0.20',
timeToLive: 1,
protocol: 'udp',
payload: { kind: 'udp', datagram: { sourcePort: 5000, destinationPort: 7, payload: new Uint8Array() } },
});
letFramesArrive(engine);
expect((engine.state.icmpObservationsByNodeId.get(lanPcId) ?? []).map((observation) => observation.message.kind)).toEqual(['timeExceeded']);
expect(engine.natEntries(gatewayId)).toEqual([]);
engine.sendIPv4Packet(wanPcId, {
identity: engine.allocatePacketIdentity(),
senderIPAddress: '42.0.0.20',
receiverIPAddress: '192.168.0.10',
timeToLive: 64,
protocol: 'udp',
payload: { kind: 'udp', datagram: { sourcePort: 7, destinationPort: 5000, payload: new Uint8Array() } },
});
letFramesArrive(engine);
expect(engine.state.packetTraces.some((trace) => trace.nodeId === gatewayId && trace.detail === 'WAN packet addressed to the LAN')).toBe(true);
expect((engine.state.deliveredIPv4PacketsByNodeId.get(lanPcId) ?? []).filter((delivered) => delivered.packet.protocol === 'udp')).toEqual([]);
});
const raw = (engine: NetworkRuntimeEngine, from: string, packet: Omit<IPv4Packet, 'identity' | 'timeToLive'>) => {
const identity = engine.allocatePacketIdentity();
engine.sendIPv4Packet(from, { identity, timeToLive: 64, ...packet });
letFramesArrive(engine);
return identity;
};
const echo = (identifier: number) => ({ kind: 'icmp' as const, message: icmpMessage('echoRequest', { identifier, sequenceNumber: 1 }) });
it('keeps static entries, and dynamic ones for 300 s times the delay factor (55 minutes at 90 %)', () => {
const engine = home({ forwarding: [['UDP', '53', '192.168.0.10', '53']] });
udpOut(engine);
engine.handle({ type: 'advance', toMilliseconds: 3_300_000 });
expect(engine.natEntries(gatewayId).map((entry) => entry.type)).toEqual(['static', 'dynamic']);
engine.handle({ type: 'advance', toMilliseconds: 3_302_000 });
expect(engine.natEntries(gatewayId).map((entry) => entry.type)).toEqual(['static']);
});
it('opens an entry with a SYN, not with other TCP segments; they leave with port 0', () => {
const engine = home();
raw(engine, lanPcId, {
senderIPAddress: '192.168.0.10',
receiverIPAddress: '42.0.0.20',
protocol: 'tcp',
payload: { kind: 'tcp', segment: { sourcePort: 5000, destinationPort: 80, sequenceNumber: 1, acknowledgementNumber: 1, flags: TCP_FLAG.acknowledgement, payload: new Uint8Array() } },
});
expect(engine.natEntries(gatewayId)).toEqual([]);
const arrived = engine.state.packetTraces.find((trace) => trace.nodeId === wanPcId && trace.detail === 'TCP destination port closed')!;
expect(arrived.beforeHeaders.find((header) => header.name === 'sourcePort')?.value).toBe('0');
});
it('lets broadcasts from the LAN be', () => {
const engine = home();
engine.sendUdp(engine.bindUdpSocket(lanPcId, { localPort: 5000 })!, new TextEncoder().encode('all'), { ipAddress: '255.255.255.255', port: 7 });
letFramesArrive(engine);
expect(engine.natEntries(gatewayId)).toEqual([]);
expect(engine.state.icmpObservationsByNodeId.get(lanPcId) ?? []).toEqual([]);
});
it('replaces an entry for the same remote end, port and protocol (Java: a map), as two hosts pinging with one identifier do', () => {
const engine = home();
raw(engine, lanPcId, { senderIPAddress: '192.168.0.10', receiverIPAddress: '42.0.0.20', protocol: 'icmp', payload: echo(1) });
raw(engine, lanPcId, { senderIPAddress: '192.168.0.11', receiverIPAddress: '42.0.0.20', protocol: 'icmp', payload: echo(1) });
expect(engine.natEntries(gatewayId).map((entry) => `${entry.remoteIPAddress}:${entry.port} ${entry.lan.ipAddress}:${entry.lan.port}`)).toEqual([
'42.0.0.20:1 192.168.0.11:1',
]);
});
it('answers a ping at its WAN address itself, even when an entry would fit', () => {
const engine = home();
raw(engine, lanPcId, { senderIPAddress: '192.168.0.10', receiverIPAddress: '42.0.0.20', protocol: 'icmp', payload: echo(1) });
raw(engine, wanPcId, { senderIPAddress: '42.0.0.20', receiverIPAddress: '42.0.0.10', protocol: 'icmp', payload: echo(1) });
const replies = (engine.state.icmpObservationsByNodeId.get(wanPcId) ?? []).filter((observation) => observation.message.kind === 'echoReply');
expect(replies.map((observation) => observation.packet.senderIPAddress)).toContain('42.0.0.10');
expect(engine.state.packetTraces.filter((trace) => trace.nodeId === lanPcId && trace.detail === 'ICMP echo reply')).toHaveLength(0);
});
it('sends an ICMP error for a packet it translated to the LAN endpoint, found by the packet it carries', () => {
const engine = home();
udpOut(engine);
const [entry] = engine.natEntries(gatewayId) as [NatEntry];
raw(engine, wanPcId, {
senderIPAddress: '42.0.0.20',
receiverIPAddress: '42.0.0.10',
protocol: 'icmp',
payload: {
kind: 'icmp',
message: icmpMessage('destinationHostUnreachable', {
embeddedOriginalPacket: {
identity: 1,
senderIPAddress: '42.0.0.10',
receiverIPAddress: '42.0.0.20',
timeToLive: 63,
protocol: 'udp',
payload: { kind: 'udp', datagram: { sourcePort: entry.port, destinationPort: 7, payload: new Uint8Array() } },
},
}),
},
});
expect((engine.state.icmpObservationsByNodeId.get(lanPcId) ?? []).map((observation) => observation.message.kind)).toEqual(['destinationHostUnreachable']);
});
it('lets a forwarded endpoint reach a new remote end through its forwarding, whose answer makes the entry', () => {
const engine = home({ forwarding: [['UDP', '53', '192.168.0.10', '53']] });
udpOut(engine, 53);
const answer = engine.bindUdpSocket(wanPcId, { localPort: 7, remoteIPAddress: '42.0.0.10', remotePort: 53 })!;
engine.sendUdp(answer, new TextEncoder().encode('back'));
letFramesArrive(engine);
expect(engine.natEntries(gatewayId).map((entry) => `${entry.type} ${entry.remoteIPAddress}:${entry.port}`)).toEqual(['static :53', 'dynamicFromStatic 42.0.0.20:53']);
});
it('drops at its WAN side what its firewall drops', () => {
const engine = home({ firewall: firewallConfiguration({ isActive: true, defaultPolicy: 'drop' }) });
const identity = raw(engine, wanPcId, {
senderIPAddress: '42.0.0.20',
receiverIPAddress: '42.0.0.10',
protocol: 'udp',
payload: { kind: 'udp', datagram: { sourcePort: 7, destinationPort: 9, payload: new Uint8Array() } },
});
expect(engine.state.packetTraces.some((trace) => trace.nodeId === gatewayId && trace.packetIdentity === identity && trace.layer === 'transport')).toBe(false);
});
it('forgets its tables on stop', () => {
const engine = home();
udpOut(engine);
engine.handle({ type: 'stop' });
expect(engine.state.natTablesByNodeId.size).toBe(0);
});
});