WireGuard server with an embedded admin console

Go backend that drives kernel WireGuard over netlink (wireguard-go as the
fallback), nftables NAT with MSS clamping, forwarding and buffer sysctls,
SQLite for peers, users, sessions, traffic history and the audit log.

React console: dashboard with live rates and usage history, peer management
with QR codes and .conf downloads, disconnect, session reset, key rotation,
expiry, client-supplied keys, settings, users with admin and viewer roles,
two-factor authentication with recovery codes, audit log.

Docker image on Alpine with compose files for bridged and host networking,
CI and GHCR publish workflows, performance notes.
This commit is contained in:
jcoffey
2026-09-12 19:56:08 -07:00
commit 6c006e1d4d
72 changed files with 11675 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
package netcfg
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// Sysctl is one kernel parameter and the value WGX wants for it.
type Sysctl struct {
Key string
Value string
// Required marks the ones the VPN cannot work without (forwarding). The
// others are throughput tuning: nice to have, and often refused inside a
// container because they are not network-namespaced.
Required bool
// Why is shown in the log and the UI when a value could not be set.
Why string
}
// Result records what happened to one sysctl.
type Result struct {
Sysctl
Applied bool
Current string
Err string
}
// Wanted returns the sysctls WGX applies at startup, in order.
func Wanted(ipv6 bool) []Sysctl {
s := []Sysctl{
{Key: "net.ipv4.ip_forward", Value: "1", Required: true, Why: "peers cannot reach anything beyond the server without forwarding"},
// Strict reverse-path filtering drops replies that arrive on the
// tunnel for a source the kernel would route elsewhere. Loose is
// what every VPN gateway runs.
{Key: "net.ipv4.conf.all.rp_filter", Value: "2", Why: "strict rp_filter drops legitimate tunnel replies"},
{Key: "net.ipv4.conf.default.rp_filter", Value: "2", Why: "strict rp_filter drops legitimate tunnel replies"},
// The remaining ones are throughput. They are global (not
// namespaced), so inside a container they usually fail and must be
// set on the host instead -- see docs/performance.md.
{Key: "net.core.rmem_max", Value: "26214400", Why: "larger UDP receive buffers stop bursts being dropped before WireGuard reads them"},
{Key: "net.core.wmem_max", Value: "26214400", Why: "larger UDP send buffers keep the encrypt path from stalling"},
{Key: "net.core.rmem_default", Value: "1048576", Why: "default socket receive buffer"},
{Key: "net.core.wmem_default", Value: "1048576", Why: "default socket send buffer"},
{Key: "net.core.netdev_max_backlog", Value: "16384", Why: "deeper per-CPU input queue for 10GbE bursts"},
{Key: "net.ipv4.udp_rmem_min", Value: "16384", Why: "minimum UDP receive buffer under memory pressure"},
{Key: "net.ipv4.udp_wmem_min", Value: "16384", Why: "minimum UDP send buffer under memory pressure"},
}
if ipv6 {
s = append(s, Sysctl{Key: "net.ipv6.conf.all.forwarding", Value: "1", Required: true, Why: "IPv6 peers cannot reach anything beyond the server without forwarding"})
}
return s
}
// ApplyAll writes each sysctl through /proc/sys and reports what happened.
// A value that is already right counts as applied. A required value that
// cannot be set is returned as an error along with the full report so the
// caller can decide whether to keep going.
func ApplyAll(want []Sysctl) ([]Result, error) {
var results []Result
var fatal []string
for _, s := range want {
r := Result{Sysctl: s}
path := filepath.Join("/proc/sys", strings.ReplaceAll(s.Key, ".", "/"))
cur, err := os.ReadFile(path)
if err == nil {
r.Current = strings.TrimSpace(string(cur))
}
if r.Current == s.Value {
r.Applied = true
results = append(results, r)
continue
}
if err := os.WriteFile(path, []byte(s.Value), 0o644); err != nil {
r.Err = err.Error()
if s.Required && !forwardingSatisfied(r.Current, s.Value) {
fatal = append(fatal, fmt.Sprintf("%s=%s (%s)", s.Key, s.Value, r.Err))
}
} else {
r.Applied = true
r.Current = s.Value
}
results = append(results, r)
}
if len(fatal) > 0 {
return results, fmt.Errorf("required sysctls could not be set: %s -- pass them with `--sysctl` or the compose `sysctls:` list", strings.Join(fatal, ", "))
}
return results, nil
}
// forwardingSatisfied treats "1" as satisfied for forwarding keys even when
// the file was read-only, which is what a compose `sysctls:` entry produces.
func forwardingSatisfied(current, want string) bool { return current == want }