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, 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.
First, the uncomfortable truth about VPNs
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:
- Your program reads from that file descriptor, and it gets IP packets the kernel wanted to send out.
- Your program writes to that file descriptor, and the kernel believes those packets just arrived from the network.
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:
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
}
Enter fullscreen mode Exit fullscreen mode
That is it. That is the interface. ifce is the TUN device, read gives you a raw packet, and now it is your problem.
The hard part is not tunneling, it is the phone book
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, 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.
The token is the entire network
Everything starts with one command:
edgevpn -g > config.yaml
# or, as a portable token
export EDGEVPNTOKEN=$(edgevpn -g -b)
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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:
Warning Exposing 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()
}
Enter fullscreen mode Exit fullscreen mode
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.
Step 1: finding each other at a rotating rendezvous
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}
Enter fullscreen mode Exit fullscreen mode
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.
Step 2: the gossip room, sealed twice
Connected peers join a GossipSub 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))
}
Enter fullscreen mode Exit fullscreen mode
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.
Step 3: the ledger, or "eventual consistency by shouting"
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"
)
Enter fullscreen mode Exit fullscreen mode
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)
}
Enter fullscreen mode Exit fullscreen mode
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.
Step 4: an actual packet, end to end
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)
Enter fullscreen mode Exit fullscreen mode
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.
The bit I did not expect to enjoy: leaderless leader election
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
}
Enter fullscreen mode Exit fullscreen mode
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.
Now, about that token
Remember that warning. In the default config, holding the token lets you write anything into any bucket. Including this:
machines/10.1.0.13 -> { PeerID: "my-peer-id-actually" }
Enter fullscreen mode Exit fullscreen mode
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.
The layer cake
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)
Enter fullscreen mode Exit fullscreen mode
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.
What I actually took away
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, docs are here, and libp2p's docs 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:
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 before it lands. 60-second setup. Completely free.
In short, git-lrc helps Prevent Outages, Breaches, and Technical Debt Before They Happen
At a glance: 10 risk categories · 100+ failure patterns tracked · every commit…







0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.