From 313786e738a5709c9271526d2c6b7e8282dd6233 Mon Sep 17 00:00:00 2001 From: Julian Kiedaisch Date: Wed, 30 Sep 2026 18:46:32 +0000 Subject: [PATCH] feat(core): recursive DNS server after Java, measured with Java FILIUS The resolver runs on the events now (a query, its dnsQueryTimeout, its answer at the socket); the blocking resolveDns waits for it, and a recursive server uses it: its own records, else the zone's name server from its records, else its own DNS server, else a referral (DNSServer.answerWithRemoteData). DnsRun plays five stories on the new dns-recursive network. Measured and not taken: a server that is its zone's name server asks itself, which asks itself again, without end (the client times out, then every level floods its own DNS server); Java guards only the step through its own DNS server. The core asks itself in neither step. That story is compared up to the client's query (JAVA_UNTIL). Co-Authored-By: Claude Opus 5.5 --- CHANGES.md | 4 +- packages/core/src/runtime/engine.ts | 250 ++++++++++--- packages/core/test/dnsJava.test.ts | 109 ++++-- packages/core/test/engine.test.ts | 74 +++- .../dns-recursive-java-address.java.json | 114 ++++++ .../dns-recursive-java-referral.java.json | 206 +++++++++++ .../dns-recursive-java-unknown.java.json | 206 +++++++++++ ...-recursive-java-unknownInOwnZone.java.json | 173 +++++++++ ...dns-recursive-java-unknownInZone.java.json | 342 ++++++++++++++++++ .../scenarios/dns-recursive.scenario.json | 142 ++++++++ tools/java-oracle/README.md | 20 + tools/java-oracle/run.sh | 10 + tools/java-oracle/src/DnsRun.java | 11 + 13 files changed, 1570 insertions(+), 91 deletions(-) create mode 100644 packages/core/test/scenarios/dns-recursive-java-address.java.json create mode 100644 packages/core/test/scenarios/dns-recursive-java-referral.java.json create mode 100644 packages/core/test/scenarios/dns-recursive-java-unknown.java.json create mode 100644 packages/core/test/scenarios/dns-recursive-java-unknownInOwnZone.java.json create mode 100644 packages/core/test/scenarios/dns-recursive-java-unknownInZone.java.json create mode 100644 packages/core/test/scenarios/dns-recursive.scenario.json diff --git a/CHANGES.md b/CHANGES.md index 59cb9df..02275fa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,7 +13,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `src/model/link.ts`, `src/model/graph.ts` | `Model/TopologyLink.swift`, `Model/TopologyGraph.swift` | übertragen, bisher nur die Daten (Abfragen folgen mit dem Editor) | | `src/model/documentation.ts` | `Model/TopologyDocumentation.swift` | übertragen; FNV-Kennung mit BigInt, gegen die IDs aus dem Swift-Import geprüft | | `src/runtime/config.ts` | `State/TopologyEditorState.swift` (Laufzeit-Konfigurationen, WLAN eines Rechners, DHCP, Portweiterleitung, Remote Link ohne LAN-Transport, Programme und ihre Konfiguration, Firewall, DNS-Einträge aus `State/TopologyDNSResolver.swift`, E-Mail-Konten und -Nachrichten aus `State/TopologyEmail.swift`); dazu Echo-Server- und Client-Konfiguration, die Swift nicht speichert | übertragen; Java-Beschriftungen der Oberfläche weggelassen | -| `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4472–5187 (UDP), ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall, UDP-Sockets (binden, freie Ports nach Javas Zufall, senden samt Broadcast, zustellen, empfangen mit Wartezeit, schließen), TCP (Z. ~5187–5913: Server- und Client-Sockets, Handschlag mit Ergebnis, Segmente zu 1460 Byte bis zum PSH, höchstens drei Versuche, Abbau über alle Java-Zustände samt `TIME_WAIT`; seit F2.3c nach Javas `TCPSocket` an Java-Filius gemessen: je Versuch und für `TIME_WAIT` 3 RTT nach der Geschwindigkeit (8250 ms bei 90 %, Swift fest 7500 bzw. 2500 ms), der Zeitablauf zählt ab der Übergabe des Segments, vor ARP; ein Segment beendet `TIME_WAIT`; die Gegenseite schließt beim FIN von selbst; nicht bestätigte Daten schließen mit FIN statt sofort; der angenommene Socket hat die Startnummer des Listeners, der Listener eine neue. seit F2.3e wie Java: SYN+ACK und FIN gehen nie ein zweites Mal hinaus; der Server gibt einen Handschlag nach 3 RTT auf und schließt mit FIN; nach dem FIN wartet ein Socket höchstens fünfmal 3 RTT, jedes Segment beendet eine Wartezeit, dann CLOSED (Java lässt den Zustand stehen). Abweichend von Java: drei SYN statt einem, weil Javas Wiederholungsschleife nach dem ersten Zeitablauf abbricht, und ein wiederholtes SYN in `SYN_RCVD` bekommt das SYN+ACK noch einmal, statt den Socket zu schließen), DHCP (Z. ~5913–6694: Server mit festen Zuordnungen, Adressbereich, Angeboten, Leases und Sperrliste; Client seit F2.3b nach Javas `DHCPClient` neu gebaut und an Java-Filius gemessen: je Versuch eine Nachricht, eine Antwort, die nicht passt, wird gelesen und verworfen; die ARP-Prüfung wartet eine RTT, eine Antwort für die Adresse beendet sie sofort, dann DECLINE; nach einem Zeitablauf wiederholt der Zustand seine Nachricht, nach dem fünften nimmt der Client eine Link-Local-Adresse `169.254.1.x/16` (ARP-Probe, ARP-Reply an alle) und gibt keine alte Adresse zurück; Swift sendete doppelt, wartete nicht auf ARP und kehrte nach zehn Versuchen zur alten Adresse zurück. Abweichend von Java: Nach fünf belegten Link-Local-Adressen gibt der Client auf (Java sucht endlos), und ein ARP-Paket für eine andere Adresse beendet die Prüfung nicht. Seit F2.3d befördert ein Kabel einen Frame nach dem anderen wie Javas `SimplexVerbindung` (ein Frame wartet im Ausgangspuffer, bis der vorige angekommen ist, und erscheint im Protokoll, wenn er losgeht); Swift schickt alle zugleich, die Option `swiftCables` behält das für den Swift-Vergleich. Die RTT der DHCP-Zeiten (Warten, Angebote, Sperrliste) folgt wie in Java der Geschwindigkeit (50 Kabel, 2750 ms bei 90 %); Swift rechnet fest mit 2500 ms. ARP seit F2.4b nach Javas `holeARPTabellenEintrag`, an Java-Filius gemessen: eine Auflösung fragt, wartet eine RTT, fragt ein zweites Mal und gibt nach einer weiteren RTT auf; dann „Host unreachable" beim Absender selbst oder vom Router zurück (bei ICMP nur für Echo-Anfragen); ein wartender Absender wacht nach den übrigen Ereignissen desselben Zeitpunkts auf; der Ping wartet danach eine RTT auf die Antwort. Swift schickt in der Ereignisverarbeitung beide Anfragen zugleich und lässt das Paket ohne Ende warten, wenn die Adresse irgendwo im Netz vorkommt (Wissen, das kein Knoten hat), und blockiert direkte Aufrufe ohne Wartezeit; die Option `swiftArp` behält das für den Swift-Vergleich. Seit F2.4b-2 kehrt ein Senden wie in Java sofort zurück, die Frames reisen, während die Uhr läuft (Swift: der Aufruf kehrt erst zurück, wenn alle Frames angekommen sind; die Option `swiftSends` behält das für den Swift-Vergleich), und UDP und TCP senden über Javas Transport-Thread je Knoten und Protokoll: ein Segment nach dem anderen; solange IP auf ARP wartet, blockiert das nächste `senden`. Wartende Absender kommen in der Reihenfolge an die Reihe, in der sie kamen (Javas Sperre überlässt das dem Zufall). Seit F2.4b-3 geht jedes ankommende Paket wie in Java durch den IP-Thread (UDP, TCP) oder den ICMP-Thread (ICMP) seines Knotens: zustellen, Echo-Reply, weiterleiten, eins nach dem anderen; solange ARP wartet, warten die folgenden. Abweichend von Java weckt eine ARP-Antwort alle, die auf die Adresse warten (Javas `notify` weckt einen beliebigen). DNS seit F2.4a nach Javas `DNSServer`/`Resolver`, an Java-Filius gemessen: Server auf Port 53 mit Einträgen aus dem Modell (die Antwort wiederholt die Frage nicht, RCODE immer 0, ein Verweis mit NS und A im Antwortteil), iterativer Resolver mit 5 RTT je Anfrage, der nur die Server behält, an die er verwiesen wurde; noch ohne Rekursion. `receiveUdp` kehrt wie Javas `empfangen` zurück, sobald ein Datagramm da ist. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, DNS, RIP, Remote Link, WLAN | +| `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4472–5187 (UDP), ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall, UDP-Sockets (binden, freie Ports nach Javas Zufall, senden samt Broadcast, zustellen, empfangen mit Wartezeit, schließen), TCP (Z. ~5187–5913: Server- und Client-Sockets, Handschlag mit Ergebnis, Segmente zu 1460 Byte bis zum PSH, höchstens drei Versuche, Abbau über alle Java-Zustände samt `TIME_WAIT`; seit F2.3c nach Javas `TCPSocket` an Java-Filius gemessen: je Versuch und für `TIME_WAIT` 3 RTT nach der Geschwindigkeit (8250 ms bei 90 %, Swift fest 7500 bzw. 2500 ms), der Zeitablauf zählt ab der Übergabe des Segments, vor ARP; ein Segment beendet `TIME_WAIT`; die Gegenseite schließt beim FIN von selbst; nicht bestätigte Daten schließen mit FIN statt sofort; der angenommene Socket hat die Startnummer des Listeners, der Listener eine neue. seit F2.3e wie Java: SYN+ACK und FIN gehen nie ein zweites Mal hinaus; der Server gibt einen Handschlag nach 3 RTT auf und schließt mit FIN; nach dem FIN wartet ein Socket höchstens fünfmal 3 RTT, jedes Segment beendet eine Wartezeit, dann CLOSED (Java lässt den Zustand stehen). Abweichend von Java: drei SYN statt einem, weil Javas Wiederholungsschleife nach dem ersten Zeitablauf abbricht, und ein wiederholtes SYN in `SYN_RCVD` bekommt das SYN+ACK noch einmal, statt den Socket zu schließen), DHCP (Z. ~5913–6694: Server mit festen Zuordnungen, Adressbereich, Angeboten, Leases und Sperrliste; Client seit F2.3b nach Javas `DHCPClient` neu gebaut und an Java-Filius gemessen: je Versuch eine Nachricht, eine Antwort, die nicht passt, wird gelesen und verworfen; die ARP-Prüfung wartet eine RTT, eine Antwort für die Adresse beendet sie sofort, dann DECLINE; nach einem Zeitablauf wiederholt der Zustand seine Nachricht, nach dem fünften nimmt der Client eine Link-Local-Adresse `169.254.1.x/16` (ARP-Probe, ARP-Reply an alle) und gibt keine alte Adresse zurück; Swift sendete doppelt, wartete nicht auf ARP und kehrte nach zehn Versuchen zur alten Adresse zurück. Abweichend von Java: Nach fünf belegten Link-Local-Adressen gibt der Client auf (Java sucht endlos), und ein ARP-Paket für eine andere Adresse beendet die Prüfung nicht. Seit F2.3d befördert ein Kabel einen Frame nach dem anderen wie Javas `SimplexVerbindung` (ein Frame wartet im Ausgangspuffer, bis der vorige angekommen ist, und erscheint im Protokoll, wenn er losgeht); Swift schickt alle zugleich, die Option `swiftCables` behält das für den Swift-Vergleich. Die RTT der DHCP-Zeiten (Warten, Angebote, Sperrliste) folgt wie in Java der Geschwindigkeit (50 Kabel, 2750 ms bei 90 %); Swift rechnet fest mit 2500 ms. ARP seit F2.4b nach Javas `holeARPTabellenEintrag`, an Java-Filius gemessen: eine Auflösung fragt, wartet eine RTT, fragt ein zweites Mal und gibt nach einer weiteren RTT auf; dann „Host unreachable" beim Absender selbst oder vom Router zurück (bei ICMP nur für Echo-Anfragen); ein wartender Absender wacht nach den übrigen Ereignissen desselben Zeitpunkts auf; der Ping wartet danach eine RTT auf die Antwort. Swift schickt in der Ereignisverarbeitung beide Anfragen zugleich und lässt das Paket ohne Ende warten, wenn die Adresse irgendwo im Netz vorkommt (Wissen, das kein Knoten hat), und blockiert direkte Aufrufe ohne Wartezeit; die Option `swiftArp` behält das für den Swift-Vergleich. Seit F2.4b-2 kehrt ein Senden wie in Java sofort zurück, die Frames reisen, während die Uhr läuft (Swift: der Aufruf kehrt erst zurück, wenn alle Frames angekommen sind; die Option `swiftSends` behält das für den Swift-Vergleich), und UDP und TCP senden über Javas Transport-Thread je Knoten und Protokoll: ein Segment nach dem anderen; solange IP auf ARP wartet, blockiert das nächste `senden`. Wartende Absender kommen in der Reihenfolge an die Reihe, in der sie kamen (Javas Sperre überlässt das dem Zufall). Seit F2.4b-3 geht jedes ankommende Paket wie in Java durch den IP-Thread (UDP, TCP) oder den ICMP-Thread (ICMP) seines Knotens: zustellen, Echo-Reply, weiterleiten, eins nach dem anderen; solange ARP wartet, warten die folgenden. Abweichend von Java weckt eine ARP-Antwort alle, die auf die Adresse warten (Javas `notify` weckt einen beliebigen). DNS seit F2.4a nach Javas `DNSServer`/`Resolver`, an Java-Filius gemessen: Server auf Port 53 mit Einträgen aus dem Modell (die Antwort wiederholt die Frage nicht, RCODE immer 0, ein Verweis mit NS und A im Antwortteil), iterativer Resolver mit 5 RTT je Anfrage, der nur die Server behält, an die er verwiesen wurde; seit F2.4c auf den Ereignissen statt blockierend und mit rekursivem Server nach `DNSServer.answerWithRemoteData` (Nameserver der Zone, dann der eigene DNS-Server, sonst Verweis), abweichend von Java fragt er sich nie selbst (Java endlos). `receiveUdp` kehrt wie Javas `empfangen` zurück, sobald ein Datagramm da ist. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, RIP, Remote Link, WLAN | | `src/runtime/dns.ts` | Java `filius.software.dns.DNSNachricht`, `Query`, `ResourceRecord` | neu nach Java statt nach Swift (Swift ersetzt Javas Textnachrichten durch `FILIUS-DNS-TYPED/1`): Kopfzeile `ID=… QR=… RCODE=… QDCOUNT=… …`, Frage- und Eintragszeilen, Suche nach Namen ohne Rücksicht auf Groß- und Kleinschreibung, NS der längsten Zone, sonst der Wurzel | | `src/runtime/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen | | `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` | @@ -31,7 +31,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `test/oracle.test.ts`, `test/scenarios/` | — | neu: 14 Szenarien (davon 2 UDP, 3 TCP, 4 DHCP), Ergebnis mit dem Swift-Kern abgeglichen (`tools/swift-oracle`); Schritte für UDP, TCP und DHCP; die vier DHCP-Szenarien folgen seit F2.3b Java, die drei TCP-Szenarien seit F2.3c; sie müssen von Swift abweichen; drei weitere (`firewall-and-loss`, `routed-two-networks`, `switch-lan`) folgen seit F2.4b ab der Spur, an der ARP scheitert oder ein Ping unbeantwortet bleibt, Java und stimmen bis dahin mit Swift | | `test/dhcpJava.test.ts`, `test/scenarios/*.java.json`, `test/scenario.ts`, `test/javaRun.ts` | — | neu: die DHCP-Szenarien gegen Java-Filius (`tools/java-oracle`, `DhcpRun`): je Client dieselben Frames in derselben Reihenfolge, Abstände auf 200 ms, dieselben Adressen am Ende | | `test/arpJava.test.ts`, `test/scenarios/arp-routed.scenario.json`, `test/scenarios/arp-java-*.java.json` | — | neu: vier ARP-Abläufe gegen Java-Filius (`ArpRun`): Frames, Abstände und wann jeder Aufruf zurückkehrt; die zwei Pings stimmen seit F2.4b-1, die zwei UDP-Abläufe seit F2.4b-2 (Javas Transport-Thread), drei Abläufe durch den Router seit F2.4b-3 (IP- und ICMP-Thread) | -| `test/dnsJava.test.ts`, `test/scenarios/dns-lan.scenario.json`, `test/scenarios/dns-java-*.java.json` | — | neu: fünf DNS-Abfragen gegen Java-Filius (`DnsRun`): Frames, Ergebnis und Dauer; seit F2.4b-1 alle wie Java | +| `test/dnsJava.test.ts`, `test/scenarios/dns-lan.scenario.json`, `test/scenarios/dns-java-*.java.json` | — | neu: fünf DNS-Abfragen gegen Java-Filius (`DnsRun`): Frames, Ergebnis und Dauer; seit F2.4b-1 alle wie Java; seit F2.4c dazu fünf mit rekursivem Server (`dns-recursive.scenario.json`, `dns-recursive-java-*.java.json`), einer nur bis zur Anfrage des Clients verglichen (`JAVA_UNTIL`: Javas Server fragt sich selbst ohne Ende) | | `test/tcpJava.test.ts`, `test/scenarios/tcp-java-*.java.json` | — | neu: sechs TCP-Abläufe gegen Java-Filius (`TcpRun`, drei davon mit genau einem verlorenen Frame, `loseFirst`): Frames mit Flags und Nummern, Socket-Zustände; benannte Java-Fehler (`JAVA_BUGS`: nur ein SYN, der Zustand bleibt nach dem Schließen stehen), `lostSynAck` nur bis zum zweiten SYN verglichen (`JAVA_UNTIL`) | | `test/routeTable.test.ts` | `TopologyJavaRouteTableTests.swift` (8 von 12 Tests) | übertragen; Geometrie- und Reducer-Tests folgen mit Editor und Dokument | diff --git a/packages/core/src/runtime/engine.ts b/packages/core/src/runtime/engine.ts index e7167ef..2fc5bcf 100644 --- a/packages/core/src/runtime/engine.ts +++ b/packages/core/src/runtime/engine.ts @@ -29,7 +29,8 @@ // or forwards ICMP); each takes one unit after the other and holds the rest // while it waits for ARP. A socket's send blocks behind its thread. // DNS after Java (filius.software.dns: DNSServer, DNSServerMitarbeiter, -// Resolver, DNSQueryAgent), not Swift's typed format; recursion not yet. +// Resolver, DNSQueryAgent), not Swift's typed format; a recursive server +// asks on without asking itself (Java does, without end). // Java counterparts: filius.hardware.Verbindung, filius.software.netzzugangsschicht, // filius.software.vermittlungsschicht (ARP, IP, ICMP), system.SwitchFirmware, // filius.software.transportschicht (UDP, TCP), filius.software.dhcp. @@ -41,7 +42,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, RIP routing, remote links, recursive DNS resolution. +// gateway NAT, RIP routing, remote links. import { compareIds, type Id } from '../ids.js'; import { isPcClassEndpoint, stableGeneratedMacAddress } from '../model/node.js'; @@ -135,6 +136,8 @@ export type ScheduledEventKind = /** The lookups waiting for an address wake up, after what else happens at that time. */ | { readonly kind: 'arpAnswered'; readonly nodeId: Id; readonly ipAddress: string; readonly macAddress: string } | { readonly kind: 'tcpTimeout'; readonly sessionId: Id } + /** A DNS query waited five round trips for its answer (DNSQueryAgent). */ + | { readonly kind: 'dnsQueryTimeout'; readonly socketId: Id } | { /** A frame that waited in the port's output buffer leaves: the cable was busy. */ readonly kind: 'ethernetLinkDeparture'; @@ -322,6 +325,10 @@ export type TcpConnectionResult = 'connected' | 'invalidSocket' | 'unreachable' /** Java's DNSQueryAgent waits five round trips for an answer. */ export const DNS_QUERY_TIMEOUT_ROUND_TRIPS = 5; export type DnsResolution = { readonly type: 'answered'; readonly records: readonly DnsMessageRecord[] } | { readonly type: 'timedOut' }; +/** A resolution as it goes on the events: the answer and authority records apart, as Java keeps them. */ +type DnsLookup = + | { readonly type: 'answered'; readonly answers: readonly DnsMessageRecord[]; readonly authority: readonly DnsMessageRecord[] } + | { readonly type: 'timedOut' }; export type DnsAddressResolution = { readonly type: 'resolved'; readonly address: string | undefined } | { readonly type: 'timedOut' }; export const DHCP_SERVER_PORT = 67; @@ -622,6 +629,8 @@ export class NetworkRuntimeEngine { /** ARP lookups waiting for their answer (Java: a thread in holeARPTabellenEintrag). */ private arpLookups: ArpLookup[] = []; private nextArpLookupId = 1; + /** DNS queries waiting for their answer, by the socket they were sent from. */ + private dnsQueriesBySocketId = new Map void>(); /** Per node and kind (`nodeId/udp`, `nodeId/icmp`, …). */ private protocolThreads = new Map(); /** Per cable end (node and port): when the last frame sent there arrives. */ @@ -710,6 +719,7 @@ export class NetworkRuntimeEngine { this.isProcessingScheduledEvents = false; this.arpLookups = []; this.protocolThreads.clear(); + this.dnsQueriesBySocketId.clear(); state.pendingEvents = []; state.arpCachesByNodeId.clear(); state.switchForwardingTablesByNodeId.clear(); @@ -908,6 +918,9 @@ export class NetworkRuntimeEngine { case 'arpAnswered': this.completeArpLookups(kind.nodeId, kind.ipAddress, kind.macAddress); return; + case 'dnsQueryTimeout': + this.finishDnsQuery(kind.socketId, undefined); + return; case 'tcpTimeout': this.handleTcpTimeout(kind.sessionId); return; @@ -1233,6 +1246,11 @@ export class NetworkRuntimeEngine { }); if (datagram.destinationPort === DHCP_CLIENT_PORT && this.readDhcpResponse(nodeId)) this.advanceDhcpClient(nodeId); if (datagram.destinationPort === DNS_PORT) this.serveDnsQueries(nodeId); + for (const socket of matching) { + if (!this.dnsQueriesBySocketId.has(socket.id)) continue; + const received = this.receiveUdp(socket.id); + this.finishDnsQuery(socket.id, received === undefined ? undefined : decodeDnsMessage(new TextDecoder().decode(received.datagram.payload))); + } } // ── DNS (after Java's DNSServer and Resolver) ──────────────────────────── @@ -1242,39 +1260,103 @@ export class NetworkRuntimeEngine { const state = this.stateValue; state.dnsServerSocketIdsByNodeId.clear(); for (const [nodeId, configuration] of state.topologySnapshot.dnsServerConfigurationsByNodeId) { - if (configuration.recursiveResolutionEnabled) throw new NotPortedError('recursive DNS resolution'); const socketId = this.bindUdpSocket(nodeId, { localPort: DNS_PORT }); if (socketId !== undefined) state.dnsServerSocketIdsByNodeId.set(nodeId, socketId); } } - /** DNSServerMitarbeiter: each query answered to where it came from, with the query's ID. */ + /** + * DNSServerMitarbeiter: each query answered to where it came from, with the + * query's ID, once every question has its records (a recursive server may + * have to ask on). + */ private serveDnsQueries(nodeId: Id): void { const state = this.stateValue; const socketId = state.dnsServerSocketIdsByNodeId.get(nodeId); - const configuration = state.topologySnapshot.dnsServerConfigurationsByNodeId.get(nodeId); - if (socketId === undefined || configuration === undefined) return; + if (socketId === undefined || !state.topologySnapshot.dnsServerConfigurationsByNodeId.has(nodeId)) return; for (let received = this.receiveUdp(socketId); received !== undefined; received = this.receiveUdp(socketId)) { + const client = { ipAddress: received.senderIPAddress, port: received.datagram.sourcePort }; const query = decodeDnsMessage(new TextDecoder().decode(received.datagram.payload)); - const records = configuration.records.map(messageRecord); - const answers = query.questions.flatMap((question) => answerDnsQuestion(question, records)); - const response: DnsMessage = { - id: query.id, - queryResponse: DNS_RESPONSE, - responseCode: 0, - questions: [], - answers, - authority: [], - additional: [], - }; - this.recordTrace({ nodeId, direction: 'outbound', layer: 'application', operation: 'sent', detail: 'DNS response' }); - this.sendUdp(socketId, new TextEncoder().encode(encodeDnsMessage(response)), { - ipAddress: received.senderIPAddress, - port: received.datagram.sourcePort, + this.answerDnsQuestions(nodeId, query.questions, [], (answers) => { + const response: DnsMessage = { + id: query.id, + queryResponse: DNS_RESPONSE, + responseCode: 0, + questions: [], + answers, + authority: [], + additional: [], + }; + this.recordTrace({ nodeId, direction: 'outbound', layer: 'application', operation: 'sent', detail: 'DNS response' }); + this.sendUdp(socketId, new TextEncoder().encode(encodeDnsMessage(response)), client); }); } } + /** The questions one after the other, as the worker loops over them. */ + private answerDnsQuestions( + nodeId: Id, + questions: readonly DnsQuestion[], + answered: readonly DnsMessageRecord[], + done: (answers: DnsMessageRecord[]) => void, + ): void { + const [question, ...rest] = questions; + if (question === undefined) { + done([...answered]); + return; + } + this.answerDnsQuestion(nodeId, question, (answers) => this.answerDnsQuestions(nodeId, rest, [...answered, ...answers], done)); + } + + /** + * DNSServer.answer: the server's own records for the question; else, when + * it resolves recursively, what it learns from others; else a referral + * (the NS record of the question's zone and its address). + */ + private answerDnsQuestion(nodeId: Id, question: DnsQuestion, done: (answers: DnsMessageRecord[]) => void): void { + const configuration = this.stateValue.topologySnapshot.dnsServerConfigurationsByNodeId.get(nodeId); + const records = (configuration?.records ?? []).map(messageRecord); + const local = localDnsAnswers(question, records); + if (local.length > 0 || configuration?.recursiveResolutionEnabled !== true) { + done(local.length > 0 ? local : dnsReferral(question, records)); + return; + } + this.resolveDnsRemotely(nodeId, question, records, (remote) => done(remote.length > 0 ? remote : dnsReferral(question, records))); + } + + /** + * DNSServer.answerWithRemoteData: resolves with the name server its records + * name for the question's zone, then, if that brings nothing, with its own + * DNS server. The records of the answer and authority sections; nothing + * when a server does not answer. + * + * Java asks its own DNS server only when that is not itself, but asks the + * zone's name server even then: a query to itself, which asks itself again, + * without end (measured: the client times out, then every level falls back + * to its own DNS server). The core asks neither when it is itself. + */ + private resolveDnsRemotely(nodeId: Id, question: DnsQuestion, records: readonly DnsMessageRecord[], done: (answers: DnsMessageRecord[]) => void): void { + const ownAddresses = new Set(this.networkInterfaces(nodeId).map((each) => each.ipAddress)); + const resolveWith = (server: string | undefined, next: (answers: DnsMessageRecord[]) => void) => { + if (server === undefined || server.trim() === '' || ownAddresses.has(server)) { + next([]); + return; + } + this.beginDnsResolution(nodeId, question.name, question.type, server, (resolution) => + next(resolution.type === 'answered' ? [...resolution.answers, ...resolution.authority] : []), + ); + }; + const nameServer = findApplicableNameServerRecord(question.name, records); + const nameServerAddress = nameServer === undefined ? undefined : findDnsRecords(nameServer.data, 'A', records)[0]?.data; + resolveWith(nameServerAddress, (answers) => { + if (answers.length > 0) { + done(answers); + return; + } + resolveWith(this.stateValue.topologySnapshot.deviceConfigurations.get(nodeId)?.dnsServer, done); + }); + } + /** * Java's Resolver.resolve, blocking on the virtual clock: asks the node's * DNS server (or `serverIPAddress`), and follows the name servers it is @@ -1283,35 +1365,69 @@ export class NetworkRuntimeEngine { * does not answer within five round trips (Java: TimeOutException). */ resolveDns(nodeId: Id, name: string, type: string, serverIPAddress?: string): DnsResolution { + let resolution: DnsLookup | undefined; + const server = serverIPAddress ?? this.stateValue.topologySnapshot.deviceConfigurations.get(nodeId)?.dnsServer; + this.beginDnsResolution(nodeId, name, type, server, (result) => { + resolution = result; + }); + // Each query ends within five round trips, by its answer or its timeout. + const window = (DNS_QUERY_TIMEOUT_ROUND_TRIPS + 1) * this.runtimeSpeed.roundTripTimeMilliseconds; + while (resolution === undefined && this.stateValue.phase === 'running' && this.stateValue.pendingEvents.length > 0) { + this.advanceUntil(() => resolution !== undefined, this.stateValue.currentTimeMilliseconds + window); + } + if (resolution === undefined || resolution.type === 'timedOut') return { type: 'timedOut' }; + return { type: 'answered', records: [...resolution.answers, ...resolution.authority] }; + } + + /** + * Resolver.resolve as a chain of queries on the events: `done` with the + * records for the name and type from the answer and authority sections of + * the responses (and, for MX and NS, the address of the first), or + * timedOut when a server did not answer. + */ + private beginDnsResolution(nodeId: Id, name: string, type: string, server: string | undefined, done: (resolution: DnsLookup) => void): void { const domainName = name.endsWith('.') ? name : `${name}.`; - const collected: DnsMessageRecord[] = []; if (/^localhost\.?$/i.test(domainName)) { - return { type: 'answered', records: [{ name: 'localhost.', type: 'A', ttl: 3600, data: LOCALHOST_IP_ADDRESS }] }; + done({ type: 'answered', answers: [{ name: 'localhost.', type: 'A', ttl: 3600, data: LOCALHOST_IP_ADDRESS }], authority: [] }); + return; } + const answers: DnsMessageRecord[] = []; + const authority: DnsMessageRecord[] = []; const queried = new Set(); - let server: string | undefined = serverIPAddress ?? this.stateValue.topologySnapshot.deviceConfigurations.get(nodeId)?.dnsServer; - while (server !== undefined && server !== '') { - const response = this.queryDnsServer(nodeId, domainName, type, server); - if (response === undefined) return { type: 'timedOut' }; - const relevant = (records: readonly DnsMessageRecord[], wanted: string, wantedType: string) => findDnsRecords(wanted, wantedType, records); - collected.push(...relevant(response.answers, domainName, type), ...relevant(response.authority, domainName, type)); - const data = [...response.answers, ...response.authority, ...response.additional].filter( - (record) => record.name.toLowerCase() === domainName.toLowerCase() && record.type === type, - ); - if (data.length > 0) { - if (type === 'MX' || type === 'NS') { - collected.push(...relevant(response.answers, data[0]!.data, 'A'), ...relevant(response.authority, data[0]!.data, 'A')); - } - break; + const ask = (serverToQuery: string | undefined): void => { + if (serverToQuery === undefined || serverToQuery === '') { + done({ type: 'answered', answers, authority }); + return; } - const all = [...response.answers, ...response.authority, ...response.additional]; - const nameServer = findApplicableNameServerRecord(domainName, all); - const next = nameServer === undefined ? undefined : findDnsRecords(nameServer.data, 'A', all)[0]?.data; - // Java remembers only the servers it was referred to, not the first one. - server = next !== undefined && !queried.has(next) ? next : undefined; - if (next !== undefined) queried.add(next); - } - return { type: 'answered', records: collected }; + this.queryDnsServer(nodeId, domainName, type, serverToQuery, (response) => { + if (response === undefined) { + done({ type: 'timedOut' }); + return; + } + answers.push(...findDnsRecords(domainName, type, response.answers)); + authority.push(...findDnsRecords(domainName, type, response.authority)); + const data = findDnsRecords(domainName, type, [...response.answers, ...response.authority, ...response.additional]); + if (data.length > 0) { + if (type === 'MX' || type === 'NS') { + answers.push(...findDnsRecords(data[0]!.data, 'A', response.answers)); + authority.push(...findDnsRecords(data[0]!.data, 'A', response.authority)); + } + done({ type: 'answered', answers, authority }); + return; + } + const all = [...response.answers, ...response.authority, ...response.additional]; + const nameServer = findApplicableNameServerRecord(domainName, all); + const next = nameServer === undefined ? undefined : findDnsRecords(nameServer.data, 'A', all)[0]?.data; + // Java remembers only the servers it was referred to, not the first one. + if (next === undefined || queried.has(next)) { + done({ type: 'answered', answers, authority }); + return; + } + queried.add(next); + ask(next); + }); + }; + ask(server); } /** Resolver.holeIPAdresse: the A record's address, undefined when there is none. */ @@ -1331,10 +1447,16 @@ export class NetworkRuntimeEngine { return { type: 'resolved', address: exchange === undefined ? undefined : findDnsRecords(exchange.data, 'A', resolution.records)[0]?.data }; } - /** DNSQueryAgent.query: one question from a fresh socket, the first datagram back within five round trips. */ - private queryDnsServer(nodeId: Id, domainName: string, type: string, serverIPAddress: string): DnsMessage | undefined { + /** + * DNSQueryAgent.query: one question from a fresh socket; `done` with the + * first datagram back, or undefined after five round trips. + */ + private queryDnsServer(nodeId: Id, domainName: string, type: string, serverIPAddress: string, done: (response: DnsMessage | undefined) => void): void { const socketId = this.bindUdpSocket(nodeId, { remoteIPAddress: serverIPAddress, remotePort: DNS_PORT }); - if (socketId === undefined) return undefined; + if (socketId === undefined) { + done(undefined); + return; + } const query: DnsMessage = { // Java: (int) (Math.random() * 65536). id: Math.floor(this.dnsIdRandom.nextDouble() * 65_536), @@ -1347,9 +1469,28 @@ export class NetworkRuntimeEngine { }; this.recordTrace({ nodeId, direction: 'outbound', layer: 'application', operation: 'sent', detail: `DNS query ${domainName} ${type}` }); this.sendUdp(socketId, new TextEncoder().encode(encodeDnsMessage(query))); - const received = this.receiveUdp(socketId, DNS_QUERY_TIMEOUT_ROUND_TRIPS * this.runtimeSpeed.roundTripTimeMilliseconds); + if (!this.stateValue.socketsById.has(socketId)) { + done(undefined); + return; + } + // The wait begins when the send returns (empfangen after senden). + this.dnsQueriesBySocketId.set(socketId, done); + this.schedule(this.stateValue.currentTimeMilliseconds + DNS_QUERY_TIMEOUT_ROUND_TRIPS * this.runtimeSpeed.roundTripTimeMilliseconds, { + kind: 'dnsQueryTimeout', + socketId, + }); + } + + /** The query's answer came (or its time ran out): its socket closes, the resolution goes on. */ + private finishDnsQuery(socketId: Id, response: DnsMessage | undefined): void { + const done = this.dnsQueriesBySocketId.get(socketId); + if (done === undefined) return; + this.dnsQueriesBySocketId.delete(socketId); + this.stateValue.pendingEvents = this.stateValue.pendingEvents.filter( + (event) => !(event.kind.kind === 'dnsQueryTimeout' && event.kind.socketId === socketId), + ); this.closeSocket(socketId); - return received === undefined ? undefined : decodeDnsMessage(new TextDecoder().decode(received.datagram.payload)); + done(response); } // ── TCP sockets ────────────────────────────────────────────────────────── @@ -4178,7 +4319,8 @@ function firewallAddressMatches(packetIPAddress: string, configuredIPAddress: st * (with the A record of an MX or NS target), else the name server of the * zone the name lies in, with its A record — in the answer section, as Java has it. */ -function answerDnsQuestion(question: DnsQuestion, records: readonly DnsMessageRecord[]): DnsMessageRecord[] { +/** DNSServer.answerWithLocalData: the records for the question, an MX's or NS's with its address. */ +function localDnsAnswers(question: DnsQuestion, records: readonly DnsMessageRecord[]): DnsMessageRecord[] { const answers: DnsMessageRecord[] = []; for (const record of findDnsRecords(question.name, question.type, records)) { answers.push(record); @@ -4187,7 +4329,11 @@ function answerDnsQuestion(question: DnsQuestion, records: readonly DnsMessageRe if (address !== undefined) answers.push(address); } } - if (answers.length > 0) return answers; + return answers; +} + +/** DNSServer.defineNameserverRecords: the NS record of the question's zone (or the root's) and its address. */ +function dnsReferral(question: DnsQuestion, records: readonly DnsMessageRecord[]): DnsMessageRecord[] { const nameServer = findApplicableNameServerRecord(question.name, records); if (nameServer === undefined) return []; const address = findDnsRecords(nameServer.data, 'A', records)[0]; diff --git a/packages/core/test/dnsJava.test.ts b/packages/core/test/dnsJava.test.ts index 2f6e6fe..955003b 100644 --- a/packages/core/test/dnsJava.test.ts +++ b/packages/core/test/dnsJava.test.ts @@ -1,6 +1,8 @@ // DNS as Java FILIUS runs it. tools/java-oracle's DnsRun called a client's // resolver on dns-lan's network in Java FILIUS and wrote every frame each -// card saw and what the call returned (dns-java-.java.json). The +// card saw and what the call returned (dns-java-.java.json); on +// dns-recursive's, where the server resolves recursively, likewise +// (dns-recursive-java-.java.json). The // engine makes the same call: the frames must come in the same order, with // the same content, at about the same distance from each other (javaRun.ts), // the call must return the same, after about as long. @@ -26,12 +28,43 @@ interface JavaRun { events: [number, string][]; } -const STORIES: Record DnsAddressResolution> = { - address: (engine) => engine.resolveDnsAddress(CLIENT, 'web.filius.de'), - mail: (engine) => engine.resolveDnsMailServer(CLIENT, 'filius.de'), - referral: (engine) => engine.resolveDnsAddress(CLIENT, 'www.schule.de'), - unknown: (engine) => engine.resolveDnsAddress(CLIENT, 'x.unbekannt.org'), - noServer: (engine) => engine.resolveDnsAddress(LOST_CLIENT, 'web.filius.de'), +type Story = (engine: NetworkRuntimeEngine) => DnsAddressResolution; + +/** Per scenario the stories DnsRun played on it. */ +const RUNS: Record> = { + 'dns-lan': { + address: (engine) => engine.resolveDnsAddress(CLIENT, 'web.filius.de'), + mail: (engine) => engine.resolveDnsMailServer(CLIENT, 'filius.de'), + referral: (engine) => engine.resolveDnsAddress(CLIENT, 'www.schule.de'), + unknown: (engine) => engine.resolveDnsAddress(CLIENT, 'x.unbekannt.org'), + noServer: (engine) => engine.resolveDnsAddress(LOST_CLIENT, 'web.filius.de'), + }, + 'dns-recursive': { + address: (engine) => engine.resolveDnsAddress(CLIENT, 'web.filius.de'), + referral: (engine) => engine.resolveDnsAddress(CLIENT, 'www.schule.de'), + unknown: (engine) => engine.resolveDnsAddress(CLIENT, 'x.unbekannt.org'), + unknownInZone: (engine) => engine.resolveDnsAddress(CLIENT, 'nix.schule.de'), + unknownInOwnZone: (engine) => engine.resolveDnsAddress(CLIENT, 'nix.filius.de'), + }, +}; + +/** The recording of a story: dns-java- for dns-lan, -java- else. */ +function recording(scenarioName: string, story: string): string { + return scenarioName === 'dns-lan' ? `dns-java-${story}.java.json` : `${scenarioName}-java-${story}.java.json`; +} + +/** + * Where Java errs, per scenario and story: compared up to the frames Java + * sends before its error shows, not the result (engine.test.ts has the rest). + * + * dns-recursive/unknownInOwnZone: the server's own zone names itself as its + * name server, and Java's server asks it — itself, which asks itself again, + * without end; the client times out, then every level falls back to .11. The + * engine asks neither itself nor, here, anyone but .11. Compared up to the + * client's query. + */ +const JAVA_UNTIL: Record number> = { + 'dns-recursive/unknownInOwnZone': (java) => Math.min(...Object.values(java.frames).flat().filter((frame) => frame.destinationPort === 53).map((frame) => frame.time)) + 1, }; /** Java's words for the result: the address, null, or the exception. */ @@ -53,35 +86,43 @@ function normalized(frames: readonly SeenFrame[]): SeenFrame[] { })); } -/** Stories the engine does not play like Java yet, with the reason; each must still differ from Java. */ -const NOT_YET_LIKE_JAVA: Record = {}; - describe('DNS as Java FILIUS runs it', () => { - const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'dns-lan.scenario.json'), 'utf8')) as Scenario; + it('cuts only where Java goes on with its error', () => { + for (const [key, until] of Object.entries(JAVA_UNTIL)) { + const [scenarioName, story] = key.split('/') as [string, string]; + const java = JSON.parse(readFileSync(join(scenarioDirectory, recording(scenarioName, story)), 'utf8')) as JavaRun; + expect(Object.values(java.frames).flat().some((frame) => frame.time >= until(java)), key).toBe(true); + } + }); - for (const [name, story] of Object.entries(STORIES)) { - it(name, () => { - const java = JSON.parse(readFileSync(join(scenarioDirectory, `dns-java-${name}.java.json`), 'utf8')) as JavaRun; - const { engine, frames } = watchedEngine(scenario); - engine.handle({ type: 'advance', toMilliseconds: 500 }); - const called = engine.state.currentTimeMilliseconds; - const result = story(engine); - const returned = engine.state.currentTimeMilliseconds; + for (const [scenarioName, stories] of Object.entries(RUNS)) { + const scenario = JSON.parse(readFileSync(join(scenarioDirectory, `${scenarioName}.scenario.json`), 'utf8')) as Scenario; + for (const [name, story] of Object.entries(stories)) { + it(`${scenarioName}: ${name}`, () => { + const java = JSON.parse(readFileSync(join(scenarioDirectory, recording(scenarioName, name)), 'utf8')) as JavaRun; + const { engine, frames } = watchedEngine(scenario); + engine.handle({ type: 'advance', toMilliseconds: 500 }); + const called = engine.state.currentTimeMilliseconds; + const result = story(engine); + const returned = engine.state.currentTimeMilliseconds; - const [[javaCalled], [javaReturned, javaOutcome]] = java.events as [[number, string], [number, string]]; - expect(`${javaOutcome.split(' ').slice(-2).join(' ')}`).toBe(javaResult(result).split(' ').slice(-2).join(' ')); - - const expected = gaps(normalized(javaFrames(java.frames))); - const actual = gaps(normalized(frames())); - if (name in NOT_YET_LIKE_JAVA) { - expect(JSON.stringify(actual), NOT_YET_LIKE_JAVA[name]).not.toBe(JSON.stringify(expected)); - return; - } - expect(Math.abs(returned - called - (javaReturned - javaCalled)), 'duration of the call').toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS); - for (let index = 0; index < Math.max(expected.length, actual.length); index += 1) { - expect(actual[index]?.text, `frame #${index + 1}`).toBe(expected[index]?.text); - expect(Math.abs(actual[index]!.gap - expected[index]!.gap), `gap before frame #${index + 1}`).toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS); - } - }); + let expected = gaps(normalized(javaFrames(java.frames))); + let actual = gaps(normalized(frames())); + const until = JAVA_UNTIL[`${scenarioName}/${name}`]?.(java); + if (until !== undefined) { + const count = javaFrames(java.frames).filter((frame) => frame.time < until).length; + expected = expected.slice(0, count); + actual = actual.slice(0, count); + } else { + const [[javaCalled], [javaReturned, javaOutcome]] = java.events as [[number, string], [number, string]]; + expect(`${javaOutcome.split(' ').slice(-2).join(' ')}`).toBe(javaResult(result).split(' ').slice(-2).join(' ')); + expect(Math.abs(returned - called - (javaReturned - javaCalled)), 'duration of the call').toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS); + } + for (let index = 0; index < Math.max(expected.length, actual.length); index += 1) { + expect(actual[index]?.text, `frame #${index + 1}`).toBe(expected[index]?.text); + expect(Math.abs(actual[index]!.gap - expected[index]!.gap), `gap before frame #${index + 1}`).toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS); + } + }); + } } }); diff --git a/packages/core/test/engine.test.ts b/packages/core/test/engine.test.ts index 2713a18..ee45a62 100644 --- a/packages/core/test/engine.test.ts +++ b/packages/core/test/engine.test.ts @@ -11,7 +11,6 @@ import { decodeDnsMessage, encodeDnsMessage, findApplicableNameServerRecord, - NotPortedError, DHCP_DYNAMIC_LEASE_LIFETIME_MILLISECONDS, deviceConfiguration, dhcpServerConfiguration, @@ -1668,6 +1667,7 @@ describe('DNS', () => { expect(engine.resolveDnsAddress(client, 'web.filius.de')).toEqual({ type: 'resolved', address: '10.30.0.9' }); // ARP there and back, then the query and its answer: four cables each way through the switch. expect(engine.state.currentTimeMilliseconds - called).toBe(4 * 110); + expect(engine.state.pendingEvents.some((event) => event.kind.kind === 'dnsQueryTimeout')).toBe(false); }); it("answers behind a datagram of its own whose ARP fails: Java's UDP thread sends one after the other", () => { @@ -1722,8 +1722,76 @@ describe('DNS', () => { expect([...engine.state.socketsById.values()].filter((socket) => socket.nodeId === client)).toEqual([]); }); - it('does not answer recursively yet', () => { - expect(() => lan({ '10.30.0.2': [] }, true)).toThrow(NotPortedError); + it('answers every question of a query, one after the other', () => { + const { engine, client } = lan({ '10.30.0.2': ['web.filius.de A 10.30.0.9', 'mail.filius.de A 10.30.0.10'] }); + const socket = engine.bindUdpSocket(client, { remoteIPAddress: '10.30.0.2', remotePort: 53 })!; + const query = encodeDnsMessage({ + id: 9, + queryResponse: 0, + responseCode: 0, + questions: [ + { name: 'web.filius.de.', type: 'A', questionClass: 'IN' }, + { name: 'mail.filius.de.', type: 'A', questionClass: 'IN' }, + ], + answers: [], + authority: [], + additional: [], + }); + engine.sendUdp(socket, new TextEncoder().encode(query)); + const response = decodeDnsMessage(new TextDecoder().decode(engine.receiveUdp(socket, 10_000)!.datagram.payload)); + + expect(response.answers.map((record) => record.data)).toEqual(['10.30.0.9', '10.30.0.10']); + }); + + it('forgets the DNS queries that wait on stop', () => { + const records = { '10.30.0.2': ['schule.de NS ns.schule.de.', 'ns.schule.de A 10.30.0.3'] }; + const { engine, client } = lan(records, true); + // The client gives up; the server still waits for .3, which runs no DNS server, when the simulation stops. + expect(engine.resolveDnsAddress(client, 'www.schule.de')).toEqual({ type: 'timedOut' }); + const snapshot = engine.state.topologySnapshot; + engine.handle({ type: 'stop' }); + engine.handle({ type: 'start', snapshot, seed: 14, initialTimeMilliseconds: 0 }); + // The sockets are numbered anew: the second takes the number of the server's query socket before. + engine.bindUdpSocket(client, { localPort: 7000 }); + const receiver = engine.bindUdpSocket(client, { localPort: 7001 })!; + const sender = engine.bindUdpSocket(uuid(0x1906), { remoteIPAddress: '10.30.0.1', remotePort: 7001 })!; + engine.sendUdp(sender, new TextEncoder().encode('still mine')); + + expect(new TextDecoder().decode(engine.receiveUdp(receiver, 10_000)?.datagram.payload)).toBe('still mine'); + }); + + it('asks on for a name it does not know, and answers with what it learns', () => { + const { engine, client } = lan( + { '10.30.0.2': ['schule.de NS ns.schule.de.', 'ns.schule.de A 10.30.0.3'], '10.30.0.3': ['www.schule.de A 10.30.0.40'] }, + true, + ); + const called = engine.state.currentTimeMilliseconds; + expect(engine.resolveDnsAddress(client, 'www.schule.de')).toEqual({ type: 'resolved', address: '10.30.0.40' }); + // ARP, query and answer between client and server, ARP, query and answer between server and .3, + // and the answer to the client: eight crossings of the switch, 110 ms each. + expect(queries(engine)).toEqual(['DNS query www.schule.de. A', 'DNS query www.schule.de. A']); + expect(engine.state.currentTimeMilliseconds - called).toBe(8 * 110); + }); + + it('asks neither itself as the name server of its own zone nor as its own DNS server: it refers (Java asks itself without end)', () => { + const { engine, client } = lan({ '10.30.0.2': ['filius.de NS ns.filius.de.', 'ns.filius.de A 10.30.0.2'] }, true); + const called = engine.state.currentTimeMilliseconds; + expect(engine.resolveDnsAddress(client, 'nix.filius.de')).toEqual({ type: 'resolved', address: undefined }); + // The client follows the referral to the server once more: two exchanges, the first with ARP. + expect(queries(engine)).toEqual(['DNS query nix.filius.de. A', 'DNS query nix.filius.de. A']); + expect(engine.state.currentTimeMilliseconds - called).toBe(4 * 110 + 2 * 110); + }); + + it('gives up on a name server that does not answer after five round trips, when the client has given up too', () => { + const { engine, client } = lan({ '10.30.0.2': ['schule.de NS ns.schule.de.', 'ns.schule.de A 10.30.0.3'] }, true); + const called = engine.state.currentTimeMilliseconds; + // .3 runs no DNS server. + expect(engine.resolveDnsAddress(client, 'www.schule.de')).toEqual({ type: 'timedOut' }); + expect(engine.state.currentTimeMilliseconds - called).toBe(5 * 2_750); + // The server's own wait ends later, then it refers the client, which no longer listens. + engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + 5 * 2_750 }); + expect(engine.state.packetTraces.filter((trace) => trace.detail === 'DNS response')).toHaveLength(1); + expect([...engine.state.socketsById.values()].filter((socket) => socket.nodeId === client)).toEqual([]); }); }); diff --git a/packages/core/test/scenarios/dns-recursive-java-address.java.json b/packages/core/test/scenarios/dns-recursive-java-address.java.json new file mode 100644 index 0000000..be81e32 --- /dev/null +++ b/packages/core/test/scenarios/dns-recursive-java-address.java.json @@ -0,0 +1,114 @@ +{ + "frames": { + "00000000-0000-0000-0000-000000000101": [ + { + "time": 727, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 963, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "arp": 2, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.20" + }, + { + "time": 965, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 17, + "sourcePort": 60119, + "destinationPort": 53, + "payload": "ID=56151 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nweb.filius.de. A IN\n" + }, + { + "time": 1196, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 17, + "sourcePort": 53, + "destinationPort": 60119, + "payload": "ID=56151 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=1 NSCOUNT=0 ARCOUNT=0 \nweb.filius.de. A 3600 192.168.0.30\n" + } + ], + "00000000-0000-0000-0000-000000000102": [ + { + "time": 850, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 851, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "arp": 2, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.20" + }, + { + "time": 1079, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 17, + "sourcePort": 60119, + "destinationPort": 53, + "payload": "ID=56151 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nweb.filius.de. A IN\n" + }, + { + "time": 1084, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 17, + "sourcePort": 53, + "destinationPort": 60119, + "payload": "ID=56151 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=1 NSCOUNT=0 ARCOUNT=0 \nweb.filius.de. A 3600 192.168.0.30\n" + } + ], + "00000000-0000-0000-0000-000000000103": [ + { + "time": 850, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + } + ], + "00000000-0000-0000-0000-000000000104": [ + { + "time": 850, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + } + ] + }, + "events": [ + [ + 717, + "holeIPAdresse web.filius.de called" + ], + [ + 1197, + "holeIPAdresse web.filius.de returned 192.168.0.30" + ] + ] +} diff --git a/packages/core/test/scenarios/dns-recursive-java-referral.java.json b/packages/core/test/scenarios/dns-recursive-java-referral.java.json new file mode 100644 index 0000000..5ef1af8 --- /dev/null +++ b/packages/core/test/scenarios/dns-recursive-java-referral.java.json @@ -0,0 +1,206 @@ +{ + "frames": { + "00000000-0000-0000-0000-000000000101": [ + { + "time": 727, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 961, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "arp": 2, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.20" + }, + { + "time": 963, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 17, + "sourcePort": 51710, + "destinationPort": 53, + "payload": "ID=39889 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nwww.schule.de. A IN\n" + }, + { + "time": 1196, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.11" + }, + { + "time": 1645, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 17, + "sourcePort": 53, + "destinationPort": 51710, + "payload": "ID=39889 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=1 NSCOUNT=0 ARCOUNT=0 \nwww.schule.de. A 3600 192.168.0.40\n" + } + ], + "00000000-0000-0000-0000-000000000102": [ + { + "time": 848, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 849, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "arp": 2, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.20" + }, + { + "time": 1076, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 17, + "sourcePort": 51710, + "destinationPort": 53, + "payload": "ID=39889 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nwww.schule.de. A IN\n" + }, + { + "time": 1085, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.11" + }, + { + "time": 1308, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:12", + "arp": 2, + "senderIp": "192.168.0.11", + "targetIp": "192.168.0.10" + }, + { + "time": 1308, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:13", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.11", + "protocol": 17, + "sourcePort": 52403, + "destinationPort": 53, + "payload": "ID=12187 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nwww.schule.de. A IN\n" + }, + { + "time": 1533, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.11", + "destinationIp": "192.168.0.10", + "protocol": 17, + "sourcePort": 53, + "destinationPort": 52403, + "payload": "ID=12187 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=1 NSCOUNT=0 ARCOUNT=0 \nwww.schule.de. A 3600 192.168.0.40\n" + }, + { + "time": 1534, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 17, + "sourcePort": 53, + "destinationPort": 51710, + "payload": "ID=39889 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=1 NSCOUNT=0 ARCOUNT=0 \nwww.schule.de. A 3600 192.168.0.40\n" + } + ], + "00000000-0000-0000-0000-000000000103": [ + { + "time": 848, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 1196, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.11" + }, + { + "time": 1196, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:12", + "arp": 2, + "senderIp": "192.168.0.11", + "targetIp": "192.168.0.10" + }, + { + "time": 1420, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:13", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.11", + "protocol": 17, + "sourcePort": 52403, + "destinationPort": 53, + "payload": "ID=12187 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nwww.schule.de. A IN\n" + }, + { + "time": 1421, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.11", + "destinationIp": "192.168.0.10", + "protocol": 17, + "sourcePort": 53, + "destinationPort": 52403, + "payload": "ID=12187 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=1 NSCOUNT=0 ARCOUNT=0 \nwww.schule.de. A 3600 192.168.0.40\n" + } + ], + "00000000-0000-0000-0000-000000000104": [ + { + "time": 848, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 1196, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.11" + } + ] + }, + "events": [ + [ + 716, + "holeIPAdresse www.schule.de called" + ], + [ + 1645, + "holeIPAdresse www.schule.de returned 192.168.0.40" + ] + ] +} diff --git a/packages/core/test/scenarios/dns-recursive-java-unknown.java.json b/packages/core/test/scenarios/dns-recursive-java-unknown.java.json new file mode 100644 index 0000000..3fc418f --- /dev/null +++ b/packages/core/test/scenarios/dns-recursive-java-unknown.java.json @@ -0,0 +1,206 @@ +{ + "frames": { + "00000000-0000-0000-0000-000000000101": [ + { + "time": 729, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 963, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "arp": 2, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.20" + }, + { + "time": 965, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 17, + "sourcePort": 59169, + "destinationPort": 53, + "payload": "ID=28614 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nx.unbekannt.org. A IN\n" + }, + { + "time": 1198, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.11" + }, + { + "time": 1645, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 17, + "sourcePort": 53, + "destinationPort": 59169, + "payload": "ID=28614 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \n" + } + ], + "00000000-0000-0000-0000-000000000102": [ + { + "time": 850, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 851, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "arp": 2, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.20" + }, + { + "time": 1078, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 17, + "sourcePort": 59169, + "destinationPort": 53, + "payload": "ID=28614 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nx.unbekannt.org. A IN\n" + }, + { + "time": 1087, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.11" + }, + { + "time": 1309, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:12", + "arp": 2, + "senderIp": "192.168.0.11", + "targetIp": "192.168.0.10" + }, + { + "time": 1309, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:13", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.11", + "protocol": 17, + "sourcePort": 61822, + "destinationPort": 53, + "payload": "ID=44286 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nx.unbekannt.org. A IN\n" + }, + { + "time": 1533, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.11", + "destinationIp": "192.168.0.10", + "protocol": 17, + "sourcePort": 53, + "destinationPort": 61822, + "payload": "ID=44286 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \n" + }, + { + "time": 1534, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 17, + "sourcePort": 53, + "destinationPort": 59169, + "payload": "ID=28614 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \n" + } + ], + "00000000-0000-0000-0000-000000000103": [ + { + "time": 850, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 1198, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.11" + }, + { + "time": 1198, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:12", + "arp": 2, + "senderIp": "192.168.0.11", + "targetIp": "192.168.0.10" + }, + { + "time": 1421, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:13", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.11", + "protocol": 17, + "sourcePort": 61822, + "destinationPort": 53, + "payload": "ID=44286 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nx.unbekannt.org. A IN\n" + }, + { + "time": 1422, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.11", + "destinationIp": "192.168.0.10", + "protocol": 17, + "sourcePort": 53, + "destinationPort": 61822, + "payload": "ID=44286 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \n" + } + ], + "00000000-0000-0000-0000-000000000104": [ + { + "time": 850, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 1198, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.11" + } + ] + }, + "events": [ + [ + 719, + "holeIPAdresse x.unbekannt.org called" + ], + [ + 1646, + "holeIPAdresse x.unbekannt.org returned null" + ] + ] +} diff --git a/packages/core/test/scenarios/dns-recursive-java-unknownInOwnZone.java.json b/packages/core/test/scenarios/dns-recursive-java-unknownInOwnZone.java.json new file mode 100644 index 0000000..78e0fa4 --- /dev/null +++ b/packages/core/test/scenarios/dns-recursive-java-unknownInOwnZone.java.json @@ -0,0 +1,173 @@ +{ + "frames": { + "00000000-0000-0000-0000-000000000101": [ + { + "time": 735, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 969, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "arp": 2, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.20" + }, + { + "time": 972, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 17, + "sourcePort": 51431, + "destinationPort": 53, + "payload": "ID=22768 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nnix.filius.de. A IN\n" + }, + { + "time": 15066, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.11" + } + ], + "00000000-0000-0000-0000-000000000102": [ + { + "time": 857, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 858, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "arp": 2, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.20" + }, + { + "time": 1085, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 17, + "sourcePort": 51431, + "destinationPort": 53, + "payload": "ID=22768 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nnix.filius.de. A IN\n" + }, + { + "time": 14910, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.11" + }, + { + "time": 15276, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:12", + "arp": 2, + "senderIp": "192.168.0.11", + "targetIp": "192.168.0.10" + }, + { + "time": 15278, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:13", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.11", + "protocol": 17, + "sourcePort": 52690, + "destinationPort": 53, + "payload": "ID=54566 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nnix.filius.de. A IN\n" + } + ], + "00000000-0000-0000-0000-000000000103": [ + { + "time": 856, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 15066, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.11" + }, + { + "time": 15073, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:12", + "arp": 2, + "senderIp": "192.168.0.11", + "targetIp": "192.168.0.10" + }, + { + "time": 15423, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:13", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.11", + "protocol": 17, + "sourcePort": 52690, + "destinationPort": 53, + "payload": "ID=54566 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nnix.filius.de. A IN\n" + }, + { + "time": 15434, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.11", + "destinationIp": "192.168.0.10", + "protocol": 17, + "sourcePort": 53, + "destinationPort": 52690, + "payload": "ID=54566 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \n" + } + ], + "00000000-0000-0000-0000-000000000104": [ + { + "time": 856, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 15059, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.11" + } + ] + }, + "events": [ + [ + 724, + "holeIPAdresse nix.filius.de called" + ], + [ + 14485, + "holeIPAdresse nix.filius.de failed TimeOutException" + ] + ] +} diff --git a/packages/core/test/scenarios/dns-recursive-java-unknownInZone.java.json b/packages/core/test/scenarios/dns-recursive-java-unknownInZone.java.json new file mode 100644 index 0000000..6dafa64 --- /dev/null +++ b/packages/core/test/scenarios/dns-recursive-java-unknownInZone.java.json @@ -0,0 +1,342 @@ +{ + "frames": { + "00000000-0000-0000-0000-000000000101": [ + { + "time": 750, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 984, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "arp": 2, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.20" + }, + { + "time": 987, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 17, + "sourcePort": 64317, + "destinationPort": 53, + "payload": "ID=59872 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nnix.schule.de. A IN\n" + }, + { + "time": 1221, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.11" + }, + { + "time": 1897, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 17, + "sourcePort": 53, + "destinationPort": 64317, + "payload": "ID=59872 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=2 NSCOUNT=0 ARCOUNT=0 \nschule.de. NS 3600 ns.schule.de.\nns.schule.de. A 3600 192.168.0.11\n" + }, + { + "time": 1899, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.11" + }, + { + "time": 2121, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:11", + "arp": 2, + "senderIp": "192.168.0.11", + "targetIp": "192.168.0.20" + }, + { + "time": 2121, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:13", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.11", + "protocol": 17, + "sourcePort": 53912, + "destinationPort": 53, + "payload": "ID=65453 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nnix.schule.de. A IN\n" + }, + { + "time": 2345, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.11", + "destinationIp": "192.168.0.20", + "protocol": 17, + "sourcePort": 53, + "destinationPort": 53912, + "payload": "ID=65453 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \n" + } + ], + "00000000-0000-0000-0000-000000000102": [ + { + "time": 871, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 873, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "arp": 2, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.20" + }, + { + "time": 1100, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 17, + "sourcePort": 64317, + "destinationPort": 53, + "payload": "ID=59872 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nnix.schule.de. A IN\n" + }, + { + "time": 1109, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.11" + }, + { + "time": 1333, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:12", + "arp": 2, + "senderIp": "192.168.0.11", + "targetIp": "192.168.0.10" + }, + { + "time": 1333, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:13", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.11", + "protocol": 17, + "sourcePort": 50451, + "destinationPort": 53, + "payload": "ID=16993 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nnix.schule.de. A IN\n" + }, + { + "time": 1558, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.11", + "destinationIp": "192.168.0.10", + "protocol": 17, + "sourcePort": 53, + "destinationPort": 50451, + "payload": "ID=16993 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \n" + }, + { + "time": 1558, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:13", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.11", + "protocol": 17, + "sourcePort": 60605, + "destinationPort": 53, + "payload": "ID=33013 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nnix.schule.de. A IN\n" + }, + { + "time": 1783, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.11", + "destinationIp": "192.168.0.10", + "protocol": 17, + "sourcePort": 53, + "destinationPort": 60605, + "payload": "ID=33013 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \n" + }, + { + "time": 1786, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 17, + "sourcePort": 53, + "destinationPort": 64317, + "payload": "ID=59872 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=2 NSCOUNT=0 ARCOUNT=0 \nschule.de. NS 3600 ns.schule.de.\nns.schule.de. A 3600 192.168.0.11\n" + }, + { + "time": 2010, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.11" + } + ], + "00000000-0000-0000-0000-000000000103": [ + { + "time": 871, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 1221, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.11" + }, + { + "time": 1221, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:12", + "arp": 2, + "senderIp": "192.168.0.11", + "targetIp": "192.168.0.10" + }, + { + "time": 1445, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:13", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.11", + "protocol": 17, + "sourcePort": 50451, + "destinationPort": 53, + "payload": "ID=16993 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nnix.schule.de. A IN\n" + }, + { + "time": 1446, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.11", + "destinationIp": "192.168.0.10", + "protocol": 17, + "sourcePort": 53, + "destinationPort": 50451, + "payload": "ID=16993 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \n" + }, + { + "time": 1670, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:13", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.11", + "protocol": 17, + "sourcePort": 60605, + "destinationPort": 53, + "payload": "ID=33013 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nnix.schule.de. A IN\n" + }, + { + "time": 1671, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.11", + "destinationIp": "192.168.0.10", + "protocol": 17, + "sourcePort": 53, + "destinationPort": 60605, + "payload": "ID=33013 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \n" + }, + { + "time": 2010, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.11" + }, + { + "time": 2010, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:11", + "arp": 2, + "senderIp": "192.168.0.11", + "targetIp": "192.168.0.20" + }, + { + "time": 2233, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:13", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.11", + "protocol": 17, + "sourcePort": 53912, + "destinationPort": 53, + "payload": "ID=65453 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \nnix.schule.de. A IN\n" + }, + { + "time": 2234, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.11", + "destinationIp": "192.168.0.20", + "protocol": 17, + "sourcePort": 53, + "destinationPort": 53912, + "payload": "ID=65453 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \n" + } + ], + "00000000-0000-0000-0000-000000000104": [ + { + "time": 871, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 1221, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.11" + }, + { + "time": 2010, + "source": "02:00:00:00:01:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.11" + } + ] + }, + "events": [ + [ + 738, + "holeIPAdresse nix.schule.de called" + ], + [ + 2345, + "holeIPAdresse nix.schule.de returned null" + ] + ] +} diff --git a/packages/core/test/scenarios/dns-recursive.scenario.json b/packages/core/test/scenarios/dns-recursive.scenario.json new file mode 100644 index 0000000..c0c1d2b --- /dev/null +++ b/packages/core/test/scenarios/dns-recursive.scenario.json @@ -0,0 +1,142 @@ +{ + "seed": 51, + "nodes": [ + { + "id": "00000000-0000-0000-0000-000000000101", + "kind": "pc", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000111", + "label": "eth0" + } + ] + }, + { + "id": "00000000-0000-0000-0000-000000000102", + "kind": "pc", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000112", + "label": "eth0" + } + ] + }, + { + "id": "00000000-0000-0000-0000-000000000103", + "kind": "pc", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000113", + "label": "eth0" + } + ] + }, + { + "id": "00000000-0000-0000-0000-000000000104", + "kind": "pc", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000114", + "label": "eth0" + } + ] + }, + { + "id": "00000000-0000-0000-0000-000000000105", + "kind": "networkSwitch", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000121", + "label": "sw1" + }, + { + "id": "00000000-0000-0000-0000-000000000122", + "label": "sw2" + }, + { + "id": "00000000-0000-0000-0000-000000000123", + "label": "sw3" + }, + { + "id": "00000000-0000-0000-0000-000000000124", + "label": "sw4" + }, + { + "id": "00000000-0000-0000-0000-000000000125", + "label": "sw5" + } + ] + } + ], + "links": [ + { + "id": "00000000-0000-0000-0000-00000000F001", + "sourceNode": "00000000-0000-0000-0000-000000000101", + "sourcePort": "00000000-0000-0000-0000-000000000111", + "targetNode": "00000000-0000-0000-0000-000000000105", + "targetPort": "00000000-0000-0000-0000-000000000121" + }, + { + "id": "00000000-0000-0000-0000-00000000F002", + "sourceNode": "00000000-0000-0000-0000-000000000102", + "sourcePort": "00000000-0000-0000-0000-000000000112", + "targetNode": "00000000-0000-0000-0000-000000000105", + "targetPort": "00000000-0000-0000-0000-000000000122" + }, + { + "id": "00000000-0000-0000-0000-00000000F003", + "sourceNode": "00000000-0000-0000-0000-000000000103", + "sourcePort": "00000000-0000-0000-0000-000000000113", + "targetNode": "00000000-0000-0000-0000-000000000105", + "targetPort": "00000000-0000-0000-0000-000000000123" + }, + { + "id": "00000000-0000-0000-0000-00000000F004", + "sourceNode": "00000000-0000-0000-0000-000000000104", + "sourcePort": "00000000-0000-0000-0000-000000000114", + "targetNode": "00000000-0000-0000-0000-000000000105", + "targetPort": "00000000-0000-0000-0000-000000000124" + } + ], + "devices": { + "00000000-0000-0000-0000-000000000101": { + "ip": "192.168.0.20", + "mask": "255.255.255.0", + "dns": "192.168.0.10" + }, + "00000000-0000-0000-0000-000000000102": { + "ip": "192.168.0.10", + "mask": "255.255.255.0", + "dns": "192.168.0.11" + }, + "00000000-0000-0000-0000-000000000103": { + "ip": "192.168.0.11", + "mask": "255.255.255.0" + }, + "00000000-0000-0000-0000-000000000104": { + "ip": "192.168.0.21", + "mask": "255.255.255.0", + "dns": "192.168.0.99" + } + }, + "dnsServers": { + "00000000-0000-0000-0000-000000000102": { + "records": [ + "web.filius.de A 192.168.0.30", + "filius.de MX mail.filius.de", + "mail.filius.de A 192.168.0.31", + "schule.de NS ns.schule.de", + "ns.schule.de A 192.168.0.11", + "filius.de NS ns.filius.de", + "ns.filius.de A 192.168.0.10" + ], + "recursive": true + }, + "00000000-0000-0000-0000-000000000103": { + "records": [ + "www.schule.de A 192.168.0.40" + ] + } + }, + "steps": [] +} diff --git a/tools/java-oracle/README.md b/tools/java-oracle/README.md index 64a9eda..b7d3112 100644 --- a/tools/java-oracle/README.md +++ b/tools/java-oracle/README.md @@ -267,6 +267,26 @@ Nameservers im Antwortteil. Der Resolver folgt dem Verweis selbst (iterativ) und wartet je Anfrage 5 RTT (13750 ms bei 90 %); ohne Server fragt ARP zweimal im Abstand einer RTT, dann scheitert der Aufruf. +Auf dem Netz von `dns-recursive` (sonst wie `dns-lan`) löst 192.168.0.10 +rekursiv auf, sein eigener DNS-Server ist .11, und er führt die Zone +`filius.de` mit sich selbst als Nameserver. Abläufe +(`dns-recursive-java-.java.json`): `address` (eigener Eintrag), +`referral` (`www.schule.de` über den NS von `schule.de`), `unknown` +(`x.unbekannt.org`), `unknownInZone` (`nix.schule.de`, das .11 nicht kennt), +`unknownInOwnZone` (`nix.filius.de`). + +Gemessen (30.09.2026): Findet der Server nichts in seinen Einträgen, fragt +er selbst, mit seinem eigenen Resolver (iterativ, 5 RTT je Anfrage): zuerst +den Nameserver der längsten passenden Zone aus seinen Einträgen, bringt das +nichts, seinen eigenen DNS-Server (nur wenn das nicht er selbst ist). Die +Antwort an den Client enthält, was er gelernt hat, sonst den Verweis (NS und +A). Bei `unknownInZone` fragt er .11 also zweimal. Bei `unknownInOwnZone` +fragt er als Nameserver der Zone sich selbst, ohne Ende: Jede Ebene erzeugt +die nächste (ohne Frames, die Anfragen bleiben im Rechner), der Client gibt +nach 5 RTT auf, dann laufen die inneren Ebenen nacheinander ab und fragen +ersatzweise .11, eine Anfrage alle 55 ms. Ein Java-Fehler; der Kern fragt +sich weder als Nameserver noch als DNS-Server selbst. + ## ARP zur Laufzeit `ArpRun` sendet auf dem Netz von `arp-routed` (10.0.0.2 über einen Switch am diff --git a/tools/java-oracle/run.sh b/tools/java-oracle/run.sh index a2dccd8..eb2cc0c 100755 --- a/tools/java-oracle/run.sh +++ b/tools/java-oracle/run.sh @@ -141,6 +141,16 @@ for run in address:5000 mail:5000 referral:5000 unknown:5000 noServer:22000; do "$root/packages/core/test/scenarios/dns-lan.scenario.json" "$story" "${run##*:}" "$output" 2>&1 | grep -v 'logback\|^$' || true test -s "$output" done +# The same with a recursive server (dns-recursive). unknownInOwnZone asks +# itself without end: a short run, and a bounded heap. +for run in address:5000 referral:5000 unknown:5000 unknownInZone:5000 unknownInOwnZone:15500; do + story="${run%%:*}" + output="$root/packages/core/test/scenarios/dns-recursive-java-$story.java.json" + rm -f "$output" + HOME="$here/.build/home" xvfb-run -a java -Xmx1g -cp "$classpath:$here/.build" DnsRun \ + "$root/packages/core/test/scenarios/dns-recursive.scenario.json" "$story" "${run##*:}" "$output" 2>&1 | grep -v 'logback\|^$' || true + test -s "$output" +done # ARP as Java FILIUS runs it: sends to addresses no card has on arp-routed's # network (ArpRun); frames and the calls, for the core's ARP comparison. diff --git a/tools/java-oracle/src/DnsRun.java b/tools/java-oracle/src/DnsRun.java index 84611b0..76a811e 100644 --- a/tools/java-oracle/src/DnsRun.java +++ b/tools/java-oracle/src/DnsRun.java @@ -12,6 +12,11 @@ // referral 192.168.0.20 asks for www.schule.de (A), referred to .11 // unknown 192.168.0.20 asks for x.unbekannt.org, which no one knows // noServer 192.168.0.21 asks its DNS server 192.168.0.99, which is not there +// +// On dns-recursive's network .10 resolves recursively, its own DNS server is +// .11, and it holds filius.de's NS record, which names itself. There also: +// unknownInZone 192.168.0.20 asks for nix.schule.de, which .11 does not know +// unknownInOwnZone 192.168.0.20 asks for nix.filius.de, in .10's own zone import filius.software.dns.Resolver; @@ -62,6 +67,12 @@ public class DnsRun { case "unknown": call("holeIPAdresse x.unbekannt.org", () -> client.holeIPAdresse("x.unbekannt.org")); break; + case "unknownInZone": + call("holeIPAdresse nix.schule.de", () -> client.holeIPAdresse("nix.schule.de")); + break; + case "unknownInOwnZone": + call("holeIPAdresse nix.filius.de", () -> client.holeIPAdresse("nix.filius.de")); + break; case "noServer": Resolver lost = network.system(LOST_CLIENT).holeDNSClient(); call("holeIPAdresse web.filius.de", () -> lost.holeIPAdresse("web.filius.de"));