bastion 0.55.3
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m51s
ci / fuzz (push) Successful in 2m55s

This commit is contained in:
Privatae
2026-10-25 23:16:45 +00:00
parent 77b7853ae1
commit 8822d5df85
136 changed files with 8638 additions and 1641 deletions
+325
View File
@@ -0,0 +1,325 @@
//go:build linux
package main
import (
"context"
"encoding/binary"
"errors"
"net"
"os"
"os/exec"
"strings"
"syscall"
"testing"
"golang.org/x/sys/unix"
)
// The rule alone, over link tables written out here: which links keep a
// packet on this machine.
func TestInsideMachineReadsTheLinkAndABridgesPorts(t *testing.T) {
links := map[uint32]linkInfo{
1: {kind: ""}, // lo
2: {kind: ""}, // a network card
3: {kind: "bridge"}, // docker0
4: {kind: "veth", master: 3}, // a container's link on docker0
5: {kind: "veth"}, // a pod's link, routed to directly (Calico)
6: {kind: "bridge"}, // virbr0
7: {kind: "dummy", master: 6}, // libvirt's placeholder port
8: {kind: "tun", master: 6}, // a VM's tap
9: {kind: "bridge"}, // br0 on a server: the LAN
10: {kind: "", master: 9}, // its network card
11: {kind: "veth", master: 9}, // and a container bridged onto the LAN
12: {kind: "bridge"}, // a Docker network with no container yet
13: {kind: "wireguard"}, // a VPN
14: {kind: "vxlan"}, // a tunnel to other machines (flannel.1)
15: {kind: "bridge"}, // a bridge over a VLAN of the card
16: {kind: "vlan", master: 15}, //
17: {kind: "bridge"}, // a bridge with a bridge as its port
18: {kind: "bridge", master: 17}, //
19: {kind: "tun"}, // a VPN's tun
20: {kind: "macvlan"}, // a macvlan over the card
}
for oif, want := range map[uint32]bool{
1: false, 2: false, 3: true, 4: true, 5: true, 6: true, 9: false, 12: true,
13: false, 14: false, 15: false, 17: false, 19: false, 20: false,
99: false, // not in the table: linkInsideMachine refuses it before asking
} {
if got := insideMachine(links, oif); got != want {
t.Errorf("link %d (%q): inside this machine = %v, want %v", oif, links[oif].kind, got, want)
}
}
}
// A link dump's answers, written out here: the kind and master of each link,
// NLMSG_DONE ends it, and a dump that ended on an error (the status NLMSG_DONE
// carries) or was interrupted is not taken as the whole table.
//
// MUTATION: return done at NLMSG_DONE without reading its status.
// RESULT: red — a dump that stopped on EMSGSIZE, missing its later links, is
// taken as the table.
func TestReadLinkDumpReadsKindsMastersAndTheDumpsStatus(t *testing.T) {
ne := binary.NativeEndian
attr := func(typ uint16, val []byte) []byte {
b := make([]byte, (unix.SizeofRtAttr+len(val)+3)&^3)
ne.PutUint16(b[0:], uint16(unix.SizeofRtAttr+len(val)))
ne.PutUint16(b[2:], typ)
copy(b[unix.SizeofRtAttr:], val)
return b
}
u32 := func(v uint32) []byte { b := make([]byte, 4); ne.PutUint32(b, v); return b }
link := func(seq, idx, master uint32, kind string, flags uint16) syscall.NetlinkMessage {
d := make([]byte, unix.SizeofIfInfomsg)
ne.PutUint32(d[4:], idx)
if kind != "" {
d = append(d, attr(unix.IFLA_LINKINFO, attr(unix.IFLA_INFO_KIND, append([]byte(kind), 0)))...)
}
if master != 0 {
d = append(d, attr(unix.IFLA_MASTER, u32(master))...)
}
return syscall.NetlinkMessage{Header: syscall.NlMsghdr{Type: unix.RTM_NEWLINK, Flags: flags, Seq: seq}, Data: d}
}
doneWith := func(seq uint32, errno syscall.Errno) syscall.NetlinkMessage {
return syscall.NetlinkMessage{Header: syscall.NlMsghdr{Type: unix.NLMSG_DONE, Seq: seq}, Data: u32(uint32(-int32(errno)))}
}
links := map[uint32]linkInfo{}
done, intr, err := readLinkDump([]syscall.NetlinkMessage{
link(7, 3, 0, "bridge", 0), link(7, 4, 3, "veth", 0), link(7, 2, 0, "", 0),
link(8, 9, 0, "bridge", 0), // another request's answer
doneWith(7, 0),
}, 7, links)
if err != nil || !done || intr {
t.Fatalf("a whole dump: done %v interrupted %v err %v", done, intr, err)
}
want := map[uint32]linkInfo{3: {kind: "bridge"}, 4: {kind: "veth", master: 3}, 2: {}}
if len(links) != len(want) {
t.Fatalf("links %v, want %v", links, want)
}
for i, l := range want {
if links[i] != l {
t.Errorf("link %d: %+v, want %+v", i, links[i], l)
}
}
if done, _, err := readLinkDump([]syscall.NetlinkMessage{link(7, 3, 0, "bridge", 0), doneWith(7, unix.EMSGSIZE)},
7, map[uint32]linkInfo{}); err == nil || done {
t.Errorf("a dump that ended on EMSGSIZE is an error, got done %v err %v", done, err)
}
if _, intr, err := readLinkDump([]syscall.NetlinkMessage{link(7, 3, 0, "bridge", unix.NLM_F_MULTI|unix.NLM_F_DUMP_INTR)},
7, map[uint32]linkInfo{}); err != nil || !intr {
t.Errorf("an answer marked interrupted marks the dump, got %v %v", intr, err)
}
if done, intr, err := readLinkDump([]syscall.NetlinkMessage{link(7, 3, 0, "bridge", 0)}, 7, map[uint32]linkInfo{}); done || intr || err != nil {
t.Errorf("a batch without NLMSG_DONE is not the end: %v %v %v", done, intr, err)
}
errMsg := doneWith(7, unix.EPERM)
errMsg.Header.Type = unix.NLMSG_ERROR
if _, _, err := readLinkDump([]syscall.NetlinkMessage{errMsg}, 7, map[uint32]linkInfo{}); !errors.Is(err, unix.EPERM) {
t.Errorf("an NLMSG_ERROR in the dump is its error, got %v", err)
}
}
const insideNetnsEnv = "PRIVATAE_TEST_INSIDE_NETNS"
// THE KERNEL'S OWN ANSWER FOR NETWORKS INSIDE THIS MACHINE. The local egress
// lane keeps the person's LAN, but a container or VM on a network made on
// this machine is a service on this machine, reached around its switches the
// same way loopback is. In a private user and network namespace this builds
// the networks Docker, libvirt and Calico make (a bridge whose only port is a
// veth, a bridge whose ports are a dummy and nothing else, a bridge with no
// port yet, a veth routed to directly, and the same with an IPv6 range and a
// public range), and links that leave the machine: a bridge over a VLAN,
// which stands for a bridge over the network card; a route straight over a
// dummy, which stands for the card itself; veths that carry this machine's
// main-table default routes, as a container's own veth does; and routes
// through a gateway. It asks the real rtnetlink lookup, the local egress lane
// on an unanchored box, a coder run's proxy and the backup lanes' wall.
//
// MUTATION: return routeLeaves for every unicast route (drop the link check).
// RESULT: red — every inside address passes the local lane, the public one
// passes the coder lane.
// MUTATION: count "dummy" as an outward port.
// RESULT: red — the libvirt-style bridge is the LAN.
// MUTATION: drop the main-table default-route links from the judgement.
// RESULT: red — from a machine that is itself a container, the network on its
// own veths (IPv4, a multipath default's second veth, IPv6) is this machine.
// MUTATION: skip a default route's RTA_MULTIPATH nexthops.
// RESULT: red — both veths of the multipath default are inside.
// MUTATION: count a default route in any table, not only main.
// RESULT: red — the veth whose default is in table 100 leaves.
// MUTATION: judge the out link of a route through a gateway too.
// RESULT: red — from a machine that is itself a container, the internet
// (IPv4 and IPv6) and a LAN behind a router VM are this machine, and every
// lane refuses them.
func TestNetworksInsideThisMachineAreThisMachine(t *testing.T) {
if os.Getenv(insideNetnsEnv) != "1" {
if _, err := exec.LookPath("ip"); err != nil {
if _, err := os.Stat("/usr/sbin/ip"); err != nil {
t.Skip("needs ip(8)")
}
}
cmd := exec.Command(os.Args[0], "-test.run=^TestNetworksInsideThisMachineAreThisMachine$", "-test.v", "-test.count=1")
cmd.Env = append(os.Environ(), insideNetnsEnv+"=1")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUSER | syscall.CLONE_NEWNET,
UidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: os.Getuid(), Size: 1}},
GidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: os.Getgid(), Size: 1}},
}
out, err := cmd.CombinedOutput()
if cmd.ProcessState == nil {
t.Skipf("no private user and network namespace here: %v", err)
}
t.Logf("in a private network namespace:\n%s", out)
if strings.Contains(string(out), "--- SKIP") {
t.Skip("the namespaced run could not build its networks")
}
if err != nil {
t.Fatalf("the namespaced run failed: %v", err)
}
if !strings.Contains(string(out), "--- PASS: TestNetworksInsideThisMachineAreThisMachine") {
t.Fatal("the namespaced run did not run")
}
return
}
ipBin, err := exec.LookPath("ip")
if err != nil {
ipBin = "/usr/sbin/ip"
}
for _, args := range [][]string{
{"link", "set", "lo", "up"},
// Docker: a bridge whose only port is a container's veth, with an IPv6
// range too, and a second network on a public range.
{"link", "add", "br-ctr", "type", "bridge"},
{"link", "add", "vh0", "type", "veth", "peer", "name", "vc0"},
{"link", "set", "vh0", "master", "br-ctr"},
{"addr", "add", "172.31.5.1/24", "dev", "br-ctr"},
{"-6", "addr", "add", "2001:db8:5::1/64", "dev", "br-ctr", "nodad"},
{"link", "add", "br-pub", "type", "bridge"},
{"link", "add", "vh1", "type", "veth", "peer", "name", "vc1"},
{"link", "set", "vh1", "master", "br-pub"},
{"addr", "add", "203.0.113.1/24", "dev", "br-pub"},
// libvirt: a bridge whose port is a dummy.
{"link", "add", "br-vm", "type", "bridge"},
{"link", "add", "dvm0", "type", "dummy"},
{"link", "set", "dvm0", "master", "br-vm"},
{"addr", "add", "192.168.122.1/24", "dev", "br-vm"},
// A Docker network no container has joined yet.
{"link", "add", "br-empty", "type", "bridge"},
{"addr", "add", "172.29.0.1/24", "dev", "br-empty"},
// Calico: a pod's veth, routed to directly (the route is added once
// the link is up, below).
{"link", "add", "cali0", "type", "veth", "peer", "name", "cali0p"},
// The LAN: a bridge over a VLAN of a card, and the card itself.
{"link", "add", "dlan0", "type", "dummy"},
{"link", "add", "link", "dlan0", "name", "dlan0.7", "type", "vlan", "id", "7"},
{"link", "add", "br-lan", "type", "bridge"},
{"link", "set", "dlan0.7", "master", "br-lan"},
{"addr", "add", "10.77.0.1/24", "dev", "br-lan"},
// The way out through a veth: this machine is itself a container, and
// its default routes go through its veths to a gateway, the machine it
// runs on: IPv4 over two (a multipath default), IPv6 over a third. A
// veth that carries a main-table default leads out of this machine, so
// the network on it is the LAN, not inside.
{"link", "add", "ctr0", "type", "veth", "peer", "name", "ctr0p"},
{"addr", "add", "10.88.0.2/24", "dev", "ctr0"},
{"link", "add", "ctr1", "type", "veth", "peer", "name", "ctr1p"},
{"addr", "add", "10.89.0.2/24", "dev", "ctr1"},
{"link", "add", "ctr6", "type", "veth", "peer", "name", "ctr6p"},
{"-6", "addr", "add", "2001:db8:88::2/64", "dev", "ctr6", "nodad"},
// A veth with a default route in another table only: still inside.
{"link", "add", "pbr0", "type", "veth", "peer", "name", "pbr0p"},
{"addr", "add", "10.91.0.2/24", "dev", "pbr0"},
// A range routed through a veth with this machine's own address on it
// as the gateway: the kernel answers it as on the link, so it is
// inside, whatever the gateway.
{"link", "add", "cilh", "type", "veth", "peer", "name", "ciln"},
{"addr", "add", "10.66.0.191/32", "dev", "cilh"},
} {
if out, err := exec.Command(ipBin, args...).CombinedOutput(); err != nil {
t.Skipf("ip %s: %v %s", strings.Join(args, " "), err, out)
}
}
for _, l := range []string{"br-ctr", "vh0", "vc0", "br-pub", "vh1", "vc1", "br-vm", "dvm0", "br-empty",
"cali0", "cali0p", "dlan0", "dlan0.7", "br-lan", "ctr0", "ctr0p", "ctr1", "ctr1p", "ctr6", "ctr6p",
"pbr0", "pbr0p", "cilh", "ciln"} {
if out, err := exec.Command(ipBin, "link", "set", l, "up").CombinedOutput(); err != nil {
t.Skipf("ip link set %s up: %v %s", l, err, out)
}
}
// The card: a link with no kind cannot be made here, so a route over the
// VLAN's lower link stands for it. A dummy as the route's own link leaves:
// only a bridge's ports are read for dummies.
for _, args := range [][]string{
{"route", "add", "172.30.9.0/24", "dev", "cali0"},
{"route", "add", "192.168.50.0/24", "dev", "dlan0"},
// Through a gateway on an inside link, the packet goes where that
// gateway sends it: the internet from a container, a LAN behind a
// router VM on a bridge of taps.
{"route", "add", "default", "nexthop", "via", "10.88.0.1", "dev", "ctr0", "nexthop", "via", "10.89.0.1", "dev", "ctr1"},
{"-6", "route", "add", "default", "via", "2001:db8:88::1", "dev", "ctr6"},
{"route", "add", "default", "via", "10.91.0.1", "dev", "pbr0", "table", "100"},
{"route", "add", "192.168.60.0/24", "via", "192.168.122.254", "dev", "br-vm"},
{"route", "add", "10.66.0.0/24", "via", "10.66.0.191", "dev", "cilh"},
} {
if out, err := exec.Command(ipBin, args...).CombinedOutput(); err != nil {
t.Skipf("ip %s: %v %s", strings.Join(args, " "), err, out)
}
}
ownAddrReset()
inside := []string{"172.31.5.9", "2001:db8:5::9", "203.0.113.9", "192.168.122.9", "172.29.0.9", "172.30.9.9",
"10.66.0.9", "10.91.0.9"}
leaves := []string{"10.77.0.9", "192.168.50.9", "198.51.100.9", "2001:db8:ff::9", "192.168.60.9",
"10.88.0.9", "10.89.0.9", "2001:db8:88::9"}
for _, a := range inside {
if got, err := ownRouteLookup(net.ParseIP(a)); err != nil || got != routeThisMachine {
t.Errorf("route lookup %s: got kind %d err %v, want this machine", a, got, err)
}
}
for _, a := range leaves {
if got, err := ownRouteLookup(net.ParseIP(a)); err != nil || got != routeLeaves {
t.Errorf("route lookup %s: got kind %d err %v, want leaves", a, got, err)
}
}
// The local egress lane on an unanchored box: an inside address, by address
// and by name, is refused as this machine; the LAN passes.
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
if currentAnchor() != nil || boxMode() != modeLocal {
t.Fatal("the namespaced run must be a local, unanchored box")
}
names := map[string][]string{"db.example.test": {"172.31.5.9"}, "vm.example.test": {"192.168.122.9"},
"nas.example.test": {"10.77.0.9"}}
stubEgressResolve(t, names)
ctx := context.Background()
for _, hp := range []string{"172.31.5.9:5432", "[2001:db8:5::9]:5432", "203.0.113.9:443", "192.168.122.9:22",
"172.29.0.9:80", "172.30.9.9:8080", "10.66.0.9:80", "10.91.0.9:80", "db.example.test:5432", "vm.example.test:22"} {
if got, why := egressVettedAddrs(ctx, hp); !strings.Contains(why, "that is this machine") {
t.Errorf("local egress: %s is on a network inside this machine and must be refused as this machine, got %v %q", hp, got, why)
}
}
for hp, want := range map[string]string{"10.77.0.9:445": "10.77.0.9:445", "192.168.50.9:631": "192.168.50.9:631",
"nas.example.test:445": "10.77.0.9:445", "198.51.100.9:443": "198.51.100.9:443",
"[2001:db8:ff::9]:443": "[2001:db8:ff::9]:443", "192.168.60.9:445": "192.168.60.9:445",
"10.88.0.9:80": "10.88.0.9:80", "10.89.0.9:80": "10.89.0.9:80", "[2001:db8:88::9]:80": "[2001:db8:88::9]:80"} {
if got, why := egressVettedAddrs(ctx, hp); why != "" || len(got) != 1 || got[0] != want {
t.Errorf("local egress: %s is the LAN and passes as %s: %v %q", hp, want, got, why)
}
}
// A coder run's proxy refuses the public range on an inside network too
// (its private ranges it refuses by class already).
if got, why := coderVettedAddr(ctx, "203.0.113.9:443", nil); !strings.Contains(why, "never this machine") {
t.Errorf("coder run: 203.0.113.9 is on a network inside this machine, got dial %q, %q", got, why)
}
// From a machine that is itself a container, the internet is through its
// veth: a coder run's proxy and the backup lanes still reach it.
if got, why := coderVettedAddr(ctx, "198.51.100.9:443", nil); why != "" || got != "198.51.100.9:443" {
t.Errorf("coder run: 198.51.100.9 is through the gateway and passes, got dial %q, %q", got, why)
}
if !backupAddrAllowed(net.ParseIP("2001:db8:ff::9")) || !backupAddrAllowed(net.ParseIP("198.51.100.9")) {
t.Error("backup: an address through the gateway leaves this machine and is allowed")
}
}