Advertisement
Open Source Projects by Phil Schwartz

Building a Python TCP Proxy for Network Debugging

A small TCP proxy can make opaque network behaviour easier to observe. It sits between a client and destination server, forwards bytes in both directions, records timing and connection details, and optionally displays a safe preview of the traffic. This is useful when diagnosing a custom protocol, checking retries, or investigating why a Linux service behaves differently across environments.

Python is well suited to the job because its standard library provides sockets, asynchronous I/O, logging, and TLS support. The example below uses asyncio, works on Linux or macOS, and can be adapted for development machines in Sydney, Melbourne, Brisbane, or elsewhere.

Why A TCP Proxy Helps

A proxy gives you a controlled observation point without changing the client application. You can measure when a connection begins, how many bytes move in each direction, whether the remote host closes the socket, and how long a response takes. This is particularly valuable for protocols that are not convenient to inspect with browser developer tools.

A TCP relay operates at the byte-stream level. It does not understand HTTP messages, JSON fields, or application commands unless you add protocol-specific parsing. TCP also has no message boundaries, so one read() operation may contain half a logical message or several messages together. Treating received data as arbitrary bytes prevents many debugging errors.

Choose The Proxy’s Boundaries

The simplest design listens on a local port, accepts a client, opens a second connection to the destination, and copies data in both directions. Binding to 127.0.0.1 keeps the listener private to the development computer. Use an explicit remote host and port rather than accepting arbitrary destinations, which could turn a debugging utility into an open relay.

Encrypted traffic needs special care. A basic TCP proxy can record connection metadata and encrypted byte counts, but it cannot display HTTPS or TLS application content. Plaintext inspection requires an authorised TLS interception setup, a locally trusted certificate, and clear handling of credentials and personal information. This matters for Australian businesses covered by privacy obligations and internal security policies.

Write The Asyncio Relay

The following program forwards a local connection on port 9000 to example.com on port 80. It logs a short hexadecimal preview instead of decoding arbitrary bytes as text. The asyncio.gather() call allows traffic to flow in both directions until one side closes.

import asyncio
import logging

LISTEN_HOST = "127.0.0.1"
LISTEN_PORT = 9000
TARGET_HOST = "example.com"
TARGET_PORT = 80

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s"
)

async def relay(reader, writer, direction):
    peer = writer.get_extra_info("peername")
    try:
        while data := await reader.read(65536):
            preview = data[:48].hex(" ")
            logging.info("%s %d bytes from %s: %s",
                         direction, len(data), peer, preview)
            writer.write(data)
            await writer.drain()
    finally:
        if not writer.is_closing():
            writer.close()
            await writer.wait_closed()

async def handle_client(client_reader, client_writer):
    target_reader, target_writer = await asyncio.open_connection(
        TARGET_HOST, TARGET_PORT
    )
    await asyncio.gather(
        relay(client_reader, target_writer, "client-to-server"),
        relay(target_reader, client_writer, "server-to-client"),
    )

async def main():
    server = await asyncio.start_server(
        handle_client, LISTEN_HOST, LISTEN_PORT
    )
    async with server:
        await server.serve_forever()

asyncio.run(main())

Run it with python3 proxy.py, then point a test client at 127.0.0.1:9000. For an HTTP test, curl can use the proxy with curl --proxy http://127.0.0.1:9000 http://example.com/. In production-quality code, add exception handling around the outbound connection so refused destinations produce a useful response and do not create noisy tracebacks.

Add Useful Traffic Inspection

Raw hexadecimal output is safe for binary protocols, but it can be difficult to interpret. A practical inspector can report connection duration, byte totals, and a printable ASCII preview while replacing control characters. Keep the preview short and redact known fields such as Authorization, cookies, API keys, and session identifiers before writing logs.

For HTTP debugging, parsing headers can reveal request methods, status codes, content lengths, and keep-alive behaviour. For a custom protocol, define a framing rule first: a fixed-size header, a length prefix, or a delimiter. Never assume that a single TCP read is a complete application message. If packet-level timing or retransmissions matter, compare the proxy logs with tcpdump or Wireshark rather than attempting to recreate packet captures in the relay.

Test Latency And Failure Modes

Start with loopback tests, then introduce controlled delays and disconnects. A relay can use await asyncio.sleep() before forwarding selected data to reproduce a slow mobile connection or an overloaded upstream service. This helps expose clients that have no timeout, retry endlessly, or mishandle a half-closed connection.

Australian network conditions make geographic testing valuable. A service hosted in Sydney may show different round-trip times for users in Perth, while a Brisbane office can experience a separate path through an internet provider. NBN performance, corporate VPNs, and cloud regions in Sydney or Melbourne can all affect results. Record timestamps in UTC or include the local AEST/AEDT offset so logs from distributed teams remain comparable.

Secure And Operate The Tool

A debugging proxy should be temporary, narrowly scoped, and easy to stop. Add command-line options for the bind address, listening port, destination, log level, and connection timeout. Keep the default bind address on loopback, and require an explicit flag before exposing the listener to a test VLAN or shared office network.

Use these operating habits when the proxy handles real application traffic:

A small Python proxy can then fit neatly into an open-source debugging toolkit: its behaviour is visible, its dependencies are minimal, and its output can be reproduced on a developer workstation or a Linux test host. Careful byte handling, explicit boundaries, and conservative logging turn a short script into a dependable network investigation utility.