Files
wireguard-ui-multi/internal/wireguard/migrate.go
T
sysopsandClaude Sonnet 5 3b3ffd8ebf 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>
2026-07-10 02:53:14 +02:00

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
}