go-firewall/capabilities_linux_test.go
2026-07-08 15:54:48 -05:00

120 lines
5 KiB
Go

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)
}