From cd3c6a432c6cda41c0a594cfd49e5083360b412a Mon Sep 17 00:00:00 2001 From: Julian Kiedaisch Date: Wed, 30 Sep 2026 11:08:43 +0000 Subject: [PATCH] test(java-oracle): TcpRun records TCP as Java FILIUS runs it Three stories of socket calls on tcp-lan's network (exchange, closedPort, lostData): every frame with TCP flags and numbers, when each call started, returned or failed, and each socket's states. Network building and the packet log move from DhcpRun to ScenarioNetwork. Measured: 3 RTT per attempt (8250 ms at 90 %), an orderly close after a failed send, TIME_WAIT of 3 RTT, the passive side closing by itself on FIN, one SYN only (Java's retry loop ends after the first timeout). Co-Authored-By: Claude Opus 5.5 --- .../scenarios/tcp-java-closedPort.java.json | 104 ++++ .../scenarios/tcp-java-exchange.java.json | 574 ++++++++++++++++++ .../scenarios/tcp-java-lostData.java.json | 368 +++++++++++ tools/java-oracle/README.md | 37 ++ tools/java-oracle/run.sh | 13 +- tools/java-oracle/src/DhcpRun.java | 398 +----------- tools/java-oracle/src/ScenarioNetwork.java | 418 +++++++++++++ tools/java-oracle/src/TcpRun.java | 213 +++++++ 8 files changed, 1740 insertions(+), 385 deletions(-) create mode 100644 packages/core/test/scenarios/tcp-java-closedPort.java.json create mode 100644 packages/core/test/scenarios/tcp-java-exchange.java.json create mode 100644 packages/core/test/scenarios/tcp-java-lostData.java.json create mode 100644 tools/java-oracle/src/ScenarioNetwork.java create mode 100644 tools/java-oracle/src/TcpRun.java diff --git a/packages/core/test/scenarios/tcp-java-closedPort.java.json b/packages/core/test/scenarios/tcp-java-closedPort.java.json new file mode 100644 index 0000000..6b024fd --- /dev/null +++ b/packages/core/test/scenarios/tcp-java-closedPort.java.json @@ -0,0 +1,104 @@ +{ + "frames": { + "00000000-0000-0000-0000-000000000101": [ + { + "time": 852, + "source": "02:00:00:00:01:13", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.30", + "targetIp": "192.168.0.10" + }, + { + "time": 854, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:13", + "arp": 2, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.30" + }, + { + "time": 1081, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.30", + "destinationIp": "192.168.0.10", + "protocol": 6, + "sourcePort": 62251, + "destinationPort": 81, + "payload": null, + "tcpFlags": "S", + "sequence": 1000000, + "acknowledgement": 0 + } + ], + "00000000-0000-0000-0000-000000000102": [ + { + "time": 852, + "source": "02:00:00:00:01:13", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.30", + "targetIp": "192.168.0.10" + } + ], + "00000000-0000-0000-0000-000000000103": [ + { + "time": 731, + "source": "02:00:00:00:01:13", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.30", + "targetIp": "192.168.0.10" + }, + { + "time": 965, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:13", + "arp": 2, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.30" + }, + { + "time": 968, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.30", + "destinationIp": "192.168.0.10", + "protocol": 6, + "sourcePort": 62251, + "destinationPort": 81, + "payload": null, + "tcpFlags": "S", + "sequence": 1000000, + "acknowledgement": 0 + } + ] + }, + "events": [ + [ + 715, + "client verbinden called" + ], + [ + 9020, + "client verbinden failed TimeOutException" + ] + ], + "states": { + "client": [ + [ + 714, + "CLOSED" + ], + [ + 728, + "SYN_SENT" + ], + [ + 8979, + "CLOSED" + ] + ] + } +} diff --git a/packages/core/test/scenarios/tcp-java-exchange.java.json b/packages/core/test/scenarios/tcp-java-exchange.java.json new file mode 100644 index 0000000..2fe65ad --- /dev/null +++ b/packages/core/test/scenarios/tcp-java-exchange.java.json @@ -0,0 +1,574 @@ +{ + "frames": { + "00000000-0000-0000-0000-000000000101": [ + { + "time": 857, + "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": 61961, + "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": 61961, + "payload": null, + "tcpFlags": "SA", + "sequence": 1000000, + "acknowledgement": 2000001 + }, + { + "time": 1312, + "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": 61961, + "destinationPort": 80, + "payload": null, + "tcpFlags": "A", + "sequence": 2000001, + "acknowledgement": 1000001 + }, + { + "time": 1367, + "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": 61961, + "destinationPort": 80, + "payload": "GET /", + "tcpFlags": "P", + "sequence": 2000001, + "acknowledgement": 0 + }, + { + "time": 1368, + "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": 61961, + "payload": null, + "tcpFlags": "A", + "sequence": 1000001, + "acknowledgement": 2000006 + }, + { + "time": 1423, + "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": 61961, + "payload": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "tcpFlags": "", + "sequence": 1000001, + "acknowledgement": 0 + }, + { + "time": 1646, + "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": 61961, + "destinationPort": 80, + "payload": null, + "tcpFlags": "A", + "sequence": 2000006, + "acknowledgement": 1001461 + }, + { + "time": 1647, + "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": 61961, + "payload": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "tcpFlags": "", + "sequence": 1001461, + "acknowledgement": 0 + }, + { + "time": 1869, + "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": 61961, + "destinationPort": 80, + "payload": null, + "tcpFlags": "A", + "sequence": 2000006, + "acknowledgement": 1002921 + }, + { + "time": 1870, + "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": 61961, + "payload": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "tcpFlags": "P", + "sequence": 1002921, + "acknowledgement": 0 + }, + { + "time": 2092, + "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": 61961, + "destinationPort": 80, + "payload": null, + "tcpFlags": "A", + "sequence": 2000006, + "acknowledgement": 1003001 + }, + { + "time": 2148, + "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": 61961, + "destinationPort": 80, + "payload": null, + "tcpFlags": "F", + "sequence": 2000006, + "acknowledgement": 0 + }, + { + "time": 2148, + "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": 61961, + "payload": null, + "tcpFlags": "A", + "sequence": 1003001, + "acknowledgement": 2000007 + }, + { + "time": 2204, + "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": 61961, + "payload": null, + "tcpFlags": "F", + "sequence": 1003001, + "acknowledgement": 0 + }, + { + "time": 2426, + "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": 61961, + "destinationPort": 80, + "payload": null, + "tcpFlags": "A", + "sequence": 2000007, + "acknowledgement": 1003002 + } + ], + "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": 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": 61961, + "destinationPort": 80, + "payload": null, + "tcpFlags": "S", + "sequence": 2000000, + "acknowledgement": 0 + }, + { + "time": 1200, + "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": 61961, + "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": 61961, + "destinationPort": 80, + "payload": null, + "tcpFlags": "A", + "sequence": 2000001, + "acknowledgement": 1000001 + }, + { + "time": 1256, + "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": 61961, + "destinationPort": 80, + "payload": "GET /", + "tcpFlags": "P", + "sequence": 2000001, + "acknowledgement": 0 + }, + { + "time": 1479, + "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": 61961, + "payload": null, + "tcpFlags": "A", + "sequence": 1000001, + "acknowledgement": 2000006 + }, + { + "time": 1535, + "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": 61961, + "payload": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "tcpFlags": "", + "sequence": 1000001, + "acknowledgement": 0 + }, + { + "time": 1535, + "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": 61961, + "destinationPort": 80, + "payload": null, + "tcpFlags": "A", + "sequence": 2000006, + "acknowledgement": 1001461 + }, + { + "time": 1758, + "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": 61961, + "payload": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "tcpFlags": "", + "sequence": 1001461, + "acknowledgement": 0 + }, + { + "time": 1758, + "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": 61961, + "destinationPort": 80, + "payload": null, + "tcpFlags": "A", + "sequence": 2000006, + "acknowledgement": 1002921 + }, + { + "time": 1981, + "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": 61961, + "payload": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "tcpFlags": "P", + "sequence": 1002921, + "acknowledgement": 0 + }, + { + "time": 1981, + "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": 61961, + "destinationPort": 80, + "payload": null, + "tcpFlags": "A", + "sequence": 2000006, + "acknowledgement": 1003001 + }, + { + "time": 2037, + "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": 61961, + "destinationPort": 80, + "payload": null, + "tcpFlags": "F", + "sequence": 2000006, + "acknowledgement": 0 + }, + { + "time": 2259, + "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": 61961, + "payload": null, + "tcpFlags": "A", + "sequence": 1003001, + "acknowledgement": 2000007 + }, + { + "time": 2315, + "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": 61961, + "payload": null, + "tcpFlags": "F", + "sequence": 1003001, + "acknowledgement": 0 + }, + { + "time": 2315, + "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": 61961, + "destinationPort": 80, + "payload": null, + "tcpFlags": "A", + "sequence": 2000007, + "acknowledgement": 1003002 + } + ], + "00000000-0000-0000-0000-000000000103": [ + { + "time": 857, + "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": [ + [ + 216, + "server oeffnen called" + ], + [ + 717, + "client verbinden called" + ], + [ + 1218, + "client verbinden returned" + ], + [ + 1218, + "client senden called" + ], + [ + 1319, + "server oeffnen returned" + ], + [ + 1319, + "server empfangen called" + ], + [ + 1368, + "server empfangen returned 5 characters" + ], + [ + 1368, + "server senden called" + ], + [ + 1479, + "client senden returned" + ], + [ + 1479, + "client empfangen called" + ], + [ + 1981, + "client empfangen returned 3000 characters" + ], + [ + 1981, + "client schliessen called" + ], + [ + 1981, + "client schliessen returned" + ], + [ + 2092, + "server senden returned" + ] + ], + "states": { + "client": [ + [ + 717, + "CLOSED" + ], + [ + 729, + "SYN_SENT" + ], + [ + 1202, + "ESTABLISHED" + ], + [ + 1982, + "FIN_WAIT_1" + ], + [ + 2260, + "FIN_WAIT_2" + ], + [ + 2315, + "TIME_WAIT" + ], + [ + 10565, + "CLOSED" + ] + ], + "server": [ + [ + 1319, + "ESTABLISHED" + ], + [ + 2148, + "LAST_ACK" + ], + [ + 2428, + "CLOSED" + ] + ] + } +} diff --git a/packages/core/test/scenarios/tcp-java-lostData.java.json b/packages/core/test/scenarios/tcp-java-lostData.java.json new file mode 100644 index 0000000..52f911d --- /dev/null +++ b/packages/core/test/scenarios/tcp-java-lostData.java.json @@ -0,0 +1,368 @@ +{ + "frames": { + "00000000-0000-0000-0000-000000000101": [ + { + "time": 854, + "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": 855, + "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": 61897, + "destinationPort": 80, + "payload": null, + "tcpFlags": "S", + "sequence": 2000000, + "acknowledgement": 0 + }, + { + "time": 1084, + "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": 61897, + "payload": null, + "tcpFlags": "SA", + "sequence": 1000000, + "acknowledgement": 2000001 + }, + { + "time": 1307, + "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": 61897, + "destinationPort": 80, + "payload": null, + "tcpFlags": "A", + "sequence": 2000001, + "acknowledgement": 1000001 + }, + { + "time": 26579, + "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": 61897, + "destinationPort": 80, + "payload": null, + "tcpFlags": "F", + "sequence": 2000009, + "acknowledgement": 0 + }, + { + "time": 26579, + "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": 61897, + "payload": null, + "tcpFlags": "A", + "sequence": 1000001, + "acknowledgement": 2000010 + }, + { + "time": 26635, + "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": 61897, + "payload": null, + "tcpFlags": "F", + "sequence": 1000001, + "acknowledgement": 0 + }, + { + "time": 26858, + "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": 61897, + "destinationPort": 80, + "payload": null, + "tcpFlags": "A", + "sequence": 2000010, + "acknowledgement": 1000002 + } + ], + "00000000-0000-0000-0000-000000000102": [ + { + "time": 731, + "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": 967, + "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": 969, + "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": 61897, + "destinationPort": 80, + "payload": null, + "tcpFlags": "S", + "sequence": 2000000, + "acknowledgement": 0 + }, + { + "time": 1196, + "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": 61897, + "payload": null, + "tcpFlags": "SA", + "sequence": 1000000, + "acknowledgement": 2000001 + }, + { + "time": 1196, + "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": 61897, + "destinationPort": 80, + "payload": null, + "tcpFlags": "A", + "sequence": 2000001, + "acknowledgement": 1000001 + }, + { + "time": 1717, + "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": 61897, + "destinationPort": 80, + "payload": "verloren", + "tcpFlags": "P", + "sequence": 2000001, + "acknowledgement": 0 + }, + { + "time": 9967, + "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": 61897, + "destinationPort": 80, + "payload": "verloren", + "tcpFlags": "P", + "sequence": 2000001, + "acknowledgement": 0 + }, + { + "time": 18217, + "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": 61897, + "destinationPort": 80, + "payload": "verloren", + "tcpFlags": "P", + "sequence": 2000001, + "acknowledgement": 0 + }, + { + "time": 26467, + "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": 61897, + "destinationPort": 80, + "payload": null, + "tcpFlags": "F", + "sequence": 2000009, + "acknowledgement": 0 + }, + { + "time": 26691, + "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": 61897, + "payload": null, + "tcpFlags": "A", + "sequence": 1000001, + "acknowledgement": 2000010 + }, + { + "time": 26746, + "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": 61897, + "payload": null, + "tcpFlags": "F", + "sequence": 1000001, + "acknowledgement": 0 + }, + { + "time": 26747, + "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": 61897, + "destinationPort": 80, + "payload": null, + "tcpFlags": "A", + "sequence": 2000010, + "acknowledgement": 1000002 + } + ], + "00000000-0000-0000-0000-000000000103": [ + { + "time": 854, + "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": [ + [ + 215, + "server oeffnen called" + ], + [ + 715, + "client verbinden called" + ], + [ + 1216, + "client verbinden returned" + ], + [ + 1317, + "server oeffnen returned" + ], + [ + 1716, + "loss on" + ], + [ + 1716, + "client senden called" + ], + [ + 26467, + "client senden failed TimeOutException" + ], + [ + 26467, + "loss off" + ] + ], + "states": { + "client": [ + [ + 715, + "CLOSED" + ], + [ + 727, + "SYN_SENT" + ], + [ + 1197, + "ESTABLISHED" + ], + [ + 26469, + "FIN_WAIT_1" + ], + [ + 26692, + "FIN_WAIT_2" + ], + [ + 26747, + "TIME_WAIT" + ], + [ + 34997, + "CLOSED" + ] + ], + "server": [ + [ + 1317, + "ESTABLISHED" + ], + [ + 26580, + "LAST_ACK" + ], + [ + 26858, + "CLOSED" + ] + ] + } +} diff --git a/tools/java-oracle/README.md b/tools/java-oracle/README.md index 57f73c9..173192b 100644 --- a/tools/java-oracle/README.md +++ b/tools/java-oracle/README.md @@ -185,3 +185,40 @@ eine Kabellänge später. Filius' Mitschnitt selbst ist dabei nicht ganz zuverlässig: Starten zwei Clients zugleich, fehlt auf der Karte des zweiten dessen eigenes DISCOVER (vier von vier Läufen von `dhcp-lan`; der Server empfängt es). + +## TCP zur Laufzeit + +`TcpRun` spielt auf dem Netz von `tcp-lan` (Server 192.168.0.10, Clients .20 +und .30) einen festen Ablauf von Socket-Aufrufen ab, wie Filius' Anwendungen +sie machen, den Server in einem eigenen Thread. Es schreibt nach +`tcp-java-.java.json` die Frames jeder Karte (mit TCP-Flags, +Sequenz- und Bestätigungsnummer), wann jeder Aufruf begann, zurückkam oder +scheiterte, und jeden Zustand jedes Sockets (alle 2 ms abgefragt). Netzbau +und Mitschnitt teilen `DhcpRun` und `TcpRun` in `ScenarioNetwork`. + +* `exchange`: Verbindung zu Port 80, `GET /`, Antwort 3000 × `x` (drei + Segmente), der Client schließt, der Server eine Sekunde nach + `CLOSE_WAIT`. +* `closedPort`: Verbindung zu Port 81, auf dem niemand lauscht. +* `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. + +Gemessen (30.09.2026, 90 %, RTT 2750 ms), was Swift anders macht: + +* Jeder Versuch wartet 3 RTT (8250 ms): Daten gehen dreimal hinaus (1720, + 9970, 18220 ms), `senden` scheitert 8250 ms nach dem dritten. +* Scheitert `senden`, schließt Java ordentlich: FIN, ACK, FIN, ACK. Swift + setzt den Socket ohne FIN auf CLOSED. +* `TIME_WAIT` dauert 3 RTT (8250 ms; oder bis zum nächsten Segment), nicht + eine RTT. +* Die Gegenseite schließt von selbst: Kommt ein FIN, geht Javas + Socket-Thread aus `listen` in `close` und sendet sein FIN sofort + (`LAST_ACK` rund 60 ms nach dem FIN), nicht erst, wenn die Anwendung + `schliessen` aufruft. +* Der Client sendet nur **ein** SYN: Nach dem ersten Zeitablauf setzt + `connectClientMode` `closeSocket`, und die Schleife über drei Versuche + endet (`verbinden` scheitert nach 3 RTT). Der Server-Socket wiederholt sein + SYN+ACK nie. +* Startnummern aus einem Zähler über alle TCP-Sockets (1000000, 2000000, …), + wie im Kern; der freie Port ist zufällig. diff --git a/tools/java-oracle/run.sh b/tools/java-oracle/run.sh index 57b6e23..1a13d53 100755 --- a/tools/java-oracle/run.sh +++ b/tools/java-oracle/run.sh @@ -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/DhcpRun.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" # 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. @@ -119,3 +119,14 @@ for run in dhcp-lan:8000 dhcp-two-servers:8000 dhcp-conflict:25000 dhcp-no-serve "$root/packages/core/test/scenarios/$name.scenario.json" "${run##*:}" "$output" 2>&1 | grep -v 'logback\|^$' || true test -s "$output" 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 + story="${run%%:*}" + output="$root/packages/core/test/scenarios/tcp-java-$story.java.json" + rm -f "$output" + HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" TcpRun \ + "$root/packages/core/test/scenarios/tcp-lan.scenario.json" "$story" "${run##*:}" "$output" 2>&1 | grep -v 'logback\|^$' || true + test -s "$output" +done diff --git a/tools/java-oracle/src/DhcpRun.java b/tools/java-oracle/src/DhcpRun.java index df756f6..fd56d7a 100644 --- a/tools/java-oracle/src/DhcpRun.java +++ b/tools/java-oracle/src/DhcpRun.java @@ -1,396 +1,26 @@ -// Runs a core scenario's network in Java FILIUS and records what DHCP really -// does: every frame each network card saw (FILIUS' own packet log, the -// Lauscher) and each host's address configuration at the end. +// Runs a core scenario's network in Java FILIUS (ScenarioNetwork) and records +// what DHCP really does: every frame each network card saw and each host's +// address configuration at the end. // // DhcpRun // -// The network is built from the scenario (nodes, links, devices, -// dhcpServers, dhcpClients; steps are ignored) the way the core reads it: a -// port's MAC is its `mac`, else the core's stableGeneratedMacAddress. The -// simulation then runs as the action mode starts it (every system software's -// starten()), in real time, for the given milliseconds. Threads make the -// times vary from run to run; the order of one client's messages does not. - -import filius.gui.GUIContainer; -import filius.hardware.Kabel; -import filius.hardware.NetzwerkInterface; -import filius.hardware.Port; -import filius.hardware.knoten.InternetKnoten; -import filius.hardware.knoten.Knoten; -import filius.hardware.knoten.LokalerKnoten; -import filius.hardware.knoten.Notebook; -import filius.hardware.knoten.Rechner; -import filius.hardware.knoten.Switch; -import filius.rahmenprogramm.nachrichten.Lauscher; -import filius.software.dhcp.DHCPServer; -import filius.software.netzzugangsschicht.EthernetFrame; -import filius.software.system.InternetKnotenBetriebssystem; -import filius.software.transportschicht.Segment; -import filius.software.vermittlungsschicht.ArpPaket; -import filius.software.vermittlungsschicht.IcmpPaket; -import filius.software.vermittlungsschicht.IpPaket; - -import java.io.FileOutputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.lang.reflect.Field; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; +// The scenario's steps are ignored: the network runs, in real time, for the +// given milliseconds. Threads make the times vary from run to run; the order +// of one client's messages does not. public class DhcpRun { public static void main(String[] args) throws Exception { - @SuppressWarnings("unchecked") - Map scenario = (Map) new JsonReader( - new String(Files.readAllBytes(Paths.get(args[0])), StandardCharsets.UTF_8)).value(); - long duration = Long.parseLong(args[1]); - - Map nodes = new LinkedHashMap<>(); - Map ports = new HashMap<>(); - Map macByNode = new LinkedHashMap<>(); - for (Object each : list(scenario.get("nodes"))) { - Map node = map(each); - String id = (String) node.get("id"); - String kind = (String) node.get("kind"); - List nodePorts = list(node.get("ports")); - Knoten knoten; - switch (kind) { - case "pc": - knoten = new Rechner(); - break; - case "notebook": - knoten = new Notebook(); - break; - case "networkSwitch": - knoten = new Switch(); - break; - default: - throw new IllegalArgumentException("node kind " + kind + " is not built by this tool"); - } - knoten.setName(id); - if (knoten instanceof InternetKnoten) { - if (nodePorts.size() != 1) throw new IllegalArgumentException("a host has one port here"); - Map port = map(nodePorts.get(0)); - NetzwerkInterface nic = ((InternetKnoten) knoten).getNetzwerkInterfaces().get(0); - String mac = port.containsKey("mac") ? (String) port.get("mac") : stableMac((String) port.get("id")); - nic.setMac(mac); - ports.put((String) port.get("id"), nic.getPort()); - macByNode.put(id, mac); - } else { - LinkedList switchPorts = ((LokalerKnoten) knoten).getAnschluesse(); - for (int index = 0; index < nodePorts.size(); index++) { - ports.put((String) map(nodePorts.get(index)).get("id"), switchPorts.get(index)); - } - } - nodes.put(id, knoten); - } - for (Map.Entry entry : map(scenario.getOrDefault("devices", new HashMap<>())).entrySet()) { - Map device = map(entry.getValue()); - NetzwerkInterface nic = ((InternetKnoten) nodes.get(entry.getKey())).getNetzwerkInterfaces().get(0); - nic.setIp((String) device.get("ip")); - nic.setSubnetzMaske((String) device.get("mask")); - nic.setGateway((String) device.getOrDefault("gateway", "")); - nic.setDns((String) device.getOrDefault("dns", "")); - } - for (Object each : list(scenario.get("links"))) { - Map link = map(each); - Kabel cable = new Kabel(); - cable.setAnschluesse(new Port[] { ports.get(link.get("sourcePort")), ports.get(link.get("targetPort")) }); - } - List servers = new ArrayList<>(); - for (Map.Entry entry : map(scenario.getOrDefault("dhcpServers", new HashMap<>())).entrySet()) { - Map config = map(entry.getValue()); - DHCPServer server = system(nodes.get(entry.getKey())).getDHCPServer(); - server.setUntergrenze((String) config.getOrDefault("lower", "0.0.0.0")); - server.setObergrenze((String) config.getOrDefault("upper", "0.0.0.0")); - server.setGatewayip((String) config.getOrDefault("gateway", "0.0.0.0")); - server.setDnsserverip((String) config.getOrDefault("dns", "0.0.0.0")); - server.setOwnSettings(Boolean.TRUE.equals(config.get("own"))); - for (Object assignment : list(config.getOrDefault("staticAssignments", new ArrayList<>()))) { - server.addStaticAssignment((String) list(assignment).get(0), (String) list(assignment).get(1)); - } - server.setAktiv(Boolean.TRUE.equals(config.get("active"))); - servers.add(entry.getKey()); - } - for (Object each : list(scenario.getOrDefault("dhcpClients", new ArrayList<>()))) { - system(nodes.get((String) each)).setDHCPKonfiguration(true); - } - - // Switches and DHCP servers first: FILIUS' client waits for every active - // server to have started (waitUntilDhcpServerStarted, through the GUI's - // node list, which this tool has not got). - // FILIUS' window exists before a simulation starts; created lazily and - // unsynchronized, the first clients to ask for it would race. - GUIContainer.getGUIContainer(); - long start = System.currentTimeMillis(); - for (Map.Entry entry : nodes.entrySet()) { - if (entry.getValue() instanceof Switch || servers.contains(entry.getKey())) entry.getValue().getSystemSoftware().starten(); - } - Thread.sleep(200); - for (Map.Entry entry : nodes.entrySet()) { - if (!(entry.getValue() instanceof Switch || servers.contains(entry.getKey()))) entry.getValue().getSystemSoftware().starten(); - } - Thread.sleep(duration); - - Field field = Lauscher.class.getDeclaredField("datenEinheiten"); - field.setAccessible(true); - @SuppressWarnings("unchecked") - Map> log = (Map>) field.get(Lauscher.getLauscher()); - - Json out = new Json(); + ScenarioNetwork network = ScenarioNetwork.read(args[0]); + network.start(); + Thread.sleep(Long.parseLong(args[1])); + ScenarioNetwork.Json out = new ScenarioNetwork.Json(); out.open('{'); - out.key("frames").open('{'); - for (Map.Entry entry : macByNode.entrySet()) { - out.key(entry.getKey()).open('['); - LinkedList frames = log.get(entry.getValue()); - if (frames != null) { - synchronized (frames) { - for (Object[] stamped : frames) frame(out, ((Long) stamped[0]) - start, (EthernetFrame) stamped[1]); - } - } - out.close(']'); - } + network.writeFrames(out); + network.writeDevices(out); out.close('}'); - out.key("devices").open('{'); - for (Map.Entry entry : nodes.entrySet()) { - if (!(entry.getValue() instanceof InternetKnoten)) continue; - NetzwerkInterface nic = ((InternetKnoten) entry.getValue()).getNetzwerkInterfaces().get(0); - out.key(entry.getKey()).open('[').string(nic.getIp()).string(nic.getSubnetzMaske()).string(nic.getGateway()) - .string(nic.getDns()).close(']'); - } - out.close('}'); - out.close('}'); - - for (Knoten knoten : nodes.values()) { - try { - knoten.getSystemSoftware().beenden(); - } catch (Exception e) {} - } - try (Writer writer = new OutputStreamWriter(new FileOutputStream(args[2]), StandardCharsets.UTF_8)) { - writer.write(out.toString()); - writer.write('\n'); - } + network.stop(); + ScenarioNetwork.write(args[2], out); System.exit(0); } - - private static void frame(Json out, long time, EthernetFrame frame) { - out.open('{').key("time").number(time).key("source").string(frame.getQuellMacAdresse()).key("destination") - .string(frame.getZielMacAdresse()); - Object data = frame.getDaten(); - if (data instanceof ArpPaket) { - ArpPaket arp = (ArpPaket) data; - out.key("arp").number(arp.getOperation()).key("senderIp").string(arp.getSenderIP()).key("targetIp") - .string(arp.getTargetIP()); - } else if (data instanceof IcmpPaket) { - out.key("icmp").number(((IcmpPaket) data).getIcmpType()); - } else if (data instanceof IpPaket) { - IpPaket ip = (IpPaket) data; - Segment segment = ip.getSegment(); - out.key("sourceIp").string(ip.getSender()).key("destinationIp").string(ip.getEmpfaenger()).key("protocol") - .number(ip.getProtocol()).key("sourcePort").number(segment.getQuellPort()).key("destinationPort") - .number(segment.getZielPort()).key("payload").string(segment.getDaten()); - } - out.close('}'); - } - - private static InternetKnotenBetriebssystem system(Knoten knoten) { - return (InternetKnotenBetriebssystem) knoten.getSystemSoftware(); - } - - /** The core's stableGeneratedMacAddress (packages/core/src/model/node.ts). */ - static String stableMac(String portId) { - String compact = portId.toUpperCase().replace("-", ""); - String suffix = compact.substring(compact.length() - 10); - StringBuilder mac = new StringBuilder("02"); - for (int offset = 0; offset < suffix.length(); offset += 2) mac.append(':').append(suffix, offset, offset + 2); - return mac.toString(); - } - - @SuppressWarnings("unchecked") - private static Map map(Object value) { - return (Map) value; - } - - @SuppressWarnings("unchecked") - private static List list(Object value) { - return (List) value; - } - - /** Just enough JSON for a scenario: objects, arrays, strings, numbers, booleans, null. */ - static final class JsonReader { - private final String text; - private int position; - - JsonReader(String text) { - this.text = text; - } - - Object value() { - skip(); - char c = text.charAt(position); - if (c == '{') { - Map result = new TreeMap<>(); - position++; - skip(); - if (text.charAt(position) == '}') { - position++; - return result; - } - for (;;) { - skip(); - String key = (String) value(); - skip(); - expect(':'); - result.put(key, value()); - skip(); - if (text.charAt(position++) == '}') return result; - } - } - if (c == '[') { - List result = new ArrayList<>(); - position++; - skip(); - if (text.charAt(position) == ']') { - position++; - return result; - } - for (;;) { - result.add(value()); - skip(); - if (text.charAt(position++) == ']') return result; - } - } - if (c == '"') { - StringBuilder result = new StringBuilder(); - position++; - for (char each = text.charAt(position++); each != '"'; each = text.charAt(position++)) { - if (each == '\\') { - char escaped = text.charAt(position++); - switch (escaped) { - case 'n': - result.append('\n'); - break; - case 't': - result.append('\t'); - break; - case 'u': - result.append((char) Integer.parseInt(text.substring(position, position + 4), 16)); - position += 4; - break; - default: - result.append(escaped); - } - } else { - result.append(each); - } - } - return result.toString(); - } - if (text.startsWith("true", position)) { - position += 4; - return Boolean.TRUE; - } - if (text.startsWith("false", position)) { - position += 5; - return Boolean.FALSE; - } - if (text.startsWith("null", position)) { - position += 4; - return null; - } - int begin = position; - while (position < text.length() && "+-.eE0123456789".indexOf(text.charAt(position)) >= 0) position++; - return Double.parseDouble(text.substring(begin, position)); - } - - private void skip() { - while (position < text.length() && Character.isWhitespace(text.charAt(position))) position++; - } - - private void expect(char c) { - if (text.charAt(position++) != c) throw new IllegalArgumentException("expected " + c + " at " + (position - 1)); - } - } - - /** A JSON writer with commas and indentation. */ - static final class Json { - private final StringBuilder text = new StringBuilder(); - private final LinkedList first = new LinkedList<>(); - private boolean afterKey; - - private void separate() { - if (afterKey) { - afterKey = false; - return; - } - if (first.isEmpty()) return; - if (!first.peek()) text.append(','); - first.pop(); - first.push(false); - text.append('\n'); - for (int i = 0; i < first.size(); i++) text.append(" "); - } - - Json open(char bracket) { - separate(); - text.append(bracket); - first.push(true); - return this; - } - - Json close(char bracket) { - boolean empty = first.pop(); - if (!empty) { - text.append('\n'); - for (int i = 0; i < first.size(); i++) text.append(" "); - } - text.append(bracket); - return this; - } - - Json key(String key) { - separate(); - quote(key); - text.append(": "); - afterKey = true; - return this; - } - - Json string(String value) { - separate(); - if (value == null) text.append("null"); - else quote(value); - return this; - } - - Json number(long value) { - separate(); - text.append(value); - return this; - } - - private void quote(String value) { - text.append('"'); - for (char c : value.toCharArray()) { - if (c == '"' || c == '\\') text.append('\\').append(c); - else if (c == '\n') text.append("\\n"); - else if (c < 0x20) text.append(String.format("\\u%04x", (int) c)); - else text.append(c); - } - text.append('"'); - } - - @Override - public String toString() { - return text.toString(); - } - } } diff --git a/tools/java-oracle/src/ScenarioNetwork.java b/tools/java-oracle/src/ScenarioNetwork.java new file mode 100644 index 0000000..46b27be --- /dev/null +++ b/tools/java-oracle/src/ScenarioNetwork.java @@ -0,0 +1,418 @@ +// A core scenario's network (packages/core/test/scenarios/*.scenario.json: +// nodes, links, devices, dhcpServers, dhcpClients) built in Java FILIUS the +// way the core reads it: a port's MAC is its `mac`, else the core's +// stableGeneratedMacAddress. Started as the action mode starts it (every +// system software's starten()); afterwards every frame each network card saw +// (FILIUS' own packet log, the Lauscher) and each host's addresses. Shared by +// DhcpRun and TcpRun. + +import filius.gui.GUIContainer; +import filius.hardware.Kabel; +import filius.hardware.NetzwerkInterface; +import filius.hardware.Port; +import filius.hardware.knoten.InternetKnoten; +import filius.hardware.knoten.Knoten; +import filius.hardware.knoten.LokalerKnoten; +import filius.hardware.knoten.Notebook; +import filius.hardware.knoten.Rechner; +import filius.hardware.knoten.Switch; +import filius.rahmenprogramm.nachrichten.Lauscher; +import filius.software.dhcp.DHCPServer; +import filius.software.netzzugangsschicht.EthernetFrame; +import filius.software.system.InternetKnotenBetriebssystem; +import filius.software.transportschicht.Segment; +import filius.software.transportschicht.TcpSegment; +import filius.software.vermittlungsschicht.ArpPaket; +import filius.software.vermittlungsschicht.IcmpPaket; +import filius.software.vermittlungsschicht.IpPaket; + +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +public class ScenarioNetwork { + final Map nodes = new LinkedHashMap<>(); + final Map macByNode = new LinkedHashMap<>(); + final List servers = new ArrayList<>(); + long start; + + @SuppressWarnings("unchecked") + static ScenarioNetwork read(String path) throws Exception { + Map scenario = (Map) new JsonReader( + new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8)).value(); + ScenarioNetwork network = new ScenarioNetwork(); + network.build(scenario); + return network; + } + + private void build(Map scenario) { + Map ports = new HashMap<>(); + for (Object each : list(scenario.get("nodes"))) { + Map node = map(each); + String id = (String) node.get("id"); + String kind = (String) node.get("kind"); + List nodePorts = list(node.get("ports")); + Knoten knoten; + switch (kind) { + case "pc": + knoten = new Rechner(); + break; + case "notebook": + knoten = new Notebook(); + break; + case "networkSwitch": + knoten = new Switch(); + break; + default: + throw new IllegalArgumentException("node kind " + kind + " is not built by this tool"); + } + knoten.setName(id); + if (knoten instanceof InternetKnoten) { + if (nodePorts.size() != 1) throw new IllegalArgumentException("a host has one port here"); + Map port = map(nodePorts.get(0)); + NetzwerkInterface nic = ((InternetKnoten) knoten).getNetzwerkInterfaces().get(0); + String mac = port.containsKey("mac") ? (String) port.get("mac") : stableMac((String) port.get("id")); + nic.setMac(mac); + ports.put((String) port.get("id"), nic.getPort()); + macByNode.put(id, mac); + } else { + LinkedList switchPorts = ((LokalerKnoten) knoten).getAnschluesse(); + for (int index = 0; index < nodePorts.size(); index++) { + ports.put((String) map(nodePorts.get(index)).get("id"), switchPorts.get(index)); + } + } + nodes.put(id, knoten); + } + for (Map.Entry entry : map(scenario.getOrDefault("devices", new HashMap<>())).entrySet()) { + Map device = map(entry.getValue()); + NetzwerkInterface nic = ((InternetKnoten) nodes.get(entry.getKey())).getNetzwerkInterfaces().get(0); + nic.setIp((String) device.get("ip")); + nic.setSubnetzMaske((String) device.get("mask")); + nic.setGateway((String) device.getOrDefault("gateway", "")); + nic.setDns((String) device.getOrDefault("dns", "")); + } + for (Object each : list(scenario.get("links"))) { + Map link = map(each); + Kabel cable = new Kabel(); + cable.setAnschluesse(new Port[] { ports.get(link.get("sourcePort")), ports.get(link.get("targetPort")) }); + } + for (Map.Entry entry : map(scenario.getOrDefault("dhcpServers", new HashMap<>())).entrySet()) { + Map config = map(entry.getValue()); + DHCPServer server = system(nodes.get(entry.getKey())).getDHCPServer(); + server.setUntergrenze((String) config.getOrDefault("lower", "0.0.0.0")); + server.setObergrenze((String) config.getOrDefault("upper", "0.0.0.0")); + server.setGatewayip((String) config.getOrDefault("gateway", "0.0.0.0")); + server.setDnsserverip((String) config.getOrDefault("dns", "0.0.0.0")); + server.setOwnSettings(Boolean.TRUE.equals(config.get("own"))); + for (Object assignment : list(config.getOrDefault("staticAssignments", new ArrayList<>()))) { + server.addStaticAssignment((String) list(assignment).get(0), (String) list(assignment).get(1)); + } + server.setAktiv(Boolean.TRUE.equals(config.get("active"))); + servers.add(entry.getKey()); + } + for (Object each : list(scenario.getOrDefault("dhcpClients", new ArrayList<>()))) { + system(nodes.get((String) each)).setDHCPKonfiguration(true); + } + + } + + /** Starts the simulation; times in the log count from here. */ + void start() throws InterruptedException { + // Switches and DHCP servers first: FILIUS' client waits for every active + // server to have started (waitUntilDhcpServerStarted, through the GUI's + // node list, which this tool has not got). + // FILIUS' window exists before a simulation starts; created lazily and + // unsynchronized, the first clients to ask for it would race. + GUIContainer.getGUIContainer(); + start = System.currentTimeMillis(); + for (Map.Entry entry : nodes.entrySet()) { + if (entry.getValue() instanceof Switch || servers.contains(entry.getKey())) entry.getValue().getSystemSoftware().starten(); + } + Thread.sleep(200); + for (Map.Entry entry : nodes.entrySet()) { + if (!(entry.getValue() instanceof Switch || servers.contains(entry.getKey()))) entry.getValue().getSystemSoftware().starten(); + } + } + + InternetKnotenBetriebssystem system(String nodeId) { + return (InternetKnotenBetriebssystem) nodes.get(nodeId).getSystemSoftware(); + } + + /** Every frame each host's card saw, by node. */ + void writeFrames(Json out) throws Exception { + Field field = Lauscher.class.getDeclaredField("datenEinheiten"); + field.setAccessible(true); + @SuppressWarnings("unchecked") + Map> log = (Map>) field.get(Lauscher.getLauscher()); + out.key("frames").open('{'); + for (Map.Entry entry : macByNode.entrySet()) { + out.key(entry.getKey()).open('['); + LinkedList frames = log.get(entry.getValue()); + if (frames != null) { + synchronized (frames) { + for (Object[] stamped : frames) frame(out, ((Long) stamped[0]) - start, (EthernetFrame) stamped[1]); + } + } + out.close(']'); + } + out.close('}'); + } + + /** Each host's address, mask, gateway and DNS server. */ + void writeDevices(Json out) { + out.key("devices").open('{'); + for (Map.Entry entry : nodes.entrySet()) { + if (!(entry.getValue() instanceof InternetKnoten)) continue; + NetzwerkInterface nic = ((InternetKnoten) entry.getValue()).getNetzwerkInterfaces().get(0); + out.key(entry.getKey()).open('[').string(nic.getIp()).string(nic.getSubnetzMaske()).string(nic.getGateway()) + .string(nic.getDns()).close(']'); + } + out.close('}'); + } + + void stop() { + for (Knoten knoten : nodes.values()) { + try { + knoten.getSystemSoftware().beenden(); + } catch (Exception e) {} + } + } + + static void write(String path, Json out) throws Exception { + try (Writer writer = new OutputStreamWriter(new FileOutputStream(path), StandardCharsets.UTF_8)) { + writer.write(out.toString()); + writer.write('\n'); + } + } + + private static void frame(Json out, long time, EthernetFrame frame) { + out.open('{').key("time").number(time).key("source").string(frame.getQuellMacAdresse()).key("destination") + .string(frame.getZielMacAdresse()); + Object data = frame.getDaten(); + if (data instanceof ArpPaket) { + ArpPaket arp = (ArpPaket) data; + out.key("arp").number(arp.getOperation()).key("senderIp").string(arp.getSenderIP()).key("targetIp") + .string(arp.getTargetIP()); + } else if (data instanceof IcmpPaket) { + out.key("icmp").number(((IcmpPaket) data).getIcmpType()); + } else if (data instanceof IpPaket) { + IpPaket ip = (IpPaket) data; + Segment segment = ip.getSegment(); + out.key("sourceIp").string(ip.getSender()).key("destinationIp").string(ip.getEmpfaenger()).key("protocol") + .number(ip.getProtocol()).key("sourcePort").number(segment.getQuellPort()).key("destinationPort") + .number(segment.getZielPort()).key("payload").string(segment.getDaten()); + if (segment instanceof TcpSegment) { + TcpSegment tcp = (TcpSegment) segment; + String flags = (tcp.isSyn() ? "S" : "") + (tcp.isAck() ? "A" : "") + (tcp.isFin() ? "F" : "") + + (tcp.isPush() ? "P" : "") + (tcp.isRst() ? "R" : ""); + out.key("tcpFlags").string(flags).key("sequence").number(tcp.getSeqNummer()).key("acknowledgement") + .number(tcp.getAckNummer()); + } + } + out.close('}'); + } + + private static InternetKnotenBetriebssystem system(Knoten knoten) { + return (InternetKnotenBetriebssystem) knoten.getSystemSoftware(); + } + + /** The core's stableGeneratedMacAddress (packages/core/src/model/node.ts). */ + static String stableMac(String portId) { + String compact = portId.toUpperCase().replace("-", ""); + String suffix = compact.substring(compact.length() - 10); + StringBuilder mac = new StringBuilder("02"); + for (int offset = 0; offset < suffix.length(); offset += 2) mac.append(':').append(suffix, offset, offset + 2); + return mac.toString(); + } + + @SuppressWarnings("unchecked") + private static Map map(Object value) { + return (Map) value; + } + + @SuppressWarnings("unchecked") + private static List list(Object value) { + return (List) value; + } + + /** Just enough JSON for a scenario: objects, arrays, strings, numbers, booleans, null. */ + static final class JsonReader { + private final String text; + private int position; + + JsonReader(String text) { + this.text = text; + } + + Object value() { + skip(); + char c = text.charAt(position); + if (c == '{') { + Map result = new TreeMap<>(); + position++; + skip(); + if (text.charAt(position) == '}') { + position++; + return result; + } + for (;;) { + skip(); + String key = (String) value(); + skip(); + expect(':'); + result.put(key, value()); + skip(); + if (text.charAt(position++) == '}') return result; + } + } + if (c == '[') { + List result = new ArrayList<>(); + position++; + skip(); + if (text.charAt(position) == ']') { + position++; + return result; + } + for (;;) { + result.add(value()); + skip(); + if (text.charAt(position++) == ']') return result; + } + } + if (c == '"') { + StringBuilder result = new StringBuilder(); + position++; + for (char each = text.charAt(position++); each != '"'; each = text.charAt(position++)) { + if (each == '\\') { + char escaped = text.charAt(position++); + switch (escaped) { + case 'n': + result.append('\n'); + break; + case 't': + result.append('\t'); + break; + case 'u': + result.append((char) Integer.parseInt(text.substring(position, position + 4), 16)); + position += 4; + break; + default: + result.append(escaped); + } + } else { + result.append(each); + } + } + return result.toString(); + } + if (text.startsWith("true", position)) { + position += 4; + return Boolean.TRUE; + } + if (text.startsWith("false", position)) { + position += 5; + return Boolean.FALSE; + } + if (text.startsWith("null", position)) { + position += 4; + return null; + } + int begin = position; + while (position < text.length() && "+-.eE0123456789".indexOf(text.charAt(position)) >= 0) position++; + return Double.parseDouble(text.substring(begin, position)); + } + + private void skip() { + while (position < text.length() && Character.isWhitespace(text.charAt(position))) position++; + } + + private void expect(char c) { + if (text.charAt(position++) != c) throw new IllegalArgumentException("expected " + c + " at " + (position - 1)); + } + } + + /** A JSON writer with commas and indentation. */ + static final class Json { + private final StringBuilder text = new StringBuilder(); + private final LinkedList first = new LinkedList<>(); + private boolean afterKey; + + private void separate() { + if (afterKey) { + afterKey = false; + return; + } + if (first.isEmpty()) return; + if (!first.peek()) text.append(','); + first.pop(); + first.push(false); + text.append('\n'); + for (int i = 0; i < first.size(); i++) text.append(" "); + } + + Json open(char bracket) { + separate(); + text.append(bracket); + first.push(true); + return this; + } + + Json close(char bracket) { + boolean empty = first.pop(); + if (!empty) { + text.append('\n'); + for (int i = 0; i < first.size(); i++) text.append(" "); + } + text.append(bracket); + return this; + } + + Json key(String key) { + separate(); + quote(key); + text.append(": "); + afterKey = true; + return this; + } + + Json string(String value) { + separate(); + if (value == null) text.append("null"); + else quote(value); + return this; + } + + Json number(long value) { + separate(); + text.append(value); + return this; + } + + private void quote(String value) { + text.append('"'); + for (char c : value.toCharArray()) { + if (c == '"' || c == '\\') text.append('\\').append(c); + else if (c == '\n') text.append("\\n"); + else if (c < 0x20) text.append(String.format("\\u%04x", (int) c)); + else text.append(c); + } + text.append('"'); + } + + @Override + public String toString() { + return text.toString(); + } + } +} diff --git a/tools/java-oracle/src/TcpRun.java b/tools/java-oracle/src/TcpRun.java new file mode 100644 index 0000000..e50322b --- /dev/null +++ b/tools/java-oracle/src/TcpRun.java @@ -0,0 +1,213 @@ +// Runs a story of TCP calls on a core scenario's network in Java FILIUS +// (ScenarioNetwork) and records what really happens: every frame each card +// saw, when each call started, returned or failed, and every state each +// socket went through (polled every few milliseconds). +// +// TcpRun +// +// The stories use the network of tcp-lan (server 192.168.0.10, clients +// 192.168.0.20 and .30) and run the calls as FILIUS' applications do, the +// server's in a thread of its own: +// exchange the client connects to port 80, sends "GET /", the server +// answers with 3000 x (three segments); the client closes, +// the server closes a second after it saw CLOSE_WAIT +// 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 + +import filius.hardware.Verbindung; +import filius.software.transportschicht.ServerSocket; +import filius.software.transportschicht.Socket; +import filius.software.transportschicht.TCPSocket; +import filius.software.vermittlungsschicht.IpPaket; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TcpRun { + static final String SERVER = "00000000-0000-0000-0000-000000000101"; + static final String CLIENT = "00000000-0000-0000-0000-000000000102"; + static final String OTHER_CLIENT = "00000000-0000-0000-0000-000000000103"; + + static ScenarioNetwork network; + static final List events = new ArrayList<>(); + static final Map> states = new LinkedHashMap<>(); + static final Map watched = new LinkedHashMap<>(); + + static synchronized void event(String what) { + events.add(new Object[] { System.currentTimeMillis() - network.start, what }); + } + + static synchronized void watch(String label, Socket socket) { + watched.put(label, (TCPSocket) socket); + states.put(label, new ArrayList<>()); + poll(); + } + + static synchronized void poll() { + for (Map.Entry entry : watched.entrySet()) { + List seen = states.get(entry.getKey()); + String state = entry.getValue().getStateAsString(); + if (seen.isEmpty() || !seen.get(seen.size() - 1)[1].equals(state)) { + seen.add(new Object[] { System.currentTimeMillis() - network.start, state }); + } + } + } + + interface Call { + Object run() throws Exception; + } + + /** A call with its start and its end (result or exception) in the events. */ + static Object call(String label, Call call) { + event(label + " called"); + try { + Object result = call.run(); + event(label + " returned" + (result instanceof String ? " " + ((String) result).length() + " characters" : "")); + return result; + } catch (Exception e) { + event(label + " failed " + e.getClass().getSimpleName()); + return null; + } + } + + static Thread server(Call body) { + Thread thread = new Thread(() -> { + try { + body.run(); + } catch (Exception e) { + event("server failed " + e.getClass().getSimpleName()); + } + }); + thread.start(); + return thread; + } + + static String repeat(String text, int count) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < count; i++) result.append(text); + return result.toString(); + } + + public static void main(String[] args) throws Exception { + network = ScenarioNetwork.read(args[0]); + String story = args[1]; + long duration = Long.parseLong(args[2]); + network.start(); + Thread poller = new Thread(() -> { + while (true) { + poll(); + try { + Thread.sleep(2); + } catch (InterruptedException e) { + return; + } + } + }); + poller.setDaemon(true); + poller.start(); + long end = network.start + duration; + + switch (story) { + case "exchange": { + server(() -> { + ServerSocket listener = new ServerSocket(network.system(SERVER), 80, IpPaket.TCP, new ArrayList<>()); + Socket accepted = (Socket) call("server oeffnen", listener::oeffnen); + watch("server", accepted); + call("server empfangen", accepted::empfangen); + call("server senden", () -> { + accepted.senden(repeat("x", 3000)); + return null; + }); + while (!((TCPSocket) accepted).getStateAsString().equals("CLOSE_WAIT")) Thread.sleep(2); + Thread.sleep(1000); + call("server schliessen", () -> { + accepted.schliessen(); + return null; + }); + 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; + }); + call("client senden", () -> { + client.senden("GET /"); + return null; + }); + call("client empfangen", client::empfangen); + call("client schliessen", () -> { + client.schliessen(); + return null; + }); + break; + } + case "closedPort": { + Thread.sleep(500); + TCPSocket client = new TCPSocket(network.system(OTHER_CLIENT), "192.168.0.10", 81); + watch("client", client); + call("client verbinden", () -> { + client.verbinden(); + return null; + }); + break; + } + case "lostData": { + 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); + event("loss on"); + Verbindung.setDrop(true); + call("client senden", () -> { + client.senden("verloren"); + return null; + }); + Verbindung.setDrop(false); + event("loss off"); + break; + } + default: + throw new IllegalArgumentException("unknown story " + story); + } + + long rest = end - System.currentTimeMillis(); + if (rest > 0) Thread.sleep(rest); + poll(); + + ScenarioNetwork.Json out = new ScenarioNetwork.Json(); + out.open('{'); + network.writeFrames(out); + synchronized (TcpRun.class) { + out.key("events").open('['); + for (Object[] each : events) out.open('[').number((Long) each[0]).string((String) each[1]).close(']'); + out.close(']'); + out.key("states").open('{'); + for (Map.Entry> entry : states.entrySet()) { + out.key(entry.getKey()).open('['); + for (Object[] each : entry.getValue()) out.open('[').number((Long) each[0]).string((String) each[1]).close(']'); + out.close(']'); + } + out.close('}'); + } + out.close('}'); + network.stop(); + ScenarioNetwork.write(args[3], out); + System.exit(0); + } +}