Add wireguard-ui-multi core: multi-server DB, WireGuard manager, REST API, UI, installers
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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3d6608ef80
commit
3b3ffd8ebf
@@ -0,0 +1,203 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user