812 lines
29 KiB
Go
812 lines
29 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// fwStandin plays this machine's firewall commands: netsh, ufw, firewall-cmd,
|
|
// socketfilterfw, nft and iptables, each with the state it would keep. Every
|
|
// command run is recorded, so a test can say what was never run.
|
|
type fwStandin struct {
|
|
mu sync.Mutex
|
|
ran []string
|
|
fail map[string]bool // a verb that fails ("add")
|
|
|
|
// Windows: rule name -> its add arguments.
|
|
win map[string]string
|
|
// ufw: on or off, and its rules as `ufw status` lists them.
|
|
ufwOn bool
|
|
ufwRules []string
|
|
// firewalld: running, ports now and permanently.
|
|
fwdOn bool
|
|
fwdNow, fwdPerm map[string]bool
|
|
macOn, macAllIn bool
|
|
macApps map[string]bool
|
|
nftRuleset, ipts string
|
|
}
|
|
|
|
func newFWStandin() *fwStandin {
|
|
return &fwStandin{fail: map[string]bool{}, win: map[string]string{}, fwdNow: map[string]bool{},
|
|
fwdPerm: map[string]bool{}, macApps: map[string]bool{}}
|
|
}
|
|
|
|
var errExit1 = errors.New("exit status 1")
|
|
|
|
func (f *fwStandin) exec(name string, args ...string) (string, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
line := path.Base(strings.ReplaceAll(name, `\`, "/")) + " " + strings.Join(args, " ")
|
|
f.ran = append(f.ran, line)
|
|
a := strings.Join(args, " ")
|
|
switch path.Base(strings.ReplaceAll(name, `\`, "/")) {
|
|
case "netsh.exe":
|
|
if !strings.HasPrefix(a, "advfirewall firewall ") || len(args) < 5 {
|
|
return "", fmt.Errorf("netsh: %s", a)
|
|
}
|
|
rule := strings.TrimPrefix(args[4], "name=")
|
|
switch args[2] {
|
|
case "show":
|
|
if _, ok := f.win[rule]; ok {
|
|
return "Rule Name: " + rule + "\n", nil
|
|
}
|
|
return "No rules match the specified criteria.\n", errExit1
|
|
case "add":
|
|
if f.fail["add"] {
|
|
return "The requested operation requires elevation (Run as administrator).", errExit1
|
|
}
|
|
f.win[rule] = strings.Join(args[5:], " ")
|
|
return "Ok.\n", nil
|
|
case "delete":
|
|
if _, ok := f.win[rule]; !ok {
|
|
return "No rules match the specified criteria.\n", errExit1
|
|
}
|
|
delete(f.win, rule)
|
|
return "\nDeleted 1 rule(s).\nOk.\n", nil
|
|
}
|
|
case "ufw":
|
|
switch {
|
|
case a == "status":
|
|
if !f.ufwOn {
|
|
return "Status: inactive\n", nil
|
|
}
|
|
return "Status: active\n\nTo Action From\n-- ------ ----\n" +
|
|
strings.Join(f.ufwRules, "\n") + "\n", nil
|
|
case strings.HasPrefix(a, "allow "):
|
|
port := args[1]
|
|
c := ""
|
|
if len(args) == 4 && args[2] == "comment" {
|
|
c = " # " + args[3]
|
|
}
|
|
f.ufwRules = append(f.ufwRules, port+" ALLOW Anywhere"+c,
|
|
port+" (v6) ALLOW Anywhere (v6)"+c)
|
|
return "Rule added\nRule added (v6)\n", nil
|
|
case strings.HasPrefix(a, "delete allow "):
|
|
port := args[2]
|
|
var keep []string
|
|
for _, r := range f.ufwRules {
|
|
if strings.Fields(r)[0] != port {
|
|
keep = append(keep, r)
|
|
}
|
|
}
|
|
if len(keep) == len(f.ufwRules) {
|
|
return "Could not delete non-existent rule\n", errExit1
|
|
}
|
|
f.ufwRules = keep
|
|
return "Rule deleted\nRule deleted (v6)\n", nil
|
|
}
|
|
case "firewall-cmd":
|
|
perm := len(args) > 0 && args[0] == "--permanent"
|
|
set := f.fwdNow
|
|
if perm {
|
|
set, args = f.fwdPerm, args[1:]
|
|
}
|
|
if len(args) == 0 {
|
|
break
|
|
}
|
|
switch {
|
|
case args[0] == "--state":
|
|
if f.fwdOn {
|
|
return "running\n", nil
|
|
}
|
|
return "not running\n", errors.New("exit status 252")
|
|
case strings.HasPrefix(args[0], "--query-port="):
|
|
if set[strings.TrimPrefix(args[0], "--query-port=")] {
|
|
return "yes\n", nil
|
|
}
|
|
return "no\n", errExit1
|
|
case strings.HasPrefix(args[0], "--add-port="):
|
|
if perm && f.fail["permanent"] {
|
|
return "Error: COMMAND_FAILED\n", errExit1
|
|
}
|
|
set[strings.TrimPrefix(args[0], "--add-port=")] = true
|
|
return "success\n", nil
|
|
case strings.HasPrefix(args[0], "--remove-port="):
|
|
p := strings.TrimPrefix(args[0], "--remove-port=")
|
|
if !set[p] {
|
|
return "Warning: NOT_ENABLED: " + p + "\nsuccess\n", nil
|
|
}
|
|
delete(set, p)
|
|
return "success\n", nil
|
|
}
|
|
case "socketfilterfw":
|
|
switch args[0] {
|
|
case "--getglobalstate":
|
|
if f.macOn {
|
|
return "Firewall is enabled. (State = 1)\n", nil
|
|
}
|
|
return "Firewall is disabled. (State = 0)\n", nil
|
|
case "--getblockall":
|
|
if f.macAllIn {
|
|
return "Firewall is set to block all non-essential incoming connections\nBlock all ENABLED!\n", nil
|
|
}
|
|
return "Block all DISABLED!\n", nil
|
|
case "--listapps":
|
|
// As macOS before 15 prints it (developer.apple.com/forums/thread/757909).
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "ALF: total number of apps = %d \n\n", len(f.macApps))
|
|
var ps []string
|
|
for p := range f.macApps {
|
|
ps = append(ps, p)
|
|
}
|
|
sort.Strings(ps)
|
|
for i, p := range ps {
|
|
st := "Block"
|
|
if f.macApps[p] {
|
|
st = "Allow"
|
|
}
|
|
fmt.Fprintf(&b, "%d : %s ( %s incoming connections )\n", i+1, p, st)
|
|
}
|
|
return b.String(), nil
|
|
case "--add":
|
|
f.macApps[args[1]] = false
|
|
return "The file " + args[1] + " added to firewall\n", nil
|
|
case "--unblockapp":
|
|
if f.fail["unblock"] {
|
|
return "Error\n", errExit1
|
|
}
|
|
f.macApps[args[1]] = true
|
|
return "Incoming connection to the application is permitted\n", nil
|
|
case "--remove":
|
|
delete(f.macApps, args[1])
|
|
return "The file " + args[1] + " removed from firewall\n", nil
|
|
}
|
|
case "nft":
|
|
return f.nftRuleset, nil
|
|
case "iptables":
|
|
return f.ipts, nil
|
|
}
|
|
return "", fmt.Errorf("the stand-in does not know %s", line)
|
|
}
|
|
|
|
func (f *fwStandin) commands() []string {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return append([]string(nil), f.ran...)
|
|
}
|
|
|
|
// fwWorld stands the firewall in for goos, with the door wanted while *want.
|
|
func fwWorld(t *testing.T, goos string, root bool) (*fwStandin, *bool) {
|
|
t.Helper()
|
|
uiTestConfigDir(t)
|
|
f := newFWStandin()
|
|
want := true
|
|
prevExec, prevFor, prevBy, prevHave, prevRoot, prevProg, prevWant, prevRead :=
|
|
doorFWExec, doorFirewallFor, doorFirewallByID, doorFWHave, doorFWRoot, doorFWProgram, doorPortWanted, doorFWReadFile
|
|
t.Cleanup(func() {
|
|
doorFWExec, doorFirewallFor, doorFirewallByID, doorFWHave, doorFWRoot, doorFWProgram, doorPortWanted, doorFWReadFile =
|
|
prevExec, prevFor, prevBy, prevHave, prevRoot, prevProg, prevWant, prevRead
|
|
})
|
|
doorFWExec = f.exec
|
|
doorFirewallFor = func(port string) (doorFirewall, string) { return detectDoorFirewall(goos, port) }
|
|
doorFirewallByID = func(id string) doorFirewall { return doorFirewallOf(goos, id) }
|
|
doorFWHave = func(paths ...string) string { return paths[0] }
|
|
doorFWRoot = func() bool { return root }
|
|
doorFWProgram = func() (string, error) { return `C:\Program Files\Privatae\privatae-bastion.exe`, nil }
|
|
doorPortWanted = func(string) bool { return want }
|
|
doorFWReadFile = func(string) ([]byte, error) { return nil, errors.New("no such file") }
|
|
return f, &want
|
|
}
|
|
|
|
func fwAudit(t *testing.T) string {
|
|
t.Helper()
|
|
d, _ := configDir()
|
|
b, _ := os.ReadFile(filepath.Join(d, "pairing_audit.jsonl"))
|
|
return string(b)
|
|
}
|
|
|
|
func countOp(t *testing.T, op string) int {
|
|
return strings.Count(fwAudit(t), `"op":"`+op+`"`)
|
|
}
|
|
|
|
// Windows: the rule is the bastion's own by name, inbound, TCP on the door's
|
|
// port only, every profile, for this program only, made by netsh from
|
|
// System32. It is made once, kept while the door is in use, and removed when
|
|
// the last pairing and window are gone, each step in the audit and in status.
|
|
func TestTheWindowsRuleIsTheBastionsOwnAndGoesWithTheDoor(t *testing.T) {
|
|
f, want := fwWorld(t, "windows", false)
|
|
said := doorFirewallEnsure(":8812")
|
|
rule := doorFWRuleName("8812")
|
|
got, ok := f.win[rule]
|
|
if !ok {
|
|
t.Fatalf("no rule %q: %v", rule, f.commands())
|
|
}
|
|
for _, part := range []string{"dir=in", "action=allow", "protocol=TCP", "localport=8812",
|
|
`program=C:\Program Files\Privatae\privatae-bastion.exe`, "profile=any", "enable=yes"} {
|
|
if !strings.Contains(got, part) {
|
|
t.Fatalf("the rule lacks %s: %s", part, got)
|
|
}
|
|
}
|
|
for _, c := range f.commands() {
|
|
if !strings.HasPrefix(c, "netsh.exe advfirewall firewall ") {
|
|
t.Fatalf("ran %q", c)
|
|
}
|
|
}
|
|
if !strings.HasPrefix(netshPath(), `C:\Windows\System32`) {
|
|
t.Fatalf("netsh is not run by its absolute path: %s", netshPath())
|
|
}
|
|
if !strings.Contains(said, "added its own rule in Windows Firewall") {
|
|
t.Fatalf("said %q", said)
|
|
}
|
|
// Again (the daemon's door after the window's): kept, not doubled.
|
|
doorFirewallEnsure(":8812")
|
|
if n := strings.Count(strings.Join(f.commands(), "\n"), " add rule "); n != 1 {
|
|
t.Fatalf("%d adds", n)
|
|
}
|
|
s := readDoorFirewallStatus()
|
|
if s == nil || len(s.Rules) != 1 || !strings.Contains(s.Rules[0], "Windows Firewall allows TCP 8812") {
|
|
t.Fatalf("status: %+v", s)
|
|
}
|
|
// Still in use: the rule stays.
|
|
doorFirewallRelease(":8812")
|
|
if _, ok := f.win[rule]; !ok {
|
|
t.Fatal("removed while the door is still in use")
|
|
}
|
|
// The last pairing and window gone: removed.
|
|
*want = false
|
|
doorFirewallRelease(":8812")
|
|
if _, ok := f.win[rule]; ok {
|
|
t.Fatal("the rule outlived the door")
|
|
}
|
|
if s := readDoorFirewallStatus(); s != nil {
|
|
t.Fatalf("status after: %+v", s)
|
|
}
|
|
if countOp(t, "door_firewall_opened") != 1 || countOp(t, "door_firewall_closed") != 1 {
|
|
t.Fatalf("the audit:\n%s", fwAudit(t))
|
|
}
|
|
// MUTATION: release removes nothing. RESULT: red, "the rule outlived the door".
|
|
// MUTATION: the add drops program=. RESULT: red, "the rule lacks program=".
|
|
// MUTATION: ensure adds without asking has(). RESULT: red, "2 adds".
|
|
}
|
|
|
|
// Windows: the bastion moved (another path): its rule is replaced by one for
|
|
// the new path; and an add refused (no administrator) is said once, with the
|
|
// command that does it by hand, in status and doctor.
|
|
func TestTheWindowsRuleFollowsTheProgramAndAFailureIsSaidOnce(t *testing.T) {
|
|
f, _ := fwWorld(t, "windows", false)
|
|
doorFirewallEnsure(":8812")
|
|
doorFWProgram = func() (string, error) { return `D:\Privatae\privatae-bastion.exe`, nil }
|
|
doorFirewallEnsure(":8812")
|
|
if got := f.win[doorFWRuleName("8812")]; !strings.Contains(got, `program=D:\Privatae\privatae-bastion.exe`) {
|
|
t.Fatalf("the rule names %s", got)
|
|
}
|
|
if st := loadDoorFW(); len(st.Rules) != 1 || st.Rules[0].Program != `D:\Privatae\privatae-bastion.exe` {
|
|
t.Fatalf("the record: %+v", st.Rules)
|
|
}
|
|
|
|
g, _ := fwWorld(t, "windows", false)
|
|
g.fail["add"] = true
|
|
doorFirewallEnsure(":8812")
|
|
doorFirewallEnsure(":8812")
|
|
if n := countOp(t, "door_firewall_failed"); n != 1 {
|
|
t.Fatalf("the failure audited %d times", n)
|
|
}
|
|
s := readDoorFirewallStatus()
|
|
if s == nil || len(s.Needs) != 1 || !strings.Contains(s.Needs[0], "New-NetFirewallRule") {
|
|
t.Fatalf("status: %+v", s)
|
|
}
|
|
cs := doorFirewallChecks(s)
|
|
if len(cs) != 1 || cs[0].OK || !strings.Contains(cs[0].Problem, "TCP 8812") {
|
|
t.Fatalf("doctor: %+v", cs)
|
|
}
|
|
// MUTATION: the program is not compared. RESULT: red, the rule names the old path.
|
|
// MUTATION: the note is audited on every ensure. RESULT: red, audited 2 times.
|
|
}
|
|
|
|
// Linux ufw: a rule only while ufw is on, with the bastion's comment; ufw is
|
|
// never turned on, off or reset; a rule of the machine's own for the port is
|
|
// neither doubled nor removed.
|
|
func TestTheUfwRuleIsAddedOnlyWhenUfwIsOnAndNeverTheMachinesOwn(t *testing.T) {
|
|
f, want := fwWorld(t, "linux", true)
|
|
// Off: nothing at all.
|
|
if said := doorFirewallEnsure(":8812"); said != "" || len(loadDoorFW().Rules) != 0 {
|
|
t.Fatalf("ufw off: %q %v", said, f.commands())
|
|
}
|
|
f.ufwOn = true
|
|
doorFirewallEnsure(":8812")
|
|
st := strings.Join(f.ufwRules, "\n")
|
|
if !strings.Contains(st, "8812/tcp") || !strings.Contains(st, "# "+doorFWComment) {
|
|
t.Fatalf("ufw rules: %s", st)
|
|
}
|
|
if s := readDoorFirewallStatus(); s == nil || len(s.Rules) != 1 || !strings.Contains(s.Rules[0], "ufw allows TCP 8812") {
|
|
t.Fatalf("status: %+v", s)
|
|
}
|
|
*want = false
|
|
doorFirewallRelease(":8812")
|
|
if len(f.ufwRules) != 0 {
|
|
t.Fatalf("left: %v", f.ufwRules)
|
|
}
|
|
for _, c := range f.commands() {
|
|
for _, never := range []string{"ufw enable", "ufw disable", "ufw reset", "ufw default", "ufw reload"} {
|
|
if strings.HasPrefix(c, never) {
|
|
t.Fatalf("ran %q", c)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The machine's own rule for the port.
|
|
g, want2 := fwWorld(t, "linux", true)
|
|
g.ufwOn = true
|
|
g.ufwRules = []string{"8812/tcp ALLOW Anywhere", "8812/tcp (v6) ALLOW Anywhere (v6)"}
|
|
said := doorFirewallEnsure(":8812")
|
|
if !strings.Contains(said, "already allowed") || len(g.ufwRules) != 2 {
|
|
t.Fatalf("said %q, rules %v", said, g.ufwRules)
|
|
}
|
|
*want2 = false
|
|
doorFirewallRelease(":8812")
|
|
if len(g.ufwRules) != 2 {
|
|
t.Fatalf("the machine's own rule was removed: %v", g.commands())
|
|
}
|
|
// MUTATION: ufwRuleFor counts any ALLOW as ours. RESULT: red, the machine's own rule was removed.
|
|
// MUTATION: detection ignores "Status: inactive". RESULT: red, ufw off.
|
|
}
|
|
|
|
// Linux firewalld: the port added now and permanently, and removed the same
|
|
// way; a port already open in it is the machine's own.
|
|
func TestTheFirewalldPortIsAddedAndRemovedNowAndPermanently(t *testing.T) {
|
|
f, want := fwWorld(t, "linux", true)
|
|
f.fwdOn = true
|
|
doorFirewallEnsure(":8812")
|
|
if !f.fwdNow["8812/tcp"] || !f.fwdPerm["8812/tcp"] {
|
|
t.Fatalf("firewalld: %v %v", f.fwdNow, f.fwdPerm)
|
|
}
|
|
doorFirewallEnsure(":8812")
|
|
if n := strings.Count(strings.Join(f.commands(), "\n"), "--add-port"); n != 2 {
|
|
t.Fatalf("%d adds: %v", n, f.commands())
|
|
}
|
|
*want = false
|
|
doorFirewallRelease(":8812")
|
|
if f.fwdNow["8812/tcp"] || f.fwdPerm["8812/tcp"] {
|
|
t.Fatalf("left: %v %v", f.fwdNow, f.fwdPerm)
|
|
}
|
|
|
|
g, want2 := fwWorld(t, "linux", true)
|
|
g.fwdOn = true
|
|
g.fwdNow["8812/tcp"], g.fwdPerm["8812/tcp"] = true, true
|
|
doorFirewallEnsure(":8812")
|
|
*want2 = false
|
|
doorFirewallRelease(":8812")
|
|
if !g.fwdNow["8812/tcp"] || !g.fwdPerm["8812/tcp"] {
|
|
t.Fatal("the machine's own open port was closed")
|
|
}
|
|
// MUTATION: remove only the runtime port. RESULT: red, left permanently.
|
|
// MUTATION: has() reports a port it did not add as ours. RESULT: red, the machine's own port closed.
|
|
}
|
|
|
|
// Linux: a firewall the bastion does not manage (nftables, iptables), or ufw
|
|
// it may not change, is said once, with what to allow, in status and doctor,
|
|
// and nothing in it is changed.
|
|
func TestAFirewallTheBastionDoesNotManageIsSaidOnceAndLeftAlone(t *testing.T) {
|
|
f, want := fwWorld(t, "linux", true)
|
|
f.nftRuleset = "table inet filter {\n\tchain input {\n\t\ttype filter hook input priority filter; policy drop;\n\t}\n}\n"
|
|
said := doorFirewallEnsure(":8812")
|
|
doorFirewallEnsure(":8812")
|
|
if !strings.Contains(said, "nftables") || !strings.Contains(said, "tcp dport 8812 accept") {
|
|
t.Fatalf("said %q", said)
|
|
}
|
|
if n := countOp(t, "door_firewall_unmanaged"); n != 1 {
|
|
t.Fatalf("said %d times", n)
|
|
}
|
|
for _, c := range f.commands() {
|
|
if strings.HasPrefix(c, "nft ") && c != "nft list ruleset" {
|
|
t.Fatalf("ran %q", c)
|
|
}
|
|
}
|
|
s := readDoorFirewallStatus()
|
|
if s == nil || len(s.Needs) != 1 || !strings.Contains(s.Needs[0], "nftables") {
|
|
t.Fatalf("status: %+v", s)
|
|
}
|
|
if cs := doorFirewallChecks(s); len(cs) != 1 || cs[0].OK {
|
|
t.Fatalf("doctor: %+v", cs)
|
|
}
|
|
*want = false
|
|
doorFirewallRelease(":8812")
|
|
if s := readDoorFirewallStatus(); s != nil {
|
|
t.Fatalf("still said with no door: %+v", s)
|
|
}
|
|
|
|
// iptables, and an account that may not change ufw.
|
|
g, _ := fwWorld(t, "linux", true)
|
|
g.ipts = "-P INPUT DROP\n"
|
|
if said := doorFirewallEnsure(":8812"); !strings.Contains(said, "iptables -I INPUT -p tcp --dport 8812 -j ACCEPT") {
|
|
t.Fatalf("iptables: %q", said)
|
|
}
|
|
h, _ := fwWorld(t, "linux", false)
|
|
doorFWReadFile = func(string) ([]byte, error) { return []byte("ENABLED=yes\n"), nil }
|
|
if said := doorFirewallEnsure(":8812"); !strings.Contains(said, "sudo ufw allow 8812/tcp") {
|
|
t.Fatalf("not root: %q", said)
|
|
}
|
|
for _, c := range h.commands() {
|
|
if strings.HasPrefix(c, "ufw ") {
|
|
t.Fatalf("an account that may not change ufw ran %q", c)
|
|
}
|
|
}
|
|
// MUTATION: nftFiltersInput always false. RESULT: red, said "".
|
|
// MUTATION: the note is not cleared at release. RESULT: red, still said with no door.
|
|
}
|
|
|
|
// macOS: with the application firewall on, this program is allowed; with it
|
|
// off nothing is done; "block all incoming" is only said.
|
|
func TestTheMacFirewallAllowsTheProgramOnlyWhenItIsOn(t *testing.T) {
|
|
f, want := fwWorld(t, "darwin", true)
|
|
doorFirewallEnsure(":8812")
|
|
if len(f.macApps) != 0 {
|
|
t.Fatal("added with the firewall off")
|
|
}
|
|
f.macOn = true
|
|
doorFirewallEnsure(":8812")
|
|
prog, _ := doorFWProgram()
|
|
if allowed, ok := f.macApps[prog]; !ok || !allowed {
|
|
t.Fatalf("apps: %v", f.macApps)
|
|
}
|
|
*want = false
|
|
doorFirewallRelease(":8812")
|
|
if len(f.macApps) != 0 {
|
|
t.Fatalf("left: %v", f.macApps)
|
|
}
|
|
g, _ := fwWorld(t, "darwin", true)
|
|
g.macOn, g.macAllIn = true, true
|
|
if said := doorFirewallEnsure(":8812"); !strings.Contains(said, "blocks all incoming") || len(g.macApps) != 0 {
|
|
t.Fatalf("block all: %q %v", said, g.macApps)
|
|
}
|
|
// MUTATION: --unblockapp not run. RESULT: red, apps not allowed.
|
|
}
|
|
|
|
// uninstall removes every rule the bastion added, whatever uses the door.
|
|
func TestUninstallRemovesTheDoorsOwnRules(t *testing.T) {
|
|
f, _ := fwWorld(t, "windows", false)
|
|
doorFirewallEnsure(":8812")
|
|
doorFirewallEnsure(":9443")
|
|
said, err := doorFirewallRemoveAll()
|
|
if err != nil || len(said) != 2 || len(f.win) != 0 {
|
|
t.Fatalf("uninstall: %v %v, left %v", said, err, f.win)
|
|
}
|
|
if len(loadDoorFW().Rules) != 0 {
|
|
t.Fatal("the record outlived the rules")
|
|
}
|
|
// MUTATION: RemoveAll skips the removal. RESULT: red, rules left.
|
|
}
|
|
|
|
// A rule left behind (a `pair --open` killed before its release, a pairing
|
|
// that went while nothing ran) is removed by the daemon's sweep once nothing
|
|
// uses its port, and kept while something does.
|
|
func TestTheDaemonsSweepRemovesARuleNothingUses(t *testing.T) {
|
|
f, want := fwWorld(t, "windows", false)
|
|
doorFirewallEnsure(":8812")
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan struct{})
|
|
go func() { doorFirewallSweep(ctx); close(done) }()
|
|
cancel()
|
|
<-done
|
|
if _, ok := f.win[doorFWRuleName("8812")]; !ok {
|
|
t.Fatal("the sweep removed a rule in use")
|
|
}
|
|
*want = false
|
|
doorFirewallReleaseUnused()
|
|
if _, ok := f.win[doorFWRuleName("8812")]; ok {
|
|
t.Fatal("the sweep left a rule nothing uses")
|
|
}
|
|
// MUTATION: doorFirewallReleaseUnused skips recorded rules. RESULT: red, the sweep left a rule.
|
|
}
|
|
|
|
// ufw deletes by rule, whatever its comment: a rule the bastion recorded and
|
|
// the person later replaced with their own, or a record that names a port the
|
|
// bastion never opened, is the machine's, and is never removed.
|
|
func TestUfwNeverRemovesARuleThatIsNotItsOwnWhateverTheRecordSays(t *testing.T) {
|
|
f, want := fwWorld(t, "linux", true)
|
|
f.ufwOn = true
|
|
doorFirewallEnsure(":8812")
|
|
// The person deletes the bastion's rule and puts their own in its place.
|
|
f.ufwRules = []string{"8812/tcp ALLOW Anywhere", "8812/tcp (v6) ALLOW Anywhere (v6)"}
|
|
doorFirewallEnsure(":8812")
|
|
*want = false
|
|
doorFirewallRelease(":8812")
|
|
if len(f.ufwRules) != 2 {
|
|
t.Fatalf("the person's own rule was removed: %v", f.commands())
|
|
}
|
|
|
|
// A record naming a port the bastion never opened (written by hand, or
|
|
// left from another time): the sweep leaves the machine's rule there.
|
|
g, want2 := fwWorld(t, "linux", true)
|
|
g.ufwOn = true
|
|
g.ufwRules = []string{"22/tcp ALLOW Anywhere"}
|
|
if err := saveDoorFW(doorFWState{Rules: []doorFWRule{{Firewall: "ufw", Port: "22", Added: "2026-01-01T00:00:00Z"}}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
*want2 = false
|
|
doorFirewallReleaseUnused()
|
|
for _, c := range g.commands() {
|
|
if strings.HasPrefix(c, "ufw delete") {
|
|
t.Fatalf("ran %q on a rule that is not the bastion's", c)
|
|
}
|
|
}
|
|
if len(g.ufwRules) != 1 || len(loadDoorFW().Rules) != 0 {
|
|
t.Fatalf("rules %v, record %+v", g.ufwRules, loadDoorFW().Rules)
|
|
}
|
|
// MUTATION: ufw remove deletes without reading the comment. RESULT: red, the person's own rule was removed.
|
|
}
|
|
|
|
// Only a rule that lets a Host in from anywhere counts as the machine's own
|
|
// allow: an outbound rule, or one for one source or interface, does not, so
|
|
// the bastion adds its own rather than saying the port is already allowed.
|
|
func TestUfwAnOutboundOrOneSourceRuleIsNotTheWayIn(t *testing.T) {
|
|
for _, r := range []string{
|
|
"8812/tcp ALLOW OUT Anywhere",
|
|
"8812/tcp ALLOW 10.0.0.0/8",
|
|
"8812/tcp on eth1 ALLOW Anywhere",
|
|
"8812/tcp DENY Anywhere",
|
|
} {
|
|
f, _ := fwWorld(t, "linux", true)
|
|
f.ufwOn = true
|
|
f.ufwRules = []string{r}
|
|
said := doorFirewallEnsure(":8812")
|
|
if !strings.Contains(said, "added its own rule in ufw") {
|
|
t.Fatalf("with %q: said %q", r, said)
|
|
}
|
|
}
|
|
for _, r := range []string{
|
|
"8812/tcp ALLOW Anywhere",
|
|
"8812/tcp ALLOW IN Anywhere",
|
|
"8812 (v6) ALLOW Anywhere (v6)",
|
|
} {
|
|
if _, other := ufwRuleFor(r, "8812"); !other {
|
|
t.Fatalf("%q lets a Host in from anywhere", r)
|
|
}
|
|
}
|
|
// MUTATION: any line with ALLOW counts. RESULT: red, said "already allowed".
|
|
}
|
|
|
|
// firewalld: a permanent add that fails takes the runtime port back out, so
|
|
// no open port is left that no record says is the bastion's.
|
|
func TestFirewalldAFailedPermanentAddLeavesNoPortOpen(t *testing.T) {
|
|
f, _ := fwWorld(t, "linux", true)
|
|
f.fwdOn = true
|
|
f.fail["permanent"] = true
|
|
said := doorFirewallEnsure(":8812")
|
|
if !strings.Contains(said, "could not add") {
|
|
t.Fatalf("said %q", said)
|
|
}
|
|
if f.fwdNow["8812/tcp"] {
|
|
t.Fatalf("the runtime port was left open with no record: %v", f.commands())
|
|
}
|
|
// Next time it is tried again, not taken for the machine's own.
|
|
f.fail["permanent"] = false
|
|
if said := doorFirewallEnsure(":8812"); !strings.Contains(said, "added its own rule") {
|
|
t.Fatalf("said %q", said)
|
|
}
|
|
// MUTATION: no undo of the runtime add. RESULT: red, the runtime port was left open.
|
|
}
|
|
|
|
// macOS: an --unblockapp that fails takes the --add back out, so no entry is
|
|
// left that would read as the machine's own and never go.
|
|
func TestTheMacAFailedUnblockLeavesNoEntry(t *testing.T) {
|
|
f, _ := fwWorld(t, "darwin", true)
|
|
f.macOn = true
|
|
f.fail["unblock"] = true
|
|
if said := doorFirewallEnsure(":8812"); !strings.Contains(said, "could not add") {
|
|
t.Fatalf("said %q", said)
|
|
}
|
|
if len(f.macApps) != 0 {
|
|
t.Fatalf("left an entry with no record: %v", f.macApps)
|
|
}
|
|
// MUTATION: no --remove after a failed --unblockapp. RESULT: red, left an entry.
|
|
}
|
|
|
|
// realDoorPortWanted is the door-in-use check fwWorld stands in.
|
|
var realDoorPortWanted = doorPortWanted
|
|
|
|
// sweepOnce runs the daemon's sweep's first pass.
|
|
func sweepOnce() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
doorFirewallSweep(ctx)
|
|
}
|
|
|
|
// A door claims the window (its file is moved aside) before the pairing is
|
|
// saved: between the two, neither is there to read. The window is still in
|
|
// use then, so a sweep in that gap keeps the rule, and puts it back if it had
|
|
// gone; once the window has its result and no pairing uses the port, the
|
|
// rule goes.
|
|
func TestAWindowClaimedForAPairingStillBeingSavedKeepsTheRule(t *testing.T) {
|
|
f, _ := fwWorld(t, "windows", false)
|
|
doorPortWanted = realDoorPortWanted
|
|
code, err := newWindowCode()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Now().UTC()
|
|
w := &pairWindow{ID: "w-claimed", Code: code, Door: ":8812", Opened: now.Format(time.RFC3339),
|
|
Expires: now.Add(windowLifetime).Format(time.RFC3339)}
|
|
if err := saveWindowFile(w); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
doorFirewallEnsure(":8812")
|
|
rule := doorFWRuleName("8812")
|
|
if _, ok := f.win[rule]; !ok {
|
|
t.Fatalf("no rule for the open window: %v", f.commands())
|
|
}
|
|
if !claimWindow(w.ID) {
|
|
t.Fatal("the window could not be claimed")
|
|
}
|
|
sweepOnce()
|
|
if _, ok := f.win[rule]; !ok {
|
|
t.Fatal("the sweep removed the rule while a pairing on the window was being saved")
|
|
}
|
|
// The rule gone in that gap (by hand, or a release that ran then): back.
|
|
delete(f.win, rule)
|
|
sweepOnce()
|
|
if _, ok := f.win[rule]; !ok {
|
|
t.Fatal("the sweep did not put back the rule of a window still in use")
|
|
}
|
|
endWindow(w.ID, windowResult{Error: "the Host refused"})
|
|
sweepOnce()
|
|
if _, ok := f.win[rule]; ok {
|
|
t.Fatal("the rule outlived the window")
|
|
}
|
|
// MUTATION: doorPortsInUse ignores a claimed window. RESULT: red, the sweep removed the rule while a pairing ... was being saved.
|
|
// MUTATION: claimedWindowUnderWay counts a window with its result. RESULT: red, the rule outlived the window.
|
|
}
|
|
|
|
// A rule that goes while its door is in use (removed by hand, the firewall
|
|
// reset) is put back by the daemon's sweep; a port a rule of the machine's
|
|
// own allows is still never given a second one; nothing in use, nothing added.
|
|
func TestTheSweepPutsBackARuleThatWentWhileTheDoorIsInUse(t *testing.T) {
|
|
f, want := fwWorld(t, "windows", false)
|
|
doorFirewallEnsure(":8812")
|
|
rule := doorFWRuleName("8812")
|
|
delete(f.win, rule) // removed by hand
|
|
sweepOnce()
|
|
if got, ok := f.win[rule]; !ok || !strings.Contains(got, "localport=8812") {
|
|
t.Fatalf("not put back: %v", f.commands())
|
|
}
|
|
if n := countOp(t, "door_firewall_opened"); n != 2 {
|
|
t.Fatalf("opened %d times:\n%s", n, fwAudit(t))
|
|
}
|
|
*want = false
|
|
sweepOnce()
|
|
if _, ok := f.win[rule]; ok {
|
|
t.Fatal("a rule nothing uses was kept")
|
|
}
|
|
sweepOnce()
|
|
if _, ok := f.win[rule]; ok {
|
|
t.Fatal("the sweep added a rule nothing uses")
|
|
}
|
|
|
|
// ufw: the bastion's rule deleted while the door is in use, back with its comment.
|
|
g, _ := fwWorld(t, "linux", true)
|
|
g.ufwOn = true
|
|
doorFirewallEnsure(":8812")
|
|
g.ufwRules = nil
|
|
sweepOnce()
|
|
if st := strings.Join(g.ufwRules, "\n"); !strings.Contains(st, "8812/tcp") || !strings.Contains(st, "# "+doorFWComment) {
|
|
t.Fatalf("ufw not put back: %v", g.commands())
|
|
}
|
|
|
|
// ufw: the machine's own rule allows the port: never a second one.
|
|
h, _ := fwWorld(t, "linux", true)
|
|
h.ufwOn = true
|
|
h.ufwRules = []string{"8812/tcp ALLOW Anywhere"}
|
|
doorFirewallEnsure(":8812")
|
|
sweepOnce()
|
|
sweepOnce()
|
|
for _, c := range h.commands() {
|
|
if strings.HasPrefix(c, "ufw allow") {
|
|
t.Fatalf("ran %q with the machine's own rule there", c)
|
|
}
|
|
}
|
|
// MUTATION: the sweep only releases. RESULT: red, not put back (and in
|
|
// TestAWindowClaimedForAPairingStillBeingSavedKeepsTheRule).
|
|
}
|
|
|
|
// macOS: an entry is for one program path; the bastion that moved removes the
|
|
// entry by the path it recorded, both when its door is released and when it
|
|
// makes the entry for its new path.
|
|
func TestTheMacEntryIsRemovedByThePathItWasAddedFor(t *testing.T) {
|
|
const a, b = "/usr/local/bin/privatae-bastion", "/opt/example/privatae-bastion"
|
|
f, want := fwWorld(t, "darwin", true)
|
|
f.macOn = true
|
|
doorFWProgram = func() (string, error) { return a, nil }
|
|
doorFirewallEnsure(":8812")
|
|
if !f.macApps[a] {
|
|
t.Fatalf("apps: %v", f.macApps)
|
|
}
|
|
doorFWProgram = func() (string, error) { return b, nil }
|
|
*want = false
|
|
doorFirewallRelease(":8812")
|
|
if len(f.macApps) != 0 {
|
|
t.Fatalf("the moved bastion left its old path allowed: %v", f.macApps)
|
|
}
|
|
|
|
// Moved while the door is in use: the old path's entry goes, the new one's is made.
|
|
g, _ := fwWorld(t, "darwin", true)
|
|
g.macOn = true
|
|
doorFWProgram = func() (string, error) { return a, nil }
|
|
doorFirewallEnsure(":8812")
|
|
doorFWProgram = func() (string, error) { return b, nil }
|
|
doorFirewallEnsure(":8812")
|
|
if _, ok := g.macApps[a]; ok || !g.macApps[b] {
|
|
t.Fatalf("apps: %v", g.macApps)
|
|
}
|
|
if st := loadDoorFW(); len(st.Rules) != 1 || st.Rules[0].Program != b {
|
|
t.Fatalf("the record: %+v", st.Rules)
|
|
}
|
|
// MUTATION: remove uses this program's path, not the record's. RESULT: red, the moved bastion left its old path allowed.
|
|
// MUTATION: the moved case removes this program's path. RESULT: red, apps: both paths.
|
|
}
|
|
|
|
// macOS: --listapps is read entry by entry, by the exact path, and only an
|
|
// "allow incoming" entry counts: a blocked entry is the person's setting and
|
|
// is said, not changed; a longer path (…/privatae-bastion.old) is not this
|
|
// program's.
|
|
func TestTheMacReadsEachEntryAndCountsOnlyAnAllow(t *testing.T) {
|
|
const prog = "/usr/local/bin/privatae-bastion"
|
|
f, _ := fwWorld(t, "darwin", true)
|
|
f.macOn = true
|
|
doorFWProgram = func() (string, error) { return prog, nil }
|
|
f.macApps[prog] = false // the person blocked it
|
|
said := doorFirewallEnsure(":8812")
|
|
if !strings.Contains(said, "block incoming connections for this program") {
|
|
t.Fatalf("said %q", said)
|
|
}
|
|
for _, c := range f.commands() {
|
|
if strings.Contains(c, "--unblockapp") || strings.Contains(c, "--add") || strings.Contains(c, "--remove") {
|
|
t.Fatalf("changed the person's setting: %q", c)
|
|
}
|
|
}
|
|
if s := readDoorFirewallStatus(); s == nil || len(s.Needs) != 1 {
|
|
t.Fatalf("status: %+v", s)
|
|
}
|
|
|
|
g, _ := fwWorld(t, "darwin", true)
|
|
g.macOn = true
|
|
doorFWProgram = func() (string, error) { return prog, nil }
|
|
g.macApps[prog+".old"] = true
|
|
if said := doorFirewallEnsure(":8812"); !strings.Contains(said, "added its own rule") || !g.macApps[prog] {
|
|
t.Fatalf("a longer path counted as this program: said %q, apps %v", said, g.macApps)
|
|
}
|
|
|
|
// Its own entry, later set to block by the person: left as it is at release.
|
|
h, want := fwWorld(t, "darwin", true)
|
|
h.macOn = true
|
|
doorFWProgram = func() (string, error) { return prog, nil }
|
|
doorFirewallEnsure(":8812")
|
|
h.macApps[prog] = false
|
|
*want = false
|
|
doorFirewallRelease(":8812")
|
|
if allowed, ok := h.macApps[prog]; !ok || allowed {
|
|
t.Fatalf("the person's block was removed: %v", h.macApps)
|
|
}
|
|
// MUTATION: has() counts a listed program whatever its state. RESULT: red, said "already allowed".
|
|
// MUTATION: the path is matched as a prefix. RESULT: red, a longer path counted as this program.
|
|
// MUTATION: remove runs --remove on a blocked entry. RESULT: red, the person's block was removed.
|
|
}
|