<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Smile</title>
    <description>The latest articles on DEV Community by Smile (@smileuwu).</description>
    <link>https://dev.to/smileuwu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3902484%2F88d7429c-8d3a-45c0-a8fc-c6b82c97314a.png</url>
      <title>DEV Community: Smile</title>
      <link>https://dev.to/smileuwu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/smileuwu"/>
    <language>en</language>
    <item>
      <title>The Story of How I Built a VPN protocol: Part 1</title>
      <dc:creator>Smile</dc:creator>
      <pubDate>Fri, 01 May 2026 22:21:13 +0000</pubDate>
      <link>https://dev.to/smileuwu/the-story-of-how-i-built-a-vpn-protocol-part-1-35d0</link>
      <guid>https://dev.to/smileuwu/the-story-of-how-i-built-a-vpn-protocol-part-1-35d0</guid>
      <description>&lt;h1&gt;
  
  
  🚨🚨🚨 Disclaimer 🚨🚨🚨
&lt;/h1&gt;

&lt;p&gt;This article and the VPN itself are written &lt;strong&gt;for educational purposes only&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  How It All Started
&lt;/h1&gt;

&lt;p&gt;I recently switched to Arch. Everything started off well: I installed all the utilities I needed, and then I decided to install the VPN I used to use. And then a problem appeared — it doesn't work on Arch (even as an AppImage).&lt;/p&gt;

&lt;p&gt;My provider also supported Shadowsocks, but instead of using it, I decided to write my own VPN. For more practice.&lt;/p&gt;




&lt;h1&gt;
  
  
  VPN Protocol
&lt;/h1&gt;

&lt;p&gt;My VPN protocol is designed for maximum stealth. In my opinion, one of the most important things here is &lt;strong&gt;encryption from the very first packet&lt;/strong&gt;. In my protocol, this is implemented just like in Shadowsocks — with a pre-shared key.&lt;/p&gt;

&lt;p&gt;Encryption algorithm: &lt;strong&gt;ChaCha20-Poly1305&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It's also worth mentioning that the protocol works over &lt;strong&gt;TCP&lt;/strong&gt;. A random amount of junk bytes is added to each packet for length obfuscation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Packet Structure
&lt;/h2&gt;

&lt;p&gt;Each packet has a 5-byte header that is masked as encrypted data using XOR with the first 5 bytes of the key.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;First 2 bytes&lt;/strong&gt; — total packet length. Needed to determine where the packet ends (since TCP can segment packets).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third byte&lt;/strong&gt; — flags byte. Currently only 2 flags are used:

&lt;ul&gt;
&lt;li&gt;Bit 1 — indicates that this packet is fake and should not be processed (not yet implemented).&lt;/li&gt;
&lt;li&gt;Bit 2 — flag for performing ECDH (Elliptic Curve Diffie‑Hellman).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Last 2 bytes&lt;/strong&gt; — ciphertext length, used to separate junk bytes from the ciphertext.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Then comes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;12 bytes&lt;/strong&gt; — randomly generated nonce;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ciphertext&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AEAD&lt;/strong&gt; (authentication tag);&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;junk bytes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Handshake and Key Exchange
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. First packet from the client
&lt;/h3&gt;

&lt;p&gt;The client sends its &lt;strong&gt;16-byte username&lt;/strong&gt; to the server (encrypted, of course).&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Server response
&lt;/h3&gt;

&lt;p&gt;If the server finds a user with that username, it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sends the client a randomly generated &lt;strong&gt;32-byte salt&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;starts computing the keys:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;sending key&lt;/strong&gt; (server → client)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;receiving key&lt;/strong&gt; (server ← client)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Key computation on the server
&lt;/h3&gt;

&lt;p&gt;The server stores the user's password in plaintext.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Receiving key&lt;/strong&gt; (for decrypting from the client) = hash(password + &lt;strong&gt;first 16 bytes&lt;/strong&gt; of salt).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sending key&lt;/strong&gt; (for encrypting to the client) = hash(password + &lt;strong&gt;last 16 bytes&lt;/strong&gt; of salt).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Client actions
&lt;/h3&gt;

&lt;p&gt;The client receives the salt, decrypts it, and does the same thing, &lt;strong&gt;but the key roles are inverted&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what is the sending key for the server becomes the receiving key for the client, and vice versa.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. ECDH and connection finalization
&lt;/h3&gt;

&lt;p&gt;After the client has generated the keys, it generates an &lt;strong&gt;ephemeral key pair&lt;/strong&gt; based on the &lt;strong&gt;Curve25519&lt;/strong&gt; elliptic curve (this pair is needed for ECDH). It then sends a connection confirmation (first byte = &lt;code&gt;0xFF&lt;/code&gt;) along with the public ephemeral key, setting the ECDH flag.&lt;/p&gt;

&lt;p&gt;The server receives the packet, deobfuscates it, and gets the confirmation and the client's ephemeral key. Then it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;assigns an IP address to the client from a local private network;&lt;/li&gt;
&lt;li&gt;generates its own ephemeral key pair;&lt;/li&gt;
&lt;li&gt;sends the client its assigned IP address and the server's public key;&lt;/li&gt;
&lt;li&gt;performs the ECDH round.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After sending, the server updates its keys by hashing the old keys with the secret obtained from ECDH.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Client finalization
&lt;/h3&gt;

&lt;p&gt;After receiving the packet with the IP address and the server's public ephemeral key, the client:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;creates a local tunnel;&lt;/li&gt;
&lt;li&gt;sets its IP address (received from the server);&lt;/li&gt;
&lt;li&gt;performs the ECDH round;&lt;/li&gt;
&lt;li&gt;updates its keys.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Main Work Loop
&lt;/h2&gt;

&lt;p&gt;After the connection is established and keys are generated, the main work loop begins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Client Side
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;3 goroutines&lt;/strong&gt; run on the client side:&lt;/p&gt;

&lt;h4&gt;
  
  
  First goroutine (reading from the tunnel and preparing packets)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Reads packets from the tunnel.&lt;/li&gt;
&lt;li&gt;Generates an &lt;strong&gt;8-byte salt&lt;/strong&gt; to update the sending key (by hashing the old sending key with the salt).&lt;/li&gt;
&lt;li&gt;Adds this 8-byte salt to the beginning of the plaintext (the salt is followed by the packet read from the tunnel).&lt;/li&gt;
&lt;li&gt;Encrypts everything.&lt;/li&gt;
&lt;li&gt;Adds random junk bytes for obfuscation.&lt;/li&gt;
&lt;li&gt;Stores the prepared packet in a buffer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Second goroutine (sending packets)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Responsible for sending already prepared packets.&lt;/li&gt;
&lt;li&gt;Packets are sent in &lt;strong&gt;batches of 1 to 5 packets&lt;/strong&gt; (the protocol is of course segmented at OSI layers 3 and 4, but I can't influence that).&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Third goroutine (receiving packets from the server)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Responsible for receiving packets from the server.&lt;/li&gt;
&lt;li&gt;Performs deobfuscation and decryption.&lt;/li&gt;
&lt;li&gt;Writes the decrypted data to the tunnel.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Server Side
&lt;/h3&gt;

&lt;p&gt;The server has &lt;strong&gt;3 main goroutines&lt;/strong&gt;, plus additional goroutines for receiving packets from clients.&lt;/p&gt;

&lt;h4&gt;
  
  
  First goroutine (handshake handling)
&lt;/h4&gt;

&lt;p&gt;Handles incoming handshake requests from clients. If the handshake is successful, a &lt;strong&gt;new goroutine&lt;/strong&gt; is created to process packets sent by that client.&lt;/p&gt;

&lt;h4&gt;
  
  
  Second goroutine (reading from the tunnel)
&lt;/h4&gt;

&lt;p&gt;Reads packets from the tunnel and sends them to clients.&lt;/p&gt;

&lt;h4&gt;
  
  
  Third goroutine (cleaning inactive connections)
&lt;/h4&gt;

&lt;p&gt;Cleans up inactive connections.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Updates
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Salt in every packet
&lt;/h3&gt;

&lt;p&gt;Every packet (whether from client or server) contains a &lt;strong&gt;salt&lt;/strong&gt;. It is used to update the keys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The server&lt;/strong&gt;, when sending a packet, includes a salt. After sending, it updates its &lt;strong&gt;sending key&lt;/strong&gt; by hashing the old key with that salt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The client&lt;/strong&gt;, when receiving and decrypting a packet, also updates a key — but not the sending key, the &lt;strong&gt;receiving key&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;When the client sends a packet, the same happens, but the roles are reversed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Periodic ECDH updates
&lt;/h3&gt;

&lt;p&gt;Every &lt;strong&gt;4 minutes&lt;/strong&gt; or after sending &lt;strong&gt;2³² packets&lt;/strong&gt; (whichever comes first), keys are updated using ECDH on elliptic curves. The keys are transmitted along with data packets.&lt;/p&gt;

&lt;p&gt;And that, in fact, is the entire protocol. During implementation, I thought about writing it in &lt;strong&gt;Go&lt;/strong&gt; or &lt;strong&gt;Rust&lt;/strong&gt;. I chose Go for its simplicity.&lt;/p&gt;




&lt;h1&gt;
  
  
  Implementation Process
&lt;/h1&gt;

&lt;p&gt;To be honest, the protocol architecture was mostly developed while writing the code. It has quite a few problems — both in terms of protocol design and implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example problems
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Constant username packet length&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The encrypted username packet has a constant length of 44 bytes (12 bytes nonce, 16 bytes ciphertext, and a 16-byte AEAD tag). Knowing this and that the user is using this protocol, you can calculate the 4th and 5th bytes of the key.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Repository duplication&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
I foolishly created two separate repositories — one for the client and one for the server. As a result, the branches containing common modules just duplicate each other.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Git flow&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
I tried to follow git flow, but failed here too.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vulnerabilities&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
I also have a feeling that there are more vulnerabilities in the code than working logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No graceful shutdown&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
There is no proper negotiated client-server disconnect — just a connection break.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Although considering this is my first project, I think it didn't turn out too badly. If anyone wants to check out this mess, here are the links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client&lt;/strong&gt;: &lt;a href="https://github.com/SmileUwUI/smileTun-client" rel="noopener noreferrer"&gt;https://github.com/SmileUwUI/smileTun-client&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server&lt;/strong&gt;: &lt;a href="https://github.com/SmileUwUI/smileTun-server" rel="noopener noreferrer"&gt;https://github.com/SmileUwUI/smileTun-server&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Currently, the implementation works. And I'm writing this article through my own VPN protocol.&lt;/p&gt;




&lt;h1&gt;
  
  
  Future Plans
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Merge both repositories into one.&lt;/li&gt;
&lt;li&gt;Add fake packet sending.&lt;/li&gt;
&lt;li&gt;Add TLS mimicry.&lt;/li&gt;
&lt;li&gt;And much more.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If anyone has any questions or recommendations — leave them in the comments. For now, I bid you farewell. Good luck to everyone!&lt;/p&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>go</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
