lightweight vpn topology client/server
Find a file
Coco (Agent) 32da36d6a8
added: v0.3.0 release notes to the changelog
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 00:38:00 -05:00
cmd added: '--expires' option to set how long an invite stays valid 2026-10-19 14:48:09 -04:00
docs changed: ranked client endpoints by confidence 2026-11-05 00:08:20 -05:00
internal refactor: tidied the LAN endpoint change after review 2026-11-07 00:24:00 -05:00
pkg added: '--expires' option to set how long an invite stays valid 2026-10-19 14:48:09 -04:00
.gitignore initial commit: simple daemon 2026-06-19 20:25:08 -04:00
AGENTS.md refactor: cleaned up and committed long-untracked docs 2026-10-12 16:13:40 -04:00
CHANGELOG.md added: v0.3.0 release notes to the changelog 2026-11-07 00:38:00 -05:00
go.mod changed: made joining a network from an invite easier to follow 2026-10-19 12:17:51 -04:00
go.sum changed: made joining a network from an invite easier to follow 2026-10-19 12:17:51 -04:00
Makefile refactor: pulled out 'runtime' from 'service' layers 2026-08-21 23:12:40 -04:00
README.md added: '--expires' option to set how long an invite stays valid 2026-10-19 14:48:09 -04:00

cord

cord coordinates WireGuard networks. Each network has one server, which owns the network's membership and decides which peers can see each other. Every machine that joins runs a client, which installs the network from an invite and keeps its WireGuard configuration in sync with the server.

If someone sent you an invite, you only need the client. Follow Install, then Join a network from an invite.

Install

You need:

  • Linux with systemd. macOS can run cord by hand; see Running a daemon by hand.
  • Go 1.26 or newer. Go 1.21 or newer usually downloads 1.26 by itself; if yours refuses, install a current release from https://go.dev/dl/.
  • git and make.
  • WireGuard support in the kernel, which Linux 5.6 and newer include.

From a checkout of this repository:

make build        # builds bin/cord
sudo make install # copies it to /usr/local/bin/cord

On Fedora, RHEL, and related distributions, sudo's secure_path omits /usr/local/bin, so sudo cord reports command not found. Resolve the path before sudo resets it, for example sudo "$(command -v cord)" client service install.

Join a network from an invite

Start the client daemon as a system service:

sudo cord client service install

This adds you to the cord group, which lets you run cord client commands without sudo. Log out and back in for the membership to take effect, or prefix the next commands with sudo.

Install the invite you were sent:

cord client network install invite.json

An invite works once and expires, normally 24 hours after it was created. The command prints the network's name and your address in it, then deletes the invite file; pass --keep-invite to keep it. You can also pipe the invite in with cord client network install -.

Check that the network is up:

cord client network topology <network>
ping cord-server.<network>.internal

The topology shows your machine and the peers you can reach. The server is always reachable. Other peers appear once the server's admin gives you access to them.

What setup changes on your machine

sudo make install copies the cord binary to /usr/local/bin/cord.

sudo cord client service install:

  • creates the cord system group if it does not exist, and adds you to it (the user who ran sudo, or --admin-user);
  • writes /etc/systemd/system/cord-client.service, enables it to start at boot, and starts it, waiting until the daemon is ready;
  • runs the daemon as root, because it creates WireGuard interfaces;
  • keeps the daemon's state, including this machine's WireGuard keys, in /var/lib/cord/data/client.db;
  • serves the daemon's control socket at /run/cord/client.sock, which only root and the cord group can use.

cord client network install:

  • generates this machine's permanent WireGuard key pair;
  • briefly creates a temporary interface, <network>-i, to redeem the invite;
  • creates a WireGuard interface named after the network, with a route for the network's address range through it;
  • sends lookups for <network>.internal names to the network's server through systemd-resolved, on that interface only. Your other DNS settings are not changed.

cord does not change your firewall.

Troubleshooting

  • permission denied on /run/cord/client.sock: your login session does not include the cord group yet. Log out and back in, or use sudo.
  • the cord client daemon is not running: check systemctl status cord-client and journalctl -u cord-client.
  • invite expired: ask the sender for a new invite.
  • redeem invite with the server at … or confirm peer with the server at … followed by a timeout: the server may be down, or a firewall may be blocking UDP to the address shown. Ask the sender to check the server.
  • Names do not resolve, but ping to an address works: name resolution needs systemd-resolved, which some distributions, such as Debian, do not run by default. cord client status shows the dns-resolver error.

Removing cord

cord client network uninstall <network>
sudo cord client service uninstall

The service uninstall stops the daemon and removes its unit, but keeps the cord group and /var/lib/cord. To remove everything, also run sudo rm /usr/local/bin/cord, sudo rm -rf /var/lib/cord, and sudo groupdel cord, unless this machine also runs a cord server.

Run a server

The server needs a public IP address that peers can reach over UDP. Start the server daemon:

sudo cord server service install

This makes the same kinds of changes as the client service install: the cord group, the unit /etc/systemd/system/cord-server.service, state in /var/lib/cord/data/server.db, and the socket /run/cord/server.sock. Log out and back in to use cord server commands without sudo.

Create a network

cord server network add office 10.42.0.0/16 203.0.113.10
cord server network enable office

The arguments are the network's name, its address range, and the server's public IP address. The server takes the first address, here 10.42.0.1. Networks start disabled; enabling one creates two WireGuard interfaces on the server:

  • office, the network itself, listening on UDP 51820;
  • office-i, which new peers use only to redeem their invites, listening on UDP 51821.

Allow inbound UDP on both ports in the server's firewall and in any cloud provider security group. A second network on the same server needs different ports; see cord server network add --help.

Invite a peer

cord server registration create office bestie 10.42.0.20 -o bestie.json

The arguments are the network, a name for the peer, and the address to give it, which must be unused and inside the network's range. Use cord server peer list office and cord server registration list office to see addresses in use. The invite file contains a private key, so send it privately. It works once and expires after 24 hours, or after the duration given with --expires, such as --expires 72h. Revoke an unused invite with cord server registration revoke office bestie.

Let peers see each other

A peer can always reach the server. Peers see each other only through groups joined by an association. Sharing a group is not enough; a group whose members should see each other needs an association with itself:

cord server group create office friends
cord server association create office friends friends
cord server registration group add office bestie friends   # before the invite is used
cord server cidr group add office laptop friends           # an existing peer, by name

Associating two different groups, such as friends and servers, lets members of each see members of the other. cord server network topology office shows the network. Clients pick up changes within two minutes, or at once with cord client network sync <network>.

Managing the daemons

Status and logs

cord client status
systemctl status cord-client
journalctl -u cord-client -f

The same commands work for the server with server in place of client.

cord client status and cord server status show the daemon's resource tree. Each resource reports health and lifecycle state; activity rows (↻) report their last result and last successful attempt. State arrows show observed versus intended state, such as failed → stopped for unfinished cleanup. Errors and reasons appear below the tree. Add --verbose (-v) for all activities and their attempt times and schedules, or --json for the complete status response.

Upgrading and changing options

An install will not replace an existing unit. After installing a new binary, move the daemon onto it with --upgrade, which keeps the options the unit was installed with and restarts the daemon:

make build
sudo make install
sudo cord client service install --upgrade

To change options such as --socket-mode or --backend, pass --overwrite with the complete set; options left out return to their defaults. --print shows the unit without installing it.

Because the daemon runs as root, the install refuses a binary that anyone but root could replace, such as bin/cord in a checkout; run the copy that make install placed. If a daemon started by hand already answers on the control socket, the install refuses to proceed until it is stopped. Only systemd is supported so far; --manager selects the init system explicitly.

Running a daemon by hand

sudo cord client daemon
sudo cord server daemon

A daemon run this way stores its database in data/ relative to the current directory, so always start it from the same directory. --backend selects the WireGuard implementation (auto, kernel, or userspace); use userspace on hosts without kernel WireGuard. --debug enables verbose logging. This is currently the only way to run cord on macOS.

Socket permissions

Daemon sockets default to mode 0660: root and the socket's group can connect. The systemd units run the daemons with group cord. A daemon started by hand with sudo gets root's group, so only root can connect unless you choose another policy with --socket-mode:

sudo cord server daemon --socket-mode 0600         # root only
sudo -g cord cord server daemon --socket-mode 0660 # root and cord group

Private DNS

Each network serves names under .internal: <peer>.<network>.internal for peers and cord-server.<network>.internal for the server. Each peer can resolve only the peers it can see. Clients need systemd-resolved on Linux; macOS clients use /etc/resolver. See docs/DNS.md for details.

Scripting

Administrative commands with --json emit their response without confirmation text. Commands with no response write nothing on success; use the exit status to check the outcome. Empty lists are []. When registration creation uses --output, the invitation is written to that file and JSON mode leaves stdout empty. Daemon logging remains text on stderr.

Development

make help # lists targets
make test
make lint

See docs/DESIGN.md for the architecture and docs/TESTING.md for test conventions.