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>
175 lines
6.2 KiB
Go
175 lines
6.2 KiB
Go
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.
|