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:
+1
-1
File diff suppressed because one or more lines are too long
@@ -837,8 +837,6 @@ export class NetworkRuntimeEngine {
|
|||||||
private nextArpLookupId = 1;
|
private nextArpLookupId = 1;
|
||||||
/** Per RIP router: its random for the beacon's pauses and its sockets (RIPBeacon, RIPServer). */
|
/** 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 }>();
|
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. */
|
/** DNS queries waiting for their answer, by the socket they were sent from. */
|
||||||
private dnsQueriesBySocketId = new Map<Id, (response: DnsMessage | undefined) => void>();
|
private dnsQueriesBySocketId = new Map<Id, (response: DnsMessage | undefined) => void>();
|
||||||
/** Per node and kind (`nodeId/udp`, `nodeId/icmp`, …). */
|
/** 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
|
* Java's GatewayFirmware.starten: each home router's port forwarding
|
||||||
* becomes its static entries (rows Java's input check rejects are left
|
* becomes its static entries (rows Java's input check rejects are left
|
||||||
* out). Dynamic entries expire as Java's timer finds them, checked every
|
* out). Dynamic entries expire exactly at their age (natEntryExpired).
|
||||||
* second from the start (natEntryExpired).
|
|
||||||
*/
|
*/
|
||||||
private initializeNatState(): void {
|
private initializeNatState(): void {
|
||||||
const state = this.stateValue;
|
const state = this.stateValue;
|
||||||
state.natTablesByNodeId.clear();
|
state.natTablesByNodeId.clear();
|
||||||
this.natStartedAtMilliseconds = state.currentTimeMilliseconds;
|
|
||||||
for (const node of state.topologySnapshot.nodes) {
|
for (const node of state.topologySnapshot.nodes) {
|
||||||
if (node.kind !== 'gateway') continue;
|
if (node.kind !== 'gateway') continue;
|
||||||
const entries: NatEntry[] = [];
|
const entries: NatEntry[] = [];
|
||||||
@@ -3368,16 +3364,14 @@ export class NetworkRuntimeEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GatewayFirmware.checkNAT: every second from a second after the start,
|
* GatewayFirmware.checkNAT: a dynamic entry whose last update is 300 s
|
||||||
* dynamic entries whose last update is 300 s (times Java's delay factor)
|
* (times Java's delay factor) old is gone. Java finds it at its timer,
|
||||||
* old are removed — at the first such check.
|
* 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 {
|
private natEntryExpired(entry: NatEntry, nowMilliseconds: number): boolean {
|
||||||
if (entry.type === 'static') return false;
|
if (entry.type === 'static') return false;
|
||||||
const start = this.natStartedAtMilliseconds;
|
return nowMilliseconds - entry.updatedAtMilliseconds >= NAT_RETENTION_MILLISECONDS * this.runtimeSpeed.javaDelayFactor;
|
||||||
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 {
|
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 {
|
private ageSwitchSatEntries(nodeId: Id): void {
|
||||||
const state = this.stateValue;
|
const state = this.stateValue;
|
||||||
const retention =
|
const retention =
|
||||||
|
|||||||
@@ -2221,15 +2221,15 @@ describe('Home router NAT', () => {
|
|||||||
expect(engine.bindUdpSocket(gatewayId, { localPort: entry.port })).toBeUndefined();
|
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 });
|
const engine = home({ speed: 100 });
|
||||||
udpOut(engine);
|
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];
|
const [entry] = engine.natEntries(gatewayId) as [NatEntry];
|
||||||
expect(entry.updatedAtMilliseconds).toBe(15);
|
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);
|
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);
|
expect(engine.natEntries(gatewayId)).toHaveLength(0);
|
||||||
|
|
||||||
// Another, now refreshed by an answer, and not by what goes out.
|
// Another, now refreshed by an answer, and not by what goes out.
|
||||||
|
|||||||
@@ -415,7 +415,8 @@ Threads zwischen Karte und Ethernet-Schicht sitzen):
|
|||||||
LAN-Adresse von außen verschwindet.
|
LAN-Adresse von außen verschwindet.
|
||||||
* Dynamische Einträge verfallen nach 300 s mal Javas Verzögerungsfaktor
|
* Dynamische Einträge verfallen nach 300 s mal Javas Verzögerungsfaktor
|
||||||
(bei 90 % 55 Minuten), geprüft jede Sekunde, aufgefrischt nur von
|
(bei 90 % 55 Minuten), geprüft jede Sekunde, aufgefrischt nur von
|
||||||
eingehenden Paketen (aus dem Code, nicht gemessen).
|
eingehenden Paketen (aus dem Code, nicht gemessen). Der Kern verfällt sie
|
||||||
|
seit 01.10.2026 genau in diesem Alter statt beim nächsten Sekundentakt.
|
||||||
|
|
||||||
|
|
||||||
## WLAN zur Laufzeit
|
## WLAN zur Laufzeit
|
||||||
@@ -468,4 +469,6 @@ nach 5 s gehen dort nur noch Broadcasts.
|
|||||||
|
|
||||||
Javas Switch prüft seine Tabelle einmal je Sekunde ab dem Start
|
Javas Switch prüft seine Tabelle einmal je Sekunde ab dem Start
|
||||||
(`SwitchFirmware.checkSAT`), ein Eintrag verfällt also bis zu einer Sekunde
|
(`SwitchFirmware.checkSAT`), ein Eintrag verfällt also bis zu einer Sekunde
|
||||||
nach der Lernzeit; der Kern genau bei ihr. Noch nicht gemessen.
|
nach der Lernzeit; der Kern genau bei ihr. Das ist gewollt und wird nicht
|
||||||
|
gemessen: Wo Java nur in einem Takt nachsieht, macht der Kern es genauer
|
||||||
|
(Entscheidung 01.10.2026; dasselbe für die NAT-Einträge des Heimrouters).
|
||||||
|
|||||||
Reference in New Issue
Block a user