Read a ufw rule's direction: an outgoing rule answers no opening, and incoming is the default ufw merges on (hq ADR 0103)

This commit is contained in:
2026-09-22 19:51:35 +02:00
parent dc861fb4a8
commit d3f2595968
4 changed files with 92 additions and 10 deletions
+16 -1
View File
@@ -479,6 +479,9 @@ func words(rule string) []string {
type ufwRule struct { type ufwRule struct {
route bool route bool
action, in, out string action, in, out string
// dir is which way the rule matches: "" (ufw's default, incoming and forwarded), "in" or
// "out". A rule on the outgoing path admits nothing that arrives.
dir string
log string log string
from, fromPort, to string from, fromPort, to string
port, proto, app string port, proto, app string
@@ -529,6 +532,7 @@ func parseRule(rule string) (ufwRule, bool) {
iface = w[i+1] iface = w[i+1]
i += 2 i += 2
} }
r.dir = dir
if dir == "in" { if dir == "in" {
r.in = iface r.in = iface
} else { } else {
@@ -586,6 +590,12 @@ func parseRule(rule string) (ufwRule, bool) {
if r.proto == "any" { if r.proto == "any" {
r.proto = "" r.proto = ""
} }
// Incoming is ufw's default direction, and it prints `allow in 9005/tcp` back as
// `allow 9005/tcp` and merges the two — captured in testdata/ufw-direction.txt. An outgoing
// rule is its own rule and stays one.
if r.dir == "in" && r.in == "" {
r.dir = ""
}
return r, true return r, true
} }
@@ -616,10 +626,15 @@ func (r ufwRule) sameAs(o ufwRule) bool {
// protocol — and on any interface, or the private network's for an opening from it. // protocol — and on any interface, or the private network's for an opening from it.
func (r ufwRule) admits(o *declaration.Opening) bool { func (r ufwRule) admits(o *declaration.Opening) bool {
want, ok := parseRule(strings.Join(Rule(o), " ")) want, ok := parseRule(strings.Join(Rule(o), " "))
if !ok || r.route != want.route || r.action != "allow" || r.out != "" || r.app != "" || if !ok || r.route != want.route || r.action != "allow" || r.app != "" ||
r.from != "any" || r.fromPort != "" || r.to != "any" { r.from != "any" || r.fromPort != "" || r.to != "any" {
return false return false
} }
// A rule on the outgoing path lets this machine reach others; it admits nothing that arrives,
// so it never answers an opening.
if r.dir == "out" || r.out != "" {
return false
}
if r.proto != "" && r.proto != want.proto { if r.proto != "" && r.proto != want.proto {
return false return false
} }
+41 -1
View File
@@ -604,11 +604,17 @@ func TestEveryCapturedRuleFormIsRead(t *testing.T) {
"allow in on mesh0 to any port 5432 proto tcp": "tcp 5432 in=mesh0 from=any", "allow in on mesh0 to any port 5432 proto tcp": "tcp 5432 in=mesh0 from=any",
"route allow 8080/tcp": "tcp 8080 in= from=any", "route allow 8080/tcp": "tcp 8080 in= from=any",
"allow 9900/tcp": "tcp 9900 in= from=any", "allow 9900/tcp": "tcp 9900 in= from=any",
"allow out 5671/tcp": "tcp 5671 in= from=any",
"deny out 5672/tcp": "tcp 5672 in= from=any",
"allow out on eth0 to any port 5673 proto tcp": "tcp 5673 in= from=any",
"allow log 9001/tcp": "tcp 9001 in= from=any",
"route allow log 8084/tcp": "tcp 8084 in= from=any",
"allow in on mesh0 log-all to any port 9002 proto tcp": "tcp 9002 in=mesh0 from=any",
} }
rules, err := added(context.Background(), func(context.Context, string, ...string) (string, error) { rules, err := added(context.Background(), func(context.Context, string, ...string) (string, error) {
return captured(t, "ufw-forms.txt"), nil return captured(t, "ufw-forms.txt"), nil
}) })
if err != nil || len(rules) != 15 { if err != nil || len(rules) != 21 {
t.Fatalf("read %d rules: %v", len(rules), err) t.Fatalf("read %d rules: %v", len(rules), err)
} }
for _, rule := range rules { for _, rule := range rules {
@@ -716,3 +722,37 @@ func TestALogTypeIsReadInEitherPlace(t *testing.T) {
} }
} }
} }
func TestAnOutgoingRuleNeverAnswersAnOpening(t *testing.T) {
// `ufw allow out 5671/tcp` lets this machine reach others; nothing arrives through it, and
// ufw keeps it as a rule of its own — captured in testdata/ufw-direction.txt.
raw := captured(t, "ufw-direction.txt")
if !strings.Contains(raw, "ufw allow out 9007/tcp\nufw allow 9007/tcp") {
t.Fatalf("the capture no longer shows an outgoing rule standing beside an incoming one:\n%s", raw)
}
for _, operators := range []string{"allow out 5671/tcp", "allow out on eth0 to any port 5671 proto tcp",
"deny out 5671/tcp"} {
f := &fakeUFW{installed: true, active: true, rules: []string{operators}}
done, err := Converge(context.Background(), f.run, opening("adoption.bus", 5671, "everywhere", "incoming", 0))
if err != nil {
t.Errorf("%q: an outgoing rule was taken for a conflict: %v", operators, err)
continue
}
if done.Action != "created" || done.SatisfiedBy != "" {
t.Errorf("%q: an outgoing rule answered an incoming opening: %+v", operators, done)
}
}
}
func TestIncomingIsUfwsDefaultDirection(t *testing.T) {
// Captured: `deny in 9006/tcp` and `allow 9006/tcp` are one rule to ufw, so the mesh must read
// them as one too, or it would take an operator's refusal over.
raw := captured(t, "ufw-direction.txt")
if !strings.Contains(raw, "ufw allow 9005/tcp") || strings.Contains(raw, "ufw deny 9006/tcp") {
t.Fatalf("the capture no longer shows `in` as the default direction:\n%s", raw)
}
f := &fakeUFW{installed: true, active: true, rules: []string{"deny in to any port 5671 proto tcp"}}
if _, err := Converge(context.Background(), f.run, opening("adoption.bus", 5671, "everywhere", "incoming", 0)); err == nil {
t.Error("an incoming refusal ufw would merge was not refused")
}
}
+21
View File
@@ -0,0 +1,21 @@
$ ufw allow in 9005/tcp
Rules updated
Rules updated (v6)
$ ufw deny in 9006/tcp
Rules updated
Rules updated (v6)
$ ufw allow 9006/tcp
Rules updated
Rules updated (v6)
$ ufw allow out 9007/tcp
Rules updated
Rules updated (v6)
$ ufw allow 9007/tcp
Rules updated
Rules updated (v6)
$ ufw show added
Added user rules (see 'ufw status' for running firewall):
ufw allow 9005/tcp
ufw allow 9006/tcp
ufw allow out 9007/tcp
ufw allow 9007/tcp
+6
View File
@@ -14,3 +14,9 @@ ufw allow 9900/tcp
ufw allow 80,443/tcp ufw allow 80,443/tcp
ufw allow in on mesh0 to any port 5432 proto tcp ufw allow in on mesh0 to any port 5432 proto tcp
ufw route allow 8080/tcp ufw route allow 8080/tcp
ufw allow out 5671/tcp
ufw deny out 5672/tcp
ufw allow out on eth0 to any port 5673 proto tcp
ufw allow log 9001/tcp
ufw route allow log 8084/tcp
ufw allow in on mesh0 log-all to any port 9002 proto tcp