- New Capabilities: PortPair, Negation, RejectAction, FamilyWithoutAddress, DenyActionFromConfig, advertised per backend. - coversDirection isolates DirForward even when output is unowned; add splitNATDualRow so a concrete-family removal re-adds the opposite family's NAT translation. - Resolve ip6tables/ufw ICMPv6 type aliases; ParseNATKind rejects the "invalid" sentinel as input while JSON round-trips it. - Sync counts additions on mid-batch failure and uses RuleBatcher. - NewManager runs a probe loop joining each backend's reason for diagnosability; services.go drops "generated" from enabled, handles it on enable, clears start-limit-hit on restart, and matches rc.local by token. - nftables: per-source connection limits (meter set), quoted-token parsing preserving log-prefix spacing, digit-led prefix sanitizing. - apf/csf: deny-action-from-config with cached STOP settings, port lists and inexpressible shapes routed through the pre-hook, confKeyApplies guard against a missing config line. - atomic config writes fsync before rename and resolve symlinks; readConfValue is last-assignment-wins; runCommand preserves the exit code through the wrapped error. - Move coreos/go-systemd to the maintained v22 module directly.
787 lines
38 KiB
Go
787 lines
38 KiB
Go
package firewall
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestUFWParseTupleRowsModelsRouteRows verifies a route/forward tuple is now
|
|
// decoded as a forward-direction rule (occupying its physical row), and ParseRules
|
|
// returns it alongside the ordinary rules.
|
|
func TestUFWParseTupleRowsModelsRouteRows(t *testing.T) {
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "user.rules")
|
|
content := "### tuple ### route:allow tcp 23 0.0.0.0/0 any 0.0.0.0/0 in\n" +
|
|
"### tuple ### allow tcp 22 0.0.0.0/0 any 0.0.0.0/0 in\n" +
|
|
"### tuple ### allow tcp 80 0.0.0.0/0 any 0.0.0.0/0 in\n"
|
|
require.NoError(t, os.WriteFile(p, []byte(content), 0644))
|
|
fw := new(UFW)
|
|
|
|
rows, err := fw.parseTupleRows(p, IPv4)
|
|
require.NoError(t, err)
|
|
require.Len(t, rows, 3, "every tuple line occupies a physical row, including the route rule")
|
|
require.NotNil(t, rows[0], "the route (forward) tuple is now modeled as a forward rule")
|
|
require.Equal(t, DirForward, rows[0].Direction)
|
|
require.EqualValues(t, 23, rows[0].Port)
|
|
require.NotNil(t, rows[1])
|
|
require.EqualValues(t, 22, rows[1].Port)
|
|
require.NotNil(t, rows[2])
|
|
require.EqualValues(t, 80, rows[2].Port)
|
|
|
|
reps, err := fw.ParseRules(p, IPv4)
|
|
require.NoError(t, err)
|
|
require.Len(t, reps, 3, "ParseRules returns every rule, the forward one included")
|
|
}
|
|
|
|
// TestUFWNativeInsertPositionCountsRouteRows verifies a preceding route rule shifts
|
|
// the native `ufw insert` position, since ufw counts route rules in its numbered
|
|
// list. Ignoring them would insert the rule one slot too early.
|
|
func TestUFWNativeInsertPositionCountsRouteRows(t *testing.T) {
|
|
fw := new(UFW)
|
|
a := &Rule{Proto: TCP, Port: 22, Action: Accept}
|
|
b := &Rule{Proto: TCP, Port: 80, Action: Accept}
|
|
// Physical order: route(nil) is #1, A is #2, B is #3. GetRules reports A=1, B=2.
|
|
rows := []*Rule{nil, a, b}
|
|
require.Equal(t, 2, fw.nativeInsertPositionFromRows(rows, 1), "insert before A lands at A's physical slot 2")
|
|
require.Equal(t, 3, fw.nativeInsertPositionFromRows(rows, 2), "insert before B lands at B's physical slot 3, not 2")
|
|
require.Equal(t, 4, fw.nativeInsertPositionFromRows(rows, 3), "past the end appends past the last physical tuple")
|
|
|
|
// With no un-representable rows the mapping is the plain physical index.
|
|
plain := []*Rule{a, b}
|
|
require.Equal(t, 1, fw.nativeInsertPositionFromRows(plain, 1))
|
|
require.Equal(t, 2, fw.nativeInsertPositionFromRows(plain, 2))
|
|
require.Equal(t, 3, fw.nativeInsertPositionFromRows(plain, 3))
|
|
}
|
|
|
|
func TestUFWRules(t *testing.T) {
|
|
fw := new(UFW)
|
|
|
|
// Parse a rule that is expected to parse right.
|
|
rule, err := fw.UnmarshalRule(`allow udp 23 0.0.0.0/0 any 192.168.0.0/24 in`, IPv4)
|
|
require.NoError(t, err)
|
|
|
|
// Re-encode the rule which should result in expected rich rule.
|
|
args, err := fw.MarshalRule(rule)
|
|
require.NoError(t, err)
|
|
require.Equal(t, `allow in proto udp from 192.168.0.0/24 to 0.0.0.0/0 port 23`, args,
|
|
"the rule did not encode as expected")
|
|
|
|
// Try encoding a bunch of invalid rules.
|
|
invalidRules := []string{
|
|
`log udp 23 0.0.0.0/0 any 192.168.0.0/24 in`, // unsupported action
|
|
`allow udp 23 0.0.0.0/0 any`, // too few fields
|
|
}
|
|
for _, richRule := range invalidRules {
|
|
_, err := fw.UnmarshalRule(richRule, IPv4)
|
|
require.Error(t, err, "this rule was parsed when it should be invalid: %s", richRule)
|
|
}
|
|
|
|
// A `route:` (forward) tuple decodes to a forward-direction rule. The `route:`
|
|
// prefix is stripped from the action, the direction is forward, and the trailing
|
|
// interface field(s) populate the in/out interfaces (a bare direction leaves
|
|
// both empty).
|
|
routeCases := []struct {
|
|
tuple string
|
|
action Action
|
|
port uint16
|
|
in string
|
|
out string
|
|
}{
|
|
{`route:allow tcp 23 0.0.0.0/0 any 0.0.0.0/0 in`, Accept, 23, "", ""},
|
|
{`route:deny tcp 25 192.168.0.1 any 10.0.0.0/8 in_eth0`, Drop, 25, "eth0", ""},
|
|
// ufw stores a dual-interface route rule as one "!"-joined trailing token.
|
|
{`route:allow tcp 80 0.0.0.0/0 any 0.0.0.0/0 in_eth0!out_eth1`, Accept, 80, "eth0", "eth1"},
|
|
}
|
|
for _, rc := range routeCases {
|
|
got, err := fw.UnmarshalRule(rc.tuple, IPv4)
|
|
require.NoError(t, err, "a route (forward) rule must decode: %s", rc.tuple)
|
|
require.Equal(t, DirForward, got.Direction, "route rule is forward: %s", rc.tuple)
|
|
require.Equal(t, rc.action, got.Action, rc.tuple)
|
|
require.EqualValues(t, rc.port, got.Port, rc.tuple)
|
|
require.Equal(t, rc.in, got.InInterface, rc.tuple)
|
|
require.Equal(t, rc.out, got.OutInterface, rc.tuple)
|
|
}
|
|
|
|
// An application-profile tuple (9 fields, with a dapp/sapp name before the
|
|
// trailing direction field) decodes like an ordinary tuple: ufw's real tuples
|
|
// carry the app's concrete protocol/port in the six core fields — e.g. ufw's own
|
|
// recorded output for "ufw allow Apache" is
|
|
// `allow tcp 80 0.0.0.0/0 any 0.0.0.0/0 Apache - in` — so the dapp/sapp name
|
|
// tokens are simply skipped to reach the direction field; they carry no
|
|
// independent match information the model needs to represent.
|
|
appRule, err := fw.UnmarshalRule(`allow tcp 80 0.0.0.0/0 any 0.0.0.0/0 Apache - in`, IPv4)
|
|
require.NoError(t, err, "a real application-profile tuple must decode")
|
|
require.Equal(t, Accept, appRule.Action)
|
|
require.Equal(t, TCP, appRule.Proto)
|
|
require.EqualValues(t, 80, appRule.Port)
|
|
require.False(t, appRule.IsOutput())
|
|
|
|
// A multi-port app profile (Samba's "137,138") and a sapp-only form (source app,
|
|
// dapp placeholder "-") both decode the same way.
|
|
sambaRule, err := fw.UnmarshalRule(`allow udp 137,138 0.0.0.0/0 any 0.0.0.0/0 Samba - in`, IPv4)
|
|
require.NoError(t, err)
|
|
require.Equal(t, UDP, sambaRule.Proto)
|
|
require.Len(t, sambaRule.Ports, 2)
|
|
require.Equal(t, PortRange{Start: 137, End: 137}, sambaRule.Ports[0])
|
|
require.Equal(t, PortRange{Start: 138, End: 138}, sambaRule.Ports[1])
|
|
|
|
sappRule, err := fw.UnmarshalRule(`allow udp any 10.0.0.1 137,138 0.0.0.0/0 - Samba in`, IPv4)
|
|
require.NoError(t, err)
|
|
require.Equal(t, UDP, sappRule.Proto)
|
|
require.Len(t, sappRule.SourcePorts, 2)
|
|
require.Equal(t, PortRange{Start: 137, End: 137}, sappRule.SourcePorts[0])
|
|
require.Equal(t, PortRange{Start: 138, End: 138}, sappRule.SourcePorts[1])
|
|
require.Equal(t, "10.0.0.1", sappRule.Destination)
|
|
|
|
// An 8-field tuple never occurs in a real ufw file (ufw always writes both dapp
|
|
// and sapp, using "-" for whichever is absent), so it is rejected as malformed.
|
|
_, err = fw.UnmarshalRule(`allow any any 0.0.0.0/0 any 0.0.0.0/0 Apache -`, IPv4)
|
|
require.Error(t, err, "an 8-field tuple is not a real ufw shape and must be rejected")
|
|
|
|
// Source ports round-trip through the tuple's sport field.
|
|
srcTuple, err := fw.UnmarshalRule(`allow tcp any 0.0.0.0/0 1024:65535 192.168.0.0/24 in`, IPv4)
|
|
require.NoError(t, err)
|
|
require.Len(t, srcTuple.SourcePorts, 1)
|
|
require.Equal(t, PortRange{Start: 1024, End: 65535}, srcTuple.SourcePorts[0])
|
|
require.Equal(t, "192.168.0.0/24", srcTuple.Source)
|
|
|
|
srcMarshal, err := fw.MarshalRule(&Rule{Family: IPv4, Proto: TCP, Source: "192.168.0.0/24", SourcePort: 1234, Port: 22, Action: Accept})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "allow in proto tcp from 192.168.0.0/24 port 1234 to 0.0.0.0/0 port 22", srcMarshal,
|
|
"unexpected source-port marshal")
|
|
|
|
// A single source port needs a port-carrying protocol. TCPUDP (ufw's ported `any`,
|
|
// tcp+udp) marshals to the native form with no proto clause; ProtocolAny (every
|
|
// protocol) is now rejected, since ufw cannot match a port across every protocol. A
|
|
// source-port range still needs a concrete tcp/udp, so a TCPUDP range is rejected.
|
|
anySrc, err := fw.MarshalRule(&Rule{Family: IPv4, Proto: TCPUDP, SourcePort: 1234, Action: Accept})
|
|
require.NoError(t, err, "a single source port with tcpudp must be accepted")
|
|
require.Contains(t, anySrc, "port 1234")
|
|
require.NotContains(t, anySrc, "proto", "a tcpudp rule omits the proto clause")
|
|
_, err = fw.MarshalRule(&Rule{Family: IPv4, Proto: ProtocolAny, SourcePort: 1234, Action: Accept})
|
|
require.Error(t, err, "a source port across every protocol has no ufw form")
|
|
_, err = fw.MarshalRule(&Rule{Family: IPv4, Proto: TCPUDP, SourcePorts: []PortRange{{Start: 1000, End: 2000}}, Action: Accept})
|
|
require.Error(t, err, "expected a source-port range without a concrete tcp/udp to be rejected")
|
|
|
|
// Test rules we typically set.
|
|
validRules := []string{
|
|
`allow udp 4789 ::/0 any ::/0 in`,
|
|
`allow udp 4789 ::/0 any ::/0 out`,
|
|
`allow tcp 4789 0.0.0.0/0 any 67.227.233.116 in`,
|
|
`allow tcp 4791 67.227.233.116 any 0.0.0.0/0 out`,
|
|
}
|
|
for _, richRule := range validRules {
|
|
_, err := fw.UnmarshalRule(richRule, IPv4)
|
|
require.NoError(t, err, "this rich rule was not parsed when it should be valid: %s", richRule)
|
|
}
|
|
|
|
// MarshalRule must not mutate the caller's rule while inferring the
|
|
// family and normalizing the destination.
|
|
orig := &Rule{Direction: DirInput, Family: IPv4, Port: 4789, Proto: TCP, Action: Accept}
|
|
before := *orig
|
|
_, err = fw.MarshalRule(orig)
|
|
require.NoError(t, err)
|
|
require.Equal(t, before, *orig, "MarshalRule mutated the caller's rule")
|
|
|
|
// An interface-bound tuple parses the interface back out of the direction.
|
|
ifRule, err := fw.UnmarshalRule(`allow tcp 22 0.0.0.0/0 any 0.0.0.0/0 in_eth0`, IPv4)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "eth0", ifRule.InInterface, "unexpected interface parse: %+v", *ifRule)
|
|
require.False(t, ifRule.IsOutput(), "unexpected interface parse: %+v", *ifRule)
|
|
|
|
// Marshalling an interface-bound rule emits `in on <iface>`.
|
|
spec, err := fw.MarshalRule(&Rule{InInterface: "eth0", Family: IPv4, Proto: TCP, Port: 22, Action: Accept})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "allow in on eth0 proto tcp to 0.0.0.0/0 port 22", spec, "unexpected interface marshal")
|
|
|
|
// Features ufw cannot express in this model are rejected. A multiport match
|
|
// without a concrete tcp/udp protocol is also rejected: ProtocolAny with a port
|
|
// means "every protocol on this port" (no ufw form), and a bare multiport — the
|
|
// shape a TCPUDP list would take — is rejected by ufw itself, so a TCPUDP port list
|
|
// has no single-tuple form either.
|
|
unsupported := []*Rule{
|
|
{Proto: ICMP, Action: Accept},
|
|
{Proto: TCP, Port: 22, State: StateEstablished, Action: Accept},
|
|
{Proto: ProtocolAny, Ports: []PortRange{{Start: 80}, {Start: 443}}, Action: Accept},
|
|
{Proto: TCPUDP, Ports: []PortRange{{Start: 80}, {Start: 443}}, Action: Accept},
|
|
}
|
|
for _, r := range unsupported {
|
|
_, err := fw.MarshalRule(r)
|
|
require.Error(t, err, "expected error marshalling unsupported rule %+v", *r)
|
|
}
|
|
|
|
// ufw supports port lists and colon ranges on tcp/udp.
|
|
portCases := []struct {
|
|
rule *Rule
|
|
want string
|
|
}{
|
|
{&Rule{Proto: TCP, Ports: []PortRange{{Start: 80}, {Start: 443}}, Family: IPv4, Action: Accept}, "allow in proto tcp to 0.0.0.0/0 port 80,443"},
|
|
{&Rule{Proto: TCP, Ports: []PortRange{{Start: 80}, {Start: 443}}, Action: Accept}, "allow in 80,443/tcp"},
|
|
{&Rule{Proto: UDP, Ports: []PortRange{{Start: 1000, End: 2000}}, Action: Accept}, "allow in 1000:2000/udp"},
|
|
}
|
|
for _, c := range portCases {
|
|
got, err := fw.MarshalRule(c.rule)
|
|
require.NoError(t, err, "failed to marshal %+v", *c.rule)
|
|
require.Equal(t, c.want, got, "marshal %+v", *c.rule)
|
|
}
|
|
|
|
// A tuple with a multiport list and a colon range parses into port specs.
|
|
multi, err := fw.UnmarshalRule("allow tcp 80,443 0.0.0.0/0 any 0.0.0.0/0 in", IPv4)
|
|
require.NoError(t, err)
|
|
require.Len(t, multi.Ports, 2, "unexpected multiport parse: %+v", *multi)
|
|
require.Equal(t, PortRange{Start: 80, End: 80}, multi.Ports[0], "unexpected multiport parse: %+v", *multi)
|
|
require.Equal(t, PortRange{Start: 443, End: 443}, multi.Ports[1], "unexpected multiport parse: %+v", *multi)
|
|
|
|
ran, err := fw.UnmarshalRule("allow tcp 1000:2000 0.0.0.0/0 any 0.0.0.0/0 in", IPv4)
|
|
require.NoError(t, err)
|
|
require.Len(t, ran.Ports, 1, "unexpected range parse: %+v", *ran)
|
|
require.Equal(t, PortRange{Start: 1000, End: 2000}, ran.Ports[0], "unexpected range parse: %+v", *ran)
|
|
}
|
|
|
|
// ufw expresses per-rule logging natively with its `log` keyword (placed after
|
|
// the direction and any interface clause), rather than diverting a logged rule to
|
|
// before.rules where it could not be removed. Only a custom log prefix — which
|
|
// ufw cannot set — still needs the raw path.
|
|
func TestUFWNativeLogging(t *testing.T) {
|
|
fw := new(UFW)
|
|
|
|
// A plain logged rule stays on the CLI/tuple path and emits `log`.
|
|
logged := &Rule{Family: IPv4, Proto: TCP, Port: 22, Log: true, Action: Accept}
|
|
require.False(t, fw.needsIPTablesRules(logged), "a plain logged rule must not be routed to before.rules")
|
|
spec, err := fw.MarshalRule(logged)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "allow in log proto tcp to 0.0.0.0/0 port 22", spec)
|
|
|
|
// The keyword follows the interface clause.
|
|
ifLogged := &Rule{Family: IPv4, Proto: TCP, Port: 22, InInterface: "eth0", Log: true, Action: Accept}
|
|
spec, err = fw.MarshalRule(ifLogged)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "allow in on eth0 log proto tcp to 0.0.0.0/0 port 22", spec)
|
|
|
|
// A custom log prefix cannot be set in a tuple: routed raw, rejected by marshal.
|
|
prefixed := &Rule{Family: IPv4, Proto: TCP, Port: 22, Log: true, LogPrefix: "DROP22", Action: Drop}
|
|
require.True(t, fw.needsIPTablesRules(prefixed), "a custom-prefix log must go to before.rules")
|
|
_, err = fw.MarshalRule(prefixed)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// ufw carries the portless IP protocols gre/esp/ah natively (its
|
|
// supported_protocols list), so they must marshal to a `proto` tuple rather than
|
|
// being diverted to before.rules where they could not be removed.
|
|
func TestUFWPortlessProtocols(t *testing.T) {
|
|
fw := new(UFW)
|
|
for _, p := range []Protocol{GRE, ESP, AH} {
|
|
require.False(t, fw.protoNeedsRaw(p), "%s is native to ufw", p)
|
|
// With an address.
|
|
spec, err := fw.MarshalRule(&Rule{Family: IPv4, Proto: p, Source: "10.0.0.0/24", Action: Accept})
|
|
require.NoError(t, err, "failed to marshal %s rule", p)
|
|
require.Contains(t, spec, "proto "+p.String())
|
|
// Without an address: still emits the proto clause (against an any dest).
|
|
spec, err = fw.MarshalRule(&Rule{Family: IPv4, Proto: p, Action: Accept})
|
|
require.NoError(t, err, "failed to marshal bare %s rule", p)
|
|
require.Contains(t, spec, "proto "+p.String(), "bare %s rule dropped its protocol: %q", p, spec)
|
|
}
|
|
// ICMP and SCTP genuinely need the raw path.
|
|
require.True(t, fw.protoNeedsRaw(ICMP))
|
|
require.True(t, fw.protoNeedsRaw(ICMPv6))
|
|
require.True(t, fw.protoNeedsRaw(SCTP))
|
|
}
|
|
|
|
// TestUFWPortlessTCPUDP guards a bare tcp/udp match ("allow all TCP inbound"):
|
|
// with no port and no address, the protocol must still be carried on an `any`
|
|
// destination rather than dropped to a bare `allow in` that ufw rejects.
|
|
func TestUFWPortlessTCPUDP(t *testing.T) {
|
|
fw := new(UFW)
|
|
for _, p := range []Protocol{TCP, UDP} {
|
|
spec, err := fw.MarshalRule(&Rule{Family: IPv4, Proto: p, Action: Accept})
|
|
require.NoError(t, err, "failed to marshal bare %s rule", p)
|
|
require.Contains(t, spec, "proto "+p.String(), "bare %s rule dropped its protocol: %q", p, spec)
|
|
require.NotEqual(t, "allow in", spec, "bare %s rule emitted the invalid bare form", p)
|
|
}
|
|
// A family-agnostic bare tcp match must cover both families via the literal any.
|
|
spec, err := fw.MarshalRule(&Rule{Proto: TCP, Action: Accept})
|
|
require.NoError(t, err)
|
|
require.Contains(t, spec, "proto tcp", "got %q", spec)
|
|
require.Contains(t, spec, "to any", "got %q", spec)
|
|
|
|
// A true match-all rule must still be a valid command, not a bare `allow in`.
|
|
spec, err = fw.MarshalRule(&Rule{Action: Accept})
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, "allow in", spec, "match-all rule emitted the invalid bare form")
|
|
require.Contains(t, spec, "to any", "got %q", spec)
|
|
}
|
|
|
|
// A forward rule marshals into a ufw route rule: the spec carries `in on`/`out on`
|
|
// interface clauses (no bare direction, which ufw rejects on a route rule) and the
|
|
// command args place the `route` keyword before the verb.
|
|
func TestUFWForwardRouteMarshal(t *testing.T) {
|
|
fw := new(UFW)
|
|
|
|
r := &Rule{Direction: DirForward, InInterface: "eth0", OutInterface: "eth1", Proto: TCP, Port: 80, Action: Accept}
|
|
spec, err := fw.MarshalRule(r)
|
|
require.NoError(t, err)
|
|
require.Contains(t, spec, "allow in on eth0 out on eth1", "route spec must carry both interface clauses: %q", spec)
|
|
require.Contains(t, spec, "80", "the destination port must be present: %q", spec)
|
|
require.NotContains(t, spec, "route", "MarshalRule leaves the route keyword to the command builder: %q", spec)
|
|
|
|
// The command builder prepends `route` before the verb.
|
|
args := fw.ruleArgs(r, []string{"prepend"}, spec)
|
|
require.Equal(t, "route", args[0], "forward command starts with route")
|
|
require.Equal(t, "prepend", args[1], "the verb follows route")
|
|
|
|
// An ordinary rule carries neither the route keyword nor an out-interface clause.
|
|
ordinary, err := fw.MarshalRule(&Rule{Proto: TCP, Port: 22, Action: Accept})
|
|
require.NoError(t, err)
|
|
inArgs := fw.ruleArgs(&Rule{Proto: TCP, Port: 22, Action: Accept}, []string{"prepend"}, ordinary)
|
|
require.Equal(t, "prepend", inArgs[0], "a non-forward rule has no leading route keyword")
|
|
}
|
|
|
|
func TestUFWParseIPTablesRules(t *testing.T) {
|
|
fw := new(UFW)
|
|
|
|
content := `# rules.before
|
|
*filter
|
|
:ufw-before-input - [0:0]
|
|
:ufw-before-output - [0:0]
|
|
# allow all on loopback
|
|
-A ufw-before-input -i lo -j ACCEPT
|
|
# quickly process packets for which we already have a connection
|
|
-A ufw-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
|
# ok icmp codes for INPUT
|
|
-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT
|
|
-A ufw-before-input -p icmp --icmp-type destination-unreachable -j ACCEPT
|
|
# a jump to an internal chain must be skipped
|
|
-A ufw-before-input -j ufw-not-local
|
|
-A ufw-not-local -m addrtype --dst-type LOCAL -j RETURN
|
|
# the forward chain is now a modeled direction
|
|
-A ufw-before-forward -p icmp --icmp-type echo-request -j ACCEPT
|
|
COMMIT
|
|
`
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "before.rules")
|
|
require.NoError(t, os.WriteFile(path, []byte(content), 0644))
|
|
|
|
rules, err := fw.ParseIPTablesRules(path, IPv4)
|
|
require.NoError(t, err)
|
|
|
|
// Expect: loopback interface, conntrack established/related, the two input ICMP
|
|
// rules, and the forward ICMP rule. The chain jump and internal chain are
|
|
// skipped.
|
|
require.Len(t, rules, 5, "expected 5 parsed iptables rules, got %+v", rules)
|
|
|
|
var foundEcho, foundForward bool
|
|
for _, r := range rules {
|
|
if r.Proto == ICMP && r.ICMPType != nil && *r.ICMPType == 8 && r.Action == Accept {
|
|
foundEcho = true
|
|
if r.IsForward() {
|
|
foundForward = true
|
|
}
|
|
}
|
|
}
|
|
require.True(t, foundEcho, "expected an icmp echo-request accept rule, got %+v", rules)
|
|
require.True(t, foundForward, "expected the forward-chain icmp rule to be modeled, got %+v", rules)
|
|
|
|
// A missing iptables rules file contributes no rules and no error.
|
|
missing, err := fw.ParseIPTablesRules(filepath.Join(dir, "nope.rules"), IPv4)
|
|
require.NoError(t, err, "expected no error for a missing file")
|
|
require.Nil(t, missing, "expected no rules for a missing file")
|
|
}
|
|
|
|
func TestUFWIPTablesRulesWrite(t *testing.T) {
|
|
fw := new(UFW)
|
|
|
|
// needsIPTablesRules routes ICMP and state rules to the iptables rules files.
|
|
require.True(t, fw.needsIPTablesRules(&Rule{Proto: ICMP, Action: Accept}),
|
|
"expected an icmp rule to need the iptables rules files")
|
|
require.True(t, fw.needsIPTablesRules(&Rule{State: StateEstablished, Action: Accept}),
|
|
"expected a state rule to need the iptables rules files")
|
|
require.False(t, fw.needsIPTablesRules(&Rule{Proto: TCP, Port: 22, Action: Accept}),
|
|
"a plain tcp rule should use the ufw cli")
|
|
|
|
// marshalIPTablesLines rewrites the iptables chain to ufw's own chain. IPv4
|
|
// rules use the `ufw-*` chains; IPv6 rules must use the `ufw6-*` chains, since
|
|
// that is what before6.rules declares.
|
|
specs, err := fw.marshalIPTablesLines(&Rule{Proto: ICMP, ICMPType: Ptr[uint8](8), Action: Accept}, IPv4)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []string{"-A ufw-before-input -p icmp -m icmp --icmp-type 8 -j ACCEPT"}, specs,
|
|
"unexpected iptables rules spec")
|
|
|
|
v6specs, err := fw.marshalIPTablesLines(&Rule{Proto: ICMPv6, ICMPType: Ptr[uint8](128), Action: Accept}, IPv6)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []string{"-A ufw6-before-input -p icmpv6 -m icmp6 --icmpv6-type 128 -j ACCEPT"}, v6specs,
|
|
"an IPv6 rule must target the ufw6- chain so ip6tables-restore accepts before6.rules")
|
|
|
|
scaffold := "*filter\n:ufw-before-input - [0:0]\n:ufw-before-output - [0:0]\nCOMMIT\n"
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "before.rules")
|
|
require.NoError(t, os.WriteFile(path, []byte(scaffold), 0644))
|
|
|
|
icmp := &Rule{Proto: ICMP, ICMPType: Ptr[uint8](8), Action: Accept}
|
|
|
|
// Adding inserts the rule before COMMIT and it parses back equal.
|
|
changed, err := fw.editIPTablesRulesFile(path, icmp, IPv4, false)
|
|
require.NoError(t, err)
|
|
require.True(t, changed, "expected add to change the file")
|
|
|
|
rules, err := fw.ParseIPTablesRules(path, IPv4)
|
|
require.NoError(t, err)
|
|
// The parsed rule is family-tagged from the file (IPv4), so compare with
|
|
// EqualBase, which ignores family.
|
|
require.Len(t, rules, 1)
|
|
require.True(t, rules[0].EqualBase(icmp, true), "expected the icmp rule to be present, got %+v", rules[0])
|
|
|
|
// Adding again is idempotent.
|
|
changed, err = fw.editIPTablesRulesFile(path, icmp, IPv4, false)
|
|
require.NoError(t, err)
|
|
require.False(t, changed, "expected a duplicate add to be a no-op")
|
|
|
|
// A different (state) rule adds alongside it.
|
|
state := &Rule{State: StateEstablished | StateRelated, Action: Accept}
|
|
_, err = fw.editIPTablesRulesFile(path, state, IPv4, false)
|
|
require.NoError(t, err)
|
|
rules, _ = fw.ParseIPTablesRules(path, IPv4)
|
|
require.Len(t, rules, 2, "expected 2 rules after adding a state rule, got %+v", rules)
|
|
|
|
// Removing the icmp rule leaves only the state rule.
|
|
changed, err = fw.editIPTablesRulesFile(path, icmp, IPv4, true)
|
|
require.NoError(t, err)
|
|
require.True(t, changed, "expected remove to change the file")
|
|
rules, _ = fw.ParseIPTablesRules(path, IPv4)
|
|
require.Len(t, rules, 1, "expected only the state rule to remain, got %+v", rules)
|
|
require.Equal(t, StateEstablished|StateRelated, rules[0].State,
|
|
"expected only the state rule to remain, got %+v", rules)
|
|
|
|
// Removing a rule that is not present is a no-op.
|
|
changed, err = fw.editIPTablesRulesFile(path, icmp, IPv4, true)
|
|
require.NoError(t, err)
|
|
require.False(t, changed, "expected removing an absent rule to be a no-op")
|
|
}
|
|
|
|
// ufw's tuple grammar has no address negation, so a negated plain source or
|
|
// destination is routed to the before.rules raw path (which expresses it as
|
|
// `iptables ! -s/-d`) rather than rejected. A non-negated address stays on the
|
|
// tuple/CLI path and marshals cleanly.
|
|
func TestUFWNegatedAddressRoutesToRaw(t *testing.T) {
|
|
fw := new(UFW)
|
|
|
|
require.True(t, fw.needsIPTablesRules(&Rule{Family: IPv4, Proto: TCP, Source: "!10.0.0.1", Port: 22, Action: Accept}),
|
|
"a negated source must route to before.rules")
|
|
require.True(t, fw.needsIPTablesRules(&Rule{Family: IPv4, Proto: TCP, Destination: "!10.0.0.1", Port: 22, Action: Accept}),
|
|
"a negated destination must route to before.rules")
|
|
|
|
// A non-negated address stays on the tuple path and marshals cleanly.
|
|
nonNeg := &Rule{Family: IPv4, Proto: TCP, Source: "10.0.0.1", Port: 22, Action: Accept}
|
|
require.False(t, fw.needsIPTablesRules(nonNeg), "a plain address stays on the CLI path")
|
|
_, err := fw.MarshalRule(nonNeg)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// A native ufw `limit` rule reads back from user.rules as an accept carrying
|
|
// ufw's built-in rate. It must be routed through the CLI/user.rules path (not the
|
|
// before.rules raw path), or it can never be removed and Restore duplicates it.
|
|
func TestUFWNativeLimitRouting(t *testing.T) {
|
|
fw := new(UFW)
|
|
r, err := fw.UnmarshalRule("limit tcp 22 0.0.0.0/0 any 0.0.0.0/0 in", IPv4)
|
|
require.NoError(t, err)
|
|
require.True(t, fw.isNativeLimit(r), "a limit tuple must be recognized as a native limit")
|
|
require.False(t, fw.needsIPTablesRules(r), "a native limit lives in user.rules, not before.rules")
|
|
|
|
spec, err := fw.MarshalRule(r)
|
|
require.NoError(t, err)
|
|
require.True(t, strings.HasPrefix(spec, "limit "), "a native limit must marshal to a `limit` tuple; got %q", spec)
|
|
|
|
// A general (non-native) rate limit still needs the raw before.rules path, and
|
|
// MarshalRule must refuse it rather than silently drop the rate.
|
|
general := &Rule{Proto: TCP, Port: 22, Action: Accept, RateLimit: &RateLimit{Rate: 100, Unit: PerSecond}}
|
|
require.True(t, fw.needsIPTablesRules(general), "a non-native rate limit needs before.rules")
|
|
_, err = fw.MarshalRule(general)
|
|
require.Error(t, err, "a non-native rate limit must not be silently dropped by MarshalRule")
|
|
}
|
|
|
|
// MarshalRule must reject a per-rule modifier it cannot put in a tuple (logging,
|
|
// a connection limit) rather than silently dropping it — those are routed to the
|
|
// before.rules files by needsIPTablesRules.
|
|
func TestUFWMarshalRejectsUnexpressibleModifiers(t *testing.T) {
|
|
fw := new(UFW)
|
|
_, err := fw.MarshalRule(&Rule{Family: IPv4, Proto: TCP, Port: 22, Action: Drop, Log: true, LogPrefix: "x"})
|
|
require.Error(t, err, "logging must not be silently dropped")
|
|
_, err = fw.MarshalRule(&Rule{Family: IPv4, Proto: TCP, Port: 22, Action: Accept, ConnLimit: &ConnLimit{Count: 10}})
|
|
require.Error(t, err, "a connection limit must not be silently dropped")
|
|
}
|
|
|
|
// A family-agnostic source-port rule must emit the literal "any" address so ufw
|
|
// installs both the IPv4 and IPv6 rule, not a v4-only zero network.
|
|
func TestUFWFamilyAnySourcePortUsesAny(t *testing.T) {
|
|
fw := new(UFW)
|
|
spec, err := fw.MarshalRule(&Rule{Proto: TCP, SourcePort: 1024, Action: Accept})
|
|
require.NoError(t, err)
|
|
require.Contains(t, spec, "from any port 1024")
|
|
require.NotContains(t, spec, "0.0.0.0/0", "a FamilyAny rule must not be pinned to an IPv4 zero network")
|
|
}
|
|
|
|
// GetRules must read tuples whose action carries a `_log` suffix or is `limit`
|
|
// rather than dropping them.
|
|
func TestUFWLimitAndLogTupleParse(t *testing.T) {
|
|
fw := new(UFW)
|
|
|
|
limit, err := fw.UnmarshalRule("limit tcp 22 0.0.0.0/0 any 0.0.0.0/0 in", IPv4)
|
|
require.NoError(t, err)
|
|
require.Equal(t, Accept, limit.Action)
|
|
require.NotNil(t, limit.RateLimit, "a limit tuple must carry a rate limit")
|
|
|
|
logged, err := fw.UnmarshalRule("allow_log tcp 80 0.0.0.0/0 any 0.0.0.0/0 in", IPv4)
|
|
require.NoError(t, err)
|
|
require.Equal(t, Accept, logged.Action)
|
|
require.True(t, logged.Log, "an allow_log tuple must be read as a logged rule")
|
|
}
|
|
|
|
// ufw's native `limit` action can carry logging (`ufw limit log ...` -> a
|
|
// `limit_log` tuple in user.rules). Such a rule must stay on the tuple path, not
|
|
// be routed to before.rules, or Restore removes it from the wrong file and
|
|
// duplicates it.
|
|
func TestUFWLimitLogStaysNative(t *testing.T) {
|
|
f := new(UFW)
|
|
r, err := f.UnmarshalRule("limit_log tcp 22 0.0.0.0/0 any 0.0.0.0/0 in", IPv4)
|
|
require.NoError(t, err)
|
|
require.True(t, r.Log, "limit_log carries logging")
|
|
require.NotNil(t, r.RateLimit)
|
|
require.Equal(t, RateLimit{Rate: 12, Unit: PerMinute, Burst: 6}, *r.RateLimit)
|
|
|
|
require.True(t, f.isNativeLimit(r), "a logged native limit is still native")
|
|
require.False(t, f.needsIPTablesRules(r),
|
|
"a logged native limit must stay on the tuple path, not go to before.rules")
|
|
|
|
// It marshals to a tuple (limit + log) rather than erroring.
|
|
out, err := f.MarshalRule(r)
|
|
require.NoError(t, err)
|
|
require.Contains(t, out, "limit")
|
|
require.Contains(t, out, "log")
|
|
|
|
// A native limit carrying a *custom* prefix cannot be a tuple, so it must still
|
|
// route to before.rules.
|
|
prefixed := &Rule{Action: Accept, Proto: TCP, Port: 22, Log: true, LogPrefix: "MINE",
|
|
RateLimit: &RateLimit{Rate: 12, Unit: PerMinute, Burst: 6}}
|
|
require.False(t, f.isNativeLimit(prefixed), "a custom-prefix limit is not tuple-expressible")
|
|
require.True(t, f.needsIPTablesRules(prefixed), "a custom-prefix limit goes to before.rules")
|
|
}
|
|
|
|
// UFW editIPTablesRulesFile remove must preserve an adjacent foreign LOG line that
|
|
// is not the removal target's own LOG half, rather than discarding it unflushed.
|
|
func TestUFWRemovePreservesForeignLogLine(t *testing.T) {
|
|
fw := new(UFW)
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "before.rules")
|
|
content := "*filter\n:ufw-before-input - [0:0]\n" +
|
|
"-A ufw-before-input -p tcp --dport 4000 -j LOG --log-prefix \"[FOREIGN] \"\n" +
|
|
"-A ufw-before-input -p icmp --icmp-type 8 -j DROP\n" +
|
|
"COMMIT\n"
|
|
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
|
|
|
|
icmp := &Rule{Proto: ICMP, ICMPType: Ptr[uint8](8), Action: Drop}
|
|
_, err := fw.editIPTablesRulesFile(path, icmp, IPv4, true)
|
|
require.NoError(t, err)
|
|
|
|
out, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
require.Contains(t, string(out), "[FOREIGN]", "the unrelated foreign LOG line must survive removal")
|
|
require.NotContains(t, string(out), "icmp", "the icmp rule must be removed")
|
|
}
|
|
|
|
// ParseRules reads a ufw user.rules file, decoding the `### tuple ###` lines and
|
|
// the hex-encoded trailing comment (with the configured prefix split off).
|
|
func TestUFWParseRulesFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "user.rules")
|
|
// A plain allow, and a deny carrying a prefixed comment "myapp dns".
|
|
comment := hex.EncodeToString([]byte("myapp dns"))
|
|
content := "*filter\n" +
|
|
"### tuple ### allow tcp 22 0.0.0.0/0 any 0.0.0.0/0 in\n" +
|
|
"### tuple ### deny udp 53 0.0.0.0/0 any 0.0.0.0/0 in comment=" + comment + "\n" +
|
|
"### RULES ###\n" +
|
|
"-A ufw-user-input -p tcp --dport 22 -j ACCEPT\n" +
|
|
"COMMIT\n"
|
|
require.NoError(t, os.WriteFile(path, []byte(content), 0644))
|
|
|
|
f := &UFW{rulePrefix: "myapp"}
|
|
rules, err := f.ParseRules(path, IPv4)
|
|
require.NoError(t, err)
|
|
require.Len(t, rules, 2, "only the two ### tuple ### lines are parsed")
|
|
|
|
require.Equal(t, Accept, rules[0].Action)
|
|
require.Equal(t, TCP, rules[0].Proto)
|
|
require.EqualValues(t, 22, rules[0].Port)
|
|
require.Empty(t, rules[0].Comment)
|
|
require.False(t, rules[0].HasPrefix, "an un-commented rule is not flagged as ours")
|
|
|
|
require.Equal(t, Drop, rules[1].Action)
|
|
require.Equal(t, UDP, rules[1].Proto)
|
|
require.EqualValues(t, 53, rules[1].Port)
|
|
require.Equal(t, "dns", rules[1].Comment, "the configured prefix is split off the comment")
|
|
require.True(t, rules[1].HasPrefix, "the prefixed comment marks the rule as ours")
|
|
}
|
|
|
|
// ufw's `any` protocol means two different things depending on whether the rule
|
|
// carries a port: on a ported rule it is tcp+udp together (TCPUDP, ufw's native
|
|
// both-transports form), and on a portless rule it is every IP protocol
|
|
// (ProtocolAny). TestUFWTCPUDPMarshal exercises the write side of that split.
|
|
func TestUFWTCPUDPMarshal(t *testing.T) {
|
|
fw := new(UFW)
|
|
|
|
// A single-port TCPUDP rule emits ufw's bare-port short form (proto rides along as
|
|
// ufw's `any`, so no `/proto` suffix and no proto clause).
|
|
spec, err := fw.MarshalRule(&Rule{Port: 80, Proto: TCPUDP, Action: Accept})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "allow in 80", spec, "a single-port TCPUDP rule uses ufw's bare-port short form")
|
|
|
|
// The same port with ProtocolAny now errors: a port across every protocol has no
|
|
// ufw form.
|
|
_, err = fw.MarshalRule(&Rule{Port: 80, Proto: ProtocolAny, Action: Accept})
|
|
require.Error(t, err, "a ProtocolAny rule carrying a port has no ufw form")
|
|
|
|
// A portless ProtocolAny rule is ufw's real `any` and still marshals fine.
|
|
spec, err = fw.MarshalRule(&Rule{Proto: ProtocolAny, Source: "1.2.3.4", Action: Accept})
|
|
require.NoError(t, err, "a portless ProtocolAny (ufw's real `any`) must marshal")
|
|
require.Contains(t, spec, "from 1.2.3.4")
|
|
require.NotContains(t, spec, "proto", "ufw's `any` protocol carries no proto clause")
|
|
|
|
// A family-specific single-port TCPUDP rule uses the full grammar with no proto
|
|
// clause (ufw's `any` on a ported rule), covering both tcp and udp.
|
|
spec, err = fw.MarshalRule(&Rule{Family: IPv4, Port: 80, Proto: TCPUDP, Action: Accept})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "allow in to 0.0.0.0/0 port 80", spec)
|
|
|
|
// A portless TCPUDP has no single-tuple form (it would widen to every protocol) and
|
|
// is rejected by MarshalRule; AddRule fans it out instead.
|
|
_, err = fw.MarshalRule(&Rule{Family: IPv4, Proto: TCPUDP, Action: Accept})
|
|
require.Error(t, err, "a portless TCPUDP match cannot be a single ufw tuple")
|
|
require.True(t, fw.tcpudpNeedsExpand(&Rule{Family: IPv4, Proto: TCPUDP, Action: Accept}),
|
|
"a portless TCPUDP rule must fan out into concrete tcp+udp tuples")
|
|
require.True(t, fw.tcpudpNeedsExpand(&Rule{Proto: TCPUDP, Ports: []PortRange{{Start: 80}, {Start: 443}}, Action: Accept}),
|
|
"a multiport TCPUDP rule must fan out into concrete tcp+udp tuples")
|
|
require.False(t, fw.tcpudpNeedsExpand(&Rule{Port: 80, Proto: TCPUDP, Action: Accept}),
|
|
"a single-port TCPUDP rule uses ufw's native both-transports tuple")
|
|
}
|
|
|
|
// TestUFWTCPUDPTupleRoundTrip verifies the read side of ufw's `any`-protocol split:
|
|
// an `any` tuple with a port decodes to TCPUDP, one without a port to ProtocolAny.
|
|
func TestUFWTCPUDPTupleRoundTrip(t *testing.T) {
|
|
fw := new(UFW)
|
|
|
|
// A ported `any` tuple is tcp+udp together.
|
|
ported, err := fw.UnmarshalRule("allow any 80 0.0.0.0/0 any 0.0.0.0/0 in", IPv4)
|
|
require.NoError(t, err)
|
|
require.Equal(t, TCPUDP, ported.Proto, "a ported `any` tuple decodes to TCPUDP")
|
|
require.EqualValues(t, 80, ported.Port)
|
|
|
|
// A source-port-only `any` tuple is likewise tcp+udp.
|
|
sported, err := fw.UnmarshalRule("allow any any 0.0.0.0/0 1024 0.0.0.0/0 in", IPv4)
|
|
require.NoError(t, err)
|
|
require.Equal(t, TCPUDP, sported.Proto, "an `any` tuple with a source port decodes to TCPUDP")
|
|
require.True(t, sported.HasSourcePorts(), "the source port must be parsed")
|
|
require.EqualValues(t, 1024, sported.SourcePort)
|
|
|
|
// A portless `any` tuple is every IP protocol.
|
|
portless, err := fw.UnmarshalRule("allow any any 0.0.0.0/0 any 1.2.3.4 in", IPv4)
|
|
require.NoError(t, err)
|
|
require.Equal(t, ProtocolAny, portless.Proto, "a portless `any` tuple decodes to ProtocolAny")
|
|
require.Equal(t, "1.2.3.4", portless.Source)
|
|
|
|
// A native single-port TCPUDP rule round-trips through marshal + unmarshal.
|
|
spec, err := fw.MarshalRule(&Rule{Family: IPv4, Port: 443, Proto: TCPUDP, Action: Accept})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "allow in to 0.0.0.0/0 port 443", spec)
|
|
}
|
|
|
|
// A separately-added tcp/80 tuple and udp/80 tuple stay two rules on read — ufw stores
|
|
// them as two tuples — but they cover the TCPUDP rule between them, so a caller (and
|
|
// Sync) sees the port as already open on both transports.
|
|
func TestUFWSeparateTransportTuplesCoverTCPUDP(t *testing.T) {
|
|
stored := []*Rule{
|
|
{Family: IPv4, Proto: TCP, Port: 80, Action: Accept},
|
|
{Family: IPv4, Proto: UDP, Port: 80, Action: Accept},
|
|
}
|
|
both := &Rule{Family: IPv4, Proto: TCPUDP, Port: 80, Action: Accept}
|
|
require.True(t, both.CoveredBy(stored))
|
|
require.False(t, both.CoveredBy(stored[:1]), "the tcp tuple alone leaves udp uncovered")
|
|
|
|
// A tcp/80 and udp/443 pair covers neither port on both transports.
|
|
require.False(t, both.CoveredBy([]*Rule{
|
|
{Family: IPv4, Proto: TCP, Port: 80, Action: Accept},
|
|
{Family: IPv4, Proto: UDP, Port: 443, Action: Accept},
|
|
}), "differing ports must not be read as cross-transport coverage")
|
|
}
|
|
|
|
// Every modeled tuple is its own rule, so a logical position maps straight to ufw's
|
|
// native slot. An unmodeled tuple (a route rule, kept as a nil row) still occupies a
|
|
// physical slot and shifts the ones after it.
|
|
func TestUFWNativeInsertPositionSkipsUnmodeledRows(t *testing.T) {
|
|
fw := new(UFW)
|
|
tcp80 := &Rule{Family: IPv4, Proto: TCP, Port: 80, Action: Accept}
|
|
udp80 := &Rule{Family: IPv4, Proto: UDP, Port: 80, Action: Accept}
|
|
tcp22 := &Rule{Family: IPv4, Proto: TCP, Port: 22, Action: Accept}
|
|
|
|
// With every tuple modeled, position N is native slot N.
|
|
rows := []*Rule{tcp80, udp80, tcp22}
|
|
require.Equal(t, 1, fw.nativeInsertPositionFromRows(rows, 1))
|
|
require.Equal(t, 2, fw.nativeInsertPositionFromRows(rows, 2))
|
|
require.Equal(t, 3, fw.nativeInsertPositionFromRows(rows, 3))
|
|
require.Equal(t, 4, fw.nativeInsertPositionFromRows(rows, 4),
|
|
"past the end appends past the last physical tuple")
|
|
|
|
// A route rule ufw counts but this backend cannot model sits at physical slot 2,
|
|
// so the second reported rule lives at slot 3.
|
|
withRoute := []*Rule{tcp80, nil, udp80, tcp22}
|
|
require.Equal(t, 1, fw.nativeInsertPositionFromRows(withRoute, 1))
|
|
require.Equal(t, 3, fw.nativeInsertPositionFromRows(withRoute, 2),
|
|
"the unmodeled route tuple shifts the native slot")
|
|
require.Equal(t, 4, fw.nativeInsertPositionFromRows(withRoute, 3))
|
|
require.Equal(t, 5, fw.nativeInsertPositionFromRows(withRoute, 4))
|
|
}
|
|
|
|
// TestUFWEditIPTablesRulesAnchorsFilterCommit pins that the raw-path add splices
|
|
// its rule into the *filter section's COMMIT, not the file's first COMMIT: the
|
|
// canonical ufw NAT setup puts a *nat block above *filter, and a filter rule in
|
|
// that block references an undeclared chain, failing the reload.
|
|
func TestUFWEditIPTablesRulesAnchorsFilterCommit(t *testing.T) {
|
|
fw := new(UFW)
|
|
natFirst := "*nat\n:POSTROUTING ACCEPT [0:0]\n-A POSTROUTING -s 10.0.0.0/8 -o eth0 -j MASQUERADE\nCOMMIT\n" +
|
|
"*filter\n:ufw-before-input - [0:0]\nCOMMIT\n"
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "before.rules")
|
|
require.NoError(t, os.WriteFile(path, []byte(natFirst), 0644))
|
|
|
|
icmp := &Rule{Proto: ICMP, ICMPType: Ptr[uint8](8), Action: Accept}
|
|
changed, err := fw.editIPTablesRulesFile(path, icmp, IPv4, false)
|
|
require.NoError(t, err)
|
|
require.True(t, changed)
|
|
|
|
data, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
body := string(data)
|
|
natEnd := strings.Index(body, "*filter")
|
|
require.NotContains(t, body[:natEnd], "--icmp-type",
|
|
"the rule must not land inside the *nat block")
|
|
require.Contains(t, body[natEnd:], "--icmp-type",
|
|
"the rule must land inside the *filter block")
|
|
// The rule sits before *filter's COMMIT.
|
|
filterPart := body[natEnd:]
|
|
require.Less(t, strings.Index(filterPart, "--icmp-type"), strings.Index(filterPart, "COMMIT"))
|
|
}
|
|
|
|
// TestUFWOldFormatTupleStaysOpaque pins that ufw's old 8-field storage format
|
|
// (six core fields plus dapp/sapp, no direction field) is rejected by the tuple
|
|
// parser, so parseTupleRows holds it as a nil row rather than mis-reading it.
|
|
func TestUFWOldFormatTupleStaysOpaque(t *testing.T) {
|
|
fw := new(UFW)
|
|
_, err := fw.UnmarshalRule("allow tcp 80 0.0.0.0/0 any 0.0.0.0/0 Apache -", IPv4)
|
|
require.Error(t, err, "the old 8-field format must stay opaque")
|
|
|
|
// A non-route tuple must not carry a "!"-joined interface pair.
|
|
_, err = fw.UnmarshalRule("allow tcp 80 0.0.0.0/0 any 0.0.0.0/0 in_eth0!out_eth1", IPv4)
|
|
require.Error(t, err, "a dual-interface token is only valid on a route rule")
|
|
}
|