Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ead1fbc160 |
@@ -1432,7 +1432,7 @@ func applyAndKeep(ctx context.Context, opts options, raw []byte, signed *store.D
|
|||||||
// (novox/hq ADR 0140). Reported whatever the node's mode: a converged node's filter needs it,
|
// (novox/hq ADR 0140). Reported whatever the node's mode: a converged node's filter needs it,
|
||||||
// and an adopted one becomes converged without a further round trip. A machine that cannot read
|
// and an adopted one becomes converged without a further round trip. A machine that cannot read
|
||||||
// its own routing table says nothing rather than guessing, and is sent no filter.
|
// its own routing table says nothing rather than guessing, and is sent no filter.
|
||||||
if links, err := outward.Links("", ""); err != nil {
|
if links, err := outward.Links(""); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "mesh-host: applied, and could not read which links face outside: %v\n", err)
|
fmt.Fprintf(os.Stderr, "mesh-host: applied, and could not read which links face outside: %v\n", err)
|
||||||
} else {
|
} else {
|
||||||
report.Outward = links
|
report.Outward = links
|
||||||
|
|||||||
@@ -1583,11 +1583,6 @@ func containerSpecReading(r *declaration.Container, declares, reads map[string]s
|
|||||||
for _, n := range r.Networks {
|
for _, n := range r.Networks {
|
||||||
b.WriteString("also-on " + n + "\n")
|
b.WriteString("also-on " + n + "\n")
|
||||||
}
|
}
|
||||||
// And the capabilities it was granted (ADR 0170): one gained or dropped is a different
|
|
||||||
// container, and the runtime cannot change a running one's.
|
|
||||||
for _, c := range r.Capabilities {
|
|
||||||
b.WriteString("cap " + c + "\n")
|
|
||||||
}
|
|
||||||
// The cadence is part of what was declared, so a changed schedule is a changed spec — the marker
|
// The cadence is part of what was declared, so a changed schedule is a changed spec — the marker
|
||||||
// moves and the install is reported "updated" and re-established. Added only when present, so no
|
// moves and the install is reported "updated" and re-established. Added only when present, so no
|
||||||
// ordinary container's or run-once step's digest moves for a field it does not set.
|
// ordinary container's or run-once step's digest moves for a field it does not set.
|
||||||
@@ -1782,9 +1777,6 @@ func applyContainer(ctx context.Context, r *declaration.Container, run Runner,
|
|||||||
if r.Network != "" {
|
if r.Network != "" {
|
||||||
args = append(args, "--network", r.Network)
|
args = append(args, "--network", r.Network)
|
||||||
}
|
}
|
||||||
for _, c := range r.Capabilities {
|
|
||||||
args = append(args, "--cap-add", c)
|
|
||||||
}
|
|
||||||
for _, d := range r.Dns {
|
for _, d := range r.Dns {
|
||||||
args = append(args, "--dns", d)
|
args = append(args, "--dns", d)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,42 +134,3 @@ func TestALeftOutModuleIsNeitherRemovedNorForgotten(t *testing.T) {
|
|||||||
t.Fatalf("keeping the left-out module's container was not said: %+v", report.Outcomes)
|
t.Fatalf("keeping the left-out module's container was not said: %+v", report.Outcomes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A container's capabilities reach the runtime and are part of its spec (novox/hq ADR 0170).
|
|
||||||
func TestACapabilityReachesTheRuntimeAndTheSpec(t *testing.T) {
|
|
||||||
var ran []string
|
|
||||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
|
||||||
if name != "docker" {
|
|
||||||
return "", errors.New("not installed")
|
|
||||||
}
|
|
||||||
switch args[0] {
|
|
||||||
case "info":
|
|
||||||
return "29.0.0\n", nil
|
|
||||||
case "container":
|
|
||||||
return "false\t\n", errors.New("no such container")
|
|
||||||
case "run":
|
|
||||||
ran = args
|
|
||||||
return "deadbeef\n", nil
|
|
||||||
}
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
d := parseTrusted(t, `{"declaration":1,"resources":[
|
|
||||||
{"id":"fw","type":"container","name":"fw","image":"`+pinned+`","network":"host","capabilities":["NET_ADMIN"]}
|
|
||||||
]}`)
|
|
||||||
_, _, _ = Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
|
|
||||||
granted := false
|
|
||||||
for i, a := range ran {
|
|
||||||
if a == "--cap-add" && i+1 < len(ran) && ran[i+1] == "NET_ADMIN" {
|
|
||||||
granted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !granted {
|
|
||||||
t.Fatalf("the capability was not granted: %v", ran)
|
|
||||||
}
|
|
||||||
with := d.Resources[0].(*declaration.Container)
|
|
||||||
without := *with
|
|
||||||
without.Capabilities = nil
|
|
||||||
if containerSpec(with, inputs{}) == containerSpec(&without, inputs{}) {
|
|
||||||
t.Fatal("a capability is not part of the container's spec")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -940,12 +940,6 @@ type Container struct {
|
|||||||
// its siblings can name before any of them can resolve anything.
|
// its siblings can name before any of them can resolve anything.
|
||||||
Dns []string `json:"dns,omitempty"`
|
Dns []string `json:"dns,omitempty"`
|
||||||
|
|
||||||
// Capabilities are the Linux capabilities this container is granted beyond the runtime's
|
|
||||||
// default set, by name (novox/hq ADR 0170): a holder's runtime that changes the machine's packet
|
|
||||||
// filter asks for NET_ADMIN. Exactly these, named in the spec so a change recreates the
|
|
||||||
// container; a privileged container stays undeclarable.
|
|
||||||
Capabilities []string `json:"capabilities,omitempty"`
|
|
||||||
|
|
||||||
// Networks are networks this container also joins once created, by name — a found network a
|
// Networks are networks this container also joins once created, by name — a found network a
|
||||||
// per-machine setting keeps for a taken container (novox/hq ADR 0163, rule 4), so a
|
// per-machine setting keeps for a taken container (novox/hq ADR 0163, rule 4), so a
|
||||||
// neighbour that resolves it there keeps resolving it until the neighbour is taken too.
|
// neighbour that resolves it there keeps resolving it until the neighbour is taken too.
|
||||||
@@ -1045,12 +1039,6 @@ func (c *Container) validate(where string, _ bool) []string {
|
|||||||
"static address anywhere but a user-defined one")
|
"static address anywhere but a user-defined one")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, cap := range c.Capabilities {
|
|
||||||
if !capabilityName.MatchString(cap) {
|
|
||||||
problems = append(problems, where+": capabilities names "+strconv.Quote(cap)+", which is not a "+
|
|
||||||
"capability's name (CAP_NET_ADMIN or NET_ADMIN)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, n := range c.Networks {
|
for _, n := range c.Networks {
|
||||||
problems = append(problems, (&Network{Name: n}).validate(where+": networks", false)...)
|
problems = append(problems, (&Network{Name: n}).validate(where+": networks", false)...)
|
||||||
if n == c.Network {
|
if n == c.Network {
|
||||||
@@ -1221,10 +1209,6 @@ type Adoption struct {
|
|||||||
Untaken map[string][]string `json:"untaken,omitempty"`
|
Untaken map[string][]string `json:"untaken,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// capabilityName is what a Linux capability is called: upper case, underscores, an optional CAP_
|
|
||||||
// prefix. The runtime accepts either spelling.
|
|
||||||
var capabilityName = regexp.MustCompile(`^(CAP_)?[A-Z][A-Z0-9_]*$`)
|
|
||||||
|
|
||||||
// AdoptionPrefix is the id prefix of what the mesh itself declares because a node is adopted —
|
// AdoptionPrefix is the id prefix of what the mesh itself declares because a node is adopted —
|
||||||
// its openings and its guard. Nothing under it belongs to a module, so none of it is ever held.
|
// its openings and its guard. Nothing under it belongs to a module, so none of it is ever held.
|
||||||
const AdoptionPrefix = "adoption."
|
const AdoptionPrefix = "adoption."
|
||||||
|
|||||||
@@ -504,23 +504,3 @@ func TestKeptNetworksAndLeftOutModulesAreReadStrictly(t *testing.T) {
|
|||||||
t.Fatalf("a carried bundle leaving modules out was accepted: %v", err)
|
t.Fatalf("a carried bundle leaving modules out was accepted: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A container may ask for a capability by name, and nothing else (novox/hq ADR 0170).
|
|
||||||
func TestACapabilityIsNamedOrRefused(t *testing.T) {
|
|
||||||
image := "postgres@sha256:" + strings.Repeat("a", 64)
|
|
||||||
d, err := Parse([]byte(`{"declaration":1,"resources":[
|
|
||||||
{"id":"fw","type":"container","name":"fw","image":"` + image + `","network":"host","capabilities":["NET_ADMIN","CAP_NET_RAW"]}
|
|
||||||
]}`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if got := d.Resources[0].(*Container).Capabilities; len(got) != 2 || got[0] != "NET_ADMIN" {
|
|
||||||
t.Fatalf("capabilities read as %v", got)
|
|
||||||
}
|
|
||||||
for _, bad := range []string{`"net_admin"`, `"ALL;rm -rf /"`, `"privileged"`} {
|
|
||||||
if _, err := Parse([]byte(`{"declaration":1,"resources":[
|
|
||||||
{"id":"fw","type":"container","name":"fw","image":"` + image + `","capabilities":[` + bad + `]}]}`)); err == nil {
|
|
||||||
t.Errorf("%s was accepted as a capability", bad)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -29,32 +29,17 @@ import (
|
|||||||
// routing table without one.
|
// routing table without one.
|
||||||
const ProcNet = "/proc/net"
|
const ProcNet = "/proc/net"
|
||||||
|
|
||||||
// SysClassNet is where the kernel lists the machine's network interfaces, one directory each. A
|
// Links are the interfaces carrying a default route, for both address families, sorted and without
|
||||||
// parameter for the same reason.
|
// repeats.
|
||||||
const SysClassNet = "/sys/class/net"
|
|
||||||
|
|
||||||
// Links are the interfaces carrying a default route, for both address families, and every interface
|
|
||||||
// backed by a physical device, sorted and without repeats.
|
|
||||||
//
|
|
||||||
// **A physical link faces outside whether or not it is up** (novox/hq issue 197). The filter accepts
|
|
||||||
// whatever did not arrive on a link named here, so a link left out of this list is not filtered at
|
|
||||||
// all. A cable unplugged when the machine last reported carries no default route, and was left out:
|
|
||||||
// plugged in, everything arriving on it was accepted until the next report and the next push — and a
|
|
||||||
// second physical link that never carries the default route was never filtered. A physical device is
|
|
||||||
// read from the kernel's own list, where it has a `device` entry; a bridge, a veth, the tunnel and the
|
|
||||||
// loopback have none, and stay what they are, this machine's own.
|
|
||||||
//
|
//
|
||||||
// A machine may have more than one: a laptop with a cable and a radio has two, and both face
|
// A machine may have more than one: a laptop with a cable and a radio has two, and both face
|
||||||
// outside. A machine with none — no route off itself — returns nothing, and the mesh refuses to
|
// outside. A machine with none — no route off itself — returns nothing, and the mesh refuses to
|
||||||
// compose a filter for it rather than writing a rule around a link with no name, which would be a
|
// compose a filter for it rather than writing a rule around a link with no name, which would be a
|
||||||
// rule set that does not load and a machine filtering nothing while its unit reports success.
|
// rule set that does not load and a machine filtering nothing while its unit reports success.
|
||||||
func Links(procNet, sysClassNet string) ([]string, error) {
|
func Links(procNet string) ([]string, error) {
|
||||||
if procNet == "" {
|
if procNet == "" {
|
||||||
procNet = ProcNet
|
procNet = ProcNet
|
||||||
}
|
}
|
||||||
if sysClassNet == "" {
|
|
||||||
sysClassNet = SysClassNet
|
|
||||||
}
|
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
|
|
||||||
four, err := defaultsV4(filepath.Join(procNet, "route"))
|
four, err := defaultsV4(filepath.Join(procNet, "route"))
|
||||||
@@ -65,11 +50,7 @@ func Links(procNet, sysClassNet string) ([]string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
devices, err := physical(sysClassNet)
|
for _, name := range append(four, six...) {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, name := range append(append(four, six...), devices...) {
|
|
||||||
if name != "" && name != "lo" {
|
if name != "" && name != "lo" {
|
||||||
seen[name] = true
|
seen[name] = true
|
||||||
}
|
}
|
||||||
@@ -83,25 +64,6 @@ func Links(procNet, sysClassNet string) ([]string, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// physical is every interface the kernel lists with a device behind it. A list that is not there is
|
|
||||||
// not an error — a machine without sysfs mounted reports what its routing table says, as before.
|
|
||||||
func physical(sysClassNet string) ([]string, error) {
|
|
||||||
entries, err := os.ReadDir(sysClassNet)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var out []string
|
|
||||||
for _, e := range entries {
|
|
||||||
if _, err := os.Stat(filepath.Join(sysClassNet, e.Name(), "device")); err == nil {
|
|
||||||
out = append(out, e.Name())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// defaultsV4 reads /proc/net/route, whose columns are
|
// defaultsV4 reads /proc/net/route, whose columns are
|
||||||
//
|
//
|
||||||
// Iface Destination Gateway Flags RefCnt Use Metric Mask ...
|
// Iface Destination Gateway Flags RefCnt Use Metric Mask ...
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func TestLinksAreTheOnesCarryingADefaultRoute(t *testing.T) {
|
|||||||
write(t, dir, "route", routeV4)
|
write(t, dir, "route", routeV4)
|
||||||
write(t, dir, "ipv6_route", routeV6)
|
write(t, dir, "ipv6_route", routeV6)
|
||||||
|
|
||||||
got, err := Links(dir, t.TempDir())
|
got, err := Links(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ func TestAZeroDestinationWithAMaskIsNotADefaultRoute(t *testing.T) {
|
|||||||
write(t, dir, "route", `Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
|
write(t, dir, "route", `Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
|
||||||
br-abc 00000000 00000000 0001 0 0 0 00FFFFFF 0 0 0
|
br-abc 00000000 00000000 0001 0 0 0 00FFFFFF 0 0 0
|
||||||
`)
|
`)
|
||||||
got, err := Links(dir, t.TempDir())
|
got, err := Links(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ br-abc 00000000 00000000 0001 0 0 0 00FFFFFF 0 0 0
|
|||||||
func TestNoDefaultRouteIsNoLinks(t *testing.T) {
|
func TestNoDefaultRouteIsNoLinks(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
write(t, dir, "route", "Iface\tDestination\tGateway \tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT\n")
|
write(t, dir, "route", "Iface\tDestination\tGateway \tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT\n")
|
||||||
got, err := Links(dir, t.TempDir())
|
got, err := Links(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ func TestNoDefaultRouteIsNoLinks(t *testing.T) {
|
|||||||
func TestAMissingTableIsNotAFailure(t *testing.T) {
|
func TestAMissingTableIsNotAFailure(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
write(t, dir, "route", routeV4)
|
write(t, dir, "route", routeV4)
|
||||||
got, err := Links(dir, t.TempDir())
|
got, err := Links(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("a missing v6 table should not fail: %v", err)
|
t.Fatalf("a missing v6 table should not fail: %v", err)
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ func TestALinkIsReportedOnce(t *testing.T) {
|
|||||||
write(t, dir, "ipv6_route",
|
write(t, dir, "ipv6_route",
|
||||||
"00000000000000000000000000000000 00 00000000000000000000000000000000 00 "+
|
"00000000000000000000000000000000 00 00000000000000000000000000000000 00 "+
|
||||||
"fe800000000000000000000000000001 00000400 00000001 00000000 00000003 enp9s0\n")
|
"fe800000000000000000000000000001 00000400 00000001 00000000 00000003 enp9s0\n")
|
||||||
got, err := Links(dir, t.TempDir())
|
got, err := Links(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -109,7 +109,7 @@ func TestALinkIsReportedOnce(t *testing.T) {
|
|||||||
// Against this machine's own routing table, so the parse is held to what the kernel actually writes
|
// Against this machine's own routing table, so the parse is held to what the kernel actually writes
|
||||||
// and not only to a fixture written to agree with it.
|
// and not only to a fixture written to agree with it.
|
||||||
func TestAgainstThisMachinesOwnTable(t *testing.T) {
|
func TestAgainstThisMachinesOwnTable(t *testing.T) {
|
||||||
got, err := Links("", "")
|
got, err := Links("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -118,39 +118,3 @@ func TestAgainstThisMachinesOwnTable(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Logf("this machine's outward links: %v", got)
|
t.Logf("this machine's outward links: %v", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sysNet is a /sys/class/net: each name a directory, with a `device` entry when a device backs it.
|
|
||||||
func sysNet(t *testing.T, physical []string, virtual []string) string {
|
|
||||||
t.Helper()
|
|
||||||
dir := t.TempDir()
|
|
||||||
for _, name := range physical {
|
|
||||||
if err := os.MkdirAll(filepath.Join(dir, name, "device"), 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, name := range virtual {
|
|
||||||
if err := os.MkdirAll(filepath.Join(dir, name), 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dir
|
|
||||||
}
|
|
||||||
|
|
||||||
// **A physical link faces outside whether or not it carries the default route** (novox/hq issue
|
|
||||||
// 197). A machine on its radio with its cable unplugged reported only the radio, and the filter then
|
|
||||||
// accepted everything arriving on the cable the moment it was plugged in. Bridges, veths, the tunnel
|
|
||||||
// and the loopback have no device behind them and stay this machine's own.
|
|
||||||
func TestEveryPhysicalLinkFacesOutsideUpOrDown(t *testing.T) {
|
|
||||||
proc := t.TempDir()
|
|
||||||
write(t, proc, "route", `Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
|
|
||||||
wlp5s0 00000000 01FEA8C0 0003 0 0 600 00000000 0 0 0
|
|
||||||
`)
|
|
||||||
sys := sysNet(t, []string{"wlp5s0", "enp6s0"}, []string{"lo", "docker0", "br-0123456789ab", "veth1", "mesh0"})
|
|
||||||
got, err := Links(proc, sys)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if want := []string{"enp6s0", "wlp5s0"}; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("outward links are %v, want %v", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ const (
|
|||||||
CapServiceManager = "service-manager"
|
CapServiceManager = "service-manager"
|
||||||
CapFirewall = "firewall"
|
CapFirewall = "firewall"
|
||||||
CapOverlay = "overlay"
|
CapOverlay = "overlay"
|
||||||
// CapVirtualisation is a running virtualisation daemon: what the lab raises its machines on
|
|
||||||
// (novox/hq ADR 0172), and what grants a module the daemon's socket.
|
|
||||||
CapVirtualisation = "virtualisation"
|
|
||||||
CapGraphicalSession = "graphical-session"
|
CapGraphicalSession = "graphical-session"
|
||||||
// CapSeat is hardware: somewhere a display server COULD run. CapGraphicalSession above is
|
// CapSeat is hardware: somewhere a display server COULD run. CapGraphicalSession above is
|
||||||
// state: whether one IS running. Assignment needs the first.
|
// state: whether one IS running. Assignment needs the first.
|
||||||
@@ -208,11 +205,6 @@ func Default(runner Runner) []Detector {
|
|||||||
why: "lists the ruleset — needs the tool AND the privilege to use it",
|
why: "lists the ruleset — needs the tool AND the privilege to use it",
|
||||||
runner: runner,
|
runner: runner,
|
||||||
},
|
},
|
||||||
commandCapability{
|
|
||||||
name: CapVirtualisation, command: "incus", args: []string{"info"},
|
|
||||||
why: "asks the virtualisation daemon about itself — a running daemon, not an installed client",
|
|
||||||
runner: runner,
|
|
||||||
},
|
|
||||||
commandCapability{
|
commandCapability{
|
||||||
name: CapOverlay, command: "wg", args: []string{"show", "interfaces"},
|
name: CapOverlay, command: "wg", args: []string{"show", "interfaces"},
|
||||||
why: "asks the kernel for interfaces — needs the module, not just the tool",
|
why: "asks the kernel for interfaces — needs the module, not just the tool",
|
||||||
|
|||||||
Reference in New Issue
Block a user