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 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 18:46:32 +00:00
co-authored by Claude Opus 5.5
parent f5c61cdce7
commit 313786e738
13 changed files with 1570 additions and 91 deletions
+71 -3
View File
@@ -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([]);
});
});