229 lines
8.5 KiB
Go
229 lines
8.5 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, Management: 15673}
|
|
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))
|
|
}
|
|
if !strings.Contains(guards[0].Content, "tcp dport { 5433, 15673 } 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)
|
|
if unit.Unit != guardUnit || unit.State != "running" || strings.Join(unit.RestartOn, ",") != guardID+","+guardUnitID {
|
|
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)
|
|
}
|
|
}
|