iptables: persist ipsets across reboot to match rule persistence. - Detect the ipset save-file + restore unit (RHEL ipset.service / /etc/sysconfig/ipset; Debian netfilter-persistent / /etc/iptables/ipsets), non-fatally. - After each set mutation, `ipset save` into the layout's save-file and auto-enable a present-but-disabled restore unit; warn when no mechanism exists (sets stay live-only). - Use ListUnitFiles (not ListUnitFilesByPatterns, which needs systemd >= 230; CentOS 7 ships 219). APF/CSF: gain address sets by persisting ipset commands in the pre-hook. - The hook carries an `ipset create/flush/add` block ordered ahead of the `-m set --match-set` rule lines, so the firewall recreates the set on every (re)start before any rule references it. - Route set-referencing rules (Source/Destination names an ipset) through the hook rather than a literal trust-file line (ruleNeedsHook/bareHostShape). - Implement the six address-set methods, advertise AddressSets, and wire sets into Backup/Restore via captureBackupState/restoreBackupSets. Validated live: reboot simulation for iptables; generated-hook source for APF/CSF. Unit tests cover the hook ipset round-trip, ordering, in-use guard and set-ref routing; the capability-gated integration subtest now covers APF/CSF.
118 lines
5 KiB
Go
118 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), and address sets by persisting ipset commands in
|
|
// that same hook, so those route to the hook rather than being rejected. What
|
|
// remains genuinely unsupported: explicit rule ordering on both backends, plus
|
|
// source NAT on CSF.
|
|
for _, m := range []Manager{csf, apf} {
|
|
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 and policy 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)
|
|
|
|
// 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.
|
|
require.False(t, csf.Capabilities().RuleCounters)
|
|
require.False(t, csf.Capabilities().DefaultPolicy)
|
|
// 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)
|
|
// CSF gains address sets by persisting ipset commands in its pre-hook.
|
|
require.True(t, csf.Capabilities().AddressSets)
|
|
|
|
// APF likewise gains NAT (routing files) and connection limiting
|
|
// (IG_*_CLIMIT) from its native config, and address sets from ipset commands
|
|
// persisted in its pre-hook.
|
|
require.True(t, apf.Capabilities().NAT)
|
|
require.True(t, apf.Capabilities().ConnLimit)
|
|
require.True(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)
|
|
}
|