120 lines
5 KiB
Go
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)
|
|
}
|