{"id":939,"date":"1970-01-01T00:00:00","date_gmt":"1970-01-01T00:00:00","guid":{"rendered":"https:\/\/imcodinggenius.com\/?p=939"},"modified":"1970-01-01T00:00:00","modified_gmt":"1970-01-01T00:00:00","slug":"building-a-secure-client-server-protocol-in-python-sockets-framing-key-exchange","status":"publish","type":"post","link":"https:\/\/imcodinggenius.com\/?p=939","title":{"rendered":"Building a Secure Client-Server Protocol in Python: Sockets, Framing &amp; Key Exchange"},"content":{"rendered":"<p>This course focuses on constructing a Secure Client-Server Protocol using the Python programming language, including security for sockets, framing, and key exchange. If you ever wanted to implement your own secure communication protocol in Python, using only the standard Python socket library and the cryptography library, this is how to do it.<\/p>\n<p>To be clear up front, <strong>this is not a complete VPN<\/strong>. No TUN\/TAP interfaces or routing of IP traffic is involved here. What we&#8217;re constructing is a secure client-server protocol \u2014 the building block upon which a real VPN, chat application, or any custom network service using encrypted, authenticated communication sits.<\/p>\n<h2>What We&#8217;re Building<\/h2>\n<p>At the end of this tutorial, you&#8217;ll have a framed message-sending client and server that negotiate a new encryption key for each session and detect tampering. It&#8217;s not a tunnel \u2014 there&#8217;s no OS-level forwarding or virtual network interface involved.<\/p>\n<h2>Design Goals<\/h2>\n<p>This differs from a toy socket example in four ways:<\/p>\n<p><strong>Proper message framing.<\/strong> TCP treats data as a stream, not discrete messages, so we define our own message boundaries.<br \/>\n<strong>Real key exchange.<\/strong> Every session negotiates its own key rather than using a fixed key hardcoded into the program.<br \/>\n<strong>Multi-client support.<\/strong> The server handles multiple clients simultaneously.<br \/>\n<strong>Visibility into wire traffic.<\/strong> We&#8217;ll inspect the bytes going over the network and confirm they&#8217;re actually encrypted.<\/p>\n<h2>Project Setup<\/h2>\n<p>python3 -m venv venv<br \/>\n<span class=\"hljs-built_in\">source<\/span> venv\/bin\/activate<br \/>\npip install cryptography<\/p>\n<p>Create a server.py and a client.py. We&#8217;ll build them piece by piece.<\/p>\n<h2>Message Framing in Python Sockets<\/h2>\n<h3>Receiving an Exact Number of Bytes<\/h3>\n<p>socket.recv() can return fewer bytes than requested. This helper loops until it has exactly what it needs:<\/p>\n<p><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">recv_exact<\/span>(<span class=\"hljs-params\">sock, n<\/span>):<\/span><br \/>\n    <span class=\"hljs-comment\"># sockets don&#8217;t guarantee a full read in one shot, so loop it<\/span><br \/>\n    buf = <span class=\"hljs-string\">b&#187;<\/span><br \/>\n    <span class=\"hljs-keyword\">while<\/span> <span class=\"hljs-built_in\">len<\/span>(buf) &lt; n:<br \/>\n        chunk = sock.recv(n &#8212; <span class=\"hljs-built_in\">len<\/span>(buf))  <span class=\"hljs-comment\"># ask only for what&#8217;s left<\/span><br \/>\n        <span class=\"hljs-keyword\">if<\/span> <span class=\"hljs-keyword\">not<\/span> chunk:<br \/>\n            <span class=\"hljs-comment\"># peer closed the connection mid-message, nothing more to do<\/span><br \/>\n            <span class=\"hljs-keyword\">raise<\/span> ConnectionError(<span class=\"hljs-string\">&#8216;socket closed before we got everything&#8217;<\/span>)<br \/>\n        buf += chunk<br \/>\n    <span class=\"hljs-keyword\">return<\/span> buf<\/p>\n<h3>Length-Prefixing Messages<\/h3>\n<p>Each message is prefixed with its length as 4 bytes (big-endian).<\/p>\n<p>A blind 4-byte length prefix is fine for a demo, but as written it lets a peer claim an arbitrarily large payload (up to ~4GB) and force the receiver to allocate that much memory before a single byte of actual data arrives \u2014 an easy denial-of-service vector. We cap it here at 10MB; adjust to whatever your real message size ceiling is.<\/p>\n<p><span class=\"hljs-keyword\">import<\/span> struct<\/p>\n<p>**MAX_MESSAGE_SIZE = <span class=\"hljs-number\">10<\/span> * <span class=\"hljs-number\">1024<\/span> * <span class=\"hljs-number\">1024<\/span>  <span class=\"hljs-comment\"># 10MB \u2014 adjust to your actual needs**<\/span><\/p>\n<p><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">send_framed<\/span>(<span class=\"hljs-params\">sock, payload<\/span>):<\/span><br \/>\n    <span class=\"hljs-comment\"># write the size first so the other end knows exactly how much to read<\/span><br \/>\n    sock.sendall(struct.pack(<span class=\"hljs-string\">&#8216;&gt;I&#8217;<\/span>, <span class=\"hljs-built_in\">len<\/span>(payload)) + payload)<\/p>\n<p><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">recv_framed<\/span>(<span class=\"hljs-params\">sock<\/span>):<\/span><br \/>\n    <span class=\"hljs-comment\"># header tells us the payload length, then we grab exactly that many bytes<\/span><br \/>\n    header = recv_exact(sock, <span class=\"hljs-number\">4<\/span>)<br \/>\n    length = struct.unpack(<span class=\"hljs-string\">&#8216;&gt;I&#8217;<\/span>, header)[<span class=\"hljs-number\">0<\/span>]<br \/>\n    **<span class=\"hljs-keyword\">if<\/span> length &gt; MAX_MESSAGE_SIZE:<br \/>\n        <span class=\"hljs-keyword\">raise<\/span> ValueError(<span class=\"hljs-string\">f&#8217;peer claims a <span class=\"hljs-subst\">{length}<\/span>-byte message, refusing (max <span class=\"hljs-subst\">{MAX_MESSAGE_SIZE}<\/span>)&#8217;<\/span>)**<br \/>\n    <span class=\"hljs-keyword\">return<\/span> recv_exact(sock, length)<\/p>\n<h3>Testing With a Large Payload<\/h3>\n<p>A quick echo test confirms the framing logic handles a payload larger than what a single recv() call would return.<\/p>\n<p><span class=\"hljs-keyword\">import<\/span> socket<br \/>\n<span class=\"hljs-keyword\">import<\/span> threading<\/p>\n<p><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">echo_once<\/span>(<span class=\"hljs-params\">conn<\/span>):<\/span><br \/>\n    <span class=\"hljs-comment\"># just bounce whatever we receive straight back<\/span><br \/>\n    data = recv_framed(conn)<br \/>\n    send_framed(conn, data)<\/p>\n<p>srv = socket.socket()<br \/>\nsrv.bind((<span class=\"hljs-string\">&#8216;127.0.0.1&#8217;<\/span>, <span class=\"hljs-number\">9000<\/span>))<br \/>\nsrv.listen(<span class=\"hljs-number\">1<\/span>)<br \/>\n<span class=\"hljs-comment\"># spin up a throwaway thread so accept() doesn&#8217;t block this test<\/span><br \/>\nthreading.Thread(target=<span class=\"hljs-keyword\">lambda<\/span>: echo_once(srv.accept()[<span class=\"hljs-number\">0<\/span>]), daemon=<span class=\"hljs-literal\">True<\/span>).start()<\/p>\n<p>c = socket.create_connection((<span class=\"hljs-string\">&#8216;127.0.0.1&#8217;<\/span>, <span class=\"hljs-number\">9000<\/span>))<br \/>\nsend_framed(c, <span class=\"hljs-string\">b&#8217;x&#8217;<\/span> * <span class=\"hljs-number\">500000<\/span>)  <span class=\"hljs-comment\"># deliberately bigger than one recv() would return<\/span><br \/>\nresult = recv_framed(c)<br \/>\n<span class=\"hljs-keyword\">assert<\/span> <span class=\"hljs-built_in\">len<\/span>(result) == <span class=\"hljs-number\">500000<\/span><br \/>\n<span class=\"hljs-built_in\">print<\/span>(<span class=\"hljs-string\">&#8216;large payload survived the round trip&#8217;<\/span>)<\/p>\n<h2>Implementing Key Exchange With ECDH<\/h2>\n<h3>Minimal ECDH Handshake<\/h3>\n<p>Each side generates an elliptic-curve key pair, and the public keys are exchanged over the wire.<\/p>\n<p><span class=\"hljs-keyword\">from<\/span> cryptography.hazmat.primitives.asymmetric <span class=\"hljs-keyword\">import<\/span> ec<br \/>\n<span class=\"hljs-keyword\">from<\/span> cryptography.hazmat.primitives <span class=\"hljs-keyword\">import<\/span> hashes, serialization<br \/>\n<span class=\"hljs-keyword\">from<\/span> cryptography.hazmat.primitives.kdf.hkdf <span class=\"hljs-keyword\">import<\/span> HKDF<\/p>\n<p><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">make_keypair<\/span>():<\/span><br \/>\n    <span class=\"hljs-comment\"># SECP384R1 &#8212; solid security margin and the library supports it well<\/span><br \/>\n    priv = ec.generate_private_key(ec.SECP384R1())<br \/>\n    <span class=\"hljs-comment\"># send this over the wire so the other side can do its half of ECDH<\/span><br \/>\n    pub_bytes = priv.public_key().public_bytes(<br \/>\n        encoding=serialization.Encoding.X962,<br \/>\n        <span class=\"hljs-built_in\">format<\/span>=serialization.PublicFormat.UncompressedPoint<br \/>\n    )<br \/>\n    <span class=\"hljs-keyword\">return<\/span> priv, pub_bytes<\/p>\n<p>Why P-384 rather than the more common P-256? Either is a reasonable choice for this tutorial \u2014 P-256 is faster and just as sound for most applications. P-384 is used here for a larger security margin; swap in ec.SECP256R1() if you&#8217;d rather match the more typical default.<\/p>\n<h3>Deriving a Per-Session Key<\/h3>\n<p>Using the shared secret from the ECDH exchange, HKDF derives a proper symmetric session key.<\/p>\n<p><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">get_session_key<\/span>(<span class=\"hljs-params\">priv, other_pub_bytes<\/span>):<\/span><br \/>\n    <span class=\"hljs-comment\"># rebuild the peer&#8217;s public key object from the bytes they sent us<\/span><br \/>\n    other_pub = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP384R1(), other_pub_bytes)<br \/>\n    shared = priv.exchange(ec.ECDH(), other_pub)<br \/>\n    <span class=\"hljs-comment\"># don&#8217;t use the raw ECDH secret as-is &#8212; HKDF turns it into a clean 32-byte key<\/span><br \/>\n    <span class=\"hljs-keyword\">return<\/span> HKDF(algorithm=hashes.SHA256(), length=<span class=\"hljs-number\">32<\/span>, salt=<span class=\"hljs-literal\">None<\/span>, info=<span class=\"hljs-string\">b&#8217;session key&#8217;<\/span>).derive(shared)<\/p>\n<h3>Why This Beats a Pre-Shared Key<\/h3>\n<p>With a single hardcoded key, compromising it once exposes every past and future session \u2014 including traffic an attacker may have already captured and stored. ECDH generates a fresh key for every connection, and no long-term secret ever crosses the wire; if one session&#8217;s key leaks, no other session is affected.<\/p>\n<h2>Authenticating the Handshake (Preventing MITM)<\/h2>\n<p>Raw ECDH as shown above negotiates a shared secret with whoever is on the other end of the socket \u2014 it does not prove that the other end is actually the client or server you intended to talk to. An attacker sitting on the network path can generate their own ephemeral keypair, complete a handshake with the server while pretending to be the client, and simultaneously complete a separate handshake with the client while pretending to be the server.<\/p>\n<p>Both sides end up with a valid-looking session key, and the attacker sits in the middle, transparently decrypting, reading, and re-encrypting every message. ECDH alone gives you confidentiality between two parties; it gives you zero guarantee about who those parties are.<\/p>\n<p>The fix is to authenticate the ephemeral public key before trusting it. The simplest way to do that here, without pulling in a full PKI or TLS stack, is a pre-shared key (PSK) that both sides already know out-of-band, used to HMAC-sign each side&#8217;s ephemeral public key. If the signature doesn&#8217;t verify, the connection is dropped before any session key is derived.<\/p>\n<p><span class=\"hljs-keyword\">import<\/span> hmac <span class=\"hljs-keyword\">as<\/span> hmac_lib<br \/>\n<span class=\"hljs-keyword\">import<\/span> hashlib<\/p>\n<p><span class=\"hljs-comment\"># PSK must be provisioned to both client and server ahead of time,<\/span><br \/>\n<span class=\"hljs-comment\"># out-of-band &#8212; e.g. baked into a config file at deploy time.<\/span><br \/>\n<span class=\"hljs-comment\"># This is NOT the session key; it only authenticates the handshake.<\/span><br \/>\nPSK = <span class=\"hljs-string\">b&#8217;replace-this-with-a-real-provisioned-secret&#8217;<\/span><\/p>\n<p><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">sign_pub<\/span>(<span class=\"hljs-params\">pub_bytes<\/span>):<\/span><br \/>\n    <span class=\"hljs-keyword\">return<\/span> hmac_lib.new(PSK, pub_bytes, hashlib.sha256).digest()<\/p>\n<p><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">verify_pub<\/span>(<span class=\"hljs-params\">pub_bytes, tag<\/span>):<\/span><br \/>\n    expected = sign_pub(pub_bytes)<br \/>\n    <span class=\"hljs-comment\"># compare_digest avoids leaking timing info about how many bytes matched<\/span><br \/>\n    <span class=\"hljs-keyword\">if<\/span> <span class=\"hljs-keyword\">not<\/span> hmac_lib.compare_digest(expected, tag):<br \/>\n        <span class=\"hljs-keyword\">raise<\/span> ValueError(<span class=\"hljs-string\">&#8216;handshake authentication failed &#8212; possible MITM&#8217;<\/span>)<\/p>\n<p><strong>&gt; The literal PSK = b&#8217;replace-this-&#8230;&#8217; above is for illustration only. In real deployments, load it from an environment variable or a secrets manager (e.g. os.environ[&#8216;PROTOCOL_PSK&#8217;].encode()) \u2014 never commit a real PSK to source control.<\/strong><\/p>\n<p>Each side now sends its ephemeral public key plus the HMAC tag over that key, and verifies the tag before deriving the session key. An attacker without the PSK can&#8217;t forge a tag that matches a substituted public key, so a swapped-in key is caught before it&#8217;s ever trusted.<\/p>\n<h2>Encrypting and Authenticating Data<\/h2>\n<p>AES-GCM is a natural fit here: it provides encryption and an authentication tag in a single operation, with no separate HMAC step needed, and any attempt to decrypt tampered ciphertext fails loudly with an exception rather than silently returning garbage.<\/p>\n<p><span class=\"hljs-keyword\">import<\/span> os<br \/>\n<span class=\"hljs-keyword\">from<\/span> cryptography.hazmat.primitives.ciphers.aead <span class=\"hljs-keyword\">import<\/span> AESGCM<\/p>\n<p><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">encrypt<\/span>(<span class=\"hljs-params\">key, data<\/span>):<\/span><br \/>\n    nonce = os.urandom(<span class=\"hljs-number\">12<\/span>)  <span class=\"hljs-comment\"># must be unique per message, never reused with the same key<\/span><br \/>\n    <span class=\"hljs-keyword\">return<\/span> nonce + AESGCM(key).encrypt(nonce, data, <span class=\"hljs-literal\">None<\/span>)<\/p>\n<p><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">decrypt<\/span>(<span class=\"hljs-params\">key, blob<\/span>):<\/span><br \/>\n    <span class=\"hljs-comment\"># nonce was tacked on the front during encrypt(), so split it back off<\/span><br \/>\n    nonce = blob[:<span class=\"hljs-number\">12<\/span>]<br \/>\n    ct = blob[<span class=\"hljs-number\">12<\/span>:]<br \/>\n    <span class=\"hljs-keyword\">return<\/span> AESGCM(key).decrypt(nonce, ct, <span class=\"hljs-literal\">None<\/span>)<\/p>\n<p><strong>&gt; A random 12-byte nonce per message is fine at this tutorial&#8217;s scale, but with a long-lived session key and a high message volume, random 96-bit nonces carry a real (if small) birthday-bound collision risk \u2014 reusing a nonce with the same key breaks AES-GCM&#8217;s security guarantees. For high-throughput or long-lived sessions, prefer a monotonic counter as the nonce instead of os.urandom(12).<\/strong><\/p>\n<h2>Building the Server<\/h2>\n<h3>Socket Setup, Multi-Client Handling, and Per-Client Keys<\/h3>\n<p>A new thread is created for every incoming connection; each connection performs its own handshake and gets its own session key. The handshake also verifies the client&#8217;s HMAC tag before trusting its public key.<\/p>\n<p><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">handle_client<\/span>(<span class=\"hljs-params\">conn, addr<\/span>):<\/span><br \/>\n    <span class=\"hljs-comment\"># every client gets its own keypair and, therefore, its own session key<\/span><br \/>\n    priv, pub = make_keypair()<br \/>\n    send_framed(conn, pub)<br \/>\n    send_framed(conn, sign_pub(pub))<br \/>\n    peer_pub = recv_framed(conn)<br \/>\n    peer_tag = recv_framed(conn)<br \/>\n    <span class=\"hljs-keyword\">try<\/span>:<br \/>\n        verify_pub(peer_pub, peer_tag)<br \/>\n    <span class=\"hljs-keyword\">except<\/span> ValueError:<br \/>\n        <span class=\"hljs-built_in\">print<\/span>(<span class=\"hljs-string\">f'[!] <span class=\"hljs-subst\">{addr}<\/span> failed handshake authentication, dropping connection&#8217;<\/span>)<br \/>\n        conn.close()<br \/>\n        <span class=\"hljs-keyword\">return<\/span><br \/>\n    session_key = get_session_key(priv, peer_pub)<br \/>\n    <span class=\"hljs-built_in\">print<\/span>(<span class=\"hljs-string\">f'[+] <span class=\"hljs-subst\">{addr}<\/span> handshake done&#8217;<\/span>)<\/p>\n<p>    <span class=\"hljs-keyword\">while<\/span> <span class=\"hljs-literal\">True<\/span>:<br \/>\n        <span class=\"hljs-keyword\">try<\/span>:<br \/>\n            blob = recv_framed(conn)<br \/>\n        **<span class=\"hljs-keyword\">except<\/span> (ConnectionError, ValueError) <span class=\"hljs-keyword\">as<\/span> e:<br \/>\n            <span class=\"hljs-built_in\">print<\/span>(<span class=\"hljs-string\">f'[!] <span class=\"hljs-subst\">{addr}<\/span> disconnected or sent a bad frame: <span class=\"hljs-subst\">{e}<\/span>&#8216;<\/span>)<br \/>\n            <span class=\"hljs-keyword\">break<\/span>**<br \/>\n        msg = decrypt(session_key, blob)<br \/>\n        <span class=\"hljs-built_in\">print<\/span>(<span class=\"hljs-string\">f'[<span class=\"hljs-subst\">{addr}<\/span>] <span class=\"hljs-subst\">{msg.decode()}<\/span>&#8216;<\/span>)<br \/>\n        send_framed(conn, encrypt(session_key, <span class=\"hljs-string\">b&#8217;ACK: &#8216;<\/span> + msg))<br \/>\n    conn.close()<\/p>\n<p><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">run_server<\/span>(<span class=\"hljs-params\">host=<span class=\"hljs-string\">&#8216;0.0.0.0&#8217;<\/span>, port=<span class=\"hljs-number\">9000<\/span><\/span>):<\/span><br \/>\n    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)<br \/>\n    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, <span class=\"hljs-number\">1<\/span>)  <span class=\"hljs-comment\"># avoid &#171;address in use&#187; on restart<\/span><br \/>\n    srv.bind((host, port))<br \/>\n    srv.listen(<span class=\"hljs-number\">5<\/span>)<br \/>\n    <span class=\"hljs-built_in\">print<\/span>(<span class=\"hljs-string\">f&#8217;listening on <span class=\"hljs-subst\">{host}<\/span>:<span class=\"hljs-subst\">{port}<\/span>&#8216;<\/span>)<br \/>\n    <span class=\"hljs-keyword\">while<\/span> <span class=\"hljs-literal\">True<\/span>:<br \/>\n        conn, addr = srv.accept()<br \/>\n        <span class=\"hljs-comment\"># hand each connection its own thread so clients don&#8217;t block one another<\/span><br \/>\n        t = threading.Thread(target=handle_client, args=(conn, addr), daemon=<span class=\"hljs-literal\">True<\/span>)<br \/>\n        t.start()<\/p>\n<p><span class=\"hljs-keyword\">if<\/span> __name__ == <span class=\"hljs-string\">&#8216;__main__&#8217;<\/span>:<br \/>\n    run_server()<\/p>\n<h2>Building the Client<\/h2>\n<p>Whatever the user types is encrypted and sent to the server; whatever arrives at the client is read and decrypted. The client also verifies the server&#8217;s HMAC tag before trusting its public key.<\/p>\n<p><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">run_client<\/span>(<span class=\"hljs-params\">host=<span class=\"hljs-string\">&#8216;127.0.0.1&#8217;<\/span>, port=<span class=\"hljs-number\">9000<\/span><\/span>):<\/span><br \/>\n    sock = socket.create_connection((host, port))<br \/>\n    priv, pub = make_keypair()<br \/>\n    <span class=\"hljs-comment\"># server sends its public key first in this version, then we send ours<\/span><br \/>\n    peer_pub = recv_framed(sock)<br \/>\n    peer_tag = recv_framed(sock)<br \/>\n    <span class=\"hljs-keyword\">try<\/span>:<br \/>\n        verify_pub(peer_pub, peer_tag)<br \/>\n    <span class=\"hljs-keyword\">except<\/span> ValueError:<br \/>\n        <span class=\"hljs-built_in\">print<\/span>(<span class=\"hljs-string\">&#8216;[!] server failed handshake authentication, aborting&#8217;<\/span>)<br \/>\n        sock.close()<br \/>\n        <span class=\"hljs-keyword\">return<\/span><br \/>\n    send_framed(sock, pub)<br \/>\n    send_framed(sock, sign_pub(pub))<br \/>\n    session_key = get_session_key(priv, peer_pub)<\/p>\n<p>    <span class=\"hljs-keyword\">while<\/span> <span class=\"hljs-literal\">True<\/span>:<br \/>\n        text = <span class=\"hljs-built_in\">input<\/span>(<span class=\"hljs-string\">&#8216;&gt; &#8216;<\/span>)<br \/>\n        <span class=\"hljs-keyword\">if<\/span> text.lower() == <span class=\"hljs-string\">&#8216;quit&#8217;<\/span>:<br \/>\n            <span class=\"hljs-keyword\">break<\/span><br \/>\n        send_framed(sock, encrypt(session_key, text.encode()))<br \/>\n        reply = decrypt(session_key, recv_framed(sock))<br \/>\n        <span class=\"hljs-built_in\">print<\/span>(reply.decode())<br \/>\n    sock.close()<\/p>\n<p><span class=\"hljs-keyword\">if<\/span> __name__ == <span class=\"hljs-string\">&#8216;__main__&#8217;<\/span>:<br \/>\n    run_client()<\/p>\n<h2>Running It: Multiple Clients<\/h2>\n<p>Start the server, then start two or three terminals in parallel running client.py \u2014 each gets its own handshake and session key, handled independently on its own thread by the server.<\/p>\n<h2>Inspecting Encrypted Traffic<\/h2>\n<p>Install a packet capture tool (tcpdump), capture traffic from a client mid-conversation, and view it in Wireshark. The length prefixes are visible, but the payload is unreadable \u2014 confirmation that the AES-GCM layer is doing its job.<\/p>\n<p>tcpdump -i lo -w capture.pcap port 9000<\/p>\n<h2>Testing: Wrong Key \/ Tampered Ciphertext<\/h2>\n<p>A single changed bit causes decryption to raise an exception instead of returning corrupted data with no indication anything went wrong \u2014 that&#8217;s the authentication tag at work.<\/p>\n<p><span class=\"hljs-comment\"># swap the real key for a random one and watch it blow up<\/span><br \/>\n<span class=\"hljs-keyword\">try<\/span>:<br \/>\n    decrypt(os.urandom(<span class=\"hljs-number\">32<\/span>), encrypt(session_key, <span class=\"hljs-string\">b&#8217;hello&#8217;<\/span>))<br \/>\n<span class=\"hljs-keyword\">except<\/span> Exception <span class=\"hljs-keyword\">as<\/span> e:<br \/>\n    <span class=\"hljs-built_in\">print<\/span>(<span class=\"hljs-string\">&#8216;decryption failed as expected:&#8217;<\/span>, e)<\/p>\n<h2>Handling Connection Errors<\/h2>\n<p>The server&#8217;s handle_client loop already wraps recv_framed in a try\/except (see the &#171;Building the Server&#187; section above), so a client that disconnects mid-session \u2014 or sends a malformed or oversized frame \u2014 just ends that one thread cleanly rather than crashing the server or affecting other connected clients. On the client side, a dropped server connection surfaces as a ConnectionError from recv_framed\/send_framed; wrap the client&#8217;s while True loop the same way if you want a clean error message instead of a stack trace.<\/p>\n<h2>How This Compares to a Real VPN<\/h2>\n<p>To be clear, this isn&#8217;t a real VPN. Production VPN protocols, according to the <a href=\"https:\/\/vpnoverview.com\/\" target=\"_blank\" rel=\"noopener\">VPNOverview research<\/a>, are distinguished by the fact that they work at the IP level with the tabling of entire network traffic flow in virtual interfaces (TUN\/TAP) instead of just securing a particular application-level socket connection. The definition of what we&#8217;ve created here is to secure one conversation between two endpoints; a true VPN secures all communications that the operating system streams to the network.<\/p>\n<h2>Where to Go From Here<\/h2>\n<p>There are two natural next steps: make this protocol work with a real TUN\/TAP interface so traffic actually passes through it, and replace the hand-rolled crypto layer with a more vetted framework such as the Noise Protocol Framework or libsodium \u2014 both handle things like replay protection and key rotation that were deliberately left out here for clarity. The PSK-based HMAC signing shown above is a minimal fix for the handshake authentication gap; a production system would more likely use mutual TLS or a Noise handshake pattern (e.g. Noise_XX or Noise_KK) to get that, along with replay protection, for free.<\/p>\n<h2>Conclusion<\/h2>\n<p>We designed a protocol for a client-server application with proper message framing, elliptic-curve key exchange, authenticated encryption, and multi-client support \u2014 without a single fixed secret baked into the code. It&#8217;s not a VPN, but it&#8217;s the foundation one is built on. The honest next step from here is TUN\/TAP tunneling paired with a production-quality crypto library, rather than continuing to extend a hand-rolled one.<\/p>","protected":false},"excerpt":{"rendered":"<p>This course focuses on constructing a Secure Client-Server Protocol using the Python programming language, including security for sockets, framing, and key exchange. If you ever wanted to implement your own secure communication protocol in Python, using only the standard Python socket library and the cryptography library, this is how to &#8230; <\/p>\n<div><a class=\"more-link bs-book_btn\" href=\"https:\/\/imcodinggenius.com\/?p=939\">Read More<\/a><\/div>\n","protected":false},"author":0,"featured_media":940,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-939","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-news"],"_links":{"self":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/posts\/939"}],"collection":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=939"}],"version-history":[{"count":0,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/posts\/939\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/media\/940"}],"wp:attachment":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=939"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=939"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=939"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}