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:
@@ -4,6 +4,7 @@ import (
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -40,6 +41,28 @@ type Kernel interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
// lanBlock picks, from the networks on the uplinks, the ones peers must not
|
||||
// reach while LAN access is off: private IPv4 networks, and IPv6 networks
|
||||
// except link-local, since a home LAN uses global IPv6 addresses. IPv6
|
||||
// prefixes shorter than /48 are left out: they are no LAN.
|
||||
func lanBlock(nets []netip.Prefix) []netip.Prefix {
|
||||
var out []netip.Prefix
|
||||
for _, p := range nets {
|
||||
a := p.Addr().Unmap()
|
||||
p = netip.PrefixFrom(a, min(p.Bits(), a.BitLen())).Masked()
|
||||
switch {
|
||||
case a.Is4() && !a.IsPrivate():
|
||||
continue
|
||||
case a.Is6() && (a.IsLinkLocalUnicast() || a.IsLoopback() || p.Bits() < 48):
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(out, p) {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// readSysctl returns the trimmed content of a /proc/sys file, or "".
|
||||
func readSysctl(path string) string {
|
||||
b, err := os.ReadFile(path)
|
||||
@@ -56,6 +79,10 @@ type Reconciler struct {
|
||||
store *Store
|
||||
trigger chan struct{}
|
||||
|
||||
// applyMu runs one apply at a time. Each reads the config once it holds
|
||||
// the lock, so the last apply always uses the newest config.
|
||||
applyMu sync.Mutex
|
||||
|
||||
mu sync.Mutex
|
||||
lastErr error
|
||||
lastApply time.Time
|
||||
@@ -76,6 +103,8 @@ func (r *Reconciler) Kick() {
|
||||
// ApplyNow applies synchronously and returns the result, so an API call can
|
||||
// report kernel errors to the user.
|
||||
func (r *Reconciler) ApplyNow() error {
|
||||
r.applyMu.Lock()
|
||||
defer r.applyMu.Unlock()
|
||||
err := r.kernel.Apply(r.store.Get())
|
||||
r.mu.Lock()
|
||||
r.lastErr, r.lastApply = err, time.Now()
|
||||
|
||||
Reference in New Issue
Block a user