Skip to content

Part 1 - A Simple Client / Server Application

In this first part, we build a minimal client/server application that will serve as the foundation for the rest of this guide.

The implementation is intentionally simple and written in Python. To keep the focus on the communication flow and the security concepts introduced in later parts, error handling and edge cases are kept to a minimum.

The following diagram illustrates the basic setup.

network topology

The server can run anywhere - on a development PC, in a data center, or in the cloud - as long as it is reachable from the embedded device over the network.

The server listens on TCP port 5000 and processes simple text-based commands. The main command we care about is:

GET_SECRET

When the server receives this command, it returns a secret string to the client.

Server Implementation

Let's start by looking at the server:

#!/usr/bin/env python3
import socket
import threading

HOST = "0.0.0.0"
PORT = 5000

def handle_command(command: str) -> str:
    command = command.strip()

    if command == "GET_SECRET":
        return "THIS_IS_THE_SECRET"
    else:
        return "UNKNOWN_COMMAND"

def handle_client(conn: socket.socket, addr):
    """Handle one client connection."""
    print(f"[+] Connection from {addr}")

    with conn:
        while True:
            data = conn.recv(1024)
            if not data:
                break

            command = data.decode("utf-8")
            print(f"[{addr}] Received: {command.strip()}")

            response = handle_command(command)
            conn.sendall((response + "\n").encode("utf-8"))

    print(f"[-] Client disconnected: {addr}")

def main():
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_sock:
        server_sock.bind((HOST, PORT))
        server_sock.listen()

        print(f"Server listening on {HOST}:{PORT}")

        while True:
            conn, addr = server_sock.accept()

            # One thread per client
            thread = threading.Thread(
                target=handle_client,
                args=(conn, addr),
                daemon=True
            )
            thread.start()

if __name__ == "__main__":
    main()

Key characteristics:

  • The server listens on all interfaces (0.0.0.0) on port 5000.
  • Each client connection is handled in its own thread.
  • Commands are simple UTF-8 encoded strings.
  • If the command is GET_SECRET, the server responds with THIS_IS_THE_SECRET.

This is deliberately straightforward so we can clearly observe what happens on the wire.

Client Implementation

Here is the corresponding client:

#!/usr/bin/env python3
import socket
import sys

SERVER_PORT = 5000

def main():
    if len(sys.argv) != 2:
        print(f"Usage: {sys.argv[0]} <server_ip_or_hostname>")
        sys.exit(1)

    server_host = sys.argv[1]

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.connect((server_host, SERVER_PORT))

        command = "GET_SECRET\n"
        print(f"Sending: {command.strip()}")
        sock.sendall(command.encode("utf-8"))

        response = sock.recv(1024)
        print(f"Received: {response.decode('utf-8').strip()}")

if __name__ == "__main__":
    main()

The client:

  1. Connects to the specified server.
  2. Sends the GET_SECRET command.
  3. Prints the server's response.

Running the Example

The code runs on any system with Python 3 installed.

For demonstration purposes:

  • server.py runs on a development PC (or later, a production server). For now it runs on 192.168.178.2
  • client.py runs on the embedded device.

Starting the server:

$ chmod u+x server.py
$ ./server.py 
Server listening on 0.0.0.0:5000
[+] Connection from ('127.0.0.1', 49956)
[('127.0.0.1', 49956)] Received: GET_SECRET
[-] Client disconnected: ('127.0.0.1', 49956)

Running the client:

$ ./client.py 192.168.178.2
Sending: GET_SECRET
Received: THIS_IS_THE_SECRET

At this point, everything works as expected. But from a security perspective, we have several serious issues.

Problem 1 - No Encryption

All communication is sent in clear text over the network.

If this traffic traverses public or otherwise untrusted infrastructure (e.g., the Internet), anyone with the ability to monitor traffic can read it.

Evil Hacker monitoring Internet traffic

You can observe this yourself using tcpdump:

$ sudo tcpdump -i any host 192.168.178.2 -w capture.pcap
tcpdump: WARNING: any: That device doesn't support promiscuous mode
(Promiscuous mode not supported on the "any" device)
tcpdump: listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
# Press Ctrl + C to abort
^C17 packets captured
17 packets received by filter
0 packets dropped by kernel

Then run the client.py again. You need to abort the tcpdump process with Ctrl+C.

You can either have a look at the resulting capture.pcap file with Wireshark if you prefer a GUI, or you can use the command line to browse the packet content.

Inspecting the packet data with Wireshark

$ tcpdump -r capture.pcap -s0 -A | grep SECRET -C 3
[..]
18:55:23.442244 lo    In  IP napier.commplex-main > napier.53388: Flags [P.], seq 1:20, ack 12, win 64, options [nop,nop,TS val 2304605798 ecr 2304605797], length 19
E..G`.@[email protected]=P.d...@.......
.]~f.]~eTHIS_IS_THE_SECRET
[..]

The "secret" is clearly visible in the packet payload.

Any attacker capable of sniffing the traffic can read it. Over networks you do not control, you cannot assume confidentiality.

Problem 2 - No Server Authentication

The client blindly trusts that the server it connects to is legitimate.

If an attacker manages to redirect traffic (for example, via DNS spoofing, ARP poisoning, or routing manipulation), the client may connect to a malicious server instead.

$ ./client.py 192.168.178.2
Sending: GET_SECRET
Received: WRONG_SECRET

Redirect Traffic to different server

From the client's perspective, the TCP connection succeeded. There is no mechanism to verify the server's identity.

Problem 3 - No Data Verification

The data sent between the server and the client cannot be verified. An attacker might replace some packet data during the transmission and neither the server nor the client could detect that.

Problem 4 - No Client Authentication

The server also has no way to verify who is connecting.

If someone clones your embedded product and connects to your backend infrastructure, the server cannot distinguish between:

  • A legitimate device
  • A cloned or unauthorized device

A cloned device making use of our server

This is particularly relevant in commercial or industrial deployments, where device identity and protection against cloning are critical.

What's Next?

To address these issues, we need:

  • Confidentiality (encryption)
  • Data Verification
  • Server authentication
  • Client Authentication

In the next part, we introduce TLS to encrypt communication and authenticate the server: 👉 Part 2 – Adding Encryption, Authentication and Verification