GHOSTWIRE 0.1.2: WireGuard server manager with web UI and API
Single Go binary that manages a WireGuard server based on pivpn's defaults: - config.json as the single source of truth, reconciled to the kernel via netlink, wgctrl and its own nftables table (NAT, forward, input) - web interface (dashboard, peers, peer detail, add peer, server, settings) and a JSON API for the future iOS app, with session and API-token auth - client private keys are never stored; configs and QR codes shown once - per-peer traffic statistics in stats.json, logs in GHOSTWIRE.jsonl - HTTPS via Let's Encrypt, self-signed, certificate files or off - self-managing: install, update (restores the old binary on failure), uninstall and passwd subcommands; systemd unit generated by the binary Tested end to end on Ubuntu 26.04 (kernel 7.0) at dev.redetzke.aero. Claude-Session: https://claude.ai/code/session_01RAnLbyQZ5ZTA7KqwXP98nw
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"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
|
||||
Down(c *Config) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// 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{}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user