{"slug": "a-vpn-is-a-lie-you-tell-your-kernel", "title": "A VPN Is a Lie You Tell Your Kernel", "summary": "Maneshwar, the developer behind git-lrc, explains how EdgeVPN, a decentralized VPN written in Go, works without a central server by using TUN devices and a gossiped ledger for peer discovery and routing. The project generates a token that allows machines to find each other across the internet and route packets without a control plane or cloud bill.", "body_md": "*Hello, I'm Maneshwar. I'm building git-lrc, a Micro AI code reviewer that runs on every commit. It is free and source-available on Github. Star git-lrc to help devs discover the project. Do give it a try and share your feedback.*\n\nA VPN is mostly a polite lie you tell your kernel. Here is how EdgeVPN tells it without a single central server, explained by actually reading the Go\n\nI have used VPNs for years without really knowing what one *is*.\n\nI typed `wg-quick up`\n\n, saw a new interface show up in `ip a`\n\n, and moved on with my life.\n\nClassic dev move: it works, ship it, never look inside.\n\nThen I found [EdgeVPN](https://github.com/mudler/edgevpn), a VPN written in Go that has **no central server at all**.\n\nYou generate a token, paste it on two machines, and they find each other across the internet and start routing packets.\n\nNo control plane, no `10.0.0.1`\n\nserver you have to keep alive, no cloud bill.\n\nThat sounded suspiciously like magic, so I did the only reasonable thing and read the whole codebase.\n\nThis post is me walking you through what I found, from \"what even is a network interface\" all the way down to how a single IP packet crosses the planet without anyone in charge.\n\nGrab a coffee. This one has depth.\n\nA VPN is not a tunnel.\n\nA VPN is a **lie you tell your operating system**, and the OS is very willing to believe you.\n\nHere is the trick.\n\nLinux (and macOS, and Windows) lets you create a ** TUN device**.\n\nIt looks like a normal network interface, it shows up in `ifconfig`\n\n, it has an IP, the kernel happily routes traffic to it.\n\nBut it is not connected to any hardware.\n\nIt is connected to a file descriptor that your program holds.\n\nThat is the whole thing. Really:\n\nEverything else a VPN does (encryption, peer discovery, routing) is just deciding what happens in the gap between that read and that write.\n\nIf you can move bytes from machine A's file descriptor to machine B's file descriptor, congratulations, you have written a VPN.\n\nEverything after this is engineering.\n\nHere is EdgeVPN's read loop, and it is honestly almost anticlimactic:\n\n``` js\nfunc getFrame(ifce io.Reader, c *Config) (ethernet.Frame, error) {\n    var frame ethernet.Frame\n    frame.Resize(c.MTU)\n\n    n, err := ifce.Read([]byte(frame))\n    if err != nil {\n        return frame, errors.Wrap(err, \"could not read from interface\")\n    }\n\n    frame = frame[:n]\n    return frame, nil\n}\n```\n\nThat is it. That is the interface. `ifce`\n\nis the TUN device, read gives you a raw packet, and now it is your problem.\n\nNow the real question.\n\nMachine A wants to send a packet to `10.1.0.13`\n\n.\n\nMachine A reads that packet off the TUN device. Cool. **Where does it send it?**\n\n`10.1.0.13`\n\nis a made up address.\n\nIt exists only inside your virtual network.\n\nSomewhere out there is a real machine, behind a real [NAT](https://en.wikipedia.org/wiki/Network_address_translation), with a real (and probably changing) public IP, that has claimed that made up address.\n\nSomething has to map one to the other.\n\nWireGuard solves this by making you do it by hand.\n\nYou write `AllowedIPs`\n\nand an `Endpoint`\n\nin a config file for every peer.\n\nIt is simple, fast, and it does not scale to \"I have twelve laptops behind twelve different routers and one of them is my friend's Raspberry Pi.\"\n\nTraditional VPNs solve it with a **server**.\n\nThe server knows everyone, everyone talks to the server, the server is the phone book. Also the server is the single point of failure, the single point of trust, and the single thing you have to pay for.\n\nEdgeVPN solves it with a **gossiped ledger**, which is a fancy way of saying: every node keeps shouting what it knows, and everyone eventually agrees. Let me build up to it.\n\nEverything starts with one command:\n\n```\nedgevpn -g > config.yaml\n# or, as a portable token\nexport EDGEVPNTOKEN=$(edgevpn -g -b)\n```\n\nThe token is just that YAML, base64'd. Nothing more. Decode one and you get roughly this:\n\n```\notp:\n  dht:\n    interval: 9000\n    key: 7Kx...43-random-chars...\n    length: 43\n  crypto:\n    interval: 9000\n    key: Qz9...43-random-chars...\n    length: 43\nroom: bT2...43-random-chars...\nrendezvous: mP4...43-random-chars...\nmdns: xL8...43-random-chars...\nmax_message_size: 20971520\n```\n\nFive random strings and two intervals.\n\nNo IPs, no hostnames, no server address, no certificates.\n\nThis file **is** your network.\n\nWhich brings us to the most important sentence:\n\nWarningExposing this file or passing-it by is equivalent to give full control to the network.\n\nHold that thought, we are coming back to it with receipts.\n\nThe clever bit is those `otp`\n\nblocks.\n\nThose keys are not used directly.\n\nThey are fed into **TOTP**, the same algorithm behind the 6 digit codes in your authenticator app, except tuned to spit out a long base64 string instead:\n\n```\nfunc TOTP(f func() hash.Hash, digits int, t int, key string) string {\n    cfg := otp.Config{\n        Hash:     f,      // sha256 here\n        Digits:   digits, // 43\n        TimeStep: otp.TimeWindow(t), // 9000 seconds\n        Key:      key,\n        Format: func(hash []byte, nb int) string {\n            return base64.StdEncoding.EncodeToString(hash)[:nb]\n        },\n    }\n    return cfg.TOTP()\n}\n```\n\nSo every 9000 seconds (2.5 hours), every node in the network independently derives **the same new secret**, without talking to each other.\n\nTime is the only coordination they need. This gets used for two different things, and both are neat.\n\nTo meet a stranger on the internet you need a place to meet.\n\nEdgeVPN uses the ** IPFS Kademlia DHT** as that meeting point, announcing itself under a key that is just\n\n`TOTP(dht.key)`\n\n. So your rendezvous on a **public** hash table moves every couple of hours.\n\nA watcher sees peers cluster around a random looking key, then that key goes cold forever.\n\nOne subtle trap, nicely handled: if everyone rotates at exactly time T, a node with a skewed clock rotates early and for a moment nobody finds anybody.\n\nSo the DHT announces on the last two rendezvous points at once:\n\n```\nrendezvousHistory: Ring{Length: 2}\n```\n\nOverlapping windows. Unglamorous, and exactly the difference between \"works in the demo\" and \"works at 3am\".\n\nOn a LAN, mDNS does the same job with the `mdns`\n\nstring as the service tag.\n\nHere is the full bootstrap:\n\nNotice the ordering. Peers connect first, and **then** they exchange the routing table.\n\nThe routing table travels over the p2p network it describes. Bootstrapping is fun.\n\nConnected peers join a [GossipSub](https://docs.libp2p.io/concepts/pubsub/overview/) topic named from the other OTP.\n\nlibp2p already encrypts every connection, and EdgeVPN still wraps **another** AES layer around each message, keyed the same rotating way:\n\n```\nfunc (e *Node) sealkey() string {\n    return internalCrypto.MD5(internalCrypto.TOTP(sha256.New, e.config.SealKeyLength, e.config.SealKeyInterval, e.config.ExchangeKey))\n}\n```\n\nWhy double up? The rendezvous is public, so anyone can find the topic and subscribe.\n\nTransport encryption protects the hop, not the payload from someone who legitimately joined the room.\n\nThe seal means a lurker hears noise, and since the key retires every 2.5 hours, brute forcing captured traffic chases a key nobody uses anymore.\n\nLayers of secrets, rotating on a timer, all from one shared string. Most elegant idea in the codebase.\n\nNow the phone book. EdgeVPN keeps a tiny blockchain. Before you close the tab: no proof of work, no mining, no coin.\n\nIt is a chain of blocks purely for cheap ordering, and each block carries a `map[bucket]map[key]value`\n\n.\n\nThe buckets are hardcoded constants that read like a table of contents for the whole product:\n\n```\nconst (\n    FilesLedgerKey    = \"files\"\n    MachinesLedgerKey = \"machines\"\n    ServicesLedgerKey = \"services\"\n    UsersLedgerKey    = \"users\"\n    HealthCheckKey    = \"healthcheck\"\n    DNSKey            = \"dns\"\n    EgressService     = \"egress\"\n    TrustZoneKey      = \"trustzone\"\n    TrustZoneAuthKey  = \"trustzoneAuth\"\n)\n```\n\nThe routing table is just the `machines`\n\nbucket keyed by virtual IP.\n\nEvery feature here is \"write to a bucket, read other people's writes\".\n\nOnce that clicked the codebase went from clever to obvious, which is the best thing you can say about an architecture.\n\nConflict resolution is beautifully blunt:\n\n```\nlast := l.blockchain.Last()\nif block.Index > last.Index ||\n    (block.Index == last.Index && block.Hash > last.Hash) {\n    l.blockchain.Add(*block)\n}\n```\n\nHighest block wins, ties broken by higher hash so every node breaks them identically. Whole block replace, no vector clocks, no Raft.\n\n\"But that loses writes,\" you say. Yes! And it does not matter, because every node reannounces its own state on a ticker until it sees itself reflected in the chain.\n\nRead it, check if the world agrees with you, and if not, say it again. Louder. Forever.\n\nThe system converges not because the merge is smart but because everyone is relentlessly repetitive.\n\nWipe the entire chain and the nodes refill it in seconds.\n\nThe tradeoff is real and the docs own it: every block goes to every peer, so this is chatty by design.\n\nFine for a routing table, which is why the README warns you off latency sensitive workloads.\n\nHere is the life of one ping, with the real function names.\n\nThe lookup is where every piece meets:\n\n```\nvalue, found := ledger.GetKey(protocol.MachinesLedgerKey, dst)\nif !found {\n    return notFoundErr\n}\nmachine := &types.Machine{}\nvalue.Unmarshal(machine)\n\n// Decode the Peer\nd, err = peer.Decode(machine.PeerID)\n```\n\nVirtual IP goes in, libp2p peer ID comes out.\n\nThat one lookup replaces the entire VPN server, and libp2p handles the rest: NAT traversal, hole punching, relays, encryption, multiplexing.\n\nThe receiving end checks the sender against that same ledger before copying a byte, and resets the stream otherwise.\n\nIf you are not in my routing table, I will not read your packets.\n\nTwo jobs, one map.\n\nEvery node drops a timestamp into a `healthcheck`\n\nbucket, and anyone older than `maxtime`\n\ncounts as gone.\n\nBut some jobs need exactly one node to run them (scrubbing stale entries, handing out IPs).\n\nWith no server, who decides? This:\n\n```\nfunc Leader(actives []string) string {\n    leaderboard := map[string]uint32{}\n    leader := actives[0]\n\n    for _, a := range actives {\n        leaderboard[a] = hash(a) // fnv32a\n        if leaderboard[leader] < leaderboard[a] {\n            leader = a\n        }\n    }\n    return leader\n}\n```\n\nHash every live peer ID, highest wins. No votes, no terms, no Raft.\n\nEvery node computes it locally and agrees, because the ledger already synced the input.\n\nWhen the leader dies it falls out of the healthcheck bucket, everyone recomputes, and a new leader appears with zero messages exchanged.\n\nIs it Paxos? No. Does deciding who deletes stale map entries need Paxos? Also no. Right sized engineering is underrated.\n\nSame trick powers the built in DHCP: nodes without an IP wait, the leader among them takes the lowest free one, next node repeats.\n\nDistributed DHCP with no DHCP server, built from a hash function and patience.\n\nRemember that warning. In the default config, holding the token lets you write anything into any bucket. Including this:\n\n``` php\nmachines/10.1.0.13 -> { PeerID: \"my-peer-id-actually\" }\n```\n\nCongratulations, you just hijacked someone's virtual IP and every node will happily route their traffic to you.\n\nThe routing table has no concept of who owns which key.\n\nThat is the sort of thing you only catch by reading source, and to their credit the project keeps layering in defenses.\n\n**PeerGuardian** (`pkg/trustzone`\n\n) adds ECDSA challenge and response, so the token alone does not get you in.\n\n**Relay ACL** (`pkg/config/relay_acl.go`\n\n) gates who can use you as a circuit relay, with an open bootstrap window because a NAT'd peer often needs a relay *before* it can prove membership. Chicken and egg, at the network layer.\n\nThe good one is **ledger ownership** (`pkg/blockchain/sign.go`\n\n): sign every entry with the node's libp2p key and enforce per key ownership on merge, in three modes, `off`\n\n, `observe`\n\n, `enforce`\n\n.\n\nThat middle mode is the detail I liked most.\n\nTurn it on in a live network, watch the violation logs for a week, then flip to enforce.\n\nThat is how you ship a breaking security change to something you cannot restart all at once.\n\nThe README also says it plainly: *\"I'm not a security expert, and this software didn't went through a full security audit\"*.\n\nRespect for that instead of stamping \"military grade encryption\" on a landing page.\n\nZooming out, here is how the whole thing stacks.\n\nEverything above the node is optional and plugs into the same two extension points:\n\nThe dotted lines matter. Control traffic (who owns what) goes through the gossip ledger, slowly and to everyone.\n\nData traffic (your actual packets) goes point to point over a dedicated libp2p stream protocol, directly to the one peer that needs it.\n\nMixing those up is how you build something that falls over at four nodes.\n\nThe extension seam is two types, and once you see them you can read any feature in the repo:\n\n```\n// something long running that gets the node and the ledger\ntype NetworkService func(context.Context, Config, *Node, *blockchain.Ledger) error\n\n// something that handles a raw stream for a given protocol ID\ntype StreamHandler func(*Node, *blockchain.Ledger) func(stream network.Stream)\n```\n\nThe VPN is a `NetworkService`\n\n. So is DNS. So is the file transfer. So is the healthcheck.\n\nAdding a feature means picking a bucket name, announcing into it, and optionally claiming a protocol ID for the data path.\n\nThat is the whole framework.\n\n**Coordination is the actual product.** WireGuard says \"you coordinate, I will be fast\". Commercial VPNs say \"a server coordinates, pay us\". EdgeVPN says \"we gossip until we agree\". The question was never encryption.\n\n**Repeating yourself beats being clever.** Every node reannounces its own truth forever, so the merge logic gets to be ten dumb lines and still heals from total data loss. Idempotent shouting as a consistency model. I am stealing this.\n\nGo read some source code. The scariest part of most systems is the part you have not looked at yet, and it is usually about 300 lines.\n\nRepo is [github.com/mudler/edgevpn](https://github.com/mudler/edgevpn), docs are [here](https://mudler.github.io/edgevpn), and [libp2p's docs](https://docs.libp2p.io/) are where the real dark magic lives.\n\nAI agents write code fast. They also silently remove logic, change behavior, and introduce bugs — without telling you. You often find out in production.\n\ngit-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.\n\nAny feedback or contributors are welcome! It's online, source-available, and ready for anyone to use.\n\n⭐ Star it on GitHub:\n\n| [🇩🇰 Dansk](https://github.com/HexmosTech/git-lrc/readme/README.da.md) | [🇪🇸 Español](https://github.com/HexmosTech/git-lrc/readme/README.es.md) | [🇮🇷 Farsi](https://github.com/HexmosTech/git-lrc/readme/README.fa.md) | [🇫🇮 Suomi](https://github.com/HexmosTech/git-lrc/readme/README.fi.md) | [🇯🇵 日本語](https://github.com/HexmosTech/git-lrc/readme/README.ja.md) | [🇳🇴 Norsk](https://github.com/HexmosTech/git-lrc/readme/README.nn.md) | [🇵🇹 Português](https://github.com/HexmosTech/git-lrc/readme/README.pt.md) | [🇷🇺 Русский](https://github.com/HexmosTech/git-lrc/readme/README.ru.md) | [🇦🇱 Shqip](https://github.com/HexmosTech/git-lrc/readme/README.sq.md) | [🇨🇳 中文](https://github.com/HexmosTech/git-lrc/readme/README.zh.md) | [🇮🇳 हिन्दी](https://github.com/HexmosTech/git-lrc/readme/README.hi.md) |\n\nGenAI today is a **race car without brakes**. It accelerates fast -- you describe something, and large blocks of code appear instantly. But AI agents *silently break things*: they remove logic, relax constraints, introduce expensive cloud calls, leak credentials, and change behavior -- without telling you. You often find out in production.\n\n** git-lrc is your braking system.** It hooks into\n\n`git commit`\n\nand runs an AI review on every diff In short, git-lrc helps **Prevent Outages, Breaches, and Technical Debt Before They Happen**\n\n**At a glance:** [10 risk categories](https://github.com/HexmosTech/git-lrc#what-git-lrc-checks-for) · [100+ failure patterns tracked](https://github.com/HexmosTech/git-lrc#what-git-lrc-checks-for) · every commit…", "url": "https://wpnews.pro/news/a-vpn-is-a-lie-you-tell-your-kernel", "canonical_source": "https://dev.to/lovestaco/a-vpn-is-a-lie-you-tell-your-kernel-41dj", "published_at": "2026-07-27 12:24:24+00:00", "updated_at": "2026-07-27 12:33:56.713525+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["EdgeVPN", "Maneshwar", "git-lrc", "WireGuard", "Go"], "alternates": {"html": "https://wpnews.pro/news/a-vpn-is-a-lie-you-tell-your-kernel", "markdown": "https://wpnews.pro/news/a-vpn-is-a-lie-you-tell-your-kernel.md", "text": "https://wpnews.pro/news/a-vpn-is-a-lie-you-tell-your-kernel.txt", "jsonld": "https://wpnews.pro/news/a-vpn-is-a-lie-you-tell-your-kernel.jsonld"}}