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-central 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.State) { nftCfg := globalCfg.Cloud.Nftables // Explicitly disabled: flush the wild-central 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 } 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(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}) }