diff --git a/internal/bootstrap/inuse.go b/internal/bootstrap/inuse.go index 3909de6..58c357b 100644 --- a/internal/bootstrap/inuse.go +++ b/internal/bootstrap/inuse.go @@ -10,13 +10,11 @@ import ( "github.com/novox/mesh-host/internal/store" ) -// quietUDP are processes whose UDP sockets every fresh machine has — name resolution, address -// configuration, time — and which serve nobody. ss names a process by its first fifteen characters, -// so both spellings are here. -// -// **Still to be measured** (novox/hq ADR 0100): this list is what a fresh machine is expected to -// hold, and it must be checked against a freshly installed lab machine before it is trusted. -var quietUDP = map[string]bool{ +// quiet are the processes every fresh machine runs that serve nobody: name resolution (whose +// link-local resolver listens on TCP as well as UDP, on every address), address configuration and +// time. ss names a process by its first fifteen characters, so both spellings are here. Measured +// on a freshly installed lab machine (testdata/fresh-machine-listeners.txt): these and nothing else. +var quiet = map[string]bool{ "systemd-resolved": true, "systemd-resolve": true, "systemd-networkd": true, "systemd-network": true, "systemd-timesyncd": true, "systemd-timesyn": true, @@ -24,8 +22,8 @@ var quietUDP = map[string]bool{ } // InUse says what makes this machine a machine in use (novox/hq ADR 0100): every running container -// no host made, and every socket listening on an address other than loopback that is not ssh's — a -// UDP one only when it is held by something other than what every fresh machine runs. ours names +// no host made, and every socket listening on an address other than loopback that is neither ssh's +// nor held by what every fresh machine runs. ours names // what the mesh itself runs, which a re-run of genesis finds and does not count. func InUse(ctx context.Context, run Runner, ours func(name string) bool) ([]string, []reachable.Reach, error) { var containers []string @@ -61,9 +59,9 @@ func counts(r reachable.Reach) bool { } switch r.Protocol { case "tcp": - return r.By != "sshd" && !(r.By == "" && r.Port == 22) + return r.By != "sshd" && !(r.By == "" && r.Port == 22) && !quiet[r.By] case "udp": - return !quietUDP[r.By] + return !quiet[r.By] } return false } diff --git a/internal/bootstrap/inuse_test.go b/internal/bootstrap/inuse_test.go index b1a1e49..98ea349 100644 --- a/internal/bootstrap/inuse_test.go +++ b/internal/bootstrap/inuse_test.go @@ -2,6 +2,7 @@ package bootstrap import ( "context" + "os" "path/filepath" "strings" "testing" @@ -13,8 +14,8 @@ import ( // and listener it counted. // Lines as `ss -Hltunp` prints them. The ssh, samba, loopback and proxy lines are captured from a -// real machine; the resolver, DHCP and time lines are written in the same shape for the processes a -// fresh machine runs, and still need measuring against one. +// real machine; the resolver, DHCP and time lines are written in the same shape. What a fresh machine +// actually runs is measured in testdata/fresh-machine-listeners.txt. const inUseSockets = `tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1188536,fd=6)) tcp LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=1188536,fd=7)) tcp LISTEN 0 32 127.0.0.1:53 0.0.0.0:* users:(("dnsmasq",pid=1189392,fd=7)) @@ -97,3 +98,25 @@ func TestARerunOfGenesisIsNotAMachineInUse(t *testing.T) { t.Errorf("what an earlier genesis raised was counted as a machine in use: %v", err) } } + +func TestAFreshlyInstalledMachineAsMeasuredIsNotInUse(t *testing.T) { + // Captured with `ss -Hltunp` on a freshly installed lab machine: its resolver listens on TCP on + // every address, which the record's words alone would count. + raw, err := os.ReadFile("testdata/fresh-machine-listeners.txt") + if err != nil { + t.Fatal(err) + } + run := func(ctx context.Context, name string, args ...string) (string, error) { + if name == "ss" { + return string(raw), nil + } + return "", nil + } + containers, listeners, err := InUse(context.Background(), run, func(string) bool { return false }) + if err != nil { + t.Fatal(err) + } + if len(containers) != 0 || len(listeners) != 0 { + t.Errorf("a fresh machine read as in use: containers %v, listeners %v", containers, listeners) + } +} diff --git a/internal/bootstrap/testdata/fresh-machine-listeners.txt b/internal/bootstrap/testdata/fresh-machine-listeners.txt new file mode 100644 index 0000000..defcf66 --- /dev/null +++ b/internal/bootstrap/testdata/fresh-machine-listeners.txt @@ -0,0 +1,12 @@ +udp UNCONN 0 0 0.0.0.0:5353 0.0.0.0:* users:(("systemd-resolve",pid=262,fd=17)) +udp UNCONN 0 0 0.0.0.0:5355 0.0.0.0:* users:(("systemd-resolve",pid=262,fd=13)) +udp UNCONN 0 0 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=262,fd=24)) +udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=262,fd=22)) +udp UNCONN 0 0 [::]:5353 [::]:* users:(("systemd-resolve",pid=262,fd=18)) +udp UNCONN 0 0 [::]:5355 [::]:* users:(("systemd-resolve",pid=262,fd=15)) +udp UNCONN 0 0 [fe80::1266:6aff:fe24:628d]%enp5s0:546 [::]:* users:(("systemd-network",pid=272,fd=36)) +tcp LISTEN 0 4096 127.0.0.1:39473 0.0.0.0:* users:(("containerd",pid=394,fd=14)) +tcp LISTEN 0 4096 0.0.0.0:5355 0.0.0.0:* users:(("systemd-resolve",pid=262,fd=14)) +tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=262,fd=23)) +tcp LISTEN 0 4096 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=262,fd=25)) +tcp LISTEN 0 4096 [::]:5355 [::]:* users:(("systemd-resolve",pid=262,fd=16)) diff --git a/internal/firewall/firewall.go b/internal/firewall/firewall.go index b896c5d..f730afe 100644 --- a/internal/firewall/firewall.go +++ b/internal/firewall/firewall.go @@ -38,6 +38,17 @@ const ( Unsupported Kind = "unsupported" ) +// deletion is the arguments that delete a rule as `ufw show added` printed it. A route rule is +// deleted with `route delete …`: ufw refuses `delete route …` as invalid syntax. And ufw answers +// success when asked to delete a rule it does not hold, so every deletion is read back. +func deletion(rule string) []string { + w := words(rule) + if len(w) > 0 && w[0] == "route" { + return append([]string{"route", "delete"}, w[1:]...) + } + return append([]string{"delete"}, w...) +} + // MeshInterface is the private network's interface, the way an opening from the mesh is known. // It must be the controller's overlay interface name. const MeshInterface = "mesh0" @@ -335,7 +346,7 @@ func Converge(ctx context.Context, run Runner, o *declaration.Opening) (string, return "unchanged", nil } for _, rule := range stale { - if _, err := run(ctx, "ufw", append([]string{"delete"}, words(rule)...)...); err != nil { + if _, err := run(ctx, "ufw", deletion(rule)...); err != nil { return "", fmt.Errorf("deleting the mesh's stale ufw rule %q: %w", rule, err) } } @@ -381,7 +392,7 @@ func Remove(ctx context.Context, run Runner, id string) (int, error) { if !markedFor(comment(rule), id) { continue } - if _, err := run(ctx, "ufw", append([]string{"delete"}, words(rule)...)...); err != nil { + if _, err := run(ctx, "ufw", deletion(rule)...); err != nil { return removed, fmt.Errorf("deleting the mesh's ufw rule %q: %w", rule, err) } removed++ diff --git a/internal/firewall/firewall_test.go b/internal/firewall/firewall_test.go index 67d8aa2..c810a03 100644 --- a/internal/firewall/firewall_test.go +++ b/internal/firewall/firewall_test.go @@ -180,9 +180,17 @@ func (f *fakeUFW) run(_ context.Context, name string, args ...string) (string, e case args[0] == "disable": f.active = false return "Firewall stopped and disabled on system startup\n", nil - case args[0] == "delete": + case args[0] == "delete" && len(args) > 1 && args[1] == "route": + // As the real ufw answers it (captured in testdata/ufw-delete.txt): a route rule is + // deleted with `route delete`, never `delete route`. + return "", errors.New("ERROR: Invalid syntax") + case args[0] == "delete", args[0] == "route" && len(args) > 1 && args[1] == "delete": + rest := args[1:] + if args[0] == "route" { + rest = append([]string{"route"}, args[2:]...) + } for i, r := range f.rules { - if strings.Join(words(r), "\x00") == strings.Join(args[1:], "\x00") { + if strings.Join(words(r), "\x00") == strings.Join(rest, "\x00") { f.rules = append(f.rules[:i], f.rules[i+1:]...) return "Rule deleted\n", nil } @@ -333,3 +341,86 @@ func TestDetectingTheFoundFirewall(t *testing.T) { } } } + +// The fixtures below were captured from a real ufw 0.36.2 on a lab machine, not written by hand: +// ufw prints a rule back in its own shorter form, so the mark in the comment is the only thing the +// host relies on. + +func TestTheMarksAreReadFromWhatUfwReallyPrints(t *testing.T) { + raw, err := os.ReadFile("testdata/ufw-show-added.txt") + if err != nil { + t.Fatal(err) + } + run := func(ctx context.Context, name string, args ...string) (string, error) { return string(raw), nil } + rules, err := added(context.Background(), run) + if err != nil { + t.Fatal(err) + } + if len(rules) != 7 { + t.Fatalf("read %d rules, want 7: %q", len(rules), rules) + } + marked := 0 + for _, r := range rules { + if strings.HasPrefix(comment(r), "mesh-host ") { + marked++ + } + } + if marked != 5 { + t.Errorf("read %d marked rules, want 5", marked) + } + if !markedFor(comment(rules[5]), "adoption.opening-tcp-8443-forwarded") { + t.Errorf("the forwarded rule from the mesh lost its mark: %q", rules[5]) + } +} + +func TestEveryRealRuleIsDeletedInTheFormUfwAccepts(t *testing.T) { + raw, err := os.ReadFile("testdata/ufw-show-added.txt") + if err != nil { + t.Fatal(err) + } + run := func(ctx context.Context, name string, args ...string) (string, error) { return string(raw), nil } + rules, _ := added(context.Background(), run) + // Each of these was run on the lab machine and answered "Rule deleted" (testdata/ufw-delete.txt). + want := map[string]string{ + "allow 5671/tcp comment 'mesh-host adoption.opening-tcp-5671-incoming 1a2b3c4d'": "delete allow 5671/tcp comment|mesh-host adoption.opening-tcp-5671-incoming 1a2b3c4d", + "allow in on mesh0 to any port 5432 proto tcp comment 'mesh-host adoption.opening-tcp-5432-incoming deadbeef'": "delete allow in on mesh0 to any port 5432 proto tcp comment|mesh-host adoption.opening-tcp-5432-incoming deadbeef", + "route allow 80/tcp comment 'mesh-host adoption.opening-tcp-8081-forwarded 0badf00d'": "route delete allow 80/tcp comment|mesh-host adoption.opening-tcp-8081-forwarded 0badf00d", + "route allow in on mesh0 to any port 443 proto tcp comment 'mesh-host adoption.opening-tcp-8443-forwarded cafe0001'": "route delete allow in on mesh0 to any port 443 proto tcp comment|mesh-host adoption.opening-tcp-8443-forwarded cafe0001", + } + seen := 0 + for _, r := range rules { + w, ok := want[r] + if !ok { + continue + } + seen++ + d := deletion(r) + got := strings.Join(d[:len(d)-1], " ") + "|" + d[len(d)-1] + if got != w { + t.Errorf("deleting %q\n got %s\n want %s", r, got, w) + } + } + if seen != len(want) { + t.Errorf("matched %d of %d captured rules", seen, len(want)) + } +} + +func TestARealUfwRulesetIsUfw(t *testing.T) { + raw, err := os.ReadFile("testdata/ufw-active.nft") + if err != nil { + t.Fatal(err) + } + status, err := os.ReadFile("testdata/ufw-status-active.txt") + if err != nil { + t.Fatal(err) + } + if !statusActive(string(status)) { + t.Fatal("the captured status does not read as active") + } + if refusing := Refusing(string(raw), true); len(refusing) > 0 { + t.Errorf("a machine with ufw active and nothing else read as refusing in %v", refusing) + } + if refusing := Refusing(string(raw), false); len(refusing) == 0 { + t.Error("ufw's drop chains, with ufw not known to be active, read as refusing nothing") + } +} diff --git a/internal/firewall/testdata/ufw-active.nft b/internal/firewall/testdata/ufw-active.nft new file mode 100644 index 0000000..93b24a4 --- /dev/null +++ b/internal/firewall/testdata/ufw-active.nft @@ -0,0 +1,478 @@ +table ip nat { + chain DOCKER { + } + + chain PREROUTING { + type nat hook prerouting priority dstnat; policy accept; + xt match "addrtype" counter packets 2 bytes 1160 jump DOCKER + } + + chain OUTPUT { + type nat hook output priority dstnat; policy accept; + ip daddr != 127.0.0.0/8 xt match "addrtype" counter packets 0 bytes 0 jump DOCKER + } + + chain POSTROUTING { + type nat hook postrouting priority srcnat; policy accept; + ip saddr 172.17.0.0/16 oifname != "docker0" counter packets 0 bytes 0 xt target "MASQUERADE" + } +} +table ip filter { + chain DOCKER { + iifname != "docker0" oifname "docker0" counter packets 0 bytes 0 drop + } + + chain DOCKER-FORWARD { + counter packets 0 bytes 0 jump DOCKER-CT + counter packets 0 bytes 0 jump DOCKER-INTERNAL + counter packets 0 bytes 0 jump DOCKER-BRIDGE + iifname "docker0" counter packets 0 bytes 0 accept + } + + chain DOCKER-BRIDGE { + oifname "docker0" counter packets 0 bytes 0 jump DOCKER + } + + chain DOCKER-CT { + oifname "docker0" xt match "conntrack" counter packets 0 bytes 0 accept + } + + chain DOCKER-INTERNAL { + } + + chain FORWARD { + type filter hook forward priority filter; policy drop; + counter packets 0 bytes 0 jump DOCKER-USER + counter packets 0 bytes 0 jump DOCKER-FORWARD + counter packets 0 bytes 0 jump ufw-before-logging-forward + counter packets 0 bytes 0 jump ufw-before-forward + counter packets 0 bytes 0 jump ufw-after-forward + counter packets 0 bytes 0 jump ufw-after-logging-forward + counter packets 0 bytes 0 jump ufw-reject-forward + counter packets 0 bytes 0 jump ufw-track-forward + } + + chain DOCKER-USER { + } + + chain ufw-before-logging-input { + } + + chain ufw-before-logging-output { + } + + chain ufw-before-logging-forward { + } + + chain ufw-before-input { + iifname "lo" counter packets 0 bytes 0 accept + xt match "conntrack" counter packets 0 bytes 0 accept + xt match "conntrack" counter packets 0 bytes 0 jump ufw-logging-deny + xt match "conntrack" counter packets 0 bytes 0 drop + ip protocol icmp xt match "icmp" counter packets 0 bytes 0 accept + ip protocol icmp xt match "icmp" counter packets 0 bytes 0 accept + ip protocol icmp xt match "icmp" counter packets 0 bytes 0 accept + ip protocol icmp xt match "icmp" counter packets 0 bytes 0 accept + udp sport 67 udp dport 68 counter packets 0 bytes 0 accept + counter packets 0 bytes 0 jump ufw-not-local + ip daddr 224.0.0.251 udp dport 5353 counter packets 0 bytes 0 accept + ip daddr 239.255.255.250 udp dport 1900 counter packets 0 bytes 0 accept + counter packets 0 bytes 0 jump ufw-user-input + } + + chain ufw-before-output { + oifname "lo" counter packets 0 bytes 0 accept + xt match "conntrack" counter packets 0 bytes 0 accept + counter packets 0 bytes 0 jump ufw-user-output + } + + chain ufw-before-forward { + xt match "conntrack" counter packets 0 bytes 0 accept + ip protocol icmp xt match "icmp" counter packets 0 bytes 0 accept + ip protocol icmp xt match "icmp" counter packets 0 bytes 0 accept + ip protocol icmp xt match "icmp" counter packets 0 bytes 0 accept + ip protocol icmp xt match "icmp" counter packets 0 bytes 0 accept + counter packets 0 bytes 0 jump ufw-user-forward + } + + chain ufw-after-input { + udp dport 137 counter packets 0 bytes 0 jump ufw-skip-to-policy-input + udp dport 138 counter packets 0 bytes 0 jump ufw-skip-to-policy-input + tcp dport 139 counter packets 0 bytes 0 jump ufw-skip-to-policy-input + tcp dport 445 counter packets 0 bytes 0 jump ufw-skip-to-policy-input + udp dport 67 counter packets 0 bytes 0 jump ufw-skip-to-policy-input + udp dport 68 counter packets 0 bytes 0 jump ufw-skip-to-policy-input + xt match "addrtype" counter packets 0 bytes 0 jump ufw-skip-to-policy-input + } + + chain ufw-after-output { + } + + chain ufw-after-forward { + } + + chain ufw-after-logging-input { + limit rate 3/minute burst 10 packets counter packets 0 bytes 0 xt target "LOG" + } + + chain ufw-after-logging-output { + } + + chain ufw-after-logging-forward { + limit rate 3/minute burst 10 packets counter packets 0 bytes 0 xt target "LOG" + } + + chain ufw-reject-input { + } + + chain ufw-reject-output { + } + + chain ufw-reject-forward { + } + + chain ufw-track-input { + } + + chain ufw-track-output { + ip protocol tcp xt match "conntrack" counter packets 0 bytes 0 accept + ip protocol udp xt match "conntrack" counter packets 0 bytes 0 accept + } + + chain ufw-track-forward { + } + + chain INPUT { + type filter hook input priority filter; policy drop; + counter packets 1 bytes 76 jump ufw-before-logging-input + counter packets 1 bytes 76 jump ufw-before-input + counter packets 0 bytes 0 jump ufw-after-input + counter packets 0 bytes 0 jump ufw-after-logging-input + counter packets 0 bytes 0 jump ufw-reject-input + counter packets 0 bytes 0 jump ufw-track-input + } + + chain OUTPUT { + type filter hook output priority filter; policy accept; + counter packets 1 bytes 76 jump ufw-before-logging-output + counter packets 1 bytes 76 jump ufw-before-output + counter packets 1 bytes 76 jump ufw-after-output + counter packets 1 bytes 76 jump ufw-after-logging-output + counter packets 1 bytes 76 jump ufw-reject-output + counter packets 1 bytes 76 jump ufw-track-output + } + + chain ufw-logging-deny { + xt match "conntrack" limit rate 3/minute burst 10 packets counter packets 0 bytes 0 return + limit rate 3/minute burst 10 packets counter packets 0 bytes 0 xt target "LOG" + } + + chain ufw-logging-allow { + limit rate 3/minute burst 10 packets counter packets 0 bytes 0 xt target "LOG" + } + + chain ufw-skip-to-policy-input { + counter packets 0 bytes 0 drop + } + + chain ufw-skip-to-policy-output { + counter packets 0 bytes 0 accept + } + + chain ufw-skip-to-policy-forward { + counter packets 0 bytes 0 drop + } + + chain ufw-not-local { + xt match "addrtype" counter packets 0 bytes 0 return + xt match "addrtype" counter packets 0 bytes 0 return + xt match "addrtype" counter packets 0 bytes 0 return + limit rate 3/minute burst 10 packets counter packets 0 bytes 0 jump ufw-logging-deny + counter packets 0 bytes 0 drop + } + + chain ufw-user-input { + tcp dport 22 counter packets 0 bytes 0 accept + tcp dport 8080 counter packets 0 bytes 0 accept + udp dport 51820 counter packets 0 bytes 0 accept + } + + chain ufw-user-output { + } + + chain ufw-user-forward { + tcp dport 80 counter packets 0 bytes 0 accept + iifname "mesh0" tcp dport 443 counter packets 0 bytes 0 accept + } + + chain ufw-user-logging-input { + } + + chain ufw-user-logging-output { + } + + chain ufw-user-logging-forward { + } + + chain ufw-user-limit { + limit rate 3/minute burst 5 packets counter packets 0 bytes 0 xt target "LOG" + counter packets 0 bytes 0 xt target "REJECT" + } + + chain ufw-user-limit-accept { + counter packets 0 bytes 0 accept + } +} +table ip6 nat { + chain DOCKER { + } + + chain PREROUTING { + type nat hook prerouting priority dstnat; policy accept; + xt match "addrtype" counter packets 0 bytes 0 jump DOCKER + } + + chain OUTPUT { + type nat hook output priority dstnat; policy accept; + ip6 daddr != ::1 xt match "addrtype" counter packets 0 bytes 0 jump DOCKER + } +} +table ip6 filter { + chain DOCKER { + } + + chain DOCKER-FORWARD { + counter packets 0 bytes 0 jump DOCKER-CT + counter packets 0 bytes 0 jump DOCKER-INTERNAL + counter packets 0 bytes 0 jump DOCKER-BRIDGE + } + + chain DOCKER-BRIDGE { + } + + chain DOCKER-CT { + } + + chain DOCKER-INTERNAL { + } + + chain FORWARD { + type filter hook forward priority filter; policy drop; + counter packets 0 bytes 0 jump DOCKER-USER + counter packets 0 bytes 0 jump DOCKER-FORWARD + counter packets 0 bytes 0 jump ufw6-before-logging-forward + counter packets 0 bytes 0 jump ufw6-before-forward + counter packets 0 bytes 0 jump ufw6-after-forward + counter packets 0 bytes 0 jump ufw6-after-logging-forward + counter packets 0 bytes 0 jump ufw6-reject-forward + counter packets 0 bytes 0 jump ufw6-track-forward + } + + chain DOCKER-USER { + } + + chain ufw6-before-logging-input { + } + + chain ufw6-before-logging-output { + } + + chain ufw6-before-logging-forward { + } + + chain ufw6-before-input { + iifname "lo" counter packets 0 bytes 0 accept + xt match "rt" counter packets 0 bytes 0 drop + xt match "conntrack" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + xt match "conntrack" counter packets 0 bytes 0 jump ufw6-logging-deny + xt match "conntrack" counter packets 0 bytes 0 drop + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + ip6 saddr fe80::/10 meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + ip6 saddr fe80::/10 meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + ip6 saddr fe80::/10 meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + ip6 saddr fe80::/10 meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + ip6 saddr fe80::/10 meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + ip6 saddr fe80::/10 meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + ip6 saddr fe80::/10 meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + ip6 saddr fe80::/10 ip6 daddr fe80::/10 udp sport 547 udp dport 546 counter packets 0 bytes 0 accept + ip6 daddr ff02::fb udp dport 5353 counter packets 0 bytes 0 accept + ip6 daddr ff02::f udp dport 1900 counter packets 0 bytes 0 accept + counter packets 0 bytes 0 jump ufw6-user-input + } + + chain ufw6-before-output { + oifname "lo" counter packets 0 bytes 0 accept + xt match "rt" counter packets 0 bytes 0 drop + xt match "conntrack" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + ip6 saddr fe80::/10 meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + ip6 saddr fe80::/10 meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + ip6 saddr fe80::/10 meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + ip6 saddr fe80::/10 meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + ip6 saddr fe80::/10 meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + ip6 saddr fe80::/10 meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + ip6 saddr fe80::/10 meta l4proto ipv6-icmp xt match "icmp6" xt match "hl" counter packets 0 bytes 0 accept + counter packets 0 bytes 0 jump ufw6-user-output + } + + chain ufw6-before-forward { + xt match "rt" counter packets 0 bytes 0 drop + xt match "conntrack" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + meta l4proto ipv6-icmp xt match "icmp6" counter packets 0 bytes 0 accept + counter packets 0 bytes 0 jump ufw6-user-forward + } + + chain ufw6-after-input { + udp dport 137 counter packets 0 bytes 0 jump ufw6-skip-to-policy-input + udp dport 138 counter packets 0 bytes 0 jump ufw6-skip-to-policy-input + tcp dport 139 counter packets 0 bytes 0 jump ufw6-skip-to-policy-input + tcp dport 445 counter packets 0 bytes 0 jump ufw6-skip-to-policy-input + udp dport 546 counter packets 0 bytes 0 jump ufw6-skip-to-policy-input + udp dport 547 counter packets 0 bytes 0 jump ufw6-skip-to-policy-input + } + + chain ufw6-after-output { + } + + chain ufw6-after-forward { + } + + chain ufw6-after-logging-input { + limit rate 3/minute burst 10 packets counter packets 0 bytes 0 xt target "LOG" + } + + chain ufw6-after-logging-output { + } + + chain ufw6-after-logging-forward { + limit rate 3/minute burst 10 packets counter packets 0 bytes 0 xt target "LOG" + } + + chain ufw6-reject-input { + } + + chain ufw6-reject-output { + } + + chain ufw6-reject-forward { + } + + chain ufw6-track-input { + } + + chain ufw6-track-output { + meta l4proto tcp xt match "conntrack" counter packets 0 bytes 0 accept + meta l4proto udp xt match "conntrack" counter packets 0 bytes 0 accept + } + + chain ufw6-track-forward { + } + + chain INPUT { + type filter hook input priority filter; policy drop; + counter packets 1 bytes 128 jump ufw6-before-logging-input + counter packets 1 bytes 128 jump ufw6-before-input + counter packets 0 bytes 0 jump ufw6-after-input + counter packets 0 bytes 0 jump ufw6-after-logging-input + counter packets 0 bytes 0 jump ufw6-reject-input + counter packets 0 bytes 0 jump ufw6-track-input + } + + chain OUTPUT { + type filter hook output priority filter; policy accept; + counter packets 4 bytes 304 jump ufw6-before-logging-output + counter packets 4 bytes 304 jump ufw6-before-output + counter packets 0 bytes 0 jump ufw6-after-output + counter packets 0 bytes 0 jump ufw6-after-logging-output + counter packets 0 bytes 0 jump ufw6-reject-output + counter packets 0 bytes 0 jump ufw6-track-output + } + + chain ufw6-logging-deny { + xt match "conntrack" limit rate 3/minute burst 10 packets counter packets 0 bytes 0 return + limit rate 3/minute burst 10 packets counter packets 0 bytes 0 xt target "LOG" + } + + chain ufw6-logging-allow { + limit rate 3/minute burst 10 packets counter packets 0 bytes 0 xt target "LOG" + } + + chain ufw6-skip-to-policy-input { + counter packets 0 bytes 0 drop + } + + chain ufw6-skip-to-policy-output { + counter packets 0 bytes 0 accept + } + + chain ufw6-skip-to-policy-forward { + counter packets 0 bytes 0 drop + } + + chain ufw6-user-input { + tcp dport 22 counter packets 0 bytes 0 accept + tcp dport 8080 counter packets 0 bytes 0 accept + udp dport 51820 counter packets 0 bytes 0 accept + } + + chain ufw6-user-output { + } + + chain ufw6-user-forward { + tcp dport 80 counter packets 0 bytes 0 accept + iifname "mesh0" tcp dport 443 counter packets 0 bytes 0 accept + } + + chain ufw6-user-logging-input { + } + + chain ufw6-user-logging-output { + } + + chain ufw6-user-logging-forward { + } + + chain ufw6-user-limit { + limit rate 3/minute burst 5 packets counter packets 0 bytes 0 xt target "LOG" + counter packets 0 bytes 0 xt target "REJECT" + } + + chain ufw6-user-limit-accept { + counter packets 0 bytes 0 accept + } +} diff --git a/internal/firewall/testdata/ufw-delete.txt b/internal/firewall/testdata/ufw-delete.txt new file mode 100644 index 0000000..b887dad --- /dev/null +++ b/internal/firewall/testdata/ufw-delete.txt @@ -0,0 +1,40 @@ +Rule deleted +Rule deleted (v6) +rc=0 +Rule deleted +Rule deleted (v6) +rc=0 +ERROR: Invalid syntax +rc=1 +===ADDED2 +Added user rules (see 'ufw status' for running firewall): +ufw allow 22/tcp +ufw allow 8080/tcp +ufw route allow 80/tcp comment 'mesh-host adoption.opening-tcp-8081-forwarded 0badf00d' +ufw route allow in on mesh0 to any port 443 proto tcp comment 'mesh-host adoption.opening-tcp-8443-forwarded cafe0001' +ufw allow 51820/udp comment 'mesh-host adoption.opening-udp-51820-incoming 11112222' +Firewall reloaded +reload rc=0 +===AFTERRELOAD +3 +Firewall stopped and disabled on system startup +===DISABLED +Status: inactive +/etc/ufw/user.rules +3 +Firewall is active and enabled on system startup +Status: active +Rule deleted +Rule deleted (v6) +rc=0 +Rule deleted +Rule deleted (v6) +rc=0 +Could not delete non-existent rule +Could not delete non-existent rule (v6) +wrongcomment rc=0 +Added user rules (see 'ufw status' for running firewall): +ufw allow 22/tcp +ufw allow 8080/tcp +ufw allow 51820/udp comment 'mesh-host adoption.opening-udp-51820-incoming 11112222' +Status: active diff --git a/internal/firewall/testdata/ufw-show-added.txt b/internal/firewall/testdata/ufw-show-added.txt new file mode 100644 index 0000000..85d9c6b --- /dev/null +++ b/internal/firewall/testdata/ufw-show-added.txt @@ -0,0 +1,8 @@ +Added user rules (see 'ufw status' for running firewall): +ufw allow 22/tcp +ufw allow 8080/tcp +ufw allow 5671/tcp comment 'mesh-host adoption.opening-tcp-5671-incoming 1a2b3c4d' +ufw allow in on mesh0 to any port 5432 proto tcp comment 'mesh-host adoption.opening-tcp-5432-incoming deadbeef' +ufw route allow 80/tcp comment 'mesh-host adoption.opening-tcp-8081-forwarded 0badf00d' +ufw route allow in on mesh0 to any port 443 proto tcp comment 'mesh-host adoption.opening-tcp-8443-forwarded cafe0001' +ufw allow 51820/udp comment 'mesh-host adoption.opening-udp-51820-incoming 11112222' diff --git a/internal/firewall/testdata/ufw-status-active.txt b/internal/firewall/testdata/ufw-status-active.txt new file mode 100644 index 0000000..9f32038 --- /dev/null +++ b/internal/firewall/testdata/ufw-status-active.txt @@ -0,0 +1,21 @@ +Status: active + +To Action From +-- ------ ---- +22/tcp ALLOW Anywhere +8080/tcp ALLOW Anywhere +5671/tcp ALLOW Anywhere # mesh-host adoption.opening-tcp-5671-incoming 1a2b3c4d +5432/tcp on mesh0 ALLOW Anywhere # mesh-host adoption.opening-tcp-5432-incoming deadbeef +51820/udp ALLOW Anywhere # mesh-host adoption.opening-udp-51820-incoming 11112222 +22/tcp (v6) ALLOW Anywhere (v6) +8080/tcp (v6) ALLOW Anywhere (v6) +5671/tcp (v6) ALLOW Anywhere (v6) # mesh-host adoption.opening-tcp-5671-incoming 1a2b3c4d +5432/tcp (v6) on mesh0 ALLOW Anywhere (v6) # mesh-host adoption.opening-tcp-5432-incoming deadbeef +51820/udp (v6) ALLOW Anywhere (v6) # mesh-host adoption.opening-udp-51820-incoming 11112222 + +80/tcp ALLOW FWD Anywhere # mesh-host adoption.opening-tcp-8081-forwarded 0badf00d +443/tcp ALLOW FWD Anywhere on mesh0 # mesh-host adoption.opening-tcp-8443-forwarded cafe0001 +80/tcp (v6) ALLOW FWD Anywhere (v6) # mesh-host adoption.opening-tcp-8081-forwarded 0badf00d +443/tcp (v6) ALLOW FWD Anywhere (v6) on mesh0 # mesh-host adoption.opening-tcp-8443-forwarded cafe0001 + +===STATUSV