Merge pull request 'Take over the found tunnel: its key, its port, its peers; stop it, never flush (hq ADR 0105)' (#24) from feat/adopt-the-tunnel into main
This commit was merged in pull request #24.
This commit is contained in:
+136
-6
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/novox/mesh-host/internal/reachable"
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
"github.com/novox/mesh-host/internal/system"
|
||||
"github.com/novox/mesh-host/internal/tunnel"
|
||||
"github.com/novox/mesh-host/internal/upgrade"
|
||||
)
|
||||
|
||||
@@ -57,6 +58,8 @@ 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
|
||||
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
|
||||
version
|
||||
|
||||
@@ -93,6 +96,7 @@ type options struct {
|
||||
state string
|
||||
token string
|
||||
nodeName string
|
||||
tunnel string
|
||||
dryRun bool
|
||||
file string
|
||||
// out is where what a command says goes. Stdout, and a buffer under test.
|
||||
@@ -123,6 +127,8 @@ func parseArgs(args []string) (string, options, error) {
|
||||
set.BoolVar(&opts.dryRun, "dry-run", false, "read and check the declaration, change nothing")
|
||||
set.StringVar(&opts.token, "token", "", "enrol: the one-time token, carried here by a person")
|
||||
set.StringVar(&opts.nodeName, "name", "", "enrol: override the name the token carries")
|
||||
set.StringVar(&opts.tunnel, "tunnel", "", "enrol, adopted: the found tunnel's interface whose key "+
|
||||
"this node takes as its own; found by itself when one is up")
|
||||
|
||||
// Parsed in a loop, because the standard library stops at the FIRST non-flag argument.
|
||||
// `mesh-host inventory --json` hit that once, and taking the subcommand off the front
|
||||
@@ -150,6 +156,13 @@ func parseArgs(args []string) (string, options, error) {
|
||||
opts.file = positionals[0]
|
||||
return command, opts, nil
|
||||
}
|
||||
if command == "overlay" {
|
||||
if len(positionals) != 1 {
|
||||
return "", opts, errors.New("overlay take [--tunnel <iface>]")
|
||||
}
|
||||
opts.file = positionals[0]
|
||||
return command, opts, nil
|
||||
}
|
||||
// Anything left over was neither the command nor a flag. Refused rather than ignored: a
|
||||
// mistyped argument that changes nothing and reports success is worse than an error.
|
||||
if len(positionals) > 0 {
|
||||
@@ -233,6 +246,8 @@ func run(ctx context.Context, command string, opts options) error {
|
||||
|
||||
case "enrol", "enroll":
|
||||
return enrol(ctx, opts)
|
||||
case "overlay":
|
||||
return overlayCommand(ctx, opts)
|
||||
|
||||
case "run":
|
||||
return runLink(ctx, opts)
|
||||
@@ -692,14 +707,39 @@ func enrol(ctx context.Context, opts options) error {
|
||||
}
|
||||
fmt.Printf("generated this node's identity: %s\n", mine.PublicBase64())
|
||||
|
||||
// Its key on the private network, generated here and now for the same reason: the private
|
||||
// Its key on the private network. Generated here and now, for the same reason: the private
|
||||
// half must never have been anywhere else. The mesh receives only the public half and uses it
|
||||
// to compute a graph it cannot impersonate.
|
||||
mine.Overlay, err = identity.GenerateOverlayKey()
|
||||
if err != nil {
|
||||
return err
|
||||
//
|
||||
// **Except on an adopted node with a tunnel** (novox/hq ADR 0105): the found interface's key
|
||||
// becomes this node's, so the peers that know the tunnel by that key keep reaching it once
|
||||
// the mesh's interface takes the tunnel over. The one case where the mesh takes a credential
|
||||
// it did not mint — read from the found configuration, written where a generated one is
|
||||
// written, never printed, never sent.
|
||||
var found *link.Tunnel
|
||||
if token.Adopted {
|
||||
tun, err := tunnel.Find(ctx, apply.ExecRunner, opts.tunnel)
|
||||
switch {
|
||||
case errors.Is(err, tunnel.ErrNone):
|
||||
fmt.Println("no tunnel is up on this machine; the private network's key is generated")
|
||||
case err != nil:
|
||||
return err
|
||||
default:
|
||||
mine.Overlay, err = identity.OverlayKeyFrom(tun.PrivateKey())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
found = carried(tun)
|
||||
fmt.Printf("this node's overlay key is the found tunnel's (%s): %s\n", tun, mine.Overlay.Public)
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
mine.Overlay, err = identity.GenerateOverlayKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("generated this node's overlay key: %s\n", mine.Overlay.Public)
|
||||
}
|
||||
fmt.Printf("generated this node's overlay key: %s\n", mine.Overlay.Public)
|
||||
|
||||
// And the key secrets are sealed to. Here, with the others, because the mesh cannot seal
|
||||
// anything to a key it has not been told about — a key made later would leave a node that
|
||||
@@ -733,7 +773,8 @@ func enrol(ctx context.Context, opts options) error {
|
||||
proof := mine.Sign(link.EnrolProof(token.Secret, mine.Public, mine.Overlay.Public,
|
||||
sealing.Public, serving.Public))
|
||||
reply, err := link.Enrol(ctx, token.Broker, token.Fingerprint, *name, token.Secret,
|
||||
mine.Public, mine.Overlay.Public, sealing.Public, serving.Public, reported, proof, opts.timeout)
|
||||
mine.Public, mine.Overlay.Public, sealing.Public, serving.Public, reported, proof, found,
|
||||
opts.timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -792,6 +833,90 @@ func enrol(ctx context.Context, opts options) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// overlayCommand is `mesh-host overlay take`: this node takes the tunnel found on its machine over
|
||||
// after it enrolled (novox/hq ADR 0105). For a node that enrolled before the mesh knew to take a
|
||||
// tunnel over — re-enrolling would rotate its identity, sealing and serving keys, and with them
|
||||
// every credential the mesh sealed to it.
|
||||
func overlayCommand(ctx context.Context, opts options) error {
|
||||
if opts.file != "take" {
|
||||
return errors.New("overlay take [--tunnel <iface>] — the one thing `overlay` does here")
|
||||
}
|
||||
identityPath := identity.Path(opts.state)
|
||||
mine, err := identity.Load(identityPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
found, err := tunnel.Find(ctx, apply.ExecRunner, opts.tunnel)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w. Nothing was changed", err)
|
||||
}
|
||||
taken, rekey, err := rekeyOnto(mine, found)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("taking over %s: this node's overlay key becomes the tunnel's, %s\n", found, taken.Overlay.Public)
|
||||
fmt.Printf(" identity, sealing and serving keys are untouched\n")
|
||||
|
||||
// Told first, then written: a mesh told and a machine not yet written is put right by running
|
||||
// this again (the mesh refuses the stale second rekey and changes nothing; the files are
|
||||
// rewritten the same). A machine written and a mesh not told would raise the mesh's interface
|
||||
// on a key the mesh does not know at the next restart.
|
||||
if err := link.Publish(ctx, link.Membership{
|
||||
Node: mine.Node, Broker: mine.Membership.Broker, Fingerprint: mine.Membership.Fingerprint,
|
||||
Password: mine.Membership.Password, Signer: mine.Membership.Signer,
|
||||
}, link.Report{Node: mine.Node, Rekey: &rekey}, opts.timeout); err != nil {
|
||||
return fmt.Errorf("the mesh could not be told; nothing was written here: %w", err)
|
||||
}
|
||||
fmt.Printf(" told the mesh signed rekey sent; `node show %s` on the controller says whether it took\n", mine.Node)
|
||||
|
||||
if err := os.WriteFile(identity.OverlayKeyPath(opts.state),
|
||||
[]byte(taken.Overlay.Private+"\n"), 0o600); err != nil {
|
||||
return fmt.Errorf("the mesh was told and this node's overlay key could not be written: %w — run this again", err)
|
||||
}
|
||||
if err := identity.Save(identityPath, taken); err != nil {
|
||||
return fmt.Errorf("the mesh was told and this node's identity could not be saved: %w — run this again", err)
|
||||
}
|
||||
fmt.Printf(" written %s and the identity; the mesh's interface reads the key when the next push restarts it\n",
|
||||
identity.OverlayKeyPath(opts.state))
|
||||
fmt.Printf(" next on the controller: `overlay place %s --hub --endpoint <host>:%d …`, `plan %s --json`, then push\n",
|
||||
mine.Node, found.Port, mine.Node)
|
||||
return nil
|
||||
}
|
||||
|
||||
// rekeyOnto is the identity with the found tunnel's key as its overlay key, and the signed rekey
|
||||
// that tells the mesh. Pure, so it can be held to: node, sealing and serving keys are the same
|
||||
// bytes in and out; only the overlay key moves. Run again after a take, the previous key it names
|
||||
// is the one before the take, so the mesh can tell a repeat from a replay.
|
||||
func rekeyOnto(mine identity.Identity, found tunnel.Found) (identity.Identity, link.Rekey, error) {
|
||||
overlay, err := identity.OverlayKeyFrom(found.PrivateKey())
|
||||
if err != nil {
|
||||
return identity.Identity{}, link.Rekey{}, err
|
||||
}
|
||||
previous := mine.Overlay.Public
|
||||
if previous == overlay.Public && mine.OverlayBefore != "" {
|
||||
previous = mine.OverlayBefore
|
||||
}
|
||||
taken := mine
|
||||
taken.Overlay = overlay
|
||||
if previous != overlay.Public {
|
||||
taken.OverlayBefore = previous
|
||||
}
|
||||
presented := carried(found)
|
||||
rekey := link.Rekey{Previous: previous, OverlayKey: overlay.Public, Tunnel: presented}
|
||||
rekey.Proof = mine.Sign(link.RekeyProof(mine.Node, previous, overlay.Public, presented))
|
||||
return taken, rekey, nil
|
||||
}
|
||||
|
||||
// carried is a found tunnel as it is presented to the mesh: everything but its private key.
|
||||
func carried(t tunnel.Found) *link.Tunnel {
|
||||
out := &link.Tunnel{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: t.Port,
|
||||
Address: t.Address, Range: t.Range, PublicKey: t.PublicKey}
|
||||
for _, p := range t.Peers {
|
||||
out.Peers = append(out.Peers, link.TunnelPeer{PublicKey: p.PublicKey, Address: p.Address})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, v := range values {
|
||||
if strings.TrimSpace(v) != "" {
|
||||
@@ -1131,6 +1256,11 @@ func applyAndKeep(ctx context.Context, opts options, raw []byte, signed *store.D
|
||||
if updated.Firewall != nil {
|
||||
report.Firewall = updated.Firewall.Kind
|
||||
}
|
||||
// And the tunnel the private network took over, as this apply found it (novox/hq ADR 0105).
|
||||
if t := outcome.Tunnel; t != nil {
|
||||
report.Tunnel = &link.CarriedTunnel{Interface: t.Interface, Port: t.Port, Range: t.Range,
|
||||
Peers: t.Peers, State: t.State, Note: t.Note, Kept: t.Kept}
|
||||
}
|
||||
reached, err := reachable.Collect(ctx, apply.ExecRunner)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "mesh-host: applied, and could not read what is reachable here: %v\n", err)
|
||||
|
||||
Reference in New Issue
Block a user