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:
@@ -17,6 +17,9 @@
|
||||
// one message per attempt, an ARP check that waits a round trip,
|
||||
// link-local after five failures; round trip times follow the speed)
|
||||
// processScheduledRuntimeEvent, dropForGlobalPacketLoss (lines ~4753–4850)
|
||||
// "Gateway NAT/PAT" (lines ~3585–3900; after Java's filius.software.nat,
|
||||
// measured with Java FILIUS: one entry per LAN endpoint, the expiry of
|
||||
// Java's GatewayFirmware.checkNAT)
|
||||
// A cable carries one frame after the other, as Java's SimplexVerbindung
|
||||
// (Swift: all at once; the option swiftCables keeps that for the comparison).
|
||||
// ARP after Java's holeARPTabellenEintrag: a lookup asks twice, a round trip
|
||||
@@ -43,7 +46,7 @@
|
||||
// advance the virtual clock inside the call, exactly as the Swift port does.
|
||||
//
|
||||
// Not ported yet — each place throws NotPortedError instead of guessing:
|
||||
// gateway NAT, remote links.
|
||||
// remote links.
|
||||
|
||||
import { compareIds, type Id } from '../ids.js';
|
||||
import { isPcClassEndpoint, stableGeneratedMacAddress } from '../model/node.js';
|
||||
@@ -51,6 +54,7 @@ import type {
|
||||
DeviceConfiguration,
|
||||
DhcpServerConfiguration,
|
||||
FirewallConfiguration,
|
||||
GatewayPortForwardingRow,
|
||||
FirewallRule,
|
||||
InterfaceConfiguration,
|
||||
InterfaceKey,
|
||||
@@ -172,8 +176,7 @@ export type ScheduledEventKind =
|
||||
readonly pairIdentifier: string;
|
||||
readonly latencyMilliseconds: number;
|
||||
readonly frame: EthernetFrame;
|
||||
}
|
||||
| { readonly kind: 'natExpirySweep' };
|
||||
};
|
||||
|
||||
export interface ScheduledEvent {
|
||||
readonly id: number;
|
||||
@@ -456,6 +459,117 @@ export const RIP_INFINITY = 16;
|
||||
export const RIP_TIMEOUT_MILLISECONDS = 75_000;
|
||||
export const RIP_FIRST_BEACON_DELAY_MILLISECONDS = 1_000;
|
||||
|
||||
/** GatewayFirmware.retentionTime: a dynamic NAT entry lasts 300 s, times Java's delay factor. */
|
||||
export const NAT_RETENTION_MILLISECONDS = 300_000;
|
||||
|
||||
export type NatProtocol = 'tcp' | 'udp' | 'icmp';
|
||||
|
||||
export interface NatEndpoint {
|
||||
readonly ipAddress: string;
|
||||
/** A port; for ICMP, the identifier. */
|
||||
readonly port: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* An entry of a home router's NAT table (Java NatEntry under its
|
||||
* PortProtocolPair): the WAN port and the remote end (empty for a static
|
||||
* entry of the port forwarding) lead to the LAN endpoint.
|
||||
*/
|
||||
export interface NatEntry {
|
||||
readonly protocol: NatProtocol;
|
||||
readonly remoteIPAddress: string;
|
||||
readonly port: number;
|
||||
readonly lan: NatEndpoint;
|
||||
/** Java NatType: StaticEntry, DynamicEntry, DynamicEnryFromStatic. */
|
||||
readonly type: 'static' | 'dynamic' | 'dynamicFromStatic';
|
||||
readonly updatedAtMilliseconds: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A port forwarding row as Java's GatewayFirmware.validateData takes it:
|
||||
* TCP or UDP in any case, or 6 or 17; ports 1…65535 without leading zeros;
|
||||
* an IPv4 address (EingabenUeberpruefung).
|
||||
*/
|
||||
function parsePortForwardingRow(
|
||||
row: GatewayPortForwardingRow,
|
||||
): { protocol: 'tcp' | 'udp'; publicPort: number; lanIPAddress: string; lanPort: number } | undefined {
|
||||
const protocolValue = row.protocolValue.toUpperCase();
|
||||
const protocol = protocolValue === 'TCP' || protocolValue === '6' ? 'tcp' : protocolValue === 'UDP' || protocolValue === '17' ? 'udp' : undefined;
|
||||
const port = (value: string) => (/^[1-9][0-9]{0,4}$/.test(value) && Number(value) <= 65_535 ? Number(value) : undefined);
|
||||
const address =
|
||||
/^(0*([1-9]?[0-9]?|1[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))\.((0*(1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))\.){2}(0*(1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))$/;
|
||||
const publicPort = port(row.publicPortValue);
|
||||
const lanPort = port(row.lanPortValue);
|
||||
if (protocol === undefined || publicPort === undefined || lanPort === undefined || !address.test(row.lanIPAddress)) return undefined;
|
||||
return { protocol, publicPort, lanIPAddress: row.lanIPAddress, lanPort };
|
||||
}
|
||||
|
||||
/** The endpoint a packet from the LAN leaves from, and whether it opens an entry (a TCP SYN, any UDP datagram, any ICMP packet). */
|
||||
function natOutboundEndpoint(packet: IPv4Packet): { protocol: NatProtocol; port: number; opens: boolean } {
|
||||
const payload = packet.payload;
|
||||
switch (payload.kind) {
|
||||
case 'tcp': {
|
||||
const flags = payload.segment.flags;
|
||||
return { protocol: 'tcp', port: payload.segment.sourcePort, opens: (flags & TCP_FLAG.synchronize) !== 0 && (flags & TCP_FLAG.acknowledgement) === 0 };
|
||||
}
|
||||
case 'udp':
|
||||
return { protocol: 'udp', port: payload.datagram.sourcePort, opens: true };
|
||||
case 'icmp':
|
||||
return { protocol: 'icmp', port: payload.message.identifier, opens: true };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NatGateway.replaceDestination's key: the remote end, the WAN port and the
|
||||
* protocol; for an ICMP error, those of the packet it carries (whose
|
||||
* receiver was the remote end and whose source port, or identifier, the WAN
|
||||
* port); for other ICMP, the sender and the identifier.
|
||||
*/
|
||||
function natInboundLookup(packet: IPv4Packet): { remoteIPAddress: string; port: number; protocol: NatProtocol } | undefined {
|
||||
const payload = packet.payload;
|
||||
switch (payload.kind) {
|
||||
case 'tcp':
|
||||
return { remoteIPAddress: packet.senderIPAddress, port: payload.segment.destinationPort, protocol: 'tcp' };
|
||||
case 'udp':
|
||||
return { remoteIPAddress: packet.senderIPAddress, port: payload.datagram.destinationPort, protocol: 'udp' };
|
||||
case 'icmp': {
|
||||
const embedded = payload.message.embeddedOriginalPacket;
|
||||
if (embedded === undefined) return { remoteIPAddress: packet.senderIPAddress, port: payload.message.identifier, protocol: 'icmp' };
|
||||
const inner = embedded.payload;
|
||||
const port = inner.kind === 'icmp' ? inner.message.identifier : inner.kind === 'tcp' ? inner.segment.sourcePort : inner.datagram.sourcePort;
|
||||
return { remoteIPAddress: embedded.receiverIPAddress, port, protocol: inner.kind };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** NatGateway.replaceSource: the WAN address; TCP and UDP the entry's port (ICMP keeps its identifier). */
|
||||
function natWithSource(packet: IPv4Packet, ipAddress: string, port: number | undefined): IPv4Packet {
|
||||
const payload = packet.payload;
|
||||
if (port === undefined || payload.kind === 'icmp') return { ...packet, senderIPAddress: ipAddress };
|
||||
return payload.kind === 'tcp'
|
||||
? { ...packet, senderIPAddress: ipAddress, payload: { kind: 'tcp', segment: { ...payload.segment, sourcePort: port } } }
|
||||
: { ...packet, senderIPAddress: ipAddress, payload: { kind: 'udp', datagram: { ...payload.datagram, sourcePort: port } } };
|
||||
}
|
||||
|
||||
/** NatGateway.replaceDestination: the LAN endpoint's address; TCP and UDP its port. */
|
||||
function natWithDestination(packet: IPv4Packet, endpoint: NatEndpoint): IPv4Packet {
|
||||
const payload = packet.payload;
|
||||
if (payload.kind === 'icmp') return { ...packet, receiverIPAddress: endpoint.ipAddress };
|
||||
return payload.kind === 'tcp'
|
||||
? { ...packet, receiverIPAddress: endpoint.ipAddress, payload: { kind: 'tcp', segment: { ...payload.segment, destinationPort: endpoint.port } } }
|
||||
: { ...packet, receiverIPAddress: endpoint.ipAddress, payload: { kind: 'udp', datagram: { ...payload.datagram, destinationPort: endpoint.port } } };
|
||||
}
|
||||
|
||||
/** VermittlungsProtokoll.isBroadcast: the limited broadcast, or the broadcast address of the sender's network. */
|
||||
function isBroadcastFrom(receiverIPAddress: string, senderIPAddress: string, subnetMask: string): boolean {
|
||||
if (receiverIPAddress === LIMITED_BROADCAST_IP_ADDRESS) return true;
|
||||
const receiver = parseIPv4(receiverIPAddress);
|
||||
const sender = parseIPv4(senderIPAddress);
|
||||
const mask = parseIPv4(subnetMask);
|
||||
if (receiver === undefined || sender === undefined || mask === undefined) return false;
|
||||
return maskAddress(receiver, mask) === maskAddress(sender, mask) && (receiver | mask) >>> 0 === 0xffff_ffff;
|
||||
}
|
||||
|
||||
/** RIPMessage: the sender's interface address, its first address, infinity, the timeout, its routes. */
|
||||
interface RipMessage {
|
||||
readonly ipAddress: string;
|
||||
@@ -564,6 +678,8 @@ export interface RuntimeState {
|
||||
dnsServerSocketIdsByNodeId: Map<Id, Id>;
|
||||
/** Each RIP router's table (RIPTable.routes): its networks, then what it learned. */
|
||||
ripTablesByNodeId: Map<Id, RouteRow[]>;
|
||||
/** Each home router's NAT table (Java NetworkAddressTranslationTable): static and dynamic entries. */
|
||||
natTablesByNodeId: Map<Id, NatEntry[]>;
|
||||
dhcpClientStatusesByNodeId: Map<Id, DhcpClientStatus>;
|
||||
dhcpClientContextsByNodeId: Map<Id, DhcpClientContext>;
|
||||
nextSocketSequenceNumber: number;
|
||||
@@ -603,6 +719,7 @@ function initialState(seed: bigint): RuntimeState {
|
||||
dhcpServerSocketIdsByNodeId: new Map(),
|
||||
dnsServerSocketIdsByNodeId: new Map(),
|
||||
ripTablesByNodeId: new Map(),
|
||||
natTablesByNodeId: new Map(),
|
||||
dhcpClientStatusesByNodeId: new Map(),
|
||||
dhcpClientContextsByNodeId: new Map(),
|
||||
nextSocketSequenceNumber: 1,
|
||||
@@ -713,6 +830,8 @@ export class NetworkRuntimeEngine {
|
||||
private nextArpLookupId = 1;
|
||||
/** Per RIP router: its random for the beacon's pauses and its sockets (RIPBeacon, RIPServer). */
|
||||
private ripRoutersByNodeId = new Map<Id, { readonly random: JavaCompatibleRandom; readonly beaconSocketId: Id; readonly serverSocketId: Id }>();
|
||||
/** When the NAT tables started: Java's expiry timer ticks every second from then. */
|
||||
private natStartedAtMilliseconds = 0;
|
||||
/** DNS queries waiting for their answer, by the socket they were sent from. */
|
||||
private dnsQueriesBySocketId = new Map<Id, (response: DnsMessage | undefined) => void>();
|
||||
/** Per node and kind (`nodeId/udp`, `nodeId/icmp`, …). */
|
||||
@@ -819,6 +938,7 @@ export class NetworkRuntimeEngine {
|
||||
state.dnsServerSocketIdsByNodeId.clear();
|
||||
state.ripTablesByNodeId.clear();
|
||||
this.ripRoutersByNodeId.clear();
|
||||
state.natTablesByNodeId.clear();
|
||||
state.firewallDecisions = [];
|
||||
return [this.output({ type: 'stopped' })];
|
||||
}
|
||||
@@ -1008,8 +1128,6 @@ export class NetworkRuntimeEngine {
|
||||
case 'remoteLinkDelivery':
|
||||
case 'lanRemoteLinkTransmission':
|
||||
throw new NotPortedError('remote links');
|
||||
case 'natExpirySweep':
|
||||
throw new NotPortedError('gateway NAT');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1080,7 +1198,7 @@ export class NetworkRuntimeEngine {
|
||||
}
|
||||
|
||||
private isUdpPortReserved(nodeId: Id, port: number): boolean {
|
||||
// Swift also counts a gateway's NAT mappings; gateways do not run here yet.
|
||||
if (this.isNatPort(nodeId, 'udp', port)) return true;
|
||||
for (const socket of this.stateValue.socketsById.values()) {
|
||||
if (socket.nodeId === nodeId && socket.protocol === 'udp' && socket.localPort === port) return true;
|
||||
}
|
||||
@@ -1643,6 +1761,7 @@ export class NetworkRuntimeEngine {
|
||||
|
||||
/** A port held by an open (not closed) TCP socket that is no listener's child. */
|
||||
isTcpPortReserved(nodeId: Id, port: number): boolean {
|
||||
if (this.isNatPort(nodeId, 'tcp', port)) return true;
|
||||
for (const socket of this.stateValue.socketsById.values()) {
|
||||
if (
|
||||
socket.nodeId === nodeId &&
|
||||
@@ -3177,13 +3296,208 @@ export class NetworkRuntimeEngine {
|
||||
this.stateValue.topologySnapshot = withSnapshot(this.stateValue.topologySnapshot, { deviceConfigurations, interfaceConfigurations });
|
||||
}
|
||||
|
||||
/**
|
||||
* Java's GatewayFirmware.starten: each home router's port forwarding
|
||||
* becomes its static entries (rows Java's input check rejects are left
|
||||
* out). Dynamic entries expire as Java's timer finds them, checked every
|
||||
* second from the start (natEntryExpired).
|
||||
*/
|
||||
private initializeNatState(): void {
|
||||
// Gateway NAT (F2): schedules the expiry sweep when a gateway exists.
|
||||
if (this.stateValue.topologySnapshot.nodes.some((node) => node.kind === 'gateway')) {
|
||||
throw new NotPortedError('gateway NAT');
|
||||
const state = this.stateValue;
|
||||
state.natTablesByNodeId.clear();
|
||||
this.natStartedAtMilliseconds = state.currentTimeMilliseconds;
|
||||
for (const node of state.topologySnapshot.nodes) {
|
||||
if (node.kind !== 'gateway') continue;
|
||||
const entries: NatEntry[] = [];
|
||||
for (const row of state.topologySnapshot.portForwardingRowsByNodeId.get(node.id) ?? []) {
|
||||
const forwarding = parsePortForwardingRow(row);
|
||||
if (forwarding === undefined) continue;
|
||||
// Java's HashMap: a second row for the same port and protocol replaces the first.
|
||||
const index = entries.findIndex((entry) => entry.protocol === forwarding.protocol && entry.port === forwarding.publicPort);
|
||||
const entry: NatEntry = {
|
||||
protocol: forwarding.protocol,
|
||||
remoteIPAddress: '',
|
||||
port: forwarding.publicPort,
|
||||
lan: { ipAddress: forwarding.lanIPAddress, port: forwarding.lanPort },
|
||||
type: 'static',
|
||||
updatedAtMilliseconds: state.currentTimeMilliseconds,
|
||||
};
|
||||
if (index >= 0) entries[index] = entry;
|
||||
else entries.push(entry);
|
||||
}
|
||||
state.natTablesByNodeId.set(node.id, entries);
|
||||
}
|
||||
}
|
||||
|
||||
/** The home router's NAT table as it stands: static entries, then the dynamic ones that have not expired. */
|
||||
natEntries(nodeId: Id): readonly NatEntry[] {
|
||||
this.expireNatEntries(nodeId);
|
||||
return this.stateValue.natTablesByNodeId.get(nodeId) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* GatewayFirmware.checkNAT: every second from a second after the start,
|
||||
* dynamic entries whose last update is 300 s (times Java's delay factor)
|
||||
* old are removed — at the first such check.
|
||||
*/
|
||||
private natEntryExpired(entry: NatEntry, nowMilliseconds: number): boolean {
|
||||
if (entry.type === 'static') return false;
|
||||
const start = this.natStartedAtMilliseconds;
|
||||
const retention = NAT_RETENTION_MILLISECONDS * this.runtimeSpeed.javaDelayFactor;
|
||||
const firstCheck = start + Math.max(1, Math.ceil((entry.updatedAtMilliseconds + retention - start) / 1_000)) * 1_000;
|
||||
return nowMilliseconds >= firstCheck;
|
||||
}
|
||||
|
||||
private expireNatEntries(nodeId: Id): void {
|
||||
const table = this.stateValue.natTablesByNodeId.get(nodeId);
|
||||
if (table === undefined) return;
|
||||
const now = this.stateValue.currentTimeMilliseconds;
|
||||
if (table.some((entry) => this.natEntryExpired(entry, now))) {
|
||||
this.stateValue.natTablesByNodeId.set(
|
||||
nodeId,
|
||||
table.filter((entry) => !this.natEntryExpired(entry, now)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** A port a dynamic entry of the home router holds (Java reserves it at the transport layer). */
|
||||
private isNatPort(nodeId: Id, protocol: 'tcp' | 'udp', port: number): boolean {
|
||||
return this.natEntries(nodeId).some((entry) => entry.type !== 'static' && entry.protocol === protocol && entry.port === port);
|
||||
}
|
||||
|
||||
/**
|
||||
* NetworkAddressTranslationTable.find: the dynamic entry for the remote end,
|
||||
* port and protocol (updated), else the static one for the port, which
|
||||
* then gets a dynamic entry for the remote end.
|
||||
*/
|
||||
private findNatEntry(nodeId: Id, port: number, remoteIPAddress: string, protocol: NatProtocol): NatEndpoint | undefined {
|
||||
const table = [...this.natEntries(nodeId)];
|
||||
const now = this.stateValue.currentTimeMilliseconds;
|
||||
const index = table.findIndex((entry) => entry.type !== 'static' && entry.protocol === protocol && entry.port === port && entry.remoteIPAddress === remoteIPAddress);
|
||||
if (index >= 0) {
|
||||
table[index] = { ...table[index]!, updatedAtMilliseconds: now };
|
||||
this.stateValue.natTablesByNodeId.set(nodeId, table);
|
||||
return table[index]!.lan;
|
||||
}
|
||||
const forwarding = table.find((entry) => entry.type === 'static' && entry.protocol === protocol && entry.port === port);
|
||||
if (forwarding === undefined) return undefined;
|
||||
this.putDynamicNatEntry(nodeId, { protocol, remoteIPAddress, port, lan: forwarding.lan, type: 'dynamicFromStatic', updatedAtMilliseconds: now });
|
||||
return forwarding.lan;
|
||||
}
|
||||
|
||||
/** Java's ConcurrentHashMap.put: an entry for the same remote end, port and protocol is replaced. */
|
||||
private putDynamicNatEntry(nodeId: Id, entry: NatEntry): void {
|
||||
const table = [...this.natEntries(nodeId)];
|
||||
const index = table.findIndex(
|
||||
(each) => each.type !== 'static' && each.protocol === entry.protocol && each.port === entry.port && each.remoteIPAddress === entry.remoteIPAddress,
|
||||
);
|
||||
if (index >= 0) table[index] = entry;
|
||||
else table.push(entry);
|
||||
this.stateValue.natTablesByNodeId.set(nodeId, table);
|
||||
}
|
||||
|
||||
/**
|
||||
* NetworkAddressTranslationTable.findPort: the WAN port of the LAN
|
||||
* endpoint's entries, 0 without one. (Java looks at the dynamic entries
|
||||
* first; an endpoint's entries all have the same port.)
|
||||
*/
|
||||
private natPortOf(nodeId: Id, lan: NatEndpoint, protocol: NatProtocol): number {
|
||||
return (
|
||||
this.natEntries(nodeId).find((entry) => entry.protocol === protocol && entry.lan.ipAddress === lan.ipAddress && entry.lan.port === lan.port)?.port ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* NatGateway.insertNewConnection: a LAN endpoint without an entry gets one
|
||||
* for the remote end, on a free port of the home router (ICMP: its
|
||||
* identifier).
|
||||
*
|
||||
* Java keeps that one entry for the endpoint's later remote ends too: they
|
||||
* get its WAN port, but their answers find no entry and never reach the LAN
|
||||
* (measured: udpTwoRemotes). The core adds an entry for each new remote end
|
||||
* on the same WAN port — unless a port forwarding serves the endpoint,
|
||||
* whose answers find their way already.
|
||||
*/
|
||||
private insertNatConnection(nodeId: Id, protocol: NatProtocol, lan: NatEndpoint, remoteIPAddress: string): void {
|
||||
const table = this.natEntries(nodeId);
|
||||
const now = this.stateValue.currentTimeMilliseconds;
|
||||
const ofEndpoint = table.filter((entry) => entry.protocol === protocol && entry.lan.ipAddress === lan.ipAddress && entry.lan.port === lan.port);
|
||||
if (ofEndpoint.length === 0) {
|
||||
const port = protocol === 'tcp' ? this.reserveFreeTcpPort(nodeId) : protocol === 'udp' ? this.reserveFreeUdpPort(nodeId) : lan.port;
|
||||
if (port === undefined) return;
|
||||
this.putDynamicNatEntry(nodeId, { protocol, remoteIPAddress, port, lan, type: 'dynamic', updatedAtMilliseconds: now });
|
||||
return;
|
||||
}
|
||||
if (ofEndpoint.some((entry) => entry.type === 'static' || entry.remoteIPAddress === remoteIPAddress)) return;
|
||||
this.putDynamicNatEntry(nodeId, { protocol, remoteIPAddress, port: this.natPortOf(nodeId, lan, protocol), lan, type: 'dynamic', updatedAtMilliseconds: now });
|
||||
}
|
||||
|
||||
/**
|
||||
* What Java's NatGatewayLANThread and NatGatewayWANThread do to a packet
|
||||
* before the home router's IP layer sees it: from the LAN, a packet to
|
||||
* elsewhere (not the LAN, not a broadcast, not the WAN address) gets the
|
||||
* WAN address and the entry's port; from the WAN, a packet to the WAN
|
||||
* address with an entry goes to its LAN endpoint, one to the LAN's
|
||||
* addresses is dropped, and an echo request to the WAN address is answered
|
||||
* by the router itself. Both sides pass the router's firewall first.
|
||||
* Undefined when the packet goes no further.
|
||||
*/
|
||||
private translateAtGateway(packet: IPv4Packet, frameIdentity: number | undefined, nodeId: Id, networkInterface: NetworkInterface): IPv4Packet | undefined {
|
||||
const interfaces = this.networkInterfaces(nodeId);
|
||||
const wan = interfaces.find((each) => each.index === 0);
|
||||
const lan = interfaces.find((each) => each.index === 1);
|
||||
if (wan === undefined || lan === undefined) return packet;
|
||||
const payload = packet.payload;
|
||||
const trace = (operation: TraceOperation, detail: string, after?: IPv4Packet) =>
|
||||
this.recordTrace({
|
||||
frameIdentity,
|
||||
packetIdentity: packet.identity,
|
||||
nodeId,
|
||||
interfaceId: networkInterface.portId,
|
||||
direction: networkInterface.portId === wan.portId ? 'inbound' : 'outbound',
|
||||
layer: 'network',
|
||||
operation,
|
||||
beforeHeaders: ipv4Headers(packet),
|
||||
...(after === undefined ? {} : { afterHeaders: ipv4Headers(after) }),
|
||||
detail,
|
||||
});
|
||||
|
||||
if (networkInterface.portId === wan.portId) {
|
||||
if (payload.kind === 'icmp' && payload.message.kind === 'echoRequest' && packet.receiverIPAddress === wan.ipAddress) return packet;
|
||||
if (sameSubnet(packet.receiverIPAddress, lan.ipAddress, lan.subnetMask)) {
|
||||
trace('dropped', 'WAN packet addressed to the LAN');
|
||||
return undefined;
|
||||
}
|
||||
if (!this.evaluateFirewall(packet, nodeId, networkInterface.portId, frameIdentity).accepted) return undefined;
|
||||
if (packet.receiverIPAddress !== wan.ipAddress) return packet;
|
||||
const lookup = natInboundLookup(packet);
|
||||
if (lookup === undefined) return packet;
|
||||
const endpoint = this.findNatEntry(nodeId, lookup.port, lookup.remoteIPAddress, lookup.protocol);
|
||||
if (endpoint === undefined) return packet;
|
||||
const translated = natWithDestination(packet, endpoint);
|
||||
trace('rewritten', 'NAT WAN to LAN', translated);
|
||||
return translated;
|
||||
}
|
||||
|
||||
if (networkInterface.portId !== lan.portId) return packet;
|
||||
if (!this.evaluateFirewall(packet, nodeId, networkInterface.portId, frameIdentity).accepted) return undefined;
|
||||
const outgoing =
|
||||
!sameSubnet(packet.receiverIPAddress, lan.ipAddress, lan.subnetMask) &&
|
||||
!isBroadcastFrom(packet.receiverIPAddress, packet.senderIPAddress, lan.subnetMask) &&
|
||||
packet.receiverIPAddress !== wan.ipAddress;
|
||||
if (!outgoing) return packet;
|
||||
if (packet.timeToLive <= 1) {
|
||||
this.emitIcmpError(nodeId, networkInterface, 'timeExceeded', packet);
|
||||
return undefined;
|
||||
}
|
||||
const endpoint = natOutboundEndpoint(packet);
|
||||
if (endpoint.opens) this.insertNatConnection(nodeId, endpoint.protocol, { ipAddress: packet.senderIPAddress, port: endpoint.port }, packet.receiverIPAddress);
|
||||
const port = endpoint.protocol === 'icmp' ? undefined : this.natPortOf(nodeId, { ipAddress: packet.senderIPAddress, port: endpoint.port }, endpoint.protocol);
|
||||
const translated = natWithSource(packet, wan.ipAddress, port);
|
||||
trace('rewritten', 'NAT LAN to WAN', translated);
|
||||
return translated;
|
||||
}
|
||||
|
||||
private isRipEnabledRouter(nodeId: Id): boolean {
|
||||
return (
|
||||
this.nodeSnapshot(nodeId)?.kind === 'router' &&
|
||||
@@ -4064,8 +4378,11 @@ export class NetworkRuntimeEngine {
|
||||
const node = this.nodeSnapshot(nodeId);
|
||||
if (node === undefined) return;
|
||||
|
||||
// Swift: the gateway branch (WAN/LAN, firewall, NAT rewrite) comes here.
|
||||
if (node.kind === 'gateway') throw new NotPortedError('gateway NAT');
|
||||
if (node.kind === 'gateway' && throughThread) {
|
||||
const translated = this.translateAtGateway(packet, frameIdentity, nodeId, networkInterface);
|
||||
if (translated === undefined) return;
|
||||
packet = translated;
|
||||
}
|
||||
if (node.kind === 'router') {
|
||||
const decision = this.evaluateFirewall(packet, nodeId, networkInterface.portId, frameIdentity);
|
||||
if (!decision.accepted) return;
|
||||
@@ -4113,7 +4430,8 @@ export class NetworkRuntimeEngine {
|
||||
if (!this.deliverIPv4Locally(packetForProcessing, nodeId, networkInterface, finished)) finished();
|
||||
return;
|
||||
}
|
||||
if (node.kind !== 'router') {
|
||||
// Java: routers and home routers forward (IpForwardingEnabled), nothing else.
|
||||
if (node.kind !== 'router' && node.kind !== 'gateway') {
|
||||
this.recordTrace({
|
||||
frameIdentity,
|
||||
packetIdentity: packetForProcessing.identity,
|
||||
|
||||
Reference in New Issue
Block a user