Skip to content

Latest commit

 

History

History
252 lines (193 loc) · 9.69 KB

File metadata and controls

252 lines (193 loc) · 9.69 KB

TCP Client

A reusable interactive TCP client that connects to a listening service, sends data, and prints the reply — the counterpart to [[TCP-Server]].

Project Overview

A TCP client establishes a reliable, ordered byte stream to a server via the three-way handshake, then exchanges data. This project builds a small command-line client you can point at your own [[TCP-Server]] (or any authorized service) to send a line and read the response, or to run an interactive session. It is the building block behind banner grabbing, custom protocol testing, and simple network debugging.

[!warning] Authorized use only Connect only to services you own or are permitted to test. Examples target 127.0.0.1 and a local test server.

Objectives

  • Establish a TCP connection and exchange data with a server.
  • Handle partial sends and partial receives correctly.
  • Apply timeouts so the client never blocks indefinitely.
  • Close connections deterministically with a context manager.
  • Optionally wrap the connection in TLS.

Requirements

Item Detail
Python 3.9 or newer
Dependencies socket, ssl, argparse (standard library)
Privileges None
Target A server you control — pair this with the [[TCP-Server]] project
Network 127.0.0.1 or a private lab network

Architecture

  • CLIargparse supplies host, port, timeout, and an optional one-shot message.
  • Connectorsocket.create_connection() resolves the host and connects with a timeout in one call.
  • Send/receive loopsendall() guarantees the full payload goes out; recv() reads the reply in chunks until the peer stops or a delimiter is seen.
  • Modes — one-shot (--message) sends a single line and exits; interactive mode reads from stdin until EOF.
argparse ─▶ create_connection((host, port)) ─▶ sendall(data)
                                                    │
                                                    ▼
                                              recv() loop ─▶ stdout

Directory Structure

tcp-client/
├── tcp_client.py       # CLI client
└── tcp_server.py       # local echo server for testing (see TCP-Server note)

Implementation Plan

  1. Connect. Create a socket, set a timeout, and connect to host and port.
  2. Send. Use sendall() rather than send() so partial writes are handled for you.
  3. Receive. Loop on recv() until the expected amount arrives or the peer closes — a single recv() is not guaranteed to return everything.
  4. Frame messages. TCP is a byte stream with no message boundaries. Add newline delimiting or a length prefix.
  5. Use a context manager. with socket.socket(...) as sock: guarantees closure on error.
  6. Add TLS. Wrap with ssl.create_default_context() for an encrypted variant.
  7. CLI. Expose host, port, timeout, and message as arguments.

Complete Example Implementation

#!/usr/bin/env python3
"""tcp_client.py - Minimal interactive TCP client (educational).

Connect only to servers you own or are authorized to test.
"""
import argparse
import socket
import sys


def recv_all(sock: socket.socket, bufsize: int = 4096) -> bytes:
    """Read one response chunk (blocks until data or timeout)."""
    return sock.recv(bufsize)


def one_shot(sock: socket.socket, message: str) -> None:
    """Send a single line and print the reply."""
    sock.sendall(message.encode() + b"\n")
    reply = recv_all(sock)
    sys.stdout.write(reply.decode(errors="replace"))


def interactive(sock: socket.socket) -> None:
    """Read lines from stdin, send them, and print each reply until EOF."""
    print("[*] Interactive mode - type a line, Ctrl-D to quit.")
    for line in sys.stdin:
        sock.sendall(line.encode())
        reply = recv_all(sock)
        if not reply:
            print("[*] Server closed the connection.")
            break
        sys.stdout.write(reply.decode(errors="replace"))


def main() -> int:
    parser = argparse.ArgumentParser(description="Interactive TCP client.")
    parser.add_argument("host", help="server host (must be authorized)")
    parser.add_argument("port", type=int, help="server port")
    parser.add_argument("-m", "--message", help="one-shot message then exit")
    parser.add_argument("--timeout", type=float, default=5.0,
                        help="socket timeout seconds (default: 5.0)")
    args = parser.parse_args()

    try:
        with socket.create_connection((args.host, args.port), timeout=args.timeout) as sock:
            print(f"[+] Connected to {args.host}:{args.port}")
            if args.message is not None:
                one_shot(sock, args.message)
            else:
                interactive(sock)
    except (ConnectionRefusedError, socket.timeout, OSError) as exc:
        print(f"[!] Connection failed: {exc}", file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
# In one terminal start the test server from the TCP-Server note, then:
python tcp_client.py 127.0.0.1 9000 -m "ping"

Output

[+] Connected to 127.0.0.1:9000
echo: ping

Usage

# Send a line to a local server
python3 tcp_client.py 127.0.0.1 9009 --message "hello"

# Interactive mode
python3 tcp_client.py 127.0.0.1 9009 --interactive

# TLS-wrapped connection
python3 tcp_client.py 127.0.0.1 9443 --tls

Example Output

$ python3 tcp_client.py 127.0.0.1 9009 --message "hello"
[*] Connecting to 127.0.0.1:9009
[+] Connected
[>] hello
[<] HELLO
[*] Connection closed cleanly
$ python3 tcp_client.py 127.0.0.1 9999 --message "hello"
[!] Connection refused by 127.0.0.1:9999

Error Handling

Condition Exception Response
Nothing listening ConnectionRefusedError Clear message, exit 1
Host unreachable or filtered socket.timeout Report the timeout value used, exit 1
Peer closes mid-exchange ConnectionResetError Report partial data received, exit 1
Hostname does not resolve socket.gaierror Report and exit 1
TLS certificate invalid ssl.SSLCertVerificationError Report the reason and exit — never fall back to unverified
User interrupt KeyboardInterrupt Close the socket and exit 130

Security Considerations

[!warning] Authorized use only Connect only to servers you own or are explicitly authorized to interact with.

  • Plain TCP is unencrypted and unauthenticated. Anything sent is readable by anyone on the path — never send credentials over a plain socket.
  • Always verify TLS certificates. Use ssl.create_default_context(), which verifies by default. Disabling verification defeats the entire purpose of TLS.
  • Bound every read. An unbounded recv() loop into a growing buffer is a memory-exhaustion denial of service if the peer is hostile.
  • Set a timeout on every operation — connect, send, and receive — so a malicious or broken server cannot hang your client indefinitely.
  • Treat all received data as untrusted. Validate it before parsing, and never eval() or pickle.loads() it.

Testing

# Round-trip against a local echo server
python3 -c "
import socket
s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('127.0.0.1', 9009)); s.listen(1)
c, _ = s.accept(); c.sendall(c.recv(1024).upper()); c.close(); s.close()
" &
sleep 0.5
python3 tcp_client.py 127.0.0.1 9009 --message "hello" | grep -q HELLO && echo PASS
from unittest.mock import MagicMock, patch

import tcp_client


def test_send_and_receive():
    fake = MagicMock()
    fake.recv.side_effect = [b"HELLO\n", b""]
    with patch("socket.socket") as mock_socket:
        mock_socket.return_value.__enter__.return_value = fake
        assert tcp_client.exchange("127.0.0.1", 9009, "hello") == "HELLO"
        fake.sendall.assert_called_once()

Cover: a successful round trip, a refused connection, a timeout, and a peer that closes early.

Extension Ideas

  • TLS — wrap the socket with ssl.create_default_context() to speak to HTTPS/TLS services.
  • Non-blocking I/O — use selectors so you can read server data while typing in interactive mode.
  • Delimiter framing — read until a newline or length prefix instead of a single recv().
  • Reconnect/retry — add exponential backoff for flaky links.
  • Hex/raw mode — send and display raw bytes for binary protocol testing.

Troubleshooting

Symptom Likely cause Fix
ConnectionRefusedError No listener on that port Start the server first; confirm with ss -tlnp
Client hangs after sending Waiting for data the server never sends Set a receive timeout and define a framing protocol
Only part of the message arrives Single recv() assumed to return everything Loop until the delimiter or expected length is reached
Only part of the message sent send() used instead of sendall() send() may write fewer bytes than requested
SSLCertVerificationError Self-signed certificate in the lab Pass the lab CA via cafile= — do not disable verification
Works locally, fails remotely Firewall or NAT between client and server Test connectivity with nc -vz host port

References

Related

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