feat(core): DNS after Java, measured with Java FILIUS
DNS follows Java's DNSServer and Resolver, not Swift's typed format: Java's text messages (dns.ts), a server on port 53 answering from its records (no question in the answer, RCODE 0, a referral with NS and A in the answer section), an iterative resolver that waits five round trips per query and remembers only the servers it was referred to. Recursion throws NotPortedError until the next step. The snapshot carries the DNS servers switched on. receiveUdp returns as soon as a datagram is there, as Java's empfangen does. dnsJava.test.ts compares DnsRun's five lookups; four match frame by frame, the missing server waits for Java's ARP (named as not yet like Java). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,10 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
decodeDhcpMessage,
|
||||
decodeDnsMessage,
|
||||
encodeDnsMessage,
|
||||
findApplicableNameServerRecord,
|
||||
NotPortedError,
|
||||
DHCP_DYNAMIC_LEASE_LIFETIME_MILLISECONDS,
|
||||
deviceConfiguration,
|
||||
dhcpServerConfiguration,
|
||||
@@ -1394,3 +1398,136 @@ describe('DHCP', () => {
|
||||
expect(decodeDhcpMessage(new TextEncoder().encode('HELLO'))).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DNS', () => {
|
||||
const lan = (records: Record<string, string[]>, recursive = false) => {
|
||||
// Hosts 10.30.0.1 (the client, DNS server .2), .2, .3 on one switch.
|
||||
const switchId = uuid(0x1900);
|
||||
const hosts = [1, 2, 3].map((index) => ({ id: uuid(0x1900 + 2 * index), portId: uuid(0x1901 + 2 * index), switchPortId: uuid(0x1910 + index) }));
|
||||
const ip = (index: number) => `10.30.0.${index + 1}`;
|
||||
const record = (line: string) => {
|
||||
const [name = '', type = 'A', target = ''] = line.split(' ');
|
||||
return { name, type: type as 'A' | 'MX' | 'NS', ttlSeconds: 3600, target };
|
||||
};
|
||||
const engine = startedEngine({
|
||||
nodes: [
|
||||
{ id: switchId, kind: 'networkSwitch', ports: hosts.map((host, index) => portSnapshot(host.switchPortId, `sw${index + 1}`)) },
|
||||
...hosts.map((host) => pc(host.id, host.portId)),
|
||||
],
|
||||
links: hosts.map((host) => link(host.id, host.portId, switchId, host.switchPortId)),
|
||||
deviceConfigurations: new Map(hosts.map((host, index) => [host.id, deviceConfiguration(ip(index), '255.255.255.0', '', index === 0 ? '10.30.0.2' : '')])),
|
||||
dnsServerConfigurationsByNodeId: new Map(
|
||||
Object.entries(records).map(([address, lines]) => [
|
||||
hosts[Number(address.split('.')[3]) - 1]!.id,
|
||||
{ recursiveResolutionEnabled: recursive, records: lines.map(record) },
|
||||
]),
|
||||
),
|
||||
});
|
||||
return { engine, client: hosts[0]!.id };
|
||||
};
|
||||
const queries = (engine: NetworkRuntimeEngine) =>
|
||||
engine.state.packetTraces.filter((trace) => trace.detail?.startsWith('DNS query') === true).map((trace) => trace.detail);
|
||||
|
||||
it("writes and reads Java's DNS messages", () => {
|
||||
const message = {
|
||||
id: 4711,
|
||||
queryResponse: 1,
|
||||
responseCode: 0,
|
||||
questions: [],
|
||||
answers: [
|
||||
{ name: 'filius.de.', type: 'MX', ttl: 3600, data: 'mail.filius.de.' },
|
||||
{ name: 'mail.filius.de.', type: 'A', ttl: 3600, data: '10.0.0.5' },
|
||||
],
|
||||
authority: [],
|
||||
additional: [],
|
||||
};
|
||||
const text = encodeDnsMessage(message);
|
||||
expect(text).toBe(
|
||||
'ID=4711 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=2 NSCOUNT=0 ARCOUNT=0 \nfilius.de. MX 3600 mail.filius.de.\nmail.filius.de. A 3600 10.0.0.5\n',
|
||||
);
|
||||
expect(decodeDnsMessage(text)).toEqual(message);
|
||||
// Java's StringTokenizer skips empty pieces; the counts say how many lines follow.
|
||||
expect(decodeDnsMessage('ID=7 QR=0 QDCOUNT=1\n\nweb.filius.de. A IN\nleft over\n')).toEqual({
|
||||
id: 7,
|
||||
queryResponse: 0,
|
||||
responseCode: 0,
|
||||
questions: [{ name: 'web.filius.de.', type: 'A', questionClass: 'IN' }],
|
||||
answers: [],
|
||||
authority: [],
|
||||
additional: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('finds the name server of the longest zone, else the root one', () => {
|
||||
const records = [
|
||||
{ name: '.', type: 'NS', ttl: 1, data: 'root.' },
|
||||
{ name: 'de.', type: 'NS', ttl: 1, data: 'ns.de.' },
|
||||
{ name: 'Schule.DE.', type: 'NS', ttl: 1, data: 'ns.schule.de.' },
|
||||
];
|
||||
expect(findApplicableNameServerRecord('www.schule.de.', records)?.data).toBe('ns.schule.de.');
|
||||
expect(findApplicableNameServerRecord('www.filius.de.', records)?.data).toBe('ns.de.');
|
||||
expect(findApplicableNameServerRecord('www.filius.org.', records)?.data).toBe('root.');
|
||||
expect(findApplicableNameServerRecord('www.filius.org.', records.slice(1))).toBeUndefined();
|
||||
});
|
||||
|
||||
it('answers localhost itself, and nothing without a DNS server', () => {
|
||||
const { engine, client } = lan({ '10.30.0.2': [] });
|
||||
expect(engine.resolveDnsAddress(client, 'LocalHost')).toEqual({ type: 'resolved', address: '127.0.0.1' });
|
||||
expect(engine.resolveDnsAddress(uuid(0x1904), 'web.filius.de')).toEqual({ type: 'resolved', address: undefined });
|
||||
expect(queries(engine)).toEqual([]);
|
||||
});
|
||||
|
||||
it('follows referrals, and asks the first server once more (Java remembers only the ones it was referred to)', () => {
|
||||
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': ['schule.de NS ns2.schule.de.', 'ns2.schule.de A 10.30.0.2'],
|
||||
});
|
||||
|
||||
expect(engine.resolveDnsAddress(client, 'www.schule.de')).toEqual({ type: 'resolved', address: undefined });
|
||||
expect(queries(engine)).toEqual(['DNS query www.schule.de. A', 'DNS query www.schule.de. A', 'DNS query www.schule.de. A']);
|
||||
expect([...engine.state.socketsById.values()].filter((socket) => socket.nodeId === client)).toEqual([]);
|
||||
});
|
||||
|
||||
it('receives a datagram when it comes, not at the end of the wait', () => {
|
||||
// A DHCP client without a server sends its DISCOVER again after a round trip (a timer, not the network).
|
||||
const [clientId, clientPortId, listenerId, listenerPortId] = [0x1981, 0x1982, 0x1983, 0x1984].map(uuid) as [string, string, string, string];
|
||||
const engine = startedEngine({
|
||||
nodes: [pc(clientId, clientPortId), pc(listenerId, listenerPortId)],
|
||||
links: [link(clientId, clientPortId, listenerId, listenerPortId)],
|
||||
deviceConfigurations: new Map([
|
||||
[clientId, device('0.0.0.0')],
|
||||
[listenerId, device('10.31.0.2')],
|
||||
]),
|
||||
dhcpClientConfigurationsByNodeId: new Map([[clientId, { isEnabled: true }]]),
|
||||
});
|
||||
const socket = engine.bindUdpSocket(listenerId, { localPort: 67 })!;
|
||||
engine.handle({ type: 'advance', toMilliseconds: 100 });
|
||||
expect(engine.receiveUdp(socket)).toBeDefined();
|
||||
|
||||
expect(engine.receiveUdp(socket, 10_000)).toBeDefined();
|
||||
expect(engine.state.currentTimeMilliseconds).toBe(2_750 + 55);
|
||||
});
|
||||
|
||||
it('returns from the answer, not at the end of the wait', () => {
|
||||
const { engine, client } = lan({ '10.30.0.2': ['web.filius.de A 10.30.0.9'] });
|
||||
const called = engine.state.currentTimeMilliseconds;
|
||||
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);
|
||||
});
|
||||
|
||||
it('gives up after five round trips when no server answers, and closes its socket', () => {
|
||||
const { engine, client } = lan({ '10.30.0.2': [] });
|
||||
const called = engine.state.currentTimeMilliseconds;
|
||||
// 10.30.0.3 is there, but no DNS server runs on it.
|
||||
expect(engine.resolveDns(client, 'web.filius.de', 'A', '10.30.0.3')).toEqual({ type: 'timedOut' });
|
||||
// Java's five round trips; before them the engine's send still blocks for
|
||||
// ARP and the query's way to .3 (Java waits from handing it down — the ARP step).
|
||||
expect(engine.state.currentTimeMilliseconds - called).toBe(3 * 110 + 5 * 2_750);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user