feat(core): SYN+ACK and FIN go once, as in Java, measured with Java FILIUS
TcpRun loses exactly one frame (loseFirst: the card's output buffer drops it; the sender's packet log still shows it, as with setDrop) in three new stories: lostSynAck, lostFin, lostLastFin. Measured: Java never repeats a SYN+ACK or a FIN. The server gives a handshake up after one timeout (3 RTT) and closes with a FIN from SYN_RCVD (its boundary check sometimes lets it wait twice; it means once). After its FIN a socket waits at most five times, each wait ended by a segment or the timeout; then it is gone. The core does the same (TCP_CLOSING_WAITS, with TIME_WAIT in the loop) and shows CLOSED where Java leaves the state standing (a named Java bug). One departure: a repeated SYN in SYN_RCVD gets the SYN+ACK once more and the wait starts anew; Java closes there, but its client never repeats the SYN. lostSynAck is compared up to the second SYN (JAVA_UNTIL); the rest is an engine test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -13,7 +13,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
||||
| `src/model/link.ts`, `src/model/graph.ts` | `Model/TopologyLink.swift`, `Model/TopologyGraph.swift` | übertragen, bisher nur die Daten (Abfragen folgen mit dem Editor) |
|
||||
| `src/model/documentation.ts` | `Model/TopologyDocumentation.swift` | übertragen; FNV-Kennung mit BigInt, gegen die IDs aus dem Swift-Import geprüft |
|
||||
| `src/runtime/config.ts` | `State/TopologyEditorState.swift` (Laufzeit-Konfigurationen, WLAN eines Rechners, DHCP, Portweiterleitung, Remote Link ohne LAN-Transport, Programme und ihre Konfiguration, Firewall, DNS-Einträge aus `State/TopologyDNSResolver.swift`, E-Mail-Konten und -Nachrichten aus `State/TopologyEmail.swift`); dazu Echo-Server- und Client-Konfiguration, die Swift nicht speichert | übertragen; Java-Beschriftungen der Oberfläche weggelassen |
|
||||
| `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4472–5187 (UDP), ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall, UDP-Sockets (binden, freie Ports nach Javas Zufall, senden samt Broadcast, zustellen, empfangen mit Wartezeit, schließen), TCP (Z. ~5187–5913: Server- und Client-Sockets, Handschlag mit Ergebnis, Segmente zu 1460 Byte bis zum PSH, höchstens drei Versuche, Abbau über alle Java-Zustände samt `TIME_WAIT`; seit F2.3c nach Javas `TCPSocket` an Java-Filius gemessen: je Versuch und für `TIME_WAIT` 3 RTT nach der Geschwindigkeit (8250 ms bei 90 %, Swift fest 7500 bzw. 2500 ms), der Zeitablauf zählt ab der Übergabe des Segments, vor ARP; ein Segment beendet `TIME_WAIT`; die Gegenseite schließt beim FIN von selbst; nicht bestätigte Daten schließen mit FIN statt sofort; der angenommene Socket hat die Startnummer des Listeners, der Listener eine neue. Abweichend von Java: drei SYN statt einem, weil Javas Wiederholungsschleife nach dem ersten Zeitablauf abbricht), DHCP (Z. ~5913–6694: Server mit festen Zuordnungen, Adressbereich, Angeboten, Leases und Sperrliste; Client seit F2.3b nach Javas `DHCPClient` neu gebaut und an Java-Filius gemessen: je Versuch eine Nachricht, eine Antwort, die nicht passt, wird gelesen und verworfen; die ARP-Prüfung wartet eine RTT, eine Antwort für die Adresse beendet sie sofort, dann DECLINE; nach einem Zeitablauf wiederholt der Zustand seine Nachricht, nach dem fünften nimmt der Client eine Link-Local-Adresse `169.254.1.x/16` (ARP-Probe, ARP-Reply an alle) und gibt keine alte Adresse zurück; Swift sendete doppelt, wartete nicht auf ARP und kehrte nach zehn Versuchen zur alten Adresse zurück. Abweichend von Java: Nach fünf belegten Link-Local-Adressen gibt der Client auf (Java sucht endlos), und ein ARP-Paket für eine andere Adresse beendet die Prüfung nicht. Seit F2.3d befördert ein Kabel einen Frame nach dem anderen wie Javas `SimplexVerbindung` (ein Frame wartet im Ausgangspuffer, bis der vorige angekommen ist, und erscheint im Protokoll, wenn er losgeht); Swift schickt alle zugleich, die Option `swiftCables` behält das für den Swift-Vergleich. Die RTT der DHCP-Zeiten (Warten, Angebote, Sperrliste) folgt wie in Java der Geschwindigkeit (50 Kabel, 2750 ms bei 90 %); Swift rechnet fest mit 2500 ms. ARP seit F2.4b nach Javas `holeARPTabellenEintrag`, an Java-Filius gemessen: eine Auflösung fragt, wartet eine RTT, fragt ein zweites Mal und gibt nach einer weiteren RTT auf; dann „Host unreachable" beim Absender selbst oder vom Router zurück (bei ICMP nur für Echo-Anfragen); ein wartender Absender wacht nach den übrigen Ereignissen desselben Zeitpunkts auf; der Ping wartet danach eine RTT auf die Antwort. Swift schickt in der Ereignisverarbeitung beide Anfragen zugleich und lässt das Paket ohne Ende warten, wenn die Adresse irgendwo im Netz vorkommt (Wissen, das kein Knoten hat), und blockiert direkte Aufrufe ohne Wartezeit; die Option `swiftArp` behält das für den Swift-Vergleich. Seit F2.4b-2 kehrt ein Senden wie in Java sofort zurück, die Frames reisen, während die Uhr läuft (Swift: der Aufruf kehrt erst zurück, wenn alle Frames angekommen sind; die Option `swiftSends` behält das für den Swift-Vergleich), und UDP und TCP senden über Javas Transport-Thread je Knoten und Protokoll: ein Segment nach dem anderen; solange IP auf ARP wartet, blockiert das nächste `senden`. Wartende Absender kommen in der Reihenfolge an die Reihe, in der sie kamen (Javas Sperre überlässt das dem Zufall). DNS seit F2.4a nach Javas `DNSServer`/`Resolver`, an Java-Filius gemessen: Server auf Port 53 mit Einträgen aus dem Modell (die Antwort wiederholt die Frage nicht, RCODE immer 0, ein Verweis mit NS und A im Antwortteil), iterativer Resolver mit 5 RTT je Anfrage, der nur die Server behält, an die er verwiesen wurde; noch ohne Rekursion. `receiveUdp` kehrt wie Javas `empfangen` zurück, sobald ein Datagramm da ist. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, DNS, RIP, Remote Link, WLAN |
|
||||
| `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4472–5187 (UDP), ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall, UDP-Sockets (binden, freie Ports nach Javas Zufall, senden samt Broadcast, zustellen, empfangen mit Wartezeit, schließen), TCP (Z. ~5187–5913: Server- und Client-Sockets, Handschlag mit Ergebnis, Segmente zu 1460 Byte bis zum PSH, höchstens drei Versuche, Abbau über alle Java-Zustände samt `TIME_WAIT`; seit F2.3c nach Javas `TCPSocket` an Java-Filius gemessen: je Versuch und für `TIME_WAIT` 3 RTT nach der Geschwindigkeit (8250 ms bei 90 %, Swift fest 7500 bzw. 2500 ms), der Zeitablauf zählt ab der Übergabe des Segments, vor ARP; ein Segment beendet `TIME_WAIT`; die Gegenseite schließt beim FIN von selbst; nicht bestätigte Daten schließen mit FIN statt sofort; der angenommene Socket hat die Startnummer des Listeners, der Listener eine neue. seit F2.3e wie Java: SYN+ACK und FIN gehen nie ein zweites Mal hinaus; der Server gibt einen Handschlag nach 3 RTT auf und schließt mit FIN; nach dem FIN wartet ein Socket höchstens fünfmal 3 RTT, jedes Segment beendet eine Wartezeit, dann CLOSED (Java lässt den Zustand stehen). Abweichend von Java: drei SYN statt einem, weil Javas Wiederholungsschleife nach dem ersten Zeitablauf abbricht, und ein wiederholtes SYN in `SYN_RCVD` bekommt das SYN+ACK noch einmal, statt den Socket zu schließen), DHCP (Z. ~5913–6694: Server mit festen Zuordnungen, Adressbereich, Angeboten, Leases und Sperrliste; Client seit F2.3b nach Javas `DHCPClient` neu gebaut und an Java-Filius gemessen: je Versuch eine Nachricht, eine Antwort, die nicht passt, wird gelesen und verworfen; die ARP-Prüfung wartet eine RTT, eine Antwort für die Adresse beendet sie sofort, dann DECLINE; nach einem Zeitablauf wiederholt der Zustand seine Nachricht, nach dem fünften nimmt der Client eine Link-Local-Adresse `169.254.1.x/16` (ARP-Probe, ARP-Reply an alle) und gibt keine alte Adresse zurück; Swift sendete doppelt, wartete nicht auf ARP und kehrte nach zehn Versuchen zur alten Adresse zurück. Abweichend von Java: Nach fünf belegten Link-Local-Adressen gibt der Client auf (Java sucht endlos), und ein ARP-Paket für eine andere Adresse beendet die Prüfung nicht. Seit F2.3d befördert ein Kabel einen Frame nach dem anderen wie Javas `SimplexVerbindung` (ein Frame wartet im Ausgangspuffer, bis der vorige angekommen ist, und erscheint im Protokoll, wenn er losgeht); Swift schickt alle zugleich, die Option `swiftCables` behält das für den Swift-Vergleich. Die RTT der DHCP-Zeiten (Warten, Angebote, Sperrliste) folgt wie in Java der Geschwindigkeit (50 Kabel, 2750 ms bei 90 %); Swift rechnet fest mit 2500 ms. ARP seit F2.4b nach Javas `holeARPTabellenEintrag`, an Java-Filius gemessen: eine Auflösung fragt, wartet eine RTT, fragt ein zweites Mal und gibt nach einer weiteren RTT auf; dann „Host unreachable" beim Absender selbst oder vom Router zurück (bei ICMP nur für Echo-Anfragen); ein wartender Absender wacht nach den übrigen Ereignissen desselben Zeitpunkts auf; der Ping wartet danach eine RTT auf die Antwort. Swift schickt in der Ereignisverarbeitung beide Anfragen zugleich und lässt das Paket ohne Ende warten, wenn die Adresse irgendwo im Netz vorkommt (Wissen, das kein Knoten hat), und blockiert direkte Aufrufe ohne Wartezeit; die Option `swiftArp` behält das für den Swift-Vergleich. Seit F2.4b-2 kehrt ein Senden wie in Java sofort zurück, die Frames reisen, während die Uhr läuft (Swift: der Aufruf kehrt erst zurück, wenn alle Frames angekommen sind; die Option `swiftSends` behält das für den Swift-Vergleich), und UDP und TCP senden über Javas Transport-Thread je Knoten und Protokoll: ein Segment nach dem anderen; solange IP auf ARP wartet, blockiert das nächste `senden`. Wartende Absender kommen in der Reihenfolge an die Reihe, in der sie kamen (Javas Sperre überlässt das dem Zufall). DNS seit F2.4a nach Javas `DNSServer`/`Resolver`, an Java-Filius gemessen: Server auf Port 53 mit Einträgen aus dem Modell (die Antwort wiederholt die Frage nicht, RCODE immer 0, ein Verweis mit NS und A im Antwortteil), iterativer Resolver mit 5 RTT je Anfrage, der nur die Server behält, an die er verwiesen wurde; noch ohne Rekursion. `receiveUdp` kehrt wie Javas `empfangen` zurück, sobald ein Datagramm da ist. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, DNS, RIP, Remote Link, WLAN |
|
||||
| `src/runtime/dns.ts` | Java `filius.software.dns.DNSNachricht`, `Query`, `ResourceRecord` | neu nach Java statt nach Swift (Swift ersetzt Javas Textnachrichten durch `FILIUS-DNS-TYPED/1`): Kopfzeile `ID=… QR=… RCODE=… QDCOUNT=… …`, Frage- und Eintragszeilen, Suche nach Namen ohne Rücksicht auf Groß- und Kleinschreibung, NS der längsten Zone, sonst der Wurzel |
|
||||
| `src/runtime/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen |
|
||||
| `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` |
|
||||
@@ -32,7 +32,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
||||
| `test/dhcpJava.test.ts`, `test/scenarios/*.java.json`, `test/scenario.ts`, `test/javaRun.ts` | — | neu: die DHCP-Szenarien gegen Java-Filius (`tools/java-oracle`, `DhcpRun`): je Client dieselben Frames in derselben Reihenfolge, Abstände auf 200 ms, dieselben Adressen am Ende |
|
||||
| `test/arpJava.test.ts`, `test/scenarios/arp-routed.scenario.json`, `test/scenarios/arp-java-*.java.json` | — | neu: vier ARP-Abläufe gegen Java-Filius (`ArpRun`): Frames, Abstände und wann jeder Aufruf zurückkehrt; die zwei Pings stimmen seit F2.4b-1, die zwei UDP-Abläufe seit F2.4b-2 (Javas Transport-Thread) |
|
||||
| `test/dnsJava.test.ts`, `test/scenarios/dns-lan.scenario.json`, `test/scenarios/dns-java-*.java.json` | — | neu: fünf DNS-Abfragen gegen Java-Filius (`DnsRun`): Frames, Ergebnis und Dauer; seit F2.4b-1 alle wie Java |
|
||||
| `test/tcpJava.test.ts`, `test/scenarios/tcp-java-*.java.json` | — | neu: drei TCP-Abläufe gegen Java-Filius (`TcpRun`): Frames mit Flags und Nummern, Socket-Zustände; ein benannter Java-Fehler (`JAVA_BUGS`: nur ein SYN) |
|
||||
| `test/tcpJava.test.ts`, `test/scenarios/tcp-java-*.java.json` | — | neu: sechs TCP-Abläufe gegen Java-Filius (`TcpRun`, drei davon mit genau einem verlorenen Frame, `loseFirst`): Frames mit Flags und Nummern, Socket-Zustände; benannte Java-Fehler (`JAVA_BUGS`: nur ein SYN, der Zustand bleibt nach dem Schließen stehen), `lostSynAck` nur bis zum zweiten SYN verglichen (`JAVA_UNTIL`) |
|
||||
| `test/routeTable.test.ts` | `TopologyJavaRouteTableTests.swift` (8 von 12 Tests) | übertragen; Geometrie- und Reducer-Tests folgen mit Editor und Dokument |
|
||||
|
||||
## packages/fls
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
// "Java-compatible TCP sockets" (lines ~5187–5913; the
|
||||
// timing and closing after Java's TCPSocket, measured with Java
|
||||
// FILIUS: 3 round trips per attempt and for TIME_WAIT, the other end
|
||||
// closes by itself on FIN, failed data closes with FIN)
|
||||
// closes by itself on FIN, failed data closes with FIN; SYN+ACK and
|
||||
// FIN go once, then Java's close loop of five waits)
|
||||
// "DHCP server and automatic client" (lines ~5913–6694; the
|
||||
// client rebuilt after Java's DHCPClient, measured with Java FILIUS:
|
||||
// one message per attempt, an ARP check that waits a round trip,
|
||||
@@ -308,6 +309,8 @@ export interface TcpSession {
|
||||
remoteSequenceNumber: number;
|
||||
lastAcknowledgedSequenceNumber: number | undefined;
|
||||
lastSentSegment: TcpSegment | undefined;
|
||||
/** While the socket closes (Java's close loop): the waits it has left; else undefined. */
|
||||
closingWaitsLeft: number | undefined;
|
||||
receiveAssembly: Uint8Array;
|
||||
receivedMessages: Uint8Array[];
|
||||
}
|
||||
@@ -411,6 +414,12 @@ function sameIgnoringCase(lhs: string | undefined, rhs: string | undefined): boo
|
||||
|
||||
export const TCP_MAXIMUM_SEGMENT_SIZE = 1_460;
|
||||
export const TCP_MAXIMUM_SEND_ATTEMPTS = 3;
|
||||
/**
|
||||
* Java's TCPSocket.close: after its FIN (sent once) a socket waits at most
|
||||
* five times, each wait ended by a segment or after defaultTimeout; then it
|
||||
* is gone.
|
||||
*/
|
||||
export const TCP_CLOSING_WAITS = 5;
|
||||
export const TCP_EPHEMERAL_PORT_LOWER_BOUND = 49_152;
|
||||
export const TCP_EPHEMERAL_PORT_UPPER_BOUND_EXCLUSIVE = 65_535;
|
||||
|
||||
@@ -1508,20 +1517,49 @@ export class NetworkRuntimeEngine {
|
||||
return;
|
||||
case 'ESTABLISHED':
|
||||
case 'SYN_RCVD':
|
||||
this.updateTcpState(socketId, 'FIN_WAIT_1');
|
||||
this.resetTcpSendAttempts(socketId);
|
||||
this.transmitTcpSegment(socketId, TCP_FLAG.finish, new Uint8Array(), 0, 'TCP FIN');
|
||||
this.beginTcpClosing(socketId, 'FIN_WAIT_1', 'TCP FIN');
|
||||
return;
|
||||
case 'CLOSE_WAIT':
|
||||
this.updateTcpState(socketId, 'LAST_ACK');
|
||||
this.resetTcpSendAttempts(socketId);
|
||||
this.transmitTcpSegment(socketId, TCP_FLAG.finish, new Uint8Array(), 0, 'TCP FIN after CLOSE_WAIT');
|
||||
this.beginTcpClosing(socketId, 'LAST_ACK', 'TCP FIN after CLOSE_WAIT');
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Java's close: the FIN goes once, never again; the first wait counts from
|
||||
* handing it down (the timeout transmitTcpSegment schedules).
|
||||
*/
|
||||
private beginTcpClosing(socketId: Id, state: 'FIN_WAIT_1' | 'LAST_ACK', detail: string): void {
|
||||
const session = this.stateValue.tcpSessionsById.get(socketId);
|
||||
this.clearTcpPendingTransmission(socketId);
|
||||
this.updateTcpState(socketId, state);
|
||||
this.resetTcpSendAttempts(socketId);
|
||||
if (session !== undefined) session.closingWaitsLeft = TCP_CLOSING_WAITS;
|
||||
this.transmitTcpSegment(socketId, TCP_FLAG.finish, new Uint8Array(), 0, detail);
|
||||
}
|
||||
|
||||
/**
|
||||
* One wait of Java's close loop has ended, with a segment (already handled)
|
||||
* or after defaultTimeout. TIME_WAIT ends with it; after the last wait the
|
||||
* socket is gone — Java leaves its state as it was, the core shows CLOSED.
|
||||
*/
|
||||
private endTcpClosingWait(socketId: Id): void {
|
||||
const socket = this.stateValue.socketsById.get(socketId);
|
||||
const session = this.stateValue.tcpSessionsById.get(socketId);
|
||||
if (socket === undefined || session?.closingWaitsLeft === undefined) return;
|
||||
this.cancelTcpTimeouts(socketId);
|
||||
session.closingWaitsLeft -= 1;
|
||||
if (socket.tcpState === 'CLOSED') return;
|
||||
if (session.closingWaitsLeft <= 0) {
|
||||
session.lastSentSegment = undefined;
|
||||
this.updateTcpState(socketId, 'CLOSED');
|
||||
return;
|
||||
}
|
||||
this.schedule(this.stateValue.currentTimeMilliseconds + this.tcpTimeoutMilliseconds(), { kind: 'tcpTimeout', sessionId: socketId });
|
||||
}
|
||||
|
||||
/** The socket at the other end of a connection, the first by id. */
|
||||
peerTcpSocketId(socketId: Id): Id | undefined {
|
||||
const socket = this.stateValue.socketsById.get(socketId);
|
||||
@@ -1657,6 +1695,11 @@ export class NetworkRuntimeEngine {
|
||||
this.updateTcpState(socketId, 'CLOSED');
|
||||
return;
|
||||
}
|
||||
if (this.stateValue.tcpSessionsById.get(socketId)?.closingWaitsLeft !== undefined) {
|
||||
this.processTcpSegmentWhileClosing(socketId, segment);
|
||||
this.endTcpClosingWait(socketId);
|
||||
return;
|
||||
}
|
||||
switch (socket.tcpState) {
|
||||
case 'SYN_SENT': {
|
||||
if (
|
||||
@@ -1674,6 +1717,16 @@ export class NetworkRuntimeEngine {
|
||||
return;
|
||||
}
|
||||
case 'SYN_RCVD': {
|
||||
if (has(TCP_FLAG.synchronize) && !has(TCP_FLAG.acknowledgement)) {
|
||||
// The client's SYN again: its SYN+ACK was lost. Java closes here
|
||||
// (its client never repeats the SYN); the core answers once more
|
||||
// and waits anew.
|
||||
const last = this.stateValue.tcpSessionsById.get(socketId)?.lastSentSegment;
|
||||
if (last === undefined) return;
|
||||
this.cancelTcpTimeouts(socketId);
|
||||
this.transmitTcpSegment(socketId, last.flags, last.payload, last.acknowledgementNumber, 'TCP SYN+ACK', true);
|
||||
return;
|
||||
}
|
||||
if (!has(TCP_FLAG.acknowledgement) || !this.acknowledgeTcpIfExpected(socketId, segment.acknowledgementNumber)) return;
|
||||
this.updateTcpState(socketId, 'ESTABLISHED');
|
||||
// Java: the socket that listened is the connection now; the server
|
||||
@@ -1684,10 +1737,6 @@ export class NetworkRuntimeEngine {
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'TIME_WAIT':
|
||||
this.clearTcpPendingTransmission(socketId);
|
||||
this.updateTcpState(socketId, 'CLOSED');
|
||||
return;
|
||||
case 'ESTABLISHED':
|
||||
if (has(TCP_FLAG.finish)) {
|
||||
this.sendTcpAcknowledgement(socketId, segment);
|
||||
@@ -1701,6 +1750,20 @@ export class NetworkRuntimeEngine {
|
||||
this.acknowledgeTcpIfExpected(socketId, segment.acknowledgementNumber);
|
||||
}
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/** A segment in Java's close loop: TIME_WAIT ends with it, the other states move on. */
|
||||
private processTcpSegmentWhileClosing(socketId: Id, segment: TcpSegment): void {
|
||||
const socket = this.stateValue.socketsById.get(socketId);
|
||||
const has = (flag: number) => (segment.flags & flag) !== 0;
|
||||
switch (socket?.tcpState) {
|
||||
case 'TIME_WAIT':
|
||||
this.clearTcpPendingTransmission(socketId);
|
||||
this.updateTcpState(socketId, 'CLOSED');
|
||||
return;
|
||||
case 'FIN_WAIT_1': {
|
||||
const hasAck = has(TCP_FLAG.acknowledgement);
|
||||
const hasFin = has(TCP_FLAG.finish);
|
||||
@@ -1842,19 +1905,29 @@ export class NetworkRuntimeEngine {
|
||||
private handleTcpTimeout(socketId: Id): void {
|
||||
const socket = this.stateValue.socketsById.get(socketId);
|
||||
if (socket === undefined || socket.tcpState === undefined) return;
|
||||
if (socket.tcpState === 'TIME_WAIT') {
|
||||
this.updateTcpState(socketId, 'CLOSED');
|
||||
this.clearTcpPendingTransmission(socketId);
|
||||
const session = this.stateValue.tcpSessionsById.get(socketId);
|
||||
if (session?.closingWaitsLeft !== undefined) {
|
||||
if (socket.tcpState === 'TIME_WAIT') {
|
||||
this.clearTcpPendingTransmission(socketId);
|
||||
this.updateTcpState(socketId, 'CLOSED');
|
||||
}
|
||||
this.endTcpClosingWait(socketId);
|
||||
return;
|
||||
}
|
||||
if (socket.tcpState === 'SYN_RCVD') {
|
||||
// Java's server sends its SYN+ACK once and waits defaultTimeout for the
|
||||
// ACK, then closes: a FIN (Java's boundary check sometimes lets it wait
|
||||
// a second time; it means one).
|
||||
this.closeTcpSocket(socketId);
|
||||
return;
|
||||
}
|
||||
const session = this.stateValue.tcpSessionsById.get(socketId);
|
||||
const last = session?.lastSentSegment;
|
||||
if (session === undefined || last === undefined) return;
|
||||
if (session.sendAttempts >= TCP_MAXIMUM_SEND_ATTEMPTS) {
|
||||
this.clearTcpPendingTransmission(socketId);
|
||||
// Java: data that no one acknowledges (sent only while ESTABLISHED)
|
||||
// ends senden with schliessen, an orderly close with FIN. A handshake
|
||||
// or a FIN that fails closes at once.
|
||||
// ends senden with schliessen, an orderly close with FIN. A connection
|
||||
// attempt that fails closes at once.
|
||||
if (socket.tcpState === 'ESTABLISHED') {
|
||||
this.closeTcpSocket(socketId);
|
||||
} else {
|
||||
@@ -1862,15 +1935,8 @@ export class NetworkRuntimeEngine {
|
||||
}
|
||||
return;
|
||||
}
|
||||
const synchronize = (last.flags & TCP_FLAG.synchronize) !== 0;
|
||||
const baseDetail = synchronize
|
||||
? (last.flags & TCP_FLAG.acknowledgement) === 0
|
||||
? 'TCP SYN'
|
||||
: 'TCP SYN+ACK'
|
||||
: (last.flags & TCP_FLAG.finish) !== 0
|
||||
? 'TCP FIN'
|
||||
: 'TCP data';
|
||||
this.transmitTcpSegment(socketId, last.flags, last.payload, last.acknowledgementNumber, baseDetail, true);
|
||||
const detail = (last.flags & TCP_FLAG.synchronize) !== 0 ? 'TCP SYN' : 'TCP data';
|
||||
this.transmitTcpSegment(socketId, last.flags, last.payload, last.acknowledgementNumber, detail, true);
|
||||
}
|
||||
|
||||
private acknowledgeTcpIfExpected(socketId: Id, acknowledgementNumber: number): boolean {
|
||||
@@ -1896,11 +1962,10 @@ export class NetworkRuntimeEngine {
|
||||
);
|
||||
}
|
||||
|
||||
/** Java's close loop: TIME_WAIT ends with the next segment, else after defaultTimeout. */
|
||||
/** Java's close loop: TIME_WAIT ends with the next wait, by a segment or after defaultTimeout. */
|
||||
private enterTcpTimeWait(socketId: Id): void {
|
||||
this.clearTcpPendingTransmission(socketId);
|
||||
this.updateTcpState(socketId, 'TIME_WAIT');
|
||||
this.schedule(this.stateValue.currentTimeMilliseconds + this.tcpTimeoutMilliseconds(), { kind: 'tcpTimeout', sessionId: socketId });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1996,6 +2061,7 @@ export class NetworkRuntimeEngine {
|
||||
remoteSequenceNumber: 0,
|
||||
lastAcknowledgedSequenceNumber: undefined,
|
||||
lastSentSegment: undefined,
|
||||
closingWaitsLeft: undefined,
|
||||
receiveAssembly: new Uint8Array(),
|
||||
receivedMessages: [],
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
type RuntimeOutput,
|
||||
type ScheduledEventKind,
|
||||
} from '../src/index.js';
|
||||
import { device, letFramesArrive, link, pc, startedEngine, uuid } from './fixtures.js';
|
||||
import { device, letFramesArrive, link, loseFirst, pc, startedEngine, uuid } from './fixtures.js';
|
||||
|
||||
const marker = (label: string): ScheduledEventKind => ({ kind: 'parityMarker', label });
|
||||
|
||||
@@ -995,6 +995,58 @@ describe('TCP sockets', () => {
|
||||
expect(engine.state.packetTraces.filter((trace) => trace.detail?.includes('TCP SYN retransmission') === true)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('answers a repeated SYN with its SYN+ACK once more: a lost SYN+ACK costs one attempt (Java would close)', () => {
|
||||
const { engine, clientId, serverId } = pair(0x17f1, 0x17f2, '10.26.0');
|
||||
loseFirst(engine, serverId, (segment) => segment.flags === (TCP_FLAG.synchronize | TCP_FLAG.acknowledgement));
|
||||
const listenerId = engine.openTcpServerSocket(serverId, 80)!;
|
||||
const clientSocketId = engine.openTcpClientSocket(clientId, '10.26.0.2', 80)!;
|
||||
|
||||
expect(engine.connectTcpWithResult(clientSocketId)).toBe('connected');
|
||||
// The second SYN leaves a timeout after the first was handed down, its SYN+ACK comes a cable later each way.
|
||||
expect(engine.state.currentTimeMilliseconds).toBe(3 * 2_750 + 2 * 55);
|
||||
letFramesArrive(engine);
|
||||
const acceptedSocketId = engine.state.tcpAcceptedSocketIdsByListenerId.get(listenerId)![0]!;
|
||||
expect(engine.state.socketsById.get(acceptedSocketId)?.tcpState).toBe('ESTABLISHED');
|
||||
expect(engine.state.packetTraces.filter((trace) => trace.detail?.startsWith('TCP SYN+ACK') === true).map((trace) => trace.detail)).toEqual([
|
||||
'TCP SYN+ACK',
|
||||
'TCP SYN+ACK retransmission attempt 2',
|
||||
]);
|
||||
});
|
||||
|
||||
it("gives a handshake up after one timeout and closes with a FIN, sent once, then five waits", () => {
|
||||
const { engine, clientId, serverId } = pair(0x17f5, 0x17f6, '10.27.0');
|
||||
const listenerId = engine.openTcpServerSocket(serverId, 80)!;
|
||||
// A SYN from the client's address, but no socket there: the SYN+ACK finds no one.
|
||||
engine.sendIPv4Packet(clientId, {
|
||||
identity: engine.allocatePacketIdentity(),
|
||||
senderIPAddress: '10.27.0.1',
|
||||
receiverIPAddress: '10.27.0.2',
|
||||
timeToLive: 64,
|
||||
protocol: 'tcp',
|
||||
payload: {
|
||||
kind: 'tcp',
|
||||
segment: { sourcePort: 5000, destinationPort: 80, sequenceNumber: 1, acknowledgementNumber: 0, flags: TCP_FLAG.synchronize, payload: new Uint8Array() },
|
||||
},
|
||||
});
|
||||
engine.handle({ type: 'advance', toMilliseconds: 60_000 });
|
||||
|
||||
const acceptedSocketId = engine.state.tcpAcceptedSocketIdsByListenerId.get(listenerId)![0]!;
|
||||
const states = engine.state.packetTraces
|
||||
.filter((trace) => trace.nodeId === serverId && trace.detail?.includes(' -> ') === true && !trace.detail.includes('LISTEN -> CLOSED') && !trace.detail.includes('CLOSED -> LISTEN'))
|
||||
.map((trace) => [trace.timeMilliseconds, trace.detail]);
|
||||
// The SYN arrives after ARP there and back: 165 ms.
|
||||
expect(states).toEqual([
|
||||
[165, 'TCP LISTEN -> SYN_RCVD'],
|
||||
[165 + 3 * 2_750, 'TCP SYN_RCVD -> FIN_WAIT_1'],
|
||||
[165 + 3 * 2_750 + 5 * 3 * 2_750, 'TCP FIN_WAIT_1 -> CLOSED'],
|
||||
]);
|
||||
expect(engine.state.socketsById.get(acceptedSocketId)?.tcpState).toBe('CLOSED');
|
||||
expect(engine.state.packetTraces.filter((trace) => trace.nodeId === serverId && trace.layer === 'transport' && trace.detail?.startsWith('TCP ') === true && trace.operation !== 'accepted' && trace.operation !== 'dropped').map((trace) => trace.detail)).toEqual([
|
||||
'TCP SYN+ACK',
|
||||
'TCP FIN',
|
||||
]);
|
||||
});
|
||||
|
||||
it('drops a SYN+ACK at a listening port with no connection of its own', () => {
|
||||
const { engine, clientId, serverId } = pair(0x1781, 0x1782, '10.21.0');
|
||||
const listenerId = engine.openTcpServerSocket(serverId, 7000)!;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
import {
|
||||
deviceConfiguration,
|
||||
type EthernetFrame,
|
||||
type TcpSegment,
|
||||
NetworkRuntimeEngine,
|
||||
portSnapshot,
|
||||
topologySnapshot,
|
||||
@@ -62,3 +64,23 @@ export function letFramesArrive(engine: NetworkRuntimeEngine): void {
|
||||
engine.handle({ type: 'advance', toMilliseconds: Math.min(...deadlines) });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The first frame the node sends whose TCP segment matches is lost, as with
|
||||
* TcpRun's loseFirst: it leaves (the sender's card shows it) and never arrives.
|
||||
*/
|
||||
export function loseFirst(engine: NetworkRuntimeEngine, nodeId: Id, which: (segment: TcpSegment) => boolean): void {
|
||||
type Depart = (fromNodeId: Id, portId: Id, endpoint: unknown, frame: EthernetFrame) => void;
|
||||
const target = engine as unknown as { departEthernetFrame: Depart };
|
||||
const depart = target.departEthernetFrame.bind(engine);
|
||||
let lost = false;
|
||||
target.departEthernetFrame = (fromNodeId, portId, endpoint, frame) => {
|
||||
depart(fromNodeId, portId, endpoint, frame);
|
||||
const payload = frame.payload.kind === 'ipv4' ? frame.payload.packet.payload : undefined;
|
||||
if (lost || fromNodeId !== nodeId || payload?.kind !== 'tcp' || !which(payload.segment)) return;
|
||||
lost = true;
|
||||
engine.state.pendingEvents = engine.state.pendingEvents.filter(
|
||||
(event) => !(event.kind.kind === 'ethernetLinkDelivery' && event.kind.frame.identity === frame.identity),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"frames": {
|
||||
"00000000-0000-0000-0000-000000000101": [
|
||||
{
|
||||
"time": 850,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"time": 852,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"arp": 2,
|
||||
"senderIp": "192.168.0.10",
|
||||
"targetIp": "192.168.0.20"
|
||||
},
|
||||
{
|
||||
"time": 1079,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 61346,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "S",
|
||||
"sequence": 2000000,
|
||||
"acknowledgement": 0
|
||||
},
|
||||
{
|
||||
"time": 1081,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 61346,
|
||||
"payload": null,
|
||||
"tcpFlags": "SA",
|
||||
"sequence": 1000000,
|
||||
"acknowledgement": 2000001
|
||||
},
|
||||
{
|
||||
"time": 1305,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 61346,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "A",
|
||||
"sequence": 2000001,
|
||||
"acknowledgement": 1000001
|
||||
}
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000102": [
|
||||
{
|
||||
"time": 729,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"time": 963,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"arp": 2,
|
||||
"senderIp": "192.168.0.10",
|
||||
"targetIp": "192.168.0.20"
|
||||
},
|
||||
{
|
||||
"time": 966,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 61346,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "S",
|
||||
"sequence": 2000000,
|
||||
"acknowledgement": 0
|
||||
},
|
||||
{
|
||||
"time": 1193,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 61346,
|
||||
"payload": null,
|
||||
"tcpFlags": "SA",
|
||||
"sequence": 1000000,
|
||||
"acknowledgement": 2000001
|
||||
},
|
||||
{
|
||||
"time": 1193,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 61346,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "A",
|
||||
"sequence": 2000001,
|
||||
"acknowledgement": 1000001
|
||||
},
|
||||
{
|
||||
"time": 1716,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 61346,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "F",
|
||||
"sequence": 2000001,
|
||||
"acknowledgement": 0
|
||||
}
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000103": [
|
||||
{
|
||||
"time": 850,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
}
|
||||
]
|
||||
},
|
||||
"events": [
|
||||
[
|
||||
214,
|
||||
"server oeffnen called"
|
||||
],
|
||||
[
|
||||
715,
|
||||
"client verbinden called"
|
||||
],
|
||||
[
|
||||
1216,
|
||||
"client verbinden returned"
|
||||
],
|
||||
[
|
||||
1317,
|
||||
"server oeffnen returned"
|
||||
],
|
||||
[
|
||||
1716,
|
||||
"client schliessen called"
|
||||
],
|
||||
[
|
||||
1716,
|
||||
"client schliessen returned"
|
||||
],
|
||||
[
|
||||
1716,
|
||||
"lost FIN"
|
||||
]
|
||||
],
|
||||
"states": {
|
||||
"client": [
|
||||
[
|
||||
715,
|
||||
"CLOSED"
|
||||
],
|
||||
[
|
||||
725,
|
||||
"SYN_SENT"
|
||||
],
|
||||
[
|
||||
1195,
|
||||
"ESTABLISHED"
|
||||
],
|
||||
[
|
||||
1717,
|
||||
"FIN_WAIT_1"
|
||||
]
|
||||
],
|
||||
"server": [
|
||||
[
|
||||
1317,
|
||||
"ESTABLISHED"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
{
|
||||
"frames": {
|
||||
"00000000-0000-0000-0000-000000000101": [
|
||||
{
|
||||
"time": 856,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"time": 858,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"arp": 2,
|
||||
"senderIp": "192.168.0.10",
|
||||
"targetIp": "192.168.0.20"
|
||||
},
|
||||
{
|
||||
"time": 1086,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 54938,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "S",
|
||||
"sequence": 2000000,
|
||||
"acknowledgement": 0
|
||||
},
|
||||
{
|
||||
"time": 1088,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 54938,
|
||||
"payload": null,
|
||||
"tcpFlags": "SA",
|
||||
"sequence": 1000000,
|
||||
"acknowledgement": 2000001
|
||||
},
|
||||
{
|
||||
"time": 1311,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 54938,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "A",
|
||||
"sequence": 2000001,
|
||||
"acknowledgement": 1000001
|
||||
},
|
||||
{
|
||||
"time": 1834,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 54938,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "F",
|
||||
"sequence": 2000001,
|
||||
"acknowledgement": 0
|
||||
},
|
||||
{
|
||||
"time": 1834,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 54938,
|
||||
"payload": null,
|
||||
"tcpFlags": "A",
|
||||
"sequence": 1000001,
|
||||
"acknowledgement": 2000002
|
||||
},
|
||||
{
|
||||
"time": 1890,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 54938,
|
||||
"payload": null,
|
||||
"tcpFlags": "F",
|
||||
"sequence": 1000001,
|
||||
"acknowledgement": 0
|
||||
}
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000102": [
|
||||
{
|
||||
"time": 734,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"time": 970,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"arp": 2,
|
||||
"senderIp": "192.168.0.10",
|
||||
"targetIp": "192.168.0.20"
|
||||
},
|
||||
{
|
||||
"time": 972,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 54938,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "S",
|
||||
"sequence": 2000000,
|
||||
"acknowledgement": 0
|
||||
},
|
||||
{
|
||||
"time": 1199,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 54938,
|
||||
"payload": null,
|
||||
"tcpFlags": "SA",
|
||||
"sequence": 1000000,
|
||||
"acknowledgement": 2000001
|
||||
},
|
||||
{
|
||||
"time": 1200,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 54938,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "A",
|
||||
"sequence": 2000001,
|
||||
"acknowledgement": 1000001
|
||||
},
|
||||
{
|
||||
"time": 1722,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 54938,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "F",
|
||||
"sequence": 2000001,
|
||||
"acknowledgement": 0
|
||||
},
|
||||
{
|
||||
"time": 1946,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 54938,
|
||||
"payload": null,
|
||||
"tcpFlags": "A",
|
||||
"sequence": 1000001,
|
||||
"acknowledgement": 2000002
|
||||
}
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000103": [
|
||||
{
|
||||
"time": 856,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
}
|
||||
]
|
||||
},
|
||||
"events": [
|
||||
[
|
||||
219,
|
||||
"server oeffnen called"
|
||||
],
|
||||
[
|
||||
720,
|
||||
"client verbinden called"
|
||||
],
|
||||
[
|
||||
1221,
|
||||
"client verbinden returned"
|
||||
],
|
||||
[
|
||||
1321,
|
||||
"server oeffnen returned"
|
||||
],
|
||||
[
|
||||
1721,
|
||||
"client schliessen called"
|
||||
],
|
||||
[
|
||||
1721,
|
||||
"client schliessen returned"
|
||||
],
|
||||
[
|
||||
1890,
|
||||
"lost FIN"
|
||||
]
|
||||
],
|
||||
"states": {
|
||||
"client": [
|
||||
[
|
||||
720,
|
||||
"CLOSED"
|
||||
],
|
||||
[
|
||||
731,
|
||||
"SYN_SENT"
|
||||
],
|
||||
[
|
||||
1201,
|
||||
"ESTABLISHED"
|
||||
],
|
||||
[
|
||||
1724,
|
||||
"FIN_WAIT_1"
|
||||
],
|
||||
[
|
||||
1947,
|
||||
"FIN_WAIT_2"
|
||||
]
|
||||
],
|
||||
"server": [
|
||||
[
|
||||
1322,
|
||||
"ESTABLISHED"
|
||||
],
|
||||
[
|
||||
1835,
|
||||
"LAST_ACK"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"frames": {
|
||||
"00000000-0000-0000-0000-000000000101": [
|
||||
{
|
||||
"time": 855,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"time": 856,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"arp": 2,
|
||||
"senderIp": "192.168.0.10",
|
||||
"targetIp": "192.168.0.20"
|
||||
},
|
||||
{
|
||||
"time": 1083,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 52633,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "S",
|
||||
"sequence": 2000000,
|
||||
"acknowledgement": 0
|
||||
},
|
||||
{
|
||||
"time": 1085,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 52633,
|
||||
"payload": null,
|
||||
"tcpFlags": "SA",
|
||||
"sequence": 1000000,
|
||||
"acknowledgement": 2000001
|
||||
},
|
||||
{
|
||||
"time": 17585,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 52633,
|
||||
"payload": null,
|
||||
"tcpFlags": "F",
|
||||
"sequence": 1000001,
|
||||
"acknowledgement": 0
|
||||
}
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000102": [
|
||||
{
|
||||
"time": 733,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"time": 968,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"arp": 2,
|
||||
"senderIp": "192.168.0.10",
|
||||
"targetIp": "192.168.0.20"
|
||||
},
|
||||
{
|
||||
"time": 970,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 52633,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "S",
|
||||
"sequence": 2000000,
|
||||
"acknowledgement": 0
|
||||
},
|
||||
{
|
||||
"time": 17697,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 52633,
|
||||
"payload": null,
|
||||
"tcpFlags": "F",
|
||||
"sequence": 1000001,
|
||||
"acknowledgement": 0
|
||||
}
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000103": [
|
||||
{
|
||||
"time": 855,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
}
|
||||
]
|
||||
},
|
||||
"events": [
|
||||
[
|
||||
218,
|
||||
"server oeffnen called"
|
||||
],
|
||||
[
|
||||
719,
|
||||
"client verbinden called"
|
||||
],
|
||||
[
|
||||
1085,
|
||||
"lost SYN+ACK"
|
||||
],
|
||||
[
|
||||
9025,
|
||||
"client verbinden failed TimeOutException"
|
||||
],
|
||||
[
|
||||
17632,
|
||||
"server oeffnen returned"
|
||||
]
|
||||
],
|
||||
"states": {
|
||||
"server": [
|
||||
[
|
||||
221,
|
||||
"LISTEN"
|
||||
],
|
||||
[
|
||||
1085,
|
||||
"SYN_RCVD"
|
||||
],
|
||||
[
|
||||
17585,
|
||||
"FIN_WAIT_1"
|
||||
]
|
||||
],
|
||||
"client": [
|
||||
[
|
||||
719,
|
||||
"CLOSED"
|
||||
],
|
||||
[
|
||||
731,
|
||||
"SYN_SENT"
|
||||
],
|
||||
[
|
||||
8980,
|
||||
"CLOSED"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,15 @@
|
||||
// client draws differs (Java's random, the engine's seed); it is compared as
|
||||
// "client". Times count from the client's SYN.
|
||||
//
|
||||
// The lost* stories lose one frame on its sender's card, as TcpRun's
|
||||
// loseFirst does in Java: the card shows it, no one receives it.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { Id, NetworkRuntimeEngine } from '../src/index.js';
|
||||
import { TCP_FLAG, type Id, type NetworkRuntimeEngine, type TcpSegment } from '../src/index.js';
|
||||
import { loseFirst } from './fixtures.js';
|
||||
import { GAP_TOLERANCE_MILLISECONDS, gaps, javaFrames, watchedEngine, type JavaFrame, type SeenFrame } from './javaRun.js';
|
||||
import type { Scenario } from './scenario.js';
|
||||
|
||||
@@ -39,12 +42,30 @@ interface JavaRun {
|
||||
|
||||
const text = (value: string) => new TextEncoder().encode(value);
|
||||
|
||||
/** The story as the engine plays it; the nodes whose socket stands for Java's "client" and "server". */
|
||||
/**
|
||||
* The story as the engine plays it; the node whose socket stands for Java's
|
||||
* "client". Java watched the server's socket from ESTABLISHED on, or, with
|
||||
* `serverListening`, from the start (LISTEN).
|
||||
*/
|
||||
interface Story {
|
||||
readonly client: Id;
|
||||
readonly serverListening?: boolean;
|
||||
readonly play: (engine: NetworkRuntimeEngine, advanceTo: (milliseconds: number) => void) => void;
|
||||
}
|
||||
|
||||
const isSynAck = (segment: TcpSegment) => (segment.flags & TCP_FLAG.synchronize) !== 0 && (segment.flags & TCP_FLAG.acknowledgement) !== 0;
|
||||
const isFin = (segment: TcpSegment) => (segment.flags & TCP_FLAG.finish) !== 0;
|
||||
|
||||
/** Connected, then the client closes half a second later; the server closes by itself. */
|
||||
function connectAndClose(engine: NetworkRuntimeEngine, advanceTo: (milliseconds: number) => void): void {
|
||||
engine.openTcpServerSocket(SERVER, 80);
|
||||
advanceTo(500);
|
||||
const client = engine.openTcpClientSocket(CLIENT, '192.168.0.10', 80)!;
|
||||
engine.connectTcpWithResult(client);
|
||||
advanceTo(engine.state.currentTimeMilliseconds + 500);
|
||||
engine.closeTcpSocket(client);
|
||||
}
|
||||
|
||||
const STORIES: Record<string, Story> = {
|
||||
exchange: {
|
||||
client: CLIENT,
|
||||
@@ -88,6 +109,30 @@ const STORIES: Record<string, Story> = {
|
||||
engine.setGlobalPacketLossEnabled(false);
|
||||
},
|
||||
},
|
||||
lostSynAck: {
|
||||
client: CLIENT,
|
||||
serverListening: true,
|
||||
play: (engine, advanceTo) => {
|
||||
loseFirst(engine, SERVER, isSynAck);
|
||||
engine.openTcpServerSocket(SERVER, 80);
|
||||
advanceTo(500);
|
||||
engine.connectTcpWithResult(engine.openTcpClientSocket(CLIENT, '192.168.0.10', 80)!);
|
||||
},
|
||||
},
|
||||
lostFin: {
|
||||
client: CLIENT,
|
||||
play: (engine, advanceTo) => {
|
||||
loseFirst(engine, CLIENT, isFin);
|
||||
connectAndClose(engine, advanceTo);
|
||||
},
|
||||
},
|
||||
lostLastFin: {
|
||||
client: CLIENT,
|
||||
play: (engine, advanceTo) => {
|
||||
loseFirst(engine, SERVER, isFin);
|
||||
connectAndClose(engine, advanceTo);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/** The states of the node's connection socket, from the engine's traces: when each was entered. */
|
||||
@@ -138,8 +183,21 @@ function normalized(frames: readonly SeenFrame[]): SeenFrame[] {
|
||||
* second and third), but its timeout branch sets closeSocket, and the loop
|
||||
* ends after the first: one SYN, and verbinden fails after one wait. The
|
||||
* engine sends the SYN three times, a wait of three round trips each.
|
||||
*
|
||||
* lostFin, lostLastFin: close waits at most five times (TCP_CLOSING_WAITS),
|
||||
* each ended by a segment or after the timeout, and then the socket is gone —
|
||||
* but its state stays FIN_WAIT_1, FIN_WAIT_2 or LAST_ACK. The engine shows
|
||||
* CLOSED after the last wait.
|
||||
*/
|
||||
const JAVA_TIMEOUT_MILLISECONDS = 3 * 2_750;
|
||||
const JAVA_CLOSING_WAITS = 5;
|
||||
|
||||
/** The socket's states, with CLOSED after Java's close loop: from `state`, `waits` more waits of the timeout. */
|
||||
function closedAfterWaits(states: [number, string][], state: string, waits: number): [number, string][] {
|
||||
const from = states.find(([, each]) => each === state)![0];
|
||||
return [...states, [from + waits * JAVA_TIMEOUT_MILLISECONDS, 'CLOSED']];
|
||||
}
|
||||
|
||||
const JAVA_BUGS: Record<string, (java: JavaRun) => JavaRun> = {
|
||||
closedPort: (java) => {
|
||||
const client = OTHER_CLIENT;
|
||||
@@ -157,6 +215,30 @@ const JAVA_BUGS: Record<string, (java: JavaRun) => JavaRun> = {
|
||||
},
|
||||
};
|
||||
},
|
||||
// The client's FIN is lost: its five waits pass without a segment.
|
||||
lostFin: (java) => ({ ...java, states: { ...java.states, client: closedAfterWaits(java.states.client!, 'FIN_WAIT_1', JAVA_CLOSING_WAITS) } }),
|
||||
// The server's FIN is lost: the client's first wait ended with the ACK, the server's five pass.
|
||||
lostLastFin: (java) => ({
|
||||
...java,
|
||||
states: {
|
||||
...java.states,
|
||||
client: closedAfterWaits(java.states.client!, 'FIN_WAIT_2', JAVA_CLOSING_WAITS - 1),
|
||||
server: closedAfterWaits(java.states.server!, 'LAST_ACK', JAVA_CLOSING_WAITS),
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* Where a story is compared only up to a time (Java's, from its start):
|
||||
* after it, Java follows its own error and the engine does not.
|
||||
*
|
||||
* lostSynAck: Java's client sends one SYN (closedPort above) and gives up a
|
||||
* timeout later; the engine's client sends it again then, the server answers
|
||||
* it with its SYN+ACK once more, and they connect (engine.test.ts). Compared
|
||||
* up to just before the second SYN.
|
||||
*/
|
||||
const JAVA_UNTIL: Record<string, (java: JavaRun) => number> = {
|
||||
lostSynAck: (java) => java.states.client!.find(([, state]) => state === 'SYN_SENT')![0] + JAVA_TIMEOUT_MILLISECONDS - GAP_TOLERANCE_MILLISECONDS,
|
||||
};
|
||||
|
||||
describe('TCP as Java FILIUS runs it', () => {
|
||||
@@ -167,6 +249,15 @@ describe('TCP as Java FILIUS runs it', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('cuts only where Java goes on with its error', () => {
|
||||
for (const [name, until] of Object.entries(JAVA_UNTIL)) {
|
||||
const java = JSON.parse(readFileSync(join(scenarioDirectory, `tcp-java-${name}.java.json`), 'utf8')) as JavaRun;
|
||||
const cut = until(java);
|
||||
const later = [...Object.values(java.frames).flat().map((frame) => frame.time), ...Object.values(java.states).flat().map(([time]) => time)];
|
||||
expect(later.some((time) => time >= cut), name).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'tcp-lan.scenario.json'), 'utf8')) as Scenario;
|
||||
|
||||
for (const [name, story] of Object.entries(STORIES)) {
|
||||
@@ -180,8 +271,14 @@ describe('TCP as Java FILIUS runs it', () => {
|
||||
story.play(engine, advanceTo);
|
||||
advanceTo(Math.max(...java.events.map(([time]) => time), ...Object.values(java.states).flatMap((each) => each.map(([time]) => time))) + 5_000);
|
||||
|
||||
const expected = normalized(javaFrames(java.frames));
|
||||
const actual = normalized(frames());
|
||||
// Times from the SYN; with JAVA_UNTIL, only what comes before the cut.
|
||||
const javaSyn = java.frames[story.client]!.find((frame) => frame.tcpFlags === 'S')!.time;
|
||||
const engineSyn = frames().find((frame) => / TCP .* S seq=/.test(frame.text))!.time;
|
||||
const until = (JAVA_UNTIL[name]?.(measured) ?? Number.POSITIVE_INFINITY) - javaSyn;
|
||||
const before = <T extends { time: number } | [number, string]>(items: T[]): T[] =>
|
||||
items.filter((item) => (Array.isArray(item) ? item[0] : item.time) < until);
|
||||
const expected = before(normalized(javaFrames(java.frames)));
|
||||
const actual = before(normalized(frames()));
|
||||
const expectedSteps = gaps(expected);
|
||||
const actualSteps = gaps(actual);
|
||||
for (let index = 0; index < Math.max(expectedSteps.length, actualSteps.length); index += 1) {
|
||||
@@ -191,14 +288,14 @@ describe('TCP as Java FILIUS runs it', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const javaSyn = java.frames[story.client]!.find((frame) => frame.tcpFlags === 'S')!.time;
|
||||
const engineSyn = frames().find((frame) => / TCP .* S seq=/.test(frame.text))!.time;
|
||||
const shift = (states: [number, string][], syn: number): [number, string][] => states.map(([time, state]) => [time - syn, state]);
|
||||
const shift = (states: [number, string][], syn: number): [number, string][] => before(states.map(([time, state]) => [time - syn, state]));
|
||||
// From SYN_SENT: Java's first CLOSED is when the socket was made.
|
||||
const javaClient = java.states.client!.filter(([, state], index) => !(index === 0 && state === 'CLOSED'));
|
||||
expectSameStates('client', shift(engineStates(engine, story.client, false), engineSyn), shift(javaClient, javaSyn));
|
||||
if (java.states.server !== undefined) {
|
||||
expectSameStates('server', shift(engineStates(engine, SERVER, true), engineSyn), shift(java.states.server, javaSyn));
|
||||
// A listening server from its first change: Java's first LISTEN is when it began to listen.
|
||||
const javaServer = story.serverListening === true ? java.states.server.slice(1) : java.states.server;
|
||||
expectSameStates('server', shift(engineStates(engine, SERVER, story.serverListening !== true), engineSyn), shift(javaServer, javaSyn));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -203,6 +203,17 @@ und Mitschnitt teilen `DhcpRun` und `TcpRun` in `ScenarioNetwork`.
|
||||
* `lostData`: verbunden, dann gehen alle Frames verloren
|
||||
(`Verbindung.setDrop`, wie der Paketverlust des Kerns), während der Client
|
||||
sendet; der Verlust endet, wenn `senden` scheitert.
|
||||
* `lostSynAck`: das erste SYN+ACK des Servers geht verloren.
|
||||
* `lostFin`: verbunden, der Client schließt; sein FIN geht verloren.
|
||||
* `lostLastFin`: verbunden, der Client schließt; das FIN des Servers geht
|
||||
verloren.
|
||||
|
||||
Einen einzelnen Frame verliert `loseFirst`: Per Reflection ersetzt es den
|
||||
Ausgangspuffer der Karte (`Port.ausgangsPuffer`) durch einen, der den ersten
|
||||
passenden Frame verwirft, statt ihn einzureihen. Das Kabel wartet am alten
|
||||
Puffer und wird dort einmal geweckt. Der Lauscher des Absenders sieht den
|
||||
Frame wie bei `setDrop` (er schreibt nach dem Einreihen); anders als dort
|
||||
belegt der Frame das Kabel nicht.
|
||||
|
||||
Gemessen (30.09.2026, 90 %, RTT 2750 ms), was Swift anders macht:
|
||||
|
||||
@@ -220,6 +231,14 @@ Gemessen (30.09.2026, 90 %, RTT 2750 ms), was Swift anders macht:
|
||||
`connectClientMode` `closeSocket`, und die Schleife über drei Versuche
|
||||
endet (`verbinden` scheitert nach 3 RTT). Der Server-Socket wiederholt sein
|
||||
SYN+ACK nie.
|
||||
* Nach einem verlorenen SYN+ACK gibt der Server nach 3 RTT auf und schickt aus
|
||||
`SYN_RCVD` ein FIN (`close`). In zwei von drei Läufen wartete er 6 RTT: Der
|
||||
Vergleich `vergangen > defaultTimeout()` liegt genau auf der Grenze.
|
||||
* Ein FIN geht einmal hinaus, nie wieder. Danach wartet `close` höchstens
|
||||
fünfmal 3 RTT; jedes ankommende Segment beendet eine Wartezeit. Dann ist
|
||||
der Socket weg, sein Zustand bleibt aber stehen (`FIN_WAIT_1`,
|
||||
`FIN_WAIT_2`, `LAST_ACK`). Die Gegenseite eines verlorenen FIN bleibt
|
||||
`ESTABLISHED`.
|
||||
* Startnummern aus einem Zähler über alle TCP-Sockets (1000000, 2000000, …),
|
||||
wie im Kern; der freie Port ist zufällig.
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ done
|
||||
|
||||
# TCP as Java FILIUS runs it: stories of socket calls on tcp-lan's network
|
||||
# (TcpRun); frames, calls and socket states, for the core's TCP comparison.
|
||||
for run in exchange:30000 closedPort:30000 lostData:45000; do
|
||||
for run in exchange:30000 closedPort:30000 lostData:45000 lostSynAck:70000 lostFin:60000 lostLastFin:60000; do
|
||||
story="${run%%:*}"
|
||||
output="$root/packages/core/test/scenarios/tcp-java-$story.java.json"
|
||||
rm -f "$output"
|
||||
|
||||
@@ -14,17 +14,31 @@
|
||||
// closedPort a client connects to port 81, where nothing listens
|
||||
// lostData connected, all frames are lost (Verbindung.setDrop) while
|
||||
// the client sends; loss ends when senden fails
|
||||
// lostSynAck the server's first SYN+ACK is lost
|
||||
// lostFin connected, the client closes; its first FIN is lost
|
||||
// lostLastFin connected, the client closes; the server's first FIN is lost
|
||||
//
|
||||
// A single frame is lost on its sender's card (loseFirst): the card's output
|
||||
// buffer drops it instead of queueing it. The packet log sees it on the
|
||||
// sender, as with setDrop; unlike setDrop, the cable does not carry it.
|
||||
|
||||
import filius.hardware.Port;
|
||||
import filius.hardware.Verbindung;
|
||||
import filius.hardware.knoten.InternetKnoten;
|
||||
import filius.software.netzzugangsschicht.EthernetFrame;
|
||||
import filius.software.transportschicht.ServerSocket;
|
||||
import filius.software.transportschicht.Socket;
|
||||
import filius.software.transportschicht.TCPSocket;
|
||||
import filius.software.transportschicht.TcpSegment;
|
||||
import filius.software.vermittlungsschicht.IpPaket;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class TcpRun {
|
||||
static final String SERVER = "00000000-0000-0000-0000-000000000101";
|
||||
@@ -85,6 +99,75 @@ public class TcpRun {
|
||||
return thread;
|
||||
}
|
||||
|
||||
/**
|
||||
* The node's card drops the first frame whose TCP segment matches, instead
|
||||
* of putting it into its output buffer. The cable waits on the buffer it
|
||||
* found: woken there once, it takes the new one.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static void loseFirst(String nodeId, String label, Predicate<TcpSegment> which) throws Exception {
|
||||
Port port = ((InternetKnoten) network.system(nodeId).getKnoten()).getNetzwerkInterfaces().get(0).getPort();
|
||||
Field field = Port.class.getDeclaredField("ausgangsPuffer");
|
||||
field.setAccessible(true);
|
||||
LinkedList<EthernetFrame> old = (LinkedList<EthernetFrame>) field.get(port);
|
||||
field.set(port, new LinkedList<EthernetFrame>() {
|
||||
boolean lost;
|
||||
|
||||
@Override
|
||||
public boolean add(EthernetFrame frame) {
|
||||
if (!lost && frame.getDaten() instanceof IpPaket
|
||||
&& ((IpPaket) frame.getDaten()).getSegment() instanceof TcpSegment
|
||||
&& which.test((TcpSegment) ((IpPaket) frame.getDaten()).getSegment())) {
|
||||
lost = true;
|
||||
event("lost " + label);
|
||||
return true;
|
||||
}
|
||||
return super.add(frame);
|
||||
}
|
||||
});
|
||||
synchronized (old) {
|
||||
old.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
/** The listener's socket that waits for a connection, as soon as there is one. */
|
||||
static void watchListening(String label, ServerSocket listener) {
|
||||
Thread thread = new Thread(() -> {
|
||||
try {
|
||||
Field field = ServerSocket.class.getDeclaredField("aktuellerSocket");
|
||||
field.setAccessible(true);
|
||||
while (field.get(listener) == null) Thread.sleep(1);
|
||||
watch(label, (Socket) field.get(listener));
|
||||
} catch (Exception e) {
|
||||
event("watch failed " + e.getClass().getSimpleName());
|
||||
}
|
||||
});
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
/** Connected, then the client closes; the server's socket closes by itself. */
|
||||
static void connectAndClose() throws Exception {
|
||||
server(() -> {
|
||||
ServerSocket listener = new ServerSocket(network.system(SERVER), 80, IpPaket.TCP, new ArrayList<>());
|
||||
Socket accepted = (Socket) call("server oeffnen", listener::oeffnen);
|
||||
watch("server", accepted);
|
||||
return null;
|
||||
});
|
||||
Thread.sleep(500);
|
||||
TCPSocket client = new TCPSocket(network.system(CLIENT), "192.168.0.10", 80);
|
||||
watch("client", client);
|
||||
call("client verbinden", () -> {
|
||||
client.verbinden();
|
||||
return null;
|
||||
});
|
||||
Thread.sleep(500);
|
||||
call("client schliessen", () -> {
|
||||
client.schliessen();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
static String repeat(String text, int count) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (int i = 0; i < count; i++) result.append(text);
|
||||
@@ -95,6 +178,19 @@ public class TcpRun {
|
||||
network = ScenarioNetwork.read(args[0]);
|
||||
String story = args[1];
|
||||
long duration = Long.parseLong(args[2]);
|
||||
switch (story) {
|
||||
case "lostSynAck":
|
||||
loseFirst(SERVER, "SYN+ACK", segment -> segment.isSyn() && segment.isAck());
|
||||
break;
|
||||
case "lostFin":
|
||||
loseFirst(CLIENT, "FIN", TcpSegment::isFin);
|
||||
break;
|
||||
case "lostLastFin":
|
||||
loseFirst(SERVER, "FIN", TcpSegment::isFin);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
network.start();
|
||||
Thread poller = new Thread(() -> {
|
||||
while (true) {
|
||||
@@ -182,6 +278,26 @@ public class TcpRun {
|
||||
event("loss off");
|
||||
break;
|
||||
}
|
||||
case "lostSynAck": {
|
||||
server(() -> {
|
||||
ServerSocket listener = new ServerSocket(network.system(SERVER), 80, IpPaket.TCP, new ArrayList<>());
|
||||
watchListening("server", listener);
|
||||
call("server oeffnen", listener::oeffnen);
|
||||
return null;
|
||||
});
|
||||
Thread.sleep(500);
|
||||
TCPSocket client = new TCPSocket(network.system(CLIENT), "192.168.0.10", 80);
|
||||
watch("client", client);
|
||||
call("client verbinden", () -> {
|
||||
client.verbinden();
|
||||
return null;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "lostFin":
|
||||
case "lostLastFin":
|
||||
connectAndClose();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("unknown story " + story);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user