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,644 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// The binary installs, updates and removes itself. These commands need root;
|
||||
// the service they set up runs as the unprivileged user serviceName.
|
||||
|
||||
var (
|
||||
installDir = "/opt/" + serviceName
|
||||
installBin = filepath.Join(installDir, appName)
|
||||
oldBin = installBin + ".old" // exists only while an update runs
|
||||
configFile = filepath.Join(installDir, "config.json")
|
||||
unitPath = "/etc/systemd/system/" + serviceName + ".service"
|
||||
sysctlPath = "/etc/sysctl.d/99-" + serviceName + ".conf"
|
||||
modulesPath = "/etc/modules-load.d/" + serviceName + ".conf"
|
||||
)
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, `%s %s — WireGuard server manager
|
||||
|
||||
Usage (as root):
|
||||
%s install [-domain vpn.example.net] [-email you@example.net] [-endpoint host]
|
||||
set up user, folder, config, sysctls and systemd service; start it
|
||||
%s update [-force]
|
||||
replace the installed binary with this one and restart
|
||||
%s uninstall [-purge] [-y]
|
||||
remove the service, interface and firewall table (-purge also deletes %s)
|
||||
%s passwd
|
||||
set the admin password of the installed service
|
||||
%s version
|
||||
|
||||
Without a command it runs the service:
|
||||
%s [-config path] [-passwd] [-check] [-down]
|
||||
`, appName, version, appName, appName, appName, installDir, appName, appName, appName)
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func runCommand(cmd string, args []string) error {
|
||||
switch cmd {
|
||||
case "install":
|
||||
return cmdInstall(args)
|
||||
case "update":
|
||||
return cmdUpdate(args)
|
||||
case "uninstall":
|
||||
return cmdUninstall(args)
|
||||
case "passwd":
|
||||
return cmdPasswd(args)
|
||||
case "version":
|
||||
fmt.Println(appName, version)
|
||||
return nil
|
||||
case "help":
|
||||
usage()
|
||||
return nil
|
||||
}
|
||||
usage()
|
||||
return fmt.Errorf("unknown command %q", cmd)
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
func step(format string, a ...any) { fmt.Printf("• "+format+"\n", a...) }
|
||||
|
||||
func requireRoot() error {
|
||||
if runtime.GOOS != "linux" {
|
||||
return errors.New("this command only works on Linux")
|
||||
}
|
||||
if os.Geteuid() != 0 {
|
||||
return errors.New("run as root, e.g. with sudo")
|
||||
}
|
||||
if _, err := exec.LookPath("systemctl"); err != nil {
|
||||
return errors.New("systemd is required (systemctl not found)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sh(name string, args ...string) error {
|
||||
out, err := exec.Command(name, args...).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %s: %v: %s", name, strings.Join(args, " "), err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func shOut(name string, args ...string) string {
|
||||
out, _ := exec.Command(name, args...).Output()
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
func serviceUser() (uid, gid int, err error) {
|
||||
u, err := user.Lookup(serviceName)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
uid, _ = strconv.Atoi(u.Uid)
|
||||
gid, _ = strconv.Atoi(u.Gid)
|
||||
return uid, gid, nil
|
||||
}
|
||||
|
||||
func fileHash(path string) string {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer f.Close()
|
||||
h := sha256.New()
|
||||
_, _ = io.Copy(h, f)
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// copyFile writes src to dst through a temp file and a rename, so a running
|
||||
// binary is never overwritten in place ("text file busy") or half-written.
|
||||
func copyFile(src, dst string, mode os.FileMode, uid, gid int) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
tmp, err := os.CreateTemp(filepath.Dir(dst), "."+filepath.Base(dst)+".*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmp.Name())
|
||||
if _, err := io.Copy(tmp, in); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Chmod(mode); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Chown(uid, gid); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Sync(); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp.Name(), dst)
|
||||
}
|
||||
|
||||
// writeIfChanged writes content to path when it differs and reports whether
|
||||
// it did.
|
||||
func writeIfChanged(path, content string, mode os.FileMode) (bool, error) {
|
||||
if old, err := os.ReadFile(path); err == nil && bytes.Equal(old, []byte(content)) {
|
||||
return false, nil
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, os.WriteFile(path, []byte(content), mode)
|
||||
}
|
||||
|
||||
// installedVersion asks the installed binary for its version.
|
||||
func installedVersion(path string) string {
|
||||
f := strings.Fields(shOut(path, "-version"))
|
||||
if len(f) == 2 {
|
||||
return f[1]
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// compareVersions compares "1.2.3" style versions (an optional "v" prefix
|
||||
// and a git-describe suffix like "-4-gabc123" are ignored). ok is false when
|
||||
// either side is not such a version.
|
||||
func compareVersions(a, b string) (cmp int, ok bool) {
|
||||
parse := func(s string) ([3]int, bool) {
|
||||
var v [3]int
|
||||
s = strings.TrimPrefix(s, "v")
|
||||
if i := strings.IndexByte(s, '-'); i >= 0 {
|
||||
s = s[:i]
|
||||
}
|
||||
parts := strings.Split(s, ".")
|
||||
if len(parts) != 3 {
|
||||
return v, false
|
||||
}
|
||||
for i, p := range parts {
|
||||
n, err := strconv.Atoi(p)
|
||||
if err != nil {
|
||||
return v, false
|
||||
}
|
||||
v[i] = n
|
||||
}
|
||||
return v, true
|
||||
}
|
||||
va, ok1 := parse(a)
|
||||
vb, ok2 := parse(b)
|
||||
if !ok1 || !ok2 {
|
||||
return 0, false
|
||||
}
|
||||
for i := range 3 {
|
||||
if va[i] != vb[i] {
|
||||
if va[i] < vb[i] {
|
||||
return -1, true
|
||||
}
|
||||
return 1, true
|
||||
}
|
||||
}
|
||||
return 0, true
|
||||
}
|
||||
|
||||
// ---------- system files ----------
|
||||
|
||||
func unitFile() string {
|
||||
return fmt.Sprintf(`# Written by %[1]s %[5]s. Changes are overwritten by "%[1]s update".
|
||||
[Unit]
|
||||
Description=%[1]s WireGuard manager (web interface and API)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=%[2]s
|
||||
Group=%[2]s
|
||||
WorkingDirectory=%[3]s
|
||||
ExecStart=%[4]s -config %[3]s/config.json
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
# Exit code 0 is used by "Restart now" in the web interface.
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
|
||||
# Runs unprivileged: CAP_NET_ADMIN for netlink, wgctrl and nftables,
|
||||
# CAP_NET_BIND_SERVICE for ports 443 and 80.
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
|
||||
NoNewPrivileges=yes
|
||||
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=%[3]s
|
||||
ProtectHome=yes
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectControlGroups=yes
|
||||
ProtectClock=yes
|
||||
ProtectHostname=yes
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK AF_UNIX
|
||||
RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
LockPersonality=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
SystemCallArchitectures=native
|
||||
UMask=0077
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`, appName, serviceName, installDir, installBin, unitVersion)
|
||||
}
|
||||
|
||||
// unitVersion changes only when the unit text changes, so "update" does not
|
||||
// rewrite the unit for every release.
|
||||
const unitVersion = "unit-1"
|
||||
|
||||
const sysctlConf = "net.ipv4.ip_forward=1\nnet.ipv6.conf.all.forwarding=1\n"
|
||||
|
||||
// writeSystemFiles writes the unit, sysctl and module files. It reports
|
||||
// whether the unit changed (systemd must then reload).
|
||||
func writeSystemFiles() (unitChanged bool, err error) {
|
||||
if unitChanged, err = writeIfChanged(unitPath, unitFile(), 0o644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
sysChanged, err := writeIfChanged(sysctlPath, sysctlConf, 0o644)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if sysChanged {
|
||||
step("Enabling IP forwarding (%s)", sysctlPath)
|
||||
if err := sh("sysctl", "-p", sysctlPath); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
if _, err := writeIfChanged(modulesPath, "wireguard\n", 0o644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := sh("modprobe", "wireguard"); err != nil {
|
||||
fmt.Fprintln(os.Stderr, " warning: could not load the wireguard kernel module:", err)
|
||||
}
|
||||
if unitChanged {
|
||||
step("Writing %s", unitPath)
|
||||
if err := sh("systemctl", "daemon-reload"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return unitChanged, nil
|
||||
}
|
||||
|
||||
// restartAndVerify restarts the service and checks that it stays up: the
|
||||
// main process must still be the same a few seconds later (Restart=always
|
||||
// would otherwise hide a crash loop).
|
||||
func restartAndVerify() error {
|
||||
if err := sh("systemctl", "restart", serviceName); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(1500 * time.Millisecond)
|
||||
pid1 := shOut("systemctl", "show", "-p", "MainPID", "--value", serviceName)
|
||||
time.Sleep(3 * time.Second)
|
||||
state := shOut("systemctl", "is-active", serviceName)
|
||||
pid2 := shOut("systemctl", "show", "-p", "MainPID", "--value", serviceName)
|
||||
if state != "active" || pid1 == "0" || pid1 == "" || pid1 != pid2 {
|
||||
return fmt.Errorf("the service did not stay running (state %s); see: journalctl -u %s -n 50", state, serviceName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------- install ----------
|
||||
|
||||
func cmdInstall(args []string) error {
|
||||
fs := flag.NewFlagSet("install", flag.ExitOnError)
|
||||
domain := fs.String("domain", "", "domain for the web interface; enables Let's Encrypt")
|
||||
email := fs.String("email", "", "contact email for Let's Encrypt (optional)")
|
||||
endpoint := fs.String("endpoint", "", "host or IP clients connect to (default: the domain)")
|
||||
_ = fs.Parse(args)
|
||||
if err := requireRoot(); err != nil {
|
||||
return err
|
||||
}
|
||||
self, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// User and folder
|
||||
if _, err := user.Lookup(serviceName); err != nil {
|
||||
step("Creating system user %s", serviceName)
|
||||
shell := "/usr/sbin/nologin"
|
||||
for _, p := range []string{"/usr/sbin/nologin", "/sbin/nologin", "/bin/false"} {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
shell = p
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := sh("useradd", "--system", "--home-dir", installDir, "--no-create-home", "--shell", shell, serviceName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
uid, gid, err := serviceUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(installDir, 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chown(installDir, uid, gid); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(installDir, 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Binary
|
||||
if self != installBin {
|
||||
step("Installing %s %s to %s", appName, version, installBin)
|
||||
if err := copyFile(self, installBin, 0o755, 0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Config: created with defaults (server key, free subnet) if missing.
|
||||
_, statErr := os.Stat(configFile)
|
||||
if errors.Is(statErr, os.ErrNotExist) {
|
||||
step("Creating %s", configFile)
|
||||
if err := os.WriteFile(configFile, []byte("{}\n"), 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := os.Chown(configFile, uid, gid); err != nil {
|
||||
return err
|
||||
}
|
||||
store, err := openStore(configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *domain != "" || *email != "" || *endpoint != "" {
|
||||
step("Saving domain and endpoint settings")
|
||||
if err := store.Update(func(c *Config) error {
|
||||
if *domain != "" {
|
||||
c.Web.TLS.Mode, c.Web.TLS.Domain = "acme", *domain
|
||||
if c.Server.Endpoint == "" {
|
||||
c.Server.Endpoint = *domain
|
||||
}
|
||||
}
|
||||
if *email != "" {
|
||||
c.Web.TLS.Email = *email
|
||||
}
|
||||
if *endpoint != "" {
|
||||
c.Server.Endpoint = *endpoint
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := writeSystemFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if store.Get().Admin.PasswordHash == "" {
|
||||
fmt.Println("\nChoose the admin password for the web interface (user \"admin\", at least 12 characters).")
|
||||
if err := setPassword(configFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Everything in the folder belongs to the service user.
|
||||
if err := chownTree(installDir, uid, gid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
step("Starting %s", serviceName)
|
||||
if err := sh("systemctl", "enable", serviceName); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := restartAndVerify(); err != nil {
|
||||
return err
|
||||
}
|
||||
printWhereToGo(store.Get())
|
||||
return nil
|
||||
}
|
||||
|
||||
func chownTree(root string, uid, gid int) error {
|
||||
return filepath.Walk(root, func(p string, _ os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Lchown(p, uid, gid)
|
||||
})
|
||||
}
|
||||
|
||||
func printWhereToGo(c *Config) {
|
||||
host := c.Web.TLS.Domain
|
||||
if host == "" {
|
||||
host = c.Server.Endpoint
|
||||
}
|
||||
if host == "" {
|
||||
host = "<server address>"
|
||||
}
|
||||
scheme, port := "https", c.Web.Listen
|
||||
if c.Web.TLS.Mode == "off" {
|
||||
scheme = "http"
|
||||
}
|
||||
if _, p, _ := strings.Cut(port, ":"); p != "" && p != "443" && p != "80" {
|
||||
host += ":" + p
|
||||
}
|
||||
fmt.Printf("\nDone. %s %s is running.\n Web interface: %s://%s/\n", appName, version, scheme, host)
|
||||
switch c.Web.TLS.Mode {
|
||||
case "acme":
|
||||
fmt.Println(" The Let's Encrypt certificate is requested on the first visit; ports 443 (and 80) must be reachable.")
|
||||
case "selfsigned":
|
||||
fmt.Println(" It uses a self-signed certificate, so the browser shows a warning the first time.")
|
||||
}
|
||||
fmt.Printf(" WireGuard: UDP %d · Log: %s/%s.jsonl · Status: systemctl status %s\n", c.Server.ListenPort, installDir, appName, serviceName)
|
||||
}
|
||||
|
||||
// ---------- update ----------
|
||||
|
||||
func requireInstalled() error {
|
||||
if _, err := os.Stat(installBin); err != nil {
|
||||
return fmt.Errorf("%s is not installed; run: sudo ./%s install", appName, appName)
|
||||
}
|
||||
if _, err := os.Stat(unitPath); err != nil {
|
||||
return fmt.Errorf("%s is missing; run: sudo ./%s install", unitPath, appName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdUpdate(args []string) error {
|
||||
fs := flag.NewFlagSet("update", flag.ExitOnError)
|
||||
force := fs.Bool("force", false, "install even if it is the same build or an older version")
|
||||
_ = fs.Parse(args)
|
||||
if err := requireRoot(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := requireInstalled(); err != nil {
|
||||
return err
|
||||
}
|
||||
self, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if self, _ = filepath.EvalSymlinks(self); self == installBin {
|
||||
return fmt.Errorf("run update from the new binary, e.g.: sudo /tmp/%s update", appName)
|
||||
}
|
||||
|
||||
// 1. Is this an update?
|
||||
oldVersion := installedVersion(installBin)
|
||||
if fileHash(self) == fileHash(installBin) && !*force {
|
||||
fmt.Printf("Already up to date (%s %s).\n", appName, oldVersion)
|
||||
return nil
|
||||
}
|
||||
if c, ok := compareVersions(version, oldVersion); ok && c < 0 && !*force {
|
||||
return fmt.Errorf("this is %s, older than the installed %s; use -force to downgrade", version, oldVersion)
|
||||
}
|
||||
fmt.Printf("Updating %s %s → %s\n", appName, oldVersion, version)
|
||||
|
||||
// 2. Can this version read the current config?
|
||||
if c, err := loadConfigFile(configFile); err != nil {
|
||||
return fmt.Errorf("this version cannot read %s, nothing changed: %w", configFile, err)
|
||||
} else if err := c.validate(); err != nil {
|
||||
return fmt.Errorf("this version rejects %s, nothing changed: %w", configFile, err)
|
||||
}
|
||||
|
||||
// 3. Back up the config, and keep the old binary until the new one runs.
|
||||
uid, gid, err := serviceUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backup := configFile + ".bak-" + oldVersion
|
||||
step("Backing up config to %s", backup)
|
||||
if err := copyFile(configFile, backup, 0o600, uid, gid); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := copyFile(installBin, oldBin, 0o755, uid, gid); err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(oldBin)
|
||||
|
||||
// 4. Install.
|
||||
step("Installing the new binary")
|
||||
if err := copyFile(self, installBin, 0o755, uid, gid); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := writeSystemFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. Restart. If the new version does not stay up, put the old one back
|
||||
// so the web interface stays reachable.
|
||||
step("Restarting %s", serviceName)
|
||||
if err := restartAndVerify(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, " The new version failed to start:", err)
|
||||
step("Restoring %s %s", appName, oldVersion)
|
||||
if rErr := copyFile(oldBin, installBin, 0o755, uid, gid); rErr != nil {
|
||||
return fmt.Errorf("update failed and restoring the old binary failed too: %v (original error: %w)", rErr, err)
|
||||
}
|
||||
if rErr := restartAndVerify(); rErr != nil {
|
||||
return fmt.Errorf("update failed and the old version does not start either: %v (original error: %w)", rErr, err)
|
||||
}
|
||||
return fmt.Errorf("update failed, %s %s is running again: %w", appName, oldVersion, err)
|
||||
}
|
||||
fmt.Printf("\nUpdated %s %s → %s.\n", appName, oldVersion, version)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------- uninstall ----------
|
||||
|
||||
func cmdUninstall(args []string) error {
|
||||
fs := flag.NewFlagSet("uninstall", flag.ExitOnError)
|
||||
purge := fs.Bool("purge", false, "also delete "+installDir+" (config, keys, logs) and the user "+serviceName)
|
||||
yes := fs.Bool("y", false, "do not ask for confirmation")
|
||||
_ = fs.Parse(args)
|
||||
if err := requireRoot(); err != nil {
|
||||
return err
|
||||
}
|
||||
if *purge && !*yes {
|
||||
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
return errors.New("-purge deletes all peers and keys; add -y to confirm")
|
||||
}
|
||||
fmt.Printf("This deletes %s with all peers, keys and logs. Type \"delete\" to continue: ", installDir)
|
||||
var answer string
|
||||
_, _ = fmt.Scanln(&answer)
|
||||
if answer != "delete" {
|
||||
return errors.New("cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
step("Stopping %s", serviceName)
|
||||
_ = sh("systemctl", "disable", "--now", serviceName)
|
||||
|
||||
step("Removing the WireGuard interface and firewall table")
|
||||
c, err := loadConfigFile(configFile)
|
||||
if err != nil {
|
||||
c = &Config{}
|
||||
c.applyDefaults()
|
||||
}
|
||||
if k, err := newKernel(); err == nil {
|
||||
if err := k.Down(c); err != nil {
|
||||
fmt.Fprintln(os.Stderr, " warning:", err)
|
||||
}
|
||||
k.Close()
|
||||
}
|
||||
|
||||
step("Removing system files")
|
||||
for _, p := range []string{unitPath, sysctlPath, modulesPath} {
|
||||
if err := os.Remove(p); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_ = sh("systemctl", "daemon-reload")
|
||||
|
||||
if *purge {
|
||||
step("Deleting %s and the user %s", installDir, serviceName)
|
||||
if err := os.RemoveAll(installDir); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = sh("userdel", serviceName)
|
||||
fmt.Println("\nRemoved everything.")
|
||||
} else {
|
||||
fmt.Printf("\nRemoved. Config, keys and logs are still in %s (uninstall -purge deletes them).\n", installDir)
|
||||
}
|
||||
fmt.Println("IP forwarding stays on until reboot; run 'sysctl -w net.ipv4.ip_forward=0' if nothing else needs it.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------- passwd ----------
|
||||
|
||||
func cmdPasswd(args []string) error {
|
||||
fs := flag.NewFlagSet("passwd", flag.ExitOnError)
|
||||
path := fs.String("config", configFile, "config.json to change")
|
||||
_ = fs.Parse(args)
|
||||
if os.Geteuid() != 0 && runtime.GOOS == "linux" {
|
||||
return errors.New("run as root, e.g. with sudo")
|
||||
}
|
||||
if err := setPassword(*path); err != nil {
|
||||
return err
|
||||
}
|
||||
if *path == configFile && shOut("systemctl", "is-active", serviceName) == "active" {
|
||||
if err := sh("systemctl", "reload", serviceName); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("The running service uses the new password now.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user