Excellent question — since Tailscale is built on top of WireGuard, understanding WireGuard will help you see the foundation of modern secure networking.
Let’s go step by step 👇
🧩 What is WireGuard?
WireGuard is a modern, lightweight, and open-source VPN (Virtual Private Network) protocol and implementation.
It’s designed to be simpler, faster, and more secure than traditional VPN protocols like IPSec or OpenVPN.
Key facts:
- Created by Jason A. Donenfeld in 2015.
- Officially merged into the Linux kernel in 2020 (Linux 5.6).
- Available for all major platforms — Linux, macOS, Windows, Android, iOS.
- Licensed under the GPLv2 open source license.
⚙️ How WireGuard Works (Simplified Flow)
WireGuard uses public key cryptography and a concept called peers to establish secure tunnels between devices.
Each device that connects to the network is called a peer.
Here’s the overall flow:
1. 🔑 Key Pair Generation
Each peer generates:
- A private key (kept secret)
- A public key (shared with others)
This works like SSH:
- Your private key stays on your device.
- Your public key identifies you to others.
2. 📜 Configuration
Each peer (e.g., a server or client) has a small configuration file specifying:
- Its private key
- Its internal VPN IP (e.g.,
10.0.0.2/24) - The public key of the remote peer
- The endpoint (IP:port) of the remote peer
- Allowed IPs (what traffic should go through the tunnel)
Example:
# On client
[Interface]
PrivateKey = <client-private-key>
Address = 10.0.0.2/24
[Peer]
PublicKey = <server-public-key>
Endpoint = 203.0.113.5:51820
AllowedIPs = 0.0.0.0/0
3. 🧠 Handshake Process
When two peers want to communicate:
- The client initiates a handshake using Noise Protocol Framework, a modern, minimal cryptographic handshake protocol.
- It uses Curve25519 for key exchange, ChaCha20 for encryption, Poly1305 for authentication, and BLAKE2s for hashing.
- The handshake results in ephemeral session keys (short-lived), adding perfect forward secrecy.
Result: Both peers derive symmetric encryption keys and start a secure tunnel.
4. 🔒 Encryption & Tunnel Creation
Once the handshake is done:
- All packets between peers are encrypted using ChaCha20-Poly1305.
- Only packets from authenticated peers (known public keys) are accepted.
- Each peer directly sends UDP packets to the other (default port
51820). - WireGuard sits at Layer 3 (network layer), providing an encrypted IP tunnel.
5. ⚡ Data Transfer
- Data moves seamlessly between peers via the tunnel.
- WireGuard doesn’t use “sessions” — it’s stateless and only keeps minimal state.
- If a peer disappears (e.g., changes IP), the other peer can reconnect automatically without restarting.
🧠 Architectural Simplicity
| Feature | WireGuard | IPSec | OpenVPN |
|---|---|---|---|
| Codebase size | ~4,000 lines | 400,000+ | 600,000+ |
| Speed | Very fast | Moderate | Slower |
| Security | Modern crypto | Older crypto, complex | Older crypto |
| Config | Simple (few lines) | Complex | Complex |
| OS Support | Kernel-level | Kernel/user | User space |
| Transport | UDP only | UDP/TCP | UDP/TCP |
Because it’s tiny, it’s easier to audit, maintain, and embed in apps and devices.
🔐 Key Security Principles
WireGuard uses:
- ChaCha20 — for encryption
- Poly1305 — for message authentication
- Curve25519 — for key exchange
- BLAKE2s — for hashing
- SipHash24 — for hashtable keys
- HKDF — for key derivation
It follows a strict “cryptographic minimalism” approach — no optional algorithms, no negotiation, fewer mistakes.
🌐 Network Topologies Supported
WireGuard supports:
- Point-to-point (single client ↔ single server)
- Mesh networks (each peer talks directly to others)
- Hub-and-spoke setups (many clients connect to one server)
- Subnet routing (a peer acts as a gateway for a LAN)
- Roaming clients (e.g., mobile devices changing IPs)
This flexibility is what allows Tailscale to build its mesh VPN model over WireGuard.
🧩 Integration with Tailscale
Tailscale uses WireGuard’s core tunneling and encryption features, but adds:
- Automatic key distribution
- NAT traversal
- DNS management
- Access control (ACLs)
- Single Sign-On (SSO)
- DERP relay fallback
So Tailscale handles all the coordination and policy logic, while WireGuard does the packet encryption and secure transport.
🚀 Advantages
✅ Blazing fast — minimal CPU overhead.
✅ Secure by design — modern cryptography only.
✅ Tiny codebase — easier auditing.
✅ Cross-platform — runs almost everywhere.
✅ Simple setup — just a few lines of config.
⚠️ Limitations
- No built-in user authentication (depends on key pairs).
- No built-in key distribution (handled manually or by tools like Tailscale).
- UDP-only (some networks block UDP).
- Kernel integration may vary by OS.
flowchart LR
%% ===============
%% WIREGUARD ARCHITECTURE (COMPONENTS + DATA PATHS)
%% ===============
%% -------- Peer A --------
subgraph PeerA["Peer A"]
direction TB
AOS["OS Kernel"]
subgraph A_WG["WireGuard Kernel Module (wg)"]
direction TB
A_wg0_iface["wg0 (virtual interface)"]
A_Crypto["ChaCha20-Poly1305\nCurve25519 key exchange\nBLAKE2s hashing\nHKDF KDF"]
A_Keys["Key Material\n- Private key (A)\n- Remote Public key (B)\n- Ephemeral session keys"]
A_Route["AllowedIPs table\n(what prefixes go to peer B)"]
end
A_Net["NIC + UDP 51820"]
end
%% -------- Peer B --------
subgraph PeerB["Peer B"]
direction TB
BOS["OS Kernel"]
subgraph B_WG["WireGuard Kernel Module (wg)"]
direction TB
B_wg0_iface["wg0 (virtual interface)"]
B_Crypto["ChaCha20-Poly1305\nCurve25519 key exchange\nBLAKE2s hashing\nHKDF KDF"]
B_Keys["Key Material\n- Private key (B)\n- Remote Public key (A)\n- Ephemeral session keys"]
B_Route["AllowedIPs table\n(routes for Peer A)"]
end
B_Net["NIC + UDP 51820"]
end
%% -------- Apps --------
subgraph AppA["User Space Apps (Peer A)"]
direction TB
A_App1["App (TCP/UDP)"]
end
subgraph AppB["User Space Apps (Peer B)"]
direction TB
B_App1["Service (TCP/UDP)"]
end
%% -------- Data path: A -> B --------
A_App1 -->|"IP dst in AllowedIPs(B)"| AOS
AOS -->|"route decision"| A_wg0_iface
A_wg0_iface -->|"encrypt packet"| A_Crypto --> A_Net
A_Net -->|"UDP/IPv4 or v6 (outer) over Internet"| B_Net
B_Net -->|"decrypt"| B_Crypto --> B_wg0_iface
B_wg0_iface -->|"deliver inner packet"| BOS --> B_App1
%% -------- Data path: B -> A (return) --------
B_App1 --> BOS --> B_wg0_iface --> B_Crypto --> B_Net --> A_Net --> A_Crypto --> A_wg0_iface --> AOS --> A_App1
%% -------- Styling --------
classDef box fill:#f6f6ff,stroke:#6366f1,stroke-width:1px,color:#111;
classDef wg fill:#eefbf4,stroke:#10b981,stroke-width:1px,color:#111;
classDef net fill:#fff7ed,stroke:#f59e0b,stroke-width:1px,color:#111;
classDef app fill:#fafafa,stroke:#9ca3af,stroke-width:1px,color:#111;
class A_WG,B_WG wg
class A_Net,B_Net net
class A_App1,B_App1 app
class A_wg0_iface,B_wg0_iface,A_Crypto,B_Crypto,A_Keys,B_Keys,A_Route,B_Route box

sequenceDiagram participant A as Peer_A_Initiator participant Net as Internet_UDP participant B as Peer_B_Responder A->>B: Initiation (Noise IK): Ae, encrypted static proof, timestamp_counter activate B B-->>A: Response: Be, encrypted payload (identity, cookie, etc.) deactivate B A->>B: First encrypted data packet (under session key) B-->>A: First encrypted data packet (reverse) rect rgba(255,248,220,0.5) A->>B: Roaming re-initiation from new IP_port tuple B-->>A: Validate tuple and rotate keys end
sequenceDiagram participant A as Peer_A_Initiator participant Net as Internet_UDP participant B as Peer_B_Responder Note over A,B: Static public keys are exchanged out of band. Peer A knows public key of B and Peer B knows public key of A. Each peer holds its own private key. A->>B: Initiation using Noise_IK pattern. Includes Ae ephemeral key and encrypted static proof with timestamp counter. activate B B-->>A: Response with Be ephemeral key and encrypted payload such as identity and cookie. deactivate B Note over A,B: Both sides derive session keys using HKDF based on static and ephemeral keys for ChaCha20 Poly1305 encryption. A->>B: First encrypted data packet using session key. B-->>A: First encrypted data packet in reverse direction. Note over A,B: Rekeying happens periodically or by counter timeout. PersistentKeepalive may maintain NAT bindings. rect rgba(255,248,220,0.5) Note over A,B: When IP or port changes the initiator sends new initiation from updated tuple. Responder validates and rotates keys seamlessly. end
flowchart TB
%% ==========================
%% DATA PLANE PACKET FLOW AND ROUTING (IN-DEPTH)
%% ==========================
subgraph A["Peer A"]
A_APP["App generates packet\nDst 10.0.0.2:443"]:::app
A_L3["Kernel Routing Table"]:::box
A_WG["wg0"]:::wg
A_ENC["Encrypt inner IP packet -> ChaCha20 Poly1305"]:::box
A_OUT["Outer UDP packet\nSrc A_public_ip:51820\nDst B_public_ip:51820"]:::net
end
subgraph WAN["Internet / NATs / Firewalls"]
NATs["NAT traversal aided by:\n- UDP 51820\n- optional PersistentKeepalive 25s\n- stateful pinholes"]:::note
end
subgraph B["Peer B"]
B_IN["Receive outer UDP"]:::net
B_DEC["Decrypt to recover inner IP packet"]:::box
B_WG["wg0"]:::wg
B_L3["Kernel Routing Table"]:::box
B_APP["Service consumes packet\n10.0.0.2:443"]:::app
end
A_APP --> A_L3 -->|prefix match 10.0.0.0/24 in AllowedIPs of B| A_WG --> A_ENC --> A_OUT --> NATs --> B_IN --> B_DEC --> B_WG --> B_L3 --> B_APP
classDef wg fill:#e8fff5,stroke:#10b981,color:#111;
classDef net fill:#fff7ed,stroke:#f59e0b,color:#111;
classDef app fill:#f4f4f5,stroke:#9ca3af,color:#111;
classDef box fill:#eef2ff,stroke:#6366f1,color:#111;
classDef note fill:#ffffff,stroke:#d1d5db,color:#111;
flowchart LR
%% ==========================
%% CONFIG RELATIONSHIPS (wg-quick / wg)
%% ==========================
subgraph Local["Peer Local Config"]
IFACE["Interface"]:::sec
IF_Pri["PrivateKey = A_priv"]:::box
IF_Addr["Address = 10.0.0.1/24"]:::box
IF_Port["ListenPort = 51820"]:::box
IF_PostUp["PostUp / PostDown actions such as iptables, routes, sysctl"]:::box
end
subgraph PeerBConf["Peer Entry for B"]
PB_Pub["PublicKey = B_pub"]:::box
PB_Endp["Endpoint = B_public_ip:51820"]:::box
PB_Allowed["AllowedIPs = 10.0.0.2/32 and 10.10.0.0/16"]:::box
PB_PSK["Optional PresharedKey"]:::box
PB_Keep["PersistentKeepalive = 25"]:::box
end
IFACE --- IF_Pri & IF_Addr & IF_Port & IF_PostUp
IFACE --- PeerBConf
PeerBConf --- PB_Pub & PB_Endp & PB_Allowed & PB_PSK & PB_Keep
classDef sec fill:#fef3c7,stroke:#f59e0b,color:#111;
classDef box fill:#eef2ff,stroke:#6366f1,color:#111;
stateDiagram-v2 %% ========================== %% KEY/TIMER LIFECYCLE (SIMPLIFIED) %% ========================== [*] --> NoSession: No valid session keys NoSession --> Initiating: Send Handshake Initiation Initiating --> Established: Receive valid Handshake Response\n(Derive session keys) Established --> Rekeying: Timer/Counter threshold reached\n(or peer IP/port changed) Rekeying --> Established: New keys installed (make-before-break) Established --> Idle: No user data for a while Idle --> Rekeying: Rekey to refresh keys on inactivity Idle --> Keepalive: Send PersistentKeepalive (if set) to maintain NAT Keepalive --> Established: Peer responds / pinhole intact Established --> [*]: Interface down / peer removed / keys cleared
flowchart TB
%% ==========================
%% SUBNET ROUTER / GATEWAY PEER (HUB-AND-SPOKE EXAMPLE)
%% ==========================
subgraph Spoke1["Spoke Peer A"]
A_IF[[wg0]]:::wg
A_Allowed[AllowedIPs includes 10.20.0.0/16 via Hub]:::box
end
subgraph Hub["Hub Peer (also a Subnet Router)"]
H_IF[[wg0]]:::wg
H_Forward[IP Forwarding enabled]:::box
H_Routes[Routes to on-prem LANs:<br/>10.20.0.0/16, 10.21.0.0/16]:::box
LAN1[(On-Prem LAN 10.20/16)]:::net
LAN2[(On-Prem LAN 10.21/16)]:::net
end
subgraph Spoke2["Spoke Peer B"]
B_IF[[wg0]]:::wg
B_Allowed[AllowedIPs includes 10.20.0.0/16 via Hub]:::box
end
A_IF -- encrypted UDP --> H_IF
B_IF -- encrypted UDP --> H_IF
H_IF --> H_Forward --> H_Routes --> LAN1
H_Routes --> LAN2
classDef wg fill:#e8fff5,stroke:#10b981,color:#111;
classDef net fill:#fff7ed,stroke:#f59e0b,color:#111;
classDef box fill:#eef2ff,stroke:#6366f1,color:#111;