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,174 @@
package v1
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/gorilla/mux"
)
// CrowdSecStatus returns whether CrowdSec is running on Wild Central
// and lists registered machines and bouncers.
func (api *API) CrowdSecStatus(w http.ResponseWriter, r *http.Request) {
status, err := api.crowdsec.GetStatus()
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get CrowdSec status: %v", err))
return
}
respondJSON(w, http.StatusOK, status)
}
// CrowdSecGetSummary returns aggregated ban counts by scenario across all active decisions.
// This includes CAPI community bans — can return tens of thousands of entries.
func (api *API) CrowdSecGetSummary(w http.ResponseWriter, r *http.Request) {
summary, err := api.crowdsec.GetBanSummary()
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get ban summary: %v", err))
return
}
respondJSON(w, http.StatusOK, summary)
}
// CrowdSecGetDecisions returns active ban/captcha decisions
func (api *API) CrowdSecGetDecisions(w http.ResponseWriter, r *http.Request) {
decisions, err := api.crowdsec.GetDecisions()
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get decisions: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]any{"decisions": decisions})
}
// CrowdSecDeleteDecision removes a ban decision by numeric ID
func (api *API) CrowdSecDeleteDecision(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
respondError(w, http.StatusBadRequest, "invalid decision ID")
return
}
if err := api.crowdsec.DeleteDecision(id); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete decision: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{"message": "Decision deleted"})
}
// CrowdSecAddDecision adds a manual ban or allow decision for an IP.
// Body: { "ip": "1.2.3.4", "type": "ban", "reason": "manual", "duration": "24h" }
func (api *API) CrowdSecAddDecision(w http.ResponseWriter, r *http.Request) {
var req struct {
IP string `json:"ip"`
Type string `json:"type"`
Reason string `json:"reason"`
Duration string `json:"duration"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.IP == "" {
respondError(w, http.StatusBadRequest, "ip is required")
return
}
if req.Type != "ban" && req.Type != "allow" {
respondError(w, http.StatusBadRequest, "type must be 'ban' or 'allow'")
return
}
if req.Reason == "" {
req.Reason = "manual"
}
if req.Duration == "" {
req.Duration = "24h"
}
if err := api.crowdsec.AddDecision(req.IP, req.Type, req.Reason, req.Duration); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to add decision: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{"message": fmt.Sprintf("Decision added: %s %s for %s", req.Type, req.IP, req.Duration)})
}
// CrowdSecDeleteDecisionByIP removes all decisions for a specific IP address
func (api *API) CrowdSecDeleteDecisionByIP(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
ip := vars["ip"]
if ip == "" {
respondError(w, http.StatusBadRequest, "ip is required")
return
}
if err := api.crowdsec.DeleteDecisionByIP(ip); err != nil {
// cscli exits non-zero when there's nothing to delete — treat as success
if strings.Contains(err.Error(), "no decision") || strings.Contains(err.Error(), "0 decision") {
respondJSON(w, http.StatusOK, map[string]string{"message": "No active decision for " + ip})
return
}
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete decision: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{"message": "Decisions removed for " + ip})
}
// CrowdSecGetAlerts returns recent detection events from registered agents.
// Query param: limit (default 50)
func (api *API) CrowdSecGetAlerts(w http.ResponseWriter, r *http.Request) {
limit := 50
if l := r.URL.Query().Get("limit"); l != "" {
if n, err := strconv.Atoi(l); err == nil && n > 0 && n <= 200 {
limit = n
}
}
alerts, err := api.crowdsec.GetAlerts(limit)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get alerts: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]any{"alerts": alerts})
}
// CrowdSecGetMachines returns all registered CrowdSec agent machines
func (api *API) CrowdSecGetMachines(w http.ResponseWriter, r *http.Request) {
machines, err := api.crowdsec.GetMachines()
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get machines: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]any{"machines": machines})
}
// CrowdSecDeleteMachine removes a registered machine
func (api *API) CrowdSecDeleteMachine(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name := vars["name"]
if err := api.crowdsec.DeleteMachine(name); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete machine: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{"message": "Machine deleted"})
}
// CrowdSecGetBouncers returns all registered bouncers
func (api *API) CrowdSecGetBouncers(w http.ResponseWriter, r *http.Request) {
bouncers, err := api.crowdsec.GetBouncers()
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get bouncers: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]any{"bouncers": bouncers})
}
// CrowdSecDeleteBouncer removes a registered bouncer
func (api *API) CrowdSecDeleteBouncer(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name := vars["name"]
if err := api.crowdsec.DeleteBouncer(name); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete bouncer: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{"message": "Bouncer deleted"})
}
// Note: CrowdSecProvision (instance-specific provisioning) has been removed.
// Instance provisioning will be handled by the Wild Cloud API, not Wild Central.