package v1 import ( "encoding/json" "fmt" "net/http" "github.com/gorilla/mux" "github.com/wild-cloud/wild-central/internal/config" "github.com/wild-cloud/wild-central/internal/wireguard" ) // VPNManager is the interface for VPN operations used by these handlers. type VPNManager interface { GetStatus() (*wireguard.Status, error) GetConfig() (*wireguard.Config, error) SaveConfig(cfg *wireguard.Config) error GetPublicKey() string GenerateKeypair() error Apply() error ListPeers() ([]*wireguard.Peer, error) AddPeer(name string) (*wireguard.Peer, error) GetPeer(id string) (*wireguard.Peer, error) DeletePeer(id string) error GeneratePeerConfigText(peerID string) (string, error) } // vpnHandlers groups VPN HTTP handlers with their dependencies. type vpnHandlers struct { vpn VPNManager statePath string syncNftables func(globalCfg *config.State) // callback to resync firewall after config changes } func (h *vpnHandlers) Status(w http.ResponseWriter, r *http.Request) { status, err := h.vpn.GetStatus() if err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get VPN status: %v", err)) return } respondJSON(w, http.StatusOK, status) } func (h *vpnHandlers) GetConfig(w http.ResponseWriter, r *http.Request) { cfg, err := h.vpn.GetConfig() if err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get VPN config: %v", err)) return } respondJSON(w, http.StatusOK, map[string]any{ "enabled": cfg.Enabled, "listenPort": cfg.ListenPort, "address": cfg.Address, "endpoint": cfg.Endpoint, "dns": cfg.DNS, "lanCIDR": cfg.LanCIDR, "publicKey": h.vpn.GetPublicKey(), }) } func (h *vpnHandlers) UpdateConfig(w http.ResponseWriter, r *http.Request) { var req wireguard.Config if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) return } if req.ListenPort == 0 { req.ListenPort = 51820 } if err := h.vpn.SaveConfig(&req); err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to save VPN config: %v", err)) return } // Resync nftables so the VPN listen port is automatically allowed/removed if globalCfg, err := config.LoadState(h.statePath); err == nil { go h.syncNftables(globalCfg) } respondJSON(w, http.StatusOK, map[string]string{"message": "VPN configuration saved"}) } func (h *vpnHandlers) GenerateKeypair(w http.ResponseWriter, r *http.Request) { if err := h.vpn.GenerateKeypair(); err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to generate keypair: %v", err)) return } respondJSON(w, http.StatusOK, map[string]string{ "message": "Server keypair generated", "publicKey": h.vpn.GetPublicKey(), }) } func (h *vpnHandlers) Apply(w http.ResponseWriter, r *http.Request) { if err := h.vpn.Apply(); err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to apply VPN config: %v", err)) return } respondJSON(w, http.StatusOK, map[string]string{"message": "WireGuard interface applied successfully"}) } func (h *vpnHandlers) ListPeers(w http.ResponseWriter, r *http.Request) { peers, err := h.vpn.ListPeers() if err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list peers: %v", err)) return } respondJSON(w, http.StatusOK, map[string]any{"peers": redactPeers(peers)}) } func (h *vpnHandlers) AddPeer(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" { respondError(w, http.StatusBadRequest, "name is required") return } peer, err := h.vpn.AddPeer(req.Name) if err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to add peer: %v", err)) return } respondJSON(w, http.StatusCreated, redactPeer(peer)) } func (h *vpnHandlers) GetPeerConfig(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] text, err := h.vpn.GeneratePeerConfigText(id) if err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to generate peer config: %v", err)) return } peer, err := h.vpn.GetPeer(id) if err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get peer: %v", err)) return } respondJSON(w, http.StatusOK, map[string]string{ "name": peer.Name, "config": text, }) } func (h *vpnHandlers) DeletePeer(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] if err := h.vpn.DeletePeer(id); err != nil { respondError(w, http.StatusNotFound, fmt.Sprintf("Failed to delete peer: %v", err)) return } respondJSON(w, http.StatusOK, map[string]string{"message": "Peer deleted"}) } // redactPeers strips private keys from a peer list before sending to clients. func redactPeers(peers []*wireguard.Peer) []map[string]any { out := make([]map[string]any, len(peers)) for i, p := range peers { out[i] = redactPeer(p) } return out } func redactPeer(p *wireguard.Peer) map[string]any { return map[string]any{ "id": p.ID, "name": p.Name, "publicKey": p.PublicKey, "allowedIPs": p.AllowedIPs, "createdAt": p.CreatedAt, } }