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:
+3
-1
File diff suppressed because one or more lines are too long
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -870,6 +870,41 @@ Gemessen (02.10.2026):
|
||||
ebenso eine unter Last (andere Sitzungen auf dem Rechner: die erste
|
||||
ARP-Runde dauert dann knapp 300 statt 225 ms).
|
||||
|
||||
## Gnutella, Beitreten (F3.6a)
|
||||
|
||||
`GnutellaRun` installiert Javas Gnutella-Programm (`PeerToPeerAnwendung`)
|
||||
auf vier Rechnern von `terminal-net` (192.168.0.20, .30, .31 und 10.0.0.2
|
||||
hinter dem Router; .10 ohne) und ruft `beitretenNetzwerk` und
|
||||
`resetNetwork` zu festen Zeiten, wie das Fenster es tut. Geschrieben werden
|
||||
je Karte die Frames samt Tabellen, die bekannten Teilnehmer jedes Programms
|
||||
bei jeder Meldung an sein Fenster und am Ende.
|
||||
|
||||
Gemessen (02.10.2026):
|
||||
|
||||
* Ein Paket ist Text, seine Felder durch `//` getrennt (`PeerToPeerPaket
|
||||
.toString`): Ping `guid//0x00//hops//ttl//0//IP`, Pong
|
||||
`guid//0x01//0//8//Bits//IP//6346//Dateien//Bytes`; die Bitlänge zählt
|
||||
die Zeichen der Adresse mal acht und für jede Zahl die kleinste
|
||||
Zweierpotenz, die sie fasst. Die GUID ist zufällig (`new Random()`).
|
||||
* Beitreten: eine Verbindung zum Port 6346 des Teilnehmers, der Ping; der
|
||||
Teilnehmer nimmt den Absender des Pings auf, antwortet mit einem Pong und
|
||||
schickt den Ping (hops+1, ttl−1) über je eine neue Verbindung an seine
|
||||
anderen Teilnehmer, außer an den Absender und den Rechner des Pings;
|
||||
deren Pongs gehen (hops+1, ttl−1) über die Verbindung zurück, auf der der
|
||||
Ping kam (`sendePaket` nach GUID). Wer einen Pong auf seinen eigenen Ping
|
||||
bekommt, nimmt dessen Rechner auf; jeder höchstens so viele, wie er
|
||||
behält. Keine Verbindung wird je geschlossen.
|
||||
* Fehler Javas: kommt derselbe Ping ein zweites Mal an, gibt `erstellePong`
|
||||
`null` zurück, und `toString()` darauf wirft; der Arbeiter schließt die
|
||||
Verbindung (gemeint war: kein Pong). Ein Text ohne Paketkopf wirft
|
||||
ebenso und schließt die Verbindung.
|
||||
* Zu sich selbst kein Ping; zu einem Rechner ohne Programm ein SYN (Javas
|
||||
eines statt drei); ins Leere nur ARP.
|
||||
* Wo viele Verbindungen zugleich laufen, kommen die Frames zweier
|
||||
Verbindungen (oder beider Richtungen einer) an einer Karte mal so, mal so
|
||||
herum, einige zehn Millisekunden auseinander: Javas Thread-Takt über
|
||||
mehrere Sprünge.
|
||||
|
||||
## Datei-Explorer, Texteditor, Bildbetrachter (F3.5b)
|
||||
|
||||
`FileCases` tut, was die Fenster dieser Programme mit den Dateien eines
|
||||
|
||||
@@ -21,7 +21,7 @@ if [ ! -f "$upstream/target/filius.jar" ]; then
|
||||
fi
|
||||
classpath="$upstream/target/filius.jar:$upstream/target/lib/*"
|
||||
mkdir -p "$here/.build"
|
||||
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$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" "$here/src/FileCases.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" "$here/src/FileCases.java" "$here/src/GnutellaRun.java"
|
||||
# FILIUS writes its settings below $HOME; keep them out of the real one.
|
||||
mkdir -p "$here/.build/home"
|
||||
# Removed first: a crashed run must not leave the previous result standing.
|
||||
@@ -286,6 +286,17 @@ for language in de en; do
|
||||
test -s "$output"
|
||||
done
|
||||
|
||||
# The Gnutella program joining peers on terminal-net (GnutellaRun;
|
||||
# packages/core/test/gnutellaJava.test.ts).
|
||||
for run in pair:6000 chain:9000 twoPaths:15000 full:11000 reset:7000 errors:40000; do
|
||||
story="${run%%:*}"
|
||||
output="$root/packages/core/test/scenarios/gnutella-java-$story.java.json"
|
||||
rm -f "$output"
|
||||
HOME="$here/.build/home" xvfb-run -a java $german -cp "$classpath:$here/.build" GnutellaRun \
|
||||
"$root/packages/core/test/scenarios/terminal-net.scenario.json" "$story" "${run##*:}" "$output" 2>&1 | grep -v 'logback\|^$\|^\s*at \|scrollpane' || true
|
||||
test -s "$output"
|
||||
done
|
||||
|
||||
# What the file explorer and the text editor do to a host's files, without a
|
||||
# running network, in both languages (FileCases; packages/core/test/filePrograms.test.ts).
|
||||
for language in de en; do
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
// Runs a story of FILIUS' Gnutella program (PeerToPeerAnwendung with its
|
||||
// PeerToPeerServer on port 6346 and PeerToPeerClient) on terminal-net in Java
|
||||
// FILIUS and records what really happens: every frame each card saw, Java's
|
||||
// tables of them, each program's known peers whenever it tells its window,
|
||||
// and at the end each program's known peers and search results.
|
||||
//
|
||||
// GnutellaRun <scenario.json> <story> <milliseconds> <out.json>
|
||||
//
|
||||
// terminal-net: the program on 192.168.0.20, .30, .31 and 10.0.0.2 (behind
|
||||
// the router), each keeping at most three peers unless the story says
|
||||
// otherwise; .10 has none. A call comes as the program's window makes it
|
||||
// (beitretenNetzwerk, resetNetwork); the acts are in the output.
|
||||
// pair .30 joins .31
|
||||
// chain .30 joins .31, then .20 joins .30 (the ping goes on to .31)
|
||||
// twoPaths .31 joins 10.0.0.2, .30 joins .31 and 10.0.0.2, then .20 joins
|
||||
// .30: the ping reaches .31 and 10.0.0.2 each on two paths
|
||||
// full .31 keeps two peers, .30 and .20 one; .30, .20 and 10.0.0.2 join .31
|
||||
// reset .30 joins .31, resets, joins again
|
||||
// errors .20 joins itself, .10 (no program), .99 (no one)
|
||||
|
||||
import filius.software.dateiaustausch.PeerToPeerAnwendung;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class GnutellaRun {
|
||||
static final String CLIENT = "00000000-0000-0000-0000-000000000401";
|
||||
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[] PEERS = { CLIENT, WEB, ECHO, FAR };
|
||||
static final String CLASS = "filius.software.dateiaustausch.PeerToPeerAnwendung";
|
||||
|
||||
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 calls: time, host, call ("join <address>" or "reset"). */
|
||||
static List<Object[]> acts(String story) {
|
||||
List<Object[]> acts = new ArrayList<>();
|
||||
switch (story) {
|
||||
case "pair":
|
||||
acts.add(new Object[] { 500L, WEB, "join 192.168.0.31" });
|
||||
break;
|
||||
case "chain":
|
||||
acts.add(new Object[] { 500L, WEB, "join 192.168.0.31" });
|
||||
acts.add(new Object[] { 3000L, CLIENT, "join 192.168.0.30" });
|
||||
break;
|
||||
case "twoPaths":
|
||||
acts.add(new Object[] { 500L, ECHO, "join 10.0.0.2" });
|
||||
acts.add(new Object[] { 3000L, WEB, "join 192.168.0.31" });
|
||||
acts.add(new Object[] { 5500L, WEB, "join 10.0.0.2" });
|
||||
acts.add(new Object[] { 8000L, CLIENT, "join 192.168.0.30" });
|
||||
break;
|
||||
case "full":
|
||||
acts.add(new Object[] { 500L, WEB, "join 192.168.0.31" });
|
||||
acts.add(new Object[] { 3000L, CLIENT, "join 192.168.0.31" });
|
||||
acts.add(new Object[] { 5500L, FAR, "join 192.168.0.31" });
|
||||
break;
|
||||
case "reset":
|
||||
acts.add(new Object[] { 500L, WEB, "join 192.168.0.31" });
|
||||
acts.add(new Object[] { 3000L, WEB, "reset" });
|
||||
acts.add(new Object[] { 3500L, WEB, "join 192.168.0.31" });
|
||||
break;
|
||||
case "errors":
|
||||
acts.add(new Object[] { 500L, CLIENT, "join 192.168.0.20" });
|
||||
acts.add(new Object[] { 1000L, CLIENT, "join 192.168.0.10" });
|
||||
acts.add(new Object[] { 20000L, CLIENT, "join 192.168.0.99" });
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("unknown story " + story);
|
||||
}
|
||||
return acts;
|
||||
}
|
||||
|
||||
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, PeerToPeerAnwendung> programs = new LinkedHashMap<>();
|
||||
for (String host : PEERS) {
|
||||
network.system(host).installApp(CLASS, false);
|
||||
PeerToPeerAnwendung program = (PeerToPeerAnwendung) network.system(host).holeSoftware(CLASS);
|
||||
program.setMaxTeilnehmerZahl(!story.equals("full") ? 3 : host.equals(ECHO) ? 2 : host.equals(FAR) ? 3 : 1);
|
||||
program.hinzuBeobachter((observable, data) -> event(host, "peers", String.join(",", program.holeBekanntePeerToPeerTeilnehmer())));
|
||||
programs.put(host, program);
|
||||
}
|
||||
network.start();
|
||||
for (Object[] act : acts) {
|
||||
sleepUntil((Long) act[0]);
|
||||
String host = (String) act[1];
|
||||
String call = (String) act[2];
|
||||
event(host, "call", call);
|
||||
if (call.startsWith("join ")) programs.get(host).beitretenNetzwerk(call.substring("join ".length()));
|
||||
else programs.get(host).resetNetwork();
|
||||
}
|
||||
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 (GnutellaRun.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.key("programs").open('{');
|
||||
for (Map.Entry<String, PeerToPeerAnwendung> entry : programs.entrySet()) {
|
||||
PeerToPeerAnwendung program = entry.getValue();
|
||||
out.key(entry.getKey()).open('{');
|
||||
out.key("peers").open('[');
|
||||
for (String peer : program.holeBekanntePeerToPeerTeilnehmer()) out.string(peer);
|
||||
out.close(']');
|
||||
out.key("results").open('[');
|
||||
for (String result : program.holeErgebnisse()) out.string(result);
|
||||
out.close(']');
|
||||
out.close('}');
|
||||
}
|
||||
out.close('}');
|
||||
out.close('}');
|
||||
network.stop();
|
||||
ScenarioNetwork.write(args[3], out);
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user