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. // // 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 if err := i.store.Pool().QueryRow(ctx, `select host(overlay_address) from node where id = $1`, node).Scan(&existing); err != nil { return "", err } if existing != nil && *existing != "" { return *existing, nil } taken := map[string]bool{} 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()] { 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 } 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, node) }