feat: Extract Wild Central as standalone Go service
Extract the Central networking functionality from wild-cloud/api into a standalone service. Wild Central manages DNS (dnsmasq), gateway (HAProxy), firewall (nftables), VPN (WireGuard), TLS (certbot), security (CrowdSec), and DDNS — all the network appliance concerns. All Cloud-specific code (instances, clusters, nodes, apps, backups, operations, kubectl/talosctl tooling) has been removed. The API struct and route registration contain only Central endpoints. Tests updated to match the new API signature. Builds and all tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
157
internal/api/v1/handlers_wireguard.go
Normal file
157
internal/api/v1/handlers_wireguard.go
Normal file
@@ -0,0 +1,157 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// VpnStatus returns the current WireGuard interface status.
|
||||
func (api *API) VpnStatus(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := api.vpn.GetStatus()
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get VPN status: %v", err))
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusOK, status)
|
||||
}
|
||||
|
||||
// VpnGetConfig returns the server interface configuration and public key.
|
||||
func (api *API) VpnGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
cfg, err := api.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": api.vpn.GetPublicKey(),
|
||||
})
|
||||
}
|
||||
|
||||
// VpnUpdateConfig updates the server interface configuration.
|
||||
func (api *API) VpnUpdateConfig(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 := api.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.LoadGlobalConfig(api.getGlobalConfigPath()); err == nil {
|
||||
go api.syncNftablesOnly(globalCfg)
|
||||
}
|
||||
respondJSON(w, http.StatusOK, map[string]string{"message": "VPN configuration saved"})
|
||||
}
|
||||
|
||||
// VpnGenerateKeypair generates a new server keypair.
|
||||
func (api *API) VpnGenerateKeypair(w http.ResponseWriter, r *http.Request) {
|
||||
if err := api.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": api.vpn.GetPublicKey(),
|
||||
})
|
||||
}
|
||||
|
||||
// VpnApply writes the wg0.conf and brings the WireGuard interface up.
|
||||
func (api *API) VpnApply(w http.ResponseWriter, r *http.Request) {
|
||||
if err := api.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"})
|
||||
}
|
||||
|
||||
// VpnListPeers returns all configured peers (without private keys).
|
||||
func (api *API) VpnListPeers(w http.ResponseWriter, r *http.Request) {
|
||||
peers, err := api.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)})
|
||||
}
|
||||
|
||||
// VpnAddPeer adds a new peer, auto-generating a keypair and assigning an IP.
|
||||
func (api *API) VpnAddPeer(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 := api.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))
|
||||
}
|
||||
|
||||
// VpnGetPeerConfig returns the wg-quick config text for a peer (used by clients and QR generation).
|
||||
func (api *API) VpnGetPeerConfig(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
text, err := api.vpn.GeneratePeerConfigText(id)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to generate peer config: %v", err))
|
||||
return
|
||||
}
|
||||
peer, err := api.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,
|
||||
})
|
||||
}
|
||||
|
||||
// VpnDeletePeer removes a peer by ID.
|
||||
func (api *API) VpnDeletePeer(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
if err := api.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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user