A re-run's apply writes the bundle's one-entry user list over the list placed at enrolment, and an enrolled machine skipped placing it: enrolled was taken for adopted, against ADR 0331 §5. Until the controller knows the nats module, the installer places the composed list again. Also drop the previous broker's AMQP and management ports, which nothing raises any more, and say why the bus always carries a ports setting.
234 lines
8.9 KiB
Go
234 lines
8.9 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
)
|
|
|
|
// Defends novox/hq ADR 0100: an adopted genesis loads no table that drops by default or holds an
|
|
// accept; the mesh guards its own ports in a table that only refuses; and genesis takes the mesh's
|
|
// own modules as it installs them, and nothing else.
|
|
|
|
// The same golden text the controller's test holds its AsGuard to.
|
|
const goldenGuard = `table inet mesh_guard {}
|
|
delete table inet mesh_guard
|
|
table inet mesh_guard {
|
|
chain prerouting {
|
|
type filter hook prerouting priority raw; policy accept;
|
|
fib daddr type local iifname != "lo" iifname != "docker0" iifname != "br-*" iifname != "mesh0" tcp dport { 5432, 15672 } drop
|
|
}
|
|
}
|
|
`
|
|
|
|
func TestTheGuardIsExactlyThisTable(t *testing.T) {
|
|
if got := AsGuard([]int{15672, 5432}); got != goldenGuard {
|
|
t.Fatalf("the guard changed:\n%s", got)
|
|
}
|
|
}
|
|
|
|
// The same golden unit the controller's test holds its guard unit to. It is loaded before the
|
|
// network is up, so it carries no default dependencies, and it is stopped only at shutdown.
|
|
const goldenGuardUnit = `[Unit]
|
|
Description=The mesh's guard: refuses its own ports from outside (novox/hq ADR 0100)
|
|
DefaultDependencies=no
|
|
Wants=network-pre.target
|
|
Before=network-pre.target shutdown.target
|
|
Conflicts=shutdown.target
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
RemainAfterExit=yes
|
|
ExecStart=nft -f /etc/mesh/guard.nft
|
|
ExecReload=nft -f /etc/mesh/guard.nft
|
|
ExecStop=nft delete table inet mesh_guard
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
`
|
|
|
|
func TestTheGuardUnitIsExactlyThisUnit(t *testing.T) {
|
|
if got := guardUnitText(); got != goldenGuardUnit {
|
|
t.Fatalf("the guard's unit changed:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestTheGuardRefusesOnlyWhatIsAddressedToThisMachine(t *testing.T) {
|
|
// A machine that routes for others — a predecessor's private-network hub — must not have a
|
|
// packet for another machine's database port refused (novox/hq ADR 0103).
|
|
for _, line := range strings.Split(AsGuard([]int{5432}), "\n") {
|
|
if strings.Contains(line, " drop") && !strings.HasPrefix(strings.TrimSpace(line), "fib daddr type local ") {
|
|
t.Errorf("a refusal matches packets not addressed to this machine: %q", line)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAnAdoptedBundleLoadsNoDroppingTableAndExactlyTheGuard(t *testing.T) {
|
|
r := producedBundle(t)
|
|
p := FoundationPorts{Store: 5433}
|
|
if _, err := RewritePorts(&r, p, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := RewriteAdopted(&r, p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Join(got.Removed, ",") != "base-filter-loaded,base-filter" {
|
|
t.Errorf("removed %v", got.Removed)
|
|
}
|
|
at := map[string]int{}
|
|
var guards []*declaration.File
|
|
for i, res := range r.Declaration.Resources {
|
|
at[res.Identity()] = i
|
|
if f, ok := res.(*declaration.File); ok {
|
|
if strings.Contains(f.Content, "policy drop") || strings.Contains(f.Content, " accept\n") &&
|
|
!strings.Contains(f.Content, "policy accept") {
|
|
t.Errorf("%s loads a table that drops or accepts: %q", f.ID, f.Content)
|
|
}
|
|
if f.Path == guardPath {
|
|
guards = append(guards, f)
|
|
}
|
|
}
|
|
if s, ok := res.(*declaration.Service); ok && s.Unit == "nftables.service" {
|
|
t.Errorf("the foundation's filter is still loaded by %s", s.ID)
|
|
}
|
|
}
|
|
if len(guards) != 1 {
|
|
t.Fatalf("%d guard table(s)", len(guards))
|
|
}
|
|
// The store's: the one foundation port the filter admits from the private network only
|
|
// (novox/hq ADR 0103). The bus is reached from anywhere, its monitoring only on loopback.
|
|
if !strings.Contains(guards[0].Content, "tcp dport 5433 drop") && !strings.Contains(guards[0].Content, "tcp dport { 5433 } drop") {
|
|
t.Errorf("the guard does not refuse this node's ports: %s", guards[0].Content)
|
|
}
|
|
if strings.Count(guards[0].Content, "accept") != 1 || !strings.Contains(guards[0].Content, "policy accept") {
|
|
t.Errorf("the guard holds an accept of its own: %s", guards[0].Content)
|
|
}
|
|
for _, id := range []string{guardID, guardUnitID, guardRunningID} {
|
|
if _, ok := at[id]; !ok {
|
|
t.Errorf("the bundle has no %s", id)
|
|
}
|
|
}
|
|
if !(at["container-runtime-running"] < at[guardID] && at[guardRunningID] < at["store"]) {
|
|
t.Errorf("the guard is not between the runtime and the store: %v", at)
|
|
}
|
|
if _, kept := at["base-filter-package"]; !kept {
|
|
t.Error("nft, which loads the guard, is no longer installed")
|
|
}
|
|
unit := r.Declaration.Resources[at[guardRunningID]].(*declaration.Service)
|
|
// A changed table is reloaded, never restarted: a restart deletes the table before loading
|
|
// it again, leaving the ports unguarded in between.
|
|
if unit.Unit != guardUnit || unit.State != "running" || strings.Join(unit.RestartOn, ",") != guardUnitID ||
|
|
strings.Join(unit.ReloadOn, ",") != guardID {
|
|
t.Errorf("the guard's service: %+v", unit)
|
|
}
|
|
stop := r.Declaration.Resources[at[guardUnitID]].(*declaration.File).Content
|
|
if !strings.Contains(stop, "ExecStop=nft delete table inet mesh_guard") || strings.Contains(stop, "flush") {
|
|
t.Errorf("stopping the guard does not delete only its own table: %s", stop)
|
|
}
|
|
}
|
|
|
|
func TestAConvergedBundleIsNotMadeAnAdoptedOne(t *testing.T) {
|
|
// The converged genesis keeps the foundation's filter, byte for byte (novox/hq ADR 0088).
|
|
r := producedBundle(t)
|
|
for _, res := range r.Declaration.Resources {
|
|
if strings.HasPrefix(res.Identity(), declaration.AdoptionPrefix) {
|
|
t.Errorf("a converged bundle carries %s", res.Identity())
|
|
}
|
|
}
|
|
if !r.declares("base-filter-loaded") {
|
|
t.Error("a converged bundle lost its filter")
|
|
}
|
|
}
|
|
|
|
func TestAnAdoptedGenesisTakesTheMeshsOwnModulesBeforePushingThem(t *testing.T) {
|
|
t.Setenv("TMPDIR", t.TempDir())
|
|
for _, c := range []struct {
|
|
module string
|
|
takes bool
|
|
}{{RegistryModule, true}, {ControlPlaneModule, true}, {BuilderModule, true}, {"gitea", false}} {
|
|
rec := &controlRecorder{settings: map[string]string{}}
|
|
control := controlPlane{container: "temp-mesh-controller", run: rec.run, timeout: time.Second}
|
|
o := Options{Node: "anchor", Adopted: true, Wait: time.Second}
|
|
if _, err := installModule(context.Background(), o, control, c.module, []byte(`{}`), quietly); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assign, take, push := rec.index("assign anchor "+c.module), rec.index("take anchor "+c.module), rec.index("push anchor")
|
|
if !c.takes {
|
|
if take >= 0 {
|
|
t.Errorf("%s was taken at genesis", c.module)
|
|
}
|
|
continue
|
|
}
|
|
if !(assign >= 0 && assign < take && take < push) {
|
|
t.Errorf("%s: assign %d, take %d, push %d: %v", c.module, assign, take, push, rec.told)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAConvergedGenesisTakesNothing(t *testing.T) {
|
|
t.Setenv("TMPDIR", t.TempDir())
|
|
rec := &controlRecorder{settings: map[string]string{}}
|
|
control := controlPlane{container: "temp-mesh-controller", run: rec.run, timeout: time.Second}
|
|
if _, err := installModule(context.Background(), Options{Node: "anchor", Wait: time.Second}, control,
|
|
RegistryModule, []byte(`{}`), quietly); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.index("take") >= 0 {
|
|
t.Errorf("a converged genesis took a module: %v", rec.told)
|
|
}
|
|
}
|
|
|
|
func TestAnAdoptedGenesisOpensTheRegistryFromAnywhere(t *testing.T) {
|
|
t.Setenv("TMPDIR", t.TempDir())
|
|
rec := &controlRecorder{settings: map[string]string{}}
|
|
control := controlPlane{container: "temp-mesh-controller", run: rec.run, timeout: time.Second}
|
|
o := Options{Node: "anchor", Adopted: true, Ports: FoundationPorts{Registry: 5100}, Wait: time.Second}
|
|
if _, err := installModule(context.Background(), o, control, RegistryModule, []byte(`{}`), quietly); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := rec.settings["distribution-settings.json"]; got != `{"expose":{"5000":"anywhere"},"ports":{"5000":5100}}` {
|
|
t.Errorf("the registry was told %s", got)
|
|
}
|
|
}
|
|
|
|
func TestAnAdoptedGenesisChoosesTheFilterAndLoadsNone(t *testing.T) {
|
|
rec := &controlRecorder{settings: map[string]string{}}
|
|
control := controlPlane{container: "mesh-controller", run: rec.run, timeout: time.Second}
|
|
o := Options{Node: "anchor", Adopted: true, Answers: map[string]string{"packet-filter": "nftables"}}
|
|
filter, err := ChooseAndInstallFilter(context.Background(), o, control, quietly)
|
|
if err != nil || filter != "nftables" {
|
|
t.Fatalf("%q %v", filter, err)
|
|
}
|
|
if len(rec.told) != 0 {
|
|
t.Errorf("an adopted genesis installed a filter: %v", rec.told)
|
|
}
|
|
}
|
|
|
|
func TestAnAdoptedNodeIsRecordedAdopted(t *testing.T) {
|
|
stop := errors.New("stop here")
|
|
runtime := &asked{answer: func(name string, args []string) (string, error) {
|
|
joined := strings.Join(args, " ")
|
|
switch {
|
|
case strings.Contains(joined, "node list"):
|
|
return "", nil
|
|
case strings.Contains(joined, "node add"):
|
|
return "", nil
|
|
}
|
|
return "", fmt.Errorf("%w: %s %v", stop, name, args)
|
|
}}
|
|
_, _ = Enrol(context.Background(), Options{
|
|
Node: "anchor", Adopted: true, State: t.TempDir() + "/state.json", Timeout: time.Second,
|
|
Host: "/usr/local/bin/mesh-host", HostInBackground: true,
|
|
}, arch(t), controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second},
|
|
func(string) {})
|
|
if !runtime.ran("node add anchor --adopted") {
|
|
t.Errorf("the node was not added adopted: %v", runtime.commands)
|
|
}
|
|
}
|