Relay hardening: only ACKED pods count toward the launch gate

Found live during the port-forward verification: external port-checker nodes
(and by extension any internet scanner) that connect to the exposed console
port were streamed the egg AND counted as pods ('1/2 pods have it' from a
node in Turkey) -- a lingering scanner could hold a phantom roster slot or
nudge the ready/launch state.

Real pods send AcknowledgeEggFileMessage (clientID=0, msgID=4) after parsing
the egg (L4NET StartConnecting); scanners never speak the protocol.  The
launch gate (auto timer AND manual-launch READY) now counts only console
connections that ACKED:

- RelayConsoleConn.acked; _console_read detects the ACK, logs
  'pod ACK from <addr> (n/m ready)', drives _check_launch_gate.
- _eggs_out (egg_sent count) replaced by _pods_ready (acked count);
  gate logic extracted to _check_launch_gate (called on each ACK).
- btoperator monitor regex updated to the ACK line.

Verified: scanner-simulation (3 lingering non-protocol connections + 1 real
ACK on a 3-pilot roster -> gate NOT tripped, ACK counted 1/3); full operator
e2e PASS (registered -> READY held -> operator launch -> both pods LAUNCHED).

(The two intermediate e2e failures during this work were a stale port-check
relay squatting on 1500 -- a test-rig cleanup bug (Get-Process CommandLine
filter matches nothing; use Get-CimInstance Win32_Process), not the
hardening.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-18 11:49:58 -05:00
co-authored by Claude Fable 5
parent 5df5c954ae
commit efd440c64b
2 changed files with 28 additions and 12 deletions
+27 -11
View File
@@ -233,6 +233,10 @@ class RelayConsoleConn:
self.sock = sock
self.addr = addr
self.egg_sent = False
self.acked = False # pod sent AcknowledgeEggFile -- a REAL pod.
# Internet hardening: the console port is exposed, and random scanners
# connect and would otherwise count as pods. Only ACKED connections
# count toward the launch gate; scanners never speak the protocol.
class Relay:
@@ -336,8 +340,7 @@ class Relay:
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)
f"({n} chunks); awaiting pod ACK", flush=True)
except OSError as e:
print(f"[relay] console conn {addr[0]}:{addr[1]}: egg send failed {e!r}",
flush=True)
@@ -348,22 +351,27 @@ class Relay:
sock.setblocking(False)
except OSError:
pass
# GLOBAL launch: arm the timer when the LAST pod has its egg (auto
# mode), or wait for the operator's 'launch' command (manual mode --
# the operator app's Launch button writes it to our stdin).
if self._eggs_out() >= len(self.roster) and self.launches_sent == 0:
def _pods_ready(self):
"""REAL pods = console connections that ACKED the egg (scanners never
speak the protocol, so they can't hold a roster slot)."""
return sum(1 for c in self.console_conns if c.acked)
def _check_launch_gate(self):
# GLOBAL launch: arm the timer when the LAST pod has ACKED its egg
# (auto mode), or wait for the operator's 'launch' command (manual
# mode -- the operator app's Launch button writes it to our stdin).
if self._pods_ready() >= len(self.roster) and self.launches_sent == 0 \
and self.eggs_done_at is None:
self.eggs_done_at = time.time()
if self.manual_launch:
print("[relay] all pods have the egg; WAITING FOR OPERATOR "
print("[relay] all pods ACKED the egg; WAITING FOR OPERATOR "
"LAUNCH", flush=True)
else:
self.launch_at = self.eggs_done_at + LAUNCH_SETTLE_SECONDS
print(f"[relay] all pods have the egg; LAUNCH pair in "
print(f"[relay] all pods ACKED 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)
@@ -379,6 +387,14 @@ class Relay:
(mlen, mid) = struct.unpack_from("<Ii", data, 16)
print(f"[relay] pod->console clientID={clid} fromHost={fh} msgID={mid} "
f"len={mlen}", flush=True)
# AcknowledgeEggFile (clientID=0 NetworkManager, msgID=4): this
# connection is a REAL pod -- count it toward the launch gate.
if clid == 0 and mid == 4 and not conn.acked:
conn.acked = True
print(f"[relay] pod ACK from {conn.addr[0]}:{conn.addr[1]} "
f"({self._pods_ready()}/{len(self.roster)} ready)",
flush=True)
self._check_launch_gate()
def _drop_console(self, conn):
try:
+1 -1
View File
@@ -54,7 +54,7 @@ ST_COLORS = {ST_IDLE: "#666666", ST_EGG: "#b58900",
class SessionMonitor:
"""Turns btconsole/relay stdout lines into per-pilot state + stats."""
RE_RELAY_EGG = re.compile(r"console conn .*egg sent .*?(\d+)/(\d+) pods")
RE_RELAY_EGG = re.compile(r"pod ACK from .*?\((\d+)/(\d+) ready\)")
RE_RELAY_REG = re.compile(r"game\[.* host=(\d+)\] REGISTERED")
RE_RELAY_RUN = re.compile(r"RunMission #(\d+) sent")
RE_RELAY_STAT = re.compile(r"\[relay-stats\] (.*)")