D1 phase 1: relay TCP core in btconsole.py (--relay mode)

The console emulator gains a relay/dispatcher mode for internet play (D1):
pods dial OUT to this process (NAT-friendly) instead of the console dialing
into each pod.  Legacy dial-out mode is untouched (verified: an unmodified
2-node mesh session still forms end-to-end through it).

--relay <consolePort> <egg> [--bind ADDR] [--udp-drop PCT], single
selectors-based event loop:
- console listener (consolePort): streams the NUL-line egg to each pod on
  connect; GLOBAL launch timer -- RunMission x2 to ALL pods at +20s/+4s after
  the LAST pod has its egg (fixes the per-pod skew of the legacy tool).
- game TCP listener (consolePort+1): envelope router {route i32, length u32}.
  First frame must be HELLO ('BTR1' + hostID validated against the egg
  [pilots] roster); PEER_UP/PEER_DOWN exchange; route>=2 unicast (route
  rewritten to sender), -1 broadcast-except-sender (the client sends each
  broadcast ONCE -- kills the N-1x upload duplication).  Frame cap 1600
  (NETWORKMANAGER_BUFFER_SIZE); protocol violations drop the connection.
- game UDP socket (consolePort+1/udp): endpoint learned per-datagram
  (NAT-rebind tolerant), HELLO->HELLO-ACK, verbatim forward by route,
  TCP-wrap fallback when the target's UDP endpoint is unknown; --udp-drop
  test hook for the robustness phase.

Verified: scratchpad/relay_stub_test.py (self-contained; spawns the relay +
stub clients) -- 24/24 checks pass: egg delivery, registration, PEER events,
unicast route-rewrite, broadcast fan-out (sender excluded), UDP ack/forward/
fallback, and all reject paths (bad magic, dup id, out-of-roster, oversize).

Plan: ~/.claude/plans/partitioned-snuggling-piglet.md (D1 relay + UDP).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-18 08:44:01 -05:00
co-authored by Claude Fable 5
parent e2c21c4db2
commit bf8c2ffaae
2 changed files with 637 additions and 1 deletions
+201
View File
@@ -0,0 +1,201 @@
#!/usr/bin/env python
"""relay_stub_test.py -- Phase-1 verification for btconsole.py --relay mode.
Spawns the relay with a 3-pilot test egg, then drives stub clients through the
whole protocol: console egg delivery, game HELLO/registration, PEER_UP/DOWN,
unicast + broadcast routing (route rewrite), UDP HELLO/ACK, UDP forward,
UDP->TCP fallback, and the reject paths (bad magic, dup id, out-of-roster,
oversize). Exits 0 with 'ALL PASS' or 1 with the first failure.
"""
import os
import socket
import struct
import subprocess
import sys
import time
HERE = os.path.dirname(os.path.abspath(__file__))
TOOLS = os.path.join(HERE, "..", "tools")
CONSOLE_PORT = 27500
GAME_PORT = CONSOLE_PORT + 1
ENV_TCP = "<iI"
ENV_UDP = "<iiI"
HELLO_MAGIC = 0x31525442
R_BCAST, R_HELLO, R_PEER_UP, R_PEER_DOWN, R_UDP_ACK = -1, -2, -3, -4, -5
fails = []
def check(cond, what):
tag = "ok" if cond else "FAIL"
print(f" [{tag}] {what}")
if not cond:
fails.append(what)
def recv_exact(sock, n, timeout=5.0):
sock.settimeout(timeout)
buf = b""
while len(buf) < n:
d = sock.recv(n - len(buf))
if not d:
raise ConnectionError("peer closed")
buf += d
return buf
def recv_env(sock, timeout=5.0):
route, length = struct.unpack(ENV_TCP, recv_exact(sock, 8, timeout))
payload = recv_exact(sock, length, timeout) if length else b""
return route, payload
def send_env(sock, route, payload):
sock.sendall(struct.pack(ENV_TCP, route, len(payload)) + payload)
def hello(sock, host_id):
send_env(sock, R_HELLO, struct.pack("<iI", HELLO_MAGIC, host_id))
def game_connect():
s = socket.create_connection(("127.0.0.1", GAME_PORT), timeout=5)
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
return s
def main():
egg = os.path.join(HERE, "relay_test.egg")
with open(egg, "w", newline="\n") as f:
f.write("[pilots]\npilot=10.99.0.1:1502\npilot=10.99.0.2:1502\n"
"pilot=10.99.0.3:1502\n[10.99.0.1:1502]\nhostType=0\n"
"[10.99.0.2:1502]\nhostType=0\n[10.99.0.3:1502]\nhostType=0\n")
relay = subprocess.Popen(
[sys.executable, os.path.join(TOOLS, "btconsole.py"),
"--relay", str(CONSOLE_PORT), egg, "--bind", "127.0.0.1"],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
time.sleep(1.0)
try:
run_tests()
finally:
relay.terminate()
try:
out, _ = relay.communicate(timeout=5)
print("---- relay log ----")
print(out)
except subprocess.TimeoutExpired:
relay.kill()
if fails:
print(f"\n{len(fails)} FAILURE(S):")
for f_ in fails:
print(f" - {f_}")
return 1
print("\nALL PASS")
return 0
def run_tests():
print("== console side: egg delivery ==")
con = socket.create_connection(("127.0.0.1", CONSOLE_PORT), timeout=5)
first = recv_exact(con, 1040)
clid, gid, fh, ts = struct.unpack_from("<iiii", first, 0)
mlen, mid, mfl, seq, total, this = struct.unpack_from("<IiIiii", first, 16)
check(clid == 0 and fh == 1 and mid == 3 and seq == 0,
f"egg chunk header (clid={clid} fh={fh} mid={mid} seq={seq})")
got = this
while got < total:
pkt = recv_exact(con, 1040)
got += struct.unpack_from("<IiIiii", pkt, 16)[5]
check(got == total, f"egg fully delivered ({got}/{total} bytes)")
print("== game side: HELLO/registration/PEER_UP ==")
a = game_connect()
hello(a, 2)
b = game_connect()
hello(b, 3)
route, payload = recv_env(a)
check(route == R_PEER_UP and struct.unpack("<i", payload)[0] == 3,
"A got PEER_UP(3) when B joined")
route, payload = recv_env(b)
check(route == R_PEER_UP and struct.unpack("<i", payload)[0] == 2,
"B (newcomer) got PEER_UP(2)")
print("== routing: unicast + route rewrite ==")
frame = b"\x00" * 16 + struct.pack("<IiI", 12, 99, 1) # minimal fake frame
send_env(a, 3, frame) # A -> unicast to 3
route, payload = recv_env(b)
check(route == 2 and payload == frame,
f"B received A's unicast with route rewritten to sender (route={route})")
print("== routing: broadcast fan-out ==")
c = game_connect()
hello(c, 4)
for s_, who in ((a, "A"), (b, "B")):
route, payload = recv_env(s_)
check(route == R_PEER_UP and struct.unpack("<i", payload)[0] == 4,
f"{who} got PEER_UP(4)")
for _ in range(2): # C gets 2 PEER_UPs
recv_env(c)
send_env(a, R_BCAST, frame) # A broadcasts ONCE
for s_, who in ((b, "B"), (c, "C")):
route, payload = recv_env(s_)
check(route == 2 and payload == frame, f"{who} received A's broadcast")
a.settimeout(0.5)
try:
a.recv(1)
check(False, "A must NOT receive its own broadcast")
except socket.timeout:
check(True, "A did not receive its own broadcast")
print("== UDP: HELLO/ACK + forward ==")
ua = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ua.bind(("127.0.0.1", 0))
ua.settimeout(5)
ua.sendto(struct.pack(ENV_UDP, R_HELLO, 2, 0), ("127.0.0.1", GAME_PORT))
ack, _ = ua.recvfrom(4096)
check(struct.unpack_from(ENV_UDP, ack)[0] == R_UDP_ACK, "UDP HELLO acked")
ub = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ub.bind(("127.0.0.1", 0))
ub.settimeout(5)
ub.sendto(struct.pack(ENV_UDP, R_HELLO, 3, 0), ("127.0.0.1", GAME_PORT))
ub.recvfrom(4096)
dg = struct.pack(ENV_UDP, 3, 2, 7) + frame # A -> unicast 3
ua.sendto(dg, ("127.0.0.1", GAME_PORT))
fwd, _ = ub.recvfrom(4096)
check(fwd == dg, "UDP unicast forwarded verbatim")
dg2 = struct.pack(ENV_UDP, R_BCAST, 2, 8) + frame # A -> broadcast
ua.sendto(dg2, ("127.0.0.1", GAME_PORT))
fwd2, _ = ub.recvfrom(4096)
check(fwd2 == dg2, "UDP broadcast reached B")
# C never sent UDP HELLO -> its copy must fall back to TCP.
route, payload = recv_env(c)
check(route == 2 and payload == frame,
"C (no UDP endpoint) got the broadcast via TCP fallback")
print("== reject paths ==")
bad = game_connect()
send_env(bad, R_HELLO, struct.pack("<iI", 0xDEAD, 5))
check(bad.recv(1) == b"", "bad HELLO magic -> connection dropped")
dup = game_connect()
hello(dup, 2)
check(dup.recv(1) == b"", "duplicate hostID -> dropped")
oor = game_connect()
hello(oor, 99)
check(oor.recv(1) == b"", "out-of-roster hostID -> dropped")
big = game_connect()
big.sendall(struct.pack(ENV_TCP, 3, 5000))
check(big.recv(1) == b"", "oversize frame -> dropped")
print("== PEER_DOWN ==")
c.close()
for s_, who in ((a, "A"), (b, "B")):
s_.settimeout(5)
route, payload = recv_env(s_)
check(route == R_PEER_DOWN and struct.unpack("<i", payload)[0] == 4,
f"{who} got PEER_DOWN(4)")
if __name__ == "__main__":
sys.exit(main())
+436 -1
View File
@@ -28,11 +28,38 @@ AcknowledgeEggFile back on this socket. The console must STAY CONNECTED (a
disconnect trips the pod's console-loss path, which also closes the game
listener -- an engine bug).
Usage:
Usage (legacy dial-out console -- the pod-authentic direction):
python btconsole.py <egg-file> <host:port> [<host:port> ...]
Example (two instances on one box, -net 1501 / -net 1601):
python btconsole.py MP.EGG 127.0.0.1:1501 127.0.0.1:1601
RELAY MODE (D1 -- internet play; pods dial OUT to this process):
python btconsole.py --relay <consolePort> <egg-file> [--bind ADDR] [--udp-drop PCT]
Example:
python btconsole.py --relay 1500 MP_RELAY.EGG
The relay listens on <consolePort>/tcp (console protocol: streams the egg to each
pod that connects, sends the LAUNCH pair once ALL pods have the egg), on
<consolePort>+1/tcp (the GAME relay: envelope-framed frame router, see
RelayEnvelope below), and on <consolePort>+1/udp (the unreliable channel
forwarder). Pods run with BT_RELAY=<host>:<consolePort> and a unique
BT_SELF=<their [pilots] entry>.
RelayEnvelope (game TCP): { int32 route; uint32 length; } + length payload bytes
route >= 2 : client->relay: unicast to that hostID
relay->client: the sender's hostID (frames also self-identify
via NetworkPacketHeader.fromHost)
route == -1: broadcast to every registered pod except the sender
route == -2: HELLO (payload: int32 magic 'BTR1', int32 myHostID)
route == -3: PEER_UP (payload: int32 hostID) relay->client only
route == -4: PEER_DOWN (payload: int32 hostID) relay->client only
UDP envelope: { int32 route; int32 fromHost; uint32 seq; } + frame
route as above; -2 = HELLO/keepalive (no payload) -> relay answers -5
(HELLO-ACK). The relay learns each pod's public endpoint from every
inbound datagram and forwards verbatim; unknown target endpoint falls
back to wrapping the frame onto the target's game TCP connection.
"""
import random
import selectors
import socket
import struct
import sys
@@ -144,7 +171,415 @@ def serve_pod(hostport, egg_bytes):
time.sleep(60)
#############################################################################
# RELAY MODE (D1)
#############################################################################
ENV_FMT_TCP = "<iI" # route, length
ENV_FMT_UDP = "<iiI" # route, fromHost, seq
ENV_TCP_SIZE = 8
ENV_UDP_SIZE = 12
ROUTE_BCAST = -1
ROUTE_HELLO = -2
ROUTE_PEER_UP = -3
ROUTE_PEER_DOWN = -4
ROUTE_UDP_ACK = -5
HELLO_MAGIC = 0x31525442 # 'BTR1' little-endian
FRAME_CAP = 1600 # NETWORKMANAGER_BUFFER_SIZE (NETWORK.h:89)
FIRST_GAME_HOST_ID = 2 # FirstLegalHostID(1) == the console; pods 2..N+1
STATS_PERIOD = 10.0
def parse_egg_roster(egg_path):
"""Count the [pilots] pilot= entries -> expected hostIDs 2..N+1 in egg order.
The relay's ONLY game knowledge; it never parses game frames."""
entries = []
in_pilots = False
for raw in open(egg_path, "rb").read().replace(b"\r\n", b"\n").split(b"\n"):
line = raw.strip()
if line.startswith(b"["):
in_pilots = (line.lower() == b"[pilots]")
continue
if in_pilots and line.lower().startswith(b"pilot="):
entries.append(line.split(b"=", 1)[1].decode("ascii", "replace"))
return entries
class RelayGameConn:
"""One accepted game-TCP connection: ACCEPTED -> (HELLO) REGISTERED -> GONE."""
def __init__(self, sock, addr):
self.sock = sock
self.addr = addr
self.buf = b""
self.host_id = None # None until HELLO registers it
def name(self):
hid = self.host_id if self.host_id is not None else "?"
return f"game[{self.addr[0]}:{self.addr[1]} host={hid}]"
class RelayConsoleConn:
"""One accepted console connection (legacy raw protocol, relay-terminated)."""
def __init__(self, sock, addr):
self.sock = sock
self.addr = addr
self.egg_sent = False
class Relay:
def __init__(self, console_port, egg_path, bind_addr, udp_drop_pct):
self.console_port = console_port
self.game_port = console_port + 1
self.bind_addr = bind_addr
self.udp_drop_pct = udp_drop_pct
self.egg_bytes = egg_wire_bytes(open(egg_path, "rb").read())
self.roster = parse_egg_roster(egg_path)
self.expected_ids = set(range(FIRST_GAME_HOST_ID,
FIRST_GAME_HOST_ID + len(self.roster)))
self.sel = selectors.DefaultSelector()
self.console_conns = []
self.game_conns = [] # RelayGameConn (incl. unregistered)
self.by_host = {} # hostID -> RelayGameConn
self.udp_endpoint = {} # hostID -> (addr, port)
self.udp_sock = None
self.launch_at = None
self.launches_sent = 0
self.stats = {"tcp_rx": 0, "tcp_tx": 0, "udp_rx": 0, "udp_tx": 0,
"udp_dropped": 0, "udp_tcp_fallback": 0}
self.stats_at = time.time() + STATS_PERIOD
# ---------------- lifecycle ----------------
def run(self):
print(f"[relay] roster: {len(self.roster)} pilot(s) -> hostIDs "
f"{sorted(self.expected_ids)}: {self.roster}", flush=True)
con_l = self._listener(self.console_port)
game_l = self._listener(self.game_port)
self.udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.udp_sock.bind((self.bind_addr, self.game_port))
self.udp_sock.setblocking(False)
self.sel.register(con_l, selectors.EVENT_READ, ("con_listen", None))
self.sel.register(game_l, selectors.EVENT_READ, ("game_listen", None))
self.sel.register(self.udp_sock, selectors.EVENT_READ, ("udp", None))
print(f"[relay] console {self.bind_addr}:{self.console_port} | game tcp/udp "
f"{self.bind_addr}:{self.game_port} | udp-drop {self.udp_drop_pct}%",
flush=True)
while True:
for key, _ in self.sel.select(timeout=0.5):
kind, obj = key.data
if kind == "con_listen":
self._accept_console(key.fileobj)
elif kind == "game_listen":
self._accept_game(key.fileobj)
elif kind == "console":
self._console_read(obj)
elif kind == "game":
self._game_read(obj)
elif kind == "udp":
self._udp_read()
self._tick_launch()
self._tick_stats()
def _listener(self, port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((self.bind_addr, port))
s.listen(16)
s.setblocking(False)
return s
# ---------------- console side (legacy protocol, relay-terminated) ----------------
def _accept_console(self, listener):
sock, addr = listener.accept()
sock.setblocking(False)
conn = RelayConsoleConn(sock, addr)
self.console_conns.append(conn)
self.sel.register(sock, selectors.EVENT_READ, ("console", conn))
# Stream the egg immediately (blocking sendall is fine: the pod's console
# pad drains fast and the egg is a few KB).
sock.setblocking(True)
try:
n = 0
for pkt in egg_packets(self.egg_bytes):
sock.sendall(pkt)
n += 1
conn.egg_sent = True
print(f"[relay] console conn {addr[0]}:{addr[1]}: egg sent "
f"({n} chunks); {self._eggs_out()}/{len(self.roster)} pods have it",
flush=True)
except OSError as e:
print(f"[relay] console conn {addr[0]}:{addr[1]}: egg send failed {e!r}",
flush=True)
self._drop_console(conn)
return
finally:
try:
sock.setblocking(False)
except OSError:
pass
# GLOBAL launch timer: arm/rearm when the LAST pod has its egg.
if self._eggs_out() >= len(self.roster) and self.launches_sent == 0:
self.launch_at = time.time() + LAUNCH_SETTLE_SECONDS
print(f"[relay] all pods have the egg; LAUNCH pair in "
f"{LAUNCH_SETTLE_SECONDS}s", flush=True)
def _eggs_out(self):
return sum(1 for c in self.console_conns if c.egg_sent)
def _console_read(self, conn):
try:
data = conn.sock.recv(4096)
except OSError:
data = b""
if not data:
print(f"[relay] console conn {conn.addr[0]}:{conn.addr[1]} closed",
flush=True)
self._drop_console(conn)
return
if len(data) >= 24:
(clid, _gid, fh, _ts) = struct.unpack_from("<iiii", data, 0)
(mlen, mid) = struct.unpack_from("<Ii", data, 16)
print(f"[relay] pod->console clientID={clid} fromHost={fh} msgID={mid} "
f"len={mlen}", flush=True)
def _drop_console(self, conn):
try:
self.sel.unregister(conn.sock)
except (KeyError, ValueError):
pass
try:
conn.sock.close()
except OSError:
pass
if conn in self.console_conns:
self.console_conns.remove(conn)
def _tick_launch(self):
if self.launch_at is None or self.launches_sent >= 2:
return
if time.time() < self.launch_at:
return
pkt = run_mission_packet()
for conn in list(self.console_conns):
try:
conn.sock.setblocking(True)
conn.sock.sendall(pkt)
conn.sock.setblocking(False)
except OSError as e:
print(f"[relay] launch send failed to {conn.addr}: {e!r}", flush=True)
self.launches_sent += 1
self.launch_at = time.time() + LAUNCH_STEP_SECONDS
print(f"[relay] RunMission #{self.launches_sent} sent to "
f"{len(self.console_conns)} pod(s)", flush=True)
# ---------------- game TCP side ----------------
def _accept_game(self, listener):
sock, addr = listener.accept()
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
sock.setblocking(False)
conn = RelayGameConn(sock, addr)
self.game_conns.append(conn)
self.sel.register(sock, selectors.EVENT_READ, ("game", conn))
print(f"[relay] {conn.name()} accepted (awaiting HELLO)", flush=True)
def _game_read(self, conn):
try:
data = conn.sock.recv(8192)
except OSError:
data = b""
if not data:
self._drop_game(conn, "closed")
return
conn.buf += data
while len(conn.buf) >= ENV_TCP_SIZE:
route, length = struct.unpack_from(ENV_FMT_TCP, conn.buf, 0)
if length > FRAME_CAP:
self._drop_game(conn, f"oversize frame ({length})")
return
if len(conn.buf) < ENV_TCP_SIZE + length:
break # partial; wait for more
payload = conn.buf[ENV_TCP_SIZE:ENV_TCP_SIZE + length]
conn.buf = conn.buf[ENV_TCP_SIZE + length:]
self.stats["tcp_rx"] += 1
self._handle_game_frame(conn, route, payload)
if conn.sock.fileno() < 0: # dropped mid-loop
return
def _handle_game_frame(self, conn, route, payload):
if conn.host_id is None:
# First frame MUST be a valid HELLO.
if route != ROUTE_HELLO or len(payload) < 8:
self._drop_game(conn, f"first frame not HELLO (route={route})")
return
magic, host_id = struct.unpack_from("<iI", payload, 0)
host_id = int(host_id)
if magic != HELLO_MAGIC:
self._drop_game(conn, f"bad HELLO magic 0x{magic:x}")
return
if host_id not in self.expected_ids:
self._drop_game(conn, f"HELLO hostID {host_id} not in roster "
f"{sorted(self.expected_ids)}")
return
if host_id in self.by_host:
self._drop_game(conn, f"HELLO hostID {host_id} already registered")
return
conn.host_id = host_id
self.by_host[host_id] = conn
print(f"[relay] {conn.name()} REGISTERED "
f"({len(self.by_host)}/{len(self.roster)})", flush=True)
# PEER_UP exchange: newcomer learns everyone; everyone learns newcomer.
for other_id in sorted(self.by_host):
if other_id != host_id:
self._send_control(conn, ROUTE_PEER_UP, other_id)
for other in self.by_host.values():
if other is not conn:
self._send_control(other, ROUTE_PEER_UP, host_id)
return
if route == ROUTE_BCAST:
env = struct.pack(ENV_FMT_TCP, conn.host_id, len(payload))
for other in list(self.by_host.values()):
if other is not conn:
self._send_raw(other, env + payload)
elif route >= FIRST_GAME_HOST_ID:
target = self.by_host.get(route)
if target is None:
return # peer gone; drop silently
env = struct.pack(ENV_FMT_TCP, conn.host_id, len(payload))
self._send_raw(target, env + payload)
else:
print(f"[relay] {conn.name()} bad route {route} -- ignored", flush=True)
def _send_control(self, conn, route, host_id):
self._send_raw(conn, struct.pack(ENV_FMT_TCP, route, 4)
+ struct.pack("<i", host_id))
def _send_raw(self, conn, data):
try:
conn.sock.setblocking(True)
conn.sock.sendall(data)
conn.sock.setblocking(False)
self.stats["tcp_tx"] += 1
except OSError as e:
self._drop_game(conn, f"send failed {e!r}")
def _drop_game(self, conn, why):
print(f"[relay] {conn.name()} dropped: {why}", flush=True)
try:
self.sel.unregister(conn.sock)
except (KeyError, ValueError):
pass
try:
conn.sock.close()
except OSError:
pass
if conn in self.game_conns:
self.game_conns.remove(conn)
if conn.host_id is not None and self.by_host.get(conn.host_id) is conn:
del self.by_host[conn.host_id]
self.udp_endpoint.pop(conn.host_id, None)
for other in self.by_host.values():
self._send_control(other, ROUTE_PEER_DOWN, conn.host_id)
# ---------------- game UDP side ----------------
def _udp_read(self):
while True:
try:
data, endpoint = self.udp_sock.recvfrom(4096)
except (BlockingIOError, InterruptedError):
return
except OSError:
return
if len(data) < ENV_UDP_SIZE:
continue
route, from_host, _seq = struct.unpack_from(ENV_FMT_UDP, data, 0)
if from_host not in self.by_host:
continue # not TCP-registered: drop
self.stats["udp_rx"] += 1
# NAT-rebind tolerant: every datagram refreshes the endpoint map.
self.udp_endpoint[from_host] = endpoint
if route == ROUTE_HELLO:
ack = struct.pack(ENV_FMT_UDP, ROUTE_UDP_ACK, CONSOLE_HOST_ID, 0)
try:
self.udp_sock.sendto(ack, endpoint)
except OSError:
pass
continue
if route == ROUTE_BCAST:
targets = [h for h in self.by_host if h != from_host]
elif route >= FIRST_GAME_HOST_ID and route in self.by_host:
targets = [route]
else:
continue
for host_id in targets:
if self.udp_drop_pct and random.random() * 100.0 < self.udp_drop_pct:
self.stats["udp_dropped"] += 1
continue
ep = self.udp_endpoint.get(host_id)
if ep is not None:
try:
self.udp_sock.sendto(data, ep) # forward VERBATIM
self.stats["udp_tx"] += 1
continue
except OSError:
pass
# Target's UDP endpoint unknown/broken: wrap the frame onto its
# game TCP connection (asymmetric-UDP-blockage fallback).
frame = data[ENV_UDP_SIZE:]
target = self.by_host.get(host_id)
if target is not None and frame:
env = struct.pack(ENV_FMT_TCP, from_host, len(frame))
self._send_raw(target, env + frame)
self.stats["udp_tcp_fallback"] += 1
# ---------------- stats ----------------
def _tick_stats(self):
if time.time() < self.stats_at:
return
self.stats_at = time.time() + STATS_PERIOD
s = self.stats
print(f"[relay-stats] tcp rx/tx {s['tcp_rx']}/{s['tcp_tx']} | "
f"udp rx/tx {s['udp_rx']}/{s['udp_tx']} "
f"(dropped {s['udp_dropped']}, tcp-fallback {s['udp_tcp_fallback']}) | "
f"registered {sorted(self.by_host)} udp-known {sorted(self.udp_endpoint)}",
flush=True)
def relay_main(argv):
"""argv: --relay <consolePort> <egg-file> [--bind ADDR] [--udp-drop PCT]"""
args = argv[:]
bind_addr = "0.0.0.0"
udp_drop = 0.0
if "--bind" in args:
i = args.index("--bind")
bind_addr = args[i + 1]
del args[i:i + 2]
if "--udp-drop" in args:
i = args.index("--udp-drop")
udp_drop = float(args[i + 1])
del args[i:i + 2]
if len(args) != 2:
print(__doc__)
return 2
console_port = int(args[0])
egg_path = args[1]
relay = Relay(console_port, egg_path, bind_addr, udp_drop)
if not relay.roster:
print(f"[relay] ERROR: no [pilots] entries in {egg_path}")
return 2
try:
relay.run()
except KeyboardInterrupt:
print("[relay] shutting down")
return 0
def main():
if len(sys.argv) >= 2 and sys.argv[1] == "--relay":
return relay_main(sys.argv[2:])
if len(sys.argv) < 3:
print(__doc__)
return 2