Files
ihasvpn/internal/netcfg/sysctl.go
T
jcoffey 6c006e1d4d 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.
2026-09-12 19:56:08 -07:00

95 lines
3.7 KiB
Go

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 }