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:
co-authored by
Claude Fable 5
parent
5df5c954ae
commit
efd440c64b
+27
-11
@@ -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
@@ -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\] (.*)")
|
||||
|
||||
Reference in New Issue
Block a user