-
Notifications
You must be signed in to change notification settings - Fork 12
Description
Summary
AgentServer.handle_new_agent uses await reader.read(ProtocolConfig.BUFFER_SIZE) with no timeout. Because max_connections is typically 2 (one attacker, one defender), an attacker who opens that many idle TCP connections blocks every legitimate agent from ever connecting — permanently, at zero cost.
Vulnerable Code
File: netsecgame/game/agent_server.py, lines 57–69
if self.current_connections < self.max_connections:
self.current_connections += 1
...
while True:
# NO timeout — blocks indefinitely if client sends nothing
data = await reader.read(ProtocolConfig.BUFFER_SIZE)There is no asyncio.wait_for, no keepalive, and no idle-connection reaper. A client that connects but never sends data holds its slot until it explicitly closes the TCP connection.
Steps to Reproduce
import socket, time
HOST, PORT = "localhost", 5000 # game port
MAX_CONNECTIONS = 2 # typical value for 1 attacker + 1 defender
slots = []
for _ in range(MAX_CONNECTIONS):
s = socket.create_connection((HOST, PORT))
slots.append(s)
# do NOT send any data — server coroutine blocks at reader.read() forever
print("All connection slots exhausted. Legitimate agents cannot connect.")
time.sleep(99999) # hold slots open indefinitelyAfter this, any legitimate agent attempting to connect receives the log message "Max connections reached. Rejecting new connection" and is immediately dropped.
Expected Behaviour
The server should enforce an idle/read timeout so that connections that do not send data within a configurable window are closed and their slots released.
Actual Behaviour
Idle TCP connections are held open indefinitely. Once max_connections slots are full, no legitimate agent can join the game.
Impact
- Complete availability loss: with
max_connections = 2, two idle TCP sockets from a single attacker machine are enough to permanently prevent any training run from starting. - No authentication is required to open those sockets (see related issue), so the attack is reachable from any host on the network.
- The
max(0, self.current_connections - 1)guard in thefinallyblock (line 110) only fires on disconnect, so slots remain consumed for as long as the attacker maintains the connections.
Recommendation
Wrap the read call with an asyncio timeout:
data = await asyncio.wait_for(
reader.read(ProtocolConfig.BUFFER_SIZE),
timeout=IDLE_TIMEOUT_SECONDS
)Catch asyncio.TimeoutError to close the connection cleanly. An appropriate idle timeout (e.g., 30–60 s) should be made configurable.