Engineering··7 min read

Why we built NexGuard after three years of WireGuard pain

Davronbek Achilov
Davronbek Achilov
Founder, NexGuard

I started using WireGuard in 2022. Our team was 6 people across Tashkent, Berlin, and Tbilisi, and we needed access to a few internal services running on a Hetzner box. WireGuard looked simple — generate keys, edit a config, restart the interface. Done.

It worked. For about two weeks.

Then a designer in Tbilisi got a new laptop. I had to SSH into the server, generate a key for her, edit /etc/wireguard/wg0.conf, restart the interface, and email her the client config. Twenty minutes. Not the worst.

A month later we onboarded a contractor in Lisbon. Same dance. Then someone's laptop got stolen and I had to remember which peer entry to remove. Then I needed to add a new server and now I'm copy-pasting allowed_ips between two configs trying not to typo a /32.

By 2024 I had a private Notion page just for "WireGuard things I always forget". The PostUp/PostDown rules for split tunneling on macOS. The MTU value that made our Berlin office stop having weird packet loss. The exact iptables incantation to NAT the right subnet without breaking inter-peer traffic.

The breaking point was a Tuesday morning. Our backend lead in Berlin couldn't reach the staging server. I spent forty-five minutes debugging it before realizing his ISP had started rate-limiting UDP on port 51820. Nothing in the WireGuard logs told me that — just silent timeouts. We switched to a different port. Two weeks later, same thing on a different ISP.

That's when I started building NexGuard.

The goal was simple: make the WireGuard part invisible. You don't generate keys. You don't edit configs. You don't think about ports. You add a teammate by typing their email, they get a token, they paste it into our app, and they're on the network. If their ISP blocks WireGuard, the traffic falls back through a TLS tunnel that looks like normal HTTPS. They don't have to know any of this.

The architecture, briefly: - Each device gets a JWT signed with the server's private token. The VPN server validates it locally — no callback to a central auth service for every connection. - When direct WireGuard fails, the client opens a TLS connection on 443 to a relay server, and we tunnel WireGuard packets inside it. Slower, but it works behind any firewall I've tested. - Server-side state is in-memory. No database. If the server restarts, peers reconnect with their saved device IDs and pick up where they left off.

Things I'd do differently if I started today: - I'd pick a Rust async runtime earlier. We started with threads-and-mutexes which was fine until we had 100+ peers per server. Switching to tokio mid-project was painful. - I'd treat the GUI as a first-class product from day one, not "the CLI plus a window". egui is great but the abstractions over native menubar widgets are still rough. - I'd write the docs alongside the code instead of after. Two months in, I had a working product and zero documentation. Catching up was demoralizing.

We've been running NexGuard internally for ten months now. The Notion page of WireGuard incantations is gone. New hires get connected before their first standup. When something breaks, the logs actually tell me what broke.

It's not novel work. WireGuard is the hard part — we just wrapped it in a way that doesn't require a full-time sysadmin to operate. But for small teams that just want a private network and want to stop thinking about it, that wrapper turns out to matter a lot.

If you're reading this and you've been keeping a similar Notion page of WireGuard tribal knowledge, you might want to give NexGuard a try. We have a 10-day trial. No credit card. If it doesn't work for you, the WireGuard configs are still there waiting.