Adopt the predecessor's tunnel in place: its range, its address, its peers

On an adopted hub the private network takes over the tunnel it finds rather
than running beside it (hq ADR 0105): two tunnels leave the mesh's unreachable
through the provider's filter, so no machine can ever join.

The node presents the found tunnel when it enrols, under the key it took as
its own; the inventory records it (node.tunnel, tunnel_peer — migration 0031)
and the mesh composes from it: the overlay's range is the adopted tunnel's,
the hub is placed at the tunnel's address on the tunnel's port, and every
peer the tunnel had is carried in the hub's peer list as a peer of the
tunnel, not a node of the mesh, until a node enrols with that key — which
then keeps the address the tunnel had for it. A fresh node never gets an
address the tunnel holds. The hub's declaration tells the host which unit to
take over; the host's account of carrying it is recorded and shown.

Every reader of the range follows the setting; nothing stores it. A found
tunnel under another key is recorded and not adopted, so ADR 0100's
non-overlap rule keeps applying where a tunnel is left running beside the
mesh's. A lab bed and test skeleton for "How it is checked" are under lab/.
This commit is contained in:
2026-09-23 23:26:34 +02:00
parent 8fb32d7ee0
commit 3c836f0abb
16 changed files with 1351 additions and 42 deletions
+54 -12
View File
@@ -16,25 +16,64 @@ import (
// node's peer list to chase it, and an address that moves is the thing declaring the hub was
// meant to stop.
//
// Allocated in order from the range, taking the lowest free one. Not random: a person reading a
// peer list should be able to guess which node an address belongs to, and reuse of a released
// address is a smaller problem than a list nobody can hold in their head.
// **The adopted tunnel's addresses come first** (novox/hq ADR 0105). The hub that took over a
// tunnel is at the tunnel's own address. A node enrolling with a key the tunnel already routed
// to keeps the address the tunnel had for it — nothing a peer knows changes. And an address the
// tunnel holds for a peer that has not enrolled is never handed to anyone else: that peer is
// still reaching the hub at it.
//
// The rest is allocated in order from the range, taking the lowest free one. Not random: a person
// reading a peer list should be able to guess which node an address belongs to, and reuse of a
// released address is a smaller problem than a list nobody can hold in their head.
func (i *Inventory) AssignAddress(ctx context.Context, node, cidr string) (string, error) {
prefix, err := netip.ParsePrefix(cidr)
if err != nil {
return "", fmt.Errorf("%q is not a network the mesh can allocate from: %w", cidr, err)
}
var existing *string
var existing, key *string
var name string
var hub bool
if err := i.store.Pool().QueryRow(ctx,
`select host(overlay_address) from node where id = $1`, node).Scan(&existing); err != nil {
`select name, host(overlay_address), overlay_key, is_hub from node where id = $1`, node).
Scan(&name, &existing, &key, &hub); err != nil {
return "", err
}
if existing != nil && *existing != "" {
return *existing, nil
}
tunnel, hubName, adopted, err := i.AdoptedTunnel(ctx)
if err != nil {
return "", err
}
if adopted && hub && hubName == name {
address, err := netip.ParsePrefix(tunnel.Address)
if err != nil {
return "", fmt.Errorf("the adopted tunnel's address %q: %w", tunnel.Address, err)
}
return i.place(ctx, node, address.Addr().String())
}
carried, err := i.CarriedPeers(ctx)
if err != nil {
return "", err
}
taken := map[string]bool{}
if adopted {
// The tunnel's own address is the hub's whether or not the hub has been placed yet.
if address, err := netip.ParsePrefix(tunnel.Address); err == nil {
taken[address.Addr().String()] = true
}
}
for _, p := range carried {
if key != nil && p.PublicKey == *key {
// The tunnel already routes to this key: the node keeps that address, and the peer
// notices nothing when its machine enrols.
return i.place(ctx, node, p.Address)
}
taken[p.Address] = true
}
rows, err := i.store.Pool().Query(ctx,
`select host(overlay_address) from node where overlay_address is not null`)
if err != nil {
@@ -58,12 +97,7 @@ func (i *Inventory) AssignAddress(ctx context.Context, node, cidr string) (strin
candidate := prefix.Masked().Addr().Next()
for prefix.Contains(candidate) {
if !taken[candidate.String()] {
if _, err := i.store.Pool().Exec(ctx,
`update node set overlay_address = $2::inet where id = $1`,
node, candidate.String()); err != nil {
return "", err
}
return candidate.String(), nil
return i.place(ctx, node, candidate.String())
}
candidate = candidate.Next()
}
@@ -72,5 +106,13 @@ func (i *Inventory) AssignAddress(ctx context.Context, node, cidr string) (strin
// halfway through assigning one node.
return "", fmt.Errorf(
"every address in %s is taken, so %s cannot be given one. The mesh has outgrown its "+
"range and renumbering it is a deliberate act", cidr, node)
"range and renumbering it is a deliberate act", cidr, name)
}
func (i *Inventory) place(ctx context.Context, node, address string) (string, error) {
if _, err := i.store.Pool().Exec(ctx,
`update node set overlay_address = $2::inet where id = $1`, node, address); err != nil {
return "", err
}
return address, nil
}
@@ -0,0 +1,27 @@
-- The tunnel a node found on its machine, and the peers it carried (novox/hq ADR 0105).
--
-- On an adopted node that is the hub, the private network takes over the tunnel it finds: its
-- key, its port, its address and range, and every peer. The node presents what it found when it
-- enrols -- the same moment it presents its keys, because the found tunnel's key IS its key on the
-- private network from then on -- and the mesh composes every address from it.
-- What the node presented: interface, unit and configuration path, port, address and range, and
-- the tunnel's public key. The private key never travels; the node keeps it as its own overlay
-- key. Null on a node that found no tunnel, which is every converged one.
alter table node add column tunnel jsonb;
-- The node's last account of carrying it: the found interface down and disabled, the mesh's up
-- in its place. Null until the node says so.
alter table node add column tunnel_carried jsonb;
-- The peers the found tunnel had: a public key and the address the tunnel routed to it. Peers of
-- the tunnel, not nodes of the mesh, until they enrol -- a machine the mesh has no record of, whose
-- identity precedes its enrolment. One row per key, and one address per key on one tunnel.
create table tunnel_peer (
node uuid not null references node(id) on delete cascade,
public_key text not null,
address inet not null,
since timestamptz not null default now(),
primary key (node, public_key),
unique (node, address)
);
+313
View File
@@ -0,0 +1,313 @@
package inventory
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/netip"
"strings"
"time"
"github.com/jackc/pgx/v5"
)
// The tunnel a node found on its machine, and the peers it carried (novox/hq ADR 0105).
//
// On an adopted node that is the hub, the private network takes over the tunnel it finds: its
// private key, its port, its address and range, and every peer the found interface had. The node
// presents what it found when it enrols, in the same breath as its keys — the found tunnel's key is
// its key on the private network from then on — and the mesh composes every address from it: the
// hub's is the tunnel's, the range is the tunnel's, and a peer that enrols keeps the address the
// tunnel already had for its key.
// Tunnel is what a node found on its machine, as it presented it. No private key: the node keeps
// it as its own overlay key, sealed like any own secret, and the mesh records only the public half
// — which is then the node's overlay key too.
type Tunnel struct {
// Interface, Unit and Config are what the host takes over: the found interface, the unit
// that raised it, and its configuration file, which is kept like any held file.
Interface string `json:"interface"`
Unit string `json:"unit"`
Config string `json:"config"`
// Port is the port the found interface listened on — one the hosting provider already lets
// through, which is why it is worth taking.
Port int `json:"port"`
// Address is the interface's own address with its prefix length, 192.0.2.1/24; Range is the
// network that prefix names, 192.0.2.0/24.
Address string `json:"address"`
Range string `json:"range"`
// PublicKey is the found interface's, which every peer knows the tunnel by.
PublicKey string `json:"public_key"`
// Peers are the found interface's peers: each a public key and the address the tunnel routed
// to it.
Peers []TunnelPeer `json:"peers,omitempty"`
// At is when the node presented it; zero on a tunnel not yet recorded.
At time.Time `json:"at,omitempty"`
}
// TunnelPeer is one peer of a found tunnel.
type TunnelPeer struct {
PublicKey string `json:"public_key"`
// Address is the one host address the tunnel routed to the peer, without a prefix.
Address string `json:"address"`
}
// Carried is what a node last said about carrying the tunnel it found: the found interface down
// and disabled, the mesh's up in its place with the found key.
type Carried struct {
Interface string `json:"interface"`
Port int `json:"port"`
Range string `json:"range"`
Peers int `json:"peers"`
// Taken is whether the found interface is down and the mesh's up with its key; Kept is where
// the found configuration's original was kept.
Taken bool `json:"taken"`
Kept string `json:"kept,omitempty"`
At time.Time `json:"at"`
}
// CarriedPeer is one peer of the adopted tunnel as the mesh holds it: a peer of the tunnel, and
// — once a node enrols with that key — a node of the mesh as well.
type CarriedPeer struct {
PublicKey string
Address string
// EnrolledAs names the node that enrolled with this key, or is empty while none has.
EnrolledAs string
}
// ErrNoTunnel is asking about a tunnel on a node that presented none.
var ErrNoTunnel = errors.New("that node presented no tunnel")
// RecordTunnel keeps what a node presented, replacing what was there: the question is the tunnel
// as the node found it now. The peers are replaced whole for the same reason.
func (i *Inventory) RecordTunnel(ctx context.Context, nodeID string, t Tunnel) error {
if strings.TrimSpace(t.Interface) == "" || strings.TrimSpace(t.PublicKey) == "" {
return errors.New("a found tunnel names its interface and its public key, and this names neither")
}
if _, err := netip.ParsePrefix(t.Range); err != nil {
return fmt.Errorf("the found tunnel's range %q is not a range: %w", t.Range, err)
}
address, err := netip.ParsePrefix(t.Address)
if err != nil {
return fmt.Errorf("the found tunnel's address %q is not an address with a prefix: %w", t.Address, err)
}
peers := make([]TunnelPeer, 0, len(t.Peers))
for _, p := range t.Peers {
host, err := peerHost(p.Address)
if err != nil {
return fmt.Errorf("the found tunnel's peer %s: %w", shortKey(p.PublicKey), err)
}
if !address.Masked().Contains(host) {
return fmt.Errorf("the found tunnel's peer %s is routed at %s, outside the tunnel's %s",
shortKey(p.PublicKey), host, t.Range)
}
peers = append(peers, TunnelPeer{PublicKey: p.PublicKey, Address: host.String()})
}
t.Peers = nil
t.At = time.Now().UTC()
raw, err := json.Marshal(t)
if err != nil {
return err
}
tx, err := i.store.Pool().Begin(ctx)
if err != nil {
return err
}
defer func() { _ = tx.Rollback(context.WithoutCancel(ctx)) }()
if _, err := tx.Exec(ctx, `update node set tunnel = $2 where id = $1`, nodeID, raw); err != nil {
return err
}
if _, err := tx.Exec(ctx, `delete from tunnel_peer where node = $1`, nodeID); err != nil {
return err
}
for _, p := range peers {
if _, err := tx.Exec(ctx,
`insert into tunnel_peer (node, public_key, address) values ($1, $2, $3::inet)`,
nodeID, p.PublicKey, p.Address); err != nil {
return fmt.Errorf("recording the found tunnel's peer %s: %w", shortKey(p.PublicKey), err)
}
}
return tx.Commit(ctx)
}
// peerHost is the one host address a peer's allowed address names: a bare address, or a /32
// (or /128). A wider prefix is refused — a peer routed a whole range is not a machine with an
// address the mesh could give a node.
func peerHost(allowed string) (netip.Addr, error) {
allowed = strings.TrimSpace(allowed)
if a, err := netip.ParseAddr(allowed); err == nil {
return a, nil
}
p, err := netip.ParsePrefix(allowed)
if err != nil {
return netip.Addr{}, fmt.Errorf("%q is not an address", allowed)
}
if !p.IsSingleIP() {
return netip.Addr{}, fmt.Errorf("%q names a range, and a peer of the tunnel is one address", allowed)
}
return p.Addr(), nil
}
func shortKey(key string) string {
if len(key) > 8 {
return key[:8] + "…"
}
return key
}
// TunnelOf is the tunnel a node presented, with its peers, or ErrNoTunnel.
func (i *Inventory) TunnelOf(ctx context.Context, name string) (Tunnel, error) {
var raw []byte
var id string
err := i.store.Pool().QueryRow(ctx,
`select id, tunnel from node where name = $1`, name).Scan(&id, &raw)
if errors.Is(err, pgx.ErrNoRows) {
return Tunnel{}, fmt.Errorf("%w: %s", ErrNoSuchNode, name)
}
if err != nil {
return Tunnel{}, err
}
if len(raw) == 0 {
return Tunnel{}, fmt.Errorf("%w: %s", ErrNoTunnel, name)
}
var t Tunnel
if err := json.Unmarshal(raw, &t); err != nil {
return Tunnel{}, err
}
t.Peers, err = i.tunnelPeers(ctx, id)
return t, err
}
func (i *Inventory) tunnelPeers(ctx context.Context, nodeID string) ([]TunnelPeer, error) {
rows, err := i.store.Pool().Query(ctx,
`select public_key, host(address) from tunnel_peer where node = $1 order by address`, nodeID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []TunnelPeer
for rows.Next() {
var p TunnelPeer
if err := rows.Scan(&p.PublicKey, &p.Address); err != nil {
return nil, err
}
out = append(out, p)
}
return out, rows.Err()
}
// AdoptedTunnel is the tunnel the mesh's private network runs over, if the hub adopted one: the
// hub's found tunnel, when the hub is adopted and its overlay key is the tunnel's. Absent, the mesh
// runs on its own range — and a hub that found a tunnel but holds another key did not adopt it,
// which `overlay show` says.
//
// The condition on the key is the condition of the whole record: a hub raised with a key of its
// own would drop every peer's packets on the found port (novox/hq ADR 0105, option 2), so the
// tunnel is adopted only when the hub answers to the key its peers know.
func (i *Inventory) AdoptedTunnel(ctx context.Context) (Tunnel, string, bool, error) {
var name string
var key *string
var adopted bool
err := i.store.Pool().QueryRow(ctx,
`select name, overlay_key, adopted from node where is_hub and tunnel is not null`).
Scan(&name, &key, &adopted)
if errors.Is(err, pgx.ErrNoRows) {
return Tunnel{}, "", false, nil
}
if err != nil {
return Tunnel{}, "", false, err
}
t, err := i.TunnelOf(ctx, name)
if err != nil {
return Tunnel{}, "", false, err
}
if !adopted || key == nil || *key != t.PublicKey {
return t, name, false, nil
}
return t, name, true, nil
}
// CarriedPeers is every peer of the adopted tunnel, with the node that enrolled under its key
// where one has: peers of the tunnel, and nodes of the mesh once they enrol. Empty when the hub
// adopted no tunnel.
func (i *Inventory) CarriedPeers(ctx context.Context) ([]CarriedPeer, error) {
rows, err := i.store.Pool().Query(ctx,
`select p.public_key, host(p.address), coalesce(n.name, '')
from tunnel_peer p
join node hub on hub.id = p.node and hub.is_hub and hub.adopted
and hub.tunnel is not null and hub.overlay_key = hub.tunnel->>'public_key'
left join node n on n.overlay_key = p.public_key
order by p.address`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []CarriedPeer
for rows.Next() {
var p CarriedPeer
if err := rows.Scan(&p.PublicKey, &p.Address, &p.EnrolledAs); err != nil {
return nil, err
}
out = append(out, p)
}
return out, rows.Err()
}
// Tunnels is every adopted node's found tunnel by node name, for the ones whose overlay key is the
// tunnel's — the ones whose private network takes it over. A found tunnel under another key is
// left running beside the mesh's, and ADR 0100's rule that the ranges differ applies to it.
func (i *Inventory) Tunnels(ctx context.Context) (map[string]Tunnel, error) {
rows, err := i.store.Pool().Query(ctx,
`select name, tunnel from node
where adopted and tunnel is not null and overlay_key = tunnel->>'public_key'`)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]Tunnel{}
for rows.Next() {
var name string
var raw []byte
if err := rows.Scan(&name, &raw); err != nil {
return nil, err
}
var t Tunnel
if err := json.Unmarshal(raw, &t); err != nil {
return nil, err
}
out[name] = t
}
return out, rows.Err()
}
// RecordCarriedTunnel keeps what a node last said about carrying its found tunnel.
func (i *Inventory) RecordCarriedTunnel(ctx context.Context, nodeID string, c Carried) error {
c.At = time.Now().UTC()
raw, err := json.Marshal(c)
if err != nil {
return err
}
_, err = i.store.Pool().Exec(ctx, `update node set tunnel_carried = $2 where id = $1`, nodeID, raw)
return err
}
// CarriedTunnelOf is a node's last account of carrying its found tunnel, and whether it ever gave one.
func (i *Inventory) CarriedTunnelOf(ctx context.Context, name string) (Carried, bool, error) {
var raw []byte
err := i.store.Pool().QueryRow(ctx, `select tunnel_carried from node where name = $1`, name).Scan(&raw)
if errors.Is(err, pgx.ErrNoRows) {
return Carried{}, false, fmt.Errorf("%w: %s", ErrNoSuchNode, name)
}
if err != nil {
return Carried{}, false, err
}
if len(raw) == 0 {
return Carried{}, false, nil
}
var c Carried
if err := json.Unmarshal(raw, &c); err != nil {
return Carried{}, false, err
}
return c, true, nil
}
+175
View File
@@ -0,0 +1,175 @@
package inventory
import (
"errors"
"strings"
"testing"
)
// novox/hq ADR 0105: the mesh adopts the predecessor's tunnel in place. The controller reads the
// hub's address and range from the adopted tunnel, assigns an enrolling node the address its key
// already had, and refuses to hand out an address the tunnel already holds.
const (
tunnelKey = "TUNNEL-KEY-the-found-interfaces-public-key="
peerTwo = "PEER-KEY-two============================="
peerThree = "PEER-KEY-three==========================="
)
// theFoundTunnel is what a hub presents at enrolment: the predecessor's interface on a
// documentation range, with two peers each routed one address.
func theFoundTunnel() Tunnel {
return Tunnel{
Interface: "wg0", Unit: "wg-quick@wg0", Config: "/etc/wireguard/wg0.conf",
Port: 51900, Address: "192.0.2.1/24", Range: "192.0.2.0/24", PublicKey: tunnelKey,
Peers: []TunnelPeer{{PublicKey: peerTwo, Address: "192.0.2.2/32"},
{PublicKey: peerThree, Address: "192.0.2.3"}},
}
}
// anAdoptedHub is an adopted node that enrolled with the found tunnel's key and presented the
// tunnel, then was placed as the hub — the order genesis does it in.
func anAdoptedHub(t *testing.T, inv *Inventory) Node {
t.Helper()
hub, err := inv.AddNodeAs(t.Context(), "anchor", true)
if err != nil {
t.Fatal(err)
}
if err := inv.RecordOverlayKey(t.Context(), hub.ID, tunnelKey); err != nil {
t.Fatal(err)
}
if err := inv.RecordTunnel(t.Context(), hub.ID, theFoundTunnel()); err != nil {
t.Fatal(err)
}
if err := inv.SetPlace(t.Context(), "anchor", "anchor.example:51900", "hosting", true, ""); err != nil {
t.Fatal(err)
}
return hub
}
func TestTheHubsAddressAndRangeComeFromTheAdoptedTunnel(t *testing.T) {
inv := fresh(t)
hub := anAdoptedHub(t, inv)
tunnel, name, adopted, err := inv.AdoptedTunnel(t.Context())
if err != nil {
t.Fatal(err)
}
if !adopted || name != "anchor" || tunnel.Range != "192.0.2.0/24" || tunnel.Port != 51900 {
t.Fatalf("the adopted tunnel did not read back: adopted=%t on %s, %+v", adopted, name, tunnel)
}
if len(tunnel.Peers) != 2 || tunnel.Peers[0].Address != "192.0.2.2" || tunnel.Peers[1].Address != "192.0.2.3" {
t.Fatalf("the peers did not read back as one host address each: %+v", tunnel.Peers)
}
// Whatever range the caller would allocate from, the hub is at the tunnel's own address.
address, err := inv.AssignAddress(t.Context(), hub.ID, "10.42.0.0/16")
if err != nil {
t.Fatal(err)
}
if address != "192.0.2.1" {
t.Fatalf("the hub was given %s, not the address the tunnel it took over had", address)
}
}
func TestAnEnrollingNodeKeepsTheAddressTheTunnelHadForItsKey(t *testing.T) {
inv := fresh(t)
anAdoptedHub(t, inv)
// A predecessor machine enrols: its host took its found interface's key as its overlay key,
// which is the key the hub's tunnel already routes to.
peer, err := inv.AddNodeAs(t.Context(), "home-server", true)
if err != nil {
t.Fatal(err)
}
if err := inv.RecordOverlayKey(t.Context(), peer.ID, peerThree); err != nil {
t.Fatal(err)
}
address, err := inv.AssignAddress(t.Context(), peer.ID, "192.0.2.0/24")
if err != nil {
t.Fatal(err)
}
if address != "192.0.2.3" {
t.Fatalf("the enrolling peer was given %s, not the 192.0.2.3 the tunnel had for its key", address)
}
carried, err := inv.CarriedPeers(t.Context())
if err != nil {
t.Fatal(err)
}
byKey := map[string]CarriedPeer{}
for _, c := range carried {
byKey[c.PublicKey] = c
}
if byKey[peerThree].EnrolledAs != "home-server" || byKey[peerTwo].EnrolledAs != "" {
t.Fatalf("the registry cannot say which peer is a node now: %+v", carried)
}
}
func TestAFreshNodeIsNeverGivenAnAddressTheTunnelHolds(t *testing.T) {
inv := fresh(t)
anAdoptedHub(t, inv)
// .1 is the hub, .2 and .3 are peers of the tunnel that have not enrolled: a new machine with
// a key of its own gets the next one, from the same range.
fresh, err := inv.AddNode(t.Context(), "laptop")
if err != nil {
t.Fatal(err)
}
if err := inv.RecordOverlayKey(t.Context(), fresh.ID, "A-KEY-OF-ITS-OWN========================"); err != nil {
t.Fatal(err)
}
address, err := inv.AssignAddress(t.Context(), fresh.ID, "192.0.2.0/24")
if err != nil {
t.Fatal(err)
}
if address != "192.0.2.4" {
t.Fatalf("a fresh node was given %s; 192.0.2.2 and .3 are the tunnel's peers and .1 its hub", address)
}
}
func TestATunnelUnderAnotherKeyIsNotAdopted(t *testing.T) {
// A hub whose overlay key is not the found tunnel's would drop every peer's packets on the
// found port (ADR 0105, option 2). Such a tunnel is recorded and not adopted: the mesh keeps
// its own range, and ADR 0100's non-overlap rule stands for it.
inv := fresh(t)
hub, err := inv.AddNodeAs(t.Context(), "anchor", true)
if err != nil {
t.Fatal(err)
}
if err := inv.RecordOverlayKey(t.Context(), hub.ID, "THE-MESHS-OWN-KEY======================="); err != nil {
t.Fatal(err)
}
if err := inv.RecordTunnel(t.Context(), hub.ID, theFoundTunnel()); err != nil {
t.Fatal(err)
}
if err := inv.SetPlace(t.Context(), "anchor", "anchor.example:51820", "hosting", true, ""); err != nil {
t.Fatal(err)
}
if _, _, adopted, err := inv.AdoptedTunnel(t.Context()); err != nil || adopted {
t.Fatalf("a tunnel under another key was adopted (err %v)", err)
}
if carried, err := inv.CarriedPeers(t.Context()); err != nil || len(carried) != 0 {
t.Fatalf("peers of a tunnel that was not adopted are carried: %+v (err %v)", carried, err)
}
if address, err := inv.AssignAddress(t.Context(), hub.ID, "10.42.0.0/16"); err != nil || address != "10.42.0.1" {
t.Fatalf("the hub was given %s (err %v); it should allocate from the mesh's own range", address, err)
}
}
func TestAPeerRoutedARangeIsRefused(t *testing.T) {
inv := fresh(t)
hub, err := inv.AddNodeAs(t.Context(), "anchor", true)
if err != nil {
t.Fatal(err)
}
found := theFoundTunnel()
found.Peers = append(found.Peers, TunnelPeer{PublicKey: "WIDE", Address: "198.51.100.0/24"})
err = inv.RecordTunnel(t.Context(), hub.ID, found)
if err == nil || !strings.Contains(err.Error(), "names a range") {
t.Fatalf("a peer routed a whole range was recorded as a machine with an address: %v", err)
}
if _, err := inv.TunnelOf(t.Context(), "anchor"); !errors.Is(err, ErrNoTunnel) {
t.Fatalf("a refused tunnel was recorded anyway: %v", err)
}
}