package firewall import ( "context" "fmt" "os" "os/exec" "time" ) // applyTable writes ruleset to a temp file and loads it with `nft -f`, // after first deleting the given table (ignoring the error - the table may // not exist yet on first apply). Only ever touches that single table, // never any other nftables state. func applyTable(tableName, ruleset string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // best-effort: drop the previous version of this table so reapplying // is idempotent. Error ignored - table may not exist yet. _ = exec.CommandContext(ctx, "nft", "delete", "table", "inet", tableName).Run() tmpFile, err := os.CreateTemp("", "wg-ui-multi-fw-*.nft") if err != nil { return "", fmt.Errorf("cannot create temp ruleset file: %w", err) } defer os.Remove(tmpFile.Name()) if _, err := tmpFile.WriteString(ruleset); err != nil { tmpFile.Close() return "", fmt.Errorf("cannot write temp ruleset file: %w", err) } if err := tmpFile.Close(); err != nil { return "", fmt.Errorf("cannot close temp ruleset file: %w", err) } cmd := exec.CommandContext(ctx, "nft", "-f", tmpFile.Name()) out, err := cmd.CombinedOutput() if err != nil { return string(out), fmt.Errorf("nft -f failed: %w", err) } return string(out), nil } // Apply loads a single server's ruleset live, scoped to TableName(serverID). func Apply(serverID, ruleset string) (string, error) { return applyTable(TableName(serverID), ruleset) } // ApplyGlobal loads the host-wide allow/block list ruleset live, scoped to // GlobalTableName. func ApplyGlobal(ruleset string) (string, error) { return applyTable(GlobalTableName, ruleset) } // DisableGlobal removes the host-wide allow/block list table entirely, // turning enforcement off without touching the stored IP list entries - // they stay in the database and can be re-applied later with ApplyGlobal. // A missing table (already disabled) is not treated as an error. func DisableGlobal() (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() out, _ := exec.CommandContext(ctx, "nft", "delete", "table", "inet", GlobalTableName).CombinedOutput() return string(out), nil } // IsGlobalEnabled reports whether the host-wide allow/block list table is // currently loaded in the live firewall. func IsGlobalEnabled() bool { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() err := exec.CommandContext(ctx, "nft", "list", "table", "inet", GlobalTableName).Run() return err == nil }