feat(core): Gnutella joining after Java, measured against Java

GnutellaRun lets Java's PeerToPeerAnwendung join peers on terminal-net.
The core's gnutella.ts does the same: a ping in Java's text format to
port 6346, a pong back, the ping sent on to the other peers and their
pongs back the way it came, at most the program's number of peers.

Departure from Java: a ping that comes a second time gets no pong and
its connection stays (Java's worker throws on the missing pong).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 12:29:54 +00:00
co-authored by Claude Opus 5.5
parent 9d42b90157
commit b4a95e6cac
18 changed files with 37937 additions and 3 deletions
+1
View File
@@ -27,6 +27,7 @@ export * from './runtime/apps/firewallSettings.js';
export * from './runtime/apps/emailServer.js';
export * from './runtime/apps/emailClient.js';
export * from './runtime/apps/filePrograms.js';
export * from './runtime/apps/gnutella.js';
export * from './runtime/apps/terminal.js';
export * from './runtime/apps/terminalTexts.js';
export * from './runtime/email.js';
+280
View File
@@ -0,0 +1,280 @@
// FILIUS' Gnutella program (filius.software.dateiaustausch: PeerToPeerAnwendung
// with its PeerToPeerServer on port 6346 and PeerToPeerClient): a host joins
// the network by a ping to one peer, which answers with a pong, takes the
// host as a peer and sends the ping on to its own peers, whose pongs come
// back the same way; each knows at most its number of peers. The messages
// are Java's text packets (PeerToPeerPaket.toString), each on a connection
// of its own that stays open, as Java's do.
//
// Departures from Java (gnutellaJava.test.ts names each):
// - a ping that comes a second time gets no pong, and its connection stays
// (Java means so, but its worker throws on the missing pong and closes the
// connection).
import type { Id } from '../../ids.js';
import type { GnutellaConfiguration } from '../config.js';
import type { NetworkRuntimeEngine } from '../engine.js';
import type { Steps } from '../threads.js';
import { ProgramLog } from './program.js';
import { TcpServerProgram } from './server.js';
/** PeerToPeerServer, PeerToPeerClientMitarbeiter: Gnutella's port. */
export const GNUTELLA_PORT = 6346;
/** Where the program keeps its files to share (PeerToPeerAnwendung.setSystemSoftware). */
export const GNUTELLA_FOLDER = '/peer2peer';
/** A packet's head (PeerToPeerPaket): its id, kind, hops, time to live and payload length in bits. */
interface GnutellaHead {
readonly guid: number;
readonly hops: number;
readonly ttl: number;
readonly payloadLength: number;
}
/** PingPaket: from the host with this address. */
export interface PingPacket extends GnutellaHead {
readonly kind: 'ping';
readonly ipAddress: string;
}
/** PongPaket: the answering host, its port, its files and their bytes (Java calls them KBs). */
export interface PongPacket extends GnutellaHead {
readonly kind: 'pong';
readonly ipAddress: string;
readonly port: number;
readonly files: number;
readonly bytes: number;
}
export type GnutellaPacket = PingPacket | PongPacket;
const PAYLOAD_KIND = { ping: '0x00', pong: '0x01' } as const;
/** PeerToPeerPaket.anzahlBenoetigterBits: the least exponent whose power of two is at least the number. */
function bitsFor(value: number): number {
let exponent = 0;
while (2 ** exponent < value) exponent += 1;
return exponent;
}
/** PongPaket.payloadLengthBerechnen. */
export function pongPayloadLength(ipAddress: string, port: number, files: number, bytes: number): number {
return ipAddress.length * 8 + bitsFor(port) + bitsFor(files) + bitsFor(bytes);
}
/** PeerToPeerPaket.toString and its subclasses': the fields joined by "//". */
export function gnutellaText(packet: GnutellaPacket): string {
const head = [packet.guid, PAYLOAD_KIND[packet.kind], packet.hops, packet.ttl, packet.payloadLength];
const body = packet.kind === 'ping' ? [packet.ipAddress] : [packet.ipAddress, packet.port, packet.files, packet.bytes];
return [...head, ...body].join('//');
}
/** A packet's fields, as Java's constructors read them: a StringTokenizer on "/", empty fields skipped. */
const fieldsOf = (text: string) => text.split('/').filter((field) => field !== '');
const numberAt = (fields: readonly string[], index: number) => (/^-?\d+$/.test(fields[index] ?? '') ? Number(fields[index]) : undefined);
/** PeerToPeerPaket(String): whether the text has a packet's head (its id, kind, hops, time to live, payload length). */
export function hasGnutellaHead(text: string): boolean {
const fields = fieldsOf(text);
return fields[1] !== undefined && [0, 2, 3, 4].every((index) => numberAt(fields, index) !== undefined);
}
/** A packet from its text; undefined for any other kind or a text that is no packet. */
export function gnutellaPacket(text: string): GnutellaPacket | undefined {
const fields = fieldsOf(text);
const number = (index: number) => numberAt(fields, index);
const [guid, hops, ttl, payloadLength] = [number(0), number(2), number(3), number(4)];
if (guid === undefined || hops === undefined || ttl === undefined || payloadLength === undefined) return undefined;
const head = { guid, hops, ttl, payloadLength };
if (fields[1] === PAYLOAD_KIND.ping && fields[5] !== undefined) return { kind: 'ping', ...head, ipAddress: fields[5] };
const [port, files, bytes] = [number(6), number(7), number(8)];
if (fields[1] === PAYLOAD_KIND.pong && fields[5] !== undefined && port !== undefined && files !== undefined && bytes !== undefined) {
return { kind: 'pong', ...head, ipAddress: fields[5], port, files, bytes };
}
return undefined;
}
/** What the program tells its window: its known peers, whenever they change. */
export type GnutellaLogEntry = { readonly kind: 'peers'; readonly peers: readonly string[] };
/** PeerToPeerServer: answers pings on 6346; a connection's worker remembers the id of its last packet. */
class GnutellaServer extends TcpServerProgram {
/** Each connection's last packet id (PeerToPeerServerMitarbeiter.guid), to send answers back on. */
private readonly lastGuidBySocket = new Map<Id, number>();
constructor(
engine: NetworkRuntimeEngine,
nodeId: Id,
private readonly peer: GnutellaPeer,
) {
super(engine, nodeId, { port: GNUTELLA_PORT, isActive: true });
}
/** Never asked: each connection runs its own dialogue (converse). */
protected answer(): string {
return '';
}
protected override *converse(socketId: Id, address: string): Steps {
for (;;) {
const received = yield* this.engine.receiveTcpSteps(socketId);
if (received.type !== 'message') {
this.lastGuidBySocket.delete(socketId);
return;
}
const text = new TextDecoder().decode(received.payload);
// PeerToPeerServerMitarbeiter.verarbeiteNachricht: a text without a packet's head throws, and the worker closes the
// connection; a packet of another kind is left.
if (!hasGnutellaHead(text)) {
this.lastGuidBySocket.delete(socketId);
return;
}
const packet = gnutellaPacket(text);
if (packet === undefined) continue;
this.lastGuidBySocket.set(socketId, packet.guid);
if (packet.kind === 'ping') yield* this.peer.answerPing(packet, address, (text) => this.engine.sendTcpSteps(socketId, new TextEncoder().encode(text)));
}
}
/** PeerToPeerServer.sendePaket: on every connection whose last packet had this id. */
sendBack(packet: GnutellaPacket): void {
for (const [socketId, guid] of this.lastGuidBySocket) {
if (guid !== packet.guid) continue;
const engine = this.engine;
engine.startThread(
(function* (): Steps {
yield* engine.sendTcpSteps(socketId, new TextEncoder().encode(gnutellaText(packet)));
})(),
);
}
}
}
export class GnutellaPeer {
readonly log: ProgramLog<GnutellaLogEntry>;
private peersValue: string[] = [];
private readonly maximumKnownPeers: number;
/** The ids of the program's own pings (eigeneAnfragen), of others' (fremdeAnfragen), and of those it sent on (schonmalVerschicktListe). */
private readonly ownRequests = new Set<number>();
private readonly foreignRequests = new Set<number>();
private readonly alreadySent = new Set<number>();
private readonly server: GnutellaServer;
constructor(
private readonly engine: NetworkRuntimeEngine,
private readonly nodeId: Id,
configuration: GnutellaConfiguration,
) {
this.log = new ProgramLog(engine);
this.maximumKnownPeers = configuration.maximumKnownPeers;
this.server = new GnutellaServer(engine, nodeId, this);
}
/** The known peers (holeBekanntePeerToPeerTeilnehmer), in the order they came. */
get peers(): readonly string[] {
return this.peersValue;
}
/** The node's first address (Betriebssystem.primaryIPAdresse). */
private get ownAddress(): string {
return this.engine.networkInterfaces(this.nodeId)[0]?.ipAddress ?? '';
}
/** beitretenNetzwerk: a ping to the peer at the address, not to the host itself. */
join(address: string): void {
if (address === this.ownAddress) return;
const ping: PingPacket = { kind: 'ping', guid: this.engine.drawGnutellaGuid(), hops: 0, ttl: 8, payloadLength: 0, ipAddress: this.ownAddress };
this.ownRequests.add(ping.guid);
this.engine.startThread(this.sendTo(address, ping));
}
/** resetNetwork: no known peers. */
reset(): void {
this.peersValue = [];
this.log.write({ kind: 'peers', peers: [] });
}
/** hinzuTeilnehmer: a new peer while there is room. */
private addPeer(address: string): void {
if (this.peersValue.includes(address) || this.peersValue.length >= this.maximumKnownPeers) return;
this.peersValue = [...this.peersValue, address];
this.log.write({ kind: 'peers', peers: this.peersValue });
}
/**
* PeerToPeerServerMitarbeiter.verarbeitePing: the sender's host taken as a
* peer, a pong unless the ping was here already, and the ping sent on to
* the other peers.
*/
*answerPing(ping: PingPacket, from: string, send: (text: string) => Steps<unknown>): Steps {
this.addPeer(ping.ipAddress);
// erstellePong: none for a ping sent on before.
if (!this.alreadySent.has(ping.guid)) {
const files = this.sharedFiles();
const bytes = files.reduce((sum, size) => sum + size, 0);
const address = this.ownAddress;
const pong: PongPacket = {
kind: 'pong',
guid: ping.guid,
hops: 0,
ttl: 8,
payloadLength: pongPayloadLength(address, GNUTELLA_PORT, files.length, bytes),
ipAddress: address,
port: GNUTELLA_PORT,
files: files.length,
bytes,
};
yield* send(gnutellaText(pong));
}
this.sendPingOn({ ...ping, hops: ping.hops + 1, ttl: ping.ttl - 1 }, from);
}
/** PeerToPeerAnwendung.sendePing: once per id, while it lives, to each peer but the sender and the ping's host. */
private sendPingOn(ping: PingPacket, from: string): void {
if (ping.ttl <= 0 || this.alreadySent.has(ping.guid)) return;
this.alreadySent.add(ping.guid);
this.foreignRequests.add(ping.guid);
for (const peer of this.peersValue) {
if (peer !== from && peer !== ping.ipAddress) this.engine.startThread(this.sendTo(peer, ping));
}
}
/** verarbeitePong: the program's own ping answered (a new peer), else the pong sent back the way its ping came. */
private takePong(pong: PongPacket): void {
if (this.ownRequests.has(pong.guid)) this.addPeer(pong.ipAddress);
else this.server.sendBack({ ...pong, ttl: pong.ttl - 1, hops: pong.hops + 1 });
}
/** The sizes of the files in /peer2peer (Datei.holeGroesse, a text in characters). */
private sharedFiles(): number[] {
const files = this.engine.state.topologySnapshot.fileSystemsByNodeId.get(this.nodeId);
if (files === undefined || !files.contains(GNUTELLA_FOLDER)) return [];
return files.entriesInCreationOrder(GNUTELLA_FOLDER).flatMap((entry) => {
const content = entry.content;
return content.kind === 'directory' ? [] : [content.kind === 'text' ? content.text.length : content.data.length];
});
}
/**
* PeerToPeerClientMitarbeiter.run: a connection to the peer's port 6346, the
* packet sent, then each answer taken until the connection ends.
*/
private *sendTo(address: string, packet: GnutellaPacket): Steps {
const engine = this.engine;
const socketId = engine.openTcpClientSocket(this.nodeId, address, GNUTELLA_PORT);
if (socketId === undefined || (yield* engine.connectTcpSteps(socketId)) !== 'connected') {
if (socketId !== undefined) engine.requestTcpClose(socketId);
return;
}
yield* engine.sendTcpSteps(socketId, new TextEncoder().encode(gnutellaText(packet)));
for (;;) {
const received = yield* engine.receiveTcpSteps(socketId);
if (received.type !== 'message') break;
const answer = gnutellaPacket(new TextDecoder().decode(received.payload));
if (answer?.kind === 'pong') this.takePong(answer);
}
engine.requestTcpClose(socketId);
}
}
+28
View File
@@ -123,6 +123,7 @@ import { firewallLogPage, forwardingTablePage, ripTablePage, routerIndexPage } f
import { applyFirewallSettings, firewallSettingsPage } from './apps/firewallSettings.js';
import { parentPath, VirtualFileSystem } from '../model/virtualFileSystem.js';
import { EMAIL_SERVER_ACCOUNTS_FILE, serverAccountsFileText } from './emailAccountsFile.js';
import { GNUTELLA_FOLDER, GnutellaPeer } from './apps/gnutella.js';
import {
PRODUCTION_TRACE_RETENTION,
type HeaderField,
@@ -882,6 +883,8 @@ export class NetworkRuntimeEngine {
private dnsIdRandom: JavaCompatibleRandom;
/** Draws the command line's first ICMP identifier (Terminal.setIdentifier: new Random()). */
private icmpIdentifierRandom: JavaCompatibleRandom;
/** Draws Gnutella packet ids (PeerToPeerPaket.guidErstellen: new Random()). */
private gnutellaGuidRandom: 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;
@@ -930,6 +933,7 @@ export class NetworkRuntimeEngine {
private readonly emailServersByNodeId = new Map<Id, EmailServer>();
private readonly emailClientsByNodeId = new Map<Id, EmailClient>();
private readonly terminalsByNodeId = new Map<Id, Terminal>();
private readonly gnutellaPeersByNodeId = new Map<Id, GnutellaPeer>();
/** The time of day a program writes into a mail (Java's new Date()). */
private readonly wallClock: () => number;
/** Each node's firewall log; it outlives a stop and a new start, as Java's log page does. */
@@ -981,6 +985,7 @@ export class NetworkRuntimeEngine {
this.linkLocalRandom = new JavaCompatibleRandom(normalizedSeed ^ 0x4c4c_4144n);
this.dnsIdRandom = new JavaCompatibleRandom(normalizedSeed ^ 0x444e_5349n);
this.icmpIdentifierRandom = new JavaCompatibleRandom(normalizedSeed ^ 0x4943_4d50n);
this.gnutellaGuidRandom = new JavaCompatibleRandom(normalizedSeed ^ 0x474e_5554n);
this.runtimeSpeed = new RuntimeSpeed();
this.packetTraceRetentionPolicy = packetTraceRetentionPolicy;
this.stateValue.simulationSpeedPercent = this.runtimeSpeed.percent;
@@ -1035,6 +1040,7 @@ export class NetworkRuntimeEngine {
this.linkLocalRandom = new JavaCompatibleRandom(seed ^ 0x4c4c_4144n);
this.dnsIdRandom = new JavaCompatibleRandom(seed ^ 0x444e_5349n);
this.icmpIdentifierRandom = new JavaCompatibleRandom(seed ^ 0x4943_4d50n);
this.gnutellaGuidRandom = new JavaCompatibleRandom(seed ^ 0x474e_5554n);
this.arpBroadcastRowRemoved.clear();
this.isProcessingScheduledEvents = false;
this.applicationThreads.clear();
@@ -1065,6 +1071,7 @@ export class NetworkRuntimeEngine {
this.emailServersByNodeId.clear();
this.emailClientsByNodeId.clear();
this.terminalsByNodeId.clear();
this.gnutellaPeersByNodeId.clear();
this.arpLookups = [];
this.protocolThreads.clear();
this.dnsQueriesBySocketId.clear();
@@ -1161,6 +1168,19 @@ export class NetworkRuntimeEngine {
return this.terminalsByNodeId.get(nodeId);
}
/** The node's Gnutella program while the simulation runs, if it has one installed. */
gnutella(nodeId: Id): GnutellaPeer | undefined {
return this.gnutellaPeersByNodeId.get(nodeId);
}
/** A Gnutella packet's id (PeerToPeerPaket.guidErstellen: Random.nextInt until it is not negative). */
drawGnutellaGuid(): number {
for (;;) {
const value = this.gnutellaGuidRandom.next(32) | 0;
if (value >= 0) return value;
}
}
/** The node's e-mail program while the simulation runs, if it has one installed. */
emailClient(nodeId: Id): EmailClient | undefined {
return this.emailClientsByNodeId.get(nodeId);
@@ -1224,6 +1244,7 @@ export class NetworkRuntimeEngine {
this.emailServersByNodeId.clear();
this.emailClientsByNodeId.clear();
this.terminalsByNodeId.clear();
this.gnutellaPeersByNodeId.clear();
for (const node of snapshot.nodes) {
const echoServer = snapshot.echoServersByNodeId.get(node.id);
if (echoServer !== undefined) this.echoServersByNodeId.set(node.id, new EchoServer(this, node.id, echoServer));
@@ -1251,6 +1272,13 @@ export class NetworkRuntimeEngine {
this.writeEmailServerAccountsFile(nodeId, emailServer.domain, emailServer.accounts);
}
if (snapshot.terminalNodeIds.has(node.id)) this.terminalsByNodeId.set(node.id, new Terminal(this, node.id, this.nodeFiles(node.id)));
const gnutella = snapshot.gnutellaByNodeId.get(node.id);
if (gnutella !== undefined) {
// PeerToPeerAnwendung.setSystemSoftware: the folder of the files to share.
const files = this.nodeFiles(node.id);
if (!files.contains(GNUTELLA_FOLDER)) files.createDirectory(GNUTELLA_FOLDER);
this.gnutellaPeersByNodeId.set(node.id, new GnutellaPeer(this, node.id, gnutella));
}
const emailClient = snapshot.emailClientsByNodeId.get(node.id);
if (emailClient !== undefined) {
const nodeId = node.id;
+5
View File
@@ -23,6 +23,7 @@ import type {
WebServerConfiguration,
EmailServerAccount,
EmailClientConfiguration,
GnutellaConfiguration,
} from './config.js';
import type { VirtualFileSystem } from '../model/virtualFileSystem.js';
import type { PageLanguage } from './apps/routerPages.js';
@@ -107,6 +108,8 @@ export interface TopologySnapshot {
readonly terminalNodeIds: ReadonlySet<Id>;
/** The installed e-mail programs, by node; a run's sent and fetched mails change their folders here. */
readonly emailClientsByNodeId: ReadonlyMap<Id, EmailClientConfiguration>;
/** The installed Gnutella programs (PeerToPeerAnwendung), by node: how many peers each keeps. */
readonly gnutellaByNodeId: ReadonlyMap<Id, GnutellaConfiguration>;
/** Each node's files, which its programs read (a web server its pages). */
readonly fileSystemsByNodeId: ReadonlyMap<Id, VirtualFileSystem>;
/** The language of the pages the devices write themselves (a router's), as Java's in the locale it runs in. */
@@ -133,6 +136,7 @@ export interface TopologySnapshotInput {
webBrowserNodeIds?: ReadonlySet<Id>;
emailServersByNodeId?: ReadonlyMap<Id, EmailServerSnapshot>;
emailClientsByNodeId?: ReadonlyMap<Id, EmailClientConfiguration>;
gnutellaByNodeId?: ReadonlyMap<Id, GnutellaConfiguration>;
terminalNodeIds?: ReadonlySet<Id>;
fileSystemsByNodeId?: ReadonlyMap<Id, VirtualFileSystem>;
language?: PageLanguage;
@@ -159,6 +163,7 @@ export function topologySnapshot(input: TopologySnapshotInput): TopologySnapshot
webBrowserNodeIds: input.webBrowserNodeIds ?? new Set(),
emailServersByNodeId: input.emailServersByNodeId ?? new Map(),
emailClientsByNodeId: input.emailClientsByNodeId ?? new Map(),
gnutellaByNodeId: input.gnutellaByNodeId ?? new Map(),
terminalNodeIds: input.terminalNodeIds ?? new Set(),
fileSystemsByNodeId: input.fileSystemsByNodeId ?? new Map(),
language: input.language ?? 'de',
+147
View File
@@ -0,0 +1,147 @@
// The Gnutella program (apps/gnutella.ts) beyond what Java's runs show
// (gnutellaJava.test.ts): its packets as Java writes and reads them, a ping
// that comes twice, the files it offers.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { GNUTELLA_FOLDER, gnutellaPacket, gnutellaText, pongPayloadLength, VirtualFileSystem, withSnapshot, type NetworkRuntimeEngine } from '../src/index.js';
import { watchedEngine } from './javaRun.js';
import { snapshotOf, type Scenario } from './scenario.js';
const scenario = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'scenarios', 'terminal-net.scenario.json'), 'utf8')) as Scenario;
const CLIENT = '00000000-0000-0000-0000-000000000401';
const ECHO = '00000000-0000-0000-0000-000000000404';
describe('Gnutella packets as Java writes and reads them', () => {
it('writes a ping and a pong with their fields joined by "//"', () => {
expect(gnutellaText({ kind: 'ping', guid: 42, hops: 1, ttl: 7, payloadLength: 0, ipAddress: '192.168.0.30' })).toBe('42//0x00//1//7//0//192.168.0.30');
expect(gnutellaText({ kind: 'pong', guid: 42, hops: 0, ttl: 8, payloadLength: 77, ipAddress: '10.0.0.2', port: 6346, files: 2, bytes: 15 })).toBe(
'42//0x01//0//8//77//10.0.0.2//6346//2//15',
);
});
it('reads them back, skipping empty fields as a StringTokenizer on "/" does', () => {
expect(gnutellaPacket('42///0x00//1//7//0//192.168.0.30/')).toEqual({ kind: 'ping', guid: 42, hops: 1, ttl: 7, payloadLength: 0, ipAddress: '192.168.0.30' });
expect(gnutellaPacket('42//0x01//0//8//77//10.0.0.2//6346//2//15')).toEqual({
kind: 'pong',
guid: 42,
hops: 0,
ttl: 8,
payloadLength: 77,
ipAddress: '10.0.0.2',
port: 6346,
files: 2,
bytes: 15,
});
});
it('reads no packet from a text that is none, or of another kind', () => {
expect(['', 'GET /x HTTP/1.1', '42//0x00//1//7//0', 'x//0x00//1//7//0//a', '42//0x80//1//7//0//1//txt', '42//0x01//0//8//77//10.0.0.2//6346//2'].map(gnutellaPacket)).toEqual(
Array(6).fill(undefined),
);
});
it("counts a pong's payload in Java's bits: the address's characters, then the least power of two for each number", () => {
expect(pongPayloadLength('10.0.0.2', 6346, 0, 0)).toBe(77);
expect(pongPayloadLength('192.168.0.31', 6346, 0, 0)).toBe(109);
expect(pongPayloadLength('1.2.3.4', 6346, 1, 5)).toBe(7 * 8 + 13 + 0 + 3);
});
});
describe('the Gnutella program in the engine', () => {
const started = (files?: VirtualFileSystem) =>
watchedEngine(
scenario,
withSnapshot(snapshotOf(scenario), {
gnutellaByNodeId: new Map([[ECHO, { maximumKnownPeers: 3 }]]),
...(files === undefined ? {} : { fileSystemsByNodeId: new Map([[ECHO, files]]) }),
}),
).engine;
/** A connection of the client's to .31's port 6346, a ping sent on it; what came back. */
const ping = (engine: NetworkRuntimeEngine, text: string) => {
const socketId = engine.openTcpClientSocket(CLIENT, '192.168.0.31', 6346)!;
expect(engine.connectTcp(socketId)).toBe(true);
expect(engine.sendTcp(socketId, new TextEncoder().encode(text))).toBe(true);
engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + 3_000 });
const answers: string[] = [];
for (let answer = engine.receiveTcp(socketId); answer !== undefined; answer = engine.receiveTcp(socketId)) answers.push(new TextDecoder().decode(answer));
return { socketId, answers };
};
it('makes its folder of files to share', () => {
const engine = started();
expect(engine.state.topologySnapshot.fileSystemsByNodeId.get(ECHO)!.entry(GNUTELLA_FOLDER).content.kind).toBe('directory');
});
it('answers a ping once, and keeps the connection of the second (Java closes it)', () => {
const engine = started();
const first = ping(engine, '7//0x00//0//8//0//192.168.0.20');
expect(first.answers).toEqual(['7//0x01//0//8//109//192.168.0.31//6346//0//0']);
const second = ping(engine, '7//0x00//0//8//0//192.168.0.20');
expect(second.answers).toEqual([]);
expect(engine.state.socketsById.get(second.socketId)?.tcpState).toBe('ESTABLISHED');
expect(engine.gnutella(ECHO)!.peers).toEqual(['192.168.0.20']);
});
it("counts the files of its folder and their size in a pong (a text's in characters)", () => {
const files = new VirtualFileSystem();
files.createDirectory(GNUTELLA_FOLDER);
files.writeTextFile(`${GNUTELLA_FOLDER}/lied.txt`, 'Hallo Welt');
files.writeBinaryFile(`${GNUTELLA_FOLDER}/daten.bin`, new Uint8Array(5));
files.createDirectory(`${GNUTELLA_FOLDER}/ordner`);
files.writeTextFile('/anderswo.txt', 'nicht geteilt');
const engine = started(files);
expect(ping(engine, '9//0x00//0//8//0//192.168.0.20').answers).toEqual([`9//0x01//0//8//${pongPayloadLength('192.168.0.31', 6346, 2, 15)}//192.168.0.31//6346//2//15`]);
});
it('joins no one at its own address, and forgets its peers when reset', () => {
const engine = started();
engine.gnutella(ECHO)!.join('192.168.0.31');
engine.handle({ type: 'advance', toMilliseconds: 3_000 });
expect(engine.capture.frames(scenario.nodes.find((node) => node.id === ECHO)!.ports[0]!.id)).toEqual([]);
ping(engine, '5//0x00//0//8//0//192.168.0.20');
engine.gnutella(ECHO)!.reset();
expect(engine.gnutella(ECHO)!.peers).toEqual([]);
expect(engine.gnutella(ECHO)!.log.all.map((line) => line.peers)).toEqual([['192.168.0.20'], []]);
});
it('closes a connection whose text has no packet head, and leaves a packet of another kind (Java: its worker throws on the one)', () => {
const engine = started();
const garbage = ping(engine, 'hallo');
expect(garbage.answers).toEqual([]);
expect(engine.state.socketsById.get(garbage.socketId)?.tcpState).not.toBe('ESTABLISHED');
const other = ping(engine, '3//0x42//0//8//0');
expect(other.answers).toEqual([]);
expect(engine.state.socketsById.get(other.socketId)?.tcpState).toBe('ESTABLISHED');
});
it('sends a ping on once, and not when its time to live runs out', () => {
const WEB = '00000000-0000-0000-0000-000000000403';
const engine = watchedEngine(
scenario,
withSnapshot(snapshotOf(scenario), {
gnutellaByNodeId: new Map([
[ECHO, { maximumKnownPeers: 3 }],
[WEB, { maximumKnownPeers: 3 }],
]),
}),
).engine;
engine.gnutella(WEB)!.join('192.168.0.31');
engine.handle({ type: 'advance', toMilliseconds: 3_000 });
const webCard = scenario.nodes.find((node) => node.id === WEB)!.ports[0]!.id;
const forwarded = () =>
engine.capture.frames(webCard).filter(({ frame }) => {
const packet = frame.payload.kind === 'ipv4' ? frame.payload.packet : undefined;
return packet?.payload.kind === 'tcp' && packet.senderIPAddress === '192.168.0.31' && new TextDecoder().decode(packet.payload.segment.payload).includes('//0x00//');
}).length;
expect(forwarded()).toBe(0);
ping(engine, '11//0x00//7//1//0//192.168.0.20');
expect(forwarded()).toBe(0);
ping(engine, '12//0x00//0//8//0//192.168.0.20');
ping(engine, '12//0x00//0//8//0//192.168.0.20');
expect(forwarded()).toBe(1);
});
});
+217
View File
@@ -0,0 +1,217 @@
// The Gnutella program as Java FILIUS runs it. tools/java-oracle's
// GnutellaRun let FILIUS' Gnutella program (PeerToPeerAnwendung) join peers
// on terminal-net and wrote every frame each card saw, Java's tables of
// them, each program's known peers whenever it told its window, and at the
// end each program's peers (gnutella-java-<story>.java.json). The engine
// plays the same calls at the same times (apps/gnutella.ts): each card's
// frames in the same order, with the same content and rows, at about the
// same time; the same peers, in the same order, 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 { withSnapshot, type NetworkRuntimeEngine } from '../src/index.js';
import {
engineText,
expectSameCaptures,
GAP_TOLERANCE_MILLISECONDS,
javaText,
scenarioCards,
watchedEngine,
withClientPortText,
type JavaFrame,
type JavaTables,
} from './javaRun.js';
import { POLL_MILLISECONDS } from './programLines.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 WEB = '00000000-0000-0000-0000-000000000403';
const ECHO = '00000000-0000-0000-0000-000000000404';
const FAR = '00000000-0000-0000-0000-000000000406';
const PEERS = [CLIENT, WEB, ECHO, FAR];
interface JavaRun extends JavaTables {
frames: Record<string, JavaFrame[]>;
acts: [number, string, string][];
/** time, host, kind (call or peers), text */
events: [number, string, string, string][];
programs: Record<string, { peers: string[]; results: string[] }>;
}
/** The stories, and how many connections each makes one after the other (Java looks every 100 ms whether one is made). */
const STORIES: Record<string, { readonly looks: number }> = {
pair: { looks: 1 },
chain: { looks: 2 },
twoPaths: { looks: 3 },
full: { looks: 3 },
reset: { looks: 1 },
errors: { looks: 1 },
};
/** The ids of Java's own pings, in the order they were made: the engine draws its own, these take their place. */
function javaPingIds(java: JavaRun): number[] {
const pings = Object.values(java.frames)
.flat()
.flatMap((frame) => {
const match = /^(\d+)\/\/0x00\/\/0\/\//.exec(frame.payload ?? '');
return match === null ? [] : [[frame.time, Number(match[1])] as const];
})
.sort((lhs, rhs) => lhs[0] - rhs[0]);
return [...new Set(pings.map(([, guid]) => guid))];
}
/** Each program's known peers as it told them, each change once (Java tells its window also when nothing changed, and an empty list at its start). */
function javaPeers(java: JavaRun): Record<string, [number, string][]> {
const peers: Record<string, [number, string][]> = Object.fromEntries(PEERS.map((host) => [host, []]));
for (const [time, host, kind, text] of java.events) {
if (kind !== 'peers') continue;
const list = peers[host]!;
if ((list.at(-1)?.[1] ?? '') === text) continue;
list.push([time, text]);
}
return peers;
}
function enginePeers(engine: NetworkRuntimeEngine): Record<string, [number, string][]> {
return Object.fromEntries(PEERS.map((host) => [host, engine.gnutella(host)!.log.all.map((line): [number, string] => [line.timeMilliseconds, line.peers.join(',')])]));
}
/**
* Where Java errs, compared only up to a time (Java's): twoPaths, from the
* first ping that comes a second time — Java's worker throws on the
* missing pong and closes the connection (gnutella.ts);
* errors, before the engine's second SYN to the closed port.
*/
/**
* twoPaths, full: many connections at once, and at a card the frames of two
* of them, or the two directions of one, may come in either order a few ten
* milliseconds apart (Java's threads over several hops). Each card's frames
* are compared connection by connection and direction by direction — the
* frames between the two hosts on the server's port, in the order the
* connections began — with their data; ARP on its own.
*/
const BY_CONNECTION = new Set(['twoPaths', 'full']);
interface FlowFrame {
readonly time: number;
/** The frame's text with its real ports, which tell the connection. */
readonly raw: string;
/** The text compared: a client's port as "client", a packet's id as "x". */
readonly text: string;
}
/** A card's frames by connection: its two hosts and the server's port (a client's port is drawn), each connection's frames in order. */
function connections(frames: readonly FlowFrame[]): Map<string, FlowFrame[][]> {
const byKey = new Map<string, Map<string, FlowFrame[]>>();
for (const frame of frames) {
const tcp = / TCP (\S+):(\d+) > (\S+):(\d+) /.exec(frame.raw);
// A connection by its client's end, and each of its directions on its own (TCP orders a direction, not the two).
const [key, connection] =
tcp === null ? ['arp', 'arp'] : tcp[2] === '6346' ? [`${tcp[3]}>${tcp[1]}:6346 answers`, `${tcp[3]}:${tcp[4]}`] : [`${tcp[1]}>${tcp[3]}:6346`, `${tcp[1]}:${tcp[2]}`];
const flows = byKey.get(key) ?? new Map<string, FlowFrame[]>();
byKey.set(key, flows);
flows.set(connection, [...(flows.get(connection) ?? []), frame]);
}
return new Map([...byKey].map(([key, flows]) => [key, [...flows.values()]]));
}
/**
* When a ping first comes to a host a second time (the same id to the same
* address, on another connection; a frame shows on both its cards); before
* it, by more than a timing difference.
*/
function secondPing(java: JavaRun): number {
const connectionsByPing = new Map<string, Set<string>>();
for (const frame of Object.values(java.frames)
.flat()
.sort((lhs, rhs) => lhs.time - rhs.time)) {
const match = /^(\d+)\/\/0x00\//.exec(frame.payload ?? '');
if (match === null) continue;
const ping = `${match[1]}>${frame.destinationIp}`;
const connections = connectionsByPing.get(ping) ?? new Set<string>();
connections.add(`${frame.sourceIp}:${frame.sourcePort}`);
connectionsByPing.set(ping, connections);
if (connections.size > 1) return frame.time - 150;
}
return Number.POSITIVE_INFINITY;
}
const UNTIL: Record<string, (java: JavaRun) => number> = {
// The SYN to a closed port goes once in Java, three times in the engine (tcpJava.test.ts, closedPort).
errors: (java) => java.frames[CLIENT]!.find((frame) => frame.tcpFlags === 'S')!.time + 1_000,
twoPaths: secondPing,
};
describe('the Gnutella program as Java FILIUS runs it', () => {
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'terminal-net.scenario.json'), 'utf8')) as Scenario;
for (const [name, story] of Object.entries(STORIES)) {
it(name, () => {
const java = JSON.parse(readFileSync(join(scenarioDirectory, `gnutella-java-${name}.java.json`), 'utf8')) as JavaRun;
const snapshot = withSnapshot(snapshotOf(scenario), {
gnutellaByNodeId: new Map(PEERS.map((host) => [host, { maximumKnownPeers: name !== 'full' ? 3 : host === ECHO ? 2 : host === FAR ? 3 : 1 }])),
});
const { engine } = watchedEngine(scenario, snapshot);
const ids = javaPingIds(java);
engine.drawGnutellaGuid = () => ids.shift() ?? 1;
for (const [time, host, call] of java.acts) {
engine.handle({ type: 'advance', toMilliseconds: time });
if (call.startsWith('join ')) engine.gnutella(host)!.join(call.slice('join '.length));
else engine.gnutella(host)!.reset();
}
engine.handle({ type: 'advance', toMilliseconds: Math.max(...java.events.map(([time]) => time)) + 10_000 });
const tolerance = GAP_TOLERANCE_MILLISECONDS + story.looks * POLL_MILLISECONDS;
const until = UNTIL[name]?.(java) ?? Number.POSITIVE_INFINITY;
if (BY_CONNECTION.has(name)) {
const portOf = new Map(scenarioCards(scenario).map((card) => [card.key, card.portId]));
const data = (text: string | undefined) => withClientPortText(text ?? '').replace(/^\d+\/\/0x/, 'x//0x');
for (const [card, list] of Object.entries(java.frames)) {
const expected = connections(
list
.filter((frame) => frame.time < until)
.map((frame) => ({ time: frame.time, raw: javaText(frame), text: `${withClientPortText(javaText(frame))} ${data(frame.payload)}` })),
);
const captured = engine.capture.frames(portOf.get(card)!).filter((each) => each.timeMilliseconds < until);
const actual = connections(
captured.map(({ timeMilliseconds, frame }) => {
const packet = frame.payload.kind === 'ipv4' ? frame.payload.packet : undefined;
const bytes = packet?.payload.kind === 'tcp' ? packet.payload.segment.payload : new Uint8Array();
const text = engineText(frame);
return { time: timeMilliseconds, raw: text, text: `${withClientPortText(text)} ${data(bytes.length === 0 ? undefined : new TextDecoder().decode(bytes))}` };
}),
);
expect([...actual.keys()].sort(), `${card}'s connections`).toEqual([...expected.keys()].sort());
for (const [key, flows] of expected) {
const texts = (each: FlowFrame[][]) => each.map((flow) => flow.map((frame) => frame.text));
expect(texts(actual.get(key)!), `${card} ${key}`).toEqual(texts(flows));
for (const [index, flow] of flows.entries()) {
for (const [at, frame] of flow.entries()) {
expect(Math.abs(actual.get(key)![index]![at]!.time - frame.time), `${card} ${key} #${index + 1} frame ${at + 1}`).toBeLessThanOrEqual(tolerance);
}
}
}
}
} else {
expectSameCaptures(engine, scenario, java.frames, { text: withClientPortText, tables: java, until: UNTIL[name]?.(java), tolerance });
}
const expected = javaPeers(java);
const actual = enginePeers(engine);
for (const host of PEERS) {
expect(
actual[host]!.map(([, peers]) => peers),
`${host}'s peers`,
).toEqual(expected[host]!.map(([, peers]) => peers));
for (const [index, [javaTime]] of expected[host]!.entries()) {
expect(Math.abs(javaTime - actual[host]![index]![0]), `${host}'s peers #${index + 1}`).toBeLessThanOrEqual(tolerance);
}
expect(engine.gnutella(host)!.peers, `${host}'s peers at the end`).toEqual(java.programs[host]!.peers);
}
});
}
});
+1 -1
View File
@@ -63,7 +63,7 @@ export interface SeenFrame {
readonly text: string;
}
function javaText(frame: JavaFrame): string {
export function javaText(frame: JavaFrame): string {
const macs = `${frame.source} > ${frame.destination}`.toUpperCase();
if (frame.arp !== undefined) return `${macs} ARP ${frame.arp === 1 ? 'request' : 'reply'} ${frame.senderIp} > ${frame.targetIp}`;
if (frame.icmp !== undefined) return icmpText(macs, frame.icmp, frame.icmpCode!, frame.sourceIp!, frame.destinationIp!, frame.sequence!);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,927 @@
{
"frames": {
"00000000-0000-0000-0000-000000000401": [
{
"time": 1018,
"source": "02:00:00:00:04:11",
"destination": "FF:FF:FF:FF:FF:FF",
"arp": 1,
"senderIp": "192.168.0.20",
"targetIp": "192.168.0.10"
},
{
"time": 1251,
"source": "02:00:00:00:04:12",
"destination": "02:00:00:00:04:11",
"arp": 2,
"senderIp": "192.168.0.10",
"targetIp": "192.168.0.20"
},
{
"time": 1253,
"source": "02:00:00:00:04:11",
"destination": "02:00:00:00:04:12",
"sourceIp": "192.168.0.20",
"destinationIp": "192.168.0.10",
"protocol": 6,
"sourcePort": 51128,
"destinationPort": 6346,
"payload": null,
"tcpFlags": "S",
"sequence": 6000000,
"acknowledgement": 0
},
{
"time": 20001,
"source": "02:00:00:00:04:11",
"destination": "FF:FF:FF:FF:FF:FF",
"arp": 1,
"senderIp": "192.168.0.20",
"targetIp": "192.168.0.99"
},
{
"time": 22751,
"source": "02:00:00:00:04:11",
"destination": "FF:FF:FF:FF:FF:FF",
"arp": 1,
"senderIp": "192.168.0.20",
"targetIp": "192.168.0.99"
}
],
"00000000-0000-0000-0000-000000000402": [
{
"time": 1138,
"source": "02:00:00:00:04:11",
"destination": "FF:FF:FF:FF:FF:FF",
"arp": 1,
"senderIp": "192.168.0.20",
"targetIp": "192.168.0.10"
},
{
"time": 1139,
"source": "02:00:00:00:04:12",
"destination": "02:00:00:00:04:11",
"arp": 2,
"senderIp": "192.168.0.10",
"targetIp": "192.168.0.20"
},
{
"time": 1367,
"source": "02:00:00:00:04:11",
"destination": "02:00:00:00:04:12",
"sourceIp": "192.168.0.20",
"destinationIp": "192.168.0.10",
"protocol": 6,
"sourcePort": 51128,
"destinationPort": 6346,
"payload": null,
"tcpFlags": "S",
"sequence": 6000000,
"acknowledgement": 0
},
{
"time": 20112,
"source": "02:00:00:00:04:11",
"destination": "FF:FF:FF:FF:FF:FF",
"arp": 1,
"senderIp": "192.168.0.20",
"targetIp": "192.168.0.99"
},
{
"time": 22863,
"source": "02:00:00:00:04:11",
"destination": "FF:FF:FF:FF:FF:FF",
"arp": 1,
"senderIp": "192.168.0.20",
"targetIp": "192.168.0.99"
}
],
"00000000-0000-0000-0000-000000000403": [
{
"time": 1138,
"source": "02:00:00:00:04:11",
"destination": "FF:FF:FF:FF:FF:FF",
"arp": 1,
"senderIp": "192.168.0.20",
"targetIp": "192.168.0.10"
},
{
"time": 20112,
"source": "02:00:00:00:04:11",
"destination": "FF:FF:FF:FF:FF:FF",
"arp": 1,
"senderIp": "192.168.0.20",
"targetIp": "192.168.0.99"
},
{
"time": 22863,
"source": "02:00:00:00:04:11",
"destination": "FF:FF:FF:FF:FF:FF",
"arp": 1,
"senderIp": "192.168.0.20",
"targetIp": "192.168.0.99"
}
],
"00000000-0000-0000-0000-000000000404": [
{
"time": 1138,
"source": "02:00:00:00:04:11",
"destination": "FF:FF:FF:FF:FF:FF",
"arp": 1,
"senderIp": "192.168.0.20",
"targetIp": "192.168.0.10"
},
{
"time": 20112,
"source": "02:00:00:00:04:11",
"destination": "FF:FF:FF:FF:FF:FF",
"arp": 1,
"senderIp": "192.168.0.20",
"targetIp": "192.168.0.99"
},
{
"time": 22863,
"source": "02:00:00:00:04:11",
"destination": "FF:FF:FF:FF:FF:FF",
"arp": 1,
"senderIp": "192.168.0.20",
"targetIp": "192.168.0.99"
}
],
"00000000-0000-0000-0000-000000000405": [
{
"time": 1138,
"source": "02:00:00:00:04:11",
"destination": "FF:FF:FF:FF:FF:FF",
"arp": 1,
"senderIp": "192.168.0.20",
"targetIp": "192.168.0.10"
},
{
"time": 20112,
"source": "02:00:00:00:04:11",
"destination": "FF:FF:FF:FF:FF:FF",
"arp": 1,
"senderIp": "192.168.0.20",
"targetIp": "192.168.0.99"
},
{
"time": 22863,
"source": "02:00:00:00:04:11",
"destination": "FF:FF:FF:FF:FF:FF",
"arp": 1,
"senderIp": "192.168.0.20",
"targetIp": "192.168.0.99"
}
],
"00000000-0000-0000-0000-000000000405#1": [],
"00000000-0000-0000-0000-000000000406": []
},
"rows": {
"00000000-0000-0000-0000-000000000401": [
[
"1",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"1",
"192.168.0.20",
"192.168.0.10",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
],
[
"2",
"02:00:00:00:04:12",
"02:00:00:00:04:11",
"",
"Netzzugang",
"0x806"
],
[
"2",
"192.168.0.10",
"192.168.0.20",
"ARP",
"Vermittlung",
"MAC ist 02:00:00:00:04:12 [op=REPLY, sender=02:00:00:00:04:12|192.168.0.10, target=02:00:00:00:04:11|192.168.0.20]"
],
[
"3",
"02:00:00:00:04:11",
"02:00:00:00:04:12",
"",
"Netzzugang",
"0x800"
],
[
"3",
"192.168.0.20",
"192.168.0.10",
"IP",
"Vermittlung",
"Protokoll: 6, TTL: 64"
],
[
"3",
"192.168.0.20:51128",
"192.168.0.10:6346",
"TCP",
"Transport",
"SYN, SEQ: 6.000.000"
],
[
"4",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"4",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
],
[
"5",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"5",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
]
],
"00000000-0000-0000-0000-000000000402": [
[
"1",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"1",
"192.168.0.20",
"192.168.0.10",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
],
[
"2",
"02:00:00:00:04:12",
"02:00:00:00:04:11",
"",
"Netzzugang",
"0x806"
],
[
"2",
"192.168.0.10",
"192.168.0.20",
"ARP",
"Vermittlung",
"MAC ist 02:00:00:00:04:12 [op=REPLY, sender=02:00:00:00:04:12|192.168.0.10, target=02:00:00:00:04:11|192.168.0.20]"
],
[
"3",
"02:00:00:00:04:11",
"02:00:00:00:04:12",
"",
"Netzzugang",
"0x800"
],
[
"3",
"192.168.0.20",
"192.168.0.10",
"IP",
"Vermittlung",
"Protokoll: 6, TTL: 64"
],
[
"3",
"192.168.0.20:51128",
"192.168.0.10:6346",
"TCP",
"Transport",
"SYN, SEQ: 6.000.000"
],
[
"4",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"4",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
],
[
"5",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"5",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
]
],
"00000000-0000-0000-0000-000000000403": [
[
"1",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"1",
"192.168.0.20",
"192.168.0.10",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
],
[
"2",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"2",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
],
[
"3",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"3",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
]
],
"00000000-0000-0000-0000-000000000404": [
[
"1",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"1",
"192.168.0.20",
"192.168.0.10",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
],
[
"2",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"2",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
],
[
"3",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"3",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
]
],
"00000000-0000-0000-0000-000000000405": [
[
"1",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"1",
"192.168.0.20",
"192.168.0.10",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
],
[
"2",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"2",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
],
[
"3",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"3",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
]
],
"00000000-0000-0000-0000-000000000405#1": [],
"00000000-0000-0000-0000-000000000406": []
},
"layeredRows": {
"00000000-0000-0000-0000-000000000401": [
[
"1",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"1",
"192.168.0.20",
"192.168.0.10",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
],
[
"2",
"02:00:00:00:04:12",
"02:00:00:00:04:11",
"",
"Netzzugang",
"0x806"
],
[
"2",
"192.168.0.10",
"192.168.0.20",
"ARP",
"Vermittlung",
"MAC ist 02:00:00:00:04:12 [op=REPLY, sender=02:00:00:00:04:12|192.168.0.10, target=02:00:00:00:04:11|192.168.0.20]"
],
[
"3",
"02:00:00:00:04:11",
"02:00:00:00:04:12",
"",
"Netzzugang",
"0x800"
],
[
"3",
"192.168.0.20",
"192.168.0.10",
"IP",
"Vermittlung",
"Protokoll: 6, TTL: 64"
],
[
"3",
"51128",
"6346",
"TCP",
"Transport",
"SYN, SEQ: 6.000.000"
],
[
"4",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"4",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
],
[
"5",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"5",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
]
],
"00000000-0000-0000-0000-000000000402": [
[
"1",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"1",
"192.168.0.20",
"192.168.0.10",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
],
[
"2",
"02:00:00:00:04:12",
"02:00:00:00:04:11",
"",
"Netzzugang",
"0x806"
],
[
"2",
"192.168.0.10",
"192.168.0.20",
"ARP",
"Vermittlung",
"MAC ist 02:00:00:00:04:12 [op=REPLY, sender=02:00:00:00:04:12|192.168.0.10, target=02:00:00:00:04:11|192.168.0.20]"
],
[
"3",
"02:00:00:00:04:11",
"02:00:00:00:04:12",
"",
"Netzzugang",
"0x800"
],
[
"3",
"192.168.0.20",
"192.168.0.10",
"IP",
"Vermittlung",
"Protokoll: 6, TTL: 64"
],
[
"3",
"51128",
"6346",
"TCP",
"Transport",
"SYN, SEQ: 6.000.000"
],
[
"4",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"4",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
],
[
"5",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"5",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
]
],
"00000000-0000-0000-0000-000000000403": [
[
"1",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"1",
"192.168.0.20",
"192.168.0.10",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
],
[
"2",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"2",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
],
[
"3",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"3",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
]
],
"00000000-0000-0000-0000-000000000404": [
[
"1",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"1",
"192.168.0.20",
"192.168.0.10",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
],
[
"2",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"2",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
],
[
"3",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"3",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
]
],
"00000000-0000-0000-0000-000000000405": [
[
"1",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"1",
"192.168.0.20",
"192.168.0.10",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
],
[
"2",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"2",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
],
[
"3",
"02:00:00:00:04:11",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"3",
"192.168.0.20",
"192.168.0.99",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.99 [op=REQUEST, sender=02:00:00:00:04:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.99]"
]
],
"00000000-0000-0000-0000-000000000405#1": [],
"00000000-0000-0000-0000-000000000406": []
},
"acts": [
[
500,
"00000000-0000-0000-0000-000000000401",
"join 192.168.0.20"
],
[
1000,
"00000000-0000-0000-0000-000000000401",
"join 192.168.0.10"
],
[
20000,
"00000000-0000-0000-0000-000000000401",
"join 192.168.0.99"
]
],
"events": [
[
214,
"00000000-0000-0000-0000-000000000401",
"peers",
""
],
[
217,
"00000000-0000-0000-0000-000000000403",
"peers",
""
],
[
218,
"00000000-0000-0000-0000-000000000404",
"peers",
""
],
[
221,
"00000000-0000-0000-0000-000000000406",
"peers",
""
],
[
500,
"00000000-0000-0000-0000-000000000401",
"call",
"join 192.168.0.20"
],
[
1000,
"00000000-0000-0000-0000-000000000401",
"call",
"join 192.168.0.10"
],
[
20000,
"00000000-0000-0000-0000-000000000401",
"call",
"join 192.168.0.99"
]
],
"programs": {
"00000000-0000-0000-0000-000000000401": {
"peers": [],
"results": []
},
"00000000-0000-0000-0000-000000000403": {
"peers": [],
"results": []
},
"00000000-0000-0000-0000-000000000404": {
"peers": [],
"results": []
},
"00000000-0000-0000-0000-000000000406": {
"peers": [],
"results": []
}
}
}
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
+2
View File
@@ -39,6 +39,7 @@ export function projectSnapshot(project: ImportedProject): TopologySnapshot {
emailServers.set(nodeId, { domain: configuration.domain, accounts: configuration.accounts, isActive: project.activePrograms.get(nodeId)?.has('emailServer') === true });
}
const emailClients = new Map([...project.emailClientConfigurations].filter(([nodeId]) => project.installedPrograms.get(nodeId)?.has('emailClient') === true));
const gnutella = new Map([...project.gnutellaConfigurations].filter(([nodeId]) => project.installedPrograms.get(nodeId)?.has('gnutella') === true));
const terminals = new Set([...project.installedPrograms].filter(([, programs]) => programs.has('commandPrompt')).map(([nodeId]) => nodeId));
const webBrowsers = new Set([...project.installedPrograms].filter(([, programs]) => programs.has('webBrowser')).map(([nodeId]) => nodeId));
return topologySnapshot({
@@ -66,6 +67,7 @@ export function projectSnapshot(project: ImportedProject): TopologySnapshot {
emailServersByNodeId: emailServers,
emailClientsByNodeId: emailClients,
terminalNodeIds: terminals,
gnutellaByNodeId: gnutella,
fileSystemsByNodeId: project.virtualFileSystems,
});
}