A VPN Is a Lie You Tell Your Kernel 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. 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. A 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 I have used VPNs for years without really knowing what one is . I typed wg-quick up , saw a new interface show up in ip a , and moved on with my life. Classic dev move: it works, ship it, never look inside. Then I found EdgeVPN https://github.com/mudler/edgevpn , a VPN written in Go that has no central server at all . You generate a token, paste it on two machines, and they find each other across the internet and start routing packets. No control plane, no 10.0.0.1 server you have to keep alive, no cloud bill. That sounded suspiciously like magic, so I did the only reasonable thing and read the whole codebase. This 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. Grab a coffee. This one has depth. A VPN is not a tunnel. A VPN is a lie you tell your operating system , and the OS is very willing to believe you. Here is the trick. Linux and macOS, and Windows lets you create a TUN device . It looks like a normal network interface, it shows up in ifconfig , it has an IP, the kernel happily routes traffic to it. But it is not connected to any hardware. It is connected to a file descriptor that your program holds. That is the whole thing. Really: Everything else a VPN does encryption, peer discovery, routing is just deciding what happens in the gap between that read and that write. If you can move bytes from machine A's file descriptor to machine B's file descriptor, congratulations, you have written a VPN. Everything after this is engineering. Here is EdgeVPN's read loop, and it is honestly almost anticlimactic: js func getFrame ifce io.Reader, c Config ethernet.Frame, error { var frame ethernet.Frame frame.Resize c.MTU n, err := ifce.Read byte frame if err = nil { return frame, errors.Wrap err, "could not read from interface" } frame = frame :n return frame, nil } That is it. That is the interface. ifce is the TUN device, read gives you a raw packet, and now it is your problem. Now the real question. Machine A wants to send a packet to 10.1.0.13 . Machine A reads that packet off the TUN device. Cool. Where does it send it? 10.1.0.13 is a made up address. It exists only inside your virtual network. Somewhere 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. Something has to map one to the other. WireGuard solves this by making you do it by hand. You write AllowedIPs and an Endpoint in a config file for every peer. It 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." Traditional VPNs solve it with a server . The 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. EdgeVPN 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. Everything starts with one command: edgevpn -g config.yaml or, as a portable token export EDGEVPNTOKEN=$ edgevpn -g -b The token is just that YAML, base64'd. Nothing more. Decode one and you get roughly this: otp: dht: interval: 9000 key: 7Kx...43-random-chars... length: 43 crypto: interval: 9000 key: Qz9...43-random-chars... length: 43 room: bT2...43-random-chars... rendezvous: mP4...43-random-chars... mdns: xL8...43-random-chars... max message size: 20971520 Five random strings and two intervals. No IPs, no hostnames, no server address, no certificates. This file is your network. Which brings us to the most important sentence: WarningExposing this file or passing-it by is equivalent to give full control to the network. Hold that thought, we are coming back to it with receipts. The clever bit is those otp blocks. Those keys are not used directly. They 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: func TOTP f func hash.Hash, digits int, t int, key string string { cfg := otp.Config{ Hash: f, // sha256 here Digits: digits, // 43 TimeStep: otp.TimeWindow t , // 9000 seconds Key: key, Format: func hash byte, nb int string { return base64.StdEncoding.EncodeToString hash :nb }, } return cfg.TOTP } So every 9000 seconds 2.5 hours , every node in the network independently derives the same new secret , without talking to each other. Time is the only coordination they need. This gets used for two different things, and both are neat. To meet a stranger on the internet you need a place to meet. EdgeVPN uses the IPFS Kademlia DHT as that meeting point, announcing itself under a key that is just TOTP dht.key . So your rendezvous on a public hash table moves every couple of hours. A watcher sees peers cluster around a random looking key, then that key goes cold forever. One 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. So the DHT announces on the last two rendezvous points at once: rendezvousHistory: Ring{Length: 2} Overlapping windows. Unglamorous, and exactly the difference between "works in the demo" and "works at 3am". On a LAN, mDNS does the same job with the mdns string as the service tag. Here is the full bootstrap: Notice the ordering. Peers connect first, and then they exchange the routing table. The routing table travels over the p2p network it describes. Bootstrapping is fun. Connected peers join a GossipSub https://docs.libp2p.io/concepts/pubsub/overview/ topic named from the other OTP. libp2p already encrypts every connection, and EdgeVPN still wraps another AES layer around each message, keyed the same rotating way: func e Node sealkey string { return internalCrypto.MD5 internalCrypto.TOTP sha256.New, e.config.SealKeyLength, e.config.SealKeyInterval, e.config.ExchangeKey } Why double up? The rendezvous is public, so anyone can find the topic and subscribe. Transport encryption protects the hop, not the payload from someone who legitimately joined the room. The 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. Layers of secrets, rotating on a timer, all from one shared string. Most elegant idea in the codebase. Now the phone book. EdgeVPN keeps a tiny blockchain. Before you close the tab: no proof of work, no mining, no coin. It is a chain of blocks purely for cheap ordering, and each block carries a map bucket map key value . The buckets are hardcoded constants that read like a table of contents for the whole product: const FilesLedgerKey = "files" MachinesLedgerKey = "machines" ServicesLedgerKey = "services" UsersLedgerKey = "users" HealthCheckKey = "healthcheck" DNSKey = "dns" EgressService = "egress" TrustZoneKey = "trustzone" TrustZoneAuthKey = "trustzoneAuth" The routing table is just the machines bucket keyed by virtual IP. Every feature here is "write to a bucket, read other people's writes". Once that clicked the codebase went from clever to obvious, which is the best thing you can say about an architecture. Conflict resolution is beautifully blunt: last := l.blockchain.Last if block.Index last.Index || block.Index == last.Index && block.Hash last.Hash { l.blockchain.Add block } Highest block wins, ties broken by higher hash so every node breaks them identically. Whole block replace, no vector clocks, no Raft. "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. Read it, check if the world agrees with you, and if not, say it again. Louder. Forever. The system converges not because the merge is smart but because everyone is relentlessly repetitive. Wipe the entire chain and the nodes refill it in seconds. The tradeoff is real and the docs own it: every block goes to every peer, so this is chatty by design. Fine for a routing table, which is why the README warns you off latency sensitive workloads. Here is the life of one ping, with the real function names. The lookup is where every piece meets: value, found := ledger.GetKey protocol.MachinesLedgerKey, dst if found { return notFoundErr } machine := &types.Machine{} value.Unmarshal machine // Decode the Peer d, err = peer.Decode machine.PeerID Virtual IP goes in, libp2p peer ID comes out. That one lookup replaces the entire VPN server, and libp2p handles the rest: NAT traversal, hole punching, relays, encryption, multiplexing. The receiving end checks the sender against that same ledger before copying a byte, and resets the stream otherwise. If you are not in my routing table, I will not read your packets. Two jobs, one map. Every node drops a timestamp into a healthcheck bucket, and anyone older than maxtime counts as gone. But some jobs need exactly one node to run them scrubbing stale entries, handing out IPs . With no server, who decides? This: func Leader actives string string { leaderboard := map string uint32{} leader := actives 0 for , a := range actives { leaderboard a = hash a // fnv32a if leaderboard leader < leaderboard a { leader = a } } return leader } Hash every live peer ID, highest wins. No votes, no terms, no Raft. Every node computes it locally and agrees, because the ledger already synced the input. When the leader dies it falls out of the healthcheck bucket, everyone recomputes, and a new leader appears with zero messages exchanged. Is it Paxos? No. Does deciding who deletes stale map entries need Paxos? Also no. Right sized engineering is underrated. Same trick powers the built in DHCP: nodes without an IP wait, the leader among them takes the lowest free one, next node repeats. Distributed DHCP with no DHCP server, built from a hash function and patience. Remember that warning. In the default config, holding the token lets you write anything into any bucket. Including this: php machines/10.1.0.13 - { PeerID: "my-peer-id-actually" } Congratulations, you just hijacked someone's virtual IP and every node will happily route their traffic to you. The routing table has no concept of who owns which key. That is the sort of thing you only catch by reading source, and to their credit the project keeps layering in defenses. PeerGuardian pkg/trustzone adds ECDSA challenge and response, so the token alone does not get you in. Relay ACL pkg/config/relay acl.go 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. The good one is ledger ownership pkg/blockchain/sign.go : sign every entry with the node's libp2p key and enforce per key ownership on merge, in three modes, off , observe , enforce . That middle mode is the detail I liked most. Turn it on in a live network, watch the violation logs for a week, then flip to enforce. That is how you ship a breaking security change to something you cannot restart all at once. The README also says it plainly: "I'm not a security expert, and this software didn't went through a full security audit" . Respect for that instead of stamping "military grade encryption" on a landing page. Zooming out, here is how the whole thing stacks. Everything above the node is optional and plugs into the same two extension points: The dotted lines matter. Control traffic who owns what goes through the gossip ledger, slowly and to everyone. Data traffic your actual packets goes point to point over a dedicated libp2p stream protocol, directly to the one peer that needs it. Mixing those up is how you build something that falls over at four nodes. The extension seam is two types, and once you see them you can read any feature in the repo: // something long running that gets the node and the ledger type NetworkService func context.Context, Config, Node, blockchain.Ledger error // something that handles a raw stream for a given protocol ID type StreamHandler func Node, blockchain.Ledger func stream network.Stream The VPN is a NetworkService . So is DNS. So is the file transfer. So is the healthcheck. Adding a feature means picking a bucket name, announcing into it, and optionally claiming a protocol ID for the data path. That is the whole framework. 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. 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. Go 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. Repo 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. AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs — without telling you. You often find out in production. git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free. Any feedback or contributors are welcome It's online, source-available, and ready for anyone to use. ⭐ Star it on GitHub: | 🇩🇰 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 | GenAI 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. git-lrc is your braking system. It hooks into git commit and runs an AI review on every diff In short, git-lrc helps Prevent Outages, Breaches, and Technical Debt Before They Happen 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…