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 --overlay-key ` — 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): } } }