LCVPN wires several sites into one network over UDP with no central server
Decentralized VPN in golang
At a glance
- What is it?
- LCVPN is a lightweight decentralized VPN written in Go that connects hosts directly over UDP, sharing one configuration file with automatic local parameter detection, and it supports AES-CBC with HMAC-SHA256, legacy unauthenticated AES-CBC, or no encryption at all. It suits engineers joining a handful of branch networks, and its own README calls it beta software that should be used at your own risk.
- Who is it for?
- Adopt LCVPN if you need to join a few sites into a single address space over UDP without standing up a central server, and you are willing to run a beta-grade daemon on Linux hosts that you control. Do not adopt it for untrusted networks or for carrying traffic you cannot afford to lose, because the mode names in the configuration make the security choices explicit, including an option that provides no encryption and one that provides no authentication.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 82 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A three hour prototype that ended up in production
The README opens with an origin story that is unusual enough to be worth repeating. The repository was originally just an answer to the question how much time it would take to write a simple VPN in Go, with the answer being about three hours for a first prototype, and it now runs in production in different environments.
That history explains the shape of the project. It is a single Go binary with no central server, and the feature list is written in the terse style of someone describing their own tool rather than a product page. The properties are listed as: very light and easy, with one similar config on all hosts; the same config for all hosts, with automatic detection of local parameters, which the README says is useful with something like puppet; AES-128, AES-192 or AES-256 encryption, with a note that AES-256 is much slower than AES-128 on most computers; optional HMAC-SHA256 or no encryption at all, described as just copying without modification; communication via UDP directly to the selected host with no central server; Linux only, because it uses a TUN device; basic routing support so it can connect several networks; and multithreaded send and receive, which it says is scalable for big traffic, with a note that the use of SO_REUSEPORT gives better results with a larger number of hosts.
The README also states plainly that it is still in beta, should be used at your own risk, and that only versions marked as release should be used.
One config file, with the local side filled in for you
The configuration model is the most distinctive part of the design, and it is worth reading closely.
Every host gets a similar configuration, and the local parameters are detected automatically. The config example shows the structure. The main section carries the UDP port, the encryption mode, the keys, the broadcast address, the netcidr, and the receive and send thread counts. Then each remote is a named section with its external IP, its local IP and its routes:
[main]
port = 23456
encryption = aescbchmac
broadcast = 192.168.3.255
netcidr = 24
recvThreads = 4
sendThreads = 4The automatic detection has one documented failure mode, and the README explains it well. If the host sits behind a firewall with a UDP port forward, LCVPN cannot work out which remote is the local one, and in that case you name the local site explicitly with the -local flag, for example -local berlin.
That single mechanism is what makes the same file usable everywhere, and it is also the thing to test first in your environment.
The other constraints are stated just as plainly. The LocIP, broadcast, route networks and external UDP endpoints must resolve to IPv4. The number of remotes is effectively unlimited, with each taking about 256 bytes of memory. And for aescbchmac the key is 32 bytes longer than the plain AES key, because the HMAC key bytes are carried alongside it.
Choosing the encryption mode, and what the names tell you
The three encryption modes are named in a way that removes the guesswork, which is unusual and worth appreciating.
aescbchmac is AES-CBC with HMAC-SHA256. legacy-aescbc-unsafe is unauthenticated legacy AES-CBC. none means no encryption at all, described in the README as just copying without modification. The word unsafe is in the configuration name, deliberately, and so is the rejection of the old aescbc name.
The README is explicit that the former aescbc configuration name is intentionally rejected, and that legacy-aescbc-unsafe should be used only when wire compatibility with an existing unauthenticated AES-CBC deployment is required. In other words, there is no way to end up on unauthenticated encryption by accident.
The key lengths follow the cipher. For legacy-aescbc-unsafe, mainkey and altkey are the hex form of a 16, 24 or 32 byte key, for AES-128, AES-192 or AES-256. For aescbchmac they are 32 bytes longer. For none they are ignored entirely.
One performance note from the feature list: AES-256 is much slower than AES-128 on most computers, which matters if you are pushing large volumes over the link.
The wire protocol, the replay window and the key rotation dance
This section of the README is the one to read before rolling the thing out, and it is the most technically detailed part of the documentation.
Version 0.3 uses a new packet format and is not wire-compatible with older versions, so the advice is to upgrade all peers together. Old peers reject the new frame before writing it to the TUN interface, which is a good failure mode, since an old peer fails closed rather than misreading traffic.
Every packet contains the intended peer's VPN address, a random per-peer session ID and a monotonically increasing sequence number. Receivers keep a 1024-packet sliding window for each session, which allows UDP reordering while rejecting duplicates and packets older than the window. Replay state is shared by all receive threads and survives configuration reloads, but it is kept in memory and is reset when the daemon restarts. That last clause is the kind of detail that matters for a threat model, since a restart opens the replay window again.
In aescbchmac mode, HMAC-SHA256 is calculated over the IV and ciphertext and verified before decryption, and the session ID and sequence number sit inside the authenticated ciphertext. The legacy-aescbc-unsafe and none modes do not provide cryptographic authentication, and their replay checks cannot protect against an active attacker who can modify packets.
Online key change is handled with the altkey option, and the README spells out the sequence: in the normal state only mainkey is set, since setting altkey is more CPU-consuming; then you set altkey to the new key on all hosts and send a HUP signal; then you exchange altkey and the AES key on all hosts and send another HUP; then you remove the old altkey from the configs and send HUP again. The comment at the end reads, in the author's words, that you are now running with the new key.
Reloading config, and the Linux-only constraint
Two operational details close out the documentation.
Config is reloaded on the HUP signal, and if the new config is invalid the daemon logs a message and keeps using the previous one. There is one limitation stated in a postscript: the listening UDP socket is not reopened for now, so on a port change a restart is needed. That is a small thing to know before you schedule a port migration.
The platform constraint is more consequential. LCVPN works only on Linux, because it uses a TUN device. The repository layout confirms that: iface_linux.go and iface_darwin.go both exist at the top level, so there is a Darwin variant of the interface code, but the feature list says Linux only, and a Darwin build without the TUN device would have nothing to attach to.
The rest of the tree is small and legible. There are config.go, encryption.go with encryption_cbc.go, encryption_hmac.go and encryption_none.go, packet.go, replay.go, routes.go, main.go, a netlink/ directory, and a lcvpn.service file for systemd. The tests are split by area, with config_test.go, encryption_hmac_test.go, packet_test.go, packet_validation_test.go, replay_test.go, routes_test.go, routing_test.go and sender_test.go, and the roadmap lists 100% unit test coverage as a goal rather than a claim.
The go.mod declares go 1.22.2, and the dependencies are small: a SO_REUSEPORT helper, tenus for the TUN interface, the water package for the network stack, golang.org/x/net, and gcfg for the config file format.
Editorial conclusion
Adopt LCVPN if you need to join a few sites into a single address space over UDP without standing up a central server, and you are willing to run a beta-grade daemon on Linux hosts that you control. Do not adopt it for untrusted networks or for carrying traffic you cannot afford to lose, because the mode names in the configuration make the security choices explicit, including an option that provides no encryption and one that provides no authentication. Before you deploy, read the wire protocol section, since version 0.3 changed the packet format and is not compatible with older peers, and note the maintenance picture, with the last push on 2026-08-02 and no GitHub releases published, so the only published versions are the ones marked as release in the repository.
Frequently asked questions
What is LCVPN and how does it connect hosts?
LCVPN is a lightweight decentralized VPN written in Go that connects hosts directly over UDP with no central server. The README says it uses one similar configuration on all hosts with local parameters detected automatically, supports basic routing so several networks can be connected, and works only on Linux because it uses a TUN device.
How do I install and run LCVPN?
You need Go installed, then go get -u github.com/kanocz/lcvpn. With a config in /etc/lcvpn.conf you run sudo $GOPATH/bin/lcvpn, and to point at a different file or run several instances you pass -config, for example sudo $GOPATH/bin/lcvpn -config lcvpn.conf.
Which encryption modes does LCVPN support?
aescbchmac is AES-CBC with HMAC-SHA256, legacy-aescbc-unsafe is unauthenticated legacy AES-CBC, and none provides no encryption at all. The old aescbc name is intentionally rejected, and the README says legacy-aescbc-unsafe should be used only when wire compatibility with an existing unauthenticated deployment is required.
Is LCVPN compatible with older versions over the wire?
No. Version 0.3 uses a new packet format and is not wire-compatible with older versions, so the README says to upgrade all peers together. Old peers reject the new frame before writing it to the TUN interface, and each session uses a 1024-packet sliding window to allow reordering while rejecting duplicates and older packets.
How do I rotate the encryption key on a running LCVPN deployment?
Use the altkey option. Set altkey to the new key on all hosts and send a HUP signal, then exchange altkey and the AES key on all hosts and send HUP again, then remove the old altkey from the configs and send HUP once more. In the normal state only mainkey is set, since setting altkey is more CPU-consuming.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/kanocz-lcvpn)Was this useful?