Merge pull request 'A machine joins through the tunnel: a token issued for its tunnel key (hq ADR 0169)' (#132) from feat/a-machine-joins-through-the-tunnel into main
This commit was merged in pull request #132.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
)
|
||||
|
||||
// aMachineJoining is a machine with a live token issued for a tunnel key, given its address — what
|
||||
// `token issue --overlay-key` records before it pushes the hub (novox/hq ADR 0169).
|
||||
func aMachineJoining(t *testing.T, ctx context.Context, inv *inventory.Inventory, name string,
|
||||
validFor time.Duration) string {
|
||||
t.Helper()
|
||||
record, err := inv.AddNode(ctx, name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := inv.IssueToken(ctx, name, validFor); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key := aPublicKey(t)
|
||||
if err := inv.RecordOverlayKey(ctx, record.ID, key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.BindTokenToKey(ctx, record.ID, key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cidr, err := overlayRange(ctx, inv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := inv.AssignAddress(ctx, record.ID, cidr); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// hubPeers are the keys the hub's composed tunnel carries.
|
||||
func hubPeers(t *testing.T, ctx context.Context, inv *inventory.Inventory) map[string]bool {
|
||||
t.Helper()
|
||||
g, err := network(ctx, inv, map[string]bool{"anchor": true, "laptop": true}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := map[string]bool{}
|
||||
for _, p := range g.Graph()["anchor"] {
|
||||
out[p.Key] = true
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// **A machine joining is a peer of the hub while its token can be used, and not after** (novox/hq
|
||||
// ADR 0169): the hub's composed tunnel carries a machine whose token was issued for its key, so the
|
||||
// tunnel answers the first time it knocks; a token that expired unused takes the peer with it at the
|
||||
// hub's next composition.
|
||||
func TestAMachineJoiningIsAPeerOfTheHubUntilItsTokenExpires(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
inv := open.inventory
|
||||
|
||||
joining := aMachineJoining(t, ctx, inv, "joiner", time.Hour)
|
||||
expired := aMachineJoining(t, ctx, inv, "late", 2*time.Second)
|
||||
time.Sleep(2100 * time.Millisecond)
|
||||
|
||||
peers := hubPeers(t, ctx, inv)
|
||||
if !peers[joining] {
|
||||
t.Fatalf("the hub does not carry the machine its live token was issued for: %v", peers)
|
||||
}
|
||||
if peers[expired] {
|
||||
t.Fatalf("the hub still carries a machine whose token expired unused: %v", peers)
|
||||
}
|
||||
|
||||
// A token issued without a key gives the hub nothing to carry: there is no key to carry.
|
||||
if _, err := inv.AddNode(ctx, "keyless"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := inv.IssueToken(ctx, "keyless", time.Hour); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if after := hubPeers(t, ctx, inv); len(after) != len(peers) {
|
||||
t.Fatalf("a token issued without a key changed the hub's peers: %v, was %v", after, peers)
|
||||
}
|
||||
}
|
||||
|
||||
// **A tunnel key that is not one is refused before anything is recorded** (novox/hq ADR 0169): a
|
||||
// token issued for it would be a tunnel the hub could never answer.
|
||||
func TestATokenIsNotIssuedForSomethingThatIsNotATunnelKey(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
record, err := open.inventory.AddNode(ctx, "joiner")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := throughTheTunnel(ctx, open, record, "not-a-key", "10.77.0.1:4222"); err == nil {
|
||||
t.Fatal("a token was issued for something that is not a tunnel key")
|
||||
}
|
||||
if held := placementOf(t, ctx, open.inventory, "joiner"); held.Key != "" || held.Address != "" {
|
||||
t.Fatalf("a refused key left a record behind: %+v", held)
|
||||
}
|
||||
}
|
||||
@@ -232,9 +232,22 @@ func network(ctx context.Context, inv *inventory.Inventory, on map[string]bool,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// **A machine joining is on the network before it is anything else** (novox/hq ADR 0169). Its
|
||||
// token was issued for its tunnel key and gave it an address, so while that token can still be
|
||||
// used the hub carries it as a peer: it brings its tunnel up from the token and enrols over it.
|
||||
// When the token is spent the machine is on the network by what it runs, as every other is; when
|
||||
// it expires unused, the peer goes with it at the hub's next composition.
|
||||
joining, err := inv.NodesWithALiveToken(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
isJoining := map[string]bool{}
|
||||
for _, name := range joining {
|
||||
isJoining[name] = true
|
||||
}
|
||||
nodes := make([]overlay.Node, 0, len(places))
|
||||
for _, p := range places {
|
||||
if !on[p.Name] {
|
||||
if !on[p.Name] && !(isJoining[p.Name] && p.Key != "" && p.Address != "") {
|
||||
continue
|
||||
}
|
||||
n := overlay.Node{
|
||||
|
||||
@@ -2,15 +2,18 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/token"
|
||||
)
|
||||
@@ -247,6 +250,8 @@ func tokenCommand(ctx context.Context, args []string) error {
|
||||
validFor := set.Duration("for", time.Hour, "how long the token may be used")
|
||||
adopted := set.Bool("adopted", false,
|
||||
"the machine joining is in use: it is adopted, and keeps what is found on it")
|
||||
tunnelKey := set.String("overlay-key", "",
|
||||
"the public half of the tunnel key the machine made (`nox-mesh-host key`): it joins through the tunnel")
|
||||
if err := set.Parse(args[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -300,6 +305,14 @@ func tokenCommand(ctx context.Context, args []string) error {
|
||||
default:
|
||||
return err
|
||||
}
|
||||
// **Through the tunnel** (novox/hq ADR 0169): the machine's key recorded, its address given, the
|
||||
// hub sent it as a peer — all before the token is shown, so the tunnel answers the first time the
|
||||
// machine knocks. The bus is then reached at its address on the private network.
|
||||
if *tunnelKey != "" {
|
||||
if made.Tunnel, made.Broker, err = throughTheTunnel(ctx, open, issued.Node, *tunnelKey, made.Broker); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
encoded, err := made.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -324,6 +337,78 @@ func tokenCommand(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// throughTheTunnel makes a machine a peer of the hub for its token, and says what the token carries
|
||||
// for it: its first tunnel, and the bus at its address on the private network (novox/hq ADR 0169).
|
||||
//
|
||||
// The hub is pushed here, before the token is shown. A token shown before the hub knew the key is a
|
||||
// tunnel that does not answer, and a machine that cannot tell that from a bus that is down.
|
||||
func throughTheTunnel(ctx context.Context, open *stores, node inventory.Node, key, busAt string) (
|
||||
*token.Tunnel, string, error) {
|
||||
inv := open.inventory
|
||||
key = strings.TrimSpace(key)
|
||||
if raw, err := base64.StdEncoding.DecodeString(key); err != nil || len(raw) != 32 {
|
||||
return nil, "", fmt.Errorf("%q is not a tunnel public key: it is 32 bytes in base64, as "+
|
||||
"`nox-mesh-host key` prints it", key)
|
||||
}
|
||||
// The bus on the private network is its holder's address at the bus's own port, so the port must
|
||||
// be known before anything is recorded.
|
||||
_, port, err := net.SplitHostPort(busAt)
|
||||
if err != nil || port == "" {
|
||||
return nil, "", fmt.Errorf("the bus's address %q has no port to reach it on", busAt)
|
||||
}
|
||||
places, err := inv.Overlays(ctx)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
var hub *inventory.Overlay
|
||||
for i := range places {
|
||||
if places[i].Hub {
|
||||
hub = &places[i]
|
||||
}
|
||||
}
|
||||
if hub == nil || hub.Key == "" || hub.Endpoint == "" || hub.Address == "" {
|
||||
return nil, "", errors.New("this mesh has no hub with a key, an address and an endpoint to " +
|
||||
"dial, so there is no tunnel to join through: place one (`overlay place <node> --hub " +
|
||||
"--endpoint <host>:<port>`), or issue the token without --overlay-key")
|
||||
}
|
||||
if err := inv.RecordOverlayKey(ctx, node.ID, key); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if err := inv.BindTokenToKey(ctx, node.ID, key); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
cidr, err := overlayRange(ctx, inv)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
address, err := inv.AssignAddress(ctx, node.ID, cidr)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
// The bus at its holder's address on the private network, reached through the hub like the rest
|
||||
// of the range; the hub's own when no machine is recorded as holding it yet.
|
||||
busAddress := hub.Address
|
||||
if holders, err := seatHolders(ctx, inv); err != nil {
|
||||
return nil, "", err
|
||||
} else if h, held := holders[catalogue.BrokerSeat]; held {
|
||||
for _, p := range places {
|
||||
if p.Name == h.Node && p.Address != "" {
|
||||
busAddress = p.Address
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := sendTo(ctx, open, []string{hub.Name}); err != nil {
|
||||
return nil, "", fmt.Errorf("%s was made a peer of the hub, and the hub could not be sent "+
|
||||
"it, so the tunnel would not answer — the token is not shown; issue it again once %s "+
|
||||
"can be pushed: %w", node.Name, hub.Name, err)
|
||||
}
|
||||
// An address, not a name — nothing resolves before the machine has joined (novox/hq ADR 0004).
|
||||
return &token.Tunnel{
|
||||
Key: key, Address: address + "/32", Range: cidr,
|
||||
HubKey: hub.Key, HubEndpoint: hub.Endpoint,
|
||||
}, net.JoinHostPort(busAddress, port), nil
|
||||
}
|
||||
|
||||
// issueFor is the inventory's half of issuing a token: the record, made when it is new, adopted
|
||||
// when the operator says so, and the one-time secret for it. The node in what it returns carries
|
||||
// its mode, which is what the token says.
|
||||
|
||||
@@ -675,6 +675,26 @@ func (a *verbArguments) commandLine() ([]string, error) {
|
||||
// Neither shape: the command says its usage, which names both, and that is the answer the
|
||||
// caller needs.
|
||||
return []string{"rotate"}, nil
|
||||
case "token":
|
||||
// `token issue` at a shell (novox/hq ADR 0169). Exactly one of node or new; the command
|
||||
// refuses both or neither in its own words.
|
||||
argv := []string{"token", "issue"}
|
||||
if n := str("node"); n != "" {
|
||||
argv = append(argv, "--node", n)
|
||||
}
|
||||
if n := str("new"); n != "" {
|
||||
argv = append(argv, "--new", n)
|
||||
}
|
||||
if k := str("overlay_key"); k != "" {
|
||||
argv = append(argv, "--overlay-key", k)
|
||||
}
|
||||
if d := str("for"); d != "" {
|
||||
argv = append(argv, "--for", d)
|
||||
}
|
||||
if str("adopted") == "true" {
|
||||
argv = append(argv, "--adopted")
|
||||
}
|
||||
return argv, nil
|
||||
case "settings":
|
||||
// `settings set|clear` at a shell (novox/hq issue 198). The values travel as an argument
|
||||
// because a tool has no file to hand the command; the command reads either.
|
||||
|
||||
@@ -270,6 +270,8 @@ var accountedFlags = map[string]map[string]string{
|
||||
},
|
||||
"builds": {"n": "=limit"},
|
||||
"plans": {"n": "=limit", "what-if": "=repository"},
|
||||
// The machine's tunnel key, named as the verb's other arguments are (novox/hq ADR 0169).
|
||||
"token issue": {"overlay-key": "=overlay_key"},
|
||||
"durations": {
|
||||
"json": "set by the verb: the answer is data",
|
||||
"all": "withheld: every measurement of a fortnight is more than a call should carry; `command` reaches it",
|
||||
|
||||
@@ -108,6 +108,14 @@ func TestRotateTakesAProvisionOrAnOwnSecret(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// `token` is `token issue` at a shell, with the machine's tunnel key (novox/hq ADR 0169).
|
||||
func TestTokenIssuesForAMachineAndItsTunnelKey(t *testing.T) {
|
||||
argv, err := argvFor("token", map[string]any{"new": "laptop", "overlay_key": "k", "for": "2h"})
|
||||
if err != nil || strings.Join(argv, " ") != "token issue --new laptop --overlay-key k --for 2h" {
|
||||
t.Fatalf("token: %v %v", argv, err)
|
||||
}
|
||||
}
|
||||
|
||||
// `settings` is `settings set|clear` at a shell, with the values passed inline (novox/hq issue 198).
|
||||
func TestSettingsSetsOrClearsALayer(t *testing.T) {
|
||||
argv, err := argvFor("settings", map[string]any{"module": "dnsmasq", "values": `{"a":1}`, "node": "ace"})
|
||||
|
||||
Reference in New Issue
Block a user