Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a332b0e6e | ||
|
|
b94e0f9a77 | ||
|
|
b840ce0a77 | ||
|
|
286865dfa7 |
@@ -1,153 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-host/internal/apply"
|
||||
"github.com/novox/mesh-host/internal/identity"
|
||||
)
|
||||
|
||||
// Joining through the tunnel (novox/hq ADR 0169).
|
||||
//
|
||||
// **The bus is never open to the internet, so a joining machine reaches it over the tunnel.** It
|
||||
// makes its tunnel key first and prints the public half; the token is issued for that key, and the
|
||||
// hub is told the key before the token is shown; the token carries the one peer this machine needs.
|
||||
// So the tunnel can come up before the mesh has said anything else — the circle ADR 0004 broke by
|
||||
// carrying the bus's address in the token is broken here by carrying the hub's.
|
||||
|
||||
// tunnelConfigPath and tunnelUnit are where the mesh's own declaration puts the private network, so
|
||||
// the first tunnel is the same interface and unit the mesh takes over, not a second one beside it.
|
||||
var (
|
||||
tunnelConfigPath = "/etc/wireguard/mesh0.conf"
|
||||
tunnelUnit = "wg-quick@mesh0"
|
||||
// lookPath finds WireGuard's tools; a variable so a test needs none installed.
|
||||
lookPath = exec.LookPath
|
||||
)
|
||||
|
||||
// keyCommand makes this machine's tunnel key, or reads the one it already made, and prints the
|
||||
// public half: what the token is issued for. Making it twice would be a token issued for a key the
|
||||
// machine no longer has, so an existing key is kept.
|
||||
func keyCommand(opts options) error {
|
||||
path := identity.OverlayKeyPath(opts.state)
|
||||
if key, err := identity.LoadOverlayKey(path); err == nil {
|
||||
fmt.Println(key.Public)
|
||||
return nil
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(identity.Path(opts.state)); err == nil {
|
||||
return fmt.Errorf("this machine has joined already (%s), and its tunnel key is its own; "+
|
||||
"there is no key to make", identity.Path(opts.state))
|
||||
}
|
||||
key, err := identity.GenerateOverlayKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(key.Private+"\n"), 0o600); err != nil {
|
||||
return fmt.Errorf("cannot write this machine's tunnel key: %w", err)
|
||||
}
|
||||
fmt.Println(key.Public)
|
||||
fmt.Fprintln(os.Stderr, "\nthis machine's tunnel key, made here; the private half stays in "+path+".\n"+
|
||||
"Issue the token for it — `token issue --new <name> --overlay-key <the line above>` — and enrol with that token.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// tunnelKeyFor is the key a token through the tunnel was issued for, read from where `key` left it.
|
||||
// Refused when there is none, or it is another: the hub knows only the key the token names.
|
||||
func tunnelKeyFor(t *identity.TokenTunnel, state string) (identity.OverlayKey, error) {
|
||||
path := identity.OverlayKeyPath(state)
|
||||
key, err := identity.LoadOverlayKey(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return identity.OverlayKey{}, fmt.Errorf("this token was issued for a tunnel key, and this " +
|
||||
"machine has none: run `nox-mesh-host key` here first and issue the token for the key it prints")
|
||||
}
|
||||
if err != nil {
|
||||
return identity.OverlayKey{}, err
|
||||
}
|
||||
if key.Public != t.Key {
|
||||
return identity.OverlayKey{}, fmt.Errorf("this token was issued for the tunnel key %s, and this "+
|
||||
"machine's is %s — it is another machine's token, or the key was made again; issue a new "+
|
||||
"token for %s", t.Key, key.Public, key.Public)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// tunnelConfig is the first tunnel: this machine's address, and the hub as its one peer, reaching the
|
||||
// whole private network through it. The private key is set from its file, as the mesh's own
|
||||
// declaration does it, so the file holds no secret.
|
||||
func tunnelConfig(t *identity.TokenTunnel, keyPath string) string {
|
||||
return fmt.Sprintf(`# Written by nox-mesh-host enrol: the one peer a joining machine needs (novox/hq ADR 0169).
|
||||
# The mesh's own declaration replaces this once the machine has joined.
|
||||
[Interface]
|
||||
Address = %s
|
||||
PostUp = wg set %%i private-key %s
|
||||
|
||||
[Peer]
|
||||
PublicKey = %s
|
||||
Endpoint = %s
|
||||
AllowedIPs = %s
|
||||
PersistentKeepalive = 25
|
||||
`, t.Address, keyPath, t.HubKey, t.HubEndpoint, t.Range)
|
||||
}
|
||||
|
||||
// bringTheTunnelUp writes the first tunnel and starts it, so the bus the token names can be reached.
|
||||
func bringTheTunnelUp(ctx context.Context, t *identity.TokenTunnel, keyPath string, run apply.Runner) error {
|
||||
if _, err := lookPath("wg-quick"); err != nil {
|
||||
return errors.New("joining through the tunnel needs WireGuard's tools on this machine " +
|
||||
"(wireguard-tools), and wg-quick is not here")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(tunnelConfigPath), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(tunnelConfigPath, []byte(tunnelConfig(t, keyPath)), 0o600); err != nil {
|
||||
return fmt.Errorf("cannot write the first tunnel: %w", err)
|
||||
}
|
||||
if out, err := run(ctx, "systemctl", "restart", tunnelUnit); err != nil {
|
||||
return fmt.Errorf("the first tunnel would not start (%s): %v %s", tunnelUnit, err, strings.TrimSpace(out))
|
||||
}
|
||||
fmt.Printf("the tunnel to the hub is up: %s, through %s\n", t.Address, t.HubEndpoint)
|
||||
return waitForTheHub(ctx, run, handshakeWithin)
|
||||
}
|
||||
|
||||
// handshakeWithin is how long the hub has to answer the first tunnel. Issuing the token sent the hub
|
||||
// this machine as a peer; the hub applies that on its own time, and a bus dialled before it has is a
|
||||
// timeout that names the bus rather than the tunnel.
|
||||
var handshakeWithin = 90 * time.Second
|
||||
|
||||
// waitForTheHub waits until the tunnel has shaken hands with the hub, so the bus is dialled over a
|
||||
// tunnel that answers — and says so in the tunnel's own words when it does not.
|
||||
func waitForTheHub(ctx context.Context, run apply.Runner, within time.Duration) error {
|
||||
deadline := time.Now().Add(within)
|
||||
for {
|
||||
out, err := run(ctx, "wg", "show", "mesh0", "latest-handshakes")
|
||||
if err == nil {
|
||||
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 2 && fields[1] != "0" {
|
||||
fmt.Println("the hub answered the tunnel")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
return fmt.Errorf("the hub has not answered the tunnel in %s: the token may be another machine's, "+
|
||||
"the hub may not have been sent this machine as a peer, or its tunnel's port is not reachable "+
|
||||
"from here", within)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(2 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-host/internal/identity"
|
||||
)
|
||||
|
||||
// `key` makes the tunnel key once and prints its public half; asked again it prints the same one,
|
||||
// because a token may already have been issued for it (novox/hq ADR 0169).
|
||||
func TestKeyMakesTheTunnelKeyOnceAndKeepsIt(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := options{state: filepath.Join(dir, "state.json")}
|
||||
first := captureStdout(t, func() {
|
||||
if err := keyCommand(opts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
second := captureStdout(t, func() {
|
||||
if err := keyCommand(opts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
if strings.TrimSpace(first) == "" || strings.TrimSpace(first) != strings.TrimSpace(second) {
|
||||
t.Fatalf("the key changed between two asks: %q then %q", first, second)
|
||||
}
|
||||
info, err := os.Stat(identity.OverlayKeyPath(opts.state))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if info.Mode().Perm() != 0o600 {
|
||||
t.Errorf("the private half is readable beyond root: %v", info.Mode().Perm())
|
||||
}
|
||||
}
|
||||
|
||||
// A token through the tunnel takes the key it was issued for, and says so when this machine has none
|
||||
// or another.
|
||||
func TestATokenThroughTheTunnelTakesItsOwnKey(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
state := filepath.Join(dir, "state.json")
|
||||
tt := &identity.TokenTunnel{Key: "x", Address: "10.42.0.9/32", Range: "10.42.0.0/16", HubKey: "h", HubEndpoint: "198.51.100.1:51820"}
|
||||
if _, err := tunnelKeyFor(tt, state); err == nil || !strings.Contains(err.Error(), "nox-mesh-host key") {
|
||||
t.Fatalf("a machine with no key was not told to make one: %v", err)
|
||||
}
|
||||
captureStdout(t, func() { _ = keyCommand(options{state: state}) })
|
||||
if _, err := tunnelKeyFor(tt, state); err == nil || !strings.Contains(err.Error(), "issued for the tunnel key x") {
|
||||
t.Fatalf("another machine's token was taken: %v", err)
|
||||
}
|
||||
mine, _ := identity.LoadOverlayKey(identity.OverlayKeyPath(state))
|
||||
tt.Key = mine.Public
|
||||
if got, err := tunnelKeyFor(tt, state); err != nil || got.Public != mine.Public {
|
||||
t.Fatalf("this machine's own token was refused: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The first tunnel is the mesh's interface and unit, with the hub as its one peer and no secret in
|
||||
// the file — the same shape the mesh's declaration replaces it with.
|
||||
func TestTheFirstTunnelIsTheMeshsInterfaceWithTheHubAsItsPeer(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
tunnelConfigPath = filepath.Join(dir, "wireguard", "mesh0.conf")
|
||||
lookPath = func(string) (string, error) { return "/usr/bin/wg-quick", nil }
|
||||
t.Cleanup(func() { tunnelConfigPath = "/etc/wireguard/mesh0.conf" })
|
||||
var ran []string
|
||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
if name == "wg" {
|
||||
return "HUBKEY\t1759400000\n", nil
|
||||
}
|
||||
ran = append(ran, name+" "+strings.Join(args, " "))
|
||||
return "", nil
|
||||
}
|
||||
tt := &identity.TokenTunnel{Key: "k", Address: "10.42.0.9/32", Range: "10.42.0.0/16", HubKey: "HUBKEY", HubEndpoint: "198.51.100.1:51820"}
|
||||
captureStdout(t, func() {
|
||||
if err := bringTheTunnelUp(context.Background(), tt, "/var/lib/mesh-host/overlay.key", run); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
raw, err := os.ReadFile(tunnelConfigPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conf := string(raw)
|
||||
for _, want := range []string{"Address = 10.42.0.9/32", "PostUp = wg set %i private-key /var/lib/mesh-host/overlay.key",
|
||||
"PublicKey = HUBKEY", "Endpoint = 198.51.100.1:51820", "AllowedIPs = 10.42.0.0/16", "PersistentKeepalive = 25"} {
|
||||
if !strings.Contains(conf, want) {
|
||||
t.Errorf("the first tunnel lacks %q:\n%s", want, conf)
|
||||
}
|
||||
}
|
||||
if strings.Contains(conf, "PrivateKey") {
|
||||
t.Error("the first tunnel's file holds the private key")
|
||||
}
|
||||
if len(ran) != 1 || ran[0] != "systemctl restart wg-quick@mesh0" {
|
||||
t.Errorf("the tunnel was started as %v", ran)
|
||||
}
|
||||
}
|
||||
|
||||
// captureStdout is what fn printed to standard output.
|
||||
func captureStdout(t *testing.T, fn func()) string {
|
||||
t.Helper()
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
was := os.Stdout
|
||||
os.Stdout = w
|
||||
fn()
|
||||
os.Stdout = was
|
||||
w.Close()
|
||||
out, _ := io.ReadAll(r)
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// The bus is dialled only once the hub has answered the tunnel, and a hub that never does is said
|
||||
// as the tunnel's fault rather than the bus's.
|
||||
func TestTheBusWaitsForTheHubToAnswer(t *testing.T) {
|
||||
asked := 0
|
||||
answersOnThird := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
asked++
|
||||
if asked < 3 {
|
||||
return "HUBKEY\t0\n", nil
|
||||
}
|
||||
return "HUBKEY\t1759400000\n", nil
|
||||
}
|
||||
captureStdout(t, func() {
|
||||
if err := waitForTheHub(context.Background(), answersOnThird, time.Minute); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
never := func(context.Context, string, ...string) (string, error) { return "HUBKEY\t0\n", nil }
|
||||
err := waitForTheHub(context.Background(), never, 0)
|
||||
if err == nil || !strings.Contains(err.Error(), "has not answered the tunnel") {
|
||||
t.Fatalf("a hub that never answered was not said: %v", err)
|
||||
}
|
||||
}
|
||||
+1
-18
@@ -96,9 +96,6 @@ const usage = `mesh-host — the node host
|
||||
reconcile make this machine match what the mesh last told it — or, before any
|
||||
mesh has, the bundle this host carries
|
||||
bundle show what this host carries
|
||||
key make this machine's tunnel key, or read the one it made, and print the public
|
||||
half: what its join token is issued for (novox/hq ADR 0169)
|
||||
enrol --token T join the mesh — through the tunnel when the token was issued for a key
|
||||
overlay take take over the tunnel found here (novox/hq ADR 0105): its key becomes this
|
||||
node's overlay key and the mesh is told, signed; --tunnel <iface> when several are up
|
||||
owned what this host has applied and still owns
|
||||
@@ -215,9 +212,6 @@ func parseArgs(args []string) (string, options, error) {
|
||||
func run(ctx context.Context, command string, opts options) error {
|
||||
jsonOut, timeout := opts.json, opts.timeout
|
||||
switch command {
|
||||
case "key":
|
||||
return keyCommand(opts)
|
||||
|
||||
case "profile":
|
||||
p := profile.Detect(ctx, profile.Default(nil), timeout)
|
||||
if jsonOut {
|
||||
@@ -794,18 +788,7 @@ func enrol(ctx context.Context, opts options) error {
|
||||
fmt.Printf("this node's overlay key is the found tunnel's (%s): %s\n", tun, mine.Overlay.Public)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case found == nil && token.Tunnel != nil:
|
||||
// Through the tunnel (novox/hq ADR 0169): the key `key` made, which the token names, and the
|
||||
// tunnel brought up from the token before the bus is dialled — the bus is reached over it.
|
||||
mine.Overlay, err = tunnelKeyFor(token.Tunnel, opts.state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bringTheTunnelUp(ctx, token.Tunnel, identity.OverlayKeyPath(opts.state), apply.ExecRunner); err != nil {
|
||||
return err
|
||||
}
|
||||
case found == nil:
|
||||
if found == nil {
|
||||
mine.Overlay, err = identity.GenerateOverlayKey()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+38
-3
@@ -1041,6 +1041,38 @@ type unitReloader interface {
|
||||
ReloadUnits(ctx context.Context, run system.Runner) error
|
||||
}
|
||||
|
||||
// serviceSettle is how long the host waits before looking at a unit a second time. A test sets it
|
||||
// to nothing; on a machine it is the window in which a daemon that refuses its configuration dies.
|
||||
var serviceSettle = 2 * time.Second
|
||||
|
||||
// stayedRunning is the state of a unit the host has just asked to run, read twice.
|
||||
//
|
||||
// **Because the first read races the failure.** A service manager returns when it has started the
|
||||
// process, and the unit is "activating" or "active" at that instant whatever the process is about
|
||||
// to do. A daemon that reads its configuration, refuses it and exits does so a fraction of a second
|
||||
// later — fail2ban took 221 milliseconds the day this was written — so a single read back says
|
||||
// running about a machine whose daemon is already gone, and the apply reports "restarted" for a
|
||||
// service that is dead. Every ban on both public machines was lost that way while every check
|
||||
// passed (novox/hq ADR 0184), which is the one shape of failure this host exists to refuse.
|
||||
//
|
||||
// So it looks again, after the moment in which that happens. It does not wait for a slow unit to
|
||||
// finish starting: a unit still coming up reads as running both times and is accepted, as before.
|
||||
// What this catches is a unit that was running and is not any more.
|
||||
func stayedRunning(ctx context.Context, sys system.System, run Runner, unit string) (string, error) {
|
||||
state, err := sys.ServiceState(ctx, run, unit)
|
||||
if err != nil || state != "running" {
|
||||
return state, err
|
||||
}
|
||||
timer := time.NewTimer(serviceSettle)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return state, ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
return sys.ServiceState(ctx, run, unit)
|
||||
}
|
||||
|
||||
func applyService(ctx context.Context, sys system.System, r *declaration.Service, run Runner,
|
||||
changed map[string]bool, previous store.Applied) (Outcome, error) {
|
||||
if r.Stateless() {
|
||||
@@ -1120,6 +1152,9 @@ func applyService(ctx context.Context, sys system.System, r *declaration.Service
|
||||
// Read back. A service manager accepting a command says the transaction was accepted,
|
||||
// not that the unit is running — one that starts and immediately dies satisfies it.
|
||||
after, err := sys.ServiceState(ctx, run, r.Unit)
|
||||
if err == nil && r.State == "running" {
|
||||
after, err = stayedRunning(ctx, sys, run, r.Unit)
|
||||
}
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
@@ -1140,7 +1175,7 @@ func applyService(ctx context.Context, sys system.System, r *declaration.Service
|
||||
}
|
||||
// Read back, for the same reason as above: a unit that starts and immediately dies
|
||||
// satisfies a service manager and nothing else.
|
||||
after, err := sys.ServiceState(ctx, run, r.Unit)
|
||||
after, err := stayedRunning(ctx, sys, run, r.Unit)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
@@ -1230,7 +1265,7 @@ func reflectOnly(ctx context.Context, sys system.System, r *declaration.Service,
|
||||
}
|
||||
// Read back: it was running, and a restart or reload that left it otherwise is a failure —
|
||||
// the machine's network manager down is not a change to report and move past.
|
||||
after, err := sys.ServiceState(ctx, run, r.Unit)
|
||||
after, err := stayedRunning(ctx, sys, run, r.Unit)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
@@ -1408,7 +1443,7 @@ func applyPackage(ctx context.Context, sys system.System, r *declaration.Package
|
||||
return out, err
|
||||
}
|
||||
if r.Absent {
|
||||
// Declared absent (novox/hq ADR 0175): removed when it is here, left alone when it is not.
|
||||
// Declared absent (novox/hq ADR 0180): removed when it is here, left alone when it is not.
|
||||
if !installed {
|
||||
out.Action = "unchanged"
|
||||
out.Detail = "not installed, as declared"
|
||||
|
||||
@@ -174,7 +174,7 @@ func TestACapabilityReachesTheRuntimeAndTheSpec(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A package may be declared absent (novox/hq ADR 0175): removed when it is installed, read back,
|
||||
// A package may be declared absent (novox/hq ADR 0180): removed when it is installed, read back,
|
||||
// left alone when it is not.
|
||||
func TestAPackageDeclaredAbsentIsRemovedWhenPresentAndLeftWhenNot(t *testing.T) {
|
||||
installed := true
|
||||
|
||||
@@ -77,7 +77,7 @@ func retireFirewall(ctx context.Context, d *declaration.Declaration, origin stri
|
||||
return "", nil
|
||||
}
|
||||
if !firewall.Installed(ctx, run) {
|
||||
// Uninstalled (novox/hq ADR 0175): retired for good, by the module that replaced it. Said
|
||||
// Uninstalled (novox/hq ADR 0180): retired for good, by the module that replaced it. Said
|
||||
// once, and nothing is asked of a command that is not there.
|
||||
if rec.RetiredBy != firewall.RetiredRemoved {
|
||||
rec.RetiredBy = firewall.RetiredRemoved
|
||||
|
||||
@@ -525,7 +525,7 @@ func TestUfwIsNotRetiredUntilTheMeshsOwnFilterIsLoaded(t *testing.T) {
|
||||
}
|
||||
|
||||
// A front end that is no longer installed is recorded as removed, said once, and asked nothing of
|
||||
// (novox/hq ADR 0175).
|
||||
// (novox/hq ADR 0180).
|
||||
func TestAnUninstalledFrontEndIsRetiredForGood(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
u := &ufwMachine{installed: false, ruleset: "table inet mesh\n"}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package apply
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
)
|
||||
|
||||
// A daemon that reads a configuration it refuses dies a fraction of a second after the service
|
||||
// manager has reported it started — fail2ban took 221 milliseconds on the control node the day this
|
||||
// was written. One read back catches nothing: the unit is active at that instant. The mesh reported
|
||||
// the service "restarted" while every ban on two public machines was gone, and every check passed
|
||||
// (novox/hq ADR 0184). The host looks again, after the moment in which that happens.
|
||||
func TestAServiceThatDiesJustAfterItsRestartIsNotReportedRestarted(t *testing.T) {
|
||||
serviceSettle = 0
|
||||
// Alive at the first look after starting, dead at the second — the shape of a daemon that
|
||||
// refuses what it was just given.
|
||||
started, looks := false, 0
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
if args[0] == "show" {
|
||||
state := "active"
|
||||
if started {
|
||||
if looks++; looks >= 2 {
|
||||
state = "failed"
|
||||
}
|
||||
}
|
||||
return "LoadState=loaded\nActiveState=" + state + "\n", nil
|
||||
}
|
||||
if args[0] == "start" {
|
||||
started = true
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
dir := t.TempDir()
|
||||
d := parse(t, `{"declaration":1,"resources":[
|
||||
{"id":"conf","type":"file","path":"`+dir+`/jail.conf","content":"[sshd]\n","mode":"0644"},
|
||||
{"id":"run","type":"service","unit":"fail2ban.service","state":"running","restart-on":["conf"]}
|
||||
]}`)
|
||||
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a service that died just after being restarted was reported as restarted")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "fail2ban.service") || !strings.Contains(err.Error(), "is stopped") {
|
||||
t.Errorf("the failure does not name the unit and what it is now: %v", err)
|
||||
}
|
||||
if looks < 2 {
|
||||
t.Errorf("the host looked at the unit %d time(s) after starting it; it must look again", looks)
|
||||
}
|
||||
}
|
||||
|
||||
// A unit still coming up reads as running at both looks and is accepted: the second look is for a
|
||||
// unit that WAS running and is not any more, never a wait for a slow one to finish starting.
|
||||
func TestAUnitStillStartingIsNotAFailure(t *testing.T) {
|
||||
serviceSettle = 0
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
if args[0] == "show" {
|
||||
return "LoadState=loaded\nActiveState=activating\n", nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
d := parse(t, `{"declaration":1,"resources":[
|
||||
{"id":"s","type":"service","unit":"slow.service","state":"running"}
|
||||
]}`)
|
||||
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil); err != nil {
|
||||
t.Fatalf("a unit still starting was reported as a failure: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// And a service the declaration asks to be stopped is not waited on at all.
|
||||
func TestAServiceAskedToStopIsNotWaitedOn(t *testing.T) {
|
||||
serviceSettle = 0
|
||||
shows := 0
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
if args[0] == "show" {
|
||||
shows++
|
||||
if shows == 1 {
|
||||
return "LoadState=loaded\nActiveState=active\n", nil
|
||||
}
|
||||
return "LoadState=loaded\nActiveState=inactive\n", nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
d := parse(t, `{"declaration":1,"resources":[
|
||||
{"id":"s","type":"service","unit":"off.service","state":"stopped"}
|
||||
]}`)
|
||||
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil); err != nil {
|
||||
t.Fatalf("stopping a service was reported as a failure: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The settle between a unit's two read-backs is a real pause on a machine and nothing in a test:
|
||||
// no test here drives a service manager that takes time, so paying it would only slow the suite
|
||||
// (novox/hq ADR 0184).
|
||||
func TestMain(m *testing.M) {
|
||||
serviceSettle = 0
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
@@ -119,11 +119,6 @@ func Enrol(ctx context.Context, o Options, sys system.System, control controlPla
|
||||
// its ports and range on and not another that came up since.
|
||||
args = append(args, "--tunnel", o.Tunnel)
|
||||
}
|
||||
// The enrolment account the token's secret is the password of exists in the mesh's records
|
||||
// and nowhere on the bus yet (novox/hq 04-ISSUES/146): placed before the machine presents it.
|
||||
if err := placeTheBusUsers(ctx, control, say); err != nil {
|
||||
return out, err
|
||||
}
|
||||
joined, err := control.run(joining, o.Host, args...)
|
||||
cancel()
|
||||
if err != nil {
|
||||
@@ -142,10 +137,6 @@ func Enrol(ctx context.Context, o Options, sys system.System, control controlPla
|
||||
}
|
||||
out.Joined = true
|
||||
say(" enrolled as " + o.Node)
|
||||
// And the node's own account, minted as it enrolled, before its agent connects as it.
|
||||
if err := placeTheBusUsers(ctx, control, say); err != nil {
|
||||
return out, err
|
||||
}
|
||||
}
|
||||
|
||||
// 4. The agent.
|
||||
@@ -157,40 +148,6 @@ func Enrol(ctx context.Context, o Options, sys system.System, control controlPla
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// busAccounts and busContainer are where the installer's bundle raises the bus: the file its
|
||||
// configuration includes, and the container that reads it. The installer raised them, so it is the
|
||||
// one that knows them (examples/foundation-first-node-nats.lock).
|
||||
const (
|
||||
busAccounts = "/var/lib/mesh-bus-conf/accounts.conf"
|
||||
busContainer = "mesh-broker"
|
||||
)
|
||||
|
||||
// placeTheBusUsers writes the mesh's composed user list beside the bus the installer raised, and makes
|
||||
// the bus re-read it.
|
||||
//
|
||||
// **Genesis's own step** (novox/hq 04-ISSUES/146). Every account on the bus reaches it in the
|
||||
// declaration of the machine that runs it — which needs that machine to be an enrolled node, and at
|
||||
// genesis it is not. The control plane composes the list and says it; whoever raised the bus places
|
||||
// it. That is this installer: it carried the bus in its bundle, so it knows where the bus reads it,
|
||||
// and the control plane never has to.
|
||||
func placeTheBusUsers(ctx context.Context, control controlPlane, say func(string)) error {
|
||||
users, err := control.tell(ctx, "broker", "accounts")
|
||||
if err != nil {
|
||||
return fmt.Errorf("the control plane would not say the bus's users, so no machine could "+
|
||||
"join it: %w", err)
|
||||
}
|
||||
if !strings.Contains(users, "accounts") {
|
||||
return fmt.Errorf("the control plane's account of the bus's users is not one:\n%s", indent(users))
|
||||
}
|
||||
script := "umask 077 && cat > " + busAccounts + ".next <<'MESHBUSUSERS'\n" + users + "\nMESHBUSUSERS\n" +
|
||||
"mv " + busAccounts + ".next " + busAccounts + " && docker kill -s HUP " + busContainer + " >/dev/null"
|
||||
if out, err := control.run(ctx, "sh", "-c", script); err != nil {
|
||||
return fmt.Errorf("the bus's users could not be placed at %s: %w\n%s", busAccounts, err, indent(out))
|
||||
}
|
||||
say(" bus users placed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// runTheHost makes sure something on this machine is listening to the mesh, and proves it.
|
||||
//
|
||||
// **The installer does not install the service, and says so.** A unit file is a packaging decision
|
||||
|
||||
@@ -261,56 +261,3 @@ func TestAListingIsMatchedByNameAndNotBySubstring(t *testing.T) {
|
||||
t.Error("registry-mirror was not found")
|
||||
}
|
||||
}
|
||||
|
||||
// **Genesis places the bus's users, before the machine enrols and again after** (novox/hq
|
||||
// 04-ISSUES/146). The enrolment account exists only in the mesh's records until somebody writes it
|
||||
// beside the bus; so does the node's own, minted as it enrols. Without the first, the machine is
|
||||
// refused by the bus it just raised; without the second, its agent is.
|
||||
func TestAFirstNodeIsLetOntoTheBusItRaised(t *testing.T) {
|
||||
enrolled := false
|
||||
runtime := &asked{answer: func(name string, args []string) (string, error) {
|
||||
joined := strings.Join(args, " ")
|
||||
switch {
|
||||
case strings.Contains(joined, "node list"):
|
||||
if enrolled {
|
||||
return "anchor here 01J0\n", nil
|
||||
}
|
||||
return "", nil
|
||||
case strings.Contains(joined, "node add"):
|
||||
return "added anchor\n", nil
|
||||
case strings.Contains(joined, "token issue"):
|
||||
return "a token for anchor, good once:\n\n " + strings.Repeat("t", 240) + "\n\n", nil
|
||||
case strings.Contains(joined, "broker accounts"):
|
||||
return "accounts {\n MESH { users = [] }\n}\n", nil
|
||||
case name == "/usr/local/bin/mesh-host":
|
||||
enrolled = true
|
||||
return "enrolled as anchor\n", nil
|
||||
case name == "pgrep", name == "sh":
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("unexpected: %s %v", name, args)
|
||||
}}
|
||||
|
||||
if _, err := Enrol(context.Background(), Options{
|
||||
Node: "anchor", State: filepath.Join(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) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
placed, enrol := []int{}, -1
|
||||
for i, c := range runtime.commands {
|
||||
if strings.HasPrefix(c, "sh -c") && strings.Contains(c, "kill -s HUP mesh-broker") &&
|
||||
strings.Contains(c, "/var/lib/mesh-bus-conf/accounts.conf") {
|
||||
placed = append(placed, i)
|
||||
}
|
||||
if strings.HasPrefix(c, "/usr/local/bin/mesh-host enrol") {
|
||||
enrol = i
|
||||
}
|
||||
}
|
||||
if enrol < 0 || len(placed) != 2 || placed[0] > enrol || placed[1] < enrol {
|
||||
t.Fatalf("the bus's users were not placed before the machine enrolled and again after "+
|
||||
"(placed at %v, enrolled at %d):\n%s", placed, enrol, strings.Join(runtime.commands, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -870,7 +870,7 @@ type Package struct {
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
Package string `json:"package"`
|
||||
// Absent declares that the package is NOT installed (novox/hq ADR 0175): the host removes it
|
||||
// Absent declares that the package is NOT installed (novox/hq ADR 0180): the host removes it
|
||||
// when it is, and leaves a machine that never had it alone. For the one case a module replaces
|
||||
// software the machine was found with and the operator has decided it does not come back — the
|
||||
// firewall front end a converged machine's filter module retired. Nothing to undo when the
|
||||
|
||||
@@ -179,27 +179,18 @@ func legacyFilters(rules, tool string, ufwActive bool) []Filter {
|
||||
}
|
||||
}
|
||||
}
|
||||
var entered func(chain string, seen map[string]bool) bool
|
||||
entered = func(chain string, seen map[string]bool) bool {
|
||||
if seen[chain] || accepting[chain] || len(jumpedFrom[chain]) == 0 {
|
||||
return false
|
||||
}
|
||||
seen[chain] = true
|
||||
for _, from := range jumpedFrom[chain] {
|
||||
if p, builtIn := policy[from]; builtIn {
|
||||
if p != "ACCEPT" {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !entered(from, seen) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
// A chain of refusals is a ban list when every refusal names the sources it refuses and the
|
||||
// chain accepts nothing — the same rule the nftables side applies, and no more.
|
||||
//
|
||||
// **The policy of the chains that jump to it says nothing about what it is.** An earlier cut
|
||||
// required every path into the chain to come from a built-in whose policy accepts, and the
|
||||
// mesh's own intrusion prevention then read as a foreign rule set on the home server: its ban
|
||||
// chain hangs off the container runtime's user chain as well as INPUT, and that machine's
|
||||
// forward policy is DROP because the runtime set it. The machine reported "NOT the mesh alone"
|
||||
// about a chain the mesh had just written (novox/hq ADR 0186). The policy is already classified
|
||||
// where it belongs — as the runtime's — so requiring it here counted it twice.
|
||||
ban := func(chain, line string) bool {
|
||||
return bansSources(line) && entered(chain, map[string]bool{})
|
||||
return bansSources(line) && !accepting[chain] && len(jumpedFrom[chain]) > 0
|
||||
}
|
||||
type seen struct {
|
||||
owner string
|
||||
@@ -320,7 +311,7 @@ func Active(ctx context.Context, run Runner) bool {
|
||||
}
|
||||
|
||||
// Installed says whether ufw is on this machine at all: a command that is not there is a front end
|
||||
// that was uninstalled (novox/hq ADR 0175), not one that is silent.
|
||||
// that was uninstalled (novox/hq ADR 0180), not one that is silent.
|
||||
func Installed(ctx context.Context, run Runner) bool {
|
||||
_, err := run(ctx, "ufw", "status")
|
||||
return !missing(err)
|
||||
@@ -330,6 +321,6 @@ func Installed(ctx context.Context, run Runner) bool {
|
||||
const (
|
||||
RetiredByMesh = "mesh"
|
||||
RetiredFoundSo = "found-inactive"
|
||||
// RetiredRemoved is a front end uninstalled by the module that replaced it (ADR 0175).
|
||||
// RetiredRemoved is a front end uninstalled by the module that replaced it (ADR 0180).
|
||||
RetiredRemoved = "removed"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The mesh's own ban list is a ban wherever it hangs (novox/hq ADR 0186).
|
||||
//
|
||||
// Captured from the home server after the intrusion prevention had banned four addresses: its ban
|
||||
// chain is jumped to from INPUT, whose policy accepts, and from the container runtime's user chain,
|
||||
// which hangs off a FORWARD the runtime set to DROP. Requiring every path to come from an accepting
|
||||
// built-in made the machine report "NOT the mesh alone" about a chain the mesh had just written.
|
||||
func TestTheMeshsOwnBanChainIsABanBehindADroppingForward(t *testing.T) {
|
||||
legacy, err := os.ReadFile("testdata/home-server-bans-S.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
filters := Filters("", map[string]string{"iptables-legacy": string(legacy)}, false)
|
||||
|
||||
var ban, other []string
|
||||
for _, f := range filters {
|
||||
switch f.Owner {
|
||||
case OwnerBan:
|
||||
ban = append(ban, f.Where)
|
||||
case OwnerOther:
|
||||
other = append(other, f.Where)
|
||||
}
|
||||
}
|
||||
if len(other) > 0 {
|
||||
t.Errorf("the machine reports %v as rule sets the mesh did not write", other)
|
||||
}
|
||||
found := false
|
||||
for _, w := range ban {
|
||||
if w == "chain f2b-route-proxy (iptables-legacy)" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("the intrusion prevention's own chain was not read as a ban; bans were %v", ban)
|
||||
}
|
||||
if !Alone(filters) {
|
||||
t.Error("a machine filtered by the mesh and its own bans does not read as the mesh alone")
|
||||
}
|
||||
}
|
||||
|
||||
// A chain that accepts anything is doing more than banning, and is still not a ban — which is what
|
||||
// keeps a predecessor's allow-and-drop chain classified as something the operator must look at.
|
||||
func TestAChainThatAcceptsIsNotABan(t *testing.T) {
|
||||
rules := "-P INPUT ACCEPT\n" +
|
||||
"-A INPUT -j HAL-MESH-ONLY\n" +
|
||||
"-N HAL-MESH-ONLY\n" +
|
||||
"-A HAL-MESH-ONLY -s 10.0.0.0/8 -j ACCEPT\n" +
|
||||
"-A HAL-MESH-ONLY -s 203.0.113.7/32 -j DROP\n"
|
||||
for _, f := range Filters("", map[string]string{"iptables-legacy": rules}, false) {
|
||||
if f.Where == "chain HAL-MESH-ONLY (iptables-legacy)" && f.Owner != OwnerOther {
|
||||
t.Errorf("a chain that accepts was classified as %s", f.Owner)
|
||||
}
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
-P INPUT ACCEPT
|
||||
-P FORWARD DROP
|
||||
-P OUTPUT ACCEPT
|
||||
-N DOCKER
|
||||
-N DOCKER-BRIDGE
|
||||
-N DOCKER-CT
|
||||
-N DOCKER-FORWARD
|
||||
-N DOCKER-INTERNAL
|
||||
-N DOCKER-USER
|
||||
-N f2b-route-proxy
|
||||
-N ufw-after-forward
|
||||
-N ufw-after-input
|
||||
-N ufw-after-logging-forward
|
||||
-N ufw-after-logging-input
|
||||
-N ufw-after-logging-output
|
||||
-N ufw-after-output
|
||||
-N ufw-before-forward
|
||||
-N ufw-before-input
|
||||
-N ufw-before-logging-forward
|
||||
-N ufw-before-logging-input
|
||||
-N ufw-before-logging-output
|
||||
-N ufw-before-output
|
||||
-N ufw-reject-forward
|
||||
-N ufw-reject-input
|
||||
-N ufw-reject-output
|
||||
-N ufw-track-forward
|
||||
-N ufw-track-input
|
||||
-N ufw-track-output
|
||||
-A INPUT -p tcp -j f2b-route-proxy
|
||||
-A INPUT -j ufw-before-logging-input
|
||||
-A INPUT -j ufw-before-input
|
||||
-A INPUT -j ufw-after-input
|
||||
-A INPUT -j ufw-after-logging-input
|
||||
-A INPUT -j ufw-reject-input
|
||||
-A INPUT -j ufw-track-input
|
||||
-A FORWARD -j DOCKER-USER
|
||||
-A FORWARD -j DOCKER-FORWARD
|
||||
-A FORWARD -j ufw-before-logging-forward
|
||||
-A FORWARD -j ufw-before-forward
|
||||
-A FORWARD -j ufw-after-forward
|
||||
-A FORWARD -j ufw-after-logging-forward
|
||||
-A FORWARD -j ufw-reject-forward
|
||||
-A FORWARD -j ufw-track-forward
|
||||
-A OUTPUT -j ufw-before-logging-output
|
||||
-A OUTPUT -j ufw-before-output
|
||||
-A OUTPUT -j ufw-after-output
|
||||
-A OUTPUT -j ufw-after-logging-output
|
||||
-A OUTPUT -j ufw-reject-output
|
||||
-A OUTPUT -j ufw-track-output
|
||||
-A DOCKER -d 172.17.0.18/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8686 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.14/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8989 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.15/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 7878 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.5/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 9117 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.13/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 6789 -j ACCEPT
|
||||
-A DOCKER -d 172.19.0.2/32 ! -i br-32062158f584 -o br-32062158f584 -p tcp -m tcp --dport 8080 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 5432 -j ACCEPT
|
||||
-A DOCKER -d 172.27.0.2/32 ! -i br-0910a98c6158 -o br-0910a98c6158 -p tcp -m tcp --dport 5678 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.21/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 3579 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.19/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8181 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.17/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8787 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.16/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 6767 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.12/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 3000 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.11/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.10/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 9443 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.10/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 9000 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.9/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 3000 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.7/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 1880 -j ACCEPT
|
||||
-A DOCKER -d 172.28.0.2/32 ! -i br-b11461b5b028 -o br-b11461b5b028 -p tcp -m tcp --dport 80 -j ACCEPT
|
||||
-A DOCKER -d 172.23.0.14/32 ! -i br-66ffa5c1cba5 -o br-66ffa5c1cba5 -p tcp -m tcp --dport 6543 -j ACCEPT
|
||||
-A DOCKER -d 172.23.0.14/32 ! -i br-66ffa5c1cba5 -o br-66ffa5c1cba5 -p tcp -m tcp --dport 5432 -j ACCEPT
|
||||
-A DOCKER -d 172.23.0.5/32 ! -i br-66ffa5c1cba5 -o br-66ffa5c1cba5 -p tcp -m tcp --dport 8000 -j ACCEPT
|
||||
-A DOCKER -d 172.26.0.3/32 ! -i br-b0fec361ccaa -o br-b0fec361ccaa -p tcp -m tcp --dport 6167 -j ACCEPT
|
||||
-A DOCKER -d 172.26.0.2/32 ! -i br-b0fec361ccaa -o br-b0fec361ccaa -p tcp -m tcp --dport 80 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.8/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8000 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p udp -m udp --dport 10001 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8880 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8843 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8443 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8080 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 6789 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p udp -m udp --dport 5514 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p udp -m udp --dport 3478 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p udp -m udp --dport 1900 -j ACCEPT
|
||||
-A DOCKER -d 172.25.0.3/32 ! -i br-b98821f7dc38 -o br-b98821f7dc38 -p tcp -m tcp --dport 8000 -j ACCEPT
|
||||
-A DOCKER -d 172.18.0.3/32 ! -i br-442a0bfc65f8 -o br-442a0bfc65f8 -p tcp -m tcp --dport 1433 -j ACCEPT
|
||||
-A DOCKER -d 172.20.0.3/32 ! -i br-afa37ac8b33d -o br-afa37ac8b33d -p tcp -m tcp --dport 8081 -j ACCEPT
|
||||
-A DOCKER -d 172.20.0.3/32 ! -i br-afa37ac8b33d -o br-afa37ac8b33d -p tcp -m tcp --dport 1883 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.4/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8086 -j ACCEPT
|
||||
-A DOCKER -d 172.21.0.2/32 ! -i br-df15d8e19ec7 -o br-df15d8e19ec7 -p tcp -m tcp --dport 6379 -j ACCEPT
|
||||
-A DOCKER -d 172.30.0.3/32 ! -i br-521eab9a3a5e -o br-521eab9a3a5e -p tcp -m tcp --dport 8283 -j ACCEPT
|
||||
-A DOCKER -d 172.17.0.3/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 3000 -j ACCEPT
|
||||
-A DOCKER ! -i br-32062158f584 -o br-32062158f584 -j DROP
|
||||
-A DOCKER ! -i docker0 -o docker0 -j DROP
|
||||
-A DOCKER ! -i br-521eab9a3a5e -o br-521eab9a3a5e -j DROP
|
||||
-A DOCKER ! -i br-df15d8e19ec7 -o br-df15d8e19ec7 -j DROP
|
||||
-A DOCKER ! -i br-afa37ac8b33d -o br-afa37ac8b33d -j DROP
|
||||
-A DOCKER ! -i br-442a0bfc65f8 -o br-442a0bfc65f8 -j DROP
|
||||
-A DOCKER ! -i br-b98821f7dc38 -o br-b98821f7dc38 -j DROP
|
||||
-A DOCKER ! -i br-b0fec361ccaa -o br-b0fec361ccaa -j DROP
|
||||
-A DOCKER ! -i br-66ffa5c1cba5 -o br-66ffa5c1cba5 -j DROP
|
||||
-A DOCKER ! -i br-b11461b5b028 -o br-b11461b5b028 -j DROP
|
||||
-A DOCKER ! -i br-2df4e541b877 -o br-2df4e541b877 -j DROP
|
||||
-A DOCKER ! -i br-0910a98c6158 -o br-0910a98c6158 -j DROP
|
||||
-A DOCKER-BRIDGE -o br-32062158f584 -j DOCKER
|
||||
-A DOCKER-BRIDGE -o docker0 -j DOCKER
|
||||
-A DOCKER-BRIDGE -o br-521eab9a3a5e -j DOCKER
|
||||
-A DOCKER-BRIDGE -o br-df15d8e19ec7 -j DOCKER
|
||||
-A DOCKER-BRIDGE -o br-afa37ac8b33d -j DOCKER
|
||||
-A DOCKER-BRIDGE -o br-442a0bfc65f8 -j DOCKER
|
||||
-A DOCKER-BRIDGE -o br-b98821f7dc38 -j DOCKER
|
||||
-A DOCKER-BRIDGE -o br-b0fec361ccaa -j DOCKER
|
||||
-A DOCKER-BRIDGE -o br-66ffa5c1cba5 -j DOCKER
|
||||
-A DOCKER-BRIDGE -o br-b11461b5b028 -j DOCKER
|
||||
-A DOCKER-BRIDGE -o br-2df4e541b877 -j DOCKER
|
||||
-A DOCKER-BRIDGE -o br-0910a98c6158 -j DOCKER
|
||||
-A DOCKER-CT -o br-32062158f584 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
-A DOCKER-CT -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
-A DOCKER-CT -o br-521eab9a3a5e -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
-A DOCKER-CT -o br-df15d8e19ec7 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
-A DOCKER-CT -o br-afa37ac8b33d -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
-A DOCKER-CT -o br-442a0bfc65f8 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
-A DOCKER-CT -o br-b98821f7dc38 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
-A DOCKER-CT -o br-b0fec361ccaa -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
-A DOCKER-CT -o br-66ffa5c1cba5 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
-A DOCKER-CT -o br-b11461b5b028 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
-A DOCKER-CT -o br-2df4e541b877 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
-A DOCKER-CT -o br-0910a98c6158 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
-A DOCKER-FORWARD -j DOCKER-CT
|
||||
-A DOCKER-FORWARD -j DOCKER-INTERNAL
|
||||
-A DOCKER-FORWARD -j DOCKER-BRIDGE
|
||||
-A DOCKER-FORWARD -i br-32062158f584 -j ACCEPT
|
||||
-A DOCKER-FORWARD -i docker0 -j ACCEPT
|
||||
-A DOCKER-FORWARD -i br-521eab9a3a5e -j ACCEPT
|
||||
-A DOCKER-FORWARD -i br-df15d8e19ec7 -j ACCEPT
|
||||
-A DOCKER-FORWARD -i br-afa37ac8b33d -j ACCEPT
|
||||
-A DOCKER-FORWARD -i br-442a0bfc65f8 -j ACCEPT
|
||||
-A DOCKER-FORWARD -i br-b98821f7dc38 -j ACCEPT
|
||||
-A DOCKER-FORWARD -i br-b0fec361ccaa -j ACCEPT
|
||||
-A DOCKER-FORWARD -i br-66ffa5c1cba5 -j ACCEPT
|
||||
-A DOCKER-FORWARD -i br-b11461b5b028 -j ACCEPT
|
||||
-A DOCKER-FORWARD -i br-2df4e541b877 -j ACCEPT
|
||||
-A DOCKER-FORWARD -i br-0910a98c6158 -j ACCEPT
|
||||
-A DOCKER-USER -p tcp -j f2b-route-proxy
|
||||
-A f2b-route-proxy -s 13.70.107.184/32 -j REJECT --reject-with icmp-port-unreachable
|
||||
-A f2b-route-proxy -s 45.138.12.51/32 -j REJECT --reject-with icmp-port-unreachable
|
||||
-A f2b-route-proxy -s 20.214.191.94/32 -j REJECT --reject-with icmp-port-unreachable
|
||||
-A f2b-route-proxy -j RETURN
|
||||
@@ -409,26 +409,3 @@ func TestATokenSaysWhatTheMeshCallsThisMachine(t *testing.T) {
|
||||
t.Fatalf("the name did not survive the token: %q", token.Node)
|
||||
}
|
||||
}
|
||||
|
||||
// A token through the tunnel carries the one peer, in the field names the control plane writes
|
||||
// (novox/hq ADR 0169), and an incomplete tunnel is refused naming what is missing.
|
||||
func TestATokenThroughTheTunnelParsesAndAPartOneIsRefused(t *testing.T) {
|
||||
whole := map[string]any{"v": 1, "node": "n", "broker": "10.42.0.1:4222", "fingerprint": "sha256:x",
|
||||
"signer": make([]byte, 32), "secret": "s",
|
||||
"tunnel": map[string]any{"key": "k", "address": "10.42.0.9/32", "range": "10.42.0.0/16",
|
||||
"hub_key": "h", "hub_endpoint": "198.51.100.1:51820"}}
|
||||
raw, _ := json.Marshal(whole)
|
||||
got, err := ParseToken(base64.RawURLEncoding.EncodeToString(raw))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Tunnel == nil || got.Tunnel.HubEndpoint != "198.51.100.1:51820" || got.Tunnel.Range != "10.42.0.0/16" {
|
||||
t.Fatalf("the tunnel was not read: %+v", got.Tunnel)
|
||||
}
|
||||
whole["tunnel"] = map[string]any{"key": "k"}
|
||||
raw, _ = json.Marshal(whole)
|
||||
if _, err := ParseToken(base64.RawURLEncoding.EncodeToString(raw)); err == nil ||
|
||||
!strings.Contains(err.Error(), "the hub's tunnel key") {
|
||||
t.Fatalf("a token with half a tunnel was taken: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The node's key on the private network, which is a different key from the one that says who it
|
||||
@@ -66,19 +64,6 @@ func OverlayKeyFrom(privateBase64 string) (OverlayKey, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LoadOverlayKey reads the key `key` made and left in its file (novox/hq ADR 0169).
|
||||
func LoadOverlayKey(path string) (OverlayKey, error) {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return OverlayKey{}, err
|
||||
}
|
||||
key, err := OverlayKeyFrom(strings.TrimSpace(string(raw)))
|
||||
if err != nil {
|
||||
return OverlayKey{}, fmt.Errorf("%s does not hold a tunnel key: %w", path, err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// OverlayKeyPath is where the private half lives: a file of its own, referenced by the interface
|
||||
// configuration rather than embedded in it.
|
||||
//
|
||||
|
||||
@@ -35,21 +35,6 @@ type Token struct {
|
||||
// firewall found here before enrolling, because an adopted node keeps that firewall in force.
|
||||
// Absent for a converged node.
|
||||
Adopted bool `json:"adopted,omitempty"`
|
||||
|
||||
// Tunnel is this machine's first tunnel, when the token was issued for the key it made with
|
||||
// `key` (novox/hq ADR 0169): its own address and the hub to reach. It brings the tunnel up from
|
||||
// this alone and reaches the bus over it, so the bus never has to face the internet.
|
||||
Tunnel *TokenTunnel `json:"tunnel,omitempty"`
|
||||
}
|
||||
|
||||
// TokenTunnel is the joining machine's side of its first tunnel. Field names are the wire format
|
||||
// the control plane writes.
|
||||
type TokenTunnel struct {
|
||||
Key string `json:"key"`
|
||||
Address string `json:"address"`
|
||||
Range string `json:"range"`
|
||||
HubKey string `json:"hub_key"`
|
||||
HubEndpoint string `json:"hub_endpoint"`
|
||||
}
|
||||
|
||||
// ParseToken reads a token a person pasted.
|
||||
@@ -85,17 +70,6 @@ func ParseToken(encoded string) (Token, error) {
|
||||
if strings.TrimSpace(t.Secret) == "" {
|
||||
missing = append(missing, "the one-time secret")
|
||||
}
|
||||
if tt := t.Tunnel; tt != nil {
|
||||
for _, part := range []struct{ value, says string }{
|
||||
{tt.Key, "the tunnel key it was issued for"}, {tt.Address, "this machine's address"},
|
||||
{tt.Range, "the private network's range"}, {tt.HubKey, "the hub's tunnel key"},
|
||||
{tt.HubEndpoint, "where the hub's tunnel is dialled"},
|
||||
} {
|
||||
if strings.TrimSpace(part.value) == "" {
|
||||
missing = append(missing, part.says)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
// Refused whole rather than used partially. A token missing the fingerprint would have
|
||||
// this node connect to whatever answers at that address, and one missing the signing key
|
||||
|
||||
@@ -52,7 +52,7 @@ type System interface {
|
||||
PackageInstalled(ctx context.Context, run Runner, name string) (bool, error)
|
||||
InstallPackage(ctx context.Context, run Runner, name string) error
|
||||
// RemovePackage uninstalls one package, leaving its dependencies and anything the operator
|
||||
// changed in its configuration where the package manager leaves them (novox/hq ADR 0175).
|
||||
// changed in its configuration where the package manager leaves them (novox/hq ADR 0180).
|
||||
RemovePackage(ctx context.Context, run Runner, name string) error
|
||||
|
||||
// ServiceState is "running" or "stopped". A unit that does not exist is an error, never
|
||||
|
||||
Reference in New Issue
Block a user