Fixes from the audit: input checks, apply order, sign-in limits

- The server endpoint must be a plain host name or IP address. It is
  written into client configs as is, so a newline could add lines such
  as PreUp, which wg-quick runs as root on the client.
- Listen addresses and the session length (1–720 hours) are checked.
  Before web settings or a restore are saved, the server tries the new
  listen addresses and certificate files, so a value it cannot start
  with is refused instead of stopping the service at the next restart.
- Kernel applies run one at a time and read the config once it is
  their turn, so an older config can no longer be applied last.
- Pending passkey sign-ins are capped: 10 per address, 1000 in total.
- Behind a local proxy, the last X-Forwarded-For entry is the client;
  earlier ones come from the client and are ignored.
- With LAN access off, peers are also kept from the IPv6 networks on
  the uplink, not only from its private IPv4 networks.
- A change that leaves no user with a password is refused, and so is a
  backup without one or from a newer version.
This commit is contained in:
Daniel Redetzke
2026-10-05 23:07:30 +03:00
parent aa4ca20296
commit 3e8dba6072
9 changed files with 485 additions and 28 deletions
+74 -4
View File
@@ -3,15 +3,18 @@ package main
import (
"cmp"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"net/netip"
"slices"
"strings"
"syscall"
"time"
)
@@ -29,7 +32,8 @@ type App struct {
geo *Geo // nil in tests
updates *Updater // nil in tests
started time.Time
shutdown func() // graceful stop; systemd restarts the service
shutdown func() // graceful stop; systemd restarts the service
webAddrs []string // the addresses the web server listens on now
}
// --- helpers ---
@@ -1090,11 +1094,15 @@ func (a *App) patchSettings(w http.ResponseWriter, r *http.Request) {
b, _ := json.Marshal(w)
return string(b)
}
before := listen()
before, oldWeb := listen(), c.Web
if err := field(m, "web", &c.Web); err != nil {
return err
}
restart = listen() != before
if restart = listen() != before; restart {
if err := a.checkWebStart(oldWeb, c.Web); err != nil {
return err
}
}
if err := field(m, "stats", &c.Stats); err != nil {
return err
}
@@ -1236,7 +1244,20 @@ func (a *App) restore(w http.ResponseWriter, r *http.Request) {
writeErr(w, badRequest("this file has no server key; is it a backup of this app?"))
return
}
if err := a.store.Update(func(c *Config) error { *c = in; return nil }); err != nil {
if in.Version > configVersion {
writeErr(w, badRequest("this backup is from a newer version of %s; update this server first", appName))
return
}
in.applyDefaults()
if !in.passwordSet() {
writeErr(w, badRequest("this backup has no user with a password; restoring it would lock everyone out"))
return
}
if err := a.store.Update(func(c *Config) error {
old := c.Web
*c = in
return a.checkWebStart(old, c.Web)
}); err != nil {
writeErr(w, err)
return
}
@@ -1244,6 +1265,55 @@ func (a *App) restore(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "applyError": a.apply(), "restartRequired": true})
}
// checkWebStart refuses web settings the service could not start with: an
// address it cannot listen on, or certificate files it cannot read. The
// service would stop at the next restart, and the web interface and the API
// with it.
func (a *App) checkWebStart(old, next WebConfig) error {
if err := validateListen(next.Listen, "listen address", false); err != nil {
return &userError{err.Error()}
}
if err := validateListen(next.HTTPListen, "HTTP listen address", true); err != nil {
return &userError{err.Error()}
}
if next.TLS.Mode == "files" && next.TLS != old.TLS {
if _, err := tls.LoadX509KeyPair(next.TLS.CertFile, next.TLS.KeyFile); err != nil {
return badRequest("the certificate files cannot be used: %v", err)
}
}
addrs := []string{next.Listen}
if next.HTTPListen != "" && next.TLS.Mode != "off" {
addrs = append(addrs, next.HTTPListen)
}
for _, addr := range addrs {
if err := a.canListen(addr); err != nil {
return badRequest("cannot listen on %s: %v", addr, err)
}
}
return nil
}
// canListen tries to listen on addr. An address the service listens on now,
// or one whose port it holds, is fine: it is free again after the restart.
func (a *App) canListen(addr string) error {
if slices.Contains(a.webAddrs, addr) {
return nil
}
ln, err := net.Listen("tcp", addr)
if err == nil {
return ln.Close()
}
if errors.Is(err, syscall.EADDRINUSE) {
_, port, _ := net.SplitHostPort(addr)
for _, own := range a.webAddrs {
if _, p, _ := net.SplitHostPort(own); p == port {
return nil
}
}
}
return err
}
// applyRuntime applies the settings that take effect without a restart: log
// level and log rotation. Traffic retention is read by the stats sampler.
func (a *App) applyRuntime(c *Config) {