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:
121
internal/api/v1/handlers_nftables.go
Normal file
121
internal/api/v1/handlers_nftables.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/wild-cloud/wild-central/internal/config"
|
||||
"github.com/wild-cloud/wild-central/internal/haproxy"
|
||||
)
|
||||
|
||||
// NftablesStatus returns the current wild-cloud nftables table contents
|
||||
func (api *API) NftablesStatus(w http.ResponseWriter, r *http.Request) {
|
||||
rules, err := api.nftables.GetStatus()
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get nftables status: %v", err))
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusOK, map[string]any{
|
||||
"rulesFile": api.nftables.GetRulesPath(),
|
||||
"rules": rules,
|
||||
})
|
||||
}
|
||||
|
||||
// NftablesApply reapplies the current nftables rules file to the kernel
|
||||
func (api *API) NftablesApply(w http.ResponseWriter, r *http.Request) {
|
||||
if err := api.nftables.ApplyRules(); err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to apply nftables rules: %v", err))
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "nftables rules applied successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// NetworkInterface describes a host network interface for WAN interface selection
|
||||
type NetworkInterface struct {
|
||||
Name string `json:"name"`
|
||||
Addresses []string `json:"addresses"`
|
||||
}
|
||||
|
||||
// vpnAutoUDPPorts returns the WireGuard listen port if VPN is enabled,
|
||||
// so it can be automatically included in nftables UDP rules.
|
||||
func (api *API) vpnAutoUDPPorts() []int {
|
||||
cfg, err := api.vpn.GetConfig()
|
||||
if err != nil || !cfg.Enabled {
|
||||
return nil
|
||||
}
|
||||
return []int{cfg.ListenPort}
|
||||
}
|
||||
|
||||
// syncNftablesOnly regenerates and applies nftables rules from the current global config
|
||||
// without touching HAProxy. Called when firewall settings change independently.
|
||||
// Runs errors are logged only — the caller does not wait for completion.
|
||||
func (api *API) syncNftablesOnly(globalCfg *config.GlobalConfig) {
|
||||
nftCfg := globalCfg.Cloud.Nftables
|
||||
|
||||
// Explicitly disabled: flush the wild-cloud table
|
||||
if nftCfg.Enabled != nil && !*nftCfg.Enabled {
|
||||
if err := api.nftables.WriteDisabledRules(); err != nil {
|
||||
slog.Error("failed to write disabled nftables rules", "component", "nftables-sync", "error", err)
|
||||
return
|
||||
}
|
||||
if err := api.nftables.ApplyRules(); err != nil {
|
||||
slog.Error("failed to apply disabled nftables rules", "component", "nftables-sync", "error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
routes, err := api.buildInstanceRoutes()
|
||||
if err != nil {
|
||||
slog.Error("failed to build instance routes for nftables sync", "component", "nftables-sync", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
var customRoutes []haproxy.CustomRoute
|
||||
for _, cr := range globalCfg.Cloud.HAProxy.CustomRoutes {
|
||||
customRoutes = append(customRoutes, haproxy.CustomRoute{Name: cr.Name, Port: cr.Port, Backend: cr.Backend})
|
||||
}
|
||||
|
||||
extraTCP, extraUDP := config.SplitExtraPorts(nftCfg.ExtraPorts)
|
||||
extraUDP = append(extraUDP, api.vpnAutoUDPPorts()...)
|
||||
ports := api.haproxy.GetListenPorts(routes, customRoutes)
|
||||
nftContent := api.nftables.Generate(ports, extraTCP, extraUDP, nftCfg.WANInterface)
|
||||
if err := api.nftables.WriteRules(nftContent); err != nil {
|
||||
slog.Error("failed to write nftables rules", "component", "nftables-sync", "error", err)
|
||||
return
|
||||
}
|
||||
if err := api.nftables.ApplyRules(); err != nil {
|
||||
slog.Error("failed to apply nftables rules", "component", "nftables-sync", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// NftablesGetInterfaces returns non-loopback network interfaces for WAN interface selection
|
||||
func (api *API) NftablesGetInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list interfaces: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]NetworkInterface, 0)
|
||||
for _, iface := range ifaces {
|
||||
// Skip loopback and interfaces that are down
|
||||
if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 {
|
||||
continue
|
||||
}
|
||||
addrs, _ := iface.Addrs()
|
||||
addrStrs := make([]string, 0, len(addrs))
|
||||
for _, a := range addrs {
|
||||
addrStrs = append(addrStrs, a.String())
|
||||
}
|
||||
result = append(result, NetworkInterface{
|
||||
Name: iface.Name,
|
||||
Addresses: addrStrs,
|
||||
})
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]any{"interfaces": result})
|
||||
}
|
||||
Reference in New Issue
Block a user