Skip to content

Latest commit

 

History

History
253 lines (195 loc) · 9.88 KB

File metadata and controls

253 lines (195 loc) · 9.88 KB

TCP Server

A concurrent TCP echo/handler server built on socket and threading, used as a safe local target for the client, scanner, and banner-grabber projects.

Project Overview

A TCP server binds a socket to an address, listens for incoming connections, and handles each accepted client. This project implements a threaded echo server: it accepts many clients at once, echoes back whatever they send, and cleanly handles disconnects. It doubles as a controlled lab target so you can test [[TCP-Client]], [[Port-Scanner]], and [[Banner-Grabber]] without touching any third-party system.

[!warning] Authorized use only Bind test servers to 127.0.0.1 (loopback) unless you intend to expose them, and never run experimental listeners on networks you do not control.

Objectives

  • Bind, listen, and accept TCP connections.
  • Serve multiple clients concurrently without blocking.
  • Guard shared state across threads.
  • Frame messages over a byte-stream protocol.
  • Shut down cleanly, releasing the port immediately.

Requirements

Item Detail
Python 3.9 or newer
Dependencies socket, threading, argparse (standard library)
Privileges None for ports above 1023; root required below that
Bind address 127.0.0.1 for lab use
Pairs with The [[TCP-Client]] project

Architecture

  • CLIargparse sets bind address, port, and a custom banner.
  • Listener — one socket with SO_REUSEADDR, bind(), and listen().
  • Accept loop — the main thread blocks on accept(), spawning a handler thread per client.
  • Handler — sends a banner, then echoes each received chunk until the client closes.
  • ShutdownKeyboardInterrupt breaks the loop and the with block closes the socket.
bind + listen ─▶ accept loop ─▶ Thread(handle_client, conn)
                                     │  recv ─▶ sendall(echo)
                                     └─ loop until client closes

Directory Structure

tcp-server/
├── tcp_server.py       # threaded echo server (CLI)
└── tcp_client.py       # test client (see TCP-Client note)

Implementation Plan

  1. Bind and listen. Create a socket, set SO_REUSEADDR, bind to 127.0.0.1, and listen with a backlog.
  2. Serve one client. Accept a connection, echo what it sends, and close.
  3. Serve many. Spawn a thread per accepted connection.
  4. Protect shared state. Hold the client registry behind a threading.Lock.
  5. Frame messages. Add newline delimiting and buffer partial reads — recv() boundaries do not match send() boundaries.
  6. Bound input. Cap the per-client buffer and drop clients that exceed it.
  7. Shut down. Handle KeyboardInterrupt, close all client sockets, and join the threads.

Complete Example Implementation

#!/usr/bin/env python3
"""tcp_server.py - Threaded TCP echo server for lab use (educational).

Bind to loopback unless you deliberately expose it.
"""
import argparse
import socket
import threading


def handle_client(conn: socket.socket, addr: tuple[str, int], banner: str) -> None:
    """Serve one client: send a banner, then echo everything it sends."""
    print(f"[+] Connection from {addr[0]}:{addr[1]}")
    try:
        conn.sendall(banner.encode() + b"\n")
        while True:
            data = conn.recv(4096)
            if not data:            # empty read == client closed
                break
            conn.sendall(b"echo: " + data)
    except OSError as exc:
        print(f"[!] {addr}: {exc}")
    finally:
        conn.close()
        print(f"[-] Disconnected {addr[0]}:{addr[1]}")


def serve(host: str, port: int, banner: str) -> None:
    """Bind, listen, and dispatch each client to its own thread."""
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv:
        srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        srv.bind((host, port))
        srv.listen(5)
        print(f"[*] Listening on {host}:{port} (Ctrl-C to stop)")
        try:
            while True:
                conn, addr = srv.accept()
                thread = threading.Thread(
                    target=handle_client, args=(conn, addr, banner), daemon=True
                )
                thread.start()
        except KeyboardInterrupt:
            print("\n[*] Shutting down.")


def main() -> int:
    parser = argparse.ArgumentParser(description="Threaded TCP echo server.")
    parser.add_argument("-b", "--bind", default="127.0.0.1",
                        help="bind address (default: 127.0.0.1)")
    parser.add_argument("-p", "--port", type=int, default=9000,
                        help="listen port (default: 9000)")
    parser.add_argument("--banner", default="Welcome to the echo server",
                        help="banner sent on connect")
    args = parser.parse_args()
    serve(args.bind, args.port, args.banner)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
python tcp_server.py --bind 127.0.0.1 --port 9000

Output

[*] Listening on 127.0.0.1:9000 (Ctrl-C to stop)
[+] Connection from 127.0.0.1:52344
[-] Disconnected 127.0.0.1:52344

Usage

# Start the server on a lab port
python3 tcp_server.py --host 127.0.0.1 --port 9009

# Larger backlog and verbose logging
python3 tcp_server.py --host 127.0.0.1 --port 9009 --backlog 32 --verbose

# In another terminal
python3 tcp_client.py 127.0.0.1 9009 --message "hello"

Example Output

$ python3 tcp_server.py --host 127.0.0.1 --port 9009
[*] Listening on 127.0.0.1:9009 (backlog 8)
[+] Client connected: 127.0.0.1:52114
[>] 127.0.0.1:52114 sent 'hello'
[-] Client disconnected: 127.0.0.1:52114
^C
[*] Shutting down, closing 0 active connections

Error Handling

Condition Exception Response
Port already bound OSError (EADDRINUSE) Report clearly and suggest SO_REUSEADDR or another port; exit 1
Binding a privileged port as a normal user PermissionError Report that ports below 1024 need root; exit 1
Client disconnects abruptly ConnectionResetError Remove that client only; the server keeps running
Client sends non-UTF-8 data UnicodeDecodeError Decode with errors="ignore" or handle as bytes
Client exceeds the buffer cap custom Log and disconnect that client
Operator stops the server KeyboardInterrupt Close every socket, join threads, exit 0

Each client is handled in its own try/except so one misbehaving peer cannot take the server down.

Security Considerations

[!warning] Authorized use only Bind to 127.0.0.1 unless you deliberately intend to expose the service. Run this only on systems you own.

  • Binding 0.0.0.0 publishes the service to every host that can reach the interface. On an untrusted network that is an unauthenticated service anyone can talk to.
  • There is no authentication or encryption here. Anyone who connects is trusted. Adding TLS via the [[SSL-Module|ssl]] module and an auth step is the first thing a real service needs.
  • Bound every read. An unbounded recv() loop is a memory-exhaustion denial of service.
  • Thread-per-client does not scale and is itself a resource-exhaustion vector — a real service would use asyncio or a bounded worker pool.
  • Never eval() or pickle.loads() network input. Both are direct remote-code-execution paths.
  • Log connections with timestamps so the service is auditable.

Testing

# Start the server, connect, and assert the echo
python3 tcp_server.py --host 127.0.0.1 --port 9009 &
sleep 0.5
echo "hello" | nc -q1 127.0.0.1 9009 | grep -qi hello && echo PASS
kill %1
import socket
import threading

import tcp_server


def test_server_echoes():
    server = tcp_server.EchoServer("127.0.0.1", 0)      # port 0 = pick a free port
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()

    with socket.create_connection(server.address, timeout=2) as sock:
        sock.sendall(b"hello\n")
        assert b"hello" in sock.recv(1024).lower()

    server.shutdown()

Cover: a single client, two concurrent clients, an abrupt disconnect, and an oversized message.

Extension Ideas

  • socketserver / asyncio — replace manual threading with socketserver.ThreadingTCPServer or an asyncio server for cleaner lifecycle handling.
  • Graceful shutdown — use a threading.Event and join handler threads instead of daemon=True.
  • TLS — wrap accepted sockets with ssl to offer an encrypted listener.
  • Protocol — replace echo with a tiny request/response protocol to practice parsing.
  • Connection limits — cap concurrent clients with a Semaphore to avoid resource exhaustion.

Troubleshooting

Symptom Likely cause Fix
OSError: [Errno 98] Address already in use Previous socket in TIME_WAIT Set SO_REUSEADDR before bind()
PermissionError on bind Port below 1024 without root Use a port above 1023
Server exits when a client drops Unhandled ConnectionResetError Catch it inside the per-client handler
Messages concatenated No framing over a byte stream Delimit with newlines and buffer partial reads
Client list corrupted Shared state without a lock Guard all access with threading.Lock
Server will not stop on Ctrl-C Threads blocked in recv() Mark threads daemon, or close their sockets to unblock

References

Related

  • [[TCP-Client]]
  • [[Port-Scanner]]
  • [[Banner-Grabber]]
  • [[Socket-Programming-for-Networking]]
  • [[Argparse-Module]]
  • [[Mini-Projects/Readme|Mini-Projects]] — module index
  • [[Readme|Python for Security Professionals]] — course home