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
+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;