fix(core): NAT entries expire exactly at their age, not at Java's next one-second check
Java's GatewayFirmware.checkNAT and SwitchFirmware.checkSAT look every second from the start, so an entry goes up to a second late. The engine expires both exactly at their age: better than Java where Java only samples, as long as saved files stay compatible (decided 01.10.2026). The switch did so already; its departure is now named in the code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -837,8 +837,6 @@ 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`, …). */
|
||||
@@ -3331,13 +3329,11 @@ export class NetworkRuntimeEngine {
|
||||
/**
|
||||
* 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).
|
||||
* out). Dynamic entries expire exactly at their age (natEntryExpired).
|
||||
*/
|
||||
private initializeNatState(): void {
|
||||
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[] = [];
|
||||
@@ -3368,16 +3364,14 @@ export class NetworkRuntimeEngine {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* GatewayFirmware.checkNAT: a dynamic entry whose last update is 300 s
|
||||
* (times Java's delay factor) old is gone. Java finds it at its timer,
|
||||
* every second from the start, up to a second later; the engine at that
|
||||
* age (better than Java where it only samples: the user, 01.10.2026).
|
||||
*/
|
||||
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;
|
||||
return nowMilliseconds - entry.updatedAtMilliseconds >= NAT_RETENTION_MILLISECONDS * this.runtimeSpeed.javaDelayFactor;
|
||||
}
|
||||
|
||||
private expireNatEntries(nodeId: Id): void {
|
||||
@@ -4026,6 +4020,12 @@ export class NetworkRuntimeEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A learned address is gone when it is the retention time old. Java's
|
||||
* SwitchFirmware.checkSAT finds it at its timer, every second from the
|
||||
* start, up to a second later; the engine at that age (better than Java
|
||||
* where it only samples: the user, 01.10.2026).
|
||||
*/
|
||||
private ageSwitchSatEntries(nodeId: Id): void {
|
||||
const state = this.stateValue;
|
||||
const retention =
|
||||
|
||||
@@ -2221,15 +2221,15 @@ describe('Home router NAT', () => {
|
||||
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", () => {
|
||||
it('drops an entry when it is 300 s times the delay factor old, not at the next of Java\'s checks each second; 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.
|
||||
// At 100 %, the factor is 1: 300 s. The entry came at 15 ms (three cables of 5 ms); it is gone at 300.015 s (Java: at its check at 301 s).
|
||||
const [entry] = engine.natEntries(gatewayId) as [NatEntry];
|
||||
expect(entry.updatedAtMilliseconds).toBe(15);
|
||||
engine.handle({ type: 'advance', toMilliseconds: 300_999 });
|
||||
engine.handle({ type: 'advance', toMilliseconds: 300_014 });
|
||||
expect(engine.natEntries(gatewayId)).toHaveLength(1);
|
||||
engine.handle({ type: 'advance', toMilliseconds: 301_000 });
|
||||
engine.handle({ type: 'advance', toMilliseconds: 300_015 });
|
||||
expect(engine.natEntries(gatewayId)).toHaveLength(0);
|
||||
|
||||
// Another, now refreshed by an answer, and not by what goes out.
|
||||
|
||||
Reference in New Issue
Block a user