From 7181675c4a78cfa18fde9b294af789659b1fbb0e Mon Sep 17 00:00:00 2001 From: jochens Date: Fri, 2 Oct 2026 18:15:11 +0200 Subject: [PATCH] A joining machine dials the bus once the hub has answered its tunnel Issuing the token sends the hub its new peer, and the hub applies it on its own time; a bus dialled before then timed out naming the bus. The first tunnel now waits for a handshake with the hub, and says so in the tunnel's words when there is none (novox/hq ADR 0169). --- cmd/mesh-host/join_tunnel.go | 36 ++++++++++++++++++++++++++++++- cmd/mesh-host/join_tunnel_test.go | 27 +++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/cmd/mesh-host/join_tunnel.go b/cmd/mesh-host/join_tunnel.go index d72c64a..3a20318 100644 --- a/cmd/mesh-host/join_tunnel.go +++ b/cmd/mesh-host/join_tunnel.go @@ -8,6 +8,7 @@ import ( "os/exec" "path/filepath" "strings" + "time" "github.com/novox/mesh-host/internal/apply" "github.com/novox/mesh-host/internal/identity" @@ -115,5 +116,38 @@ func bringTheTunnelUp(ctx context.Context, t *identity.TokenTunnel, keyPath stri 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 nil + 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): + } + } } diff --git a/cmd/mesh-host/join_tunnel_test.go b/cmd/mesh-host/join_tunnel_test.go index 146fc7e..6f6757b 100644 --- a/cmd/mesh-host/join_tunnel_test.go +++ b/cmd/mesh-host/join_tunnel_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" "testing" + "time" "github.com/novox/mesh-host/internal/identity" ) @@ -67,6 +68,9 @@ func TestTheFirstTunnelIsTheMeshsInterfaceWithTheHubAsItsPeer(t *testing.T) { 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 } @@ -110,3 +114,26 @@ func captureStdout(t *testing.T, fn func()) string { 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) + } +}