// Package emitter: nft.go implements the nftables emitter for the // R-017 ingress hybrid model (P15.5, REQ-099). Orca's ingress is // "hybrid": Traefik binds the loopback 127.0.0.1:8443 (mTLS, app L7) // and nftables DNATs the public :443 to 127.0.0.1:8443 so the kernel // owns the public-facing surface (D-218: modern nft, not legacy // iptables; D-217: idempotent `nft -f` apply). // // The rendered file /etc/nftables.d/orca.nft is a self-contained nft // ruleset fragment. It starts with `#!/usr/sbin/nft -f` so it can be // applied directly (`nft -f /etc/nftables.d/orca.nft`) and is // idempotent: the table is flushed+recreated on each apply (D-217). // // The ruleset defines: // // - table inet orca-ingress // - set orca_trusted_probes (ipv4_addr interval, default 127.0.0.1) // - input chain (SYN-flood filter on :443) // - prerouting chain (DNAT :443->127.0.0.1:8443, :80->127.0.0.1:8080) // - forward chain (rate-limit meter on :443) // // The emitter is pure (no I/O): RenderNftConfig returns a []File; the // SSH-push transport writes the file to each peer. package emitter import ( "errors" "fmt" "strings" ) // NftEmitter renders the R-017 nftables ingress ruleset (REQ-099). type NftEmitter struct{} // nftConfigPath is the canonical on-peer path for the rendered // ruleset. systemd-locale-independent; nft -f loads it on boot. const nftConfigPath = "/etc/nftables.d/orca.nft" // NftClusterConfig carries the cluster-wide knobs the emitter needs. // All fields have safe defaults so a zero-value config renders a // working ruleset. type NftClusterConfig struct { // TrustedProbes is the list of source IPs exempt from the // SYN-flood filter and rate-limit (monitoring probes, the orca // lead itself). Defaults to [127.0.0.1, ::1]. TrustedProbes []string // RateLimit is the per-source rate limit (packets/second) for the // forward-chain meter on :443. Defaults to 100. RateLimit int // RateBurst is the per-source burst (packets) for the meter. // Defaults to 200. RateBurst int } // withDefaults returns a copy of c with zero values replaced by the // canonical defaults. func (c NftClusterConfig) withDefaults() NftClusterConfig { out := c if len(out.TrustedProbes) == 0 { out.TrustedProbes = []string{"127.0.0.1", "::1"} } if out.RateLimit <= 0 { out.RateLimit = 100 } if out.RateBurst <= 0 { out.RateBurst = 200 } return out } // RenderNftConfig renders the /etc/nftables.d/orca.nft file for the // given cluster config. The output is a single File whose Path is // nftConfigPath, Mode is 0644, and Content starts with the // `#!/usr/sbin/nft -f` shebang (so `nft -f` applies it and so a // drift-check `nft -c -f` validates the syntax). // // Returns an error only when the config is internally inconsistent // (e.g. a negative rate, which the defaults already prevent). func (NftEmitter) RenderNftConfig(clusterConfig NftClusterConfig) ([]File, error) { if clusterConfig.RateLimit < 0 || clusterConfig.RateBurst < 0 { return nil, errors.New("emitter/nft: rate/burst must be non-negative") } cfg := clusterConfig.withDefaults() content := renderNftRuleset(cfg) return []File{{Path: nftConfigPath, Content: content, Mode: "0644"}}, nil } // renderNftRuleset builds the nft ruleset string. The shape is // documented in the package comment; the exact lines are load-bearing // for `orca doctor nft` (which greps the live table for them) and for // `nft -c -f` (which parses the syntax). func renderNftRuleset(cfg NftClusterConfig) string { var b strings.Builder b.WriteString("#!/usr/sbin/nft -f\n\n") b.WriteString("flush table inet orca-ingress\n\n") b.WriteString("table inet orca-ingress {\n") b.WriteString("\tset orca_trusted_probes {\n") b.WriteString("\t\ttype ipv4_addr\n") b.WriteString("\t\tflags interval\n") b.WriteString("\t\telements = { ") for i, p := range cfg.TrustedProbes { if i > 0 { b.WriteString(", ") } b.WriteString(p) } b.WriteString(" }\n") b.WriteString("\t}\n\n") b.WriteString("\tchain input {\n") b.WriteString("\t\ttype filter hook input priority filter; policy accept;\n") b.WriteString("\t\tct state invalid drop\n") b.WriteString("\t\tct state established,related accept\n") b.WriteString("\t\ttcp dport 443 tcp-flags != syn,rst,ack,fin notrack drop\n") b.WriteString("\t}\n\n") b.WriteString("\tchain prerouting {\n") b.WriteString("\t\ttype nat hook prerouting priority -100; policy accept;\n") b.WriteString("\t\ttcp dport 443 dnat to 127.0.0.1:8443\n") b.WriteString("\t\ttcp dport 80 dnat to 127.0.0.1:8080\n") b.WriteString("\t}\n\n") b.WriteString("\tchain forward {\n") b.WriteString("\t\ttype filter hook forward priority filter; policy accept;\n") b.WriteString(fmt.Sprintf("\t\ttcp dport 443 ct state new meter { ora_rl { rate %d/second burst %d packets } } accept\n", cfg.RateLimit, cfg.RateBurst)) b.WriteString("\t}\n") b.WriteString("}\n") return b.String() }