2026-10-04 02:31:36 +03:00
2026-10-04 02:31:36 +03:00
2026-10-04 02:31:36 +03:00

GHOSTWIRE

ゴーストワイヤー · A self-hosted WireGuard server manager in a single Go binary, with a web interface, a JSON API and a native iPhone app.

GHOSTWIRE sets up the WireGuard server, manages peers (add, change, disable, remove), hands out client configs as a download or QR code, and records traffic and connection history per peer. There are no install scripts and no dependencies on the server: the binary installs, updates and removes itself.

Features

  • One file of state: everything lives in config.json. The kernel is reconciled to it, so there is no /etc/wireguard, no wg-quick and no wireguard-tools.
  • Kernel access: netlink creates wg0 and sets its addresses and MTU; wgctrl sets keys and peers; nftables holds the rules in its own inet GHOSTWIRE table.
  • Live peer changes: only peers that changed are touched, the same effect as wg syncconf, so connected peers stay connected.
  • IPv4 and IPv6: IPv6 inside the tunnel is turned on automatically when the server has a global IPv6 address.
  • Traffic history: kept in stats.json, hourly for 48 h and daily for 400 days by default (Settings → Data retention).
  • Connection history: every online session per peer, with start, duration, address and traffic. A new session starts when a device changes networks. Country and network operator come from the free DB-IP Lite databases (CC BY 4.0). GHOSTWIRE downloads them monthly (about 20 MB) and looks addresses up locally, so peer addresses never leave the server. You can switch this off under Settings → Data retention.
  • Logs: written to GHOSTWIRE.jsonl, rotated at 10 MB with 5 old files kept by default. Changes are marked as audit entries.
  • HTTPS built in: Let's Encrypt, a self-signed certificate, your own certificate files, or plain HTTP behind a reverse proxy.

Security

  • Client private keys are never stored. A config is shown once, as a download or QR code. "Issue new config" makes new keys.
  • Setup links: instead of showing the QR code, you can send the device's owner a one-time link, valid for 1 hour, 24 hours or 7 days and protected by a 4-digit PIN by default. The keys are made only when the link is opened. The link works once, and 5 wrong PINs revoke it.
  • The service is not root. It runs as user ghostwire with only CAP_NET_ADMIN and CAP_NET_BIND_SERVICE, and can write only to /opt/ghostwire.
  • Sign-in: one admin account. The password is stored as an argon2id hash. After 5 failed attempts, sign-in is locked for 15 minutes. Sessions use an HttpOnly, SameSite=Strict cookie and last 12 hours by default.
  • API tokens are stored only as hashes and can be read-only or full access.
  • config.json holds the server private key and is readable only by the service (0600).

Requirements

  • Linux with kernel 5.6 or newer (WireGuard built in), nftables and systemd
  • Ports: UDP 51820 (WireGuard; another port can be chosen at install), TCP 443 (web), TCP 80 (optional, Let's Encrypt http-01 and redirect)

Build

Building needs Go 1.27 or newer. The binaries are static (no cgo), so they run on any Linux distribution.

make linux-amd64      # dist/amd64/GHOSTWIRE
make linux-arm64      # dist/arm64/GHOSTWIRE (Raspberry Pi 64-bit, ARM servers)
make linux-arm        # dist/armv7/GHOSTWIRE (Raspberry Pi OS 32-bit)
make test

Install

The binary installs itself. Copy it to the server and run it as root:

scp dist/amd64/GHOSTWIRE server:/tmp/
ssh server
sudo /tmp/GHOSTWIRE install

It asks a few questions, shows a summary and changes nothing until you confirm:

Web interface
  Domain name for the web interface (empty: no domain, self-signed certificate)
  > vpn.example.net
  Email for Let's Encrypt expiry warnings (optional)
  > you@example.net

WireGuard
  Address devices connect to [vpn.example.net]
  >
  UDP port [51820]
  >

Admin account
  Password for "admin" (at least 12 characters): ************
  Repeat password: ************

Summary
  Web interface   https://vpn.example.net/ (Let's Encrypt, you@example.net)
  Endpoint        vpn.example.net:51820/udp
  Tunnel network  10.214.86.0/24 (random free range) · IPv6 on
  Firewall        443/tcp, 80/tcp, 51820/udp must be reachable

Install with these settings? [Y/n]

A domain turns on Let's Encrypt and is also the default WireGuard endpoint. Without one, the web interface uses a self-signed certificate and the endpoint defaults to the server's detected public IP.

Unattended install

For scripts, cloud-init or Ansible, give the settings as flags. Questions are skipped for every flag given, and entirely with -y or when there is no terminal:

sudo /tmp/GHOSTWIRE install -y -domain vpn.example.net -email you@example.net -port 51820
Flag Default
-domain none: self-signed certificate
-email none
-endpoint the domain
-port 51820, or the current port when already installed

The admin password is then read from standard input, e.g. echo "$PASSWORD" | sudo ./GHOSTWIRE install -y …. Every value is checked before anything is changed.

install:

  1. creates the system user ghostwire and /opt/ghostwire
  2. copies itself to /opt/ghostwire/GHOSTWIRE
  3. creates config.json with defaults, if missing
  4. writes /etc/sysctl.d/99-ghostwire.conf (IP forwarding) and /etc/modules-load.d/ghostwire.conf, and loads the kernel module
  5. writes /etc/systemd/system/ghostwire.service
  6. sets the admin password (first install only)
  7. enables and starts the service, and checks that it stays up

Running it again is safe: steps that are already done are skipped, and the questions offer the current settings, so Enter keeps them. If a changed endpoint or port means existing devices need a new config, the summary says how many.

Then open https://vpn.example.net and sign in as admin. Root is needed only for the commands below, never for the running service.

Commands (as root)

Command What it does
GHOSTWIRE install [-domain d] [-email e] [-endpoint h] [-port p] [-y] Sets up and starts the service, as above. Asks for the settings no flag gave; -y never asks.
GHOSTWIRE update [-force] Run from the new binary, e.g. sudo /tmp/GHOSTWIRE update. Checks that it can read the current config.json (nothing changes if not), backs up the config to config.json.bak-<old version>, replaces the binary, updates the unit if needed and restarts. If the new version does not stay up, the old binary is put back and restarted. It refuses older versions without -force.
GHOSTWIRE uninstall [-purge] [-y] Stops and removes the service, wg0 and the firewall table. -purge also deletes /opt/ghostwire and the user.
GHOSTWIRE passwd Sets the admin password and reloads the running service.
GHOSTWIRE version Prints the version.

Updating restarts only the management service. VPN connections stay up, because wg0 lives in the kernel.

config.json

A minimal file is enough. Missing values are filled with defaults on first start: server key, a random free /24 subnet, port 51820, MTU 1420, Quad9 DNS, full tunnel.

{
  "web": {
    "listen": ":443",
    "httpListen": ":80",
    "tls": { "mode": "acme", "domain": "vpn.example.net", "email": "admin@example.net" }
  },
  "server": { "endpoint": "vpn.example.net" }
}

web.tls.mode can be:

Mode What it does
acme Let's Encrypt, automatic. Uses tls-alpn-01 on :443, or http-01 when httpListen is set. Certificates are cached in /opt/ghostwire/acme. "staging": true uses the test CA.
selfsigned Generates a certificate in /opt/ghostwire/tls. The iOS app pins its fingerprint.
files Uses certFile and keyFile, and reloads them when they change.
off Plain HTTP, for running behind a reverse proxy on localhost.

After editing config.json by hand, run sudo systemctl reload ghostwire.

Files in /opt/ghostwire

File Content
GHOSTWIRE the program
config.json all settings, server key, peers, token hashes (0600)
stats.json traffic and connection history per peer
geo-country.mmdb, geo-asn.mmdb DB-IP Lite databases for country and network lookups
GHOSTWIRE.jsonl log, one JSON object per line. Changes carry "audit":true
acme/, tls/ certificates

API

Base path /api/v1. The web interface signs in with a session cookie. Apps and scripts use Authorization: Bearer <token>; create the token under Settings → Pair iOS app. A read-only token may only use GET. Full-access tokens can do everything the web interface does except the admin-only endpoints: password, API tokens, backup and restore, and changing the admin username.

POST   /auth/login · /auth/logout        GET /auth/me        POST /auth/password (admin)
GET    /status                           GET /stats?range=24h|7d|30d|90d
GET    /server         PATCH /server     POST /server/rotate-key     GET /server/detect-ip
GET    /peers          POST /peers       (returns the config and QR once)
GET    /peers/{id}     PATCH /peers/{id} DELETE /peers/{id}
POST   /peers/{id}/enable | /disable | /issue-config
GET    /peers/{id}/stats?range=…        GET /peers/{id}/sessions?limit=100
GET    /peers/{id}/setup (not read-only) DELETE /peers/{id}/setup
GET    /settings       PATCH /settings   POST /restart
GET    /logs?level=&limit=&audit=1       GET /logs/download
admin: GET|POST /tokens · DELETE /tokens/{id} · GET /backup · POST /restore
public: GET /setup/{token} · POST /setup/{token} {"pin"}   (what a setup link opens)

POST /peers and POST /peers/{id}/issue-config take {"delivery": "link", "linkHours": 1|24|168, "linkPIN": true} to answer with a setup link (setup.url, setup.pin, setup.qr) instead of a config. With a link, the peer's current keys keep working until the link is opened.

Traffic is reported from the peer's point of view: down is what the peer downloaded, up is what it uploaded.

Firewall note

GHOSTWIRE's rules sit in their own nftables table. An accept there cannot override a drop in another table, so if ufw or firewalld is active, allow UDP 51820 (and TCP 443/80) in that firewall too.

iOS app

ios/ holds the native iPhone app (SwiftUI, iOS 17+). It does everything the web interface does except password, API tokens and backups. Pair it in the web interface under Settings → Pair iOS app: scan the QR code, or tap "Copy pairing code" and paste it into the app's "Enter manually". Self-signed certificates are pinned during pairing.

  • Open ios/GHOSTWIRE.xcodeproj in Xcode to build and run.
  • TEAM_ID=<your team> ios/release.sh archives and uploads a build to App Store Connect.
  • ios/AppStore/ has the store listing text, privacy details, review notes and 6.9-inch screenshots.

Development

On macOS (or any non-Linux system), make dev starts the app on http://127.0.0.1:8080 with a traffic simulator in place of the kernel. Set a password first:

make build && mkdir -p dev && ./GHOSTWIRE -config dev/config.json -passwd

License

GHOSTWIRE is released under the MIT License.

The DB-IP Lite databases it downloads are by DB-IP and licensed under CC BY 4.0.

S
Description
No description provided
Readme MIT 5.4 MiB
2026-10-04 21:14:51 +00:00
Languages
Go 63.9%
JavaScript 30.4%
CSS 5.2%
HTML 0.3%
Makefile 0.2%