207 lines
6.9 KiB
Go
207 lines
6.9 KiB
Go
//go:build linux
|
|
|
|
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"errors"
|
|
"net"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// fwFake is a firewall the bastion manages, kept in memory.
|
|
type fwFake struct {
|
|
mu sync.Mutex
|
|
rules map[string]bool
|
|
adds int
|
|
}
|
|
|
|
func (f *fwFake) id() string { return "ufw" }
|
|
func (f *fwFake) name() string { return "ufw" }
|
|
func (f *fwFake) has(port string) (bool, bool, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.rules[port], false, nil
|
|
}
|
|
func (f *fwFake) add(port string) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.rules[port] = true
|
|
f.adds++
|
|
return nil
|
|
}
|
|
func (f *fwFake) remove(port, _ string) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if !f.rules[port] {
|
|
return errors.New("no such rule")
|
|
}
|
|
delete(f.rules, port)
|
|
return nil
|
|
}
|
|
func (f *fwFake) manual(port string) string { return "allow " + port }
|
|
func (f *fwFake) open(port string) bool { ok, _, _ := f.has(port); return ok }
|
|
|
|
func fwFakeWorld(t *testing.T) *fwFake {
|
|
t.Helper()
|
|
f := &fwFake{rules: map[string]bool{}}
|
|
prevFor, prevBy := doorFirewallFor, doorFirewallByID
|
|
t.Cleanup(func() { doorFirewallFor, doorFirewallByID = prevFor, prevBy })
|
|
doorFirewallFor = func(string) (doorFirewall, string) { return f, "" }
|
|
doorFirewallByID = func(string) doorFirewall { return f }
|
|
return f
|
|
}
|
|
|
|
// The window's own door opens the way in while the window is open, and a
|
|
// window that ends with nothing paired takes it away again.
|
|
func TestAPairingWindowOpensTheFirewallAndClosingItRemovesTheRule(t *testing.T) {
|
|
pairingBox(t)
|
|
f := fwFakeWorld(t)
|
|
addr := freeDoorAddr(t)
|
|
port := doorPort(addr)
|
|
_, out, res := openWindowFor(t, addr)
|
|
if !f.open(port) || !strings.Contains(out.String(), "added its own rule in ufw") {
|
|
t.Fatalf("the window is open and the firewall is not: %v\n%s", f.rules, out.String())
|
|
}
|
|
w := loadOpenWindow()
|
|
removeWindow(w.ID) // closed at the terminal, as Ctrl-C does
|
|
select {
|
|
case <-res:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("pair --open did not end")
|
|
}
|
|
if f.open(port) {
|
|
t.Fatal("the rule outlived a window that paired nothing")
|
|
}
|
|
a := pairingAuditText(t)
|
|
if !strings.Contains(a, `"door_firewall_opened"`) || !strings.Contains(a, `"door_firewall_closed"`) {
|
|
t.Fatalf("the audit:\n%s", a)
|
|
}
|
|
// MUTATION: pair --open does not release. RESULT: red, the rule outlived the window.
|
|
}
|
|
|
|
// A pairing made through the window keeps the rule; the daemon's door keeps
|
|
// it while it serves the pairing and when the bastion stops (the pairing has
|
|
// not gone); unpairing removes it.
|
|
func TestThePairingsDoorKeepsTheRuleUntilThePairingGoes(t *testing.T) {
|
|
pairingBox(t)
|
|
laneTestProcess(t)
|
|
// Opened without machine authority, so the Host is not put in charge
|
|
// (anchorAfterWindow) and `unpair` below may remove it: which Host is in
|
|
// charge is TestAServerPairedThroughTheWindowHasThatHostInCharge's.
|
|
prevAuth := machineAuthorityHeld
|
|
machineAuthorityHeld = func() bool { return false }
|
|
t.Cleanup(func() { machineAuthorityHeld = prevAuth })
|
|
f := fwFakeWorld(t)
|
|
addr := freeDoorAddr(t)
|
|
port := doorPort(addr)
|
|
code, _, res := openWindowFor(t, addr)
|
|
ca := newTestCA(t, "home host CA")
|
|
h := newStandinHost(t, ca, "home", "home.cseries.localhost", addr)
|
|
if done, err := h.pair(formatWindowCode(code)); err != nil || done.State != pairingActive {
|
|
t.Fatalf("pair: %+v %v", done, err)
|
|
}
|
|
if err := <-res; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !f.open(port) {
|
|
t.Fatal("the window's end removed the rule its active pairing needs")
|
|
}
|
|
cfgs, _ := loadPairings()
|
|
cfg := pairingFor(cfgs, "home")
|
|
// Its local control on a free port: the machine running the suite may
|
|
// have its own bastion on the default one.
|
|
cfg.ListenAddr = freeDoorAddr(t)
|
|
stop := runPairingBG(t, cfg)
|
|
doorWaitFor(t, 5*time.Second, "the door to bind", func() bool {
|
|
c, err := net.DialTimeout("tcp", addr, time.Second)
|
|
if err == nil {
|
|
_ = c.Close()
|
|
}
|
|
return err == nil
|
|
})
|
|
if err := stop(); err != nil && !errors.Is(err, errPairingNotActive) {
|
|
t.Fatal(err)
|
|
}
|
|
if !f.open(port) {
|
|
t.Fatal("the bastion stopping removed the rule of a pairing that is still there")
|
|
}
|
|
if f.adds != 1 {
|
|
t.Fatalf("%d adds", f.adds)
|
|
}
|
|
if err := cmdUnpair("home"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if f.open(port) {
|
|
t.Fatal("the rule outlived the last pairing")
|
|
}
|
|
// MUTATION: cmdUnpair does not release. RESULT: red, the rule outlived the last pairing.
|
|
// MUTATION: doorPortWanted ignores pairings. RESULT: red, the window's end removed the rule.
|
|
}
|
|
|
|
// The daemon's door alone: bound, it opens the way in; its last pairing
|
|
// stopped and gone, the rule goes with it.
|
|
func TestTheDaemonsDoorRemovesTheRuleWhenItsLastPairingGoes(t *testing.T) {
|
|
pairingBox(t)
|
|
laneTestProcess(t)
|
|
f := fwFakeWorld(t)
|
|
ca := newTestCA(t, "home host CA")
|
|
addr := freeDoorAddr(t)
|
|
cfg := hostDialsPairingFor(t, ca, "home.cseries.localhost", addr) // never saved: gone once it stops
|
|
stop := runPairingBG(t, cfg)
|
|
doorWaitFor(t, 5*time.Second, "the rule", func() bool { return f.open(doorPort(addr)) })
|
|
if err := stop(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
doorWaitFor(t, 5*time.Second, "the rule to go", func() bool { return !f.open(doorPort(addr)) })
|
|
// MUTATION: the door's release does not call doorFirewallRelease. RESULT: red, timed out waiting for the rule to go.
|
|
// MUTATION: bindLoop does not ensure. RESULT: red, timed out waiting for the rule.
|
|
}
|
|
|
|
// fwHung is a firewall whose commands hang until let go.
|
|
type fwHung struct {
|
|
*fwFake
|
|
gate chan struct{}
|
|
}
|
|
|
|
func (f *fwHung) has(port string) (bool, bool, error) { <-f.gate; return f.fwFake.has(port) }
|
|
|
|
// A firewall command that hangs never holds the daemon's door shut: the door
|
|
// serves while the bastion waits on the firewall.
|
|
func TestTheDoorServesWhileTheFirewallHangs(t *testing.T) {
|
|
pairingBox(t)
|
|
laneTestProcess(t)
|
|
f := &fwHung{fwFake: &fwFake{rules: map[string]bool{}}, gate: make(chan struct{})}
|
|
prevFor, prevBy := doorFirewallFor, doorFirewallByID
|
|
t.Cleanup(func() { doorFirewallFor, doorFirewallByID = prevFor, prevBy })
|
|
doorFirewallFor = func(string) (doorFirewall, string) { return f, "" }
|
|
doorFirewallByID = func(string) doorFirewall { return f }
|
|
var once sync.Once
|
|
release := func() { once.Do(func() { close(f.gate) }) }
|
|
t.Cleanup(release)
|
|
ca := newTestCA(t, "home host CA")
|
|
addr := freeDoorAddr(t)
|
|
cfg := hostDialsPairingFor(t, ca, "home.cseries.localhost", addr)
|
|
stop := runPairingBG(t, cfg)
|
|
doorWaitFor(t, 5*time.Second, "the door to answer a handshake", func() bool {
|
|
c, err := net.DialTimeout("tcp", addr, time.Second)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer c.Close()
|
|
_ = c.SetDeadline(time.Now().Add(time.Second))
|
|
err = tls.Client(c, &tls.Config{InsecureSkipVerify: true, ServerName: tcpServerName}).Handshake()
|
|
var ne net.Error
|
|
return err == nil || !(errors.As(err, &ne) && ne.Timeout())
|
|
})
|
|
release()
|
|
doorWaitFor(t, 5*time.Second, "the rule", func() bool { return f.open(doorPort(addr)) })
|
|
if err := stop(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// MUTATION: bindLoop ensures before it serves. RESULT: red, timed out waiting for the door to answer a handshake.
|
|
}
|