Skip to content

christophwitzko/wg-hub

Repository files navigation

wg-hub

This application acts as a WireGuard® hub server to connect multiple clients (behind a NAT) with each other through a single hub. It runs entirely in the user space and can easily be deployed as a docker container or directly to Fly.io (see fly.toml).

For example, if Host A and Host B want to communicate with each other, they both connect to the wg-hub server.

Host A example WireGuard® config:

[Interface]
Address = 192.168.0.1/32
PrivateKey = ...

[Peer]
PublicKey = hub/...
Endpoint = 1.2.3.4:9999
AllowedIPs = 192.168.0.0/24
PersistentKeepalive = 30

Host B example WireGuard® config:

[Interface]
Address = 192.168.0.2/32
PrivateKey = ...

[Peer]
PublicKey = hub/...
Endpoint = 1.2.3.4:9999
AllowedIPs = 192.168.0.0/24
PersistentKeepalive = 30

wireguard-hub.yaml example config:

privateKey: ...
port: 9999
peers:
  - publicKey: hostA/...
    allowedIPs: 192.168.0.1/32
  - publicKey: hostB/...
    allowedIPs: 192.168.0.2/32

Start the wg-hub instance:

$ ./wg-hub --log-level info
INFO[2023-01-20T20:15:10+01:00] using config: wireguard-hub.yaml
INFO[2023-01-20T20:15:10+01:00] listening on :9999
INFO[2023-01-20T20:15:10+01:00] adding peer(876f…29ed): 192.168.0.1/32
INFO[2023-01-20T20:15:10+01:00] adding peer(876f…92de): 192.168.0.2/32

Now Host A and Host B can communicate with each other through the wg-hub server.

Installation

Binary

curl -SL https://get-release.xyz/christophwitzko/wg-hub/linux/amd64 -o ./wg-hub && chmod +x ./wg-hub

Docker

docker run -it --rm \
  -e PRIVATE_KEY="..."
  -e PORT=9999 \
  -e PEER_1="hostA/...,192.168.0.1/32" \
  -e PEER_2="hostB/...,192.168.0.2/32" \
  -p 9999:9999/udp \
  ghcr.io/christophwitzko/wg-hub

Webui

To enable the Webui and dynamically manage peers the following config options need to be set.

hubAddress: 192.168.0.254
webui: true
webuiJWTSecret: supersecure # random secret to sign JWT tokens
# use `caddy hash-password` or `htpasswd -nB admin` to generate the hash
webuiAdminPasswordHash: $2a$14$hTHK6KAynSb7tWknK4CvUum2eFVHIDSzbOuOlgDeP4bQW91ujnlli #admin

The Webui will be running on the hubAddress and port 80 (e.g. http://192.168.0.254).

API

The API will be served under the same endpoint the Webui is running (hubAddress). All API request (except POST /api/auth) require a valid Authorization header with the value Bearer <token>. The token can be obtained by sending a POST /api/auth request with the username and password in the request body.

POST /api/auth

Example request body
{
  "username": "admin",
  "password": "admin"
}
Example response body
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTM3MTI2NTgsImlhdCI6MTcwNzMxMjY1OCwidXNlcm5hbWUiOiJhZG1pbiJ9.fVvahKZPJ2LUZE_dOIYQ6toYFN4x_r295jxINqlXY88"
}

GET /api/auth

Example response body
{
  "exp": "2026-11-03T13:30:58Z",
  "iat": "2024-02-07T13:30:58Z",
  "username": "admin"
}

GET /api/peers

Example response body
[
  {
    "publicKey": "ZbSHDrKwqmsQKpO5T6lOY/iipbcJpT4DPXTHGsLaGUU=",
    "allowedIP": "192.168.0.254/32",
    "endpoint": "127.0.0.1:64404",
    "lastHandshake": 1707312755,
    "txBytes": 4696,
    "rxBytes": 4968,
    "isHub": true,
    "isRequester": false
  },
  {
    "publicKey": "h1/wJ5KoQX1fQzQ25rlHb18wgAG80vkDLtn8B7pxOW0=",
    "allowedIP": "192.168.0.1/32",
    "endpoint": "127.0.0.1:58646",
    "lastHandshake": 1707312760,
    "txBytes": 4152,
    "rxBytes": 5640,
    "isHub": false,
    "isRequester": true
  },
  {
    "publicKey": "h2/PAmEgoIRLYBDDTL3dZKAOaLEhu4270vlNWXFMSys=",
    "allowedIP": "192.168.0.2/32",
    "endpoint": "",
    "lastHandshake": 0,
    "txBytes": 0,
    "rxBytes": 0,
    "isHub": false,
    "isRequester": false
  }
]

GET /api/config

Example response body
{
  "config": "privateKey: <redacted>\nport: 9999\nlogLevel: debug\nhubAddress: 192.168.0.254\ndebugServer: true\nwebui: true\nwebuiJWTSecret: <redacted>\nwebuiAdminPasswordHash: $2a$14$hTHK6KAynSb7tWknK4CvUum2eFVHIDSzbOuOlgDeP4bQW91ujnlli\npeers:\n    - publicKey: h1/wJ5KoQX1fQzQ25rlHb18wgAG80vkDLtn8B7pxOW0=\n      allowedIP: 192.168.0.1/32\n    - publicKey: h2/PAmEgoIRLYBDDTL3dZKAOaLEhu4270vlNWXFMSys=\n      allowedIP: 192.168.0.2/32\n"
}

POST /api/peers

Example requeset body
{
  "allowedIP": "192.168.0.55/32"
}
Example response body
{
  "privateKey": "KEta3N3FXLlSlY7o2C22ty2nXnw+FJ44zyCFXxznrHU=",
  "publicKey": "ylD5KC3idzgxdA+LnAW5QclS5tg/vilMbqn9Y6oKpwQ=",
  "allowedIP": "192.168.0.55/32",
  "hubNetwork": "192.168.0.0/24"
}

PUT /api/peers/:publicKey

Example requeset body
{
  "allowedIP": "192.168.0.55/32"
}
Example response body
{
  "allowedIP": "192.168.0.55/32",
  "hubNetwork": "192.168.0.0/24"
}

DELETE /api/peers/:publicKey

Example response body
{
  "status": "ok"
}

GET /api/hub

Example response body
{
  "publicKey": "hub/+QaIRMomZNnjd6zZqZY+MiyH0R9aalxhhbnvPXE=",
  "port": 9999,
  "hubNetwork": "192.168.0.0/24",
  "randomFreeIP": "192.168.0.130/32"
}

Legal

WireGuard is a registered trademark of Jason A. Donenfeld.