refactor: agent/ → agent-bsd/, README.md für BSD-Agent erstellt, Referenzen aktualisiert
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
backendURL string
|
||||
agentID string
|
||||
apiKey string
|
||||
insecure bool
|
||||
|
||||
conn *websocket.Conn
|
||||
connMux sync.RWMutex
|
||||
writeMux sync.Mutex
|
||||
|
||||
reconnectInterval time.Duration
|
||||
pingInterval time.Duration
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
messageHandler *Handler
|
||||
tunnelManager *TunnelManager
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Command string `json:"command,omitempty"`
|
||||
Event string `json:"event,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Params map[string]interface{} `json:"params,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func NewClient(backendURL, agentID, apiKey string, insecure bool) *Client {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
client := &Client{
|
||||
backendURL: backendURL,
|
||||
agentID: agentID,
|
||||
apiKey: apiKey,
|
||||
insecure: insecure,
|
||||
reconnectInterval: 5 * time.Second,
|
||||
pingInterval: 30 * time.Second,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
client.messageHandler = NewHandler(client)
|
||||
client.tunnelManager = NewTunnelManager(client)
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
log.Println("WebSocket Client startet...")
|
||||
|
||||
go c.reconnectLoop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Stop() {
|
||||
log.Println("WebSocket Client stoppt...")
|
||||
c.cancel()
|
||||
|
||||
c.connMux.Lock()
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
c.conn = nil
|
||||
}
|
||||
c.connMux.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) reconnectLoop() {
|
||||
backoff := c.reconnectInterval
|
||||
maxBackoff := 5 * time.Minute
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
if err := c.connect(); err != nil {
|
||||
log.Printf("WebSocket-Verbindung fehlgeschlagen: %v", err)
|
||||
|
||||
select {
|
||||
case <-time.After(backoff):
|
||||
backoff = time.Duration(float64(backoff) * 1.5)
|
||||
if backoff > maxBackoff {
|
||||
backoff = maxBackoff
|
||||
}
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
connStart := time.Now()
|
||||
log.Println("WebSocket-Verbindung hergestellt")
|
||||
|
||||
// Ping/Pong und Message-Handling starten
|
||||
c.runConnection()
|
||||
|
||||
connDuration := time.Since(connStart)
|
||||
log.Printf("WebSocket-Verbindung getrennt (Dauer: %v)", connDuration)
|
||||
|
||||
// Alte Verbindung explizit schliessen
|
||||
c.connMux.Lock()
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
c.conn = nil
|
||||
}
|
||||
c.connMux.Unlock()
|
||||
|
||||
// Minimum-Delay zwischen Reconnects (verhindert Tight-Loop)
|
||||
if connDuration < 5*time.Second {
|
||||
backoff = time.Duration(float64(backoff) * 1.5)
|
||||
if backoff > maxBackoff {
|
||||
backoff = maxBackoff
|
||||
}
|
||||
log.Printf("Verbindung zu kurz, warte %v vor Reconnect", backoff)
|
||||
select {
|
||||
case <-time.After(backoff):
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
}
|
||||
} else {
|
||||
backoff = c.reconnectInterval
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) connect() error {
|
||||
// WebSocket-URL aufbauen
|
||||
u, err := url.Parse(c.backendURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Backend-URL parsen: %w", err)
|
||||
}
|
||||
|
||||
if u.Scheme == "https" {
|
||||
u.Scheme = "wss"
|
||||
} else {
|
||||
u.Scheme = "ws"
|
||||
}
|
||||
|
||||
u.Path = "/api/v1/agent/ws"
|
||||
q := u.Query()
|
||||
q.Set("agent_id", c.agentID)
|
||||
q.Set("api_key", c.apiKey)
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
// Dialer mit TLS-Config
|
||||
dialer := websocket.Dialer{
|
||||
HandshakeTimeout: 10 * time.Second,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: c.insecure,
|
||||
},
|
||||
}
|
||||
|
||||
conn, _, err := dialer.Dial(u.String(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("WebSocket-Dial: %w", err)
|
||||
}
|
||||
|
||||
c.connMux.Lock()
|
||||
c.conn = conn
|
||||
c.connMux.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) runConnection() {
|
||||
// Ping-Ticker starten
|
||||
pingTicker := time.NewTicker(c.pingInterval)
|
||||
defer pingTicker.Stop()
|
||||
|
||||
// Pong-Handler setzen
|
||||
c.connMux.RLock()
|
||||
conn := c.conn
|
||||
c.connMux.RUnlock()
|
||||
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
conn.SetPongHandler(func(string) error {
|
||||
conn.SetReadDeadline(time.Now().Add(c.pingInterval * 2))
|
||||
return nil
|
||||
})
|
||||
conn.SetReadDeadline(time.Now().Add(c.pingInterval * 2))
|
||||
|
||||
// Goroutine für eingehende Nachrichten
|
||||
msgChan := make(chan Message, 10)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
go c.readMessages(conn, msgChan, errChan)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
|
||||
case <-pingTicker.C:
|
||||
c.writeMux.Lock()
|
||||
c.connMux.RLock()
|
||||
conn := c.conn
|
||||
c.connMux.RUnlock()
|
||||
if conn != nil {
|
||||
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
c.writeMux.Unlock()
|
||||
log.Printf("Ping senden fehlgeschlagen: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
c.writeMux.Unlock()
|
||||
|
||||
case msg := <-msgChan:
|
||||
go c.handleMessage(msg)
|
||||
|
||||
case err := <-errChan:
|
||||
log.Printf("WebSocket-Lesefehler: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) readMessages(conn *websocket.Conn, msgChan chan<- Message, errChan chan<- error) {
|
||||
defer close(msgChan)
|
||||
|
||||
for {
|
||||
messageType, data, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
if messageType == websocket.BinaryMessage {
|
||||
// Binary-Message für Tunnel-Daten verarbeiten
|
||||
if err := c.tunnelManager.ProcessBinaryMessage(data); err != nil {
|
||||
log.Printf("Binary-Message verarbeiten fehlgeschlagen: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// JSON-Message parsen
|
||||
var msg Message
|
||||
if err := json.Unmarshal(data, &msg); err != nil {
|
||||
log.Printf("JSON-Message parsen fehlgeschlagen: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case msgChan <- msg:
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) handleMessage(msg Message) {
|
||||
switch msg.Type {
|
||||
case "command":
|
||||
c.messageHandler.HandleCommand(msg)
|
||||
case "event":
|
||||
// Events vom Backend verarbeiten (falls nötig)
|
||||
log.Printf("Event erhalten: %s", msg.Event)
|
||||
default:
|
||||
log.Printf("Unbekannter Nachrichtentyp: %s", msg.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SendMessage(msg Message) error {
|
||||
c.connMux.RLock()
|
||||
conn := c.conn
|
||||
c.connMux.RUnlock()
|
||||
|
||||
if conn == nil {
|
||||
return fmt.Errorf("keine WebSocket-Verbindung")
|
||||
}
|
||||
|
||||
c.writeMux.Lock()
|
||||
err := conn.WriteJSON(msg)
|
||||
c.writeMux.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) SendBinaryData(data []byte) error {
|
||||
c.connMux.RLock()
|
||||
conn := c.conn
|
||||
c.connMux.RUnlock()
|
||||
|
||||
if conn == nil {
|
||||
return fmt.Errorf("keine WebSocket-Verbindung")
|
||||
}
|
||||
|
||||
c.writeMux.Lock()
|
||||
err := conn.WriteMessage(websocket.BinaryMessage, data)
|
||||
c.writeMux.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) IsConnected() bool {
|
||||
c.connMux.RLock()
|
||||
connected := c.conn != nil
|
||||
c.connMux.RUnlock()
|
||||
|
||||
return connected
|
||||
}
|
||||
@@ -0,0 +1,661 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
func NewHandler(client *Client) *Handler {
|
||||
return &Handler{client: client}
|
||||
}
|
||||
|
||||
func (h *Handler) HandleCommand(msg Message) {
|
||||
log.Printf("Command erhalten: %s (ID: %s)", msg.Command, msg.ID)
|
||||
|
||||
var response Message
|
||||
response.Type = "response"
|
||||
response.ID = msg.ID
|
||||
|
||||
switch msg.Command {
|
||||
case "exec":
|
||||
response = h.handleExec(msg)
|
||||
case "update":
|
||||
response = h.handleUpdate(msg)
|
||||
case "major_update":
|
||||
response = h.handleMajorUpdate(msg)
|
||||
case "update_check":
|
||||
response = h.handleUpdateCheck(msg)
|
||||
case "reboot":
|
||||
response = h.handleReboot(msg)
|
||||
case "backup":
|
||||
response = h.handleBackup(msg)
|
||||
case "wg_add_peer":
|
||||
response = h.handleWGAddPeer(msg)
|
||||
case "wg_delete_peer":
|
||||
response = h.handleWGDeletePeer(msg)
|
||||
case "wg_list_peers":
|
||||
response = h.handleWGListPeers(msg)
|
||||
case "tunnel_connect":
|
||||
response = h.handleTunnelConnect(msg)
|
||||
case "tunnel_disconnect":
|
||||
h.handleTunnelDisconnect(msg)
|
||||
return
|
||||
case "tunnel_open":
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{"message": "tunnel_open deprecated, use tunnel_connect"}
|
||||
case "tunnel_close":
|
||||
response.Status = "ok"
|
||||
case "agent_update":
|
||||
response = h.handleAgentUpdate(msg)
|
||||
default:
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Unbekanntes Command: %s", msg.Command)
|
||||
}
|
||||
|
||||
if err := h.client.SendMessage(response); err != nil {
|
||||
log.Printf("Response senden fehlgeschlagen: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleExec fuehrt einen beliebigen Befehl aus
|
||||
func (h *Handler) handleExec(msg Message) Message {
|
||||
response := Message{Type: "response", ID: msg.ID}
|
||||
|
||||
command, ok := msg.Params["command"].(string)
|
||||
if !ok || command == "" {
|
||||
response.Status = "error"
|
||||
response.Error = "Parameter 'command' erforderlich"
|
||||
return response
|
||||
}
|
||||
|
||||
timeoutSec := 30
|
||||
if t, ok := msg.Params["timeout"]; ok {
|
||||
if ts, ok := t.(float64); ok {
|
||||
timeoutSec = int(ts)
|
||||
} else if ts, ok := t.(string); ok {
|
||||
if parsed, err := strconv.Atoi(ts); err == nil {
|
||||
timeoutSec = parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output, err := h.runCommand(command, timeoutSec)
|
||||
|
||||
if err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = err.Error()
|
||||
response.Data = map[string]interface{}{"output": output}
|
||||
} else {
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{"output": output}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// handleUpdateCheck prueft ob Updates verfuegbar sind
|
||||
func (h *Handler) handleUpdateCheck(msg Message) Message {
|
||||
response := Message{Type: "response", ID: msg.ID}
|
||||
log.Println("Update-Check gestartet...")
|
||||
|
||||
result := map[string]interface{}{}
|
||||
|
||||
// 1. OPNsense Core Update pruefen
|
||||
// opnsense-update -c gibt Exit 1 wenn Updates da sind, aber auch bei anderen Fehlern.
|
||||
// Nur wenn stdout tatsaechlich Update-Infos enthaelt, ist ein Update verfuegbar.
|
||||
coreOutput, coreErr := h.runCommand("/usr/local/sbin/opnsense-update -c", 60)
|
||||
coreInfo := strings.TrimSpace(coreOutput)
|
||||
if coreErr != nil && coreInfo != "" {
|
||||
// Exit 1 MIT Output = echtes Update verfuegbar
|
||||
result["core_update_available"] = true
|
||||
result["core_update_info"] = coreInfo
|
||||
} else {
|
||||
result["core_update_available"] = false
|
||||
result["core_update_info"] = "Kein Core-Update verfuegbar"
|
||||
}
|
||||
|
||||
// Reboot-Check: Base/Kernel Version != installierte Version?
|
||||
baseVer, _ := h.runCommand("cat /usr/local/opnsense/version/base", 5)
|
||||
kernelVer, _ := h.runCommand("cat /usr/local/opnsense/version/kernel", 5)
|
||||
coreJson, _ := h.runCommand("cat /usr/local/opnsense/version/core", 5)
|
||||
baseVer = strings.TrimSpace(baseVer)
|
||||
kernelVer = strings.TrimSpace(kernelVer)
|
||||
|
||||
rebootRequired := false
|
||||
// Pruefen ob CORE_SERIES sich von Base/Kernel unterscheidet
|
||||
if strings.Contains(coreJson, "CORE_NEXT") && baseVer != "" && kernelVer != "" {
|
||||
// Einfacher Check: wenn base/kernel neuer als core → reboot noetig
|
||||
coreVer := ""
|
||||
for _, line := range strings.Split(coreJson, "\n") {
|
||||
if strings.Contains(line, "CORE_PKGVERSION") {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
coreVer = strings.Trim(strings.TrimSpace(parts[1]), "\",")
|
||||
}
|
||||
}
|
||||
}
|
||||
if coreVer != "" && baseVer != "" && !strings.HasPrefix(baseVer, strings.Split(coreVer, ".")[0]+"."+strings.Split(coreVer, ".")[1]) {
|
||||
// Base-Version Major.Minor unterscheidet sich von Core → Reboot
|
||||
rebootRequired = true
|
||||
}
|
||||
}
|
||||
result["reboot_required"] = rebootRequired
|
||||
|
||||
// 2. Paket-Updates pruefen
|
||||
pkgOutput, _ := h.runCommand("/usr/sbin/pkg upgrade -n", 60)
|
||||
pendingPackages := h.parsePkgUpgrade(pkgOutput)
|
||||
result["package_update_available"] = len(pendingPackages) > 0
|
||||
result["pending_packages"] = pendingPackages
|
||||
result["pending_count"] = len(pendingPackages)
|
||||
|
||||
response.Status = "ok"
|
||||
response.Data = result
|
||||
log.Printf("Update-Check abgeschlossen: Core=%v, Packages=%d",
|
||||
result["core_update_available"], len(pendingPackages))
|
||||
return response
|
||||
}
|
||||
|
||||
// handleUpdate fuehrt ein normales Update durch (Core + Packages)
|
||||
func (h *Handler) handleUpdate(msg Message) Message {
|
||||
response := Message{Type: "response", ID: msg.ID}
|
||||
log.Println("Normales Update gestartet...")
|
||||
|
||||
reboot := false
|
||||
if r, ok := msg.Params["reboot"].(bool); ok {
|
||||
reboot = r
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"steps": []map[string]interface{}{},
|
||||
}
|
||||
var steps []map[string]interface{}
|
||||
|
||||
// Schritt 1: opnsense-update (Core + Firmware)
|
||||
log.Println("Update Schritt 1: opnsense-update")
|
||||
coreOutput, coreErr := h.runCommand("/usr/local/sbin/opnsense-update", 600)
|
||||
step1 := map[string]interface{}{
|
||||
"step": "opnsense-update",
|
||||
"output": coreOutput,
|
||||
"ok": coreErr == nil,
|
||||
}
|
||||
if coreErr != nil {
|
||||
step1["error"] = coreErr.Error()
|
||||
}
|
||||
steps = append(steps, step1)
|
||||
|
||||
// Schritt 2: pkg upgrade -y (Paket-Updates)
|
||||
log.Println("Update Schritt 2: pkg upgrade -y")
|
||||
pkgOutput, pkgErr := h.runCommand("/usr/sbin/pkg upgrade -y", 600)
|
||||
step2 := map[string]interface{}{
|
||||
"step": "pkg upgrade",
|
||||
"output": pkgOutput,
|
||||
"ok": pkgErr == nil,
|
||||
}
|
||||
if pkgErr != nil {
|
||||
step2["error"] = pkgErr.Error()
|
||||
}
|
||||
steps = append(steps, step2)
|
||||
|
||||
// Schritt 3: opnsense-update -c (Nachpruefung)
|
||||
log.Println("Update Schritt 3: Nachpruefung")
|
||||
checkOutput, checkErr := h.runCommand("/usr/local/sbin/opnsense-update -c", 60)
|
||||
rebootRequired := checkErr != nil // Exit 1 nach Update = Reboot noetig
|
||||
step3 := map[string]interface{}{
|
||||
"step": "verify",
|
||||
"output": checkOutput,
|
||||
"reboot_required": rebootRequired,
|
||||
}
|
||||
steps = append(steps, step3)
|
||||
|
||||
result["steps"] = steps
|
||||
result["reboot_required"] = rebootRequired
|
||||
result["reboot_requested"] = reboot
|
||||
|
||||
// Gesamtstatus
|
||||
if coreErr == nil && pkgErr == nil {
|
||||
response.Status = "ok"
|
||||
if rebootRequired {
|
||||
result["message"] = "Update erfolgreich — Reboot erforderlich"
|
||||
} else {
|
||||
result["message"] = "Update erfolgreich abgeschlossen"
|
||||
}
|
||||
} else {
|
||||
response.Status = "ok"
|
||||
result["message"] = "Update mit Warnungen abgeschlossen"
|
||||
}
|
||||
|
||||
// Reboot wenn gewuenscht
|
||||
if reboot {
|
||||
log.Println("Reboot nach Update angefordert...")
|
||||
result["reboot"] = "Reboot in 5 Sekunden..."
|
||||
response.Data = result
|
||||
|
||||
// Response erst senden, dann rebooten
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second)
|
||||
log.Println("Reboot wird ausgefuehrt...")
|
||||
exec.Command("/sbin/shutdown", "-r", "now").Run()
|
||||
}()
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
response.Data = result
|
||||
log.Println("Update abgeschlossen")
|
||||
return response
|
||||
}
|
||||
|
||||
// handleMajorUpdate fuehrt ein Major-Upgrade durch
|
||||
// Ablauf: Phase 1: opnsense-update -r <ver> -bkp → Reboot
|
||||
// Phase 2 (nach Reboot): pkg-static update -f → pkg-static upgrade -y → optionaler Reboot
|
||||
// WICHTIG: Das neue Paket-Repository wird erst NACH dem Reboot aktiv,
|
||||
// daher muss pkg upgrade zwingend nach dem Reboot laufen.
|
||||
func (h *Handler) handleMajorUpdate(msg Message) Message {
|
||||
response := Message{Type: "response", ID: msg.ID}
|
||||
|
||||
targetVersion, ok := msg.Params["version"].(string)
|
||||
if !ok || targetVersion == "" {
|
||||
response.Status = "error"
|
||||
response.Error = "Parameter 'version' erforderlich (z.B. '26.1')"
|
||||
return response
|
||||
}
|
||||
|
||||
// Phase 2: Wenn phase=2, nur pkg upgrade ausfuehren (nach Reboot)
|
||||
phase := "1"
|
||||
if p, ok := msg.Params["phase"].(string); ok && p != "" {
|
||||
phase = p
|
||||
} else if p, ok := msg.Params["phase"].(float64); ok {
|
||||
phase = fmt.Sprintf("%.0f", p)
|
||||
}
|
||||
|
||||
log.Printf("Major-Update auf Version %s, Phase %s gestartet...", targetVersion, phase)
|
||||
|
||||
result := map[string]interface{}{
|
||||
"target_version": targetVersion,
|
||||
"phase": phase,
|
||||
}
|
||||
var steps []map[string]interface{}
|
||||
hasError := false
|
||||
|
||||
if phase == "2" {
|
||||
// Phase 2: Nach Reboot — Repo auf neue Version umstellen, dann pkg upgrade
|
||||
// Das OPNsense pkg-Repo wird vom opnsense Package kontrolliert (CORE_REPOSITORY in /usr/local/opnsense/version/core)
|
||||
// Nach opnsense-update -r <ver> -bkp + Reboot zeigt das Repo noch die alte Version
|
||||
// Wir muessen die Repo-Config manuell umschreiben
|
||||
repoConf := "/usr/local/etc/pkg/repos/OPNsense.conf"
|
||||
log.Printf("Phase 2: Repo-Config auf %s umstellen", targetVersion)
|
||||
// Aktuelle Series aus dem Repo-Config lesen und durch targetVersion ersetzen
|
||||
sedCmd := fmt.Sprintf("/usr/bin/sed -i '' 's|/[0-9][0-9]\\.[0-9]/latest|/%s/latest|g' %s", targetVersion, repoConf)
|
||||
sedOutput, sedErr := h.runCommand(sedCmd, 10)
|
||||
step0 := map[string]interface{}{
|
||||
"step": "repo-switch to " + targetVersion,
|
||||
"output": sedOutput,
|
||||
"ok": sedErr == nil,
|
||||
}
|
||||
if sedErr != nil {
|
||||
step0["error"] = sedErr.Error()
|
||||
hasError = true
|
||||
}
|
||||
steps = append(steps, step0)
|
||||
|
||||
log.Println("Phase 2: pkg-static update -f")
|
||||
catalogOutput, catalogErr := h.runCommand("/usr/local/sbin/pkg-static update -f", 120)
|
||||
step1 := map[string]interface{}{
|
||||
"step": "pkg-static update -f",
|
||||
"output": catalogOutput,
|
||||
"ok": catalogErr == nil,
|
||||
}
|
||||
if catalogErr != nil {
|
||||
step1["error"] = catalogErr.Error()
|
||||
hasError = true
|
||||
}
|
||||
steps = append(steps, step1)
|
||||
|
||||
log.Println("Phase 2: pkg-static upgrade -y")
|
||||
pkgOutput, pkgErr := h.runCommand("/usr/local/sbin/pkg-static upgrade -y", 900)
|
||||
step2 := map[string]interface{}{
|
||||
"step": "pkg-static upgrade -y",
|
||||
"output": pkgOutput,
|
||||
"ok": pkgErr == nil,
|
||||
}
|
||||
if pkgErr != nil {
|
||||
step2["error"] = pkgErr.Error()
|
||||
hasError = true
|
||||
}
|
||||
steps = append(steps, step2)
|
||||
|
||||
// Verify
|
||||
verOutput, _ := h.runCommand("/usr/local/sbin/opnsense-version", 10)
|
||||
verStep := map[string]interface{}{
|
||||
"step": "verify",
|
||||
"output": strings.TrimSpace(verOutput),
|
||||
"ok": true,
|
||||
}
|
||||
steps = append(steps, verStep)
|
||||
|
||||
result["steps"] = steps
|
||||
result["reboot_required"] = false
|
||||
|
||||
if !hasError {
|
||||
result["message"] = fmt.Sprintf("Major-Update Phase 2 auf %s abgeschlossen — Packages aktualisiert", targetVersion)
|
||||
} else {
|
||||
result["message"] = fmt.Sprintf("Major-Update Phase 2 auf %s mit Fehlern", targetVersion)
|
||||
}
|
||||
|
||||
response.Status = "ok"
|
||||
response.Data = result
|
||||
return response
|
||||
}
|
||||
|
||||
// Phase 1: Base + Kernel installieren, dann Reboot
|
||||
reboot := true
|
||||
if r, ok := msg.Params["reboot"].(bool); ok {
|
||||
reboot = r
|
||||
}
|
||||
|
||||
log.Printf("Phase 1: opnsense-update -r %s -bkp", targetVersion)
|
||||
releaseCmd := fmt.Sprintf("/usr/local/sbin/opnsense-update -r %s -bkp", targetVersion)
|
||||
releaseOutput, releaseErr := h.runCommand(releaseCmd, 900)
|
||||
step1 := map[string]interface{}{
|
||||
"step": fmt.Sprintf("opnsense-update -r %s -bkp", targetVersion),
|
||||
"output": releaseOutput,
|
||||
"ok": releaseErr == nil,
|
||||
}
|
||||
if releaseErr != nil {
|
||||
step1["error"] = releaseErr.Error()
|
||||
hasError = true
|
||||
}
|
||||
steps = append(steps, step1)
|
||||
|
||||
result["steps"] = steps
|
||||
result["reboot_required"] = true
|
||||
result["reboot_requested"] = reboot
|
||||
|
||||
if !hasError {
|
||||
result["message"] = fmt.Sprintf("Major-Update Phase 1 auf %s abgeschlossen — Reboot erforderlich, danach Phase 2 ausfuehren", targetVersion)
|
||||
} else {
|
||||
result["message"] = fmt.Sprintf("Major-Update Phase 1 auf %s mit Fehlern", targetVersion)
|
||||
}
|
||||
|
||||
response.Status = "ok"
|
||||
|
||||
if reboot {
|
||||
log.Println("Reboot nach Phase 1 angefordert...")
|
||||
result["reboot"] = "Reboot in 5 Sekunden..."
|
||||
response.Data = result
|
||||
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second)
|
||||
log.Println("Reboot wird ausgefuehrt...")
|
||||
exec.Command("/sbin/shutdown", "-r", "now").Run()
|
||||
}()
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
response.Data = result
|
||||
log.Printf("Major-Update Phase 1 auf %s abgeschlossen (Reboot noch ausstehend)", targetVersion)
|
||||
return response
|
||||
}
|
||||
|
||||
// handleReboot fuehrt einen Reboot durch
|
||||
func (h *Handler) handleReboot(msg Message) Message {
|
||||
response := Message{Type: "response", ID: msg.ID}
|
||||
|
||||
delaySec := 5
|
||||
if d, ok := msg.Params["delay"].(float64); ok && d > 0 {
|
||||
delaySec = int(d)
|
||||
}
|
||||
|
||||
log.Printf("Reboot angefordert (Verzoegerung: %ds)", delaySec)
|
||||
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{
|
||||
"message": fmt.Sprintf("Reboot in %d Sekunden", delaySec),
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(time.Duration(delaySec) * time.Second)
|
||||
log.Println("Reboot wird ausgefuehrt...")
|
||||
exec.Command("/sbin/shutdown", "-r", "now").Run()
|
||||
}()
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// --- Backup Handler ---
|
||||
|
||||
// handleBackup liest die OPNsense config.xml und sendet sie zurueck
|
||||
func (h *Handler) handleBackup(msg Message) Message {
|
||||
response := Message{Type: "response", ID: msg.ID}
|
||||
|
||||
configPath := "/conf/config.xml"
|
||||
if p, ok := msg.Params["path"].(string); ok && p != "" {
|
||||
configPath = p
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Config lesen fehlgeschlagen: %v", err)
|
||||
return response
|
||||
}
|
||||
|
||||
// SHA256-Hash berechnen
|
||||
hash := sha256.Sum256(data)
|
||||
hashStr := hex.EncodeToString(hash[:])
|
||||
|
||||
// Base64-kodiert senden (config.xml ist Text, aber sicherstellen dass nichts verloren geht)
|
||||
encoded := base64.StdEncoding.EncodeToString(data)
|
||||
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{
|
||||
"config": encoded,
|
||||
"hash": hashStr,
|
||||
"size": len(data),
|
||||
"path": configPath,
|
||||
"encoding": "base64",
|
||||
}
|
||||
|
||||
log.Printf("Backup erstellt: %s (%d Bytes, Hash: %s)", configPath, len(data), hashStr[:12])
|
||||
return response
|
||||
}
|
||||
|
||||
// --- Tunnel Handlers ---
|
||||
|
||||
func (h *Handler) handleTunnelConnect(msg Message) Message {
|
||||
response := Message{Type: "response", ID: msg.ID}
|
||||
|
||||
sessionID, _ := msg.Params["session_id"].(string)
|
||||
tunnelID, _ := msg.Params["tunnel_id"].(string)
|
||||
targetHost, _ := msg.Params["target_host"].(string)
|
||||
targetPortFloat, _ := msg.Params["target_port"].(float64)
|
||||
targetPort := int(targetPortFloat)
|
||||
|
||||
if sessionID == "" || targetHost == "" || targetPort <= 0 {
|
||||
response.Status = "error"
|
||||
response.Error = "session_id, target_host und target_port erforderlich"
|
||||
return response
|
||||
}
|
||||
|
||||
if err := h.client.tunnelManager.ConnectSession(sessionID, tunnelID, targetHost, targetPort); err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Session-Connect fehlgeschlagen: %v", err)
|
||||
return response
|
||||
}
|
||||
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{"session_id": sessionID}
|
||||
return response
|
||||
}
|
||||
|
||||
func (h *Handler) handleTunnelDisconnect(msg Message) {
|
||||
sessionID, _ := msg.Params["session_id"].(string)
|
||||
if sessionID != "" {
|
||||
h.client.tunnelManager.DisconnectSession(sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helper ---
|
||||
|
||||
// runCommand fuehrt einen Shell-Befehl mit Timeout aus
|
||||
func (h *Handler) runCommand(command string, timeoutSec int) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command)
|
||||
output, err := cmd.CombinedOutput()
|
||||
return string(output), err
|
||||
}
|
||||
|
||||
// parsePkgUpgrade parst die Ausgabe von "pkg upgrade -n"
|
||||
func (h *Handler) parsePkgUpgrade(output string) []map[string]string {
|
||||
var packages []map[string]string
|
||||
|
||||
inUpgradeBlock := false
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
if strings.Contains(trimmed, "packages to be UPGRADED") ||
|
||||
strings.Contains(trimmed, "will be affected") {
|
||||
inUpgradeBlock = true
|
||||
continue
|
||||
}
|
||||
|
||||
if inUpgradeBlock && strings.Contains(trimmed, "->") {
|
||||
parts := strings.SplitN(trimmed, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
pkg := strings.TrimSpace(parts[0])
|
||||
versions := strings.TrimSpace(parts[1])
|
||||
|
||||
arrowParts := strings.SplitN(versions, "->", 2)
|
||||
if len(arrowParts) == 2 {
|
||||
packages = append(packages, map[string]string{
|
||||
"package": pkg,
|
||||
"current_version": strings.TrimSpace(arrowParts[0]),
|
||||
"new_version": strings.TrimSpace(arrowParts[1]),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if inUpgradeBlock && (trimmed == "" || strings.HasPrefix(trimmed, "Number of")) {
|
||||
if !strings.HasPrefix(trimmed, "Number of") {
|
||||
inUpgradeBlock = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
func (h *Handler) handleAgentUpdate(msg Message) Message {
|
||||
response := Message{Type: "response", ID: msg.ID}
|
||||
|
||||
// Binary kommt als Base64 im Data-Feld
|
||||
data, ok := msg.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
response.Status = "error"
|
||||
response.Error = "Ungueltige Daten"
|
||||
return response
|
||||
}
|
||||
|
||||
binaryB64, _ := data["binary"].(string)
|
||||
expectedHash, _ := data["hash"].(string)
|
||||
newVersion, _ := data["version"].(string)
|
||||
|
||||
if binaryB64 == "" || expectedHash == "" {
|
||||
response.Status = "error"
|
||||
response.Error = "Binary oder Hash fehlt"
|
||||
return response
|
||||
}
|
||||
|
||||
log.Printf("Agent-Update empfangen: Version %s, Hash %s", newVersion, expectedHash[:16])
|
||||
|
||||
// Base64 dekodieren
|
||||
binary, err := base64.StdEncoding.DecodeString(binaryB64)
|
||||
if err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Base64-Dekodierung fehlgeschlagen: %v", err)
|
||||
return response
|
||||
}
|
||||
|
||||
// Hash pruefen
|
||||
hash := sha256.Sum256(binary)
|
||||
actualHash := hex.EncodeToString(hash[:])
|
||||
if actualHash != expectedHash {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Hash-Mismatch: erwartet %s, bekommen %s", expectedHash[:16], actualHash[:16])
|
||||
return response
|
||||
}
|
||||
|
||||
// Aktuelles Binary finden
|
||||
binaryPath := "/usr/local/rmm/rmm-agent"
|
||||
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
|
||||
// Fallback: eigener Pfad
|
||||
binaryPath, _ = os.Executable()
|
||||
}
|
||||
|
||||
// Backup vom alten Binary
|
||||
backupPath := binaryPath + ".old"
|
||||
if err := os.Rename(binaryPath, backupPath); err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Backup fehlgeschlagen: %v", err)
|
||||
return response
|
||||
}
|
||||
|
||||
// Neues Binary schreiben
|
||||
if err := os.WriteFile(binaryPath, binary, 0755); err != nil {
|
||||
// Rollback
|
||||
os.Rename(backupPath, binaryPath)
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Binary schreiben fehlgeschlagen: %v", err)
|
||||
return response
|
||||
}
|
||||
|
||||
log.Printf("Neues Binary geschrieben: %s (%d bytes)", binaryPath, len(binary))
|
||||
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{
|
||||
"message": fmt.Sprintf("Update auf %s erfolgreich, Neustart...", newVersion),
|
||||
"old_version": data["old_version"],
|
||||
"new_version": newVersion,
|
||||
}
|
||||
|
||||
// Response senden, dann Neustart
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
log.Printf("Agent-Neustart nach Update auf %s", newVersion)
|
||||
exec.Command("/usr/sbin/service", "rmm_agent", "restart").Run()
|
||||
}()
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// SendSessionData schickt Tunnel-Daten als Binary-Message zurueck zum Backend
|
||||
func (h *Handler) SendSessionData(sessionID string, data []byte) error {
|
||||
idBytes := []byte(sessionID)
|
||||
if len(idBytes) > 255 {
|
||||
return fmt.Errorf("session_id zu lang")
|
||||
}
|
||||
|
||||
message := make([]byte, 1+len(idBytes)+len(data))
|
||||
message[0] = byte(len(idBytes))
|
||||
copy(message[1:], idBytes)
|
||||
copy(message[1+len(idBytes):], data)
|
||||
|
||||
return h.client.SendBinaryData(message)
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TunnelSession repraesentiert eine einzelne TCP-Verbindung zu einem Ziel
|
||||
type TunnelSession struct {
|
||||
ID string
|
||||
TunnelID string
|
||||
TargetHost string
|
||||
TargetPort int
|
||||
Conn net.Conn
|
||||
Active bool
|
||||
}
|
||||
|
||||
// TunnelManager verwaltet alle Sessions auf Agent-Seite
|
||||
type TunnelManager struct {
|
||||
client *Client
|
||||
sessions map[string]*TunnelSession
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func NewTunnelManager(client *Client) *TunnelManager {
|
||||
return &TunnelManager{
|
||||
client: client,
|
||||
sessions: make(map[string]*TunnelSession),
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectSession oeffnet eine neue TCP-Verbindung fuer eine Session
|
||||
func (tm *TunnelManager) ConnectSession(sessionID, tunnelID, targetHost string, targetPort int) error {
|
||||
target := fmt.Sprintf("%s:%d", targetHost, targetPort)
|
||||
conn, err := net.DialTimeout("tcp", target, 10*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Verbindung zu %s fehlgeschlagen: %w", target, err)
|
||||
}
|
||||
|
||||
session := &TunnelSession{
|
||||
ID: sessionID,
|
||||
TunnelID: tunnelID,
|
||||
TargetHost: targetHost,
|
||||
TargetPort: targetPort,
|
||||
Conn: conn,
|
||||
Active: true,
|
||||
}
|
||||
|
||||
tm.mutex.Lock()
|
||||
tm.sessions[sessionID] = session
|
||||
tm.mutex.Unlock()
|
||||
|
||||
// Ziel -> Backend Forwarding starten
|
||||
go tm.forwardFromTarget(session)
|
||||
|
||||
log.Printf("Session %s: Verbindung zu %s hergestellt", sessionID, target)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisconnectSession schliesst eine Session
|
||||
func (tm *TunnelManager) DisconnectSession(sessionID string) {
|
||||
tm.mutex.Lock()
|
||||
session, exists := tm.sessions[sessionID]
|
||||
if !exists {
|
||||
tm.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
session.Active = false
|
||||
delete(tm.sessions, sessionID)
|
||||
tm.mutex.Unlock()
|
||||
|
||||
if session.Conn != nil {
|
||||
session.Conn.Close()
|
||||
}
|
||||
|
||||
log.Printf("Session %s: geschlossen", sessionID)
|
||||
}
|
||||
|
||||
// SendDataToSession schreibt Daten in die Ziel-Verbindung einer Session
|
||||
func (tm *TunnelManager) SendDataToSession(sessionID string, data []byte) error {
|
||||
tm.mutex.RLock()
|
||||
session, exists := tm.sessions[sessionID]
|
||||
tm.mutex.RUnlock()
|
||||
|
||||
if !exists || !session.Active {
|
||||
return fmt.Errorf("Session %s nicht aktiv", sessionID)
|
||||
}
|
||||
|
||||
_, err := session.Conn.Write(data)
|
||||
if err != nil {
|
||||
tm.DisconnectSession(sessionID)
|
||||
return fmt.Errorf("Session %s: Write fehlgeschlagen: %w", sessionID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// forwardFromTarget liest Daten vom Ziel und schickt sie als Binary-Message ans Backend
|
||||
func (tm *TunnelManager) forwardFromTarget(session *TunnelSession) {
|
||||
defer func() {
|
||||
if session.Active {
|
||||
tm.DisconnectSession(session.ID)
|
||||
}
|
||||
log.Printf("Session %s: Forwarding beendet", session.ID)
|
||||
}()
|
||||
|
||||
buffer := make([]byte, 32768)
|
||||
|
||||
for session.Active {
|
||||
session.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
|
||||
n, err := session.Conn.Read(buffer)
|
||||
if err != nil {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
continue
|
||||
}
|
||||
if err == io.EOF {
|
||||
log.Printf("Session %s: Ziel-Verbindung geschlossen", session.ID)
|
||||
} else if session.Active {
|
||||
log.Printf("Session %s: Read-Fehler: %v", session.ID, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if n > 0 {
|
||||
if err := tm.client.messageHandler.SendSessionData(session.ID, buffer[:n]); err != nil {
|
||||
log.Printf("Session %s: WebSocket senden fehlgeschlagen: %v", session.ID, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessBinaryMessage verarbeitet eingehende Binary-Messages vom Backend (Proxy -> Ziel)
|
||||
func (tm *TunnelManager) ProcessBinaryMessage(data []byte) error {
|
||||
sessionID, payload, err := ParseBinaryMessage(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Binary-Message parsen: %w", err)
|
||||
}
|
||||
|
||||
return tm.SendDataToSession(sessionID, payload)
|
||||
}
|
||||
|
||||
// GetActiveSessions gibt die Anzahl aktiver Sessions zurueck
|
||||
func (tm *TunnelManager) GetActiveSessions() int {
|
||||
tm.mutex.RLock()
|
||||
defer tm.mutex.RUnlock()
|
||||
return len(tm.sessions)
|
||||
}
|
||||
|
||||
func (tm *TunnelManager) generateID() string {
|
||||
bytes := make([]byte, 8)
|
||||
rand.Read(bytes)
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
// ParseBinaryMessage parst das Binary-Format: [id_len:1][id][data]
|
||||
func ParseBinaryMessage(data []byte) (id string, payload []byte, err error) {
|
||||
if len(data) < 1 {
|
||||
return "", nil, fmt.Errorf("Daten zu kurz")
|
||||
}
|
||||
|
||||
idLen := int(data[0])
|
||||
if len(data) < 1+idLen {
|
||||
return "", nil, fmt.Errorf("ID zu kurz")
|
||||
}
|
||||
|
||||
id = string(data[1 : 1+idLen])
|
||||
payload = data[1+idLen:]
|
||||
return id, payload, nil
|
||||
}
|
||||
@@ -0,0 +1,604 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// --- OPNsense config.xml Structs ---
|
||||
|
||||
type OPNsenseConfig struct {
|
||||
XMLName xml.Name `xml:"opnsense"`
|
||||
WireGuard *WGConfig `xml:"OPNsense>wireguard"`
|
||||
Rest []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type WGConfig struct {
|
||||
General WGGeneral `xml:"general"`
|
||||
Server WGServer `xml:"server"`
|
||||
Client WGClient `xml:"client"`
|
||||
}
|
||||
|
||||
type WGGeneral struct {
|
||||
XMLName xml.Name `xml:"general"`
|
||||
Version string `xml:"version,attr"`
|
||||
PersistedAt string `xml:"persisted_at,attr"`
|
||||
Description string `xml:"description,attr"`
|
||||
Enabled string `xml:"enabled"`
|
||||
}
|
||||
|
||||
type WGServer struct {
|
||||
XMLName xml.Name `xml:"server"`
|
||||
Version string `xml:"version,attr"`
|
||||
PersistedAt string `xml:"persisted_at,attr"`
|
||||
Description string `xml:"description,attr"`
|
||||
Servers []WGServerInst `xml:"servers>server"`
|
||||
}
|
||||
|
||||
type WGServerInst struct {
|
||||
UUID string `xml:"uuid,attr"`
|
||||
Enabled string `xml:"enabled"`
|
||||
Name string `xml:"name"`
|
||||
Instance string `xml:"instance"`
|
||||
PubKey string `xml:"pubkey"`
|
||||
PrivKey string `xml:"privkey"`
|
||||
Port string `xml:"port"`
|
||||
MTU string `xml:"mtu"`
|
||||
DNS string `xml:"dns"`
|
||||
TunnelAddress string `xml:"tunneladdress"`
|
||||
DisableRoutes string `xml:"disableroutes"`
|
||||
Gateway string `xml:"gateway"`
|
||||
CarpDependOn string `xml:"carp_depend_on"`
|
||||
Peers string `xml:"peers"`
|
||||
Debug string `xml:"debug"`
|
||||
Endpoint string `xml:"endpoint"`
|
||||
PeerDNS string `xml:"peer_dns"`
|
||||
}
|
||||
|
||||
type WGClient struct {
|
||||
XMLName xml.Name `xml:"client"`
|
||||
Version string `xml:"version,attr"`
|
||||
PersistedAt string `xml:"persisted_at,attr"`
|
||||
Description string `xml:"description,attr"`
|
||||
Clients []WGClientInst `xml:"clients>client"`
|
||||
}
|
||||
|
||||
type WGClientInst struct {
|
||||
UUID string `xml:"uuid,attr"`
|
||||
Enabled string `xml:"enabled"`
|
||||
Name string `xml:"name"`
|
||||
PubKey string `xml:"pubkey"`
|
||||
PSK string `xml:"psk"`
|
||||
TunnelAddress string `xml:"tunneladdress"`
|
||||
ServerAddress string `xml:"serveraddress"`
|
||||
ServerPort string `xml:"serverport"`
|
||||
KeepAlive string `xml:"keepalive"`
|
||||
}
|
||||
|
||||
// handleWGAddPeer erzeugt einen neuen WireGuard-Peer
|
||||
func (h *Handler) handleWGAddPeer(msg Message) Message {
|
||||
response := Message{Type: "response", ID: msg.ID}
|
||||
|
||||
name, _ := msg.Params["name"].(string)
|
||||
allowedIPs, _ := msg.Params["allowed_ips"].(string)
|
||||
if name == "" {
|
||||
response.Status = "error"
|
||||
response.Error = "Parameter 'name' erforderlich"
|
||||
return response
|
||||
}
|
||||
if allowedIPs == "" {
|
||||
allowedIPs = "0.0.0.0/0"
|
||||
}
|
||||
|
||||
// Optional: DNS fuer Client-Config
|
||||
dns, _ := msg.Params["dns"].(string)
|
||||
|
||||
// Server-Name (default: ALWAYSON_VPN)
|
||||
serverName := "ALWAYSON_VPN"
|
||||
if sn, ok := msg.Params["server_name"].(string); ok && sn != "" {
|
||||
serverName = sn
|
||||
}
|
||||
|
||||
log.Printf("WG Add Peer: name=%s, server=%s, allowed_ips=%s", name, serverName, allowedIPs)
|
||||
|
||||
// 1. config.xml lesen
|
||||
configData, err := os.ReadFile("/conf/config.xml")
|
||||
if err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("config.xml lesen: %v", err)
|
||||
return response
|
||||
}
|
||||
|
||||
// 2. Server-Instanz finden (direkt im XML-Text arbeiten statt Unmarshal,
|
||||
// um die gesamte config.xml nicht zu zerstoeren)
|
||||
// Wir parsen nur den WireGuard-Teil, den Rest lassen wir in Ruhe.
|
||||
|
||||
// Server-Instanz per configctl/PHP auslesen und bearbeiten
|
||||
// Sicherer Weg: OPNsense PHP-API nutzen
|
||||
serverUUID, serverPubKey, serverPort, tunnelSubnet, err := h.findWGServer(string(configData), serverName)
|
||||
if err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = err.Error()
|
||||
return response
|
||||
}
|
||||
|
||||
// 3. Naechste freie IP im Subnetz bestimmen
|
||||
usedIPs := h.getUsedPeerIPs(string(configData), serverUUID, tunnelSubnet)
|
||||
nextIP, err := h.nextFreeIP(tunnelSubnet, usedIPs)
|
||||
if err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Keine freie IP: %v", err)
|
||||
return response
|
||||
}
|
||||
|
||||
// 4. Keypair generieren
|
||||
privKeyOutput, err := h.runCommand("/usr/bin/wg genkey", 5)
|
||||
if err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("wg genkey fehlgeschlagen: %v", err)
|
||||
return response
|
||||
}
|
||||
clientPrivKey := strings.TrimSpace(privKeyOutput)
|
||||
|
||||
pubKeyOutput, err := h.runCommand(fmt.Sprintf("echo '%s' | /usr/bin/wg pubkey", clientPrivKey), 5)
|
||||
if err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("wg pubkey fehlgeschlagen: %v", err)
|
||||
return response
|
||||
}
|
||||
clientPubKey := strings.TrimSpace(pubKeyOutput)
|
||||
|
||||
// 5. UUID fuer neuen Peer generieren
|
||||
uuidOutput, err := h.runCommand("/bin/uuidgen", 5)
|
||||
if err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("uuidgen fehlgeschlagen: %v", err)
|
||||
return response
|
||||
}
|
||||
peerUUID := strings.ToLower(strings.TrimSpace(uuidOutput))
|
||||
|
||||
// 6. Peer in config.xml eintragen (direkt per sed — zuverlaessiger als PHP parse_config)
|
||||
err = h.addPeerViaSed(peerUUID, name, clientPubKey, nextIP, serverUUID)
|
||||
if err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Peer eintragen fehlgeschlagen: %v", err)
|
||||
return response
|
||||
}
|
||||
|
||||
// 7. WireGuard neu laden
|
||||
reloadOutput, reloadErr := h.runCommand("/usr/local/sbin/configctl wireguard configure", 15)
|
||||
if reloadErr != nil {
|
||||
log.Printf("WireGuard reload Warnung: %v (%s)", reloadErr, reloadOutput)
|
||||
}
|
||||
|
||||
// 8. Endpoint ermitteln
|
||||
// Prioritaet: 1. Parameter "endpoint" 2. Default-Gateway-Interface IP 3. hostname
|
||||
port := serverPort
|
||||
if port == "" {
|
||||
port = "51820"
|
||||
}
|
||||
|
||||
endpoint := ""
|
||||
if ep, ok := msg.Params["endpoint"].(string); ok && ep != "" {
|
||||
// Wenn nur Host/IP ohne Port angegeben
|
||||
if !strings.Contains(ep, ":") {
|
||||
endpoint = fmt.Sprintf("%s:%s", ep, port)
|
||||
} else {
|
||||
endpoint = ep
|
||||
}
|
||||
} else {
|
||||
// WAN-IP ueber Default-Route Interface ermitteln
|
||||
routeOutput, _ := h.runCommand("/sbin/route -n get default 2>/dev/null | /usr/bin/grep interface | /usr/bin/awk '{print $2}'", 5)
|
||||
wanIface := strings.TrimSpace(routeOutput)
|
||||
if wanIface != "" {
|
||||
ipOutput, _ := h.runCommand(fmt.Sprintf("/sbin/ifconfig %s 2>/dev/null | /usr/bin/grep 'inet ' | /usr/bin/awk '{print $2}' | /usr/bin/head -1", wanIface), 5)
|
||||
wanIP := strings.TrimSpace(ipOutput)
|
||||
if wanIP != "" {
|
||||
endpoint = fmt.Sprintf("%s:%s", wanIP, port)
|
||||
}
|
||||
}
|
||||
if endpoint == "" {
|
||||
hostnameOutput, _ := h.runCommand("/bin/hostname", 5)
|
||||
endpoint = fmt.Sprintf("%s:%s", strings.TrimSpace(hostnameOutput), port)
|
||||
}
|
||||
}
|
||||
|
||||
// 9. Client-Config zusammenbauen
|
||||
clientConfig := fmt.Sprintf(`[Interface]
|
||||
PrivateKey = %s
|
||||
Address = %s/32
|
||||
`, clientPrivKey, nextIP)
|
||||
|
||||
if dns != "" {
|
||||
clientConfig += fmt.Sprintf("DNS = %s\n", dns)
|
||||
}
|
||||
|
||||
clientConfig += fmt.Sprintf(`
|
||||
[Peer]
|
||||
PublicKey = %s
|
||||
AllowedIPs = %s
|
||||
Endpoint = %s
|
||||
PersistentKeepalive = 25
|
||||
`, serverPubKey, allowedIPs, endpoint)
|
||||
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{
|
||||
"peer_uuid": peerUUID,
|
||||
"name": name,
|
||||
"client_ip": nextIP,
|
||||
"public_key": clientPubKey,
|
||||
"server_pubkey": serverPubKey,
|
||||
"endpoint": endpoint,
|
||||
"allowed_ips": allowedIPs,
|
||||
"client_config": clientConfig,
|
||||
"server_name": serverName,
|
||||
"server_uuid": serverUUID,
|
||||
}
|
||||
|
||||
log.Printf("WG Peer erstellt: %s (%s) -> %s", name, nextIP, serverName)
|
||||
return response
|
||||
}
|
||||
|
||||
// handleWGDeletePeer entfernt einen WireGuard-Peer
|
||||
func (h *Handler) handleWGDeletePeer(msg Message) Message {
|
||||
response := Message{Type: "response", ID: msg.ID}
|
||||
|
||||
peerUUID, _ := msg.Params["peer_uuid"].(string)
|
||||
if peerUUID == "" {
|
||||
response.Status = "error"
|
||||
response.Error = "Parameter 'peer_uuid' erforderlich"
|
||||
return response
|
||||
}
|
||||
|
||||
log.Printf("WG Delete Peer: uuid=%s", peerUUID)
|
||||
|
||||
// Peer per sed entfernen
|
||||
if err := h.removePeerViaSed(peerUUID); err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Peer loeschen fehlgeschlagen: %v", err)
|
||||
return response
|
||||
}
|
||||
|
||||
// WireGuard reload
|
||||
h.runCommand("/usr/local/sbin/configctl wireguard configure", 15)
|
||||
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{
|
||||
"peer_uuid": peerUUID,
|
||||
"message": "Peer entfernt",
|
||||
}
|
||||
|
||||
log.Printf("WG Peer geloescht: %s", peerUUID)
|
||||
return response
|
||||
}
|
||||
|
||||
// handleWGListPeers listet alle Peers einer WG-Instanz
|
||||
func (h *Handler) handleWGListPeers(msg Message) Message {
|
||||
response := Message{Type: "response", ID: msg.ID}
|
||||
|
||||
serverName := "ALWAYSON_VPN"
|
||||
if sn, ok := msg.Params["server_name"].(string); ok && sn != "" {
|
||||
serverName = sn
|
||||
}
|
||||
|
||||
configData, err := os.ReadFile("/conf/config.xml")
|
||||
if err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("config.xml lesen: %v", err)
|
||||
return response
|
||||
}
|
||||
|
||||
configStr := string(configData)
|
||||
serverUUID, _, _, tunnelSubnet, err := h.findWGServer(configStr, serverName)
|
||||
if err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = err.Error()
|
||||
return response
|
||||
}
|
||||
|
||||
// Peer-UUIDs aus Server lesen
|
||||
peerUUIDs := h.getServerPeerUUIDs(configStr, serverUUID)
|
||||
|
||||
// Peer-Details sammeln
|
||||
var peers []map[string]interface{}
|
||||
for _, uuid := range peerUUIDs {
|
||||
peer := h.getPeerByUUID(configStr, uuid)
|
||||
if peer != nil {
|
||||
peers = append(peers, peer)
|
||||
}
|
||||
}
|
||||
|
||||
if peers == nil {
|
||||
peers = []map[string]interface{}{}
|
||||
}
|
||||
|
||||
// Live WG-Status (Handshake, Transfer)
|
||||
wgStatus, _ := h.runCommand("/usr/bin/wg show all", 5)
|
||||
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{
|
||||
"server_name": serverName,
|
||||
"server_uuid": serverUUID,
|
||||
"subnet": tunnelSubnet,
|
||||
"peer_count": len(peers),
|
||||
"peers": peers,
|
||||
"wg_status": strings.TrimSpace(wgStatus),
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// --- Helper Functions ---
|
||||
|
||||
// findWGServer sucht eine WG-Server-Instanz nach Name in der config.xml
|
||||
func (h *Handler) findWGServer(configXML, serverName string) (uuid, pubkey, port, tunnelAddr string, err error) {
|
||||
type serverEntry struct {
|
||||
UUID string `xml:"uuid,attr"`
|
||||
Name string `xml:"name"`
|
||||
PubKey string `xml:"pubkey"`
|
||||
Port string `xml:"port"`
|
||||
TunnelAddress string `xml:"tunneladdress"`
|
||||
Peers string `xml:"peers"`
|
||||
}
|
||||
|
||||
// Einfaches XML-Parsing nur fuer den WG-Teil
|
||||
type opnsenseWG struct {
|
||||
Servers []serverEntry `xml:"OPNsense>wireguard>server>servers>server"`
|
||||
}
|
||||
|
||||
var cfg opnsenseWG
|
||||
if xmlErr := xml.Unmarshal([]byte(configXML), &cfg); xmlErr != nil {
|
||||
return "", "", "", "", fmt.Errorf("XML parsen: %v", xmlErr)
|
||||
}
|
||||
|
||||
for _, srv := range cfg.Servers {
|
||||
if srv.Name == serverName {
|
||||
return srv.UUID, srv.PubKey, srv.Port, srv.TunnelAddress, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", "", "", fmt.Errorf("WireGuard-Server '%s' nicht gefunden", serverName)
|
||||
}
|
||||
|
||||
// getUsedPeerIPs sammelt alle vergebenen IPs im Server-Subnetz
|
||||
func (h *Handler) getUsedPeerIPs(configXML, serverUUID, tunnelAddr string) []string {
|
||||
type clientEntry struct {
|
||||
UUID string `xml:"uuid,attr"`
|
||||
TunnelAddress string `xml:"tunneladdress"`
|
||||
}
|
||||
type opnsenseClients struct {
|
||||
Clients []clientEntry `xml:"OPNsense>wireguard>client>clients>client"`
|
||||
}
|
||||
|
||||
var cfg opnsenseClients
|
||||
xml.Unmarshal([]byte(configXML), &cfg)
|
||||
|
||||
peerUUIDs := h.getServerPeerUUIDs(configXML, serverUUID)
|
||||
peerSet := make(map[string]bool)
|
||||
for _, u := range peerUUIDs {
|
||||
peerSet[u] = true
|
||||
}
|
||||
|
||||
var ips []string
|
||||
for _, c := range cfg.Clients {
|
||||
if peerSet[c.UUID] {
|
||||
// tunneladdress kann "10.33.144.2/32" sein
|
||||
addr := strings.Split(c.TunnelAddress, "/")[0]
|
||||
if addr != "" {
|
||||
ips = append(ips, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ips
|
||||
}
|
||||
|
||||
// getServerPeerUUIDs liest die Peer-UUIDs aus dem Server-Eintrag
|
||||
func (h *Handler) getServerPeerUUIDs(configXML, serverUUID string) []string {
|
||||
type serverEntry struct {
|
||||
UUID string `xml:"uuid,attr"`
|
||||
Peers string `xml:"peers"`
|
||||
}
|
||||
type opnsenseServers struct {
|
||||
Servers []serverEntry `xml:"OPNsense>wireguard>server>servers>server"`
|
||||
}
|
||||
|
||||
var cfg opnsenseServers
|
||||
xml.Unmarshal([]byte(configXML), &cfg)
|
||||
|
||||
for _, srv := range cfg.Servers {
|
||||
if srv.UUID == serverUUID {
|
||||
var uuids []string
|
||||
for _, p := range strings.Split(srv.Peers, ",") {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
uuids = append(uuids, p)
|
||||
}
|
||||
}
|
||||
return uuids
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getPeerByUUID liest Peer-Details aus config.xml
|
||||
func (h *Handler) getPeerByUUID(configXML, uuid string) map[string]interface{} {
|
||||
type clientEntry struct {
|
||||
UUID string `xml:"uuid,attr"`
|
||||
Enabled string `xml:"enabled"`
|
||||
Name string `xml:"name"`
|
||||
PubKey string `xml:"pubkey"`
|
||||
TunnelAddress string `xml:"tunneladdress"`
|
||||
}
|
||||
type opnsenseClients struct {
|
||||
Clients []clientEntry `xml:"OPNsense>wireguard>client>clients>client"`
|
||||
}
|
||||
|
||||
var cfg opnsenseClients
|
||||
xml.Unmarshal([]byte(configXML), &cfg)
|
||||
|
||||
for _, c := range cfg.Clients {
|
||||
if c.UUID == uuid {
|
||||
return map[string]interface{}{
|
||||
"uuid": c.UUID,
|
||||
"enabled": c.Enabled,
|
||||
"name": c.Name,
|
||||
"public_key": c.PubKey,
|
||||
"tunnel_address": c.TunnelAddress,
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// nextFreeIP berechnet die naechste freie IP in einem Subnetz
|
||||
func (h *Handler) nextFreeIP(tunnelAddr string, usedIPs []string) (string, error) {
|
||||
// tunnelAddr: "10.33.144.1/24"
|
||||
_, ipNet, err := net.ParseCIDR(tunnelAddr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Subnetz parsen: %v", err)
|
||||
}
|
||||
|
||||
usedSet := make(map[string]bool)
|
||||
// Server-IP selbst ist auch belegt
|
||||
serverIP := strings.Split(tunnelAddr, "/")[0]
|
||||
usedSet[serverIP] = true
|
||||
for _, ip := range usedIPs {
|
||||
usedSet[ip] = true
|
||||
}
|
||||
|
||||
// Ab .2 durchiterieren
|
||||
ip := make(net.IP, len(ipNet.IP))
|
||||
copy(ip, ipNet.IP)
|
||||
|
||||
for i := 2; i < 255; i++ {
|
||||
candidate := make(net.IP, len(ip))
|
||||
copy(candidate, ip)
|
||||
candidate[len(candidate)-1] = byte(i)
|
||||
|
||||
if !ipNet.Contains(candidate) {
|
||||
break
|
||||
}
|
||||
if !usedSet[candidate.String()] {
|
||||
return candidate.String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("kein freier IP-Platz im Subnetz %s", tunnelAddr)
|
||||
}
|
||||
|
||||
// addPeerViaSed fuegt Peer per sed-Kommandos in config.xml ein
|
||||
// Zuverlaessiger als PHP parse_config, behandelt <peers/> und <peers>...</peers> korrekt
|
||||
func (h *Handler) addPeerViaSed(peerUUID, name, pubKey, clientIP, serverUUID string) error {
|
||||
configPath := "/conf/config.xml"
|
||||
|
||||
// 1. Client-Block vor </clients> einfuegen (direkt in Go)
|
||||
configData, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("config.xml lesen: %v", err)
|
||||
}
|
||||
|
||||
clientBlock := fmt.Sprintf(
|
||||
"\n <client uuid=\"%s\">\n <enabled>1</enabled>\n <name>%s</name>\n <pubkey>%s</pubkey>\n <psk/>\n <tunneladdress>%s/32</tunneladdress>\n <serveraddress/>\n <serverport/>\n <keepalive/>\n </client>\n ",
|
||||
peerUUID, name, pubKey, clientIP)
|
||||
|
||||
configStr := string(configData)
|
||||
// Vor dem letzten </clients> einfuegen
|
||||
lastIdx := strings.LastIndex(configStr, "</clients>")
|
||||
if lastIdx < 0 {
|
||||
return fmt.Errorf("</clients> nicht in config.xml gefunden")
|
||||
}
|
||||
configStr = configStr[:lastIdx] + clientBlock + configStr[lastIdx:]
|
||||
|
||||
if err := os.WriteFile(configPath, []byte(configStr), 0644); err != nil {
|
||||
return fmt.Errorf("config.xml schreiben: %v", err)
|
||||
}
|
||||
|
||||
// 2. Peer-UUID zum Server hinzufuegen
|
||||
// Config nochmal lesen (wurde gerade geschrieben)
|
||||
configData2, err2 := os.ReadFile(configPath)
|
||||
if err2 != nil {
|
||||
return fmt.Errorf("config.xml lesen: %v", err2)
|
||||
}
|
||||
configStr = string(configData2)
|
||||
|
||||
serverTag := fmt.Sprintf(`uuid="%s"`, serverUUID)
|
||||
idx := strings.Index(configStr, serverTag)
|
||||
if idx < 0 {
|
||||
return fmt.Errorf("Server %s nicht gefunden", serverUUID)
|
||||
}
|
||||
|
||||
// Suche im Server-Block (max 2000 Zeichen nach dem UUID-Tag)
|
||||
searchEnd := idx + 2000
|
||||
if searchEnd > len(configStr) {
|
||||
searchEnd = len(configStr)
|
||||
}
|
||||
searchFrom := configStr[idx:searchEnd]
|
||||
|
||||
// Fall 1: <peers/> (self-closing, leer)
|
||||
selfClosingIdx := strings.Index(searchFrom, "<peers/>")
|
||||
if selfClosingIdx >= 0 {
|
||||
absIdx := idx + selfClosingIdx
|
||||
newPeersTag := fmt.Sprintf("<peers>%s</peers>", peerUUID)
|
||||
configStr = configStr[:absIdx] + newPeersTag + configStr[absIdx+8:]
|
||||
} else {
|
||||
// Fall 2: <peers>...</peers>
|
||||
peersStart := strings.Index(searchFrom, "<peers>")
|
||||
peersEnd := strings.Index(searchFrom, "</peers>")
|
||||
if peersStart >= 0 && peersEnd >= 0 {
|
||||
absStart := idx + peersStart + 7
|
||||
absEnd := idx + peersEnd
|
||||
currentPeers := strings.TrimSpace(configStr[absStart:absEnd])
|
||||
var newPeers string
|
||||
if currentPeers == "" {
|
||||
newPeers = peerUUID
|
||||
} else {
|
||||
newPeers = currentPeers + "," + peerUUID
|
||||
}
|
||||
configStr = configStr[:absStart] + newPeers + configStr[absEnd:]
|
||||
} else {
|
||||
return fmt.Errorf("peers Tag nicht im Server-Block gefunden")
|
||||
}
|
||||
}
|
||||
|
||||
return os.WriteFile(configPath, []byte(configStr), 0644)
|
||||
}
|
||||
|
||||
// removePeerViaSed entfernt einen Peer per sed aus config.xml
|
||||
func (h *Handler) removePeerViaSed(peerUUID string) error {
|
||||
configPath := "/conf/config.xml"
|
||||
|
||||
// 1. Client-Block entfernen (alles von <client uuid="..."> bis </client>)
|
||||
sedRemove := fmt.Sprintf(
|
||||
`/usr/bin/sed -i '' '/<client uuid="%s">/,/<\/client>/d' %s`,
|
||||
peerUUID, configPath)
|
||||
if _, err := h.runCommand(sedRemove, 5); err != nil {
|
||||
return fmt.Errorf("Client entfernen: %v", err)
|
||||
}
|
||||
|
||||
// 2. UUID aus allen <peers>-Tags entfernen
|
||||
configData, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configStr := string(configData)
|
||||
|
||||
// Alle Vorkommen der UUID in <peers>-Tags bereinigen
|
||||
// Varianten: "uuid", "uuid,other", "other,uuid", "other,uuid,more"
|
||||
replacements := []struct{ old, new string }{
|
||||
{fmt.Sprintf("<peers>%s,", peerUUID), "<peers>"},
|
||||
{fmt.Sprintf(",%s</peers>", peerUUID), "</peers>"},
|
||||
{fmt.Sprintf("<peers>%s</peers>", peerUUID), "<peers/>"},
|
||||
{fmt.Sprintf(",%s,", peerUUID), ","},
|
||||
}
|
||||
|
||||
for _, r := range replacements {
|
||||
configStr = strings.ReplaceAll(configStr, r.old, r.new)
|
||||
}
|
||||
|
||||
return os.WriteFile(configPath, []byte(configStr), 0644)
|
||||
}
|
||||
Reference in New Issue
Block a user