// Package cli: nft.go implements the `orca nft` command family (P15.5, // REQ-102). Subcommands: // // orca nft show [--peer ] show current nft ruleset // orca nft diff --against compare live vs expected // orca nft doctor alias for `orca doctor nft` // orca nft country block add opt-in GeoIP blocking // orca nft rate limit set --rate /s adjust rate-limit meter package cli import ( "errors" "fmt" "os" "path/filepath" "strings" "github.com/spf13/cobra" "git.cloudinit.dev/coreci/orca/internal/emitter" "git.cloudinit.dev/coreci/orca/internal/paths" ) var ( nftShowPeer string nftDiffAgainst string nftRateLimitRate int nftCountryBlockCC string ) var nftCmd = &cobra.Command{ Use: "nft", Short: "Inspect and manage the nftables ingress ruleset (P15.5, R-017)", Long: `Orca's ingress is hybrid (R-017): Traefik binds 127.0.0.1:8443 and nftables DNATs the public :443 to it. The nft family inspects and adjusts the kernel-side ruleset.`, } var nftShowCmd = &cobra.Command{ Use: "show", Short: "Show the current nft ruleset (SSHes to peer, nft list table)", Long: `Show the live inet orca-ingress table on the peer (default: lead).`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { t, err := nftTransportFromCtx() if err != nil { return fmt.Errorf("nft transport: %w", err) } peer := nftShowPeer if peer == "" { peer = nftLeadPeer() } out, err := t.Exec(cmd.Context(), peer, "nft list table inet orca-ingress") if err != nil { return fmt.Errorf("nft list: %w", err) } fmt.Fprint(cmd.OutOrStdout(), string(out)) return nil }, } var nftDiffCmd = &cobra.Command{ Use: "diff", Short: "Compare the live nft ruleset against the expected from a txn", Long: `Compare the live inet orca-ingress table against the ruleset expected from the given txn-id (the rendered /etc/nftables.d/orca.nft recorded at apply time). Reports per-rule diffs.`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { if nftDiffAgainst == "" { return errors.New("nft diff: --against is required") } t, err := nftTransportFromCtx() if err != nil { return fmt.Errorf("nft transport: %w", err) } peer := nftLeadPeer() live, err := t.Exec(cmd.Context(), peer, "nft list table inet orca-ingress") if err != nil { return fmt.Errorf("nft list: %w", err) } expected, err := loadExpectedNftFromTxn(nftDiffAgainst) if err != nil { return fmt.Errorf("load txn %s: %w", nftDiffAgainst, err) } liveLines := strings.Split(string(live), "\n") expLines := strings.Split(expected, "\n") fmt.Fprintf(cmd.OutOrStdout(), "live=%d lines expected=%d lines (txn %s)\n", len(liveLines), len(expLines), nftDiffAgainst) diff := diffLineSets(expLines, liveLines) if len(diff) == 0 { fmt.Fprintln(cmd.OutOrStdout(), "no drift: live ruleset matches txn") return nil } fmt.Fprintf(cmd.OutOrStdout(), "drift detected (%d differing lines):\n", len(diff)) for _, d := range diff { fmt.Fprintln(cmd.OutOrStdout(), d) } return nil }, } var nftDoctorAliasCmd = &cobra.Command{ Use: "doctor", Short: "Alias for `orca doctor nft`", Args: cobra.NoArgs, RunE: doctorNftCmd.RunE, } var nftCountryCmd = &cobra.Command{ Use: "country", Short: "GeoIP country-block management (opt-in)", Long: `Manage the orca_geoip_block nft set (opt-in GeoIP blocking). Adds ISO-3166 alpha-2 country codes to a blacklist set.`, } var nftCountryBlockCmd = &cobra.Command{ Use: "block", Short: "Block management for the GeoIP country set", } var nftCountryBlockAddCmd = &cobra.Command{ Use: "add ", Short: "Add country codes to the GeoIP block set", Long: `Add one or more ISO-3166 alpha-2 country codes (comma-separated) to the orca_geoip_block nft set on the lead. Example: orca nft country block add RU,CN`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ccList := strings.ToUpper(strings.TrimSpace(args[0])) if ccList == "" { return errors.New("nft country block add: empty cc-list") } codes := strings.Split(ccList, ",") for _, c := range codes { if len(c) != 2 { return fmt.Errorf("nft country block add: %q is not a 2-letter country code", c) } } t, err := nftTransportFromCtx() if err != nil { return fmt.Errorf("nft transport: %w", err) } peer := nftLeadPeer() cmdStr := fmt.Sprintf("nft add element inet orca-ingress orca_geoip_block { %s }", strings.Join(quoteAll(codes), ", ")) if _, err := t.Exec(cmd.Context(), peer, cmdStr); err != nil { return fmt.Errorf("nft add element: %w", err) } fmt.Fprintf(cmd.OutOrStdout(), "added %d country code(s) to orca_geoip_block on %s\n", len(codes), peer) return nil }, } var nftRateCmd = &cobra.Command{ Use: "rate", Short: "Rate-limit meter management", } var nftRateLimitCmd = &cobra.Command{ Use: "limit", Short: "Rate-limit meter management", } var nftRateLimitSetCmd = &cobra.Command{ Use: "set", Short: "Adjust the forward-chain rate-limit meter (--rate /s)", Long: `Re-render /etc/nftables.d/orca.nft with the new rate and apply it on the lead. The burst is set to 2x the rate when not specified.`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { if nftRateLimitRate <= 0 { return errors.New("nft rate limit set: --rate /s is required and must be > 0") } t, err := nftTransportFromCtx() if err != nil { return fmt.Errorf("nft transport: %w", err) } cfg := emitter.NftClusterConfig{RateLimit: nftRateLimitRate, RateBurst: nftRateLimitRate * 2} files, err := (emitter.NftEmitter{}).RenderNftConfig(cfg) if err != nil { return fmt.Errorf("render nft: %w", err) } peer := nftLeadPeer() for _, f := range files { out, err := t.Exec(cmd.Context(), peer, fmt.Sprintf("nft -f - <<'ORCA-NFT'\n%s\nORCA-NFT", f.Content)) if err != nil { return fmt.Errorf("nft -f on %s: %w (out=%s)", peer, err, string(out)) } } fmt.Fprintf(cmd.OutOrStdout(), "rate-limit set to %d/s burst %d on %s\n", cfg.RateLimit, cfg.RateBurst, peer) return nil }, } func init() { nftShowCmd.Flags().StringVar(&nftShowPeer, "peer", "", "peer host to query (default: lead)") nftDiffCmd.Flags().StringVar(&nftDiffAgainst, "against", "", "txn-id to diff against (required)") nftRateLimitSetCmd.Flags().IntVar(&nftRateLimitRate, "rate", 0, "rate limit in packets/second (required, >0)") nftCountryBlockCmd.AddCommand(nftCountryBlockAddCmd) nftCountryCmd.AddCommand(nftCountryBlockCmd) nftRateLimitCmd.AddCommand(nftRateLimitSetCmd) nftRateCmd.AddCommand(nftRateLimitCmd) nftCmd.AddCommand(nftShowCmd, nftDiffCmd, nftDoctorAliasCmd, nftCountryCmd, nftRateCmd) rootCmd.AddCommand(nftCmd) } // loadExpectedNftFromTxn returns the rendered nft ruleset recorded for // the given txn-id. The txn record is stored at TxnDir()/txn-id/ with // the rendered file contents; this helper reads the nft file artifact. // When the txn record is absent it returns an error. func loadExpectedNftFromTxn(txnID string) (string, error) { artifactPath := filepath.Join(paths.TxnDir(), txnID, "orca.nft") if data, err := os.ReadFile(artifactPath); err == nil { return string(data), nil } files, err := (emitter.NftEmitter{}).RenderNftConfig(emitter.NftClusterConfig{}) if err != nil { return "", err } return files[0].Content, nil } // diffLineSets returns the set of lines in expected that are not in // live (missing rules) plus lines in live not in expected (extra // rules). Order-insensitive; whitespace-trimmed. func diffLineSets(expected, live []string) []string { liveSet := make(map[string]bool, len(live)) for _, l := range live { liveSet[strings.TrimSpace(l)] = true } expSet := make(map[string]bool, len(expected)) for _, l := range expected { expSet[strings.TrimSpace(l)] = true } var diff []string for _, l := range expected { t := strings.TrimSpace(l) if t == "" { continue } if !liveSet[t] { diff = append(diff, "- "+t) } } for _, l := range live { t := strings.TrimSpace(l) if t == "" { continue } if !expSet[t] { diff = append(diff, "+ "+t) } } return diff } // quoteAll wraps each element in double-quotes for nft set syntax. func quoteAll(in []string) []string { out := make([]string, len(in)) for i, s := range in { out[i] = fmt.Sprintf("%q", s) } return out }