5
0
Fork
You've already forked wg-daemon
1
WireGuard Interface Management Daemon
  • Go 89.7%
  • Shell 6.5%
  • Makefile 3.8%
François Kooman 00b1e14415
Some checks failed
/ tests (push) Failing after 44s
update dependencies
2026年07月08日 19:48:12 +02:00
.forgejo/workflows print versions 2026年01月13日 10:40:12 +01:00
cmd/wg-daemon set maintenance mode through API 2026年06月09日 14:59:01 +02:00
.gitignore initial commit 2025年12月16日 15:57:04 +01:00
AUTHORS.md initial commit 2025年12月16日 15:57:04 +01:00
CHANGES.md initial commit 2025年12月16日 15:57:04 +01:00
go.mod update dependencies 2026年07月08日 19:48:12 +02:00
go.sum update dependencies 2026年07月08日 19:48:12 +02:00
LICENSE initial commit 2025年12月16日 15:57:04 +01:00
make_release.sh new signing key 2025年12月16日 16:05:06 +01:00
Makefile enable -extra with gofumpt 2026年03月04日 20:12:35 +01:00
README.md update README 2026年06月09日 15:04:39 +02:00

Summary: WireGuard Management Daemon

Description: This is a simple daemon written in Go providing a HTTP(S) API to manage WireGuard server configuration.

License: MIT

NOTE: we are embedding code from the wgctrl-go project which is licensed under the MIT license. You can find it in the wgctrl directory of this project.

API

Info

Call Description Method
/status Request Node Information GET

Node Information

Request node information.

Request

$ curl http://localhost:51194/status

Response

HTTP/1.1 200 OK
Content-Type: application/json
{
 "connection_count": 1,
 "cpu_count": 1,
 "maintenance_mode": false,
 "node_uptime": 522990,
 "public_key": "EJQtm4KT8g/fUH/2/PRZnmpJTQggz737jwggN6K00ys=",
 "system_load": 5
}

The system_load is based on the output of /proc/loadavg. See proc(5) for more information. If the system the daemon runs on has no /proc/loadavg, or it is not available the value returned will be 0.

The system_load value is calculated as follows:

$ cat /proc/loadavg | awk -v nproc=$(nproc) {'print int(1ドル*100/nproc)'}

The cpu_count field indicates the number of CPUs/cores available on the node.

The node_uptime is the output of the first field of /proc/uptime converted to int. It indicates the number of seconds since the system was booted, i.e. system uptime.

maintenance_mode is true if the node does not want to accept new connections, for example in preparation for a software update or reboot. It can be toggled with the /config API.

The server_public_key field contains the WireGuard public key of the node.

The connection_count field contain the number of VPN clients connected to this node.

NOTE: cpu_count field value contains the number of CPUs the host had the moment the daemon was started. To reflect changes to number of CPU cores, the wg-daemon process MUST be restarted.

Configure

Call Description Method
/config Set Node Config POST

Configure Node

Currently allows you to enable/disable maintenance mode.

Parameter Description Required Default Example(s)
maintenance_mode Enable/disable "Maintenance Mode" No N/A 1, 0

Request

To enable "maintenance mode":

$ curl \
 -d 'maintenance_mode=1' \
 http://localhost:51194/config

Response

HTTP/1.1 204 OK

WireGuard

Call Description Method
/configure Configure WireGuard POST
/add_peer Add a peer POST
/peer_list List of configured peers GET
/remove_peer Remove a peer POST

Configure WireGuard

This call is used to configure the WireGuard interface on the server. You can set the IPs, the MTU and the port WireGuard listens on.

Parameter Description Required Default Example(s)
wg_mtu MTU of the WireGuard interface No N/A 1392
wg_port UDP Port No 51820 443
ip_net The IP network address(es) Yes, >= 1 N/A 10.43.43.1/24, fd43::1/64

Request

$ curl \
 -d 'ip_net=10.43.43.1/24' \
 -d 'ip_net=fd43::1/64' \
 -d 'wg_port=443' \
 -d 'wg_mtu=1392' \
 http://localhost:51194/configure

Add Peer

Add a new WireGuard peer.

Parameter Description Required Example(s)
public_key The public key Yes QWR3JLun5e/YCzRyV7/IHl8qnAI8dC1WvJTSSZwRBEo=
preshared_key The shared key (PresharedKey) Yes lTW6cmZhm3Wqhw+rGp1VKd7QmstB9o9Gw86+kHNfiFw=
allowed_ip The IP network address(es) of the peer Yes, >= 1 10.43.43.2/32, fd43::2/128

Request

To specify both an IPv4 and IPv6 address that the peer is allowed to use, you can use this call:

$ curl \
 --data-urlencode 'allowed_ip=10.43.43.2/32' \
 --data-urlencode 'allowed_ip=fd43::2/128' \
 --data-urlencode 'public_key=QWR3JLun5e/YCzRyV7/IHl8qnAI8dC1WvJTSSZwRBEo=' \
 --data-urlencode 'preshared_key=lTW6cmZhm3Wqhw+rGp1VKd7QmstB9o9Gw86+kHNfiFw=' \
 http://localhost:51194/add_peer

Response

HTTP/1.1 204 No Content

Peer List

Get a list of configured peers. Note, this is not a list of currently connected WireGuard clients.

Parameter Description Required Default Example(s)
show_all Whether to also show "offline" peers No no yes, no

Offline peers are peers that have never performed a handshake with the WireGuard server, or performed the last handshake more than 3 minutes ago. The default is no.

Request

$ curl http://localhost:51194/peer_list

Response

HTTP/1.1 200 OK
Content-Type: application/json
{
 "peer_list": [
 {
 "bytes_in": 682104,
 "bytes_out": 1070200,
 "ip_net": [
 "10.10.10.12/32",
 "fd10::c/128"
 ],
 "last_handshake_time": "2026年02月20日T15:58:22.498948935Z",
 "public_key": "7ZjQNqKut+Al4uxqgX30QPlMTHEAgGufXRCKeE1AYHk="
 }
 ]
}

If the peer was not seen yet, the key last_handshake_time is omitted from the response. The format of last_handshake_time is RFC 3339, always in UTC, i.e. ends with Z.

Remove Peer

Remove a peer.

Parameter Description Required Example
public_key The WireGuard public key Yes QWR3JLun5e/YCzRyV7/IHl8qnAI8dC1WvJTSSZwRBEo=

Request

$ curl \
 --data-urlencode 'public_key=QWR3JLun5e/YCzRyV7/IHl8qnAI8dC1WvJTSSZwRBEo=' \
 http://localhost:51194/remove_peer

Response

HTTP/1.1 200 OK
Content-Type: application/json
{
 "bytes_in": 21480,
 "bytes_out": 34200,
 "ip_net": [
 "10.43.43.2/32",
 "fd43::2/128"
 ],
 "last_handshake_time": "2025年03月06日T10:49:18Z",
 "public_key": "QWR3JLun5e/YCzRyV7/IHl8qnAI8dC1WvJTSSZwRBEo="
}

If the peer was not seen yet, the key last_handshake_time is omitted from the response. The format of last_handshake_time is RFC 3339, always in UTC, i.e. ends with Z.

Or, in case the peer was not registered at the node:

HTTP/1.1 204 No Content

Building

$ git clone https://codeberg.org/eduVPN/wg-daemon
$ cd wg-daemon
$ go build -o wg-daemon codeberg.org/eduVPN/wg-daemon/cmd/wg-daemon

A Makefile is also provided for your convenience.

Running

You can run wg-daemon as root, or use systemd to manage the daemon.

TLS

By default, having access to the TCP port of wg-daemon is enough to use the API. This is fine when the daemon is only used on one system. When deploying multiple nodes, TLS, i.e. HTTPS can be enabled with client certificate authentication.

TLS is automatically enabled when the CREDENTIALS_DIRECTORY environment variable is set. This variable should point to the directory containing the ca.crt, server.crt and server.key files. The ca.crt file will be used to validate the client certificates. If your CA uses intermediate certificates, they MUST be included in the server.crt as well.

In order to quickly set up your own CA and test TLS, you can do the following:

$ vpn-ca -init-ca
$ vpn-ca -server -name server
$ vpn-ca -client -name client

Start wg-daemon:

$ sudo CREDENTIALS_DIRECTORY=. ./wg-daemon

Now you can test accessing the daemon through TLS:

$ curl \
 --cacert ca.crt \
 --cert client.crt \
 --key client.key \
 --connect-to server:51194:localhost https://server:51194/status 

If your CA has already been installed in your OS, you do no need to specify the --cacert option to curl.

Design

The daemon is written in Go. For configuring WireGuard the daemon integrates the wgctrl library.

The daemon is secured with systemd which makes it possible to run it without root permissions, see the wg-daemon.service file below.

Whenever a VPN peer is added, either through the portal or API the peer is immediately configured through the daemon(s) as well. For removal it is exactly the same, so there is no waiting time before a peer can connect.

Our approach at surviving reboots/crashes is that the portal that uses the daemon periodically, e.g. every 5 minutes, synchronizes the list of known WireGuard peers.

The VPN portal is the source of truth. It has a list of all WireGuard peers that should be configured through the daemon(s). The portal retrieves all peers known by the daemon and compares that list with the list it has in the database. Based on this it adds/removes peers known to the daemon. This way after a reboot of the node(s) within 5 minutes the node is back in the correct state.

We do not know how well this scales with thousands of VPN peers over multiple nodes, but we'll find this out and can optimize "Just in Time" when this no longer scales and push portal/daemon updates as needed.

systemd

This is /etc/systemd/system/wg-daemon.service:

[Unit]
Description=WireGuard Management Daemon
[Service]
AmbientCapabilities=CAP_NET_ADMIN
AmbientCapabilities=CAP_NET_BIND_SERVICE
Environment=WG_DEVICE=wg0
Environment=LISTEN=127.0.0.1:51194
ExecStart=wg-daemon
Restart=on-failure
PrivateDevices=yes
DynamicUser=yes
StateDirectory=%N
SystemCallFilter=@system-service
NoNewPrivileges=yes
RestrictNamespaces=yes
ProtectHostname=yes
ProtectHome=yes
ProtectUsers=yes
[Install]
WantedBy=multi-user.target

TLS

In order to enable TLS, you can put the following in the file /etc/systemd/system/wg-daemon.service.d/credentials.conf:

[Service]
LoadCredential=ca.crt:/etc/ssl/wg-daemon/ca.crt
LoadCredential=server.crt:/etc/ssl/wg-daemon/server.crt
LoadCredential=server.key:/etc/ssl/wg-daemon/private/server.key

This uses the Credentials feature of systemd. Make sure the certificates and key are put in the right place, and run the following commands:

$ sudo systemctl daemon-reload
$ sudo systemctl restart wg-daemon