WireGuard Peer Management: Add/List/Delete Peers via API
- Agent: wg_add_peer, wg_delete_peer, wg_list_peers Commands - Keypair-Generierung auf der Firewall (wg genkey/pubkey) - Automatische IP-Vergabe aus dem Tunnel-Subnetz - WAN-IP Auto-Detection via Default-Route Interface - Endpoint als Parameter uebergebbar (fuer DynDNS etc.) - Config-Aenderungen direkt in config.xml (Go-basiert, kein PHP) - configctl wireguard configure fuer Live-Reload - Client-Config in Response (ready for import) - Server-Name default: ALWAYSON_VPN - Backend: 3 neue API-Endpoints (POST/GET/DELETE) - README mit vollstaendiger Dokumentation
This commit is contained in:
parent
b895d08f15
commit
c10c311424
61
README.md
61
README.md
@ -60,6 +60,9 @@ Client ──► Backend:ProxyPort ──► WebSocket (Binary) ──► Agent
|
|||||||
| `update` | `POST /agents/{id}/update` | Normales Update (Core + Packages, opt. Reboot) |
|
| `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) |
|
| `major_update` | `POST /agents/{id}/major-update` | Major-Upgrade (2-Phasen: Base/Kernel → Reboot → Packages) |
|
||||||
| `reboot` | `POST /agents/{id}/reboot` | Reboot mit konfigurierbarer Verzoegerung |
|
| `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_connect` | `POST /agents/{id}/tunnel` | TCP-Tunnel zu lokalem Dienst oeffnen |
|
||||||
| `tunnel_disconnect` | `DELETE /agents/{id}/tunnel/{tid}` | Tunnel schliessen |
|
| `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}'
|
-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
|
### Backup-Beispiele
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -389,7 +446,8 @@ rmm/
|
|||||||
│ │ └── updates.go # Pending Updates (core + packages)
|
│ │ └── updates.go # Pending Updates (core + packages)
|
||||||
│ └── ws/
|
│ └── ws/
|
||||||
│ ├── client.go # WebSocket Client, Reconnect, Ping/Pong
|
│ ├── 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
|
│ └── tunnel.go # Session-basierter Tunnel Manager
|
||||||
├── backend/
|
├── backend/
|
||||||
│ ├── main.go # Entry, TLS, HTTP Server
|
│ ├── main.go # Entry, TLS, HTTP Server
|
||||||
@ -404,6 +462,7 @@ rmm/
|
|||||||
│ │ ├── handlers.go # REST API Handler
|
│ │ ├── handlers.go # REST API Handler
|
||||||
│ │ ├── tunnel_proxy.go # Tunnel + Exec + Update REST Endpoints
|
│ │ ├── tunnel_proxy.go # Tunnel + Exec + Update REST Endpoints
|
||||||
│ │ ├── backup.go # Config-Backup Endpoints (trigger, list, download, diff)
|
│ │ ├── backup.go # Config-Backup Endpoints (trigger, list, download, diff)
|
||||||
|
│ │ ├── wireguard.go # WireGuard Peer API Endpoints
|
||||||
│ │ └── middleware.go # API-Key Auth, Logging
|
│ │ └── middleware.go # API-Key Auth, Logging
|
||||||
│ └── ws/
|
│ └── ws/
|
||||||
│ ├── handler.go # WebSocket Upgrade, Connection R/W Pumps
|
│ ├── handler.go # WebSocket Upgrade, Connection R/W Pumps
|
||||||
|
|||||||
@ -42,6 +42,12 @@ func (h *Handler) HandleCommand(msg Message) {
|
|||||||
response = h.handleReboot(msg)
|
response = h.handleReboot(msg)
|
||||||
case "backup":
|
case "backup":
|
||||||
response = h.handleBackup(msg)
|
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":
|
case "tunnel_connect":
|
||||||
response = h.handleTunnelConnect(msg)
|
response = h.handleTunnelConnect(msg)
|
||||||
case "tunnel_disconnect":
|
case "tunnel_disconnect":
|
||||||
|
|||||||
604
agent/ws/wireguard.go
Normal file
604
agent/ws/wireguard.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
@ -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("PUT /api/v1/agents/{id}/proxy/{tunnel_id}/", h.httpProxy(hub))
|
||||||
mux.HandleFunc("DELETE /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
|
// Backup-Endpoints
|
||||||
mux.HandleFunc("POST /api/v1/agents/{id}/backup", h.triggerBackup(hub))
|
mux.HandleFunc("POST /api/v1/agents/{id}/backup", h.triggerBackup(hub))
|
||||||
mux.HandleFunc("GET /api/v1/agents/{id}/backups", h.listBackups())
|
mux.HandleFunc("GET /api/v1/agents/{id}/backups", h.listBackups())
|
||||||
|
|||||||
100
backend/api/wireguard.go
Normal file
100
backend/api/wireguard.go
Normal file
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user