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:
+9
-7
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,11 @@ export type NodeKind =
|
||||
| 'remoteLink'
|
||||
| 'unsupported';
|
||||
|
||||
/** Whether a node of this kind forwards packets unless its settings say otherwise: routers and home routers (Java's constructors). */
|
||||
export function forwardsIPv4ByDefault(kind: NodeKind): boolean {
|
||||
return kind === 'router' || kind === 'gateway';
|
||||
}
|
||||
|
||||
export function isPcClassEndpoint(kind: NodeKind): boolean {
|
||||
return kind === 'pc' || kind === 'notebook';
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
// remote links.
|
||||
|
||||
import { compareIds, type Id } from '../ids.js';
|
||||
import { isPcClassEndpoint, stableGeneratedMacAddress } from '../model/node.js';
|
||||
import { forwardsIPv4ByDefault, isPcClassEndpoint, stableGeneratedMacAddress } from '../model/node.js';
|
||||
import type {
|
||||
DeviceConfiguration,
|
||||
DhcpServerConfiguration,
|
||||
@@ -4087,7 +4087,12 @@ export class NetworkRuntimeEngine {
|
||||
nodeId: Id,
|
||||
networkInterface: NetworkInterface,
|
||||
): void {
|
||||
if (packet.senderIPAddress !== UNSPECIFIED_IP_ADDRESS) {
|
||||
// Java's ARPThread: a packet from the network's own address is dropped; an entry comes from a
|
||||
// request for the card's address or from a reply (Swift, and swiftArp: from every packet).
|
||||
const network = maskAddress(parseIPv4(networkInterface.ipAddress) ?? 0, parseIPv4(networkInterface.subnetMask) ?? 0);
|
||||
if (!this.swiftArp && parseIPv4(packet.senderIPAddress) === network) return;
|
||||
const learns = this.swiftArp || packet.targetIPAddress === networkInterface.ipAddress || packet.operation === 'reply';
|
||||
if (packet.senderIPAddress !== UNSPECIFIED_IP_ADDRESS && learns) {
|
||||
getOrCreate(this.stateValue.arpCachesByNodeId, nodeId, () => new Map()).set(packet.senderIPAddress, {
|
||||
ipAddress: packet.senderIPAddress,
|
||||
macAddress: packet.senderMacAddress,
|
||||
@@ -4495,8 +4500,8 @@ export class NetworkRuntimeEngine {
|
||||
if (!this.deliverIPv4Locally(packetForProcessing, nodeId, networkInterface, finished)) finished();
|
||||
return;
|
||||
}
|
||||
// Java: routers and home routers forward (IpForwardingEnabled), nothing else.
|
||||
if (node.kind !== 'router' && node.kind !== 'gateway') {
|
||||
// Java: routers and home routers forward, a host when its IP forwarding is on (ipForwardingEnabled).
|
||||
if (!(this.stateValue.topologySnapshot.ipForwardingByNodeId.get(nodeId) ?? forwardsIPv4ByDefault(node.kind))) {
|
||||
this.recordTrace({
|
||||
frameIdentity,
|
||||
packetIdentity: packetForProcessing.identity,
|
||||
|
||||
@@ -63,6 +63,8 @@ export interface TopologySnapshot {
|
||||
/** A home router's port forwarding (Java GatewayFirmware.staticNAT), by node. */
|
||||
readonly portForwardingRowsByNodeId: ReadonlyMap<Id, readonly GatewayPortForwardingRow[]>;
|
||||
readonly switchConfigurationsByNodeId: ReadonlyMap<Id, SwitchConfiguration>;
|
||||
/** Nodes whose IP forwarding differs from their kind's (forwardsIPv4ByDefault): a host that forwards. */
|
||||
readonly ipForwardingByNodeId: ReadonlyMap<Id, boolean>;
|
||||
}
|
||||
|
||||
export interface TopologySnapshotInput {
|
||||
@@ -78,6 +80,7 @@ export interface TopologySnapshotInput {
|
||||
firewallConfigurationsByNodeId?: ReadonlyMap<Id, FirewallConfiguration>;
|
||||
portForwardingRowsByNodeId?: ReadonlyMap<Id, readonly GatewayPortForwardingRow[]>;
|
||||
switchConfigurationsByNodeId?: ReadonlyMap<Id, SwitchConfiguration>;
|
||||
ipForwardingByNodeId?: ReadonlyMap<Id, boolean>;
|
||||
}
|
||||
|
||||
export function topologySnapshot(input: TopologySnapshotInput): TopologySnapshot {
|
||||
@@ -94,6 +97,7 @@ export function topologySnapshot(input: TopologySnapshotInput): TopologySnapshot
|
||||
firewallConfigurationsByNodeId: input.firewallConfigurationsByNodeId ?? new Map(),
|
||||
portForwardingRowsByNodeId: input.portForwardingRowsByNodeId ?? new Map(),
|
||||
switchConfigurationsByNodeId: input.switchConfigurationsByNodeId ?? new Map(),
|
||||
ipForwardingByNodeId: input.ipForwardingByNodeId ?? new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -77,6 +77,7 @@ import {
|
||||
base64Encode,
|
||||
defaultSwitchConfiguration,
|
||||
dhcpServerConfiguration,
|
||||
forwardsIPv4ByDefault,
|
||||
effectiveMacAddress,
|
||||
firewallConfiguration,
|
||||
FIREWALL_PROTOCOL,
|
||||
@@ -128,6 +129,8 @@ export interface ProjectToExport {
|
||||
readonly switchConfigurations: ReadonlyMap<Id, SwitchConfiguration>;
|
||||
readonly manualRoutesByNodeId: ReadonlyMap<Id, readonly ManualRoute[]>;
|
||||
readonly ripEnabledByNodeId: ReadonlyMap<Id, boolean>;
|
||||
/** Nodes whose IP forwarding differs from their kind's; none when left out. */
|
||||
readonly ipForwardingByNodeId?: ReadonlyMap<Id, boolean>;
|
||||
readonly firewallConfigurationsByNodeId: ReadonlyMap<Id, FirewallConfiguration>;
|
||||
readonly dhcpClientConfigurations: ReadonlyMap<Id, DhcpClientConfiguration>;
|
||||
readonly dhcpServerConfigurations: ReadonlyMap<Id, DhcpServerConfiguration>;
|
||||
@@ -277,6 +280,7 @@ function exportSystem(node: TopologyNode, project: ProjectToExport, servingModem
|
||||
route.interfaceIPAddress,
|
||||
]),
|
||||
dhcpClient: project.dhcpClientConfigurations.get(id)?.isEnabled === true,
|
||||
ipForwardingEnabled: project.ipForwardingByNodeId?.get(id) ?? forwardsIPv4ByDefault(node.kind),
|
||||
dhcpServer: exportDhcpServer(project.dhcpServerConfigurations.get(id) ?? dhcpServerConfiguration()),
|
||||
};
|
||||
switch (node.kind) {
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
|
||||
import {
|
||||
canonicalMacAddress,
|
||||
forwardsIPv4ByDefault,
|
||||
deepEqual,
|
||||
defaultSwitchConfiguration,
|
||||
dhcpServerConfiguration,
|
||||
@@ -114,6 +115,8 @@ export interface ImportedProject {
|
||||
manualRoutesByNodeId: Map<Id, ManualRoute[]>;
|
||||
/** Only routers with RIP switched on. */
|
||||
ripEnabledByNodeId: Map<Id, boolean>;
|
||||
/** Nodes whose IP forwarding differs from their kind's (forwardsIPv4ByDefault). */
|
||||
ipForwardingByNodeId: Map<Id, boolean>;
|
||||
firewallConfigurationsByNodeId: Map<Id, FirewallConfiguration>;
|
||||
dhcpClientConfigurations: Map<Id, DhcpClientConfiguration>;
|
||||
/** Only servers whose settings differ from Java's constructor. */
|
||||
@@ -178,6 +181,7 @@ export function importProject(
|
||||
hostWirelessConfigurations: new Map(),
|
||||
manualRoutesByNodeId: new Map(),
|
||||
ripEnabledByNodeId: new Map(),
|
||||
ipForwardingByNodeId: new Map(),
|
||||
firewallConfigurationsByNodeId: new Map(),
|
||||
dhcpClientConfigurations: new Map(),
|
||||
dhcpServerConfigurations: new Map(),
|
||||
@@ -398,6 +402,7 @@ type SystemConfigurations = Pick<
|
||||
| 'hostWirelessConfigurations'
|
||||
| 'manualRoutesByNodeId'
|
||||
| 'ripEnabledByNodeId'
|
||||
| 'ipForwardingByNodeId'
|
||||
| 'firewallConfigurationsByNodeId'
|
||||
| 'dhcpClientConfigurations'
|
||||
| 'dhcpServerConfigurations'
|
||||
@@ -616,6 +621,9 @@ function registerSystemConfigurations(
|
||||
}
|
||||
if (routes.length > 0) system.manualRoutesByNodeId.set(node.id, routes);
|
||||
if (node.kind === 'router' && source.system.ripEnabled === true) system.ripEnabledByNodeId.set(node.id, true);
|
||||
if (source.system.ipForwardingEnabled !== undefined && source.system.ipForwardingEnabled !== forwardsIPv4ByDefault(node.kind)) {
|
||||
system.ipForwardingByNodeId.set(node.id, source.system.ipForwardingEnabled);
|
||||
}
|
||||
if ((node.kind === 'router' || node.kind === 'gateway') && source.system.firewall !== undefined) {
|
||||
system.firewallConfigurationsByNodeId.set(node.id, firewallConfigurationOf(source.system.firewall, where, warnings));
|
||||
}
|
||||
|
||||
@@ -11,3 +11,4 @@ export * from './topology.js';
|
||||
export * from './topologyWriter.js';
|
||||
export * from './zip.js';
|
||||
export * from './exportProject.js';
|
||||
export * from './projectSnapshot.js';
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// An imported project as the simulation core runs it: its nodes with their
|
||||
// ports (a port's MAC from the file, else derived from its id), its cables,
|
||||
// and the configurations the core reads.
|
||||
//
|
||||
// Only the cables count: a host's SSID connects nothing at run time, as in
|
||||
// Java (Swift joins a wireless host to the switch of its SSID; not taken).
|
||||
// A DNS server runs only when it is switched on (Java: DNSServer.aktiv).
|
||||
|
||||
import { portSnapshot, topologySnapshot, type DnsServerConfiguration, type Id, type TopologySnapshot } from '@mdg-filius/core';
|
||||
import type { ImportedProject } from './importProject.js';
|
||||
|
||||
export function projectSnapshot(project: ImportedProject): TopologySnapshot {
|
||||
const dnsServers = new Map<Id, DnsServerConfiguration>();
|
||||
for (const [nodeId, configuration] of project.dnsServerConfigurations) {
|
||||
if (project.activePrograms.get(nodeId)?.has('dnsServer') === true) dnsServers.set(nodeId, configuration);
|
||||
}
|
||||
return topologySnapshot({
|
||||
nodes: project.graph.nodes.map((node) => ({
|
||||
id: node.id,
|
||||
kind: node.kind,
|
||||
ports: node.ports.map((port) => portSnapshot(port.id, port.label, port.importedMacAddress)),
|
||||
})),
|
||||
links: project.graph.links,
|
||||
deviceConfigurations: project.deviceConfigurations,
|
||||
interfaceConfigurations: project.interfaceConfigurations,
|
||||
manualRoutesByNodeId: project.manualRoutesByNodeId,
|
||||
ripEnabledByNodeId: project.ripEnabledByNodeId,
|
||||
dhcpClientConfigurationsByNodeId: project.dhcpClientConfigurations,
|
||||
dhcpServerConfigurationsByNodeId: project.dhcpServerConfigurations,
|
||||
dnsServerConfigurationsByNodeId: dnsServers,
|
||||
firewallConfigurationsByNodeId: project.firewallConfigurationsByNodeId,
|
||||
portForwardingRowsByNodeId: project.portForwardingRowsByNodeId,
|
||||
switchConfigurationsByNodeId: project.switchConfigurations,
|
||||
ipForwardingByNodeId: project.ipForwardingByNodeId,
|
||||
});
|
||||
}
|
||||
@@ -200,6 +200,12 @@ export interface FlsSystemSoftware {
|
||||
firewall?: FlsFirewall;
|
||||
/** Every node with an IP stack: whether it asks a DHCP server for its address. */
|
||||
dhcpClient?: boolean;
|
||||
/**
|
||||
* Every node with an IP stack: whether it forwards packets for others
|
||||
* (InternetKnotenBetriebssystem.ipForwardingEnabled; routers and home
|
||||
* routers do from their constructor, hosts only when the file says so).
|
||||
*/
|
||||
ipForwardingEnabled?: boolean;
|
||||
/** Every node with an IP stack. */
|
||||
dhcpServer?: FlsDhcpServer;
|
||||
/** Gateway: rows of protocol, public port, LAN address, LAN port — as the file has them. */
|
||||
@@ -229,6 +235,11 @@ export interface FlsSystemSoftware {
|
||||
|
||||
type Warn = (message: string) => void;
|
||||
|
||||
/** Whether Java's constructor switches IP forwarding on: routers and home routers (VermittlungsrechnerBetriebssystem, GatewayFirmware). */
|
||||
export function forwardsByDefault(javaClass: FlsNodeClass): boolean {
|
||||
return javaClass === 'Vermittlungsrechner' || javaClass === 'Gateway';
|
||||
}
|
||||
|
||||
const FIREWALL_CLASS = 'filius.software.firewall.Firewall';
|
||||
const WEB_SERVER_CLASS = 'filius.software.www.WebServer';
|
||||
|
||||
@@ -263,6 +274,7 @@ export function readSystemSoftware(device: JavaObject, javaClass: FlsNodeClass,
|
||||
if (javaClass !== 'Switch' && javaClass !== 'Modem') {
|
||||
result.manualRoutes = manualRoutes(value('weiterleitungstabelle'), warn);
|
||||
result.dhcpClient = boolean(value('DHCPKonfiguration'), false, 'DHCPKonfiguration', warn);
|
||||
result.ipForwardingEnabled = boolean(value('ipForwardingEnabled'), forwardsByDefault(javaClass), 'ipForwardingEnabled', warn);
|
||||
result.dhcpServer = dhcpServer(value('DHCPServer'), warn);
|
||||
}
|
||||
if (javaClass === 'Gateway') result.staticNAT = rowsOfStrings(value('staticNAT'));
|
||||
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
short,
|
||||
} from './javaBeansWriter.js';
|
||||
import { addressEntry, type JavaAddressEntry, type JavaContact, type JavaEmail, type JavaEmailAccount, type JavaRecipient } from './javaEmail.js';
|
||||
import type { FlsApplication, FlsFileTree, FlsFirewall, FlsSystemSoftware } from './systemSoftware.js';
|
||||
import { forwardsByDefault, type FlsApplication, type FlsFileTree, type FlsFirewall, type FlsSystemSoftware } from './systemSoftware.js';
|
||||
import type { FlsDocumentationItem, FlsInterface, FlsLink, FlsNode, FlsNodeClass } from './topology.js';
|
||||
|
||||
/** The version line of the files this writer makes: the format of FILIUS 2.14. */
|
||||
@@ -239,6 +239,10 @@ function systemStatements(system: SystemToWrite, javaClass: FlsNodeClass, self:
|
||||
});
|
||||
}
|
||||
if (system.dhcpClient !== undefined) statements.push({ set: 'DHCPKonfiguration', value: system.dhcpClient });
|
||||
// Written only where it differs from what Java's constructor sets, as XMLEncoder does.
|
||||
if (system.ipForwardingEnabled !== undefined && system.ipForwardingEnabled !== forwardsByDefault(javaClass)) {
|
||||
statements.push({ set: 'ipForwardingEnabled', value: system.ipForwardingEnabled });
|
||||
}
|
||||
if (system.manualRoutes !== undefined) {
|
||||
statements.push({ get: 'weiterleitungstabelle', statements: [{ set: 'manuelleTabelle', value: rows(system.manualRoutes) }] });
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,289 @@
|
||||
// FILIUS' own examples run as Java FILIUS runs them (the acceptance of F2).
|
||||
// tools/java-oracle's ExampleRun opened each file in Java FILIUS as FILIUS
|
||||
// opens it, started it as the action mode does, played a story and wrote
|
||||
// every frame each card saw, the capture's tables, the story's calls and the
|
||||
// hosts' addresses (examples/<story>.java.json). Here the file is read with
|
||||
// this package's reader and run in the core (projectSnapshot); each card's
|
||||
// capture must hold the same frames at about the same times with the same
|
||||
// table rows (javaRun.ts, cards by their MAC address from the file), the
|
||||
// calls must return the same, the hosts must end with the same addresses.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { interfaceKey, NetworkRuntimeEngine, stableMacAddress, type DnsAddressResolution, type Id, type IPv4DeliveryResult } from '@mdg-filius/core';
|
||||
import { openProjectArchive, projectSnapshot, type ImportedProject } from '../src/index.js';
|
||||
import {
|
||||
engineText,
|
||||
expectSameCaptures,
|
||||
GAP_TOLERANCE_MILLISECONDS,
|
||||
gaps,
|
||||
javaFrames,
|
||||
withClientPortText,
|
||||
type CaptureCard,
|
||||
type JavaFrame,
|
||||
type JavaTables,
|
||||
type SeenFrame,
|
||||
} from '../../core/test/javaRun.js';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const examples = join(here, '../../../corpus/java-examples');
|
||||
const SEED = 14;
|
||||
|
||||
interface JavaRun extends JavaTables {
|
||||
frames: Record<string, JavaFrame[]>;
|
||||
events: [number, string][];
|
||||
devices: Record<string, [string, string, string, string]>;
|
||||
/** Each card's MAC address in Java and its node's name with the card's index, `name/index`. */
|
||||
cards: Record<string, string>;
|
||||
}
|
||||
|
||||
type Call = (label: string, run: () => string) => void;
|
||||
|
||||
interface Story {
|
||||
readonly file: string;
|
||||
/**
|
||||
* Hosts that start their DHCP clients together: which sends first is up to
|
||||
* Java's threads, and its log loses some of their frames (dhcpJava.test.ts).
|
||||
* Compared per client instead of per card: its own frames and the
|
||||
* server's answers to it, the long waits at about the same distances. Which address a
|
||||
* client gets follows the order the server hears them in: an address of
|
||||
* the server's range (`dhcpRange`) is a placeholder, and the hosts must
|
||||
* end with the same addresses, in any order.
|
||||
*/
|
||||
readonly dhcpClients?: readonly string[];
|
||||
readonly dhcpRange?: RegExp;
|
||||
/**
|
||||
* When the story's calls begin (the engine's time), after the DHCP clients
|
||||
* are done: the clients are compared before it, each card from it on, both
|
||||
* counted from the first call.
|
||||
*/
|
||||
readonly storyStart?: number;
|
||||
readonly play: (engine: NetworkRuntimeEngine, node: (name: string) => Id, call: Call) => void;
|
||||
}
|
||||
|
||||
const ICMP_CODES: Record<string, string> = { timeExceeded: '11/0', destinationNetworkUnreachable: '3/0', destinationHostUnreachable: '3/1' };
|
||||
|
||||
/** A ping in Java's words (ICMP.sendProbe): the answer's type, code and sender, or null. */
|
||||
function ping(engine: NetworkRuntimeEngine, from: Id, target: string, sequenceNumber: number): string {
|
||||
const result: IPv4DeliveryResult = engine.sendIcmpEcho(from, target, { sequenceNumber, identifier: 1 });
|
||||
const observations = engine.state.icmpObservationsByNodeId.get(from) ?? [];
|
||||
if (result.type === 'icmpError') {
|
||||
const error = observations.findLast((observation) => observation.message.kind === result.kind);
|
||||
return `ICMP ${ICMP_CODES[result.kind]} from ${error?.packet.senderIPAddress ?? '?'}`;
|
||||
}
|
||||
const reply = observations.find((observation) => observation.message.kind === 'echoReply' && observation.message.sequenceNumber === sequenceNumber);
|
||||
return reply === undefined ? 'null' : `ICMP 0/0 from ${reply.packet.senderIPAddress}`;
|
||||
}
|
||||
|
||||
/** A lookup in Java's words (Resolver): the address, null, or the exception. */
|
||||
function lookup(resolution: DnsAddressResolution): string {
|
||||
return resolution.type === 'timedOut' ? 'failed TimeOutException' : `returned ${resolution.address ?? 'null'}`;
|
||||
}
|
||||
|
||||
const STORIES: Record<string, Story> = {
|
||||
routing: {
|
||||
file: 'internetwork_komplett.fls',
|
||||
play: (engine, node, call) => {
|
||||
call('sendProbe 141.99.5.10', () => ping(engine, node('Notebook 1.10'), '141.99.5.10', 1));
|
||||
call('sendProbe 141.99.5.10', () => ping(engine, node('Notebook 1.11'), '141.99.5.10', 2));
|
||||
},
|
||||
},
|
||||
dhcp: {
|
||||
file: 'einfaches_rechnernetz_komplett.fls',
|
||||
dhcpClients: ['Notebook 1', 'Notebook 2', 'Notebook 3', 'Notebook 4'],
|
||||
dhcpRange: /192\.168\.0\.2\d/g,
|
||||
play: () => {},
|
||||
},
|
||||
dns: {
|
||||
file: 'dns_server.fls',
|
||||
play: (engine, node, call) => {
|
||||
call('holeIPAdresse www.filius.de', () => lookup(engine.resolveDnsAddress(node('Notebook'), 'www.filius.de')));
|
||||
call('holeIPAdresseMailServer filius.de', () => lookup(engine.resolveDnsMailServer(node('Notebook'), 'filius.de')));
|
||||
},
|
||||
},
|
||||
firewall: {
|
||||
file: 'Internet_Komplett_mit_eMail_Webserver_Intranet_Portforwarding_Firewall_DHCP_DE.fls',
|
||||
dhcpClients: ['Notebook', 'Notebook#2', 'Heimrouter', 'Notebook#3', 'iPad 1', 'iPad 2', 'iPad 3', 'iPad 4'],
|
||||
dhcpRange: /10\.50\.50\.\d+/g,
|
||||
storyStart: 8_000,
|
||||
play: (engine, node, call) => {
|
||||
engine.handle({ type: 'advance', toMilliseconds: 8_000 });
|
||||
call('sendProbe 130.100.200.25', () => ping(engine, node('iPad 1'), '130.100.200.25', 1));
|
||||
call('sendProbe 130.100.200.25', () => ping(engine, node('Notebook'), '130.100.200.25', 2));
|
||||
call('sendProbe 42.0.0.10', () => ping(engine, node('mail.gmx.de (130.100.200.25)'), '42.0.0.10', 3));
|
||||
},
|
||||
},
|
||||
nat: {
|
||||
file: 'nat_with_mapped_port.fls',
|
||||
play: (engine, node, call) => {
|
||||
call('sendProbe 141.99.4.10', () => ping(engine, node('Notebook 1.10'), '141.99.4.10', 1));
|
||||
call('sendProbe 141.99.5.10', () => ping(engine, node('Notebook 1.10'), '141.99.5.10', 2));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Where Java errs, per story: its run corrected, as Java means it.
|
||||
*
|
||||
* routing: Notebook 1.11 has no gateway; Java's sendProbe fails with a
|
||||
* NullPointerException (no route), the engine answers network unreachable
|
||||
* from the notebook itself, as a router does.
|
||||
*
|
||||
* nat: both pings leave with identifier 1. Java's Heimrouter Paul keeps one
|
||||
* entry per LAN endpoint, for its first remote end (natJava.test.ts,
|
||||
* udpTwoRemotes): the echo reply of the second, Heimrouter Vincent, finds no
|
||||
* entry and stays at Paul's WAN card. The engine adds an entry for the second
|
||||
* remote end: the reply goes on through Paul's LAN card to the notebook, a
|
||||
* cable later.
|
||||
*/
|
||||
const JAVA_BUGS: Record<string, (java: JavaRun) => JavaRun> = {
|
||||
routing: (java) => ({
|
||||
...java,
|
||||
events: java.events.map(([time, event]) => [time, event === 'sendProbe 141.99.5.10 failed NullPointerException' ? 'sendProbe 141.99.5.10 returned ICMP 3/0 from 141.99.1.11' : event]),
|
||||
}),
|
||||
nat: (java) => {
|
||||
const card = (name: string) => Object.entries(java.cards).find(([, each]) => each === name)![0];
|
||||
const [paulWan, paulLan, notebook] = [card('Heimrouter Paul/0'), card('Heimrouter Paul/1'), card('Notebook 1.10/0')];
|
||||
const reply = java.frames[paulWan]!.find((frame) => frame.icmp === 0 && frame.sourceIp === '141.99.5.10')!;
|
||||
const request = java.frames[paulLan]!.find((frame) => frame.icmp === 8 && frame.destinationIp === '141.99.5.10')!;
|
||||
const onLan: JavaFrame = { ...reply, time: reply.time + 1, source: request.destination, destination: request.source, destinationIp: request.sourceIp! };
|
||||
const called = java.events.findIndex(([, event]) => event === 'sendProbe 141.99.5.10 called');
|
||||
return {
|
||||
...java,
|
||||
frames: { ...java.frames, [paulLan]: [...java.frames[paulLan]!, onLan], [notebook]: [...java.frames[notebook]!, { ...onLan, time: onLan.time + 55 }] },
|
||||
events: java.events.map(([time, event], index) =>
|
||||
index === called + 1 ? [onLan.time + 55, 'sendProbe 141.99.5.10 returned ICMP 0/0 from 141.99.5.10'] : [time, event],
|
||||
),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/** Java's names as ScenarioNetwork.fromFile gives them: a repeated name gets #2, #3, … in the file's order. */
|
||||
function namesInJavaOrder(project: ImportedProject): Map<string, Id> {
|
||||
const names = new Map<string, Id>();
|
||||
for (const node of project.graph.nodes) {
|
||||
let name = node.displayName;
|
||||
for (let count = 2; names.has(name); count += 1) name = `${node.displayName}#${count}`;
|
||||
names.set(name, node.id);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
describe("FILIUS' examples as Java FILIUS runs them", () => {
|
||||
it('corrects Java only where it errs', () => {
|
||||
for (const [name, correct] of Object.entries(JAVA_BUGS)) {
|
||||
const java = JSON.parse(readFileSync(join(here, 'examples', `${name}.java.json`), 'utf8')) as JavaRun;
|
||||
expect(JSON.stringify(correct(java)), name).not.toBe(JSON.stringify(java));
|
||||
}
|
||||
});
|
||||
|
||||
for (const [name, story] of Object.entries(STORIES)) {
|
||||
it(`${name}: ${story.file}`, () => {
|
||||
const measured = JSON.parse(readFileSync(join(here, 'examples', `${name}.java.json`), 'utf8')) as JavaRun;
|
||||
const java = JAVA_BUGS[name]?.(measured) ?? measured;
|
||||
let counter = 0;
|
||||
const project = openProjectArchive(new Uint8Array(readFileSync(join(examples, story.file))), () => `00000000-0000-0000-0000-${(++counter).toString(16).padStart(12, '0')}`);
|
||||
const names = namesInJavaOrder(project);
|
||||
const engine = new NetworkRuntimeEngine(SEED);
|
||||
engine.handle({ type: 'start', snapshot: projectSnapshot(project), seed: SEED, initialTimeMilliseconds: 0 });
|
||||
engine.handle({ type: 'advance', toMilliseconds: 500 });
|
||||
|
||||
const events: [number, string][] = [];
|
||||
story.play(engine, (nodeName) => names.get(nodeName)!, (label, run) => {
|
||||
events.push([engine.state.currentTimeMilliseconds, `${label} called`]);
|
||||
events.push([engine.state.currentTimeMilliseconds, `${label} ${run().replace(/^(?!returned |failed )/, 'returned ')}`]);
|
||||
});
|
||||
const last = Math.max(...Object.values(java.frames).flat().map((frame) => frame.time), ...java.events.map(([time]) => time));
|
||||
engine.handle({ type: 'advance', toMilliseconds: Math.max(engine.state.currentTimeMilliseconds, last) + 1_000 });
|
||||
|
||||
expect(events.map(([, event]) => event)).toEqual(java.events.map(([, event]) => event));
|
||||
// Each card by its node and index. A card the file has no MAC address for gets one Java rolls at
|
||||
// each load, and the model one derived from its port: Java's stands for the engine's in the texts.
|
||||
const engineMacByJavaMac = new Map<string, string>();
|
||||
const cards: CaptureCard[] = Object.entries(java.cards).map(([mac, card]) => {
|
||||
const [nodeName, index] = card.split('/') as [string, string];
|
||||
const node = project.graph.nodes.find((each) => each.id === names.get(nodeName))!;
|
||||
const port = node.ports[Number(index)]!;
|
||||
engineMacByJavaMac.set(mac.toUpperCase(), (port.importedMacAddress ?? stableMacAddress(port.id)).toUpperCase());
|
||||
return { key: mac, portId: port.id };
|
||||
});
|
||||
const withEngineMacs = (text: string) => text.replace(/[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5}/g, (mac) => engineMacByJavaMac.get(mac.toUpperCase()) ?? mac);
|
||||
const javaStoryStart = java.events[0]?.[0];
|
||||
if (story.storyStart !== undefined) {
|
||||
expectSameCaptures(engine, cards, java.frames, {
|
||||
text: (text) => withClientPortText(withEngineMacs(text)).replace(/ID=\d+/g, 'ID=x'),
|
||||
cell: withEngineMacs,
|
||||
tables: measured,
|
||||
from: javaStoryStart!,
|
||||
starts: { java: javaStoryStart!, engine: story.storyStart },
|
||||
});
|
||||
}
|
||||
if (story.dhcpClients === undefined) {
|
||||
expectSameCaptures(engine, cards, java.frames, {
|
||||
text: (text) => withClientPortText(withEngineMacs(text)).replace(/ID=\d+/g, 'ID=x'),
|
||||
cell: withEngineMacs,
|
||||
tables: measured,
|
||||
});
|
||||
} else {
|
||||
// Each frame where it first shows on any card. Java's log loses some of a client's own frames at
|
||||
// its card, and four clients' broadcasts queue on the cables: a frame reaches the cards up to some
|
||||
// 450 ms apart, so the same text within a second is the same frame (a client repeats a message
|
||||
// only after a round trip, 2750 ms).
|
||||
const engineFrames = new Map<number, SeenFrame>();
|
||||
for (const { portId } of cards) {
|
||||
for (const each of engine.capture.frames(portId)) {
|
||||
if (!engineFrames.has(each.frame.identity)) engineFrames.set(each.frame.identity, { time: each.timeMilliseconds, text: engineText(each.frame) });
|
||||
}
|
||||
}
|
||||
const allEngine = [...engineFrames.values()].filter((frame) => frame.time < (story.storyStart ?? Infinity)).sort((lhs, rhs) => lhs.time - rhs.time);
|
||||
const allJava = javaFrames(java.frames, 1_000)
|
||||
.filter((frame) => story.storyStart === undefined || frame.time < javaStoryStart!).map((frame) => ({ ...frame, text: withEngineMacs(frame.text) }));
|
||||
for (const client of story.dhcpClients) {
|
||||
const port = project.graph.nodes.find((node) => node.id === names.get(client))!.ports[0]!;
|
||||
const mac = (port.importedMacAddress ?? stableMacAddress(port.id)).toUpperCase();
|
||||
const own = (frames: readonly SeenFrame[]) =>
|
||||
gaps(
|
||||
frames
|
||||
.filter((frame) => frame.text.startsWith(`${mac} >`) || frame.text.includes(`chaddr=${mac}`))
|
||||
.map((frame) => ({ ...frame, text: frame.text.replace(story.dhcpRange!, 'leased') })),
|
||||
);
|
||||
const [expected, actual] = [own(allJava), own(allEngine)];
|
||||
for (let index = 0; index < Math.max(expected.length, actual.length); index += 1) {
|
||||
expect(actual[index]?.text, `${client} frame #${index + 1}`).toBe(expected[index]?.text);
|
||||
// The server serves clients that come together one after the other, in the order Java's
|
||||
// threads happen to send, and their broadcasts queue on the cables: a short step depends on
|
||||
// how many frames are ahead (measured: up to some 550 ms with eight clients). Only the long
|
||||
// waits (a round trip) are compared, as tightly as elsewhere.
|
||||
if (expected[index]!.gap < 1_000) continue;
|
||||
expect(Math.abs(actual[index]!.gap - expected[index]!.gap), `${client} gap before frame #${index + 1}`).toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Java's addresses of a host's card (ScenarioNetwork.writeDevices: the first card of every node with
|
||||
// an IP stack); a router without a gateway has only its cards' settings in the model.
|
||||
const snapshot = engine.state.topologySnapshot;
|
||||
const devices = Object.fromEntries(
|
||||
[...names].flatMap(([nodeName, id]) => {
|
||||
const device = snapshot.deviceConfigurations.get(id);
|
||||
if (device !== undefined) return [[nodeName, [device.ipAddress, device.subnetMask, device.defaultGateway, device.dnsServer]]];
|
||||
const node = project.graph.nodes.find((each) => each.id === id)!;
|
||||
const card = node.ports[0] === undefined ? undefined : snapshot.interfaceConfigurations.get(interfaceKey(id, node.ports[0].id));
|
||||
return card === undefined || node.kind === 'networkSwitch' ? [] : [[nodeName, [card.ipAddress, card.subnetMask, '', '']]];
|
||||
}),
|
||||
);
|
||||
if (story.dhcpRange === undefined) {
|
||||
expect(devices).toEqual(java.devices);
|
||||
} else {
|
||||
const leased = (byName: Record<string, string[]>) =>
|
||||
Object.values(byName)
|
||||
.map((device) => device.join(' '))
|
||||
.sort();
|
||||
expect(leased(devices)).toEqual(leased(java.devices));
|
||||
expect(Object.keys(devices).sort()).toEqual(Object.keys(java.devices).sort());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -548,6 +548,7 @@ const SYNTHETIC: ProjectToExport = {
|
||||
['rt', [{ destinationNetwork: '10.6.0.0', subnetMask: '255.255.0.0', gateway: '10.1.0.2', interfaceIPAddress: '10.1.0.1' }]],
|
||||
]),
|
||||
ripEnabledByNodeId: new Map([['rt', true]]),
|
||||
ipForwardingByNodeId: new Map([['pc', true]]),
|
||||
firewallConfigurationsByNodeId: new Map([
|
||||
[
|
||||
'pc',
|
||||
@@ -742,6 +743,7 @@ describe('a model beyond the corpus, as Java loads it', () => {
|
||||
ssid: null,
|
||||
manualRoutes: [['10.5.0.0', '255.255.0.0', '10.0.0.254', '10.0.0.1']],
|
||||
dhcpClient: true,
|
||||
ipForwardingEnabled: true,
|
||||
dhcpServer: JAVA_DHCP_DEFAULTS,
|
||||
fileSystem: sortedTree({
|
||||
directory: 'root',
|
||||
@@ -836,6 +838,7 @@ describe('a model beyond the corpus, as Java loads it', () => {
|
||||
ssid: 'net',
|
||||
manualRoutes: [],
|
||||
dhcpClient: false,
|
||||
ipForwardingEnabled: false,
|
||||
dhcpServer: JAVA_DHCP_DEFAULTS,
|
||||
// Made by the peer-to-peer application.
|
||||
fileSystem: { directory: 'root', children: [{ directory: 'peer2peer', children: [] }] },
|
||||
@@ -872,6 +875,7 @@ describe('a model beyond the corpus, as Java loads it', () => {
|
||||
fileSystem: sortedTree({ directory: 'root', children: templateOf('Vermittlungsrechner') as JavaTree[] }),
|
||||
manualRoutes: [['10.6.0.0', '255.255.0.0', '10.1.0.2', '10.1.0.1']],
|
||||
dhcpClient: false,
|
||||
ipForwardingEnabled: true,
|
||||
dhcpServer: JAVA_DHCP_DEFAULTS,
|
||||
firewall: {
|
||||
activated: true,
|
||||
@@ -902,6 +906,7 @@ describe('a model beyond the corpus, as Java loads it', () => {
|
||||
fileSystem: sortedTree({ directory: 'root', children: templateOf('Gateway') as JavaTree[] }),
|
||||
manualRoutes: [],
|
||||
dhcpClient: true,
|
||||
ipForwardingEnabled: true,
|
||||
dhcpServer: {
|
||||
aktiv: true,
|
||||
untergrenze: '192.168.1.100',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,7 @@ import {
|
||||
import {
|
||||
decodeJavaBeans,
|
||||
importProject,
|
||||
projectSnapshot,
|
||||
readFlsArchive,
|
||||
readTopology,
|
||||
type FlsApplication,
|
||||
@@ -708,6 +709,45 @@ describe('importProject rules', () => {
|
||||
expect(project.hostWirelessConfigurations.get(withoutNetwork!)).toEqual({ isEnabled: true, ssid: '' });
|
||||
});
|
||||
|
||||
it('runs only the DNS servers that are switched on (projectSnapshot)', () => {
|
||||
const dnsServer = (isActive: boolean): FlsApplication => ({ key: 'dns', javaClass: 'filius.software.dns.DNSServer', source: flsNode('Rechner').source, isActive });
|
||||
const project = importProject(
|
||||
topology({
|
||||
nodes: [
|
||||
flsNode('Rechner', { interfaces: [nic('10.0.0.1')], system: { applications: [dnsServer(true)] } }),
|
||||
flsNode('Rechner', { interfaces: [nic('10.0.0.2')], system: { applications: [dnsServer(false)] } }),
|
||||
],
|
||||
}),
|
||||
counter(),
|
||||
);
|
||||
const [on, off] = project.graph.nodes.map((node) => node.id);
|
||||
|
||||
expect([...project.dnsServerConfigurations.keys()]).toEqual([on, off]);
|
||||
expect([...projectSnapshot(project).dnsServerConfigurationsByNodeId.keys()]).toEqual([on]);
|
||||
});
|
||||
|
||||
it("keeps a node's IP forwarding where it differs from its kind's: a host that forwards, a router that does not", () => {
|
||||
const system = (ipForwardingEnabled: boolean) => ({ manualRoutes: [], ipForwardingEnabled });
|
||||
const project = importProject(
|
||||
topology({
|
||||
nodes: [
|
||||
flsNode('Rechner', { interfaces: [nic('10.0.0.1')], system: system(true) }),
|
||||
flsNode('Notebook', { interfaces: [nic('10.0.0.2')], system: system(false) }),
|
||||
flsNode('Vermittlungsrechner', { portCount: 2, interfaces: [nic('10.0.0.3'), nic('10.1.0.3')], system: system(true) }),
|
||||
flsNode('Vermittlungsrechner', { portCount: 2, interfaces: [nic('10.0.0.4'), nic('10.1.0.4')], system: system(false) }),
|
||||
],
|
||||
}),
|
||||
counter(),
|
||||
);
|
||||
const [pc, notebook, router, quietRouter] = project.graph.nodes.map((node) => node.id);
|
||||
|
||||
expect([...project.ipForwardingByNodeId]).toEqual([
|
||||
[pc, true],
|
||||
[quietRouter, false],
|
||||
]);
|
||||
expect([notebook, router].some((id) => project.ipForwardingByNodeId.has(id!))).toBe(false);
|
||||
});
|
||||
|
||||
it('takes DHCP settings for hosts and home routers, trimmed, and only when they are not the defaults', () => {
|
||||
const defaults = { isActive: false, lowerBound: '0.0.0.0', upperBound: '0.0.0.0', gateway: '0.0.0.0', dnsServer: '0.0.0.0', useOwnSettings: false, staticAssignedAddresses: [] };
|
||||
const project = importProject(
|
||||
|
||||
@@ -177,6 +177,7 @@ const SYNTHETIC: TopologyToWrite = {
|
||||
ssid: 'Lab',
|
||||
manualRoutes: [['10.1.0.0', '255.255.0.0', '10.0.0.254', '10.0.0.1']],
|
||||
dhcpClient: true,
|
||||
ipForwardingEnabled: true,
|
||||
dhcpServer: {
|
||||
isActive: true,
|
||||
lowerBound: '10.0.0.100',
|
||||
@@ -425,6 +426,7 @@ describe('a topology beyond the corpus, as Java loads it', () => {
|
||||
ssid: 'Lab',
|
||||
manualRoutes: [['10.1.0.0', '255.255.0.0', '10.0.0.254', '10.0.0.1']],
|
||||
dhcpClient: true,
|
||||
ipForwardingEnabled: true,
|
||||
dhcpServer: {
|
||||
aktiv: true,
|
||||
untergrenze: '10.0.0.100',
|
||||
@@ -524,6 +526,7 @@ describe('a topology beyond the corpus, as Java loads it', () => {
|
||||
fileSystem: { directory: 'root', children: [...routerTemplate(), { directory: 'www.conf', children: [] }] },
|
||||
manualRoutes: [],
|
||||
dhcpClient: false,
|
||||
ipForwardingEnabled: true,
|
||||
dhcpServer: JAVA_DHCP_DEFAULTS,
|
||||
firewall: {
|
||||
activated: true,
|
||||
@@ -560,6 +563,7 @@ describe('a topology beyond the corpus, as Java loads it', () => {
|
||||
fileSystem: { directory: 'own', children: [javaFile('x', 'text', 'y')] },
|
||||
manualRoutes: [],
|
||||
dhcpClient: false,
|
||||
ipForwardingEnabled: true,
|
||||
dhcpServer: JAVA_DHCP_DEFAULTS,
|
||||
staticNAT: [],
|
||||
firewall: JAVA_ROUTER_FIREWALL,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -506,3 +506,43 @@ RIP: Der Anlauf entscheidet, wie viele Zufallspausen jeder Router zieht
|
||||
(jede Nachricht zieht eine), und der hängt an Javas Startreihenfolge. Eine
|
||||
Aufnahme, deren Anlauf von dem des Kerns (alle Router zugleich) abweicht,
|
||||
liegt danach in allen Zeiten daneben; das Werkzeug wird dann wiederholt.
|
||||
|
||||
## Beispiele von Filius (Abnahme F2)
|
||||
|
||||
`ExampleRun` öffnet eine Beispieldatei aus `corpus/java-examples` so, wie
|
||||
Filius eine Datei öffnet (`ScenarioNetwork.fromFile`:
|
||||
`SzenarioVerwaltung.laden` in die Listen des Fensters), startet sie wie der
|
||||
Aktionsmodus (jedes Gerät in der Reihenfolge der Datei), spielt einen Ablauf
|
||||
und schreibt nach `packages/fls/test/examples/<ablauf>.java.json` die Frames
|
||||
jeder Karte (unter ihrer MAC-Adresse), Javas Tabellen, die Zuordnung der
|
||||
Karten (`cards`: MAC → `Gerät/Index`, ein wiederholter Name mit `#2`, `#3`, …),
|
||||
die Aufrufe, die Adressen am Ende und jede Änderung der RIP-Tabellen. Abläufe:
|
||||
`routing` (internetwork_komplett: Pings über drei Router, dann von einem
|
||||
Notebook ohne Gateway), `dhcp` (einfaches_rechnernetz_komplett: vier
|
||||
Clients), `dns` (dns_server: rekursiv über Wurzel, `de.`, `filius.de.`),
|
||||
`nat` (nat_with_mapped_port: Pings durch zwei Heimrouter), `firewall`
|
||||
(Internet_Komplett_…_DE: acht DHCP-Clients, dann Pings durch die Firewall).
|
||||
|
||||
Gemessen (01.10.2026):
|
||||
|
||||
* **`ipForwardingEnabled`** eines Rechners: Java speichert und beachtet den
|
||||
Schalter (`meine-schule.de` leitet die Pakete der iPads weiter); Leser,
|
||||
Modell, Kern und `FlsOracle` kannten ihn nicht. `FlsOracle` schreibt ihn
|
||||
jetzt für jeden Knoten mit IP-Stapel; Korpus, Schreiber und Export neu
|
||||
geprüft.
|
||||
* **ARP-Lernregel** (`ARPThread`): Einen Eintrag lernt ein Gerät nur aus einer
|
||||
Anfrage nach seiner eigenen Adresse oder aus einer Antwort, eine Antwort
|
||||
gibt es nur auf eine Anfrage mit Ziel-MAC `ff:ff:ff:ff:ff:ff`, und ein
|
||||
Paket von der Netzadresse wird verworfen. Der Kern lernte aus jedem Paket.
|
||||
* Javas Firewall wendet ihre Regeln nur auf TCP und UDP an; ICMP lässt sie
|
||||
durch, außer „ICMP verwerfen" ist an (`checkAcceptIcmp`). Der Kern ebenso.
|
||||
* Ein Ping von einem Rechner ohne Gateway an ein fremdes Netz wirft in Java
|
||||
eine `NullPointerException`; der Kern meldet „Netz nicht erreichbar".
|
||||
* **Gleich lange RIP-Wege**: Hat ein Router zwei gleich lange Wege, nimmt
|
||||
Java den, dessen Nachricht seine Threads zuerst verarbeiten; im
|
||||
Firewall-Beispiel wechselte das zwischen Läufen (Router #3 zu 42.0.0.0
|
||||
über Router `""` oder über #2). Der Ablauf meidet solche Ziele.
|
||||
* Gleichzeitig startende DHCP-Clients bedient der Server in der Reihenfolge,
|
||||
in der Javas Threads senden; mit acht Clients stauen sich die Broadcasts,
|
||||
ein Frame erreicht die Karten bis zu rund 450 ms versetzt, und Javas
|
||||
Mitschnitt verliert eigene Frames eines Clients.
|
||||
|
||||
@@ -21,7 +21,7 @@ if [ ! -f "$upstream/target/filius.jar" ]; then
|
||||
fi
|
||||
classpath="$upstream/target/filius.jar:$upstream/target/lib/*"
|
||||
mkdir -p "$here/.build"
|
||||
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java"
|
||||
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java"
|
||||
# FILIUS writes its settings below $HOME; keep them out of the real one.
|
||||
mkdir -p "$here/.build/home"
|
||||
# Removed first: a crashed run must not leave the previous result standing.
|
||||
@@ -193,3 +193,15 @@ for story in pingWired pingWireless pingWithoutCable pingFromWithoutCable; do
|
||||
"$root/packages/core/test/scenarios/wlan-lan.scenario.json" "$story" 12000 "$output" 2>&1 | grep -v 'logback\|^$' || true
|
||||
test -s "$output"
|
||||
done
|
||||
|
||||
# FILIUS' own examples as Java FILIUS runs them, opened and started as FILIUS
|
||||
# does (ExampleRun): the acceptance of F2 (packages/fls/test/examplesJava.test.ts).
|
||||
for run in internetwork_komplett:routing:12000 einfaches_rechnernetz_komplett:dhcp:20000 dns_server:dns:12000 \
|
||||
nat_with_mapped_port:nat:15000 Internet_Komplett_mit_eMail_Webserver_Intranet_Portforwarding_Firewall_DHCP_DE:firewall:16000; do
|
||||
file="${run%%:*}"; rest="${run#*:}"; story="${rest%%:*}"
|
||||
output="$root/packages/fls/test/examples/$story.java.json"
|
||||
rm -f "$output"
|
||||
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" ExampleRun \
|
||||
"$root/corpus/java-examples/$file.fls" "$story" "${rest##*:}" "$output" 2>&1 | grep -v 'logback\|^$' || true
|
||||
test -s "$output"
|
||||
done
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
// Runs one of FILIUS' own example files (corpus/java-examples) in Java FILIUS,
|
||||
// loaded and started as FILIUS opens and starts it (ScenarioNetwork.fromFile),
|
||||
// plays a story on it and records every frame each card saw, the capture's
|
||||
// tables, the story's calls and each host's addresses at the end.
|
||||
//
|
||||
// ExampleRun <file.fls> <story> <milliseconds> <out.json>
|
||||
//
|
||||
// The stories:
|
||||
// routing internetwork_komplett: Notebook 1.10 pings Webserver 5.10 across
|
||||
// three routers; then Notebook 1.11, which has no gateway
|
||||
// dhcp einfaches_rechnernetz_komplett: four notebooks get addresses
|
||||
// from the server
|
||||
// dns dns_server: the notebook asks its DNS server (recursive) for
|
||||
// www.filius.de, then for the mail server of filius.de
|
||||
// nat nat_with_mapped_port: Notebook 1.10 (behind Heimrouter Paul)
|
||||
// pings Webserver www.jupiter.de, then Heimrouter Vincent's WAN
|
||||
// address
|
||||
// firewall Internet_Komplett_…_DE: when the DHCP clients have their
|
||||
// addresses (8 s), iPad 1 (its network dropped by the firewall,
|
||||
// which leaves ICMP to its own switch) pings mail.gmx.de, then
|
||||
// the first Notebook (accepted), then mail.gmx.de pings the
|
||||
// firewall's WAN address. Targets with one shortest RIP path
|
||||
// each: where two are equal, Java takes whichever its threads
|
||||
// learn first
|
||||
|
||||
import filius.hardware.knoten.Knoten;
|
||||
import filius.software.dns.Resolver;
|
||||
import filius.software.rip.RIPRoute;
|
||||
import filius.software.rip.RIPTable;
|
||||
import filius.software.system.VermittlungsrechnerBetriebssystem;
|
||||
import filius.software.system.InternetKnotenBetriebssystem;
|
||||
import filius.software.vermittlungsschicht.IcmpPaket;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ExampleRun {
|
||||
static ScenarioNetwork network;
|
||||
static final List<Object[]> events = new ArrayList<>();
|
||||
/** Every change of a RIP router's table: time, router, table. */
|
||||
static final List<Object[]> ripChanges = new ArrayList<>();
|
||||
static final Map<String, String> lastTables = new java.util.HashMap<>();
|
||||
|
||||
/** A RIP router's table as one line: `network/mask gateway interface hops; …`, in its order. */
|
||||
static String tableText(RIPTable table) {
|
||||
StringBuilder text = new StringBuilder();
|
||||
synchronized (table) {
|
||||
for (RIPRoute route : table.routes) {
|
||||
if (text.length() > 0) text.append("; ");
|
||||
text.append(route.getNetAddress()).append('/').append(route.getNetMask()).append(' ').append(route.getGateway())
|
||||
.append(' ').append(route.getInterfaceIpAddress()).append(' ').append(route.hops);
|
||||
}
|
||||
}
|
||||
return text.toString();
|
||||
}
|
||||
|
||||
static synchronized void pollTables() {
|
||||
for (Map.Entry<String, Knoten> entry : network.nodes.entrySet()) {
|
||||
if (!(entry.getValue().getSystemSoftware() instanceof VermittlungsrechnerBetriebssystem)) continue;
|
||||
RIPTable table = ((VermittlungsrechnerBetriebssystem) entry.getValue().getSystemSoftware()).getRIPTable();
|
||||
if (table == null) continue;
|
||||
String text = tableText(table);
|
||||
if (!text.equals(lastTables.get(entry.getKey()))) {
|
||||
lastTables.put(entry.getKey(), text);
|
||||
ripChanges.add(new Object[] { System.currentTimeMillis() - network.start, entry.getKey(), text });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static synchronized void event(String what) {
|
||||
events.add(new Object[] { System.currentTimeMillis() - network.start, what });
|
||||
}
|
||||
|
||||
interface Call {
|
||||
String run() throws Exception;
|
||||
}
|
||||
|
||||
static void call(String label, Call call) {
|
||||
event(label + " called");
|
||||
try {
|
||||
event(label + " returned " + call.run());
|
||||
} catch (Exception e) {
|
||||
event(label + " failed " + e.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
static String icmp(IcmpPaket packet) {
|
||||
return packet == null ? "null" : "ICMP " + packet.getIcmpType() + "/" + packet.getIcmpCode() + " from " + packet.getSender();
|
||||
}
|
||||
|
||||
static void ping(String node, String target, int sequence) {
|
||||
call("sendProbe " + target, () -> icmp(network.system(node).holeICMP().sendProbe(target, 64, 1, sequence)));
|
||||
}
|
||||
|
||||
static Resolver resolver(String node) {
|
||||
return network.system(node).holeDNSClient();
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
network = ScenarioNetwork.fromFile(args[0]);
|
||||
String story = args[1];
|
||||
long end = Long.parseLong(args[2]);
|
||||
network.start();
|
||||
Thread poller = new Thread(() -> {
|
||||
while (true) {
|
||||
pollTables();
|
||||
try {
|
||||
Thread.sleep(2);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
poller.setDaemon(true);
|
||||
poller.start();
|
||||
Thread.sleep(500);
|
||||
switch (story) {
|
||||
case "routing":
|
||||
ping("Notebook 1.10", "141.99.5.10", 1);
|
||||
ping("Notebook 1.11", "141.99.5.10", 2);
|
||||
break;
|
||||
case "dhcp":
|
||||
break;
|
||||
case "dns":
|
||||
call("holeIPAdresse www.filius.de", () -> resolver("Notebook").holeIPAdresse("www.filius.de"));
|
||||
call("holeIPAdresseMailServer filius.de", () -> resolver("Notebook").holeIPAdresseMailServer("filius.de"));
|
||||
break;
|
||||
case "nat":
|
||||
ping("Notebook 1.10", "141.99.4.10", 1);
|
||||
ping("Notebook 1.10", "141.99.5.10", 2);
|
||||
break;
|
||||
case "firewall":
|
||||
Thread.sleep(7500);
|
||||
ping("iPad 1", "130.100.200.25", 1);
|
||||
ping("Notebook", "130.100.200.25", 2);
|
||||
ping("mail.gmx.de (130.100.200.25)", "42.0.0.10", 3);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("unknown story " + story);
|
||||
}
|
||||
long rest = network.start + end - System.currentTimeMillis();
|
||||
if (rest > 0) Thread.sleep(rest);
|
||||
|
||||
ScenarioNetwork.Json out = new ScenarioNetwork.Json();
|
||||
out.open('{');
|
||||
network.writeFrames(out);
|
||||
network.writeDevices(out);
|
||||
// Every change of each RIP router's table (polled every 2 ms), as RipRun writes them.
|
||||
synchronized (ExampleRun.class) {
|
||||
out.key("ripTables").open('[');
|
||||
for (Object[] each : ripChanges) out.open('[').number((Long) each[0]).string((String) each[1]).string((String) each[2]).close(']');
|
||||
out.close(']');
|
||||
}
|
||||
synchronized (ExampleRun.class) {
|
||||
out.key("events").open('[');
|
||||
for (Object[] each : events) out.open('[').number((Long) each[0]).string((String) each[1]).close(']');
|
||||
out.close(']');
|
||||
}
|
||||
out.close('}');
|
||||
network.stop();
|
||||
ScenarioNetwork.write(args[3], out);
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
@@ -292,6 +292,7 @@ public class FlsOracle {
|
||||
json.put("fileSystem", system.getDateisystem() == null ? null : fileTree(system.getDateisystem().getRoot()));
|
||||
if (system instanceof Betriebssystem) json.put("applications", applications(system));
|
||||
json.put("dhcpClient", system.isDHCPKonfiguration());
|
||||
json.put("ipForwardingEnabled", system.isIpForwardingEnabled());
|
||||
DHCPServer server = system.getDHCPServer();
|
||||
if (server != null) {
|
||||
Map<String, Object> dhcp = new LinkedHashMap<>();
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
// DhcpRun and TcpRun.
|
||||
|
||||
import filius.gui.GUIContainer;
|
||||
import filius.gui.netzwerksicht.GUIKnotenItem;
|
||||
import filius.rahmenprogramm.SzenarioVerwaltung;
|
||||
import filius.hardware.Kabel;
|
||||
import filius.hardware.NetzwerkInterface;
|
||||
import filius.hardware.Port;
|
||||
@@ -54,8 +56,40 @@ public class ScenarioNetwork {
|
||||
/** Each card's MAC address, under its node's id (the first card) or `id#index` (the others). */
|
||||
final Map<String, String> macByNode = new LinkedHashMap<>();
|
||||
final List<String> servers = new ArrayList<>();
|
||||
/** Loaded from a .fls file (fromFile): started as the action mode starts it, in the file's order. */
|
||||
boolean fromFile;
|
||||
/** fromFile: each card's MAC address and its node's name with the card's index, `name/index`. */
|
||||
final Map<String, String> cardByMac = new LinkedHashMap<>();
|
||||
long start;
|
||||
|
||||
/**
|
||||
* A .fls file loaded as FILIUS opens it (SzenarioVerwaltung.laden into the
|
||||
* window's lists): the nodes under their names (a repeated name gets
|
||||
* `#2`, `#3`, …), each card's frames under its MAC address.
|
||||
*/
|
||||
static ScenarioNetwork fromFile(String path) throws Exception {
|
||||
GUIContainer gui = GUIContainer.getGUIContainer();
|
||||
if (!SzenarioVerwaltung.getInstance().laden(path, gui.getKnotenItems(), gui.getCableItems(), gui.getDocuItems())) {
|
||||
throw new IllegalStateException("FILIUS could not load " + path);
|
||||
}
|
||||
ScenarioNetwork network = new ScenarioNetwork();
|
||||
network.fromFile = true;
|
||||
for (GUIKnotenItem item : gui.getKnotenItems()) {
|
||||
Knoten knoten = item.getKnoten();
|
||||
String name = knoten.getName();
|
||||
for (int count = 2; network.nodes.containsKey(name); count++) name = knoten.getName() + "#" + count;
|
||||
network.nodes.put(name, knoten);
|
||||
if (knoten instanceof InternetKnoten) {
|
||||
List<NetzwerkInterface> nics = ((InternetKnoten) knoten).getNetzwerkInterfaces();
|
||||
for (int index = 0; index < nics.size(); index++) {
|
||||
network.macByNode.put(nics.get(index).getMac(), nics.get(index).getMac());
|
||||
network.cardByMac.put(nics.get(index).getMac(), name + "/" + index);
|
||||
}
|
||||
}
|
||||
}
|
||||
return network;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static ScenarioNetwork read(String path) throws Exception {
|
||||
Map<String, Object> scenario = (Map<String, Object>) new JsonReader(
|
||||
@@ -204,6 +238,12 @@ public class ScenarioNetwork {
|
||||
|
||||
/** Starts the simulation; times in the log count from here. */
|
||||
void start() throws InterruptedException {
|
||||
if (fromFile) {
|
||||
// GUIMainMenu.selectMode(MODUS_AKTION): every node in the window's order.
|
||||
start = System.currentTimeMillis();
|
||||
for (GUIKnotenItem item : GUIContainer.getGUIContainer().getKnotenItems()) item.getKnoten().getSystemSoftware().starten();
|
||||
return;
|
||||
}
|
||||
// Switches and DHCP servers first: FILIUS' client waits for every active
|
||||
// server to have started (waitUntilDhcpServerStarted, through the GUI's
|
||||
// node list, which this tool has not got).
|
||||
@@ -244,6 +284,11 @@ public class ScenarioNetwork {
|
||||
out.close('}');
|
||||
writeRows(out, "rows", true);
|
||||
writeRows(out, "layeredRows", false);
|
||||
if (fromFile) {
|
||||
out.key("cards").open('{');
|
||||
for (Map.Entry<String, String> entry : cardByMac.entrySet()) out.key(entry.getKey()).string(entry.getValue());
|
||||
out.close('}');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user