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:
@@ -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-<story>.java.json). The
|
||||
// card saw and what the call returned (dns-java-<story>.java.json); on
|
||||
// dns-recursive's, where the server resolves recursively, likewise
|
||||
// (dns-recursive-java-<story>.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<string, (engine: NetworkRuntimeEngine) => 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<string, Record<string, Story>> = {
|
||||
'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-<story> for dns-lan, <scenario>-java-<story> 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<string, (java: JavaRun) => 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<string, string> = {};
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user