Take over the found tunnel: its key, its port, its peers; stop it, never flush
On an adopted machine the private network takes the predecessor's tunnel over in place (hq ADR 0105). Genesis finds the one interface up besides the mesh's own, settles the hub's port and the mesh's range on it, and skips ADR 0100's non-overlap check for a range that is now the tunnel's; a --hub-port or --overlay-range that disagrees is refused naming the tunnel's. At enrolment the found interface's private key becomes this node's overlay key — the one credential the mesh takes rather than mints — stored where a generated one is stored, never printed and never sent; the tunnel (port, address, range, peers) travels with the keys so the mesh composes from it before the first declaration. The interface's service may say what it takes over. Before the mesh's unit starts, the found configuration is kept like any held file and the found unit is stopped and disabled; nothing is flushed, and an interface still up after its unit stopped refuses the takeover rather than half-working. The report says what was carried: interface, port, range, peer count, taken or not, and where the original was kept.
This commit is contained in:
@@ -2,6 +2,9 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdh"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -12,6 +15,7 @@ import (
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
"github.com/novox/mesh-host/internal/reachable"
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
"github.com/novox/mesh-host/internal/tunnel"
|
||||
)
|
||||
|
||||
// Defends novox/hq ADR 0100: the foundation's ports are the node's — inputs to genesis, checked free,
|
||||
@@ -130,9 +134,9 @@ func TestTwoThingsOnOnePortAreRefused(t *testing.T) {
|
||||
|
||||
// machineRunner answers ss, docker ps, docker inspect and ip from fixtures.
|
||||
type machineRunner struct {
|
||||
ss, ps, addrs, routes string
|
||||
unlabelled map[string]bool
|
||||
labelled map[string]bool
|
||||
ss, ps, addrs, routes, wg string
|
||||
unlabelled map[string]bool
|
||||
labelled map[string]bool
|
||||
}
|
||||
|
||||
func (m machineRunner) run(_ context.Context, name string, args ...string) (string, error) {
|
||||
@@ -154,6 +158,8 @@ func (m machineRunner) run(_ context.Context, name string, args ...string) (stri
|
||||
return m.addrs, nil
|
||||
case name == "ip" && args[1] == "route":
|
||||
return m.routes, nil
|
||||
case name == "wg":
|
||||
return m.wg, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
@@ -295,3 +301,76 @@ func TestARerunOfGenesisFindsItsOwnPackageRegistry(t *testing.T) {
|
||||
t.Error("a container under the package registry's name on a fresh machine was not refused")
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0105: an adopted genesis takes over the tunnel it finds — its port is the hub's,
|
||||
// its range the mesh's, and neither is refused for being held by it.
|
||||
func TestAnAdoptedGenesisSettlesOnTheTunnelItFinds(t *testing.T) {
|
||||
private, err := aFoundKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conf := "[Interface]\nPrivateKey = " + private + "\nListenPort = 51900\nAddress = 192.0.2.1/24\n" +
|
||||
"[Peer]\nPublicKey = PEER=\nAllowedIPs = 192.0.2.2/32\n"
|
||||
tunnel.ReadFile = func(path string) ([]byte, error) {
|
||||
if path == tunnel.ConfigDir+"/wg0.conf" {
|
||||
return []byte(conf), nil
|
||||
}
|
||||
return nil, errors.New("no such file")
|
||||
}
|
||||
t.Cleanup(func() { tunnel.ReadFile = os.ReadFile })
|
||||
m := machineRunner{
|
||||
wg: "wg0\n",
|
||||
ss: "udp UNCONN 0 0 0.0.0.0:51900 0.0.0.0:*\n",
|
||||
addrs: "5: wg0 inet 192.0.2.1/24 scope global wg0\n",
|
||||
routes: "192.0.2.0/24 dev wg0 proto kernel scope link src 192.0.2.1\n",
|
||||
}
|
||||
|
||||
o := Options{Adopted: true, Ports: DefaultPorts(), OverlayRange: DefaultOverlayRange, State: filepath.Join(t.TempDir(), "state.json")}
|
||||
found, err := TakeTheTunnel(&o, m.run)
|
||||
if err != nil || found == nil {
|
||||
t.Fatalf("the tunnel was not found and taken: %+v %v", found, err)
|
||||
}
|
||||
if o.Tunnel != "wg0" || o.Ports.Hub != 51900 || o.OverlayRange != "192.0.2.0/24" {
|
||||
t.Fatalf("genesis did not settle on the tunnel's port and range: %+v", o)
|
||||
}
|
||||
// Its port is held by the tunnel and its range overlaps the tunnel's — by design, not refused.
|
||||
if err := CheckTheMachine(context.Background(), o, m.run, producedBundle(t).Declaration, func(string) {}); err != nil {
|
||||
t.Fatalf("the machine was refused for the tunnel it takes over: %v", err)
|
||||
}
|
||||
// Whereas the same machine not taking it over is refused on both counts (ADR 0100).
|
||||
plain := o
|
||||
plain.Tunnel = ""
|
||||
if err := CheckTheMachine(context.Background(), plain, m.run, producedBundle(t).Declaration, func(string) {}); err == nil ||
|
||||
!strings.Contains(err.Error(), "51900") {
|
||||
t.Fatalf("a tunnel not taken over stopped being refused for holding the hub's port: %v", err)
|
||||
}
|
||||
plain.Ports.Hub = 51821
|
||||
if err := CheckTheMachine(context.Background(), plain, m.run, producedBundle(t).Declaration, func(string) {}); err == nil ||
|
||||
!strings.Contains(err.Error(), "wg0") {
|
||||
t.Fatalf("a tunnel not taken over stopped being refused for overlapping the range: %v", err)
|
||||
}
|
||||
|
||||
// Numbers that disagree with the tunnel are refused, naming the tunnel's.
|
||||
for name, given := range map[string]Options{
|
||||
"--hub-port": {Adopted: true, Ports: FoundationPorts{Hub: 51821}, OverlayRange: DefaultOverlayRange},
|
||||
"--overlay-range": {Adopted: true, Ports: DefaultPorts(), OverlayRange: "10.77.0.0/16"},
|
||||
} {
|
||||
if _, err := TakeTheTunnel(&given, m.run); err == nil || !strings.Contains(err.Error(), name) {
|
||||
t.Errorf("a %s disagreeing with the tunnel was accepted: %v", name, err)
|
||||
}
|
||||
}
|
||||
// And no tunnel up is an ordinary machine.
|
||||
m.wg = "mesh0\n"
|
||||
none := Options{Adopted: true, Ports: DefaultPorts(), OverlayRange: DefaultOverlayRange}
|
||||
if found, err := TakeTheTunnel(&none, m.run); err != nil || found != nil || none.Ports.Hub != DefaultPorts().Hub {
|
||||
t.Errorf("a machine with no tunnel was not left as it was: %+v %v", found, err)
|
||||
}
|
||||
}
|
||||
|
||||
func aFoundKey() (string, error) {
|
||||
k, err := ecdh.X25519().GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(k.Bytes()), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user