diff --git a/README.md b/README.md index 04b1611..10a6de9 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,9 @@ Client ──► Backend:ProxyPort ──► WebSocket (Binary) ──► Agent | `update` | `POST /agents/{id}/update` | Normales Update (Core + Packages, opt. Reboot) | | `major_update` | `POST /agents/{id}/major-update` | Major-Upgrade (2-Phasen: Base/Kernel → Reboot → Packages) | | `reboot` | `POST /agents/{id}/reboot` | Reboot mit konfigurierbarer Verzoegerung | +| `wg_add_peer` | `POST /agents/{id}/wireguard/peers` | WireGuard-Peer anlegen (Keypair + Config) | +| `wg_list_peers` | `GET /agents/{id}/wireguard/peers` | Alle Peers einer WG-Instanz auflisten | +| `wg_delete_peer` | `DELETE /agents/{id}/wireguard/peers/{uuid}` | WireGuard-Peer loeschen | | `tunnel_connect` | `POST /agents/{id}/tunnel` | TCP-Tunnel zu lokalem Dienst oeffnen | | `tunnel_disconnect` | `DELETE /agents/{id}/tunnel/{tid}` | Tunnel schliessen | @@ -262,6 +265,60 @@ curl -sk -H "X-API-Key: 01532e5a7c9e70bf2757df77a2f5b9b9" -X POST \ -d '{"command":"uptime","timeout":30}' ``` +### WireGuard-Peer Management + +| Methode | Endpoint | Beschreibung | +|---------|----------|-------------| +| POST | `/api/v1/agents/{id}/wireguard/peers` | Neuen Peer anlegen | +| GET | `/api/v1/agents/{id}/wireguard/peers` | Alle Peers auflisten (`?server_name=X`) | +| DELETE | `/api/v1/agents/{id}/wireguard/peers/{uuid}` | Peer loeschen | + +**Peer anlegen — Parameter:** + +| Parameter | Pflicht | Default | Beschreibung | +|-----------|---------|---------|-------------| +| `name` | Ja | — | Peer-Name (z.B. "VPN_MUELLER") | +| `allowed_ips` | Nein | `0.0.0.0/0` | AllowedIPs fuer Client-Config | +| `dns` | Nein | — | DNS-Server fuer Client-Config | +| `server_name` | Nein | `ALWAYSON_VPN` | Name der WG-Server-Instanz | +| `endpoint` | Nein | WAN-IP:Port | Oeffentlicher Endpoint (Auto-Detect ueber Default-Route) | + +**Ablauf beim Anlegen:** +1. Keypair wird auf der Firewall generiert (`wg genkey`/`wg pubkey`) +2. Naechste freie IP aus dem Tunnel-Subnetz wird automatisch vergeben +3. Peer wird in `config.xml` eingetragen und dem Server zugewiesen +4. `configctl wireguard configure` laedt die Aenderung +5. Response enthaelt die komplette Client-Config (ready for import) + +**Beispiel:** + +```bash +# Peer anlegen +curl -sk -H "X-API-Key: API_KEY" -X POST \ + https://BACKEND/api/v1/agents/AGENT_ID/wireguard/peers \ + -d '{"name":"VPN_MUELLER","allowed_ips":"0.0.0.0/0","dns":"10.172.100.210"}' + +# Response enthaelt client_config: +# [Interface] +# PrivateKey = ... +# Address = 10.33.144.2/32 +# DNS = 10.172.100.210 +# +# [Peer] +# PublicKey = ... +# AllowedIPs = 0.0.0.0/0 +# Endpoint = 79.238.149.26:8080 +# PersistentKeepalive = 25 + +# Peers auflisten +curl -sk -H "X-API-Key: API_KEY" \ + https://BACKEND/api/v1/agents/AGENT_ID/wireguard/peers + +# Peer loeschen +curl -sk -H "X-API-Key: API_KEY" -X DELETE \ + https://BACKEND/api/v1/agents/AGENT_ID/wireguard/peers/PEER_UUID +``` + ### Backup-Beispiele ```bash @@ -389,7 +446,8 @@ rmm/ │ │ └── updates.go # Pending Updates (core + packages) │ └── ws/ │ ├── client.go # WebSocket Client, Reconnect, Ping/Pong -│ ├── handler.go # Command Handler (exec, tunnel_connect/disconnect) +│ ├── handler.go # Command Handler (exec, backup, tunnel_connect/disconnect) +│ ├── wireguard.go # WireGuard Peer Management (add/delete/list) │ └── tunnel.go # Session-basierter Tunnel Manager ├── backend/ │ ├── main.go # Entry, TLS, HTTP Server @@ -404,6 +462,7 @@ rmm/ │ │ ├── handlers.go # REST API Handler │ │ ├── tunnel_proxy.go # Tunnel + Exec + Update REST Endpoints │ │ ├── backup.go # Config-Backup Endpoints (trigger, list, download, diff) +│ │ ├── wireguard.go # WireGuard Peer API Endpoints │ │ └── middleware.go # API-Key Auth, Logging │ └── ws/ │ ├── handler.go # WebSocket Upgrade, Connection R/W Pumps diff --git a/agent/ws/handler.go b/agent/ws/handler.go index 321b1f5..23cc0ef 100644 --- a/agent/ws/handler.go +++ b/agent/ws/handler.go @@ -42,6 +42,12 @@ func (h *Handler) HandleCommand(msg Message) { 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": diff --git a/agent/ws/wireguard.go b/agent/ws/wireguard.go new file mode 100644 index 0000000..7b89c5a --- /dev/null +++ b/agent/ws/wireguard.go @@ -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 und ... korrekt +func (h *Handler) addPeerViaSed(peerUUID, name, pubKey, clientIP, serverUUID string) error { + configPath := "/conf/config.xml" + + // 1. Client-Block vor einfuegen (direkt in Go) + configData, err := os.ReadFile(configPath) + if err != nil { + return fmt.Errorf("config.xml lesen: %v", err) + } + + clientBlock := fmt.Sprintf( + "\n \n 1\n %s\n %s\n \n %s/32\n \n \n \n \n ", + peerUUID, name, pubKey, clientIP) + + configStr := string(configData) + // Vor dem letzten einfuegen + lastIdx := strings.LastIndex(configStr, "") + if lastIdx < 0 { + return fmt.Errorf(" 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: (self-closing, leer) + selfClosingIdx := strings.Index(searchFrom, "") + if selfClosingIdx >= 0 { + absIdx := idx + selfClosingIdx + newPeersTag := fmt.Sprintf("%s", peerUUID) + configStr = configStr[:absIdx] + newPeersTag + configStr[absIdx+8:] + } else { + // Fall 2: ... + peersStart := strings.Index(searchFrom, "") + peersEnd := strings.Index(searchFrom, "") + 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 bis ) + sedRemove := fmt.Sprintf( + `/usr/bin/sed -i '' '//,/<\/client>/d' %s`, + peerUUID, configPath) + if _, err := h.runCommand(sedRemove, 5); err != nil { + return fmt.Errorf("Client entfernen: %v", err) + } + + // 2. UUID aus allen -Tags entfernen + configData, err := os.ReadFile(configPath) + if err != nil { + return err + } + configStr := string(configData) + + // Alle Vorkommen der UUID in -Tags bereinigen + // Varianten: "uuid", "uuid,other", "other,uuid", "other,uuid,more" + replacements := []struct{ old, new string }{ + {fmt.Sprintf("%s,", peerUUID), ""}, + {fmt.Sprintf(",%s", peerUUID), ""}, + {fmt.Sprintf("%s", peerUUID), ""}, + {fmt.Sprintf(",%s,", peerUUID), ","}, + } + + for _, r := range replacements { + configStr = strings.ReplaceAll(configStr, r.old, r.new) + } + + return os.WriteFile(configPath, []byte(configStr), 0644) +} diff --git a/backend/api/tunnel_proxy.go b/backend/api/tunnel_proxy.go index 3c3742a..618fca9 100644 --- a/backend/api/tunnel_proxy.go +++ b/backend/api/tunnel_proxy.go @@ -26,6 +26,11 @@ func (h *Handler) setupTunnelRoutes(mux *http.ServeMux, hub *ws.Hub) { mux.HandleFunc("PUT /api/v1/agents/{id}/proxy/{tunnel_id}/", h.httpProxy(hub)) mux.HandleFunc("DELETE /api/v1/agents/{id}/proxy/{tunnel_id}/", h.httpProxy(hub)) + // WireGuard-Endpoints + mux.HandleFunc("POST /api/v1/agents/{id}/wireguard/peers", h.wgAddPeer(hub)) + mux.HandleFunc("GET /api/v1/agents/{id}/wireguard/peers", h.wgListPeers(hub)) + mux.HandleFunc("DELETE /api/v1/agents/{id}/wireguard/peers/{uuid}", h.wgDeletePeer(hub)) + // Backup-Endpoints mux.HandleFunc("POST /api/v1/agents/{id}/backup", h.triggerBackup(hub)) mux.HandleFunc("GET /api/v1/agents/{id}/backups", h.listBackups()) diff --git a/backend/api/wireguard.go b/backend/api/wireguard.go new file mode 100644 index 0000000..8b0a1a4 --- /dev/null +++ b/backend/api/wireguard.go @@ -0,0 +1,100 @@ +package api + +import ( + "encoding/json" + "log" + "net/http" + "time" + + "github.com/cynfo/rmm-backend/ws" +) + +// POST /api/v1/agents/{id}/wireguard/peers — Neuen WG-Peer anlegen +func (h *Handler) wgAddPeer(hub *ws.Hub) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + agentID := r.PathValue("id") + + var req struct { + Name string `json:"name"` + AllowedIPs string `json:"allowed_ips"` + DNS string `json:"dns"` + ServerName string `json:"server_name"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" { + writeError(w, http.StatusBadRequest, "Parameter 'name' erforderlich") + return + } + + params := map[string]interface{}{ + "name": req.Name, + } + if req.AllowedIPs != "" { + params["allowed_ips"] = req.AllowedIPs + } + if req.DNS != "" { + params["dns"] = req.DNS + } + if req.ServerName != "" { + params["server_name"] = req.ServerName + } + + h.sendCommandAndWait(hub, agentID, "wg_add_peer", params, 30, w) + } +} + +// DELETE /api/v1/agents/{id}/wireguard/peers/{uuid} — Peer loeschen +func (h *Handler) wgDeletePeer(hub *ws.Hub) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + agentID := r.PathValue("id") + peerUUID := r.PathValue("uuid") + + params := map[string]interface{}{ + "peer_uuid": peerUUID, + } + + h.sendCommandAndWait(hub, agentID, "wg_delete_peer", params, 30, w) + } +} + +// GET /api/v1/agents/{id}/wireguard/peers — Alle Peers auflisten +func (h *Handler) wgListPeers(hub *ws.Hub) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + agentID := r.PathValue("id") + + if !hub.IsAgentConnected(agentID) { + writeError(w, http.StatusServiceUnavailable, "Agent nicht verbunden") + return + } + + serverName := r.URL.Query().Get("server_name") + params := map[string]interface{}{} + if serverName != "" { + params["server_name"] = serverName + } + + cmdID := generateID() + cmd := map[string]interface{}{ + "type": "command", + "id": cmdID, + "command": "wg_list_peers", + "params": params, + } + + responseCh := hub.RegisterResponseWaiter(cmdID) + defer hub.UnregisterResponseWaiter(cmdID) + + cmdJSON, _ := json.Marshal(cmd) + if err := hub.SendToAgent(agentID, cmdJSON); err != nil { + log.Printf("WG list peers senden fehlgeschlagen: %v", err) + writeError(w, http.StatusInternalServerError, "Command senden fehlgeschlagen") + return + } + + select { + case resp := <-responseCh: + writeJSON(w, http.StatusOK, resp) + case <-time.After(30 * time.Second): + writeError(w, http.StatusGatewayTimeout, "Timeout") + } + } +}