From cbaf322759f4c4cf831fc605b4c6605bfc2e7b56 Mon Sep 17 00:00:00 2001 From: Nomads_Reach <144523850+NomadsReach@users.noreply.github.com> Date: Sat, 15 Aug 2026 22:51:50 -0400 Subject: [PATCH] Harden Protocol V2 test packet framing --- server/tests/test_networking_v2.py | 54 ++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/server/tests/test_networking_v2.py b/server/tests/test_networking_v2.py index 8d1c19b..b97191f 100644 --- a/server/tests/test_networking_v2.py +++ b/server/tests/test_networking_v2.py @@ -9,12 +9,19 @@ import pytest from server_core import PROTOCOL_VERSION, FalloutTogetherServer, states_share_interest +_RECV_BUFFERS: dict[socket.socket, bytes] = {} + + def recv_packet(sock: socket.socket, timeout: float = 2.0) -> dict: sock.settimeout(timeout) - data = b"" + data = _RECV_BUFFERS.get(sock, b"") while b"\n" not in data: - data += sock.recv(4096) - line, _ = data.split(b"\n", 1) + chunk = sock.recv(4096) + if not chunk: + raise ConnectionError("socket closed before a complete packet was received") + data += chunk + line, remainder = data.split(b"\n", 1) + _RECV_BUFFERS[sock] = remainder return json.loads(line.decode("utf-8")) @@ -22,12 +29,20 @@ def recv_until(sock: socket.socket, packet_type: str, timeout: float = 2.0) -> d deadline = time.monotonic() + timeout while time.monotonic() < deadline: remaining = max(0.01, deadline - time.monotonic()) - packet = recv_packet(sock, timeout=remaining) + try: + packet = recv_packet(sock, timeout=remaining) + except socket.timeout: + break if packet.get("type") == packet_type: return packet raise AssertionError(f"did not receive packet type {packet_type}") +def assert_no_packet_type(sock: socket.socket, packet_type: str, timeout: float = 0.2) -> None: + with pytest.raises(AssertionError): + recv_until(sock, packet_type, timeout=timeout) + + def send_packet(sock: socket.socket, packet: dict) -> None: sock.sendall(json.dumps(packet, separators=(",", ":")).encode() + b"\n") @@ -82,6 +97,18 @@ def test_interest_exact_cell_and_exterior_radius(): assert not states_share_interest(a, other_world) +def test_recv_packet_preserves_coalesced_lines(): + reader, writer = socket.socketpair() + try: + writer.sendall(b'{"type":"first"}\n{"type":"second"}\n') + assert recv_packet(reader)["type"] == "first" + assert recv_packet(reader)["type"] == "second" + finally: + _RECV_BUFFERS.pop(reader, None) + reader.close() + writer.close() + + def test_idle_transport_does_not_take_world_authority(): server = start_server() idle, _ = connect(server) @@ -103,8 +130,7 @@ def test_max_players_applies_to_activated_sessions_not_probes(): hello(first) second, _ = connect(server) send_packet(second, {"type": "hello", "protocolVersion": PROTOCOL_VERSION}) - ended = recv_packet(second) - assert ended["type"] == "sessionEnded" + ended = recv_until(second, "sessionEnded") assert ended["code"] == "server_full" first.close() second.close() @@ -115,8 +141,7 @@ def test_protocol_mismatch_is_rejected(): server = start_server() sock, _ = connect(server) send_packet(sock, {"type": "hello", "protocolVersion": PROTOCOL_VERSION + 1}) - ended = recv_packet(sock) - assert ended["type"] == "sessionEnded" + ended = recv_until(sock, "sessionEnded") assert ended["code"] == "protocol_mismatch" sock.close() server.stop() @@ -147,9 +172,7 @@ def test_transform_interest_filters_distant_peer(): send_packet(b, transform("00000020", 25000, 25000)) time.sleep(0.05) send_packet(a, transform("00000010", 0, 0)) - b.settimeout(0.2) - with pytest.raises(socket.timeout): - b.recv(4096) + assert_no_packet_type(b, "transform") assert server.get_stats()["transformPacketsInterestFiltered"] >= 1 a.close() b.close() @@ -174,9 +197,7 @@ def test_rejected_normal_teleport_is_corrected_and_never_relayed(): assert correction["x"] == 0.0 assert correction["cellId"] == "00000010" - b.settimeout(0.2) - with pytest.raises(socket.timeout): - b.recv(4096) + assert_no_packet_type(b, "transform") client = server._find_client_by_player_id(a_welcome["playerId"]) assert client is not None @@ -243,8 +264,7 @@ def test_oversized_unterminated_packet_closes_session(): server = start_server() sock, _ = connect(server) sock.sendall(b"x" * (64 * 1024 + 1)) - ended = recv_packet(sock) - assert ended["type"] == "sessionEnded" + ended = recv_until(sock, "sessionEnded") assert ended["code"] == "packet_too_large" sock.close() - server.stop() + server.stop() \ No newline at end of file