package firewall import ( "context" "testing" "github.com/stretchr/testify/require" ) func TestCSFAPFStillUnsupported(t *testing.T) { ctx := context.Background() csf := &CSF{} apf := &APF{} // Logging, rate limiting, connection-state and interface matching are now // expressed by injecting iptables rules through the pre-hook (see // TestHookScriptRoundTrip), so those route to the hook rather than being // rejected. What remains genuinely unsupported: address sets and explicit // rule ordering on both backends, plus source NAT on CSF. for _, m := range []Manager{csf, apf} { require.ErrorIs(t, m.AddAddressSet(ctx, &AddressSet{Name: "x"}), ErrUnsupportedSet, "%s should reject address sets", m.Type()) require.ErrorIs(t, m.InsertRule(ctx, "", 1, &Rule{Port: 22, Proto: TCP, Action: Accept}), ErrUnsupportedOrdering, "%s should reject explicit ordering", m.Type()) // NAT ordering is unsupported on both even though they store NAT rules. dnat := &NATRule{Kind: DNAT, Proto: TCP, Port: 80, ToAddress: "10.0.0.5", ToPort: 8080} require.ErrorIs(t, m.InsertNATRule(ctx, "", 1, dnat), ErrUnsupportedOrdering, "%s should reject NAT ordering", m.Type()) } // csf.redirect expresses only destination NAT, so a source NAT is rejected // with the NAT sentinel before any file is touched. snat := &NATRule{Kind: SNAT, ToAddress: "1.2.3.4"} require.ErrorIs(t, csf.AddNATRule(ctx, "", snat), ErrUnsupportedNAT, "csf should reject source NAT") } // Every unsupported-feature path wraps a sentinel error that callers can match // with errors.Is. The umbrella ErrUnsupported matches them all. func TestSentinelErrors(t *testing.T) { ctx := context.Background() csf := &CSF{} // NAT, policy and address-set rejections carry their specific sentinel. CSF // supports destination NAT (csf.redirect) but not source NAT, so a SNAT rule // carries the NAT sentinel. nat := &NATRule{Kind: SNAT, ToAddress: "1.2.3.4"} err := csf.AddNATRule(ctx, "", nat) require.ErrorIs(t, err, ErrUnsupportedNAT) require.ErrorIs(t, err, ErrUnsupported) _, err = csf.GetDefaultPolicy(ctx, "") require.ErrorIs(t, err, ErrUnsupportedPolicy) err = csf.AddAddressSet(ctx, &AddressSet{Name: "x"}) require.ErrorIs(t, err, ErrUnsupportedSet) // The shared per-rule reject helper wraps sentinels too (used by the wf backend). err = (&Rule{Proto: TCP, Port: 22, Action: Accept, Log: true}).rejectLogAndLimit("csf") require.ErrorIs(t, err, ErrUnsupportedLog) require.ErrorIs(t, err, ErrUnsupported) } // Capabilities advertise each backend's supported features consistently with // its actual behavior. func TestCapabilities(t *testing.T) { nft := &NFT{} ipt := &IPTables{} csf := &CSF{} apf := &APF{} require.True(t, nft.Capabilities().RuleCounters, "nftables exposes rule counters") require.True(t, nft.Capabilities().DefaultPolicy, "nftables manages a default policy") require.True(t, nft.Capabilities().AddressSets, "nftables manages address sets") require.True(t, nft.Capabilities().NAT) require.True(t, ipt.Capabilities().RuleCounters, "iptables exposes rule counters") require.True(t, ipt.Capabilities().DefaultPolicy) require.True(t, ipt.Capabilities().AddressSets, "iptables manages ipsets") // CSF is a deliberately minimal backend: no counters, no policy, no sets. require.False(t, csf.Capabilities().RuleCounters) require.False(t, csf.Capabilities().DefaultPolicy) require.False(t, csf.Capabilities().AddressSets) // CSF does express (destination) NAT through csf.redirect and per-port // connection limiting through CONNLIMIT. require.True(t, csf.Capabilities().NAT) require.True(t, csf.Capabilities().ConnLimit) // APF likewise gains NAT (routing files) and connection limiting // (IG_*_CLIMIT) from its native config, but still no address sets. require.True(t, apf.Capabilities().NAT) require.True(t, apf.Capabilities().ConnLimit) require.False(t, apf.Capabilities().AddressSets) // Both gain logging, rate limiting, connection-state, interface matching and // forward-chain rules by injecting iptables rules through the pre-hook. for _, c := range []Capabilities{csf.Capabilities(), apf.Capabilities()} { require.True(t, c.Logging) require.True(t, c.RateLimit) require.True(t, c.ConnState) require.True(t, c.InterfaceMatch) require.True(t, c.Forward) } // nftables, iptables and ufw express forward-chain rules natively. require.True(t, nft.Capabilities().Forward, "nftables expresses forward rules") require.True(t, ipt.Capabilities().Forward, "iptables expresses forward rules") // CSF's ICMPv6 always goes through that same pre-hook (raw ip6tables), so it // is unconditionally supported regardless of csf.conf's IPV6 setting. require.True(t, csf.Capabilities().ICMPv6) // APF's plain ICMPv6 rules instead use its native IG_ICMPV6_TYPES/ // EG_ICMPV6_TYPES lists, which apf itself silently no-ops unless conf.apf's // USE_IPV6 is "1" — so the capability mirrors ipv6Enabled. require.False(t, apf.Capabilities().ICMPv6, "USE_IPV6 not confirmed enabled, so ICMPv6 must not be advertised") apfV6 := &APF{ipv6Enabled: true} require.True(t, apfV6.Capabilities().ICMPv6) }