// 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 // 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. // // Java draws each message's ID with Math.random and a client's free port with // its random; both are compared as placeholders, and an answer must carry the // ID of its question. import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import type { DnsAddressResolution, NetworkRuntimeEngine } from '../src/index.js'; import { GAP_TOLERANCE_MILLISECONDS, gaps, javaFrames, watchedEngine, type JavaFrame, type SeenFrame } from './javaRun.js'; import type { Scenario } from './scenario.js'; const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios'); const CLIENT = '00000000-0000-0000-0000-000000000101'; const LOST_CLIENT = '00000000-0000-0000-0000-000000000104'; interface JavaRun { frames: Record; 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'), }; /** Java's words for the result: the address, null, or the exception. */ function javaResult(resolution: DnsAddressResolution): string { return resolution.type === 'timedOut' ? 'failed TimeOutException' : `returned ${resolution.address ?? 'null'}`; } /** IDs and free ports as placeholders; each ID stands for the question it first appeared in. */ function normalized(frames: readonly SeenFrame[]): SeenFrame[] { const ids = new Map(); return frames.map((frame) => ({ time: frame.time, text: frame.text .replace(/ID=(\d+)/, (_, id: string) => { if (!ids.has(id)) ids.set(id, `q${ids.size + 1}`); return `ID=${ids.get(id)}`; }) .replace(/:(\d{5})\b/g, (whole, port: string) => (Number(port) >= 49_152 ? ':client' : whole)), })); } /** * Stories the engine does not play like Java yet, with the reason; each must * still differ from Java (else it belongs to the others) and return the same. * * noServer: Java's transport thread sends while the resolver already waits, * and asks ARP twice, a round trip apart. The engine's ARP (Swift's) blocks * the call and asks again at once — rebuilt after Java in its own step. */ const NOT_YET_LIKE_JAVA: Record = { noServer: "ARP: Java's second request comes a round trip after the first, while the resolver waits", }; describe('DNS as Java FILIUS runs it', () => { const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'dns-lan.scenario.json'), 'utf8')) as Scenario; 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; 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); } }); } });