package inventory import ( "context" "fmt" "net/netip" ) // The mesh assigns overlay addresses. A node computes nothing about the network it is joining — // it generates a keypair, publishes the public half, and receives the rest // (novox/hq 08-connectivity). // AssignAddress gives a node its place on the private network, if it has none. // // Idempotent: a node that already has an address keeps it. Reassigning would change every other // node's peer list to chase it, and an address that moves is the thing declaring the hub was // meant to stop. // // **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, key *string var name string var hub bool if err := i.store.Pool().QueryRow(ctx, `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. // // **Unless the operator named it something else** (novox/hq issue 112). The name is // what the mesh has been answering for this address in the meantime; a machine // enrolling under a different one would silently split the two — the name resolving // here, the node known as that — so it is refused where the operator can read it. if p.Named != "" && p.Named != name { return "", fmt.Errorf( "the carried peer at %s was named %q, and %q is enrolling under its key — "+ "enrol it as %q, or rename the peer first (`overlay name`)", p.Address, p.Named, name, p.Named) } 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 { return "", err } for rows.Next() { var a string if err := rows.Scan(&a); err != nil { rows.Close() return "", err } taken[a] = true } rows.Close() if err := rows.Err(); err != nil { return "", err } // The first address in a range is conventionally the network itself and is skipped, so // allocation starts one past it. candidate := prefix.Masked().Addr().Next() for prefix.Contains(candidate) { if !taken[candidate.String()] { return i.place(ctx, node, candidate.String()) } candidate = candidate.Next() } // Said plainly rather than returning an empty address that fails later on a machine. A mesh // that has outgrown its range needs a person, and renumbering is not something to attempt // 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, 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 }