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:
2026-07-08 23:31:16 +00:00
commit beb643f76f
52 changed files with 12814 additions and 0 deletions

View 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,
}
}