From c80d212c903b11167b2077e487315e52418c43f2 Mon Sep 17 00:00:00 2001 From: Julian Kiedaisch Date: Wed, 30 Sep 2026 06:38:01 +0000 Subject: [PATCH] feat(fls): DNS records from /dns/hosts, as Java's server answers them Java's DNS server reads its records from the text file /dns/hosts on every query. dnsHosts.ts ports that reading (DNSServer.leseRecordListe, ResourceRecord, Dateisystem.holeDatei) and keeps in the model the records Java answers: type exactly A, MX or NS, and a name Java accepted. The file stays in the file system as it is. Both oracles read 76 hand-picked hosts files (dns-hosts.cases.txt): the Java oracle through DNSServer.holeResourceRecords (DnsHostsCases), the Swift oracle through TopologyDNSHostsFileCodec (--dns-hosts). The Java view matches Java on every case; the model matches Swift except for 33 named cases in twelve reasons: Java splits at single spaces and LF only, has no comments or "IN", turns a TTL that is no int into 3600, keeps duplicates, and empties names with a leading digit. The Swift dump now carries each server's records; the corpus comparison covers them on 16 files. Co-Authored-By: Claude Opus 5.5 --- CHANGES.md | 6 +- corpus/corpus.swift.json | 400 ++++++++++ packages/core/src/runtime/config.ts | 17 +- packages/fls/src/dnsHosts.ts | 153 ++++ packages/fls/src/importProject.ts | 14 +- packages/fls/src/index.ts | 1 + packages/fls/test/dns-hosts.cases.txt | 76 ++ packages/fls/test/dns-hosts.java.json | 754 +++++++++++++++++++ packages/fls/test/dns-hosts.swift.json | 749 ++++++++++++++++++ packages/fls/test/dnsHosts.test.ts | 209 +++++ packages/fls/test/importProject.test.ts | 34 +- tools/java-oracle/README.md | 13 + tools/java-oracle/run.sh | 9 +- tools/java-oracle/src/DnsHostsCases.java | 50 ++ tools/swift-oracle/README.md | 5 + tools/swift-oracle/Sources/Oracle/main.swift | 36 + tools/swift-oracle/run.sh | 3 +- 17 files changed, 2518 insertions(+), 11 deletions(-) create mode 100644 packages/fls/src/dnsHosts.ts create mode 100644 packages/fls/test/dns-hosts.cases.txt create mode 100644 packages/fls/test/dns-hosts.java.json create mode 100644 packages/fls/test/dns-hosts.swift.json create mode 100644 packages/fls/test/dnsHosts.test.ts create mode 100644 tools/java-oracle/src/DnsHostsCases.java diff --git a/CHANGES.md b/CHANGES.md index 1c08722..c5a2f76 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,7 +12,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `src/model/node.ts` | `Model/TopologyNode.swift` | übertragen; Standard-Ports und -Namen wie Java; ein gegebener Name bleibt, auch leer (Java lässt Router unbenannt) | | `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); dazu Echo-Server- und Client-Konfiguration, die Swift nicht speichert | übertragen; Java-Beschriftungen der Oberfläche weggelassen | +| `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`); 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, ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, UDP, TCP, DHCP, DNS, RIP, Remote Link, WLAN | | `src/runtime/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen | | `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` | @@ -37,14 +37,16 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `src/zip.ts` | `Persistence/TopologyFLSArchive.swift` (Fehler, Grenzen, Pfadregeln, ZIP-Leser/-Schreiber, CRC-32) | übertragen; DEFLATE über `fflate`, mit Apples Abschneide-Verhalten nachgebildet; Pfadfaltung (NFC, Breite, volles Case-Folding) an 41 Paaren gegen das Swift-Orakel gemessen | | `src/archive.ts` | ebenda, Archiv-Teil von Import und Export | übertragen, ohne die XML-Ebene | | `src/javabeans.ts`, `src/javaDefaults.json` | — | neu: XMLEncoder-Leser nach den Regeln von `java.beans.XMLDecoder` (Swift liest elementweise und verliert geteilte Werte); Getter-Standardwerte und statische Konstanten mit `tools/java-oracle` gemessen | -| `src/importProject.ts` | `Persistence/TopologyProjectStore.swift` (Knoten, Ports, Kabel, IP-Konfiguration, Switch/WLAN, Routen, RIP, Firewall, DHCP, Portweiterleitung, Modem, Dateisysteme, Programme, Doku-Elemente des Imports) | übertragen, Eingabe ist Javas Sicht: Name und Port-Anzahl wie Java, abgelehnte Adresse → Swift-Standard, Router-Standardgateway wie Java (Swift verliert es), jede Router-Firewall eingetragen, Konfigurationstexte wie in Swift getrimmt (`swiftTrim`), Rechner ohne Dateisystem leer wie in Java (Swift: Demo-Dateien), alle Programme bleiben (Swift verwirft sechs), Webserver-Dateien bleiben in `/webserver` (Swift: `/www`), keine versteckten Zustandsdateien im Dateisystem; gegen Swifts importiertes Modell auf 13 Dateien geprüft | +| `src/importProject.ts` | `Persistence/TopologyProjectStore.swift` (Knoten, Ports, Kabel, IP-Konfiguration, Switch/WLAN, Routen, RIP, Firewall, DHCP, Portweiterleitung, Modem, Dateisysteme, Programme, Doku-Elemente des Imports) | übertragen, Eingabe ist Javas Sicht: Name und Port-Anzahl wie Java, abgelehnte Adresse → Swift-Standard, Router-Standardgateway wie Java (Swift verliert es), jede Router-Firewall eingetragen, Konfigurationstexte wie in Swift getrimmt (`swiftTrim`), Rechner ohne Dateisystem leer wie in Java (Swift: Demo-Dateien), alle Programme bleiben (Swift verwirft sechs), Webserver-Dateien bleiben in `/webserver` (Swift: `/www`), keine versteckten Zustandsdateien im Dateisystem, DNS-Einträge so, wie Javas Server sie aus `/dns/hosts` beantwortet; gegen Swifts importiertes Modell auf 13 Dateien geprüft | | `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, zwei begründete Abweichungen), Vollständigkeit aller 23, Pings im Kern (über einen Router; über drei Router mit manuellen Routen und Router-Gateways) | | `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule`, `DHCPServer`, `ModemFirmware`, `Dateisystem`, `Datei` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht), DHCP, statische NAT, Modem, Dateisystem und installierte Programme von Rechner und Notebook; `Character.isWhitespace` am JDK gemessen; gegen Java geprüft | | `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen | +| `src/dnsHosts.ts` | Java `DNSServer.leseRecordListe`, `ResourceRecord`, `Dateisystem.holeDatei`; Swift `State/TopologyDNSResolver.swift` (`TopologyDNSHostsFileCodec`) als Vergleich | neu nach Java: Felder nur an einzelnen Leerzeichen, keine Kommentare, ungültige TTL → 3600, Doppelte bleiben; ins Modell kommt nur, was Java beantwortet (Typ genau A/MX/NS, Name nicht leer); an 76 Fällen mit Java und Swift gemessen | | `src/topology.ts` | Java `SzenarioVerwaltung.netzwerkLaden`, Konstruktoren und Setter der Geräte; Swift `Persistence/TopologyProjectStore.swift` (Parser) als Vergleich | neu nach Java statt Übertragung: Geräteart aus der Klasse statt aus der Beschriftung, Geräte ohne Koordinaten bei 0,0 statt verworfen, Werte durch Javas Setter | | `test/javabeans.test.ts` | — | neu: je XMLDecoder-Regel ein Test; alle 23 Korpus-Dateien, Klasse und Position jedes Geräts wie Java | | `test/topology.test.ts` | — | neu: alle 23 Korpus-Dateien Feld für Feld wie Java (Geräte, Schnittstellen, Kabel, Doku-Elemente), dazu die Regeln, die der Korpus nicht berührt | | `test/javaIp.test.ts`, `test/ip-check.*` | — | neu: `IP.ipCheck`-Fälle, Ergebnis aus Java | +| `test/dnsHosts.test.ts`, `test/dns-hosts.*` | — | neu: `/dns/hosts`-Fälle, Javas Sicht wie Java, das Modell wie Swift bis auf 33 benannte Fälle in zwölf Gründen | | `test/zip.test.ts` | `TopologyProjectPersistenceTests.swift` (Archiv-Ablehnungstests, `makeMinimalZIP`, `makePayloadZIP`) | übertragen, ergänzt um DEFLATE-Grenzfälle | | `test/corpus.test.ts` | — | neu: 23 Korpus-Dateien, gegengeprüft mit `fflate.unzipSync` | | `test/pathFolding.test.ts`, `test/path-folding.*.json` | — | neu: welche Pfade dieselbe Datei meinen, Ergebnis aus dem Swift-Kern | diff --git a/corpus/corpus.swift.json b/corpus/corpus.swift.json index b4dc99f..2bfb85b 100644 --- a/corpus/corpus.swift.json +++ b/corpus/corpus.swift.json @@ -266,6 +266,26 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "filius.de", + "MX", + 3600, + "smtp.filius.de." + ], + [ + "dns-root", + "A", + 3600, + "141.99.5.13" + ], + [ + ".", + "NS", + 3600, + "dns-root." + ] + ], "recursiveResolutionEnabled" : true }, "installedPrograms" : [ @@ -464,6 +484,20 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "dns.de", + "A", + 3600, + "141.99.5.14" + ], + [ + "de", + "NS", + 3600, + "dns.de." + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -678,6 +712,20 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "dns.filius.de", + "A", + 3600, + "141.99.5.15" + ], + [ + "filius.de", + "NS", + 3600, + "dns.filius.de." + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -727,6 +775,14 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "www.filius.de", + "A", + 3600, + "141.99.5.10" + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -1219,6 +1275,14 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "mywebserver.de", + "A", + 3600, + "160.10.20.10" + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -1886,6 +1950,14 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "www.filius.de", + "A", + 3600, + "141.99.2.10" + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -3480,6 +3552,26 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "filius.de", + "MX", + 3600, + "smtp.filius.de." + ], + [ + "dns-root", + "A", + 3600, + "141.99.5.13" + ], + [ + ".", + "NS", + 3600, + "dns-root." + ] + ], "recursiveResolutionEnabled" : true }, "installedPrograms" : [ @@ -3678,6 +3770,20 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "dns.de", + "A", + 3600, + "141.99.5.14" + ], + [ + "de", + "NS", + 3600, + "dns.de." + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -3892,6 +3998,20 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "dns.filius.de", + "A", + 3600, + "141.99.5.15" + ], + [ + "filius.de", + "NS", + 3600, + "dns.filius.de." + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -3941,6 +4061,14 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "www.filius.de", + "A", + 3600, + "141.99.5.10" + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -4475,6 +4603,20 @@ }, "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "www.mein-server.de", + "A", + 3600, + "192.168.0.100" + ], + [ + "xy", + "A", + 3600, + "192.168.0.100" + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -5868,6 +6010,38 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "www.filius.de", + "A", + 3600, + "141.99.50.231" + ], + [ + "mail-filius.de", + "A", + 3600, + "141.99.50.232" + ], + [ + "mail-senior.de", + "A", + 3600, + "141.99.50.233" + ], + [ + "filius.de", + "MX", + 3600, + "mail-filius.de." + ], + [ + "senior.de", + "MX", + 3600, + "mail-senior.de." + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -6958,6 +7132,44 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "webserver.com", + "A", + 3600, + "120.130.140.80" + ], + [ + "gmx.com", + "MX", + 3600, + "mail.gmx.com." + ], + [ + "mail.gmx.com", + "A", + 3600, + "130.100.200.25" + ], + [ + "mail.webmail.com", + "A", + 3600, + "140.100.200.25" + ], + [ + "webmail.com", + "MX", + 3600, + "mail.webmail.com." + ], + [ + "myschool.org", + "A", + 3600, + "42.0.0.10" + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -7198,6 +7410,14 @@ }, "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "meine-schule.de", + "A", + 3600, + "10.0.0.1" + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -8687,6 +8907,44 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "webserver.de", + "A", + 3600, + "120.130.140.80" + ], + [ + "gmx.de", + "MX", + 3600, + "mail.gmx.de." + ], + [ + "mail.gmx.de", + "A", + 3600, + "130.100.200.25" + ], + [ + "mail.webmail.de", + "A", + 3600, + "140.100.200.25" + ], + [ + "webmail.de", + "MX", + 3600, + "mail.webmail.de." + ], + [ + "meine-schule.de", + "A", + 3600, + "42.0.0.10" + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -8927,6 +9185,14 @@ }, "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "meine-schule.de", + "A", + 3600, + "10.0.0.1" + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -10361,6 +10627,14 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "www.filius.de", + "A", + 3600, + "141.99.5.10" + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -11142,6 +11416,32 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "mail-senior.de", + "A", + 3600, + "141.99.50.233" + ], + [ + "mail-filius.de", + "A", + 3600, + "141.99.50.232" + ], + [ + "filius.de", + "MX", + 3600, + "mail-filius.de." + ], + [ + "senior.de", + "MX", + 3600, + "mail-senior.de." + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -11409,6 +11709,38 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "www.filius.de", + "A", + 3600, + "141.99.50.231" + ], + [ + "filius.de", + "MX", + 3600, + "mail-filius.de." + ], + [ + "senior.de", + "MX", + 3600, + "mail-senior.de." + ], + [ + "mail-filius.de", + "A", + 3600, + "141.99.50.184" + ], + [ + "mail-senior.de", + "A", + 3600, + "141.99.50.184" + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -12923,6 +13255,20 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "www.filius.de", + "A", + 3600, + "141.99.5.10" + ], + [ + "www.jupiter.de", + "A", + 3600, + "141.99.4.10" + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -14481,6 +14827,14 @@ }, "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "www.filius.de", + "A", + 3600, + "141.99.2.10" + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -15260,6 +15614,14 @@ }, "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "www.filius.de", + "A", + 3600, + "42.0.0.2" + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -15417,6 +15779,14 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "webserver.de", + "A", + 3600, + "120.130.140.10" + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -15956,6 +16326,14 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "mywebserver.de", + "A", + 3600, + "160.10.20.10" + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -16939,6 +17317,14 @@ "system" : { "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "www.filius.de", + "A", + 3600, + "141.99.2.10" + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ @@ -17670,6 +18056,20 @@ }, "dnsServer" : { "forwardingServerIPAddress" : null, + "records" : [ + [ + "www.mein-server.de", + "A", + 3600, + "192.168.0.100" + ], + [ + "xy", + "A", + 3600, + "192.168.0.100" + ] + ], "recursiveResolutionEnabled" : false }, "installedPrograms" : [ diff --git a/packages/core/src/runtime/config.ts b/packages/core/src/runtime/config.ts index 4f774d4..26311c1 100644 --- a/packages/core/src/runtime/config.ts +++ b/packages/core/src/runtime/config.ts @@ -180,10 +180,25 @@ export interface GnutellaConfiguration { readonly maximumKnownPeers: number; } -/** The DNS records follow with the hosts file (Java's /dns/hosts). */ +export type DnsRecordType = 'A' | 'MX' | 'NS'; + +/** + * One record a DNS server answers with (Swift's TopologyDNSResourceRecord): + * `name` in lower case without the final dot (the root is "."), `target` an + * IPv4 address for A, an absolute name ("mail.filius.de.") for MX and NS. + */ +export interface DnsResourceRecord { + readonly name: string; + readonly type: DnsRecordType; + readonly ttlSeconds: number; + readonly target: string; +} + export interface DnsServerConfiguration { readonly recursiveResolutionEnabled: boolean; readonly forwardingServerIPAddress?: string; + /** In the order of Java's /dns/hosts, which stays in the file system as it is. */ + readonly records: readonly DnsResourceRecord[]; } /** diff --git a/packages/fls/src/dnsHosts.ts b/packages/fls/src/dnsHosts.ts new file mode 100644 index 0000000..80ed4df --- /dev/null +++ b/packages/fls/src/dnsHosts.ts @@ -0,0 +1,153 @@ +// Java's DNS server keeps its records in the text file /dns/hosts and reads +// them on every query (DNSServer.leseRecordListe, ResourceRecord(String)). +// Measured on test/dns-hosts.cases.txt against Java (dns-hosts.java.json) +// and Swift (dns-hosts.swift.json); where the two differ, Java decides: +// - fields are split at single spaces only: a tab or NBSP joins two fields; +// - there are no comments, no "IN" and no zone-file order; +// - a TTL that is not a Java int silently becomes 3600; +// - a name Java does not accept becomes the empty name, and the record +// still counts - it just never answers, like any record whose type is +// not exactly A, MX or NS (lower-case "a" included); +// - duplicates stay. + +import type { DnsResourceRecord } from '@mdg-filius/core'; +import { javaIpCheck, javaParseInt } from './javaIp.js'; +import type { FlsFileTree } from './systemSoftware.js'; + +/** A record as Java's ResourceRecord holds it (the getters). */ +export interface JavaResourceRecord { + readonly domainName: string; + readonly type: string; + readonly ttl: number; + readonly rdata: string; +} + +export const DNS_HOSTS_PATH = '/dns/hosts'; + +// EingabenUeberpruefung.musterDomain, including its quirks: labels start with +// a letter, labels after the first may hold a backslash, and the empty +// string is a valid domain. +const JAVA_DOMAIN = /^([a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*)?\.?$/; +// EingabenUeberpruefung.musterIpAdresse. +const JAVA_IP_ADDRESS = + /^(0*([1-9]?[0-9]?|1[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))\.((0*(1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))\.){2}(0*(1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))$/; + +/** `String.trim()`: every UTF-16 unit up to U+0020 off both ends. */ +function javaTrim(text: string): string { + return text.replace(/^[\u0000-\u0020]+|[\u0000-\u0020]+$/g, ''); +} + +/** + * The content of /dns/hosts as `Dateisystem.holeDatei` finds it: the first + * node in preorder whose path equals it ignoring case, and only if that node + * is a file. Undefined when there is none; a file without content reads as + * empty (Java would fail on it). + */ +export function javaHostsFileContent(root: FlsFileTree | undefined): string | undefined { + if (root === undefined) return undefined; + const found = firstInPreorder(root, '', (path) => javaEqualsIgnoreCase(path, DNS_HOSTS_PATH)); + return found?.kind === 'file' ? (found.content ?? '') : undefined; +} + +/** Preorder below the root; a node's path is "/" plus each name after the root's. */ +function firstInPreorder(node: FlsFileTree, path: string, matches: (path: string) => boolean): FlsFileTree | undefined { + for (const child of node.children) { + const childPath = `${path}/${child.kind === 'other' ? child.className : String(child.name)}`; + if (matches(childPath)) return child; + const found = firstInPreorder(child, childPath, matches); + if (found !== undefined) return found; + } + return undefined; +} + +/** + * `String.equalsIgnoreCase` against an ASCII path, unit by unit: equal after + * Character.toUpperCase, so the long s (U+017F) is an "s". Java also + * compares the lower case of both; for the letters of /dns/hosts that adds + * no unit (measured over the BMP). + */ +function javaEqualsIgnoreCase(text: string, asciiPath: string): boolean { + if (text.length !== asciiPath.length) return false; + for (let index = 0; index < text.length; index++) { + if (text[index]!.toUpperCase() !== asciiPath[index]!.toUpperCase()) return false; + } + return true; +} + +/** `DNSServer.leseRecordListe` on the content of /dns/hosts. */ +export function readJavaDnsHosts(content: string): JavaResourceRecord[] { + const records: JavaResourceRecord[] = []; + for (const token of content.split('\n')) { + const line = javaTrim(token); + // line.split(" ", 5).length < 4: fewer than three single spaces. + if (line === '' || line.split(' ').length < 4) continue; + const record = javaResourceRecord(line); + if (isValidJavaRecord(record)) records.push(record); + } + return records; +} + +/** `new ResourceRecord(line)`: up to four space-separated fields. */ +function javaResourceRecord(line: string): JavaResourceRecord { + const fields = line.split(' ').filter((field) => field !== '').map(javaTrim); + let domainName = ''; + let type = 'A'; + let ttl = 3600; + let rdata = ''; + if (fields[0] !== undefined && JAVA_DOMAIN.test(fields[0])) { + domainName = fields[0].endsWith('.') ? fields[0] : `${fields[0]}.`; + } + if (fields[1] !== undefined) type = fields[1]; + if (fields[2] !== undefined) ttl = javaParseInt(fields[2]) ?? ttl; + if (fields[3] !== undefined) { + // Java tests ".*\." (no line terminator before the dot); a target with + // one fails the domain check anyway, so endsWith says the same. + const needsDot = (type === 'NS' || type === 'MX') && !fields[3].endsWith('.'); + rdata = needsDot ? `${fields[3]}.` : fields[3]; + } + return { domainName, type, ttl, rdata }; +} + +function isValidJavaRecord(record: JavaResourceRecord): boolean { + switch (record.type) { + case 'A': + return JAVA_DOMAIN.test(record.domainName) && JAVA_IP_ADDRESS.test(record.rdata); + case 'MX': + case 'NS': + return JAVA_DOMAIN.test(record.domainName) && JAVA_DOMAIN.test(record.rdata); + default: + return true; + } +} + +/** + * The records Java answers with, in the model's form: Java looks them up by + * type (exactly "A", "MX", "NS") and by name ignoring case, so a record with + * the empty name or another type never answers and is left out. The file + * itself stays as it is. + */ +export function dnsRecordsOf(records: readonly JavaResourceRecord[], warn: (message: string) => void): DnsResourceRecord[] { + const answered: DnsResourceRecord[] = []; + for (const record of records) { + if (record.domainName === '') continue; + const name = record.domainName === '.' ? '.' : record.domainName.slice(0, -1).toLowerCase(); + switch (record.type) { + case 'A': { + const address = javaIpCheck(record.rdata); + if (address === null) { + warn(`DNS record '${name} A ${record.rdata}' has no usable IPv4 address, left out (it stays in ${DNS_HOSTS_PATH})`); + continue; + } + answered.push({ name, type: 'A', ttlSeconds: record.ttl, target: address }); + break; + } + case 'MX': + case 'NS': + answered.push({ name, type: record.type, ttlSeconds: record.ttl, target: record.rdata.toLowerCase() }); + break; + default: + break; + } + } + return answered; +} diff --git a/packages/fls/src/importProject.ts b/packages/fls/src/importProject.ts index 36097a5..dfff84c 100644 --- a/packages/fls/src/importProject.ts +++ b/packages/fls/src/importProject.ts @@ -33,7 +33,10 @@ // - a web server's files stay in /webserver (its document root), where Swift // moves them to /www; and no program state is written into a host's files // (Swift keeps Gnutella and e-mail state there as hidden JSON files). -// Not here yet: DNS records (from /dns/hosts) and e-mail accounts. +// - a DNS server's records are what Java's server answers from /dns/hosts +// (see dnsHosts.ts); Swift reads the file its own way — tabs, comments, +// "IN", names with a leading digit — and drops Java's duplicates. +// Not here yet: e-mail accounts. import { canonicalMacAddress, @@ -88,6 +91,7 @@ import { type TopologyNode, type VirtualFileContent, } from '@mdg-filius/core'; +import { dnsRecordsOf, javaHostsFileContent, readJavaDnsHosts } from './dnsHosts.js'; import type { FlsApplicationClass, FlsFileTree, FlsFirewall } from './systemSoftware.js'; import type { FlsDocumentationItem, FlsInterface, FlsNode, FlsNodeClass, FlsTopology } from './topology.js'; @@ -415,11 +419,15 @@ function registerPrograms(source: FlsNode, node: TopologyNode, system: SystemCon const forwarder = trimmedOrEmpty(source.interfaces[0]?.dns); const primary = trimmedOrEmpty(source.interfaces[0]?.ip); const forwarding = recursive && forwarder !== '' && forwarder !== primary; + const hosts = javaHostsFileContent(source.system.fileSystem); + const records = dnsRecordsOf(readJavaDnsHosts(hosts ?? ''), (message) => + warnings.push(`node '${node.displayName}': ${message}`), + ); system.dnsServerConfigurations.set( node.id, forwarding - ? { recursiveResolutionEnabled: recursive, forwardingServerIPAddress: forwarder } - : { recursiveResolutionEnabled: recursive }, + ? { recursiveResolutionEnabled: recursive, forwardingServerIPAddress: forwarder, records } + : { recursiveResolutionEnabled: recursive, records }, ); break; } diff --git a/packages/fls/src/index.ts b/packages/fls/src/index.ts index add02f6..654e981 100644 --- a/packages/fls/src/index.ts +++ b/packages/fls/src/index.ts @@ -1,5 +1,6 @@ // Public surface of the .fls codec. export * from './archive.js'; +export * from './dnsHosts.js'; export * from './importProject.js'; export * from './javaIp.js'; export * from './javabeans.js'; diff --git a/packages/fls/test/dns-hosts.cases.txt b/packages/fls/test/dns-hosts.cases.txt new file mode 100644 index 0000000..540f084 --- /dev/null +++ b/packages/fls/test/dns-hosts.cases.txt @@ -0,0 +1,76 @@ +www.filius.de. A 3600 1.2.3.4\u000A +www.filius.de A 3600 1.2.3.4\u000A +www.filius.de A 3600 1.2.3.4 +WWW.Filius.DE A 3600 1.2.3.4\u000A +www.filius.de a 3600 1.2.3.4\u000A +filius.de mx 3600 mail.filius.de\u000A +www.filius.de A 3600 1.2.3.4\u000A +www.filius.de\u0009A\u00093600\u00091.2.3.4\u000A + www.filius.de A 3600 1.2.3.4 \u000A +www.filius.de A 3600 1.2.3.4 # comment\u000A +# www.filius.de A 3600 1.2.3.4\u000A +www.filius.de A 3600 1.2.3.4#x\u000A +www.filius.de A IN 3600 1.2.3.4\u000A +www.filius.de 3600 IN A 1.2.3.4\u000A +www.filius.de A x 1.2.3.4\u000A +www.filius.de A -5 1.2.3.4\u000A +www.filius.de A 0 1.2.3.4\u000A +www.filius.de A +5 1.2.3.4\u000A +www.filius.de A 4294967295 1.2.3.4\u000A +www.filius.de A 99999999999 1.2.3.4\u000A +www.filius.de A 3600 010.0.0.1\u000A +www.filius.de A 3600 1.2.3\u000A +www.filius.de A 3600 256.0.0.1\u000A +www.filius.de A 3600 1.2.3.4.\u000A +www.filius.de A 3600 1.2.3.4 extra\u000A +www.filius.de A 3600\u000A +1und1.de A 3600 1.2.3.4\u000A +a_b.de A 3600 1.2.3.4\u000A +-a.de A 3600 1.2.3.4\u000A +a-.de A 3600 1.2.3.4\u000A +www..de A 3600 1.2.3.4\u000A +www.filius.de.. A 3600 1.2.3.4\u000A +www.1und1.de A 3600 1.2.3.4\u000A +www.a\u005Cb.de A 3600 1.2.3.4\u000A +m\u00FCller.de A 3600 1.2.3.4\u000A +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.de A 3600 1.2.3.4\u000A +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.de A 3600 1.2.3.4\u000A +localhost A 3600 127.0.0.1\u000A +. NS 3600 dns-root.\u000A +de NS 3600 dns.de\u000A +de. NS 3600 .\u000A +filius.de MX 3600 mail.filius.de\u000A +filius.de MX 3600 10 mail.filius.de\u000A +filius.de MX 3600 1.2.3.4\u000A +filius.de CNAME 3600 www.filius.de\u000A +filius.de TXT 3600 hello\u000A +www.filius.de A 3600 1.2.3.4\u000Awww.filius.de A 3600 1.2.3.4\u000A +www.filius.de A 3600 1.2.3.4\u000Awww.filius.de A 7200 1.2.3.4\u000A +www.filius.de A 3600 1.2.3.4\u000AWWW.filius.de. A 3600 1.2.3.4\u000A +www.filius.de A 3600 1.2.3.4\u000Awww.filius.de A 3600 1.2.3.5\u000A +www.filius.de A 3600 1.2.3.4\u000D\u000Amail.filius.de A 3600 1.2.3.5\u000D\u000A +www.filius.de A 3600 1.2.3.4\u000Dmail.filius.de A 3600 1.2.3.5\u000D +www.filius.de A 3600 1.2.3.4\u2028mail.filius.de A 3600 1.2.3.5\u000A +www.filius.de\u00A0A 3600 1.2.3.4\u000A +www.filius.de A 3600 1.2.3.4\u0085\u000A + +\u000A\u000Awww.filius.de A 3600 1.2.3.4\u000A\u000A +filius.de. MX 3600 smtp.filius.de.\u000Adns-root. A 3600 141.99.5.13\u000A. NS 3600 dns-root.\u000A +www.filius.de A \u0661\u0660 1.2.3.4\u000A +www.filius.de A 2147483647 1.2.3.4\u000A +www.filius.de A -2147483648 1.2.3.4\u000A +www.filius.de A 2147483648 1.2.3.4\u000A +www.filius.de \u0009 A 3600 1.2.3.4\u000A +www.filius.de A \u0009 3600 1.2.3.4\u000A +www.filius.de A 3600 1.2.3.4 \u0009\u000A +www.filius.de A 3600 1.2.3.4\u000A +www.filius.de A 3600 1.2.3.4\u000A +www.filius.de A 3600 0.0.0.0\u000A +www.filius.de A 3600 00000000000000000001.2.3.4\u000A +filius.de NS 3600 dns..de\u000A +filius.de NS 3600 dns.filius.de..\u000A +filius.de. MX 3600 Mail.Filius.DE.\u000A +www.filius.de A 3600 .1.2.3\u000A +www.filius.de A 3600 1.2.3.4\u0001\u000A +www.filius.de A 3600 1.2.3.4\u00A0\u000A +\u001Fwww.filius.de A 3600 1.2.3.4\u000A diff --git a/packages/fls/test/dns-hosts.java.json b/packages/fls/test/dns-hosts.java.json new file mode 100644 index 0000000..ed0d739 --- /dev/null +++ b/packages/fls/test/dns-hosts.java.json @@ -0,0 +1,754 @@ +[ + [ + "www.filius.de. A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "WWW.Filius.DE A 3600 1.2.3.4\\u000A", + [ + [ + "WWW.Filius.DE.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de a 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "a", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "filius.de mx 3600 mail.filius.de\\u000A", + [ + [ + "filius.de.", + "mx", + 3600, + "mail.filius.de" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de\\u0009A\\u00093600\\u00091.2.3.4\\u000A", + [] + ], + [ + " www.filius.de A 3600 1.2.3.4 \\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4 # comment\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "# www.filius.de A 3600 1.2.3.4\\u000A", + [ + [ + "", + "www.filius.de", + 3600, + "3600" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4#x\\u000A", + [] + ], + [ + "www.filius.de A IN 3600 1.2.3.4\\u000A", + [] + ], + [ + "www.filius.de 3600 IN A 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "3600", + 3600, + "A" + ] + ] + ], + [ + "www.filius.de A x 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A -5 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + -5, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 0 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + 0, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A +5 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + 5, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 4294967295 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 99999999999 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 010.0.0.1\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "010.0.0.1" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3\\u000A", + [] + ], + [ + "www.filius.de A 3600 256.0.0.1\\u000A", + [] + ], + [ + "www.filius.de A 3600 1.2.3.4.\\u000A", + [] + ], + [ + "www.filius.de A 3600 1.2.3.4 extra\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600\\u000A", + [] + ], + [ + "1und1.de A 3600 1.2.3.4\\u000A", + [ + [ + "", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "a_b.de A 3600 1.2.3.4\\u000A", + [ + [ + "", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "-a.de A 3600 1.2.3.4\\u000A", + [ + [ + "", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "a-.de A 3600 1.2.3.4\\u000A", + [ + [ + "", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www..de A 3600 1.2.3.4\\u000A", + [ + [ + "", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de.. A 3600 1.2.3.4\\u000A", + [ + [ + "", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.1und1.de A 3600 1.2.3.4\\u000A", + [ + [ + "", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.a\\u005Cb.de A 3600 1.2.3.4\\u000A", + [ + [ + "www.a\\b.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "m\\u00FCller.de A 3600 1.2.3.4\\u000A", + [ + [ + "", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.de A 3600 1.2.3.4\\u000A", + [ + [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.de A 3600 1.2.3.4\\u000A", + [ + [ + "", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "localhost A 3600 127.0.0.1\\u000A", + [ + [ + "localhost.", + "A", + 3600, + "127.0.0.1" + ] + ] + ], + [ + ". NS 3600 dns-root.\\u000A", + [ + [ + ".", + "NS", + 3600, + "dns-root." + ] + ] + ], + [ + "de NS 3600 dns.de\\u000A", + [ + [ + "de.", + "NS", + 3600, + "dns.de." + ] + ] + ], + [ + "de. NS 3600 .\\u000A", + [ + [ + "de.", + "NS", + 3600, + "." + ] + ] + ], + [ + "filius.de MX 3600 mail.filius.de\\u000A", + [ + [ + "filius.de.", + "MX", + 3600, + "mail.filius.de." + ] + ] + ], + [ + "filius.de MX 3600 10 mail.filius.de\\u000A", + [] + ], + [ + "filius.de MX 3600 1.2.3.4\\u000A", + [] + ], + [ + "filius.de CNAME 3600 www.filius.de\\u000A", + [ + [ + "filius.de.", + "CNAME", + 3600, + "www.filius.de" + ] + ] + ], + [ + "filius.de TXT 3600 hello\\u000A", + [ + [ + "filius.de.", + "TXT", + 3600, + "hello" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000Awww.filius.de A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ], + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000Awww.filius.de A 7200 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ], + [ + "www.filius.de.", + "A", + 7200, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000AWWW.filius.de. A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ], + [ + "WWW.filius.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000Awww.filius.de A 3600 1.2.3.5\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ], + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.5" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000D\\u000Amail.filius.de A 3600 1.2.3.5\\u000D\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ], + [ + "mail.filius.de.", + "A", + 3600, + "1.2.3.5" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000Dmail.filius.de A 3600 1.2.3.5\\u000D", + [] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u2028mail.filius.de A 3600 1.2.3.5\\u000A", + [] + ], + [ + "www.filius.de\\u00A0A 3600 1.2.3.4\\u000A", + [] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u0085\\u000A", + [] + ], + [ + "", + [] + ], + [ + "\\u000A\\u000Awww.filius.de A 3600 1.2.3.4\\u000A\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "filius.de. MX 3600 smtp.filius.de.\\u000Adns-root. A 3600 141.99.5.13\\u000A. NS 3600 dns-root.\\u000A", + [ + [ + "filius.de.", + "MX", + 3600, + "smtp.filius.de." + ], + [ + "dns-root.", + "A", + 3600, + "141.99.5.13" + ], + [ + ".", + "NS", + 3600, + "dns-root." + ] + ] + ], + [ + "www.filius.de A \\u0661\\u0660 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + 10, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 2147483647 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + 2147483647, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A -2147483648 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + -2147483648, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 2147483648 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de \\u0009 A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "", + 3600, + "3600" + ] + ] + ], + [ + "www.filius.de A \\u0009 3600 1.2.3.4\\u000A", + [] + ], + [ + "www.filius.de A 3600 1.2.3.4 \\u0009\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 0.0.0.0\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "0.0.0.0" + ] + ] + ], + [ + "www.filius.de A 3600 00000000000000000001.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "00000000000000000001.2.3.4" + ] + ] + ], + [ + "filius.de NS 3600 dns..de\\u000A", + [] + ], + [ + "filius.de NS 3600 dns.filius.de..\\u000A", + [] + ], + [ + "filius.de. MX 3600 Mail.Filius.DE.\\u000A", + [ + [ + "filius.de.", + "MX", + 3600, + "Mail.Filius.DE." + ] + ] + ], + [ + "www.filius.de A 3600 .1.2.3\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + ".1.2.3" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u0001\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u00A0\\u000A", + [] + ], + [ + "\\u001Fwww.filius.de A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de.", + "A", + 3600, + "1.2.3.4" + ] + ] + ] +] diff --git a/packages/fls/test/dns-hosts.swift.json b/packages/fls/test/dns-hosts.swift.json new file mode 100644 index 0000000..28ab792 --- /dev/null +++ b/packages/fls/test/dns-hosts.swift.json @@ -0,0 +1,749 @@ +[ + [ + "www.filius.de. A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "WWW.Filius.DE A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de a 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "filius.de mx 3600 mail.filius.de\\u000A", + [ + [ + "filius.de", + "MX", + 3600, + "mail.filius.de." + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de\\u0009A\\u00093600\\u00091.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + " www.filius.de A 3600 1.2.3.4 \\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4 # comment\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "# www.filius.de A 3600 1.2.3.4\\u000A", + [ + + ] + ], + [ + "www.filius.de A 3600 1.2.3.4#x\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A IN 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de 3600 IN A 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A x 1.2.3.4\\u000A", + [ + + ] + ], + [ + "www.filius.de A -5 1.2.3.4\\u000A", + [ + + ] + ], + [ + "www.filius.de A 0 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 0, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A +5 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 5, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 4294967295 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 4294967295, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 99999999999 1.2.3.4\\u000A", + [ + + ] + ], + [ + "www.filius.de A 3600 010.0.0.1\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "10.0.0.1" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3\\u000A", + [ + + ] + ], + [ + "www.filius.de A 3600 256.0.0.1\\u000A", + [ + + ] + ], + [ + "www.filius.de A 3600 1.2.3.4.\\u000A", + [ + + ] + ], + [ + "www.filius.de A 3600 1.2.3.4 extra\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600\\u000A", + [ + + ] + ], + [ + "1und1.de A 3600 1.2.3.4\\u000A", + [ + [ + "1und1.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "a_b.de A 3600 1.2.3.4\\u000A", + [ + + ] + ], + [ + "-a.de A 3600 1.2.3.4\\u000A", + [ + + ] + ], + [ + "a-.de A 3600 1.2.3.4\\u000A", + [ + + ] + ], + [ + "www..de A 3600 1.2.3.4\\u000A", + [ + + ] + ], + [ + "www.filius.de.. A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.1und1.de A 3600 1.2.3.4\\u000A", + [ + [ + "www.1und1.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.a\\u005Cb.de A 3600 1.2.3.4\\u000A", + [ + + ] + ], + [ + "m\\u00FCller.de A 3600 1.2.3.4\\u000A", + [ + + ] + ], + [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.de A 3600 1.2.3.4\\u000A", + [ + [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.de A 3600 1.2.3.4\\u000A", + [ + + ] + ], + [ + "localhost A 3600 127.0.0.1\\u000A", + [ + [ + "localhost", + "A", + 3600, + "127.0.0.1" + ] + ] + ], + [ + ". NS 3600 dns-root.\\u000A", + [ + [ + ".", + "NS", + 3600, + "dns-root." + ] + ] + ], + [ + "de NS 3600 dns.de\\u000A", + [ + [ + "de", + "NS", + 3600, + "dns.de." + ] + ] + ], + [ + "de. NS 3600 .\\u000A", + [ + + ] + ], + [ + "filius.de MX 3600 mail.filius.de\\u000A", + [ + [ + "filius.de", + "MX", + 3600, + "mail.filius.de." + ] + ] + ], + [ + "filius.de MX 3600 10 mail.filius.de\\u000A", + [ + [ + "filius.de", + "MX", + 3600, + "10." + ] + ] + ], + [ + "filius.de MX 3600 1.2.3.4\\u000A", + [ + [ + "filius.de", + "MX", + 3600, + "1.2.3.4." + ] + ] + ], + [ + "filius.de CNAME 3600 www.filius.de\\u000A", + [ + + ] + ], + [ + "filius.de TXT 3600 hello\\u000A", + [ + + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000Awww.filius.de A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000Awww.filius.de A 7200 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000AWWW.filius.de. A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000Awww.filius.de A 3600 1.2.3.5\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ], + [ + "www.filius.de", + "A", + 3600, + "1.2.3.5" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000D\\u000Amail.filius.de A 3600 1.2.3.5\\u000D\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ], + [ + "mail.filius.de", + "A", + 3600, + "1.2.3.5" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000Dmail.filius.de A 3600 1.2.3.5\\u000D", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ], + [ + "mail.filius.de", + "A", + 3600, + "1.2.3.5" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u2028mail.filius.de A 3600 1.2.3.5\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ], + [ + "mail.filius.de", + "A", + 3600, + "1.2.3.5" + ] + ] + ], + [ + "www.filius.de\\u00A0A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u0085\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "", + [ + + ] + ], + [ + "\\u000A\\u000Awww.filius.de A 3600 1.2.3.4\\u000A\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "filius.de. MX 3600 smtp.filius.de.\\u000Adns-root. A 3600 141.99.5.13\\u000A. NS 3600 dns-root.\\u000A", + [ + [ + "filius.de", + "MX", + 3600, + "smtp.filius.de." + ], + [ + "dns-root", + "A", + 3600, + "141.99.5.13" + ], + [ + ".", + "NS", + 3600, + "dns-root." + ] + ] + ], + [ + "www.filius.de A \\u0661\\u0660 1.2.3.4\\u000A", + [ + + ] + ], + [ + "www.filius.de A 2147483647 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 2147483647, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A -2147483648 1.2.3.4\\u000A", + [ + + ] + ], + [ + "www.filius.de A 2147483648 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 2147483648, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de \\u0009 A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A \\u0009 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4 \\u0009\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "www.filius.de A 3600 0.0.0.0\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "0.0.0.0" + ] + ] + ], + [ + "www.filius.de A 3600 00000000000000000001.2.3.4\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "filius.de NS 3600 dns..de\\u000A", + [ + + ] + ], + [ + "filius.de NS 3600 dns.filius.de..\\u000A", + [ + [ + "filius.de", + "NS", + 3600, + "dns.filius.de." + ] + ] + ], + [ + "filius.de. MX 3600 Mail.Filius.DE.\\u000A", + [ + [ + "filius.de", + "MX", + 3600, + "mail.filius.de." + ] + ] + ], + [ + "www.filius.de A 3600 .1.2.3\\u000A", + [ + + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u0001\\u000A", + [ + + ] + ], + [ + "www.filius.de A 3600 1.2.3.4\\u00A0\\u000A", + [ + [ + "www.filius.de", + "A", + 3600, + "1.2.3.4" + ] + ] + ], + [ + "\\u001Fwww.filius.de A 3600 1.2.3.4\\u000A", + [ + + ] + ] +] diff --git a/packages/fls/test/dnsHosts.test.ts b/packages/fls/test/dnsHosts.test.ts new file mode 100644 index 0000000..2d2ff3c --- /dev/null +++ b/packages/fls/test/dnsHosts.test.ts @@ -0,0 +1,209 @@ +// Java's reading of /dns/hosts (DNSServer.leseRecordListe): every case in +// dns-hosts.cases.txt was run through Java (dns-hosts.java.json) and through +// Swift's hosts-file codec (dns-hosts.swift.json, tools/swift-oracle). The +// Java view must match Java exactly; the records the model keeps must match +// Swift except where Java answers differently — each case named below. + +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import type { FlsFileTree } from '../src/index.js'; +import { dnsRecordsOf, javaHostsFileContent, readJavaDnsHosts } from '../src/dnsHosts.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +type Row = [string, string, number, string]; +const javaMeasured = JSON.parse(readFileSync(join(here, 'dns-hosts.java.json'), 'utf8')) as [string, Row[]][]; +const swiftMeasured = JSON.parse(readFileSync(join(here, 'dns-hosts.swift.json'), 'utf8')) as [string, Row[]][]; + +/** The cases file's escapes: a backslash, `u` and four hex digits for one UTF-16 unit. */ +function unescape(line: string): string { + return line.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex: string) => String.fromCharCode(Number.parseInt(hex, 16))); +} + +function modelRows(line: string, warnings: string[] = []): Row[] { + return dnsRecordsOf(readJavaDnsHosts(unescape(line)), (message) => warnings.push(message)).map((record) => [ + record.name, + record.type, + record.ttlSeconds, + record.target, + ]); +} + +/** Lines Swift reads as records that Java never answers. */ +const SWIFT_READS_JAVA_NEVER_ANSWERS: { reason: string; lines: string[] }[] = [ + { + reason: 'a type in lower case is another type in Java, which never answers', + lines: [ + String.raw`www.filius.de a 3600 1.2.3.4\u000A`, + String.raw`filius.de mx 3600 mail.filius.de\u000A`, + ], + }, + { + reason: 'Java splits fields at single spaces only: a tab or NBSP is part of a field', + lines: [ + String.raw`www.filius.de\u0009A\u00093600\u00091.2.3.4\u000A`, + String.raw`www.filius.de\u00A0A 3600 1.2.3.4\u000A`, + String.raw`www.filius.de A 3600 1.2.3.4\u00A0\u000A`, + String.raw`www.filius.de \u0009 A 3600 1.2.3.4\u000A`, + String.raw`www.filius.de A \u0009 3600 1.2.3.4\u000A`, + ], + }, + { + reason: 'Java splits lines at LF only: CR, LS and NEL stay in the line', + lines: [ + String.raw`www.filius.de A 3600 1.2.3.4\u000Dmail.filius.de A 3600 1.2.3.5\u000D`, + String.raw`www.filius.de A 3600 1.2.3.4\u2028mail.filius.de A 3600 1.2.3.5\u000A`, + String.raw`www.filius.de A 3600 1.2.3.4\u0085\u000A`, + ], + }, + { + reason: 'Java has no comments, no "IN" and no zone-file order', + lines: [ + String.raw`www.filius.de A 3600 1.2.3.4#x\u000A`, + String.raw`www.filius.de A IN 3600 1.2.3.4\u000A`, + String.raw`www.filius.de 3600 IN A 1.2.3.4\u000A`, + ], + }, + { + reason: 'a label that starts with a digit, or two final dots, make the name empty in Java', + lines: [ + String.raw`1und1.de A 3600 1.2.3.4\u000A`, + String.raw`www.1und1.de A 3600 1.2.3.4\u000A`, + String.raw`www.filius.de.. A 3600 1.2.3.4\u000A`, + ], + }, + { + reason: 'an MX or NS target must be a name Java accepts: no preference, no address, one final dot', + lines: [ + String.raw`filius.de MX 3600 10 mail.filius.de\u000A`, + String.raw`filius.de MX 3600 1.2.3.4\u000A`, + String.raw`filius.de NS 3600 dns.filius.de..\u000A`, + ], + }, +]; + +/** Lines Java answers that Swift drops or reads otherwise. */ +const JAVA_ANSWERS_SWIFT_DROPS: { reason: string; lines: string[] }[] = [ + { + reason: 'a TTL that is no Java int is 3600', + lines: [ + String.raw`www.filius.de A x 1.2.3.4\u000A`, + String.raw`www.filius.de A 4294967295 1.2.3.4\u000A`, + String.raw`www.filius.de A 99999999999 1.2.3.4\u000A`, + String.raw`www.filius.de A 2147483648 1.2.3.4\u000A`, + ], + }, + { + reason: 'a TTL is Java\'s Integer.parseInt: negative, and in any script\'s digits', + lines: [ + String.raw`www.filius.de A -5 1.2.3.4\u000A`, + String.raw`www.filius.de A -2147483648 1.2.3.4\u000A`, + String.raw`www.filius.de A \u0661\u0660 1.2.3.4\u000A`, + ], + }, + { + reason: 'labels after the first may hold a backslash (Java\'s domain pattern)', + lines: [ + String.raw`www.a\u005Cb.de A 3600 1.2.3.4\u000A`, + ], + }, + { + reason: 'Java trims control characters off a line (String.trim)', + lines: [ + String.raw`www.filius.de A 3600 1.2.3.4\u0001\u000A`, + String.raw`\u001Fwww.filius.de A 3600 1.2.3.4\u000A`, + ], + }, + { + reason: 'an NS record may name the root', + lines: [ + String.raw`de. NS 3600 .\u000A`, + ], + }, + { + reason: 'duplicates stay, as in Java\'s list', + lines: [ + String.raw`www.filius.de A 3600 1.2.3.4\u000Awww.filius.de A 3600 1.2.3.4\u000A`, + String.raw`www.filius.de A 3600 1.2.3.4\u000Awww.filius.de A 7200 1.2.3.4\u000A`, + String.raw`www.filius.de A 3600 1.2.3.4\u000AWWW.filius.de. A 3600 1.2.3.4\u000A`, + ], + }, +]; + +const departures = new Map( + [...SWIFT_READS_JAVA_NEVER_ANSWERS, ...JAVA_ANSWERS_SWIFT_DROPS].flatMap(({ reason, lines }) => + lines.map((line) => [line, reason] as const), + ), +); + +describe('readJavaDnsHosts against Java', () => { + it('covers every line of the cases file, in both oracles', () => { + const lines = readFileSync(join(here, 'dns-hosts.cases.txt'), 'ascii').split('\n').slice(0, -1); + expect(javaMeasured.map(([line]) => line)).toEqual(lines); + expect(swiftMeasured.map(([line]) => line)).toEqual(lines); + }); + + for (const [line, expected] of javaMeasured) { + it(JSON.stringify(line), () => { + const records = readJavaDnsHosts(unescape(line)); + expect(records.map((record) => [record.domainName, record.type, record.ttl, record.rdata])).toEqual(expected); + }); + } +}); + +describe('the records the model keeps, against Swift', () => { + it('names only cases of the cases file as departures', () => { + const lines = new Set(swiftMeasured.map(([line]) => line)); + expect([...departures.keys()].filter((line) => !lines.has(line))).toEqual([]); + }); + + for (const [line, swift] of swiftMeasured) { + const reason = departures.get(line); + it(reason === undefined ? JSON.stringify(line) : `${JSON.stringify(line)} (departs: ${reason})`, () => { + if (reason === undefined) expect(modelRows(line)).toEqual(swift); + else expect(modelRows(line)).not.toEqual(swift); + }); + } +}); + +describe('the records Java answers', () => { + it('leave out records Java never answers: an empty name, or a type other than A, MX, NS', () => { + expect(modelRows(String.raw`1und1.de A 3600 1.2.3.4\u000Afilius.de CNAME 3600 www.filius.de\u000Afilius.de a 3600 1.2.3.4`)).toEqual([]); + }); + + it('take names in lower case without the final dot, A targets as FILIUS normalises addresses', () => { + expect(modelRows(String.raw`WWW.Filius.DE. A 60 010.000.0.1\u000Afilius.de MX 3600 Mail.Filius.DE\u000A. NS 3600 dns-root`)).toEqual([ + ['www.filius.de', 'A', 60, '10.0.0.1'], + ['filius.de', 'MX', 3600, 'mail.filius.de.'], + ['.', 'NS', 3600, 'dns-root.'], + ]); + }); + + it('warn about an A record Java answers with an address FILIUS cannot use', () => { + const warnings: string[] = []; + expect(modelRows(String.raw`www.filius.de A 3600 .1.2.3`, warnings)).toEqual([]); + expect(warnings).toEqual(["DNS record 'www.filius.de A .1.2.3' has no usable IPv4 address, left out (it stays in /dns/hosts)"]); + }); +}); + +describe('javaHostsFileContent', () => { + const file = (name: string, content: string | null): FlsFileTree => ({ kind: 'file', name, type: 'text', content, children: [] }); + const directory = (name: string, ...children: FlsFileTree[]): FlsFileTree => ({ kind: 'directory', name, children }); + + it('finds /dns/hosts ignoring case, as Java does', () => { + expect(javaHostsFileContent(directory('root', directory('DNS', file('Hosts', 'x'))))).toBe('x'); + expect(javaHostsFileContent(directory('root', directory('dns', file('ho\u017Fts', 'long s'))))).toBe('long s'); + }); + + it('takes the first match in preorder, and nothing when that is a directory', () => { + expect(javaHostsFileContent(directory('root', directory('dns', file('hosts', 'first')), directory('DNS', file('hosts', 'second'))))).toBe('first'); + expect(javaHostsFileContent(directory('root', directory('dns', directory('hosts'), file('HOSTS', 'later'))))).toBeUndefined(); + }); + + it('is undefined without the file, and empty for a file without content', () => { + expect(javaHostsFileContent(undefined)).toBeUndefined(); + expect(javaHostsFileContent(directory('root', file('hosts', 'x')))).toBeUndefined(); + expect(javaHostsFileContent(directory('root', directory('dns', file('hosts', null))))).toBe(''); + }); +}); diff --git a/packages/fls/test/importProject.test.ts b/packages/fls/test/importProject.test.ts index f79bdde..133236c 100644 --- a/packages/fls/test/importProject.test.ts +++ b/packages/fls/test/importProject.test.ts @@ -145,6 +145,7 @@ function systemOf(project: ImportedProject, nodeId: Id) { result.dnsServer = { recursiveResolutionEnabled: dns.recursiveResolutionEnabled, forwardingServerIPAddress: dns.forwardingServerIPAddress ?? null, + records: dns.records.map((record) => [record.name, record.type, record.ttlSeconds, record.target]), }; } const remoteLink = project.remoteLinkConfigurations.get(nodeId); @@ -787,12 +788,39 @@ describe('importProject rules', () => { expect(project.simpleClientConfigurations.get(notebook!)).toEqual({ targetAddress: '10.0.0.9', targetPort: 7 }); expect(project.firewallConfigurationsByNodeId.get(notebook!)).toMatchObject({ isActive: true, defaultPolicy: 'accept' }); expect([forwarding, self, iterative].map((id) => project.dnsServerConfigurations.get(id!))).toEqual([ - { recursiveResolutionEnabled: true, forwardingServerIPAddress: '8.8.8.8' }, - { recursiveResolutionEnabled: true }, - { recursiveResolutionEnabled: false }, + { recursiveResolutionEnabled: true, forwardingServerIPAddress: '8.8.8.8', records: [] }, + { recursiveResolutionEnabled: true, records: [] }, + { recursiveResolutionEnabled: false, records: [] }, ]); }); + it("takes a DNS server's records from its /dns/hosts, and names the node in a warning", () => { + const hosts = 'www.filius.de A 3600 141.99.2.10\nfilius.de MX 3600 mail.filius.de\nbroken.de A 3600 .1.2.3\n'; + const server = flsNode('Rechner', { + name: 'DNS', + interfaces: [nic('10.0.0.1')], + system: { + applications: [{ key: 'dns', javaClass: 'filius.software.dns.DNSServer', source: flsNode('Rechner').source, isActive: true }], + fileSystem: { + kind: 'directory', + name: 'root', + children: [ + { kind: 'directory', name: 'dns', children: [{ kind: 'file', name: 'hosts', type: 'text', content: hosts, children: [] }] }, + ], + }, + }, + }); + const project = importProject(topology({ nodes: [server] }), counter()); + const [id] = project.graph.nodes.map((node) => node.id); + + expect(project.dnsServerConfigurations.get(id!)?.records).toEqual([ + { name: 'www.filius.de', type: 'A', ttlSeconds: 3600, target: '141.99.2.10' }, + { name: 'filius.de', type: 'MX', ttlSeconds: 3600, target: 'mail.filius.de.' }, + ]); + expect(project.virtualFileSystems.get(id!)?.textFile('/dns/hosts')).toBe(hosts); + expect(project.warnings).toEqual(["node 'DNS': DNS record 'broken.de A .1.2.3' has no usable IPv4 address, left out (it stays in /dns/hosts)"]); + }); + it('leaves out a cable to itself and one to a port that already has a cable', () => { const project = importProject( topology({ diff --git a/tools/java-oracle/README.md b/tools/java-oracle/README.md index 354cf69..fd20b20 100644 --- a/tools/java-oracle/README.md +++ b/tools/java-oracle/README.md @@ -64,3 +64,16 @@ Java-Filius schickt jede IP-Adresse und Maske beim Laden durch zählen nicht (`10..0.0.1`), Vorzeichen und führende Nullen gehen, ebenso Ziffern jeder Schrift in der BMP (Thai, Devanagari, Vollbreite) — aber kein Leerzeichen und kein 𝟏 (Ersatzpaar). + +## `/dns/hosts` + +Javas DNS-Server liest seine Einträge bei jeder Anfrage aus der Textdatei +`/dns/hosts`. `DnsHostsCases` legt für jede Zeile von +`packages/fls/test/dns-hosts.cases.txt` (eine ganze Datei je Zeile, ASCII, +Zeilenumbruch als `\u000A`) einen Rechner mit dieser Datei an und schreibt, +was `DNSServer.holeResourceRecords()` liefert, nach `dns-hosts.java.json`. +Gemessen: Felder trennt nur ein einzelnes Leerzeichen (Tab und NBSP nicht), +Zeilen nur LF; ein Name, den Javas Muster ablehnt (`1und1.de`, `müller.de`), +wird leer, der Eintrag bleibt trotzdem; Typen außer A/MX/NS (auch `a`, +`CNAME`, eine Kommentarzeile) gelten als gültig; eine TTL, die kein `int` +ist, wird still 3600. diff --git a/tools/java-oracle/run.sh b/tools/java-oracle/run.sh index 7aa666e..dd02e51 100755 --- a/tools/java-oracle/run.sh +++ b/tools/java-oracle/run.sh @@ -21,7 +21,7 @@ if [ ! -f "$upstream/target/filius.jar" ]; then fi classpath="$upstream/target/filius.jar:$upstream/target/lib/*" mkdir -p "$here/.build" -javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" +javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" # FILIUS writes its settings below $HOME; keep them out of the real one. mkdir -p "$here/.build/home" # Removed first: a crashed run must not leave the previous result standing. @@ -66,3 +66,10 @@ ipcheck="$root/packages/fls/test/ip-check.java.json" rm -f "$ipcheck" HOME="$here/.build/home" java -cp "$classpath:$here/.build" IpCheckCases "$cases" "$ipcheck" 2>&1 | grep -v 'logback\|^$' || true test -s "$ipcheck" + +# DNSServer's own reading of /dns/hosts on hand-picked files (packages/fls/src/dnsHosts.ts). +cases="$root/packages/fls/test/dns-hosts.cases.txt" +dnshosts="$root/packages/fls/test/dns-hosts.java.json" +rm -f "$dnshosts" +HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" DnsHostsCases "$cases" "$dnshosts" 2>&1 | grep -v 'logback\|^$' || true +test -s "$dnshosts" diff --git a/tools/java-oracle/src/DnsHostsCases.java b/tools/java-oracle/src/DnsHostsCases.java new file mode 100644 index 0000000..bbb611b --- /dev/null +++ b/tools/java-oracle/src/DnsHostsCases.java @@ -0,0 +1,50 @@ +// Runs FILIUS's own reading of /dns/hosts (DNSServer.holeResourceRecords) +// on every case of a list and writes the records Java keeps — the reference +// for packages/fls/src/dnsHosts.ts. One case per line: a whole hosts file, +// ASCII with Java-style unicode escapes (backslash, u, four hex digits), so +// line breaks and odd spaces survive editors and hooks. +// +// Usage: java -cp filius.jar:lib/*:classes DnsHostsCases + +import filius.hardware.knoten.Rechner; +import filius.software.dns.DNSServer; +import filius.software.dns.ResourceRecord; +import filius.software.system.Datei; +import filius.software.system.Dateisystem; +import filius.software.system.InternetKnotenBetriebssystem; + +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class DnsHostsCases { + + public static void main(String[] args) throws Exception { + List result = new ArrayList<>(); + for (String line : Files.readAllLines(Path.of(args[0]), StandardCharsets.US_ASCII)) { + InternetKnotenBetriebssystem system = (InternetKnotenBetriebssystem) new Rechner().getSystemSoftware(); + Dateisystem fileSystem = system.getDateisystem(); + fileSystem.erstelleVerzeichnis(fileSystem.getRoot(), "dns"); + Datei hosts = new Datei(); + hosts.setName("hosts"); + hosts.setDateiInhalt(IpCheckCases.unescape(line)); + fileSystem.speicherDatei(fileSystem.holeRootPfad() + Dateisystem.FILE_SEPARATOR + "dns", hosts); + DNSServer server = new DNSServer(); + server.setSystemSoftware(system); + List records = new ArrayList<>(); + for (ResourceRecord record : server.holeResourceRecords()) { + records.add(Arrays.asList(record.getDomainname(), record.getType(), record.getTtl(), record.getRdata())); + } + result.add(Arrays.asList(line, records)); + } + try (FileOutputStream out = new FileOutputStream(args[1])) { + out.write((FlsOracle.Json.write(result, "") + "\n").getBytes(StandardCharsets.UTF_8)); + } + System.out.println("dns hosts cases: " + result.size()); + System.exit(0); + } +} diff --git a/tools/swift-oracle/README.md b/tools/swift-oracle/README.md index 8088ea0..79acaf5 100644 --- a/tools/swift-oracle/README.md +++ b/tools/swift-oracle/README.md @@ -38,6 +38,11 @@ Sortierung, Base64 — und die ganzen Mengen `whitespacesAndNewlines` und unter Linux; ihre Steuerzeichen-Tabelle hat in Ebene 14 einen Fehler, den der Port nicht übernimmt. +`--dns-hosts` liest dieselben `/dns/hosts`-Fälle wie das Java-Orakel +(`packages/fls/test/dns-hosts.cases.txt`) mit Swifts +`TopologyDNSHostsFileCodec`; unter `state` stehen die DNS-Einträge jedes +Servers (`dnsServer.records`). + ## Wie es gebaut wird `fetch-upstream.sh` holt `filius-ipad` am festen Stand `3b5ded0` und kopiert diff --git a/tools/swift-oracle/Sources/Oracle/main.swift b/tools/swift-oracle/Sources/Oracle/main.swift index 2c88b13..0af184d 100644 --- a/tools/swift-oracle/Sources/Oracle/main.swift +++ b/tools/swift-oracle/Sources/Oracle/main.swift @@ -401,6 +401,7 @@ func describeSystem(_ state: TopologyEditorState, _ node: TopologyNode) -> JSON json["dnsServer"] = [ "recursiveResolutionEnabled": dns.recursiveResolutionEnabled, "forwardingServerIPAddress": dns.forwardingServerIPAddress ?? NSNull(), + "records": dns.typedRecords.map { dnsRecordJSON($0) }, ] as JSON } if let link = state.remoteLinkConfigurationsByNodeID[node.id] { @@ -614,6 +615,32 @@ func foldPaths(_ cases: [PathCase]) -> [JSON] { } } +// MARK: - /dns/hosts cases (packages/fls/test/dns-hosts.cases.txt) + +func dnsRecordJSON(_ record: TopologyDNSResourceRecord) -> [Any] { + [record.name.rawValue, record.type.rawValue, Int(record.ttlSeconds), record.target] +} + +/// One case per line: a whole hosts file, ASCII with Java-style unicode +/// escapes (backslash, u, four hex digits) — the file the Java oracle reads. +func readDNSHostsCases(_ url: URL) throws -> [(line: String, text: String)] { + let lines = try String(contentsOf: url, encoding: .ascii).components(separatedBy: "\n") + return lines.dropLast().map { line in + var units: [UInt16] = [] + var rest = Substring(line) + while let first = rest.first { + if rest.hasPrefix("\\u"), rest.count >= 6, let unit = UInt16(rest.dropFirst(2).prefix(4), radix: 16) { + units.append(unit) + rest = rest.dropFirst(6) + } else { + units.append(contentsOf: String(first).utf16) + rest = rest.dropFirst() + } + } + return (line, String(decoding: units, as: UTF16.self)) + } +} + // MARK: - Main if CommandLine.arguments.count == 4, CommandLine.arguments[1] == "--path-folding" { @@ -641,6 +668,15 @@ if CommandLine.arguments.count == 4, CommandLine.arguments[1] == "--strings" { exit(0) } +if CommandLine.arguments.count == 4, CommandLine.arguments[1] == "--dns-hosts" { + let cases = try readDNSHostsCases(URL(fileURLWithPath: CommandLine.arguments[2])) + let output: [[Any]] = cases.map { [$0.line, TopologyDNSHostsFileCodec.records(from: $0.text).map { dnsRecordJSON($0) }] } + let data = try JSONSerialization.data(withJSONObject: output, options: [.prettyPrinted, .sortedKeys]) + try (data + Data("\n".utf8)).write(to: URL(fileURLWithPath: CommandLine.arguments[3])) + print("dns hosts: \(cases.count) cases") + exit(0) +} + if CommandLine.arguments.count == 4, CommandLine.arguments[1] == "--corpus" { let summary = try readCorpus(root: URL(fileURLWithPath: CommandLine.arguments[2])) let data = try JSONSerialization.data(withJSONObject: summary, options: [.prettyPrinted, .sortedKeys]) diff --git a/tools/swift-oracle/run.sh b/tools/swift-oracle/run.sh index b2d970a..5d5b300 100755 --- a/tools/swift-oracle/run.sh +++ b/tools/swift-oracle/run.sh @@ -14,4 +14,5 @@ docker run --rm -v "$root:/work" -w /work/tools/swift-oracle swift:6.1-jammy sh .build/debug/Oracle --corpus /work/corpus /work/corpus/corpus.swift.json .build/debug/Oracle --vfs /work/packages/core/test/vfs.cases.json /work/packages/core/test/vfs.swift.json .build/debug/Oracle --strings /work/packages/core/test/strings.cases.json /work/packages/core/test/strings.swift.json - .build/debug/Oracle --path-folding /work/packages/fls/test/path-folding.cases.json /work/packages/fls/test/path-folding.swift.json' + .build/debug/Oracle --path-folding /work/packages/fls/test/path-folding.cases.json /work/packages/fls/test/path-folding.swift.json + .build/debug/Oracle --dns-hosts /work/packages/fls/test/dns-hosts.cases.txt /work/packages/fls/test/dns-hosts.swift.json'