Implements the from-scratch multi-server WireGuard management fork per CLAUDE.md spec: sqlite schema (servers/peers/audit_log/users), Curve25519 key generation, per-interface config rendering + wg-quick/systemd control, nftables hook scaffolding, session+CSRF-protected REST API with QR code and config download endpoints, a minimal vanilla-JS web UI, legacy wg0.conf migration, and both a native installer and a Proxmox LXC provisioning script (with auto-detected latest Debian template). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
204 lines
5.2 KiB
Go
204 lines
5.2 KiB
Go
package wireguard
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gitea.perlbach24.de/scripte/wireguard-ui-multi/internal/server"
|
|
)
|
|
|
|
// ParsedLegacyConfig is the parsed result of a legacy wg-quick style config file.
|
|
type ParsedLegacyConfig struct {
|
|
PrivateKey string
|
|
Address string // e.g. "10.10.0.1/24" (used as AddressRange for the new Server)
|
|
ListenPort int
|
|
DNS string
|
|
MTU int
|
|
Peers []ParsedLegacyPeer
|
|
}
|
|
|
|
// ParsedLegacyPeer is a single [Peer] section from a legacy config.
|
|
type ParsedLegacyPeer struct {
|
|
Name string
|
|
PublicKey string
|
|
PresharedKey string
|
|
AllowedIPs string
|
|
Endpoint string
|
|
PersistentKeepalive int
|
|
}
|
|
|
|
// ParseLegacyConfig reads and parses a wg-quick INI-style config file (e.g. /etc/wireguard/wg0.conf).
|
|
func ParseLegacyConfig(path string) (*ParsedLegacyConfig, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
cfg := &ParsedLegacyConfig{}
|
|
var curSection string
|
|
var curPeer *ParsedLegacyPeer
|
|
|
|
// pendingName holds a comment found on the line(s) immediately before a
|
|
// "[Peer]" header, e.g. "# client-laptop". wg-quick has no native peer
|
|
// name field, so this is the only place a human-readable name can come
|
|
// from; it's consumed (and reset) as soon as the next [Peer] section starts.
|
|
var pendingName string
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
if strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
|
|
pendingName = strings.TrimSpace(strings.TrimLeft(line, "#;"))
|
|
continue
|
|
}
|
|
|
|
// Strip inline comments.
|
|
if idx := strings.IndexAny(line, "#;"); idx >= 0 {
|
|
line = strings.TrimSpace(line[:idx])
|
|
if line == "" {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
|
section := strings.ToLower(strings.TrimSpace(line[1 : len(line)-1]))
|
|
switch section {
|
|
case "interface":
|
|
curSection = "interface"
|
|
curPeer = nil
|
|
case "peer":
|
|
curSection = "peer"
|
|
cfg.Peers = append(cfg.Peers, ParsedLegacyPeer{Name: pendingName})
|
|
curPeer = &cfg.Peers[len(cfg.Peers)-1]
|
|
default:
|
|
curSection = ""
|
|
curPeer = nil
|
|
}
|
|
pendingName = ""
|
|
continue
|
|
}
|
|
|
|
key, value, ok := splitKV(line)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
switch curSection {
|
|
case "interface":
|
|
switch {
|
|
case strings.EqualFold(key, "PrivateKey"):
|
|
cfg.PrivateKey = value
|
|
case strings.EqualFold(key, "Address"):
|
|
cfg.Address = value
|
|
case strings.EqualFold(key, "ListenPort"):
|
|
cfg.ListenPort, _ = strconv.Atoi(value)
|
|
case strings.EqualFold(key, "DNS"):
|
|
cfg.DNS = value
|
|
case strings.EqualFold(key, "MTU"):
|
|
cfg.MTU, _ = strconv.Atoi(value)
|
|
}
|
|
case "peer":
|
|
if curPeer == nil {
|
|
continue
|
|
}
|
|
switch {
|
|
case strings.EqualFold(key, "PublicKey"):
|
|
curPeer.PublicKey = value
|
|
case strings.EqualFold(key, "PresharedKey"):
|
|
curPeer.PresharedKey = value
|
|
case strings.EqualFold(key, "AllowedIPs"):
|
|
curPeer.AllowedIPs = value
|
|
case strings.EqualFold(key, "Endpoint"):
|
|
curPeer.Endpoint = value
|
|
case strings.EqualFold(key, "PersistentKeepalive"):
|
|
curPeer.PersistentKeepalive, _ = strconv.Atoi(value)
|
|
}
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func splitKV(line string) (key, value string, ok bool) {
|
|
idx := strings.Index(line, "=")
|
|
if idx < 0 {
|
|
return "", "", false
|
|
}
|
|
key = strings.TrimSpace(line[:idx])
|
|
value = strings.TrimSpace(line[idx+1:])
|
|
if key == "" {
|
|
return "", "", false
|
|
}
|
|
return key, value, true
|
|
}
|
|
|
|
// ImportLegacyServer parses legacyConfPath and creates a corresponding Server + its Peers
|
|
// in the given store, using serverName and interfaceName for the new Server record.
|
|
// Returns the new server's ID.
|
|
func ImportLegacyServer(store *server.Store, legacyConfPath, serverName, interfaceName string) (int64, error) {
|
|
parsed, err := ParseLegacyConfig(legacyConfPath)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("parse legacy config %q: %w", legacyConfPath, err)
|
|
}
|
|
|
|
pubKey, err := PublicFromPrivate(parsed.PrivateKey)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("derive public key: %w", err)
|
|
}
|
|
|
|
mtu := parsed.MTU
|
|
if mtu == 0 {
|
|
mtu = 1420
|
|
}
|
|
|
|
srv := &server.Server{
|
|
Name: serverName,
|
|
InterfaceName: interfaceName,
|
|
ListenPort: parsed.ListenPort,
|
|
PrivateKey: parsed.PrivateKey,
|
|
PublicKey: pubKey,
|
|
AddressRange: parsed.Address,
|
|
DNS: parsed.DNS,
|
|
MTU: mtu,
|
|
Enabled: true,
|
|
}
|
|
|
|
serverID, err := store.CreateServer(srv)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("create server: %w", err)
|
|
}
|
|
|
|
for i, pp := range parsed.Peers {
|
|
name := pp.Name
|
|
if name == "" {
|
|
name = fmt.Sprintf("peer-%d", i+1)
|
|
}
|
|
peer := &server.Peer{
|
|
ServerID: serverID,
|
|
Name: name,
|
|
PublicKey: pp.PublicKey,
|
|
PresharedKey: pp.PresharedKey,
|
|
AllowedIPs: pp.AllowedIPs,
|
|
Endpoint: pp.Endpoint,
|
|
PersistentKeepalive: pp.PersistentKeepalive,
|
|
Enabled: true,
|
|
}
|
|
if _, err := store.CreatePeer(peer); err != nil {
|
|
return serverID, fmt.Errorf("create peer %q (index %d): %w", name, i, err)
|
|
}
|
|
}
|
|
|
|
return serverID, nil
|
|
}
|