test: F2 acceptance — five of FILIUS' examples run as Java FILIUS runs them
ExampleRun opens an example as FILIUS opens it (SzenarioVerwaltung.laden into the window's lists), starts it as the action mode does, plays a story and writes every card's frames and tables, the calls, the addresses and the RIP tables' changes. examplesJava.test.ts reads the same file with this package's reader and runs it in the core (projectSnapshot, new): routing (internetwork_komplett), DHCP (einfaches_rechnernetz_komplett), DNS (dns_server), NAT (nat_with_mapped_port) and firewall (Internet_Komplett_..._DE) match card by card. Found and fixed: a host's ipForwardingEnabled, which Java saves and obeys, was unknown to the reader, the model, the core, the writer, the export and FlsOracle; and Java's ARP learns only from a request for its own address or a reply. Named Java bugs: a ping without a gateway throws a NullPointerException; a home router drops the answer of a second remote end. Open: Java picks between equally long RIP routes by thread timing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1264,7 +1264,22 @@ describe('DHCP', () => {
|
||||
},
|
||||
16,
|
||||
);
|
||||
return { engine, hostIds };
|
||||
return { engine, hostIds, portIds: hosts.map((host) => host.portId) };
|
||||
};
|
||||
|
||||
/** A host tells everyone its address with an ARP reply to all (as Java answers a probe from 0.0.0.0): every card learns it. */
|
||||
const announce = (engine: NetworkRuntimeEngine, nodeId: string, portId: string, ipAddress: string) => {
|
||||
const mac = stableMacAddress(portId);
|
||||
engine.sendEthernetFrame(nodeId, portId, {
|
||||
identity: engine.allocateFrameIdentity(),
|
||||
sourceMacAddress: mac,
|
||||
destinationMacAddress: ETHERNET_BROADCAST_MAC_ADDRESS,
|
||||
payload: {
|
||||
kind: 'arp',
|
||||
packet: { operation: 'reply', senderMacAddress: mac, senderIPAddress: ipAddress, targetMacAddress: ETHERNET_BROADCAST_MAC_ADDRESS, targetIPAddress: '255.255.255.255' },
|
||||
},
|
||||
});
|
||||
letFramesArrive(engine);
|
||||
};
|
||||
|
||||
/** When the node sent a DHCP message of the type. */
|
||||
@@ -1470,14 +1485,14 @@ describe('DHCP', () => {
|
||||
|
||||
it('skips a link-local address the ARP cache knows', () => {
|
||||
const [known, next] = linkLocalDraws(2) as [string, string];
|
||||
const { engine, hostIds } = onSwitch(0x16a1, [
|
||||
const { engine, hostIds, portIds } = onSwitch(0x16a1, [
|
||||
device('0.0.0.0'),
|
||||
device(known, { mask: '255.255.0.0' }),
|
||||
device('169.254.9.9', { mask: '255.255.0.0' }),
|
||||
]);
|
||||
const clientId = hostIds[0]!;
|
||||
engine.handle({ type: 'advance', toMilliseconds: 1_000 });
|
||||
engine.resolveMacAddress(hostIds[1]!, '169.254.9.9', { maxRetries: 1 });
|
||||
announce(engine, hostIds[1]!, portIds[1]!, known);
|
||||
expect(engine.state.arpCachesByNodeId.get(clientId)?.has(known)).toBe(true);
|
||||
|
||||
engine.handle({ type: 'advance', toMilliseconds: 20_000 });
|
||||
@@ -1509,8 +1524,8 @@ describe('DHCP', () => {
|
||||
// Java's ARP lookup waits on the whole ARP table: any entry that changes
|
||||
// ends the wait, and the address counts as free. The engine waits for the
|
||||
// address it checks, or the round trip.
|
||||
it('waits out the address check when an unrelated ARP packet comes (Java would stop waiting)', () => {
|
||||
const { engine, hostIds } = onSwitch(
|
||||
it('waits out the address check when an unrelated ARP reply comes (Java would stop waiting)', () => {
|
||||
const { engine, hostIds, portIds } = onSwitch(
|
||||
0x1661,
|
||||
[device('0.0.0.0'), device('192.168.0.10'), device('192.168.0.30')],
|
||||
new Map([['192.168.0.10', { lowerBoundIPAddress: '192.168.0.100', upperBoundIPAddress: '192.168.0.100' }]]),
|
||||
@@ -1519,7 +1534,7 @@ describe('DHCP', () => {
|
||||
engine.handle({ type: 'advance', toMilliseconds: 300 });
|
||||
expect(engine.state.dhcpClientStatusesByNodeId.get(clientId)?.state).toBe('VALIDATE');
|
||||
|
||||
engine.resolveMacAddress(hostIds[2]!, '192.168.0.77', { maxRetries: 1 });
|
||||
announce(engine, hostIds[2]!, portIds[2]!, '192.168.0.30');
|
||||
expect(engine.state.arpCachesByNodeId.get(clientId)?.has('192.168.0.30')).toBe(true);
|
||||
engine.handle({ type: 'advance', toMilliseconds: 5_000 });
|
||||
|
||||
@@ -1793,6 +1808,56 @@ describe('DNS', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('IP forwarding on a host', () => {
|
||||
// A on the switch with H, whose IP forwarding may be on; H's gateway is the router to 20.0.0.0/24.
|
||||
function hostRouting(forwards: boolean | undefined) {
|
||||
const [hostA, hostH, routerId, hostB] = [0x3001, 0x3002, 0x3003, 0x3004].map(uuid) as [string, string, string, string];
|
||||
const [portA, portH, routerLan, routerWan, portB] = [0x3011, 0x3012, 0x3013, 0x3014, 0x3015].map(uuid) as [string, string, string, string, string];
|
||||
const switchId = uuid(0x3005);
|
||||
const switchPorts = [1, 2, 3].map((index) => portSnapshot(uuid(0x3020 + index), `sw${index}`));
|
||||
const engine = startedEngine({
|
||||
nodes: [
|
||||
pc(hostA, portA),
|
||||
pc(hostH, portH),
|
||||
{ id: routerId, kind: 'router', ports: [portSnapshot(routerLan, 'rt1'), portSnapshot(routerWan, 'rt2')] },
|
||||
pc(hostB, portB),
|
||||
{ id: switchId, kind: 'networkSwitch', ports: switchPorts },
|
||||
],
|
||||
links: [
|
||||
link(hostA, portA, switchId, switchPorts[0]!.id),
|
||||
link(hostH, portH, switchId, switchPorts[1]!.id),
|
||||
link(routerId, routerLan, switchId, switchPorts[2]!.id),
|
||||
link(routerId, routerWan, hostB, portB),
|
||||
],
|
||||
deviceConfigurations: new Map([
|
||||
[hostA, device('10.0.0.5', { gateway: '10.0.0.1' })],
|
||||
[hostH, device('10.0.0.1', { gateway: '10.0.0.254' })],
|
||||
[routerId, device('10.0.0.254')],
|
||||
[hostB, device('20.0.0.5', { gateway: '20.0.0.254' })],
|
||||
]),
|
||||
interfaceConfigurations: new Map([
|
||||
[interfaceKey(routerId, routerLan), { ipAddress: '10.0.0.254', subnetMask: '255.255.255.0' }],
|
||||
[interfaceKey(routerId, routerWan), { ipAddress: '20.0.0.254', subnetMask: '255.255.255.0' }],
|
||||
]),
|
||||
...(forwards === undefined ? {} : { ipForwardingByNodeId: new Map([[hostH, forwards]]) }),
|
||||
});
|
||||
return { engine, hostA };
|
||||
}
|
||||
|
||||
it('forwards for others only when its IP forwarding is on (Java ipForwardingEnabled), as a router', () => {
|
||||
for (const [forwards, delivered] of [
|
||||
[undefined, false],
|
||||
[false, false],
|
||||
[true, true],
|
||||
] as const) {
|
||||
const { engine, hostA } = hostRouting(forwards);
|
||||
engine.sendIcmpEcho(hostA, '20.0.0.5');
|
||||
const answered = (engine.state.icmpObservationsByNodeId.get(hostA) ?? []).some((each) => each.message.kind === 'echoReply');
|
||||
expect(answered, String(forwards)).toBe(delivered);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ARP', () => {
|
||||
const requestsFor = (engine: NetworkRuntimeEngine, ipAddress: string) =>
|
||||
engine.state.packetTraces.filter((trace) => trace.detail === `ARP request for ${ipAddress}`).map((trace) => trace.timeMilliseconds);
|
||||
@@ -1827,6 +1892,40 @@ describe('ARP', () => {
|
||||
expect(observed()).toEqual(['destinationHostUnreachable']);
|
||||
});
|
||||
|
||||
it("learns only from a request for its own address and from a reply, and drops one from the network's address (Java ARPThread)", () => {
|
||||
const [hostA, hostB, hostC] = [0x2f01, 0x2f02, 0x2f03].map(uuid) as [string, string, string];
|
||||
const [portA, portB, portC] = [0x2f11, 0x2f12, 0x2f13].map(uuid) as [string, string, string];
|
||||
const switchId = uuid(0x2f04);
|
||||
const switchPorts = [1, 2, 3].map((index) => portSnapshot(uuid(0x2f20 + index), `sw${index}`));
|
||||
const engine = startedEngine({
|
||||
nodes: [pc(hostA, portA), pc(hostB, portB), pc(hostC, portC), { id: switchId, kind: 'networkSwitch', ports: switchPorts }],
|
||||
links: [link(hostA, portA, switchId, switchPorts[0]!.id), link(hostB, portB, switchId, switchPorts[1]!.id), link(hostC, portC, switchId, switchPorts[2]!.id)],
|
||||
deviceConfigurations: new Map([
|
||||
[hostA, device('10.0.0.1')],
|
||||
[hostB, device('10.0.0.2')],
|
||||
[hostC, device('10.0.0.3')],
|
||||
]),
|
||||
});
|
||||
const knows = (nodeId: string, ipAddress: string) => engine.state.arpCachesByNodeId.get(nodeId)?.has(ipAddress) === true;
|
||||
|
||||
expect(engine.resolveMacAddress(hostA, '10.0.0.2')).toBe(stableMacAddress(portB));
|
||||
expect([knows(hostB, '10.0.0.1'), knows(hostA, '10.0.0.2'), knows(hostC, '10.0.0.1')]).toEqual([true, true, false]);
|
||||
|
||||
// A request from the network's own address: neither learned nor answered.
|
||||
engine.sendEthernetFrame(hostA, portA, {
|
||||
identity: engine.allocateFrameIdentity(),
|
||||
sourceMacAddress: stableMacAddress(portA),
|
||||
destinationMacAddress: ETHERNET_BROADCAST_MAC_ADDRESS,
|
||||
payload: {
|
||||
kind: 'arp',
|
||||
packet: { operation: 'request', senderMacAddress: stableMacAddress(portA), senderIPAddress: '10.0.0.0', targetMacAddress: ETHERNET_BROADCAST_MAC_ADDRESS, targetIPAddress: '10.0.0.3' },
|
||||
},
|
||||
});
|
||||
letFramesArrive(engine);
|
||||
expect(knows(hostC, '10.0.0.0')).toBe(false);
|
||||
expect(engine.capture.frames(portC).filter((each) => each.frame.sourceMacAddress === stableMacAddress(portC))).toEqual([]);
|
||||
});
|
||||
|
||||
it('forgets the lookups that wait on stop', () => {
|
||||
const [firstId, firstPortId, secondId, secondPortId] = [0x1a01, 0x1a02, 0x1a03, 0x1a04].map(uuid) as [string, string, string, string];
|
||||
const input = (secondIp: string) => ({
|
||||
|
||||
@@ -96,7 +96,7 @@ const FLAG_LETTERS: [number, string][] = [
|
||||
[TCP_FLAG.reset, 'R'],
|
||||
];
|
||||
|
||||
function engineText(frame: EthernetFrame): string {
|
||||
export function engineText(frame: EthernetFrame): string {
|
||||
const macs = `${frame.sourceMacAddress} > ${frame.destinationMacAddress}`.toUpperCase();
|
||||
if (frame.payload.kind === 'arp') {
|
||||
const arp = frame.payload.packet;
|
||||
@@ -146,14 +146,14 @@ export function watchedEngine(scenario: Scenario) {
|
||||
}
|
||||
|
||||
/** Java's frames of all cards, each once: the same text within the window is the same frame. */
|
||||
export function javaFrames(framesByNode: Record<string, JavaFrame[]>): SeenFrame[] {
|
||||
export function javaFrames(framesByNode: Record<string, JavaFrame[]>, windowMilliseconds = DUPLICATE_WINDOW_MILLISECONDS): SeenFrame[] {
|
||||
const all = Object.values(framesByNode)
|
||||
.flat()
|
||||
.map((frame) => ({ time: frame.time, text: javaText(frame) }))
|
||||
.sort((lhs, rhs) => lhs.time - rhs.time);
|
||||
const frames: SeenFrame[] = [];
|
||||
for (const frame of all) {
|
||||
if (frames.some((each) => each.text === frame.text && frame.time - each.time <= DUPLICATE_WINDOW_MILLISECONDS)) continue;
|
||||
if (frames.some((each) => each.text === frame.text && frame.time - each.time <= windowMilliseconds)) continue;
|
||||
frames.push(frame);
|
||||
}
|
||||
return frames;
|
||||
@@ -174,9 +174,22 @@ export function gaps(frames: readonly SeenFrame[]) {
|
||||
* `normalized` does so for a card's whole list; with `until` (Java's time),
|
||||
* only the frames before it; with `from`, only those from it.
|
||||
*/
|
||||
/** A card to compare: the key Java's lists have it under, and its port in the engine. */
|
||||
export interface CaptureCard {
|
||||
readonly key: string;
|
||||
readonly portId: string;
|
||||
}
|
||||
|
||||
/** A scenario's cards: its hosts' ports, under the node's id for the first card and `id#index` for the others (ScenarioNetwork). */
|
||||
export function scenarioCards(scenario: Scenario): CaptureCard[] {
|
||||
return scenario.nodes
|
||||
.filter((node) => node.kind !== 'networkSwitch')
|
||||
.flatMap((node) => node.ports.map((port, index) => ({ key: index === 0 ? node.id : `${node.id}#${index}`, portId: port.id })));
|
||||
}
|
||||
|
||||
export function expectSameCaptures(
|
||||
engine: NetworkRuntimeEngine,
|
||||
scenario: Scenario,
|
||||
cardsOrScenario: Scenario | readonly CaptureCard[],
|
||||
java: Record<string, JavaFrame[]>,
|
||||
options: {
|
||||
text?: (text: string) => string;
|
||||
@@ -194,13 +207,10 @@ export function expectSameCaptures(
|
||||
const text = options.text ?? ((each: string) => each);
|
||||
const engineByCard = new Map<string, SeenFrame[]>();
|
||||
const capturedByCard = new Map<string, readonly CapturedFrame[]>();
|
||||
for (const node of scenario.nodes) {
|
||||
if (node.kind === 'networkSwitch') continue;
|
||||
node.ports.forEach((port, index) => {
|
||||
const card = index === 0 ? node.id : `${node.id}#${index}`;
|
||||
capturedByCard.set(card, engine.capture.frames(port.id));
|
||||
engineByCard.set(card, capturedByCard.get(card)!.map((each) => ({ time: each.timeMilliseconds, text: text(engineText(each.frame)) })));
|
||||
});
|
||||
const cards = 'nodes' in cardsOrScenario ? scenarioCards(cardsOrScenario) : cardsOrScenario;
|
||||
for (const { key, portId } of cards) {
|
||||
capturedByCard.set(key, engine.capture.frames(portId));
|
||||
engineByCard.set(key, capturedByCard.get(key)!.map((each) => ({ time: each.timeMilliseconds, text: text(engineText(each.frame)) })));
|
||||
}
|
||||
const javaByCard = new Map(Object.entries(java).map(([card, frames]) => [card, frames.map((frame) => ({ time: frame.time, text: text(javaText(frame)) }))]));
|
||||
expect([...engineByCard.keys()].sort()).toEqual([...javaByCard.keys()].sort());
|
||||
|
||||
Reference in New Issue
Block a user