This repository contains coursework for the Network Programming class.
Project 1 reads a network inventory file, parses device records, analyzes them, and generates a simple audit report.
- Reads
inventory.txt - Splits raw text into device records
- Parses records into
Deviceobjects - Counts hosts, switches, and routers
- Lists devices that need attention
- Lists inactive switches
- Writes
network_audit.txtwhen report generation is selected
- project1/main.py: menu and program entry point
- project1/reader.py: reads and splits inventory data into records
- project1/parser.py: converts records into
Deviceobjects - project1/analyzer.py: counts device types and finds attention items
- project1/writer.py: writes the audit report
- project1/device.py: device model
- project1/inventory.txt: input data
__main__.py — entry point and menu:
__main__.py:16-37—main()orchestrates the full pipeline: callsreader.read_file()to load inventory,parser.parse_devices()to buildDeviceobjects, andanalyze()to count types and find items needing attention.__main__.py:41-69— The menu loop offers three options:- Check CLI output — calls
main()and prints results to the terminal. - Check inventory file — runs the full pipeline and calls
writer.write_report()to producenetwork_audit.txt. - Exit — breaks the loop.
- Check CLI output — calls
reader.py:9-35 — read_file():
Joins all non-empty lines into a single string, then splits into records using regex \s+(?=(?:HOST|SWITCH|ROUTER),) — the positive lookahead keeps the device type as the start of each record. Each record is split on commas with whitespace stripped (spaces removed from fields with .replace(" ", "")). Returns a list of field lists like [["HOST","192.168.1.180","Active"], ...].
parser.py:5-47 — parse_devices():
Iterates the field-list records. Normalizes device type with .strip().upper(), then dispatches by type:
- HOST (3 fields): creates
Device("HOST", ip, ip, status) - SWITCH (4 fields): creates
Device("SWITCH", name, None, status, vlan) - ROUTER (4 fields): creates
Device("ROUTER", name, ip, status)Returns a list ofDeviceobjects.
device.py:1-42 — Device class:
needs_attention()(device.py:17-18) — returnsTrueif status is"inactive"or"maintenance".get_device_info()(device.py:20-42) — formats output differently per type (host shows IP, switch shows name+VLAN, router shows name+IP).
analyzer.py:1-24 — analyze():
Iterates the device list, counts hosts/switches/routers, collects devices matching needs_attention(), and separately tracks inactive switches. Returns (host_count, switch_count, router_count, needs_attention_list, inactive_switches_list).
writer.py:4-15 — write_report():
Writes the formatted audit report to project1/network_audit.txt with device counts and the attention list.
The inventory contains records such as:
HOST,192.168.1.180,Active
SWITCH,SW-Floor-4-22,Inactive,VLAN32
ROUTER,Router-Core-19,Active,10.0.0.5
reader.py converts the file into a list of records like:
[
["HOST", "192.168.1.180", "Active"],
["SWITCH", "SW-Floor-4-22", "Inactive", "VLAN32"],
["ROUTER", "Router-Core-19", "Active", "10.0.0.5"],
]parser.py then turns those records into Device objects.
From the repository root:
python3 project1/__main__.py-
Check CLI outputPrints the counts and device attention list in the terminal. -
Check inventory fileReads the inventory, analyzes it, and writesproject1/network_audit.txt. -
ExitCloses the program.
With the current inventory.txt, the parser reads:
550records442hosts78switches30routers
A previous bug happened because the parser expected a flat list of tokens, while the reader was already returning a list of records. The parser now correctly processes one record at a time and normalizes device types with .strip().upper().
- Add unit tests for
reader.pyandparser.py - Avoid removing internal spaces from all fields unless needed
- Improve handling for malformed records
- Add clearer report formatting
Project 2 is a TCP socket chat application. It has one server that accepts multiple clients, lets users chat in a shared room, send private messages, upload/download small files, and play Rock Paper Scissors.
- Starts a threaded TCP server on port
5050 - Allows multiple clients to connect at the same time
- Registers each client with a unique username
- Broadcasts normal chat messages to all connected users
- Supports private messages between users
- Lists active users
- Uploads files to the server, up to
150 KB - Lists and downloads files stored on the server
- Supports a two-player Rock Paper Scissors game
- project2/server.py: server socket, client threads, shared state, chat commands, file storage, and RPS game logic
- project2/client.py: client connection, user input, command handling, uploads, downloads, and message display
project2/uploads/: created by the server when files are uploadedproject2/downloads/: created by the client when files are downloaded
Server (server.py):
server.py:427-447—start_server()creates a TCP socket withSO_REUSEADDR, binds to0.0.0.0:5050, listens, and accepts connections. Each accepted client socket gets a daemon thread runninghandle_client().server.py:369-424—handle_client()callsregister_client()to validate and store the new user, then enters a receive loop. Incoming JSON payloads are dispatched by key:message(broadcast or/command),rps_choice, orfile_upload.server.py:164-184—register_client()reads a JSON{"username": ...}packet, checks for duplicates, creates aClientobject, and stores it inclients_by_nameandclients_by_socketunderstate_lock.server.py:99-115— Protocol layer:receive_exact()loops until exactly 64 header bytes are read.receive_json()(server.py:118-137) decodes the header to get the payload length, reads exactly that many bytes, and runsjson.loads().server.py:140-146—send_json_to_sock()serializes the payload, prepends a 64-byte space-padded length header, and sends both viasock.sendall().server.py:339-363—handle_command()routes:/help→ help text,/users→ online list,/pm→ private message,/rps→ start game,/files→ list uploads,/download→ send file as hex,/quit→ raiseConnectionError.server.py:32-87—RPSclass:add_choice()registers each player's choice, applies thebeatsdict (rock→scissors,paper→rock,scissors→paper), and ends the game when a player reaches 3 wins or after 5 scored rounds.server.py:187-214—unregister_client()removes the client from global state understate_lock, notifies the RPS opponent if a game was in progress, and closes the socket.server.py:220-226—broadcast_message()iterates all connected clients and sends the chat payload to everyone except the sender.server.py:388-412— File upload: receives hex-encoded content, validates the 150 KB limit, and writes touploads/<filename>.server.py:324-336—handle_download()reads a file fromuploads/, hex-encodes it, and sends as{"file": {"name": ..., "content": hex}}.
Client (client.py):
client.py:189-247—main()parsessys.argvforhostandport(defaults127.0.0.1:5050), callsconnect_and_register(), prints the help menu, spawns a backgroundreceive_loopthread, then enters an input loop./uploadis intercepted to callsend_upload(), RPS choices (rock/paper/scissors) are sent as{"rps_choice": ...}, and/quitsetsrunning = False.client.py:40-76—receive_loop()runs in a daemon thread: reads the 64-byte length header, reads the exact payload, and dispatches toprint_message()orhandle_incoming_file().client.py:82-90—connect_and_register()creates a TCP socket, connects, prompts for a username, and sends{"username": ...}.client.py:142-168—print_message()decodes structured JSON payloads (system,error,chat,private_message,users,files) and displays them with a\r\033[Kprefix to cleanly overwrite any existing prompt text.client.py:93-118—send_upload()reads a local file, validates the 150 KB size limit, hex-encodes the bytes, and sends as{"file_upload": {"name": ..., "content": hex}}.client.py:22-37—send_json()serializes with a padded 64-byte length header and sends viasock.sendall().
Open one terminal for the server:
cd project2
python3 server.pyOpen another terminal for each client:
cd project2
python3 client.pyThe client connects to 127.0.0.1:5050 by default. To connect to a different server host or port:
python3 client.py <host> <port>Example:
python3 client.py 192.168.1.10 5050/help: shows the command menu/quit: disconnects from the server/users: lists online users/pm <username> <message>: sends a private message/rps <username>: starts a Rock Paper Scissors game with another user/files: lists files stored on the server/upload <filepath>: uploads a local file smaller than150 KB/download <filename>: downloads a file from the server
Anything that does not start with / is sent as a public chat message.
Start a game with:
/rps <username>
After the game starts, each player types one of:
rock
paper
scissors
The server compares both choices, updates the score, and sends the result to both players. The game ends when one player reaches 3 wins or after 5 scored rounds.
Upload a file:
/upload notes.txt
List server files:
/files
Download a file:
/download notes.txt
Uploaded files are saved in project2/uploads/. Downloaded files are saved in project2/downloads/.
- The server uses one thread per connected client.
- Shared dictionaries store connected users and active RPS games.
- Locks are used around shared state and socket sends to reduce race conditions.
- Files are sent as hex strings inside JSON messages.
- The server strips uploaded file paths down to the filename to avoid basic path traversal issues.
- The file size limit is
150 KB.
- Add passwords or account-based login
- Add timestamps to chat messages
- Add better validation for empty command arguments
- Add unit tests for the JSON send/receive helpers
- Store chat history while the server is running
Final Project — For less technical details, check HOW_IT_WORKS.md.
The final project automates a simulated GNS3 network, monitors router CPU, sniffs packets, manages end devices, and runs a TCP chat application across the lab hosts.
- Simulated network with VyOS router, Arista/MikroTik switches, and Alpine hosts
- Automates device configuration via SSH (with Telnet fallback) using Netmiko with Rich status display
- Monitors VyOS CPU load in real time and generates Matplotlib trend graphs
- Sniffs network traffic using Scapy with a live-updating Rich table
- Manages Alpine end devices: parallel SSH commands, hostname changes, SCP file distribution, interactive shell, remote chat server deployment
- Multi-client TCP chat with file upload/download, private messages, and Rock Paper Scissors
- Final_Project/guide.py: Rich interactive launcher menu (entry point)
- Final_Project/src/automate.py: Task 1–2 — network device automation
- Final_Project/src/cpu_monitor.py: Task 3 — VyOS CPU profiling
- Final_Project/src/packet_sniffer.py: Task 4 — Scapy packet capture
- Final_Project/src/end_device_manager.py: Task 5 — end device SSH management
- Final_Project/src/chat/server.py: Task 6 — TCP chat server
- Final_Project/src/chat/client.py: Task 6 — TCP chat client
- Final_Project/src/validator.py: input file validation
- Final_Project/src/network.py: Netmiko profiles, ping, command dispatch
- Final_Project/src/menu.py: legacy arrow-key launcher
Final_Project/config/: device inventory, credentials, commands, end devicesFinal_Project/data/: runtime logs (e.g.cpu_log.txt)Final_Project/assets/: generated graphs (e.g.cpu_progress.png)
Each task maps to a Python module under Final_Project/src/. The project is launched via guide.py, a Rich interactive launcher.
Reads device inventory (config/devices.txt), login credentials (config/login.txt), and per-device commands (config/commands.txt). For each device:
- Validates all input files via
src/validator.py - Pings the device (ICMP reachability via
src/network.py) - Connects over SSH first; falls back to Telnet if SSH is unavailable
- Pushes configuration commands — Arista enters enable/config mode and saves; VyOS uses
send_config_setthencommit+save; MikroTik uses directsend_command - Displays a Rich summary table showing reachability, transport used, and result per device
How it works internally:
automate.py:50—main()loads devices, credentials, and commands throughvalidator.py, which strips comments, checks IPv4 validity viaipaddress.IPv4Address, and validates device types against{"vyos", "arista_eos", "mikrotik_routeros"}.automate.py:72-86— Each device is first ICMP-pinged vianetwork.check_reachability(). Unreachable hosts are skipped and recorded as"skipped".automate.py:106-110—network.push_configurations()iterates overbuild_connection_profiles()which yields SSH first, then a Telnet fallback profile (mapping device type to its_telnetvariant, e.g.vyos→vyos_telnet).network.py:108-126— Withinpush_configurations(),ConnectHandleris opened inside awithblock. On success,_send_commands()dispatches by device type:- Arista (
network.py:56-80): callsenable(), entersconfig_mode(), runs commands withsend_command_timing(), resyncs prompt afterhostnamechanges, thenwrite memory. - VyOS (
network.py:82-88): usessend_config_set(), thencommit+save. - MikroTik (
network.py:90-95): iterates withsend_command().
- Arista (
- On failure (NetmikoTimeoutException, authentication error, etc.), the next transport profile is tried. If all fail, the error list is joined and returned as a
ConnectionResult(ok=False).
SSHs into the VyOS router and polls CPU utilization:
- Connects using credentials from
config/login.txt - Runs
top -b -n 2 -d 1(withsudofallback viaCPU_COMMANDSlist at line 17) - Parses the output using regex to extract the idle percentage, computing CPU load as
100 - idle - Logs each sample with a timestamp to
data/cpu_log.txt - After all samples, renders a Matplotlib line graph saved to
assets/cpu_progress.png - Sample count and interval are configurable via CLI flags (
-n,-i) or interactive prompts
How it works internally:
cpu_monitor.py:73-103—monitor_system()loads the VyOS device entry and credentials viavalidator.py, then builds a Netmiko profile. It iterates transport profiles (SSH, then Telnet) fromnetwork.build_connection_profiles().cpu_monitor.py:107-144— After connecting, it opensdata/cpu_log.txtand writes a CSV header. A progress-loop polls CPU viaread_cpu_percent()at the configured interval.cpu_monitor.py:55-70—read_cpu_percent()tries each command inCPU_COMMANDS(top -b -n 2 -d 1, thensudo top -b -n 2 -d 1), callsparse_cpu_percent(), and returns the first successful parse.cpu_monitor.py:24-52—parse_cpu_percent()applies regex fallbacks in order:%cpu(s):lines — extracts theid(idle) field from the last occurrence.X% idle— alternative format.cpu states: X% user— legacy VyOSshow system cpuformat.%cpu(s): X us— direct user percentage.
- If no pattern matches,
Noneis returned and the sample is skipped with a warning. cpu_monitor.py:161-183— After collection, a Matplotlib figure is plotted (timestamps on x-axis, CPU % on y-axis, grid enabled) and saved toassets/cpu_progress.png.
Note: Packet capture requires root privileges. Run with sudo or the sniffer will prompt to re-execute with elevated permissions.
Scapy-based live packet capture:
- Lists available network interfaces (Linux
sys/class/netor Windowsipconfig) - Prompts for interface selection, packet count, and optional BPF filter
- Captures packets with
scapy.all.sniff()and displays each in a live Rich table (timestamp, source, destination, protocol, length) - Auto-re-executes with
sudoif insufficient permissions
How it works internally:
packet_sniffer.py:19-35—load_scapy_sniff()lazily importsscapy.all.sniff. If Scapy is unavailable in the current interpreter, it re-executes using the project venv's Python viaos.execv().packet_sniffer.py:59-106—sniff_packets()creates a RichTablewith columns (Time, Source, Destination, Protocol, Length). Thehandle_packet()callback extractspacket.src,packet.dst,packet.lastlayer().namefor protocol, andlen(packet)for length. On each packet the console is cleared and the table is re-rendered.packet_sniffer.py:108-123—list_interfaces()reads/sys/class/netentries on Linux (filtering outlo), or parsesipconfigoutput on Windows.packet_sniffer.py:38-56—prompt_sudo_reexec()asks the user if they want to re-run withsudowhen aPermissionErroroccurs, then callsos.execv()on thesudobinary.- CLI supports
-i <interface>,-c <count>, and-f <BPF filter>. Missing flags trigger interactive prompts viaPrompt.ask().
SSH-based management of Alpine end devices and the App Server (read from config/end_devices.txt):
- List devices — shows name, IP, and ICMP reachability
- Execute command — runs a shell command in parallel across selected devices using threading + Paramiko
- Change hostname — auto-detects OS (VyOS, Ubuntu/Debian, CentOS/Fedora, or generic) and applies the appropriate command
- SCP file — copies a local file to remote devices in parallel using
scp.SCPClient - Interactive shell — full raw terminal SSH session with arrow-key support
- Start chat server — SSHs into the Server device, optionally uploads
server.py, and launches it in background or foreground
How it works internally:
end_device_manager.py:26-46—load_end_devices()parsesconfig/end_devices.txtusing regexname: ip (Password: pass)and returns a dict of device entries.end_device_manager.py:114-132—_execute_all()is the parallel execution engine: it creates a thread per target device, each running a givenaction_fn, with a shared lock for safe result collection. Threads join with a configurable timeout.end_device_manager.py:149-163—exec_on_devices()sets_commandon each device info dict, calls_execute_all()withexec_cmd_on_device(), and renders a Rich results table.end_device_manager.py:166-192—change_hostname_on_device()runscat /etc/os-releaseto detect OS, then applies:- VyOS:
/configure set system host-name ... && /configure commit && /configure save - Ubuntu/Debian:
hostnamectl set-hostname ...+/etc/hostsupdate - CentOS/Fedora:
hostnamectl set-hostname ... - Generic/other:
hostname ...+ write to/etc/hostname
- VyOS:
end_device_manager.py:209-224—scp_to_device()usesscp.SCPClientover an existing Paramiko transport to copy files with a 30-second socket timeout.end_device_manager.py:249-337—interactive_shell()opens a raw Paramikoinvoke_shell()channel, sets the terminal to raw mode viatty.setraw(), runs a background reader thread that strips ANSI cursor-position reports (\x1b[...R), and forwards stdin to the channel byte-by-byte with arrow-key passthrough.end_device_manager.py:340-391—start_chat_server_on_device()connects to theServerdevice, optionally uploadssrc/chat/server.pyvia SCP if not already present, then starts the server either as a backgroundnohupprocess or in interactive foreground mode.
Threaded TCP chat server:
- Binds to a user-selected interface and port (default
0.0.0.0:5050) - Each client gets a dedicated thread
- JSON protocol: 64-byte padded length header + JSON payload
- Features: broadcast chat,
/pmprivate messages,/users,/rps(Rock Paper Scissors — first to 3 wins or 5 rounds),/files,/download - File uploads stored in
storage/uploads/(max 150 KB) - Client disconnect cleanup (notifies opponent if mid-game)
How it works internally:
server.py:578-598—start_server()creates a TCP socket withSO_REUSEADDR, binds, listens, and accepts connections in a loop. Each accepted socket gets a daemon thread runninghandle_client().server.py:518-575—handle_client()callsregister_client()to validate the username (24-char max, no duplicates), then enters a receive loop callingreceive_json().server.py:225-264— The protocol layer:receive_exact()loops until exactlyHEADER_SIZE(64) bytes are read;receive_json()decodes the header to get the payload length, then reads exactly that many bytes and runsjson.loads().server.py:267-276—send_json_to_sock()serializes the payload, prepends a 64-byte space-padded length header, and sends both halves viasock.sendall().server.py:488-512—handle_command()routes incoming client messages:/userslists online users,/pmsends private messages,/rpsstarts a game,/fileslists uploads,/downloadsends a file as hex,/quitraisesConnectionError.server.py:158-213— TheRPSclass manages game state:add_choice()compares choices, applies thebeatsdict (rock→scissors,paper→rock,scissors→paper), updates scores, and ends when a player reaches 3 wins or after 5 scored rounds.server.py:318-352—unregister_client()safely removes the client from global state dicts and notifies their RPS opponent if a game was in progress.
TCP chat client that connects to the server:
- Prompts for server IP and port (default
192.168.2.100:5050) - Registers with a username, then runs a background receive thread for non-blocking message display
- Commands:
/help,/quit,/users,/pm,/rps,/files,/upload,/download - During RPS, typing
rock,paper, orscissorssends the choice directly - Downloads saved to
storage/downloads/
How it works internally:
client.py:107-118—connect_and_register()creates a TCP socket, connects with a 15-second timeout, sends a JSON{"username": ...}packet, then sets the socket to blocking mode.client.py:67-101—receive_loop()runs in a background daemon thread. It reads the 64-byte header, extracts the JSON payload length, reads the exact payload bytes, and dispatches toprint_message()orhandle_incoming_file().client.py:168-194—print_message()decodes structured JSON:systemmessages,error,chat(broadcast),private_message,userslists,fileslists. Each is displayed with a\r\033[Kprefix to overwrite any prior prompt text.client.py:121-147—send_upload()reads a local file, checks it is under 150 KB, hex-encodes the bytes, and sends as{"file_upload": {"name": ..., "content": hex}}.client.py:248-312—main()connects, prints the help table, spawns the receiver thread, then enters an input loop./uploadis intercepted to callsend_upload(), RPS choices (rock/paper/scissors) are sent as{"rps_choice": ...}, and/quitsetsrunning = Falseand breaks.
src/validator.py— loads and validatesdevices.txt,login.txt, andcommands.txt:load_and_validate_devices()(validator.py:22) — iterates comma-separated lines, validates IPv4 viaipaddress.IPv4Address, checks device type againstVALID_DEVICE_TYPES, skips malformed lines with warningsload_credentials()(validator.py:51) — parsesname,username,passwordtriplets, returns a{name: {username, password}}dictload_commands()(validator.py:71) — parsesname: cmd1 | cmd2 | cmd3lines, splits on|, returns{name: [cmd, ...]}
src/network.py— ICMP reachability check, builds SSH-first/Telnet-fallback Netmiko profiles, executes commands per device type:check_reachability()(network.py:27) — runs a singleping -c 1with a 2-second timeoutbuild_connection_profiles()(network.py:41) — returns[("ssh", profile), ("telnet", telnet_profile)]where the Telnet variant is looked up inTELNET_DEVICE_TYPES_send_commands()(network.py:53) — dispatches by device type: Arista entersenable()/config_mode()thenwrite memory; VyOS runssend_config_set()thencommit+save; MikroTik uses directsend_command()push_configurations()(network.py:98) — iterates transport profiles, catchesNetmikoTimeoutException/NetmikoAuthenticationException/EOFError, returns aConnectionResultdataclassConnectionResult(network.py:19) — dataclass withok: bool,output: str,transport: str,error: str
src/menu.py— legacy arrow-key launcher (↑/↓ to navigate, Enter to select, Ctrl+C to quit):menu.py:16-27—_getch()reads a single raw keystroke usingtermios/tty, decodes arrow escape sequences (\x1b[A/\x1b[B)menu.py:41-55—draw(sel)clears the console and renders a Rich panel with a highlighted selectionmenu.py:57-88—main()runs the input loop, mapping ↑/↓ to index changes, Enter tosubprocess.run()of the selected module, and Ctrl+C to exit
guide.py is the main entry point for the final project, located at Final_Project/guide.py. It presents a Rich interactive menu with numbered options:
| Key | Module | Tasks | Description |
|---|---|---|---|
| 1 | Network Automation | 1–2 | Push configuration commands to network devices |
| 2 | CPU Monitor | 3 | Real-time VyOS CPU profiling with trend graph |
| 3 | Packet Sniffer | 4 | Scapy live packet capture with protocol inspection |
| 4 | End Device Manager | 5 | SSH management, hostname changes, SCP, shell |
| 5 | Chat Server | 6 | Multi-client TCP chat server |
| 6 | Chat Client | 6 | Connect to the chat server |
| 7 | Legacy Menu | — | Original arrow-key launcher |
| 8 | Exit | — | Leave the guide |
How it works internally:
guide.py:16-72— TheMODULESlist defines each entry with akey,name,tasks, dottedmodulepath (e.g."src.automate"), anddescription. Option 8 (Exit) hasmodule: None.guide.py:75-106—draw_welcome()clears the console, renders aPaneltitle with subtitle, then builds a RichTablewith columns Key/Module/Tasks/Description using theMODULESlist (skipping the Exit entry).guide.py:109-133—launch_module(module_name)runs the module as a child process viasubprocess.run([sys.executable, "-m", module_name], cwd=BASE). It catchesFileNotFoundError(module missing),OSError,KeyboardInterrupt, and general exceptions. After the subprocess exits, it waits for the user to press Enter before returning to the menu.guide.py:136-167—main()loops: callsdraw_welcome(), reads user input withinput(), matches against theMODULESlist by key. If the choice is"8"orKeyboardInterruptis caught, the loop breaks. Invalid choices show an error and re-prompt. On launch,launch_module()is called with the matched module's dotted path.
[ Fedora Host Machine ]
|
[ Mgmt / WAN Cloud ]
|
[ VyOS Router ] (192.168.1.254, 192.168.2.254)
/ \
[ Arista ] [ MikroTik ]
192.168.1.250 192.168.2.250
| \ | \
Alpine Alpine Alpine App Server
1.10 1.11 2.10 2.100
The chat server runs on 192.168.2.100:5050. Clients connect from the three Alpine hosts.
cd Final_Project
pip install -r requirements.txt
python guide.py # Interactive launcher (all tasks)
python -m src.automate # Task 1–2: automation
python -m src.cpu_monitor # Task 3: CPU monitoring
sudo python -m src.packet_sniffer -i eth0 -c 20 # Task 4: packet sniffing
python -m src.end_device_manager # Task 5: end device manager
python -m src.chat.server --host 0.0.0.0 --port 5050 # Task 6: chat server
python -m src.chat.client # Task 6: chat client- Missing or malformed input files
- Invalid IPv4 addresses
- ICMP unreachable devices
- SSH/Telnet timeout or handshake failure
- Authentication failure
- Broken JSON payloads and oversized messages
- Client disconnect during active game