3e8dba6072
- 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.
141 lines
3.6 KiB
Go
141 lines
3.6 KiB
Go
package main
|
|
|
|
import (
|
|
"log/slog"
|
|
"net/netip"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// PeerSample is one reading of a peer's kernel counters.
|
|
type PeerSample struct {
|
|
PublicKey string
|
|
RxBytes int64 // received by the server = uploaded by the peer
|
|
TxBytes int64 // sent by the server = downloaded by the peer
|
|
LastHandshake time.Time
|
|
Endpoint string
|
|
}
|
|
|
|
// Check is one line of the health report.
|
|
type Check struct {
|
|
Name string `json:"name"`
|
|
OK bool `json:"ok"`
|
|
Detail string `json:"detail"`
|
|
}
|
|
|
|
// Kernel applies the desired state to the system. The Linux implementation
|
|
// uses netlink, wgctrl and nftables; other platforms get a simulator so the
|
|
// web UI can be developed without a Linux box.
|
|
type Kernel interface {
|
|
Apply(c *Config) error
|
|
Sample(iface string) ([]PeerSample, error)
|
|
Checks(c *Config) []Check
|
|
Uplink(c *Config, v6 bool) string
|
|
// Ping sends one echo request to each address and returns the round-trip
|
|
// times of the replies that came within timeout.
|
|
Ping(dsts []netip.Addr, timeout time.Duration) (map[netip.Addr]time.Duration, error)
|
|
Down(c *Config) error
|
|
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)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(string(b))
|
|
}
|
|
|
|
// Reconciler applies the config to the kernel whenever it is triggered and
|
|
// remembers the outcome for the health report.
|
|
type Reconciler struct {
|
|
kernel Kernel
|
|
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
|
|
}
|
|
|
|
func newReconciler(k Kernel, s *Store) *Reconciler {
|
|
return &Reconciler{kernel: k, store: s, trigger: make(chan struct{}, 1)}
|
|
}
|
|
|
|
// Kick schedules an apply; several kicks in a row collapse into one.
|
|
func (r *Reconciler) Kick() {
|
|
select {
|
|
case r.trigger <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
r.mu.Unlock()
|
|
if err != nil {
|
|
slog.Error("apply failed", "err", err)
|
|
} else {
|
|
slog.Debug("config applied to kernel")
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Run applies on every kick and re-applies every 5 minutes, which repairs
|
|
// drift such as a flushed nftables ruleset or a deleted interface.
|
|
func (r *Reconciler) Run(stop <-chan struct{}) {
|
|
t := time.NewTicker(5 * time.Minute)
|
|
defer t.Stop()
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
case <-r.trigger:
|
|
case <-t.C:
|
|
}
|
|
_ = r.ApplyNow()
|
|
}
|
|
}
|
|
|
|
func (r *Reconciler) Status() (time.Time, error) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
return r.lastApply, r.lastErr
|
|
}
|