feat(core): the command line's network commands ping, host, nslookup, arp and netstat after Java, measured against Java

TerminalRun types lines into Java's command line on terminal-net at set
times; the engine types the same lines. New in the engine: an ICMP echo
in a program's thread (the first ICMP message after the send, whatever
it is), Java's route choice, the ARP table with Java's broadcast row, DNS
in a program's thread, ICMP broadcasts of a node's own network, and an
ARP wait that ends after what else happens at that millisecond.

Departures from Java: an echo is answered from the address asked
(RFC 792), network unreachable carries the packet with its sender, ping
names the host and takes only -b before it, arp and netstat list in the
order learned, netstat lists a UDP server's listening socket only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 08:03:10 +00:00
co-authored by Claude Opus 5.5
parent 016f9461df
commit 495c449ef2
22 changed files with 33994 additions and 42 deletions
+4 -3
View File
File diff suppressed because one or more lines are too long
+236 -10
View File
@@ -1,10 +1,21 @@
// FILIUS' command line (filius.software.lokal.Terminal, with the input // FILIUS' command line (filius.software.lokal.Terminal, with the input
// handling of GUIApplicationTerminalWindow): a line's first word is the // handling of GUIApplicationTerminalWindow): a line's first word is the
// command, the others its parameters; the program tells its window texts, // command, the others its parameters; the program tells its window texts,
// in the project's language, as Java writes them. The files are the node's // in the project's language, as Java writes them, and marks a command that
// (the snapshot's file system, the same the project keeps). // runs on (ping, host, nslookup) busy and done. The files are the node's
// (the snapshot's file system, the same the project keeps); the network
// commands ask the engine (ARP table, sockets, DNS, ICMP echo).
// //
// Departures from Java (terminal.test.ts names each): // Departures from Java on the network (terminalJava.test.ts names each):
// - ping names the host where Java names its first parameter (-b in the
// statistics of ping -b), and takes only -b before the host (Java takes
// any word, and pings a broadcast);
// - arp and netstat list their rows in the order learned or made (Java: a
// HashMap's order); netstat lists a UDP server's listening socket only
// (Java also a socket for each address and port it ever answered, never
// closed).
//
// Departures from Java on the files (terminal.test.ts names each):
// - a text file's size is its length in characters (Java decodes the text as // - a text file's size is its length in characters (Java decodes the text as
// Base64 and counts the bytes: "Hallo Welt" is 6); // Base64 and counts the bytes: "Hallo Welt" is 6);
// - a folder that is not there is "not there" for ls (Java: "empty"), and // - a folder that is not there is "not there" for ls (Java: "empty"), and
@@ -25,13 +36,15 @@
import type { Id } from '../../ids.js'; import type { Id } from '../../ids.js';
import type { VirtualFileContent, VirtualFileSystem } from '../../model/virtualFileSystem.js'; import type { VirtualFileContent, VirtualFileSystem } from '../../model/virtualFileSystem.js';
import { entryName, parentPath } from '../../model/virtualFileSystem.js'; import { entryName, parentPath } from '../../model/virtualFileSystem.js';
import type { NetworkRuntimeEngine } from '../engine.js'; import { javaIpCheck } from '../../util/javaIp.js';
import { isBroadcastFrom, type NetworkRuntimeEngine, type SocketRecord } from '../engine.js';
import { javaTrim } from '../http.js'; import { javaTrim } from '../http.js';
import type { Steps } from '../threads.js'; import { pause, type Steps } from '../threads.js';
import { javaBase64 } from '../../util/javaBase64.js'; import { javaBase64 } from '../../util/javaBase64.js';
import { CommandThread, ProgramLog } from './program.js'; import { CommandThread, ProgramLog } from './program.js';
import type { PageLanguage } from './routerPages.js'; import type { PageLanguage } from './routerPages.js';
import { TERMINAL_TEXTS, type TerminalTextKey } from './terminalTexts.js'; import { TERMINAL_TEXTS, type TerminalTextKey } from './terminalTexts.js';
import { IP_ADDRESS_PATTERN } from './web.js';
/** What the command line tells its window: a text, or whether a command runs on (Java's Boolean). */ /** What the command line tells its window: a text, or whether a command runs on (Java's Boolean). */
export type TerminalLogEntry = { readonly kind: 'text'; readonly text: string } | { readonly kind: 'busy'; readonly busy: boolean }; export type TerminalLogEntry = { readonly kind: 'text'; readonly text: string } | { readonly kind: 'busy'; readonly busy: boolean };
@@ -87,11 +100,48 @@ function evaluatedPath(path: string): string {
/** A path's last part (Dateisystem.getBasename). */ /** A path's last part (Dateisystem.getBasename). */
const basenameOf = (path: string) => path.slice(path.lastIndexOf('/') + 1); const basenameOf = (path: string) => path.slice(path.lastIndexOf('/') + 1);
/** The commands Terminal has a method for; the others are unknown (terminal_msg2). */
const COMMANDS = new Set([
'arp',
'cat',
'cd',
'copy',
'cp',
'del',
'dir',
'help',
'host',
'ipconfig',
'ls',
'mkdir',
'move',
'mv',
'netstat',
'nslookup',
'ping',
'pwd',
'rm',
'route',
'test',
'touch',
'type',
]);
/** Information.getPingRepeat: the echoes a ping sends. */
const PING_REPEAT = 4;
/** A line of 40 dashes under arp's head and at its end. */
const ARP_RULE = `${'-'.repeat(40)}\n`;
export class Terminal { export class Terminal {
readonly log: ProgramLog<TerminalLogEntry>; readonly log: ProgramLog<TerminalLogEntry>;
private directoryValue = '/'; private directoryValue = '/';
/** The program's thread (Anwendung.ausfuehren): one command after the other. */ /** The program's thread (Anwendung.ausfuehren): one command after the other. */
private readonly commands: CommandThread; private readonly commands: CommandThread;
/** Ctrl+C was pressed (Terminal.interrupted): a ping ends before its next echo. */
private interrupted = false;
/** The last ICMP identifier, -1 before the first (Terminal.identifier). */
private identifier = -1;
constructor( constructor(
private readonly engine: NetworkRuntimeEngine, private readonly engine: NetworkRuntimeEngine,
@@ -135,16 +185,32 @@ export class Terminal {
const [command, ...parameters] = words as [string, ...string[]]; const [command, ...parameters] = words as [string, ...string[]];
if (command === 'exit') return 'exit'; if (command === 'exit') return 'exit';
if (command === 'reset' || command === 'cls') return 'clear'; if (command === 'reset' || command === 'cls') return 'clear';
// terminalEingabeAuswerten, in the window's thread: an unknown command is told at once, even while another runs;
// a known one clears Ctrl+C and is handed to the program's thread.
if (!COMMANDS.has(command)) {
this.tell(`${this.text('terminal_msg2')}\n${this.text('terminal_msg3')}`);
return undefined;
}
this.interrupted = false;
this.commands.hand(() => this.run(command, parameters)); this.commands.hand(() => this.run(command, parameters));
this.engine.resumeThreads(); this.engine.resumeThreads();
return undefined; return undefined;
} }
/** The window's Ctrl+C (Terminal.setInterrupt). */
interrupt(): void {
this.interrupted = true;
}
private tell(text: string): void { private tell(text: string): void {
this.log.write({ kind: 'text', text }); this.log.write({ kind: 'text', text });
} }
/** Terminal.terminalEingabeAuswerten: the method of the command's name, if Java has one. */ private busy(busy: boolean): void {
this.log.write({ kind: 'busy', busy });
}
/** In the program's thread (Anwendung.ausfuehren): the method of the command's name. */
private *run(command: string, parameters: readonly string[]): Steps { private *run(command: string, parameters: readonly string[]): Steps {
const local: Record<string, (parameters: readonly string[]) => void> = { const local: Record<string, (parameters: readonly string[]) => void> = {
cat: (each) => this.cat(each), cat: (each) => this.cat(each),
@@ -166,12 +232,18 @@ export class Terminal {
touch: (each) => this.touch(each), touch: (each) => this.touch(each),
type: (each) => this.cat(each), type: (each) => this.cat(each),
}; };
const handler = Object.hasOwn(local, command) ? local[command] : undefined; const network: Record<string, (parameters: readonly string[]) => Steps> = {
if (handler === undefined) { host: (each) => this.host(each),
this.tell(`${this.text('terminal_msg2')}\n${this.text('terminal_msg3')}`); nslookup: (each) => this.nslookup(each),
ping: (each) => this.ping(each),
};
if (Object.hasOwn(network, command)) {
yield* network[command]!(parameters);
return; return;
} }
handler(parameters); if (command === 'arp') return this.arp(parameters);
if (command === 'netstat') return this.netstat();
local[command]!(parameters);
} }
/** Terminal.usage: the command's usage, or the list of commands. */ /** Terminal.usage: the command's usage, or the list of commands. */
@@ -360,4 +432,158 @@ export class Terminal {
const table = rows.map((row) => `| ${[row.destinationNetwork, row.subnetMask, row.nextHop, row.interfaceIPAddress].map(cell).join('')}\n`).join(''); const table = rows.map((row) => `| ${[row.destinationNetwork, row.subnetMask, row.nextHop, row.interfaceIPAddress].map(cell).join('')}\n`).join('');
this.tell(this.text('sw_terminal_msg7') + table); this.tell(this.text('sw_terminal_msg7') + table);
} }
/** Terminal.setIdentifier: at random the first time, then one more each time. */
private nextIdentifier(): number {
this.identifier = this.identifier === -1 ? this.engine.drawIcmpIdentifier() : this.identifier + 1;
return this.identifier;
}
/**
* Terminal.ping: the host's address (by DNS for a name), then four echo
* requests, one a second, each waiting for the first ICMP message back; a
* broadcast only with -b.
*/
private *ping(parameters: readonly string[]): Steps {
let target: string;
let broadcast = false;
if (parameters.length === 1) {
target = parameters[0]!;
} else if (parameters.length === 2 && parameters[0] === '-b') {
target = parameters[1]!;
broadcast = true;
} else {
return this.usage('ping');
}
let address = javaIpCheck(target) ?? undefined;
if (address === undefined) {
const resolution = yield* this.engine.resolveDnsAddressSteps(this.nodeId, target);
if (resolution.type === 'timedOut') return this.tell(`${this.text('sw_terminal_msg31')} (DNS)`);
address = resolution.address;
if (address === undefined) return this.tell(this.text('sw_terminal_msg30'));
}
const route = this.engine.routeRow(this.nodeId, address);
if (route === undefined) return this.tell(this.text('sw_terminal_msg52'));
if (!broadcast && isBroadcastFrom(address, route.interfaceIPAddress, route.subnetMask)) return this.tell(this.text('sw_terminal_msg53'));
const identifier = this.nextIdentifier();
this.busy(true);
this.tell(`PING ${target} (${address})`);
let received = 0;
let sequenceNumber = 1;
for (; !this.interrupted && sequenceNumber <= PING_REPEAT; sequenceNumber += 1) {
const start = this.engine.state.currentTimeMilliseconds;
const response = yield* this.engine.icmpEchoSteps(this.nodeId, address, { identifier, sequenceNumber });
const message = response?.message;
// ICMP.startSinglePing: anything but the echo's reply is a timeout, and the next echo goes at once.
if (message?.kind !== 'echoReply' || message.sequenceNumber !== sequenceNumber) {
this.tell(`\nFrom ${target} (${address}): icmp_seq=${sequenceNumber} -- Timeout!`);
continue;
}
if (message.identifier === identifier) {
const time = Math.round(this.engine.state.currentTimeMilliseconds - start);
this.tell(`\nFrom ${response!.packet.senderIPAddress}: icmp_seq=${sequenceNumber} ttl=${response!.packet.timeToLive} time=${time}ms`);
received += 1;
}
yield pause(start + 1_000);
}
const sent = sequenceNumber - 1;
const loss = sent === 0 ? 0 : Math.round((1 - received / sent) * 100);
this.tell(
`\n--- ${target} ${this.text('sw_terminal_msg45')} ---\n${sent} ${this.text('sw_terminal_msg46')}, ${received} ${this.text('sw_terminal_msg47')}, ${loss}% ${this.text('sw_terminal_msg48')}\n`,
);
this.busy(false);
}
/** Terminal.host: the name's A records, then its MX records. */
private *host(parameters: readonly string[]): Steps {
this.busy(true);
if (parameters.length !== 1) {
this.usage('host');
} else {
yield* this.lookUp(parameters[0]!);
}
this.busy(false);
}
private *lookUp(name: string): Steps {
const addresses = yield* this.engine.resolveDnsSteps(this.nodeId, name, 'A');
if (addresses.type === 'timedOut') return this.tell(this.text('sw_terminal_msg31'));
let found = 0;
// A resolution holds only the name's records of the type (Resolver.copyResourceRecords); for MX also the exchange's A.
for (const record of [...addresses.answers, ...addresses.authority]) {
this.tell(`${record.name} ${this.text('sw_terminal_msg28')} ${record.data}\n`);
found += 1;
}
const exchanges = yield* this.engine.resolveDnsSteps(this.nodeId, name, 'MX');
if (exchanges.type === 'timedOut') return this.tell(this.text('sw_terminal_msg31'));
for (const record of [...exchanges.answers, ...exchanges.authority].filter((each) => each.type === 'MX')) {
this.tell(`${record.name} ${this.text('sw_terminal_msg58')} ${record.data}\n`);
found += 1;
}
if (found === 0) this.tell(this.text('sw_terminal_msg30'));
}
/** Terminal.nslookup: the server asked and the answer's records. */
private *nslookup(parameters: readonly string[]): Steps {
if (parameters.length !== 1) return this.usage('nslookup');
const server = this.engine.state.topologySnapshot.deviceConfigurations.get(this.nodeId)?.dnsServer ?? '';
this.busy(true);
const response = yield* this.engine.resolveDnsSteps(this.nodeId, parameters[0]!, 'A', server);
if (response.type === 'timedOut') {
this.tell(`${this.text('sw_terminal_msg31')}\n`);
} else {
const shown = response.isLocal ? '127.0.0.1' : server;
this.tell(`Server: ${shown}\n`);
this.tell(`Address: ${shown}\n`);
this.tell('\nNon-authoritative Answer:\n');
for (const record of response.answers) {
this.tell(`Name: ${record.name}\n`);
this.tell(`Address: ${record.data}\n`);
}
}
this.busy(false);
}
/** Terminal.arp: the ARP table; -a an address's row only, -d an address (or all) forgotten first. */
private arp(parameters: readonly string[]): void {
let filter: string | undefined;
if (parameters.length === 2 && IP_ADDRESS_PATTERN.test(parameters[1]!)) {
if (parameters[0] === '-d') this.engine.removeArpEntry(this.nodeId, parameters[1]!);
else if (parameters[0] === '-a') filter = parameters[1];
} else if (parameters.length === 1 && parameters[0] === '-d') {
this.engine.resetArpTable(this.nodeId);
}
const rows = this.engine.arpTable(this.nodeId).filter((row) => filter === undefined || row.ipAddress === filter);
const table = rows.map((row) => `| ${row.ipAddress.padEnd(15)} | ${row.macAddress.padEnd(17)} |\n`).join('');
this.tell(`${this.text('sw_terminal_msg50')}${ARP_RULE}${table}${ARP_RULE}`);
}
/**
* Terminal.netstat: the node's TCP sockets, then its UDP sockets; a
* listening one with the connections it accepted. A UDP server lists its
* listening socket only (Java also lists a socket for each address and
* port it ever answered, which it never closes).
*/
private netstat(): void {
const sockets = [...this.engine.state.socketsById.values()].filter((socket) => socket.nodeId === this.nodeId && socket.tcpState !== 'CLOSED');
const cell = (host: string, port: string) => `| ${host.padStart(15)}:${port.padEnd(5)} `;
const row = (protocol: string, socket: SocketRecord) => {
const name = `| ${protocol.padEnd(7)} `;
if (socket.remoteIPAddress === undefined || socket.listening === true) {
return `${name}${cell('0.0.0.0', String(socket.localPort))}${cell('-', '-')}| ${'LISTEN'.padEnd(12)} |\n`;
}
const local = this.engine.routeRow(this.nodeId, socket.remoteIPAddress)?.interfaceIPAddress ?? '<unknown>';
const state = socket.tcpState ?? 'CONNECTED';
return `${name}${cell(local, String(socket.localPort))}${cell(socket.remoteIPAddress, String(socket.remotePort))}| ${state.padEnd(12)} |\n`;
};
let text = `${this.text('sw_terminal_msg49')}${'-'.repeat(74)}\n`;
for (const protocol of ['tcp', 'udp'] as const) {
for (const socket of sockets.filter((each) => each.protocol === protocol && each.parentListenerSocketId === undefined)) {
text += row(protocol.toUpperCase(), socket);
for (const accepted of sockets.filter((each) => each.parentListenerSocketId === socket.id)) text += row(protocol.toUpperCase(), accepted);
}
}
this.tell(text);
}
} }
@@ -50,6 +50,7 @@ export const TERMINAL_TEXTS = {
sw_terminal_msg38: 'Datei konnte nicht gelöscht werden\n', sw_terminal_msg38: 'Datei konnte nicht gelöscht werden\n',
sw_terminal_msg4: 'IP Adresse . . . :', sw_terminal_msg4: 'IP Adresse . . . :',
sw_terminal_msg45: 'Paketstatistik', sw_terminal_msg45: 'Paketstatistik',
sw_terminal_msg46: 'Paket(e) gesendet',
sw_terminal_msg47: 'Paket(e) empfangen', sw_terminal_msg47: 'Paket(e) empfangen',
sw_terminal_msg48: 'Paketverlust', sw_terminal_msg48: 'Paketverlust',
sw_terminal_msg49: '| Proto | Lokale Adresse | Entfernte Adresse | Zustand |\n', sw_terminal_msg49: '| Proto | Lokale Adresse | Entfernte Adresse | Zustand |\n',
@@ -157,6 +158,7 @@ export const TERMINAL_TEXTS = {
sw_terminal_msg38: 'File could not be deleted\n', sw_terminal_msg38: 'File could not be deleted\n',
sw_terminal_msg4: 'IP address . . .:', sw_terminal_msg4: 'IP address . . .:',
sw_terminal_msg45: 'packet statistics', sw_terminal_msg45: 'packet statistics',
sw_terminal_msg46: 'packet(s) transmitted',
sw_terminal_msg47: 'packet(s) received', sw_terminal_msg47: 'packet(s) received',
sw_terminal_msg48: 'packet loss', sw_terminal_msg48: 'packet loss',
sw_terminal_msg49: '| Proto | Local Address | Remote Address | State |\n', sw_terminal_msg49: '| Proto | Local Address | Remote Address | State |\n',
+184 -28
View File
@@ -348,6 +348,8 @@ export interface SocketRecord {
tcpState: TcpSocketState | undefined; tcpState: TcpSocketState | undefined;
/** A socket a TCP listener accepted: that listener. */ /** A socket a TCP listener accepted: that listener. */
readonly parentListenerSocketId: Id | undefined; readonly parentListenerSocketId: Id | undefined;
/** UDP: bound to its port only, a server's (Java's ServerSocket); its remote is the last sender. */
readonly listening?: boolean;
} }
/** Swift's TopologyRuntimeTCPSessionRecord; sequence numbers are unsigned 32-bit. */ /** Swift's TopologyRuntimeTCPSessionRecord; sequence numbers are unsigned 32-bit. */
@@ -379,9 +381,13 @@ export type TcpReceived = { readonly type: 'message'; readonly payload: Uint8Arr
/** Java's DNSQueryAgent waits five round trips for an answer. */ /** Java's DNSQueryAgent waits five round trips for an answer. */
export const DNS_QUERY_TIMEOUT_ROUND_TRIPS = 5; export const DNS_QUERY_TIMEOUT_ROUND_TRIPS = 5;
export type DnsResolution = { readonly type: 'answered'; readonly records: readonly DnsMessageRecord[] } | { readonly type: 'timedOut' }; export type DnsResolution = { readonly type: 'answered'; readonly records: readonly DnsMessageRecord[] } | { readonly type: 'timedOut' };
/** A resolution as it goes on the events: the answer and authority records apart, as Java keeps them. */ /**
type DnsLookup = * A resolution as it goes on the events: the answer and authority records
| { readonly type: 'answered'; readonly answers: readonly DnsMessageRecord[]; readonly authority: readonly DnsMessageRecord[] } * apart, as Java keeps them; isLocal for localhost, answered without asking
* (DNSNachricht.isLocal).
*/
export type DnsLookup =
| { readonly type: 'answered'; readonly answers: readonly DnsMessageRecord[]; readonly authority: readonly DnsMessageRecord[]; readonly isLocal: boolean }
| { readonly type: 'timedOut' }; | { readonly type: 'timedOut' };
export type DnsAddressResolution = { readonly type: 'resolved'; readonly address: string | undefined } | { readonly type: 'timedOut' }; export type DnsAddressResolution = { readonly type: 'resolved'; readonly address: string | undefined } | { readonly type: 'timedOut' };
@@ -610,7 +616,7 @@ function natWithDestination(packet: IPv4Packet, endpoint: NatEndpoint): IPv4Pack
} }
/** VermittlungsProtokoll.isBroadcast: the limited broadcast, or the broadcast address of the sender's network. */ /** VermittlungsProtokoll.isBroadcast: the limited broadcast, or the broadcast address of the sender's network. */
function isBroadcastFrom(receiverIPAddress: string, senderIPAddress: string, subnetMask: string): boolean { export function isBroadcastFrom(receiverIPAddress: string, senderIPAddress: string, subnetMask: string): boolean {
if (receiverIPAddress === LIMITED_BROADCAST_IP_ADDRESS) return true; if (receiverIPAddress === LIMITED_BROADCAST_IP_ADDRESS) return true;
const receiver = parseIPv4(receiverIPAddress); const receiver = parseIPv4(receiverIPAddress);
const sender = parseIPv4(senderIPAddress); const sender = parseIPv4(senderIPAddress);
@@ -808,6 +814,8 @@ interface ArpLookup {
readonly ipAddress: string; readonly ipAddress: string;
readonly networkInterface: NetworkInterface; readonly networkInterface: NetworkInterface;
attemptsLeft: number; attemptsLeft: number;
/** Its round trip is over and it waits for what else happens at that time (Java's thread wakes after it). */
woken?: boolean;
/** Swift, for the comparison: a lookup made while events run waits in a list emptied by the answer itself. */ /** Swift, for the comparison: a lookup made while events run waits in a list emptied by the answer itself. */
readonly swiftQueued: boolean; readonly swiftQueued: boolean;
readonly done: (macAddress: string | undefined) => void; readonly done: (macAddress: string | undefined) => void;
@@ -871,6 +879,10 @@ export class NetworkRuntimeEngine {
private tcpPortRandom: JavaCompatibleRandom; private tcpPortRandom: JavaCompatibleRandom;
private linkLocalRandom: JavaCompatibleRandom; private linkLocalRandom: JavaCompatibleRandom;
private dnsIdRandom: JavaCompatibleRandom; private dnsIdRandom: JavaCompatibleRandom;
/** Draws the command line's first ICMP identifier (Terminal.setIdentifier: new Random()). */
private icmpIdentifierRandom: JavaCompatibleRandom;
/** Nodes whose ARP table lost its broadcast row (arp -d 255.255.255.255), until the table is reset. */
private readonly arpBroadcastRowRemoved = new Set<Id>();
private runtimeSpeed: RuntimeSpeed; private runtimeSpeed: RuntimeSpeed;
private readonly packetTraceRetentionPolicy: TraceRetentionPolicy; private readonly packetTraceRetentionPolicy: TraceRetentionPolicy;
private isProcessingScheduledEvents = false; private isProcessingScheduledEvents = false;
@@ -967,6 +979,7 @@ export class NetworkRuntimeEngine {
this.tcpPortRandom = new JavaCompatibleRandom(normalizedSeed ^ 0x5443_5050n); this.tcpPortRandom = new JavaCompatibleRandom(normalizedSeed ^ 0x5443_5050n);
this.linkLocalRandom = new JavaCompatibleRandom(normalizedSeed ^ 0x4c4c_4144n); this.linkLocalRandom = new JavaCompatibleRandom(normalizedSeed ^ 0x4c4c_4144n);
this.dnsIdRandom = new JavaCompatibleRandom(normalizedSeed ^ 0x444e_5349n); this.dnsIdRandom = new JavaCompatibleRandom(normalizedSeed ^ 0x444e_5349n);
this.icmpIdentifierRandom = new JavaCompatibleRandom(normalizedSeed ^ 0x4943_4d50n);
this.runtimeSpeed = new RuntimeSpeed(); this.runtimeSpeed = new RuntimeSpeed();
this.packetTraceRetentionPolicy = packetTraceRetentionPolicy; this.packetTraceRetentionPolicy = packetTraceRetentionPolicy;
this.stateValue.simulationSpeedPercent = this.runtimeSpeed.percent; this.stateValue.simulationSpeedPercent = this.runtimeSpeed.percent;
@@ -1020,6 +1033,8 @@ export class NetworkRuntimeEngine {
this.tcpPortRandom = new JavaCompatibleRandom(seed ^ 0x5443_5050n); this.tcpPortRandom = new JavaCompatibleRandom(seed ^ 0x5443_5050n);
this.linkLocalRandom = new JavaCompatibleRandom(seed ^ 0x4c4c_4144n); this.linkLocalRandom = new JavaCompatibleRandom(seed ^ 0x4c4c_4144n);
this.dnsIdRandom = new JavaCompatibleRandom(seed ^ 0x444e_5349n); this.dnsIdRandom = new JavaCompatibleRandom(seed ^ 0x444e_5349n);
this.icmpIdentifierRandom = new JavaCompatibleRandom(seed ^ 0x4943_4d50n);
this.arpBroadcastRowRemoved.clear();
this.isProcessingScheduledEvents = false; this.isProcessingScheduledEvents = false;
this.applicationThreads.clear(); this.applicationThreads.clear();
this.applicationWakes.clear(); this.applicationWakes.clear();
@@ -1054,6 +1069,7 @@ export class NetworkRuntimeEngine {
this.dnsQueriesBySocketId.clear(); this.dnsQueriesBySocketId.clear();
state.pendingEvents = []; state.pendingEvents = [];
state.arpCachesByNodeId.clear(); state.arpCachesByNodeId.clear();
this.arpBroadcastRowRemoved.clear();
state.switchForwardingTablesByNodeId.clear(); state.switchForwardingTablesByNodeId.clear();
state.switchForwardingUpdatedAtMillisecondsByNodeId.clear(); state.switchForwardingUpdatedAtMillisecondsByNodeId.clear();
state.switchSeenFrameIdentitiesByNodeId.clear(); state.switchSeenFrameIdentitiesByNodeId.clear();
@@ -1530,6 +1546,7 @@ export class NetworkRuntimeEngine {
remotePort: options.remotePort, remotePort: options.remotePort,
tcpState: undefined, tcpState: undefined,
parentListenerSocketId: undefined, parentListenerSocketId: undefined,
listening: options.remoteIPAddress === undefined,
}); });
state.udpReceiveQueuesBySocketId.set(id, []); state.udpReceiveQueuesBySocketId.set(id, []);
return id; return id;
@@ -1919,7 +1936,7 @@ export class NetworkRuntimeEngine {
private beginDnsResolution(nodeId: Id, name: string, type: string, server: string | undefined, done: (resolution: DnsLookup) => void): void { private beginDnsResolution(nodeId: Id, name: string, type: string, server: string | undefined, done: (resolution: DnsLookup) => void): void {
const domainName = name.endsWith('.') ? name : `${name}.`; const domainName = name.endsWith('.') ? name : `${name}.`;
if (/^localhost\.?$/i.test(domainName)) { if (/^localhost\.?$/i.test(domainName)) {
done({ type: 'answered', answers: [{ name: 'localhost.', type: 'A', ttl: 3600, data: LOCALHOST_IP_ADDRESS }], authority: [] }); done({ type: 'answered', answers: [{ name: 'localhost.', type: 'A', ttl: 3600, data: LOCALHOST_IP_ADDRESS }], authority: [], isLocal: true });
return; return;
} }
const answers: DnsMessageRecord[] = []; const answers: DnsMessageRecord[] = [];
@@ -1927,7 +1944,7 @@ export class NetworkRuntimeEngine {
const queried = new Set<string>(); const queried = new Set<string>();
const ask = (serverToQuery: string | undefined): void => { const ask = (serverToQuery: string | undefined): void => {
if (serverToQuery === undefined || serverToQuery === '') { if (serverToQuery === undefined || serverToQuery === '') {
done({ type: 'answered', answers, authority }); done({ type: 'answered', answers, authority, isLocal: false });
return; return;
} }
this.queryDnsServer(nodeId, domainName, type, serverToQuery, (response) => { this.queryDnsServer(nodeId, domainName, type, serverToQuery, (response) => {
@@ -1943,7 +1960,7 @@ export class NetworkRuntimeEngine {
answers.push(...findDnsRecords(data[0]!.data, 'A', response.answers)); answers.push(...findDnsRecords(data[0]!.data, 'A', response.answers));
authority.push(...findDnsRecords(data[0]!.data, 'A', response.authority)); authority.push(...findDnsRecords(data[0]!.data, 'A', response.authority));
} }
done({ type: 'answered', answers, authority }); done({ type: 'answered', answers, authority, isLocal: false });
return; return;
} }
const all = [...response.answers, ...response.authority, ...response.additional]; const all = [...response.answers, ...response.authority, ...response.additional];
@@ -1951,7 +1968,7 @@ export class NetworkRuntimeEngine {
const next = nameServer === undefined ? undefined : findDnsRecords(nameServer.data, 'A', all)[0]?.data; const next = nameServer === undefined ? undefined : findDnsRecords(nameServer.data, 'A', all)[0]?.data;
// Java remembers only the servers it was referred to, not the first one. // Java remembers only the servers it was referred to, not the first one.
if (next === undefined || queried.has(next)) { if (next === undefined || queried.has(next)) {
done({ type: 'answered', answers, authority }); done({ type: 'answered', answers, authority, isLocal: false });
return; return;
} }
queried.add(next); queried.add(next);
@@ -1969,17 +1986,22 @@ export class NetworkRuntimeEngine {
return { type: 'resolved', address: findDnsRecords(domainName, 'A', resolution.records)[0]?.data }; return { type: 'resolved', address: findDnsRecords(domainName, 'A', resolution.records)[0]?.data };
} }
/** Resolver.holeIPAdresse in a program's thread. */ /** Resolver.resolve in a program's thread: asks the node's DNS server, or `serverIPAddress`. */
*resolveDnsAddressSteps(nodeId: Id, name: string): Steps<DnsAddressResolution> { *resolveDnsSteps(nodeId: Id, name: string, type: string, serverIPAddress?: string): Steps<DnsLookup> {
const domainName = name.endsWith('.') ? name : `${name}.`;
let lookup: DnsLookup | undefined; let lookup: DnsLookup | undefined;
const server = this.stateValue.topologySnapshot.deviceConfigurations.get(nodeId)?.dnsServer; const server = serverIPAddress ?? this.stateValue.topologySnapshot.deviceConfigurations.get(nodeId)?.dnsServer;
this.beginDnsResolution(nodeId, domainName, 'A', server, (result) => { this.beginDnsResolution(nodeId, name, type, server, (result) => {
lookup = result; lookup = result;
}); });
yield* waitUntil(() => lookup !== undefined || this.stateValue.phase !== 'running'); yield* waitUntil(() => lookup !== undefined || this.stateValue.phase !== 'running');
const result = lookup as DnsLookup | undefined; return (lookup as DnsLookup | undefined) ?? { type: 'timedOut' };
if (result === undefined || result.type === 'timedOut') return { type: 'timedOut' }; }
/** Resolver.holeIPAdresse in a program's thread. */
*resolveDnsAddressSteps(nodeId: Id, name: string): Steps<DnsAddressResolution> {
const domainName = name.endsWith('.') ? name : `${name}.`;
const result = yield* this.resolveDnsSteps(nodeId, domainName, 'A');
if (result.type === 'timedOut') return result;
return { type: 'resolved', address: findDnsRecords(domainName, 'A', [...result.answers, ...result.authority])[0]?.data }; return { type: 'resolved', address: findDnsRecords(domainName, 'A', [...result.answers, ...result.authority])[0]?.data };
} }
@@ -2007,14 +2029,8 @@ export class NetworkRuntimeEngine {
/** Resolver.holeIPAdresseMailServer in a program's thread (the e-mail server sending a mail on). */ /** Resolver.holeIPAdresseMailServer in a program's thread (the e-mail server sending a mail on). */
*resolveDnsMailServerSteps(nodeId: Id, domain: string): Steps<DnsAddressResolution> { *resolveDnsMailServerSteps(nodeId: Id, domain: string): Steps<DnsAddressResolution> {
const domainName = domain.endsWith('.') ? domain : `${domain}.`; const domainName = domain.endsWith('.') ? domain : `${domain}.`;
let lookup: DnsLookup | undefined; const result = yield* this.resolveDnsSteps(nodeId, domainName, 'MX');
const server = this.stateValue.topologySnapshot.deviceConfigurations.get(nodeId)?.dnsServer; if (result.type === 'timedOut') return result;
this.beginDnsResolution(nodeId, domainName, 'MX', server, (result) => {
lookup = result;
});
yield* waitUntil(() => lookup !== undefined || this.stateValue.phase !== 'running');
const result = lookup as DnsLookup | undefined;
if (result === undefined || result.type === 'timedOut') return { type: 'timedOut' };
const records = [...result.answers, ...result.authority]; const records = [...result.answers, ...result.authority];
const exchange = findDnsRecords(domainName, 'MX', records)[0]; const exchange = findDnsRecords(domainName, 'MX', records)[0];
return { type: 'resolved', address: exchange === undefined ? undefined : findDnsRecords(exchange.data, 'A', records)[0]?.data }; return { type: 'resolved', address: exchange === undefined ? undefined : findDnsRecords(exchange.data, 'A', records)[0]?.data };
@@ -4018,6 +4034,28 @@ export class NetworkRuntimeEngine {
} }
} }
/**
* The node's ARP table as Java's holds it (ARP.holeARPTabelle): the
* broadcast row Java puts in at each reset, then each learned address, in
* the order learned (Java: a HashMap's order).
*/
arpTable(nodeId: Id): { readonly ipAddress: string; readonly macAddress: string }[] {
const learned = [...(this.stateValue.arpCachesByNodeId.get(nodeId)?.values() ?? [])].map(({ ipAddress, macAddress }) => ({ ipAddress, macAddress }));
return this.arpBroadcastRowRemoved.has(nodeId) ? learned : [{ ipAddress: LIMITED_BROADCAST_IP_ADDRESS, macAddress: ETHERNET_BROADCAST_MAC_ADDRESS }, ...learned];
}
/** ARP.removeARPTableEntry: forgets the address (the broadcast row too, until the table is reset). */
removeArpEntry(nodeId: Id, ipAddress: string): void {
if (ipAddress === LIMITED_BROADCAST_IP_ADDRESS) this.arpBroadcastRowRemoved.add(nodeId);
this.stateValue.arpCachesByNodeId.get(nodeId)?.delete(ipAddress);
}
/** ARP.resetArpTable: every learned address forgotten, the broadcast row back. */
resetArpTable(nodeId: Id): void {
this.clearArpCache(nodeId);
this.arpBroadcastRowRemoved.delete(nodeId);
}
/** /**
* The MAC address for `targetIPAddress`, asking by ARP broadcast when it is * The MAC address for `targetIPAddress`, asking by ARP broadcast when it is
* not cached. Blocks like Java: each request waits a round trip for its * not cached. Blocks like Java: each request waits a round trip for its
@@ -4103,10 +4141,19 @@ export class NetworkRuntimeEngine {
for (const lookup of answered) lookup.done(macAddress); for (const lookup of answered) lookup.done(macAddress);
} }
/** No answer within the round trip: ask again, or give up. */ /**
* No answer within the round trip: ask again, or give up — after what
* else happens at that time (an answer coming then still counts).
*/
private handleArpTimeout(lookupId: number): void { private handleArpTimeout(lookupId: number): void {
const lookup = this.arpLookups.find((each) => each.id === lookupId); const lookup = this.arpLookups.find((each) => each.id === lookupId);
if (lookup === undefined) return; if (lookup === undefined) return;
if (lookup.woken !== true) {
lookup.woken = true;
this.schedule(this.stateValue.currentTimeMilliseconds, { kind: 'arpTimeout', lookupId });
return;
}
lookup.woken = false;
if (lookup.attemptsLeft > 0) { if (lookup.attemptsLeft > 0) {
lookup.attemptsLeft -= 1; lookup.attemptsLeft -= 1;
this.schedule(this.stateValue.currentTimeMilliseconds + this.runtimeSpeed.roundTripTimeMilliseconds, { kind: 'arpTimeout', lookupId }); this.schedule(this.stateValue.currentTimeMilliseconds + this.runtimeSpeed.roundTripTimeMilliseconds, { kind: 'arpTimeout', lookupId });
@@ -4667,6 +4714,88 @@ export class NetworkRuntimeEngine {
return result; return result;
} }
/** The first ICMP identifier of a command line (Terminal.setIdentifier: new Random().nextInt(43) * 23). */
drawIcmpIdentifier(): number {
return this.icmpIdentifierRandom.nextInt(43) * 23;
}
/**
* InternetKnotenBetriebssystem.determineRoute: the row of the forwarding
* table with the longest mask that holds the address, the first of equal
* ones; undefined when none does (RouteNotFoundException).
*/
routeRow(nodeId: Id, ipAddress: string): RouteRow | undefined {
const target = parseIPv4(ipAddress);
if (target === undefined) return undefined;
let best: RouteRow | undefined;
let bestMask = -1;
for (const row of this.forwardingTable(nodeId)) {
const mask = parseIPv4(row.subnetMask);
if (mask === undefined || mask <= bestMask || parseIPv4(row.destinationNetwork) !== maskAddress(target, mask)) continue;
best = row;
bestMask = mask;
}
return best;
}
/**
* ICMP.startSinglePing and sendProbe in a program's thread: the node's
* queue of ICMP answers emptied, an echo request sent (its ARP waits as
* Java's send does; a broadcast of the route's network goes to every card
* with TTL 1, ICMP.sendBroadcast), then a round trip waited for the first
* ICMP message to come (ICMPThread.waitForIcmpResponse), which it returns,
* whatever it answers; undefined when none came.
*/
*icmpEchoSteps(
nodeId: Id,
targetIPAddress: string,
options: { readonly identifier: number; readonly sequenceNumber: number; readonly timeToLive?: number },
): Steps<IcmpObservation | undefined> {
const interfaces = this.networkInterfaces(nodeId);
const route = this.routeForPacket(nodeId, targetIPAddress);
const sourceInterface = route.type === 'local' || route.type === 'egress' ? (route.interface ?? interfaces[0]) : interfaces[0];
if (sourceInterface === undefined) return undefined;
const packet: IPv4Packet = {
identity: this.allocatePacketIdentity(),
senderIPAddress: sourceInterface.ipAddress,
receiverIPAddress: targetIPAddress,
timeToLive: options.timeToLive ?? 64,
protocol: 'icmp',
payload: {
kind: 'icmp',
message: icmpMessage('echoRequest', { identifier: options.identifier, sequenceNumber: options.sequenceNumber, data: new Uint8Array() }),
},
};
this.recordTrace({
packetIdentity: packet.identity,
nodeId,
interfaceId: sourceInterface.portId,
direction: 'outbound',
layer: 'network',
operation: 'created',
afterHeaders: ipv4Headers(packet),
detail: 'ICMP echo request',
});
const observations = () => this.stateValue.icmpObservationsByNodeId.get(nodeId) ?? [];
const queued = observations().length;
let sentAt: number | undefined;
if (route.type === 'egress' && isBroadcastFrom(targetIPAddress, sourceInterface.ipAddress, sourceInterface.subnetMask)) {
this.transmitIPv4Packet(nodeId, { ...packet, timeToLive: 1 }, sourceInterface, ETHERNET_BROADCAST_MAC_ADDRESS, 'sent');
sentAt = this.stateValue.currentTimeMilliseconds;
} else {
this.sendIPv4PacketWithOptions(nodeId, packet, sourceInterface.portId, true, 'sent', {
onFinished: () => {
sentAt = this.stateValue.currentTimeMilliseconds;
},
});
}
const running = () => this.stateValue.phase === 'running';
yield* waitUntil(() => sentAt !== undefined || !running());
if (sentAt === undefined) return undefined;
yield* waitUntil(() => observations().length > queued || !running(), sentAt + this.runtimeSpeed.roundTripTimeMilliseconds);
return observations()[queued];
}
/** `traceroute`: echoes with TTL 1, 2, … until one is answered. */ /** `traceroute`: echoes with TTL 1, 2, … until one is answered. */
traceIcmpEcho( traceIcmpEcho(
fromNodeId: Id, fromNodeId: Id,
@@ -4894,10 +5023,16 @@ export class NetworkRuntimeEngine {
const node = this.nodeSnapshot(nodeId)!; const node = this.nodeSnapshot(nodeId)!;
const packetForProcessing = packet; const packetForProcessing = packet;
const localAddresses = new Set(this.networkInterfaces(nodeId).map((candidate) => candidate.ipAddress)); const interfaces = this.networkInterfaces(nodeId);
const localAddresses = new Set(interfaces.map((candidate) => candidate.ipAddress));
// ICMPThread: a broadcast of a network of the node's, from that network, is the node's; one of another network is dropped.
const isIcmp = packetForProcessing.payload.kind === 'icmp';
const isBroadcastOf = (candidate: NetworkInterface) => isBroadcastFrom(packetForProcessing.receiverIPAddress, candidate.ipAddress, candidate.subnetMask);
if ( if (
localAddresses.has(packetForProcessing.receiverIPAddress) || localAddresses.has(packetForProcessing.receiverIPAddress) ||
packetForProcessing.receiverIPAddress === LIMITED_BROADCAST_IP_ADDRESS packetForProcessing.receiverIPAddress === LOCALHOST_IP_ADDRESS ||
packetForProcessing.receiverIPAddress === LIMITED_BROADCAST_IP_ADDRESS ||
(isIcmp && interfaces.some((candidate) => isBroadcastOf(candidate) && sameSubnet(packetForProcessing.senderIPAddress, candidate.ipAddress, candidate.subnetMask)))
) { ) {
if ( if (
isPcClassEndpoint(node.kind) && isPcClassEndpoint(node.kind) &&
@@ -4912,6 +5047,21 @@ export class NetworkRuntimeEngine {
if (!this.deliverIPv4Locally(packetForProcessing, nodeId, networkInterface, finished)) finished(); if (!this.deliverIPv4Locally(packetForProcessing, nodeId, networkInterface, finished)) finished();
return; return;
} }
if (isIcmp && interfaces.some(isBroadcastOf)) {
this.recordTrace({
frameIdentity,
packetIdentity: packetForProcessing.identity,
nodeId,
interfaceId: networkInterface.portId,
direction: 'inbound',
layer: 'network',
operation: 'dropped',
beforeHeaders: ipv4Headers(packetForProcessing),
detail: 'ICMP broadcast of another network',
});
finished();
return;
}
// Java: routers and home routers forward, a host when its IP forwarding is on (ipForwardingEnabled). // 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))) { if (!(this.stateValue.topologySnapshot.ipForwardingByNodeId.get(nodeId) ?? forwardsIPv4ByDefault(node.kind))) {
this.recordTrace({ this.recordTrace({
@@ -4957,8 +5107,9 @@ export class NetworkRuntimeEngine {
}, },
onFinished: finished, onFinished: finished,
}); });
// As for host unreachable, the packet as the router would have forwarded it (Java: half made, without its sender).
if (result.type === 'dropped' && this.routeForPacket(nodeId, packetForProcessing.receiverIPAddress).type === 'unavailable') { if (result.type === 'dropped' && this.routeForPacket(nodeId, packetForProcessing.receiverIPAddress).type === 'unavailable') {
this.emitIcmpError(nodeId, networkInterface, 'destinationNetworkUnreachable', packet); this.emitIcmpError(nodeId, networkInterface, 'destinationNetworkUnreachable', forwarded);
} }
} }
@@ -4971,9 +5122,14 @@ export class NetworkRuntimeEngine {
switch (message.kind) { switch (message.kind) {
case 'echoRequest': { case 'echoRequest': {
if (packet.receiverIPAddress === LIMITED_BROADCAST_IP_ADDRESS && packet.timeToLive !== 1) return false; if (packet.receiverIPAddress === LIMITED_BROADCAST_IP_ADDRESS && packet.timeToLive !== 1) return false;
// From the address asked (RFC 792: the addresses reversed); for a broadcast, the card's. Java answers from the
// address its route back gives: from the near card when a router's far address was asked, from 127.0.0.1 when the
// node's own address was.
const askedOwnAddress =
packet.receiverIPAddress === LOCALHOST_IP_ADDRESS || this.networkInterfaces(nodeId).some((candidate) => candidate.ipAddress === packet.receiverIPAddress);
const reply: IPv4Packet = { const reply: IPv4Packet = {
identity: this.allocatePacketIdentity(), identity: this.allocatePacketIdentity(),
senderIPAddress: networkInterface.ipAddress, senderIPAddress: askedOwnAddress ? packet.receiverIPAddress : networkInterface.ipAddress,
receiverIPAddress: packet.senderIPAddress, receiverIPAddress: packet.senderIPAddress,
timeToLive: 64, timeToLive: 64,
protocol: 'icmp', protocol: 'icmp',
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
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,210 @@
{
"frames": {
"00000000-0000-0000-0000-000000000401": [],
"00000000-0000-0000-0000-000000000402": [],
"00000000-0000-0000-0000-000000000403": [],
"00000000-0000-0000-0000-000000000404": [],
"00000000-0000-0000-0000-000000000405": [],
"00000000-0000-0000-0000-000000000405#1": [],
"00000000-0000-0000-0000-000000000406": []
},
"rows": {
"00000000-0000-0000-0000-000000000401": [],
"00000000-0000-0000-0000-000000000402": [],
"00000000-0000-0000-0000-000000000403": [],
"00000000-0000-0000-0000-000000000404": [],
"00000000-0000-0000-0000-000000000405": [],
"00000000-0000-0000-0000-000000000405#1": [],
"00000000-0000-0000-0000-000000000406": []
},
"layeredRows": {
"00000000-0000-0000-0000-000000000401": [],
"00000000-0000-0000-0000-000000000402": [],
"00000000-0000-0000-0000-000000000403": [],
"00000000-0000-0000-0000-000000000404": [],
"00000000-0000-0000-0000-000000000405": [],
"00000000-0000-0000-0000-000000000405#1": [],
"00000000-0000-0000-0000-000000000406": []
},
"acts": [
[
500,
"00000000-0000-0000-0000-000000000401",
"ping 192.168.0.20"
],
[
6000,
"00000000-0000-0000-0000-000000000401",
"ping localhost"
],
[
11500,
"00000000-0000-0000-0000-000000000401",
"ping 127.0.0.1"
]
],
"events": [
[
500,
"00000000-0000-0000-0000-000000000401",
"call",
"ping 192.168.0.20"
],
[
502,
"00000000-0000-0000-0000-000000000401",
"busy",
"true"
],
[
502,
"00000000-0000-0000-0000-000000000401",
"text",
"PING 192.168.0.20 (192.168.0.20)"
],
[
508,
"00000000-0000-0000-0000-000000000401",
"text",
"\nFrom 127.0.0.1: icmp_seq=1 ttl=64 time=4ms"
],
[
1504,
"00000000-0000-0000-0000-000000000401",
"text",
"\nFrom 127.0.0.1: icmp_seq=2 ttl=64 time=0ms"
],
[
2505,
"00000000-0000-0000-0000-000000000401",
"text",
"\nFrom 127.0.0.1: icmp_seq=3 ttl=64 time=1ms"
],
[
3504,
"00000000-0000-0000-0000-000000000401",
"text",
"\nFrom 127.0.0.1: icmp_seq=4 ttl=64 time=0ms"
],
[
4506,
"00000000-0000-0000-0000-000000000401",
"text",
"\n--- 192.168.0.20 Paketstatistik ---\n4 Paket(e) gesendet, 4 Paket(e) empfangen, 0% Paketverlust\n"
],
[
4506,
"00000000-0000-0000-0000-000000000401",
"busy",
"false"
],
[
6000,
"00000000-0000-0000-0000-000000000401",
"call",
"ping localhost"
],
[
6001,
"00000000-0000-0000-0000-000000000401",
"busy",
"true"
],
[
6001,
"00000000-0000-0000-0000-000000000401",
"text",
"PING localhost (127.0.0.1)"
],
[
6001,
"00000000-0000-0000-0000-000000000401",
"text",
"\nFrom 127.0.0.1: icmp_seq=1 ttl=64 time=0ms"
],
[
7002,
"00000000-0000-0000-0000-000000000401",
"text",
"\nFrom 127.0.0.1: icmp_seq=2 ttl=64 time=1ms"
],
[
8001,
"00000000-0000-0000-0000-000000000401",
"text",
"\nFrom 127.0.0.1: icmp_seq=3 ttl=64 time=0ms"
],
[
9002,
"00000000-0000-0000-0000-000000000401",
"text",
"\nFrom 127.0.0.1: icmp_seq=4 ttl=64 time=1ms"
],
[
10001,
"00000000-0000-0000-0000-000000000401",
"text",
"\n--- localhost Paketstatistik ---\n4 Paket(e) gesendet, 4 Paket(e) empfangen, 0% Paketverlust\n"
],
[
10001,
"00000000-0000-0000-0000-000000000401",
"busy",
"false"
],
[
11500,
"00000000-0000-0000-0000-000000000401",
"call",
"ping 127.0.0.1"
],
[
11500,
"00000000-0000-0000-0000-000000000401",
"busy",
"true"
],
[
11500,
"00000000-0000-0000-0000-000000000401",
"text",
"PING 127.0.0.1 (127.0.0.1)"
],
[
11501,
"00000000-0000-0000-0000-000000000401",
"text",
"\nFrom 127.0.0.1: icmp_seq=1 ttl=64 time=1ms"
],
[
12500,
"00000000-0000-0000-0000-000000000401",
"text",
"\nFrom 127.0.0.1: icmp_seq=2 ttl=64 time=0ms"
],
[
13501,
"00000000-0000-0000-0000-000000000401",
"text",
"\nFrom 127.0.0.1: icmp_seq=3 ttl=64 time=1ms"
],
[
14501,
"00000000-0000-0000-0000-000000000401",
"text",
"\nFrom 127.0.0.1: icmp_seq=4 ttl=64 time=0ms"
],
[
15503,
"00000000-0000-0000-0000-000000000401",
"text",
"\n--- 127.0.0.1 Paketstatistik ---\n4 Paket(e) gesendet, 4 Paket(e) empfangen, 0% Paketverlust\n"
],
[
15503,
"00000000-0000-0000-0000-000000000401",
"busy",
"false"
]
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,202 @@
{
"seed": 91,
"nodes": [
{
"id": "00000000-0000-0000-0000-000000000401",
"kind": "pc",
"ports": [
{
"id": "00000000-0000-0000-0000-000000000411",
"label": "eth0"
}
]
},
{
"id": "00000000-0000-0000-0000-000000000402",
"kind": "pc",
"ports": [
{
"id": "00000000-0000-0000-0000-000000000412",
"label": "eth0"
}
]
},
{
"id": "00000000-0000-0000-0000-000000000403",
"kind": "pc",
"ports": [
{
"id": "00000000-0000-0000-0000-000000000413",
"label": "eth0"
}
]
},
{
"id": "00000000-0000-0000-0000-000000000404",
"kind": "pc",
"ports": [
{
"id": "00000000-0000-0000-0000-000000000414",
"label": "eth0"
}
]
},
{
"id": "00000000-0000-0000-0000-000000000405",
"kind": "router",
"ports": [
{
"id": "00000000-0000-0000-0000-000000000451",
"label": "eth0"
},
{
"id": "00000000-0000-0000-0000-000000000452",
"label": "eth1"
}
]
},
{
"id": "00000000-0000-0000-0000-000000000406",
"kind": "pc",
"ports": [
{
"id": "00000000-0000-0000-0000-000000000416",
"label": "eth0"
}
]
},
{
"id": "00000000-0000-0000-0000-000000000407",
"kind": "networkSwitch",
"ports": [
{
"id": "00000000-0000-0000-0000-000000000471",
"label": "sw1"
},
{
"id": "00000000-0000-0000-0000-000000000472",
"label": "sw2"
},
{
"id": "00000000-0000-0000-0000-000000000473",
"label": "sw3"
},
{
"id": "00000000-0000-0000-0000-000000000474",
"label": "sw4"
},
{
"id": "00000000-0000-0000-0000-000000000475",
"label": "sw5"
}
]
}
],
"links": [
{
"id": "00000000-0000-0000-0000-00000000F401",
"sourceNode": "00000000-0000-0000-0000-000000000401",
"sourcePort": "00000000-0000-0000-0000-000000000411",
"targetNode": "00000000-0000-0000-0000-000000000407",
"targetPort": "00000000-0000-0000-0000-000000000471"
},
{
"id": "00000000-0000-0000-0000-00000000F402",
"sourceNode": "00000000-0000-0000-0000-000000000402",
"sourcePort": "00000000-0000-0000-0000-000000000412",
"targetNode": "00000000-0000-0000-0000-000000000407",
"targetPort": "00000000-0000-0000-0000-000000000472"
},
{
"id": "00000000-0000-0000-0000-00000000F403",
"sourceNode": "00000000-0000-0000-0000-000000000403",
"sourcePort": "00000000-0000-0000-0000-000000000413",
"targetNode": "00000000-0000-0000-0000-000000000407",
"targetPort": "00000000-0000-0000-0000-000000000473"
},
{
"id": "00000000-0000-0000-0000-00000000F404",
"sourceNode": "00000000-0000-0000-0000-000000000404",
"sourcePort": "00000000-0000-0000-0000-000000000414",
"targetNode": "00000000-0000-0000-0000-000000000407",
"targetPort": "00000000-0000-0000-0000-000000000474"
},
{
"id": "00000000-0000-0000-0000-00000000F405",
"sourceNode": "00000000-0000-0000-0000-000000000405",
"sourcePort": "00000000-0000-0000-0000-000000000451",
"targetNode": "00000000-0000-0000-0000-000000000407",
"targetPort": "00000000-0000-0000-0000-000000000475"
},
{
"id": "00000000-0000-0000-0000-00000000F406",
"sourceNode": "00000000-0000-0000-0000-000000000405",
"sourcePort": "00000000-0000-0000-0000-000000000452",
"targetNode": "00000000-0000-0000-0000-000000000406",
"targetPort": "00000000-0000-0000-0000-000000000416"
}
],
"devices": {
"00000000-0000-0000-0000-000000000401": {
"ip": "192.168.0.20",
"mask": "255.255.255.0",
"gateway": "192.168.0.1",
"dns": "192.168.0.10"
},
"00000000-0000-0000-0000-000000000402": {
"ip": "192.168.0.10",
"mask": "255.255.255.0"
},
"00000000-0000-0000-0000-000000000403": {
"ip": "192.168.0.30",
"mask": "255.255.255.0",
"gateway": "192.168.0.1",
"dns": "192.168.0.10"
},
"00000000-0000-0000-0000-000000000404": {
"ip": "192.168.0.31",
"mask": "255.255.255.0",
"gateway": "192.168.0.1",
"dns": "192.168.0.10"
},
"00000000-0000-0000-0000-000000000405": {
"ip": "192.168.0.1",
"mask": "255.255.255.0"
},
"00000000-0000-0000-0000-000000000406": {
"ip": "10.0.0.2",
"mask": "255.255.255.0",
"gateway": "10.0.0.1",
"dns": "192.168.0.53"
}
},
"interfaces": [
{
"node": "00000000-0000-0000-0000-000000000405",
"port": "00000000-0000-0000-0000-000000000451",
"ip": "192.168.0.1",
"mask": "255.255.255.0"
},
{
"node": "00000000-0000-0000-0000-000000000405",
"port": "00000000-0000-0000-0000-000000000452",
"ip": "10.0.0.1",
"mask": "255.255.255.0"
}
],
"dnsServers": {
"00000000-0000-0000-0000-000000000402": {
"records": [
"www.filius.de A 192.168.0.30",
"filius.de A 192.168.0.31",
"filius.de MX mail.filius.de",
"mail.filius.de A 192.168.0.30",
"multi.filius.de A 192.168.0.30",
"multi.filius.de A 192.168.0.31",
"fern.filius.de A 10.0.0.2",
"weg.filius.de A 192.168.0.99",
"post.filius.de MX mail.filius.de"
]
}
}
}
+5
View File
@@ -217,6 +217,11 @@ describe('the command line as Java FILIUS answers it', () => {
const cases = javaCases(language); const cases = javaCases(language);
const text = (key: TerminalTextKey) => TERMINAL_TEXTS[language][key]; const text = (key: TerminalTextKey) => TERMINAL_TEXTS[language][key];
it(`has Java's texts (${language})`, () => {
const javaTexts = (JSON.parse(readFileSync(join(here, `terminal.${language}.java.json`), 'utf8')) as { texts: Record<string, string> }).texts;
for (const [key, value] of Object.entries(TERMINAL_TEXTS[language])) expect(value, key).toBe(javaTexts[key]);
});
it(`corrects Java only where it errs (${language})`, () => { it(`corrects Java only where it errs (${language})`, () => {
for (const [name, fix] of Object.entries(JAVA_BUGS)) { for (const [name, fix] of Object.entries(JAVA_BUGS)) {
const javaCase = cases.find((each) => each.name === name)!; const javaCase = cases.find((each) => each.name === name)!;
+411
View File
@@ -0,0 +1,411 @@
// The command line's network commands as Java FILIUS runs them.
// tools/java-oracle's TerminalRun typed lines into FILIUS' command line on
// the hosts of terminal-net at set times and wrote every frame each card
// saw, Java's tables of them, and everything each command line told its
// window (terminal-java-<story>.java.json). The engine types the same lines
// at the same times (apps/terminal.ts): each card's frames in the same
// order, with the same content and rows, at about the same time; the same
// texts and busy marks, at about the same time.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { NetworkRuntimeEngine, withSnapshot } from '../src/index.js';
import { expectSameCaptures, GAP_TOLERANCE_MILLISECONDS, watchedEngine, withClientPortText, type JavaFrame, type JavaTables, type SeenFrame } from './javaRun.js';
import { snapshotOf, type Scenario } from './scenario.js';
const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios');
const CLIENT = '00000000-0000-0000-0000-000000000401';
const DNS = '00000000-0000-0000-0000-000000000402';
const WEB = '00000000-0000-0000-0000-000000000403';
const ECHO = '00000000-0000-0000-0000-000000000404';
const FAR = '00000000-0000-0000-0000-000000000406';
const HOSTS = [CLIENT, DNS, WEB, ECHO, FAR];
type Told = ['text' | 'busy', string];
interface JavaRun extends JavaTables {
frames: Record<string, JavaFrame[]>;
acts: [number, string, string][];
/** time, host, kind (call, text or busy), text */
events: [number, string, string, string][];
}
const STORIES = ['pingLan', 'pingRouted', 'pingNobody', 'pingUnreachable', 'pingBroadcast', 'pingSelf', 'pingErrors', 'interrupt', 'dns', 'dnsTimeout', 'arp', 'netstat'] as const;
type Lines = Record<string, [number, Told][]>;
function javaLines(java: JavaRun): Lines {
const lines: Lines = Object.fromEntries(HOSTS.map((host) => [host, []]));
for (const [time, host, kind, text] of java.events) {
if (kind === 'call') continue;
lines[host]!.push([time, [kind as Told[0], text]]);
}
return lines;
}
function engineLines(engine: NetworkRuntimeEngine): Lines {
return Object.fromEntries(
HOSTS.map((host) => [
host,
engine.terminal(host)!.log.all.map((line): [number, Told] => [line.timeMilliseconds, line.kind === 'text' ? ['text', line.text] : ['busy', String(line.busy)]]),
]),
);
}
/**
* A text with placeholders: a client's port; a ping's time (compared on its
* own); the rows of an ARP or netstat table sorted (Java lists them in a
* HashMap's order, the engine in the order learned, or made).
*/
function lineText(text: string): string {
const table = /^(\| [^\n]*\|\n-+\n)((?:\|[^\n]*\n)*)(-+\n)?$/.exec(text);
// Sorted within each protocol of netstat's, the protocols in the order they come (TCP, then UDP, in Java too).
const rows = table?.[2]!.split('\n').filter(Boolean) ?? [];
const protocols = [...new Set(rows.map((row) => /^\| (TCP|UDP) /.exec(row)?.[1] ?? ''))];
const protocol = (row: string) => protocols.indexOf(/^\| (TCP|UDP) /.exec(row)?.[1] ?? '');
rows.sort((lhs, rhs) => protocol(lhs) - protocol(rhs) || (lhs < rhs ? -1 : lhs > rhs ? 1 : 0));
const sorted = table === null ? text : `${table[1]}${rows.join('\n')}\n${table[3] ?? ''}`;
return withClientPortText(sorted).replace(/time=\d+ms/, 'time=…ms');
}
const pingTime = (text: string) => Number(/time=(\d+)ms/.exec(text)?.[1] ?? 0);
/** The identifier a ping draws at random (Java: new Random()). */
const cellText = (text: string) => text.replace(/Identifier: \d+/, 'Identifier: x').replace(/identifier=\d+/, 'identifier=x');
/** Java's run with what each line told replaced where Java errs. */
function withTold(java: JavaRun, change: (host: string, text: string, time: number) => string | undefined): JavaRun {
return {
...java,
events: java.events.flatMap(([time, host, kind, text]): [number, string, string, string][] => {
if (kind !== 'text') return [[time, host, kind, text]];
const changed = change(host, text, time);
return changed === undefined ? [] : [[time, host, kind, changed]];
}),
};
}
/** The answers to an echo from `from` to the client, in Java's frames and rows, as from `to`. */
function withEchoReplySource(java: JavaRun, from: string, to: string, after: number): JavaRun {
const frames = Object.fromEntries(
Object.entries(java.frames).map(([card, list]) => [
card,
list.map((frame) => (frame.icmp === 0 && frame.sourceIp === from && frame.time >= after ? { ...frame, sourceIp: to } : frame)),
]),
);
const replies = (card: string) =>
new Set(java.frames[card]!.flatMap((frame, index) => (frame.icmp === 0 && frame.sourceIp === from && frame.time >= after ? [String(index + 1)] : [])));
const rows = (tables: Record<string, (string | null)[][]> | undefined) =>
tables === undefined
? undefined
: Object.fromEntries(
Object.entries(tables).map(([card, list]) => [card, list.map((row) => (replies(card).has(row[0]!) && row[1] === from ? [row[0]!, to, ...row.slice(2)] : row))]),
);
return { ...java, frames, rows: rows(java.rows), layeredRows: rows(java.layeredRows) };
}
/** Java's tables with each cell changed. */
function withCells(java: JavaRun, change: (cell: string) => string): JavaRun {
const tables = (list: Record<string, (string | null)[][]> | undefined) =>
list === undefined
? undefined
: Object.fromEntries(Object.entries(list).map(([card, rows]) => [card, rows.map((row) => row.map((cell) => (cell === null ? null : change(cell))))]));
return { ...java, rows: tables(java.rows), layeredRows: tables(java.layeredRows) };
}
const USAGE_PING = 'ping - test connection to other computer\nUsage: ping <remote host> \n <remote host> the remote address as IP address or host name\n';
/**
* Where Java errs: its run corrected, as Java means it.
* - Java's ping names its first parameter where it means the host (-b in the
* statistics of ping -b), and takes any first word for -b (ping -x pings).
* - An echo's answer comes from the address asked (RFC 792: the addresses
* reversed): Java answers from 127.0.0.1 when the host's own address is
* asked, from the near card when a router's far address is.
* - netstat on a UDP server lists its listening socket only: Java also lists
* a socket for each address and port it ever answered (never closed).
* - A router's network unreachable carries the echo as it would have
* forwarded it, with its sender (Java's is half made: sender null).
*/
const JAVA_BUGS: Partial<Record<(typeof STORIES)[number], (java: JavaRun) => JavaRun>> = {
pingBroadcast: (java) =>
withTold(java, (host, text, time) => {
const target = host === CLIENT && time >= 7_000 ? '255.255.255.255' : '192.168.0.255';
return text.replace('--- -b ', `--- ${target} `).replace('From -b (', `From ${target} (`);
}),
pingErrors: (java) => {
const typed = java.acts.find(([, , line]) => line.startsWith('ping -x'))![0];
const corrected = withTold(java, (_, text, time) => (time < typed ? text : undefined));
return {
...corrected,
frames: Object.fromEntries(Object.entries(java.frames).map(([card, list]) => [card, list.filter((frame) => frame.time < typed)])),
events: [...corrected.events.filter(([time, , kind]) => time < typed || kind !== 'busy'), [typed, CLIENT, 'text', USAGE_PING]],
};
},
pingSelf: (java) => withTold(java, (_, text, time) => (time < 6_000 ? text.replace('From 127.0.0.1:', 'From 192.168.0.20:') : text)),
pingRouted: (java) => {
const typed = java.acts.find(([, , line]) => line === 'ping 10.0.0.1')![0];
return withEchoReplySource(
withTold(java, (_, text, time) => (time >= typed ? text.replace('From 192.168.0.1:', 'From 10.0.0.1:') : text)),
'192.168.0.1',
'10.0.0.1',
typed,
);
},
pingUnreachable: (java) => withCells(java, (cell) => cell.replace('sender=null;', 'sender=192.168.0.20;')),
netstat: (java) => withTold(java, (host, text) => (host === DNS ? text.replace(/\| UDP {5}\| {4}192\.168\.0\.10:53 {4}\|[^\n]*CONNECTED +\|\n/g, '') : text)),
};
/** Where the frames are compared only up to a time (Java's): pingErrors, the ping Java sends for ping -x. */
const UNTIL: Partial<Record<(typeof STORIES)[number], (java: JavaRun) => number>> = {
pingErrors: (java) => java.acts.find(([, , line]) => line.startsWith('ping -x'))![0],
};
/** The hosts that answer a broadcast echo at once: which answer comes first is a race of Java's threads. */
const BROADCAST_ANSWER = /From 192\.168\.0\.(1|10|30|31): icmp_seq/;
/**
* pingBroadcast: the hosts answer each broadcast echo at once, and the order
* of their ARP requests and answers on the cables is a race of Java's
* threads. Its frames are compared second by second (each ping's), as a
* set; Java's tables, which go frame by frame, are left out.
*/
const RACING = new Set(['pingBroadcast']);
const bySecond = (frames: SeenFrame[]): SeenFrame[] =>
frames
.map((frame) => ({ time: Math.floor((frame.time - 900) / 1_000) * 1_000 + 900, text: frame.text }))
.sort((lhs, rhs) => lhs.time - rhs.time || lhs.text.localeCompare(rhs.text));
describe('the command line on the network as Java FILIUS runs it', () => {
it('corrects Java only where it errs, and cuts only after the story began', () => {
for (const [name, correct] of Object.entries(JAVA_BUGS)) {
const java = JSON.parse(readFileSync(join(scenarioDirectory, `terminal-java-${name}.java.json`), 'utf8')) as JavaRun;
expect(JSON.stringify(correct(java)), name).not.toBe(JSON.stringify(java));
expect(UNTIL[name as (typeof STORIES)[number]]?.(java) ?? Number.POSITIVE_INFINITY, name).toBeGreaterThan(1_000);
}
});
// ICMPThread.waitForIcmpResponse misses its wake-up when the answer is there before it waits (a race of Java's
// threads: an echo to the host itself, the host unreachable a host gives itself), and waits a whole round trip.
// The engine answers at once; a recording with a missed wake-up is made again (tools/java-oracle/run.sh).
it("was recorded without a missed wake-up of Java's", () => {
const told = (name: string) =>
(JSON.parse(readFileSync(join(scenarioDirectory, `terminal-java-${name}.java.json`), 'utf8')) as JavaRun).events.filter(([, , kind]) => kind === 'text');
for (const [, , , text] of told('pingSelf')) expect(pingTime(text), `pingSelf: ${JSON.stringify(text)}`).toBeLessThan(1_000);
const timeouts = told('pingNobody').filter(([, , , text]) => text.includes('Timeout!'));
for (const [index, [time, , , text]] of timeouts.slice(1).entries()) {
if (text.includes('icmp_seq=1 ')) continue;
expect(time - timeouts[index]![0], 'pingNobody: two ARP round trips per echo').toBeLessThan(6_000);
}
});
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'terminal-net.scenario.json'), 'utf8')) as Scenario;
for (const name of STORIES) {
it(name, () => {
const measured = JSON.parse(readFileSync(join(scenarioDirectory, `terminal-java-${name}.java.json`), 'utf8')) as JavaRun;
const java = JAVA_BUGS[name]?.(measured) ?? measured;
const snapshot = withSnapshot(snapshotOf(scenario), {
language: 'de',
terminalNodeIds: new Set(HOSTS),
...(name === 'netstat'
? {
echoServersByNodeId: new Map([
[ECHO, { port: 55555, isActive: true }],
[DNS, { port: 55555, isActive: true }],
]),
simpleClientsByNodeId: new Map([[CLIENT, { targetAddress: '', targetPort: 55555 }]]),
}
: {}),
});
const { engine } = watchedEngine(scenario, snapshot);
const acts: [number, () => void][] = java.acts.map(([time, host, line]) => [
time,
() => {
if (line === '^C') engine.terminal(host)!.interrupt();
else if (line === 'disconnect') engine.simpleClient(CLIENT)!.disconnect();
else engine.terminal(host)!.enter(line);
},
]);
if (name === 'netstat') acts.unshift([500, () => engine.simpleClient(CLIENT)!.connect('192.168.0.31', 55555)]);
for (const [time, act] of acts) {
engine.handle({ type: 'advance', toMilliseconds: time });
act();
}
engine.handle({ type: 'advance', toMilliseconds: Math.max(...java.events.map(([time]) => time)) + 5_000 });
const racing = RACING.has(name);
expectSameCaptures(engine, scenario, java.frames, {
text: (text) => withClientPortText(text).replace(/ID=\d+/g, 'ID=x'),
...(racing ? { normalized: bySecond, starts: { java: 0, engine: 0 } } : { tables: java }),
cell: cellText,
until: UNTIL[name]?.(measured),
});
if (racing) {
// ICMP.sendBroadcast: with TTL 1, to every card.
const echoes = engine.capture.frames(scenario.nodes.find((node) => node.id === CLIENT)!.ports[0]!.id).flatMap(({ frame }) => {
const packet = frame.payload.kind === 'ipv4' ? frame.payload.packet : undefined;
return packet?.payload.kind === 'icmp' && packet.payload.message.kind === 'echoRequest' ? [[frame.destinationMacAddress, packet.timeToLive]] : [];
});
expect(echoes).toEqual(Array.from({ length: 8 }, () => ['FF:FF:FF:FF:FF:FF', 1]));
}
const expected = javaLines(java);
const actual = engineLines(engine);
const told = ([kind, text]: Told): Told => [kind, lineText(name === 'pingBroadcast' ? text.replace(BROADCAST_ANSWER, 'From <broadcast>: icmp_seq') : text)];
for (const host of HOSTS) {
expect(
actual[host]!.map(([, line]) => told(line)),
`${host}'s lines`,
).toEqual(expected[host]!.map(([, line]) => told(line)));
for (const [index, [javaTime, line]] of expected[host]!.entries()) {
const [engineTime, engineLine] = actual[host]![index]!;
const late = javaTime - engineTime;
expect(Math.abs(late), `${host}'s line ${JSON.stringify(line[1].slice(0, 60))}: Java ${late} ms later`).toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS);
expect(Math.abs(pingTime(line[1]) - pingTime(engineLine[1])), `${host}'s ping time in ${JSON.stringify(line[1])}`).toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS);
}
}
});
}
});
describe('the command line on the network beyond the stories', () => {
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'terminal-net.scenario.json'), 'utf8')) as Scenario;
const started = () => watchedEngine(scenario, withSnapshot(snapshotOf(scenario), { language: 'de', terminalNodeIds: new Set(HOSTS) })).engine;
const texts = (engine: NetworkRuntimeEngine) => engine.terminal(CLIENT)!.log.all.flatMap((line) => (line.kind === 'text' ? [line.text] : []));
const echoIdentifiers = (engine: NetworkRuntimeEngine) =>
engine.capture.frames(scenario.nodes.find((node) => node.id === CLIENT)!.ports[0]!.id).flatMap(({ frame }) => {
const packet = frame.payload.kind === 'ipv4' ? frame.payload.packet : undefined;
return packet?.payload.kind === 'icmp' && packet.payload.message.kind === 'echoRequest' ? [packet.payload.message.identifier] : [];
});
it('sends no echo for a ping whose Ctrl+C came before it began', () => {
const engine = started();
engine.terminal(CLIENT)!.enter('host www.filius.de');
engine.terminal(CLIENT)!.enter('ping 192.168.0.31');
engine.terminal(CLIENT)!.interrupt();
engine.handle({ type: 'advance', toMilliseconds: 5_000 });
expect(texts(engine).slice(-2)).toEqual([
'PING 192.168.0.31 (192.168.0.31)',
'\n--- 192.168.0.31 Paketstatistik ---\n0 Paket(e) gesendet, 0 Paket(e) empfangen, 0% Paketverlust\n',
]);
expect(echoIdentifiers(engine)).toEqual([]);
});
it("counts no answer to another ping's echo, and waits out its second", () => {
const engine = started();
engine.terminal(CLIENT)!.enter('ping 192.168.0.31');
// An echo reply with the first echo's number, but another identifier (a multiple of 23 is drawn), first in the queue.
const reply = { kind: 'echoReply', identifier: 1, sequenceNumber: 1, data: new Uint8Array() } as const;
const packet = {
identity: 0,
senderIPAddress: '192.168.0.31',
receiverIPAddress: '192.168.0.20',
timeToLive: 64,
protocol: 'icmp',
payload: { kind: 'icmp', message: reply },
} as const;
const queue = engine.state.icmpObservationsByNodeId;
queue.set(CLIENT, [...(queue.get(CLIENT) ?? []), { nodeId: CLIENT, packet, message: reply }]);
engine.handle({ type: 'advance', toMilliseconds: 6_000 });
const lines = engine.terminal(CLIENT)!.log.all.flatMap((line) => (line.kind === 'text' ? [[line.timeMilliseconds, line.text] as const] : []));
expect(lines.map(([, text]) => text.replace(/time=\d+ms/, 'time=…ms'))).toEqual([
'PING 192.168.0.31 (192.168.0.31)',
'\nFrom 192.168.0.31: icmp_seq=2 ttl=64 time=…ms',
'\nFrom 192.168.0.31: icmp_seq=3 ttl=64 time=…ms',
'\nFrom 192.168.0.31: icmp_seq=4 ttl=64 time=…ms',
'\n--- 192.168.0.31 Paketstatistik ---\n4 Paket(e) gesendet, 3 Paket(e) empfangen, 25% Paketverlust\n',
]);
expect(lines[1]![0]).toBeGreaterThanOrEqual(1_000);
});
it('goes on with the next identifier, from one drawn at random', () => {
const engine = started();
engine.terminal(CLIENT)!.enter('ping 192.168.0.31');
engine.terminal(CLIENT)!.enter('ping 192.168.0.30');
engine.handle({ type: 'advance', toMilliseconds: 10_000 });
const identifiers = echoIdentifiers(engine);
expect(identifiers).toHaveLength(8);
expect(identifiers[0]! % 23).toBe(0);
expect(identifiers[0]!).toBeLessThanOrEqual(42 * 23);
expect(identifiers).toEqual([...Array(4).fill(identifiers[0]), ...Array(4).fill(identifiers[0]! + 1)]);
});
/** An ICMP message put first in the client's queue, as if it had come (an answer to another echo). */
const queueEchoReply = (engine: NetworkRuntimeEngine, sequenceNumber: number) => {
const reply = { kind: 'echoReply', identifier: 1, sequenceNumber, data: new Uint8Array() } as const;
const packet = {
identity: 0,
senderIPAddress: '192.168.0.31',
receiverIPAddress: '192.168.0.20',
timeToLive: 64,
protocol: 'icmp',
payload: { kind: 'icmp', message: reply },
} as const;
const queue = engine.state.icmpObservationsByNodeId;
queue.set(CLIENT, [...(queue.get(CLIENT) ?? []), { nodeId: CLIENT, packet, message: reply }]);
};
it('takes an echo reply with another number for a timeout, and sends the next echo at once', () => {
const engine = started();
engine.terminal(CLIENT)!.enter('ping 192.168.0.31');
queueEchoReply(engine, 9);
engine.handle({ type: 'advance', toMilliseconds: 6_000 });
const lines = engine.terminal(CLIENT)!.log.all.flatMap((line) => (line.kind === 'text' ? [[line.timeMilliseconds, line.text] as const] : []));
expect(lines[1]![1]).toBe('\nFrom 192.168.0.31 (192.168.0.31): icmp_seq=1 -- Timeout!');
// The next echo goes at once, and the late answer to the first comes first: a timeout too.
expect(lines[2]![1]).toBe('\nFrom 192.168.0.31 (192.168.0.31): icmp_seq=2 -- Timeout!');
expect(lines[2]![0] - lines[1]![0]).toBeLessThan(500);
});
it('takes the first ICMP message that came, not the last', () => {
const engine = started();
engine.terminal(CLIENT)!.enter('ping 192.168.0.99');
// First the answer to another echo, then (when ARP gives up) the host unreachable the client gives itself.
queueEchoReply(engine, 1);
engine.handle({ type: 'advance', toMilliseconds: 30_000 });
expect(texts(engine).slice(1)).toEqual([
'\nFrom 192.168.0.99 (192.168.0.99): icmp_seq=2 -- Timeout!',
'\nFrom 192.168.0.99 (192.168.0.99): icmp_seq=3 -- Timeout!',
'\nFrom 192.168.0.99 (192.168.0.99): icmp_seq=4 -- Timeout!',
'\n--- 192.168.0.99 Paketstatistik ---\n4 Paket(e) gesendet, 0 Paket(e) empfangen, 100% Paketverlust\n',
]);
});
it('rounds the loss (Math.round)', () => {
const engine = started();
engine.terminal(CLIENT)!.enter('ping 192.168.0.31');
queueEchoReply(engine, 1);
engine.handle({ type: 'advance', toMilliseconds: 1_000 });
queueEchoReply(engine, 2);
engine.handle({ type: 'advance', toMilliseconds: 2_500 });
engine.terminal(CLIENT)!.interrupt();
engine.handle({ type: 'advance', toMilliseconds: 6_000 });
expect(texts(engine).at(-1)).toBe('\n--- 192.168.0.31 Paketstatistik ---\n3 Paket(e) gesendet, 1 Paket(e) empfangen, 67% Paketverlust\n');
});
it("refuses the broadcast of the route's network, whatever its mask", () => {
const wide = { ...scenario, devices: { ...scenario.devices, [CLIENT]: { ...scenario.devices![CLIENT]!, mask: '255.255.0.0' } } } as Scenario;
const engine = watchedEngine(wide, withSnapshot(snapshotOf(wide), { language: 'de', terminalNodeIds: new Set([CLIENT]) })).engine;
engine.terminal(CLIENT)!.enter('ping 192.168.255.255');
expect(texts(engine)).toEqual(['Ping zu Broadcast-Adressen wird nicht unterstützt.']);
});
it('draws the first identifier at random', () => {
const drawn = new Set(Array.from({ length: 10 }, (_, seed) => new NetworkRuntimeEngine(seed).drawIcmpIdentifier()));
expect(drawn.size).toBeGreaterThan(1);
});
it('routes by the first of rows with the same mask (determineRouteFromStaticRoutingTable)', () => {
const snapshot = withSnapshot(snapshotOf(scenario), {
manualRoutesByNodeId: new Map([[CLIENT, [{ destinationNetwork: '192.168.0.0', subnetMask: '255.255.255.0', gateway: '192.168.0.99', interfaceIPAddress: '192.168.0.20' }]]]),
});
const engine = watchedEngine(scenario, snapshot).engine;
const rows = engine.forwardingTable(CLIENT).filter((row) => row.destinationNetwork === '192.168.0.0' && row.subnetMask === '255.255.255.0');
expect(rows).toHaveLength(2);
expect(engine.routeRow(CLIENT, '192.168.0.31')).toEqual(rows[0]);
});
});
+68
View File
@@ -793,3 +793,71 @@ Gemessen (01.10.2026):
Pfad an den Arbeitsordner, `rm /` löscht den Arbeitsordner und meldet Pfad an den Arbeitsordner, `rm /` löscht den Arbeitsordner und meldet
Erfolg; `mv` auf die Datei selbst (auch in anderer Schreibweise) löscht Erfolg; `mv` auf die Datei selbst (auch in anderer Schreibweise) löscht
sie. sie.
## Befehlszeile, Netzbefehle (F3.4b)
`TerminalRun` tippt Zeilen in Javas `Terminal` auf den Rechnern von
`terminal-net` (LAN 192.168.0.0/24 mit Client .20, DNS-Server .10 ohne
Gateway, .30, .31, Router .1/10.0.0.1, dahinter 10.0.0.2 mit einem
DNS-Server, den es nicht gibt), zu festen Zeiten und so, wie das Fenster sie
übergibt (`terminalEingabeAuswerten`); `^C` ist Strg+C (`setInterrupt`).
Geschrieben werden je Karte die Frames samt Javas Tabellen, je Rechner was
die Befehlszeile sagt (Texte und Laufmarken) mit der Zeit, und die getippten
Zeilen mit ihren Zeiten, die der Test des Kerns ebenso tippt. Die JVM läuft
deutsch.
Gemessen (02.10.2026):
* `ping`: vier Echos, je eines pro Sekunde ab dem Senden des vorigen; die
Zeit zählt ab vor dem ARP (473 ms im LAN mit ARP, dann 223 ms; über den
Router 673 und 334 ms, TTL 63). Gewartet wird nach dem Senden eine RTT
(hier 2750 ms) auf die *erste* ICMP-Nachricht, gleich welche; alles außer
der Antwort mit der Sequenznummer ist „Timeout!“, und das nächste Echo geht
sofort. Ohne ARP-Antwort legt sich der Rechner selbst „Host Unreachable“
hin (je Echo zwei ARP-Anfragen, 5,5 s); hinter dem Router fragt dieser
seine ARP-Anfragen nacheinander ab, so beantwortet das dritte Echo die
Meldung zum ersten. Ohne Route am Router kommt „Network Unreachable“
sofort, viermal in 900 ms. Strg+C wirkt vor dem nächsten Echo, die
Statistik zählt die gesendeten; ein danach getippter bekannter Befehl
hebt es auf (`setInterrupt(false)` beim Übergeben), ein unbekannter nicht.
Einen unbekannten Befehl meldet das Fenster sofort, auch während ein
anderer läuft.
* Broadcast: ohne `-b` „nicht unterstützt“ (auch 255.255.255.255 über die
Standardroute, Maske 0.0.0.0); mit `-b` geht das Echo mit TTL 1 an
FF:FF:FF:FF:FF:FF, alle im Netz antworten (auch der Router), jeder fragt
dazu per ARP nach dem Absender. Welche Antwort zuerst kommt, ist ein
Wettlauf der Threads. Ein gerichteter Broadcast aus einem anderen Netz
(10.0.0.2 an 192.168.0.255) wird am Router verworfen.
* Eigene Adresse: Javas Route geht über 127.0.0.1, die Antwort kommt „From
127.0.0.1“; ebenso für `localhost` und 127.0.0.1, ohne Frame.
* Ferne Adresse eines Routers: die Antwort kommt von der nahen Karte
(„From 192.168.0.1“ für `ping 10.0.0.1`).
* Fehler Javas: in der Statistik und bei „Timeout!“ steht der erste Parameter
statt des Hosts (`--- -b Paketstatistik`); jedes erste Wort gilt als `-b`
(`ping -x 192.168.0.31` pingt). „Network Unreachable“ vom Router bettet ein
halb gebautes Paket ein (`sender=null`, TTL 63).
* `host`: A, dann MX, Namen mit Punkt am Ende; „nicht aufgelöst“ erst nach
beiden Anfragen (auch MX allein zählt); `localhost` ohne Anfrage. Ohne erreichbaren DNS-Server
nach fünf RTT „Zeitüberschreitung“, bei `ping` mit „ (DNS)“ und ohne
Laufmarken. Ohne eingetragenen DNS-Server sofort „nicht aufgelöst“.
* `nslookup`: Server und Adresse (127.0.0.1 für `localhost`), dann nur der
Antwortteil; ohne Treffer nur der Kopf. Ohne Parameter die Hilfe ohne
Laufmarken (`host` mit).
* `arp`: Javas Tabelle ist eine `HashMap` mit der Zeile 255.255.255.255,
die jedes Zurücksetzen (`-d`) neu anlegt; `-a` zeigt eine Zeile, `-d` mit
Adresse löscht sie und zeigt dann die Tabelle; unbekannte Optionen und
`-a`/`-d` mit etwas anderem als einer Adresse zeigen die Tabelle. `arp -d
255.255.255.255` entfernt auch die Broadcast-Zeile, bis `arp -d`.
* `netstat`: TCP, dann UDP, je Socket die lokale Adresse nach der Route zur
Gegenstelle; ein Server mit `0.0.0.0` und `-:-` in LISTEN, darunter seine
Verbindungen; eine getrennte Verbindung steht beim Client in `TIME_WAIT`,
beim Server ist sie ausgetragen. Javas UDP-Server behält für jede je beantwortete Adresse und
jeden Port einen Socket (`CONNECTED`), dessen Arbeiter für immer wartet.
Die Reihenfolge ist die einer `HashMap` bzw. `Hashtable`.
* Javas `waitForIcmpResponse` verpasst sein Aufwecken, wenn die Antwort
schon da ist, bevor es wartet (ein Wettlauf der Threads: Echo an sich
selbst, das „Host Unreachable“, das sich ein Rechner selbst hinlegt), und
wartet dann eine volle RTT. Eine Aufnahme damit (ein Ping an sich selbst
über 1 s, mehr als 6 s zwischen zwei Timeouts ins Leere) wird wiederholt;
ebenso eine unter Last (andere Sitzungen auf dem Rechner: die erste
ARP-Runde dauert dann knapp 300 statt 225 ms).
+13 -1
View File
@@ -21,7 +21,7 @@ if [ ! -f "$upstream/target/filius.jar" ]; then
fi fi
classpath="$upstream/target/filius.jar:$upstream/target/lib/*" classpath="$upstream/target/filius.jar:$upstream/target/lib/*"
mkdir -p "$here/.build" 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" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java" "$here/src/TerminalCases.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" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java" "$here/src/TerminalCases.java" "$here/src/TerminalRun.java"
# FILIUS writes its settings below $HOME; keep them out of the real one. # FILIUS writes its settings below $HOME; keep them out of the real one.
mkdir -p "$here/.build/home" mkdir -p "$here/.build/home"
# Removed first: a crashed run must not leave the previous result standing. # Removed first: a crashed run must not leave the previous result standing.
@@ -286,6 +286,18 @@ for language in de en; do
test -s "$output" test -s "$output"
done done
# The command line's network commands on terminal-net, typed at set times
# (TerminalRun; packages/core/test/terminalJava.test.ts), in the German locale.
for run in pingLan:8000 pingRouted:13000 pingNobody:49000 pingUnreachable:45000 pingBroadcast:27000 pingSelf:17000 \
pingErrors:14000 interrupt:16000 dns:15000 dnsTimeout:80000 arp:11000 netstat:10000; do
story="${run%%:*}"
output="$root/packages/core/test/scenarios/terminal-java-$story.java.json"
rm -f "$output"
HOME="$here/.build/home" xvfb-run -a java $german -cp "$classpath:$here/.build" TerminalRun \
"$root/packages/core/test/scenarios/terminal-net.scenario.json" "$story" "${run##*:}" "$output" 2>&1 | grep -v 'logback\|^$\|^\s*at ' || true
test -s "$output"
done
# FILIUS' own examples as Java FILIUS runs them, opened and started as FILIUS # 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). # 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 \ for run in internetwork_komplett:routing:12000 einfaches_rechnernetz_komplett:dhcp:20000 dns_server:dns:12000 \
+254
View File
@@ -0,0 +1,254 @@
// Runs a story of FILIUS' command line (Terminal) on terminal-net in Java
// FILIUS and records what really happens: every frame each card saw, Java's
// tables of them, and everything each host's command line told its window
// (each text and each Boolean "busy" mark), with the time.
//
// TerminalRun <scenario.json> <story> <milliseconds> <out.json>
//
// terminal-net: the client 192.168.0.20 (gateway .1, DNS server .10), the
// DNS server .10 (no gateway, no DNS server), .30, .31 (the echo server on
// 55555 in netstat, as on the DNS server), the router .1 / 10.0.0.1, and 10.0.0.2 behind it
// (DNS server 192.168.0.53, which is not there). The DNS server knows
// www.filius.de, filius.de (A and MX), mail.filius.de, multi.filius.de (two
// A), fern.filius.de (10.0.0.2), weg.filius.de (.99, no one) and post.filius.de
// (MX only). A line is typed as the window hands it over
// (Terminal.terminalEingabeAuswerten, the
// first word the command, the others its parameters, then three empty ones):
// the program's thread runs it, one after the other; "^C" is the window's
// Ctrl+C (setInterrupt). Each story's lines with their times are in the
// output (acts), for the core's test to type them at the same times.
// pingLan ping .31, arp typed while it runs
// pingRouted ping fern.filius.de (behind the router), arp on both ends, ping the router's far address
// pingNobody ping .99, which no one has, then by name (weg.filius.de)
// pingUnreachable ping 10.0.0.99 (no one behind the router), 172.16.0.1 (no route at the router)
// pingBroadcast ping .255, ping -b .255, arp, ping (-b) 255.255.255.255, also without a route;
// ping -b .255 from behind the router
// pingSelf ping its own address, localhost, 127.0.0.1
// pingErrors usage, unknown names, no route, no DNS server, a first word other than -b
// interrupt ping .31, Ctrl+C after 1.8 s; again, then arp typed at once; again, then an unknown command
// dns host and nslookup on known and unknown names, localhost, usage, a name with MX only
// dnsTimeout host, nslookup and ping by name where the DNS server is not there
// arp arp after a ping and a lookup, -a, -d, unknown options, no address, the broadcast row removed
// netstat netstat on the client connected to the echo server, on the echo server, on the DNS server;
// the connection closed, on both again
import filius.software.clientserver.ClientBaustein;
import filius.software.clientserver.ServerBaustein;
import filius.software.lokal.Terminal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
public class TerminalRun {
static final String CLIENT = "00000000-0000-0000-0000-000000000401";
static final String DNS = "00000000-0000-0000-0000-000000000402";
static final String WEB = "00000000-0000-0000-0000-000000000403";
static final String ECHO = "00000000-0000-0000-0000-000000000404";
static final String FAR = "00000000-0000-0000-0000-000000000406";
static final String TERMINAL_CLASS = "filius.software.lokal.Terminal";
static ScenarioNetwork network;
static final List<Object[]> events = new ArrayList<>();
static synchronized void event(String host, String kind, String text) {
events.add(new Object[] { System.currentTimeMillis() - network.start, host, kind, text });
}
static void sleepUntil(long milliseconds) throws InterruptedException {
long rest = network.start + milliseconds - System.currentTimeMillis();
if (rest > 0) Thread.sleep(rest);
}
/** The story's lines: time, host, line. */
static List<Object[]> acts(String story) {
List<Object[]> acts = new ArrayList<>();
switch (story) {
case "pingLan":
acts.add(new Object[] { 500L, CLIENT, "ping 192.168.0.31" });
acts.add(new Object[] { 1500L, CLIENT, "arp" });
break;
case "pingRouted":
acts.add(new Object[] { 500L, CLIENT, "ping fern.filius.de" });
acts.add(new Object[] { 6500L, CLIENT, "arp" });
acts.add(new Object[] { 6700L, FAR, "arp" });
acts.add(new Object[] { 7000L, CLIENT, "ping 10.0.0.1" });
break;
case "pingNobody":
acts.add(new Object[] { 500L, CLIENT, "ping 192.168.0.99" });
acts.add(new Object[] { 23500L, CLIENT, "ping weg.filius.de" });
break;
case "pingUnreachable":
acts.add(new Object[] { 500L, CLIENT, "ping 10.0.0.99" });
acts.add(new Object[] { 40000L, CLIENT, "ping 172.16.0.1" });
break;
case "pingBroadcast":
acts.add(new Object[] { 500L, CLIENT, "ping 192.168.0.255" });
acts.add(new Object[] { 1000L, CLIENT, "ping -b 192.168.0.255" });
acts.add(new Object[] { 7000L, CLIENT, "arp" });
acts.add(new Object[] { 7200L, CLIENT, "ping 255.255.255.255" });
acts.add(new Object[] { 7400L, CLIENT, "ping -b 255.255.255.255" });
acts.add(new Object[] { 12500L, DNS, "ping 255.255.255.255" });
acts.add(new Object[] { 13000L, FAR, "ping -b 192.168.0.255" });
break;
case "pingSelf":
acts.add(new Object[] { 500L, CLIENT, "ping 192.168.0.20" });
acts.add(new Object[] { 6000L, CLIENT, "ping localhost" });
acts.add(new Object[] { 11500L, CLIENT, "ping 127.0.0.1" });
break;
case "pingErrors":
acts.add(new Object[] { 500L, CLIENT, "ping" });
acts.add(new Object[] { 700L, CLIENT, "ping a b c" });
acts.add(new Object[] { 900L, CLIENT, "ping gibtsnicht.filius.de" });
acts.add(new Object[] { 2500L, CLIENT, "ping 1.2.3" });
acts.add(new Object[] { 4000L, DNS, "ping 10.0.0.2" });
acts.add(new Object[] { 4200L, DNS, "ping www.filius.de" });
acts.add(new Object[] { 8000L, CLIENT, "ping -x 192.168.0.31" });
break;
case "interrupt":
acts.add(new Object[] { 500L, CLIENT, "ping 192.168.0.31" });
acts.add(new Object[] { 2300L, CLIENT, "^C" });
acts.add(new Object[] { 5000L, CLIENT, "ping 192.168.0.31" });
acts.add(new Object[] { 5800L, CLIENT, "^C" });
acts.add(new Object[] { 5900L, CLIENT, "arp" });
acts.add(new Object[] { 10000L, CLIENT, "ping 192.168.0.31" });
acts.add(new Object[] { 10800L, CLIENT, "^C" });
acts.add(new Object[] { 10900L, CLIENT, "gibtsnicht" });
break;
case "dns":
acts.add(new Object[] { 500L, CLIENT, "host www.filius.de" });
acts.add(new Object[] { 2000L, CLIENT, "host filius.de" });
acts.add(new Object[] { 3500L, CLIENT, "host multi.filius.de" });
acts.add(new Object[] { 5000L, CLIENT, "host gibtsnicht.filius.de" });
acts.add(new Object[] { 6500L, CLIENT, "nslookup www.filius.de" });
acts.add(new Object[] { 8000L, CLIENT, "nslookup multi.filius.de" });
acts.add(new Object[] { 9500L, CLIENT, "nslookup localhost" });
acts.add(new Object[] { 9700L, CLIENT, "nslookup gibtsnicht.filius.de" });
acts.add(new Object[] { 11500L, CLIENT, "host" });
acts.add(new Object[] { 11600L, CLIENT, "nslookup" });
acts.add(new Object[] { 11700L, CLIENT, "host localhost" });
acts.add(new Object[] { 11900L, CLIENT, "nslookup filius.de" });
acts.add(new Object[] { 12100L, CLIENT, "host post.filius.de" });
break;
case "dnsTimeout":
acts.add(new Object[] { 500L, FAR, "host www.filius.de" });
acts.add(new Object[] { 30000L, FAR, "nslookup www.filius.de" });
acts.add(new Object[] { 60000L, FAR, "ping www.filius.de" });
break;
case "arp":
acts.add(new Object[] { 500L, CLIENT, "ping 192.168.0.31" });
acts.add(new Object[] { 5500L, CLIENT, "host www.filius.de" });
acts.add(new Object[] { 7000L, CLIENT, "arp" });
acts.add(new Object[] { 7200L, CLIENT, "arp -a 192.168.0.31" });
acts.add(new Object[] { 7400L, CLIENT, "arp -a 192.168.0.99" });
acts.add(new Object[] { 7600L, CLIENT, "arp -d 192.168.0.31" });
acts.add(new Object[] { 7800L, CLIENT, "arp" });
acts.add(new Object[] { 8000L, CLIENT, "arp -x 1.2.3.4" });
acts.add(new Object[] { 8200L, CLIENT, "arp foo" });
acts.add(new Object[] { 8250L, CLIENT, "arp -a foo" });
acts.add(new Object[] { 8300L, CLIENT, "arp -d foo" });
acts.add(new Object[] { 8400L, CLIENT, "arp -d" });
acts.add(new Object[] { 8600L, CLIENT, "arp" });
acts.add(new Object[] { 8800L, CLIENT, "arp -d 255.255.255.255" });
acts.add(new Object[] { 9000L, CLIENT, "arp" });
acts.add(new Object[] { 9200L, CLIENT, "arp -d" });
break;
case "netstat":
acts.add(new Object[] { 2500L, CLIENT, "netstat" });
acts.add(new Object[] { 2700L, ECHO, "netstat" });
acts.add(new Object[] { 2900L, CLIENT, "host www.filius.de" });
acts.add(new Object[] { 4500L, DNS, "netstat" });
acts.add(new Object[] { 4700L, WEB, "netstat" });
acts.add(new Object[] { 4900L, CLIENT, "netstat x" });
acts.add(new Object[] { 5000L, CLIENT, "disconnect" });
acts.add(new Object[] { 8000L, CLIENT, "netstat" });
acts.add(new Object[] { 8200L, ECHO, "netstat" });
break;
default:
throw new IllegalArgumentException("unknown story " + story);
}
return acts;
}
/** GUIApplicationTerminalWindow on Enter (or Ctrl+C): the line handed to the program. */
static void type(Terminal terminal, String host, String line) {
event(host, "call", line);
if (line.equals("^C")) {
terminal.setInterrupt(true);
return;
}
StringTokenizer tokens = new StringTokenizer(line, " ");
String command = tokens.nextToken();
String[] parameters = new String[3 + tokens.countTokens()];
Arrays.fill(parameters, "");
for (int index = 0; tokens.hasMoreTokens(); index++) parameters[index] = tokens.nextToken();
terminal.terminalEingabeAuswerten(command, parameters);
}
public static void main(String[] args) throws Exception {
network = ScenarioNetwork.read(args[0]);
String story = args[1];
long end = Long.parseLong(args[2]);
List<Object[]> acts = acts(story);
Map<String, Terminal> terminals = new LinkedHashMap<>();
for (String host : new String[] { CLIENT, DNS, WEB, ECHO, FAR }) {
network.system(host).installApp(TERMINAL_CLASS, false);
Terminal terminal = (Terminal) network.system(host).holeSoftware(TERMINAL_CLASS);
terminal.hinzuBeobachter((observable, data) -> {
if (data instanceof Boolean) event(host, "busy", data.toString());
else if (data != null) event(host, "text", data.toString());
});
terminals.put(host, terminal);
}
ClientBaustein client = null;
if (story.equals("netstat")) {
for (String host : new String[] { ECHO, DNS }) {
network.system(host).installApp("filius.software.clientserver.ServerBaustein", false);
ServerBaustein server = (ServerBaustein) network.system(host).holeSoftware("filius.software.clientserver.ServerBaustein");
server.setPort(55555);
server.setAktiv(true);
}
network.system(CLIENT).installApp("filius.software.clientserver.ClientBaustein", false);
client = (ClientBaustein) network.system(CLIENT).holeSoftware("filius.software.clientserver.ClientBaustein");
}
network.start();
if (client != null) {
sleepUntil(500);
event(CLIENT, "call", "connect 192.168.0.31:55555");
client.setZielIPAdresse("192.168.0.31");
client.setZielPort(55555);
client.verbinden();
}
for (Object[] act : acts) {
sleepUntil((Long) act[0]);
if (act[2].equals("disconnect")) {
// The echo client's window: its connection closed (ClientBaustein.trennen).
event(CLIENT, "call", "disconnect");
client.trennen();
continue;
}
type(terminals.get((String) act[1]), (String) act[1], (String) act[2]);
}
sleepUntil(end);
ScenarioNetwork.Json out = new ScenarioNetwork.Json();
out.open('{');
network.writeFrames(out);
out.key("acts").open('[');
for (Object[] act : acts) out.open('[').number((Long) act[0]).string((String) act[1]).string((String) act[2]).close(']');
out.close(']');
synchronized (TerminalRun.class) {
out.key("events").open('[');
for (Object[] each : events) out.open('[').number((Long) each[0]).string((String) each[1]).string((String) each[2]).string((String) each[3]).close(']');
out.close(']');
}
out.close('}');
network.stop();
ScenarioNetwork.write(args[3], out);
System.exit(0);
}
}