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"` // State is one of the CarriedStates: the found interface is still up and the mesh's is not // (not taken), the found one is down and the mesh's up with its key (taken), or the found one // is down and the mesh's is not up — the one state where the peers reach nothing. Note is // what the host did about it, when it did something. Kept is where the found configuration's // original was kept. State string `json:"state"` Note string `json:"note,omitempty"` Kept string `json:"kept,omitempty"` At time.Time `json:"at"` } // The states a carried tunnel's account can be in, as the host says them. const ( CarriedNotTaken = "not-taken" CarriedTaken = "taken" CarriedDown = "down" ) // 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, single := peerHost(p.Address) if !single { // A peer routed a range rather than one address is a spoke's view of its hub — the // predecessor gives a spoke the whole subnet through the hub — and a hub is not a peer // the mesh carries. Skipped, not refused: a spoke enrols with what it found, and only // the hub's peers are ever carried (novox/hq ADR 0105). continue } 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) — and false for anything wider or unreadable: 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, bool) { allowed = strings.TrimSpace(allowed) if a, err := netip.ParseAddr(allowed); err == nil { return a, true } p, err := netip.ParsePrefix(allowed) if err != nil || !p.IsSingleIP() { return netip.Addr{}, false } return p.Addr(), true } 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's 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. **Not a condition on the // node's mode**: the range and the carried peers are facts of the mesh once the tunnel is taken, // and converging the hub — which flips its mode — must not renumber the mesh or drop the peers // still reaching it. func (i *Inventory) AdoptedTunnel(ctx context.Context) (Tunnel, string, bool, error) { var name string var key *string err := i.store.Pool().QueryRow(ctx, `select name, overlay_key from node where is_hub and tunnel is not null`). Scan(&name, &key) 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 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.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() } // FoundTunnel is a node's found tunnel with the node's mode, for composing: the takeover is // declared to an adopted node only, since only there is a found unit kept to be stopped. type FoundTunnel struct { Tunnel NodeAdopted bool } // Tunnels is every 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]FoundTunnel, error) { rows, err := i.store.Pool().Query(ctx, `select name, tunnel, adopted from node where tunnel is not null and overlay_key = tunnel->>'public_key'`) if err != nil { return nil, err } defer rows.Close() out := map[string]FoundTunnel{} for rows.Next() { var name string var raw []byte var adopted bool if err := rows.Scan(&name, &raw, &adopted); err != nil { return nil, err } var t Tunnel if err := json.Unmarshal(raw, &t); err != nil { return nil, err } out[name] = FoundTunnel{Tunnel: t, NodeAdopted: adopted} } return out, rows.Err() } // ErrStaleRekey is a rekey that names a previous overlay key other than the one recorded: a // replay of a rekey already done, or one made against a record that has since moved on. var ErrStaleRekey = errors.New("the rekey names a previous overlay key that is not the node's current one") // Rekey records that a node took a found tunnel's key as its overlay key after enrolling (novox/hq // ADR 0105): the key and the tunnel are recorded as enrolment would have, and a hub is moved to the // tunnel's address so nothing derived from it is stale. The caller has verified the node signed // for this; what is checked here is that it follows the record — `previous` is the overlay key the // node holds now — so the same message cannot be applied twice. func (i *Inventory) Rekey(ctx context.Context, nodeID, previous, key string, t Tunnel) error { if key != t.PublicKey { return errors.New("a rekey takes a tunnel over with the tunnel's own key, and this names another") } var current *string var hub bool if err := i.store.Pool().QueryRow(ctx, `select overlay_key, is_hub from node where id = $1`, nodeID).Scan(¤t, &hub); err != nil { return err } if (current == nil && previous != "") || (current != nil && *current != previous) { return ErrStaleRekey } if err := i.RecordOverlayKey(ctx, nodeID, key); err != nil { return err } if err := i.RecordTunnel(ctx, nodeID, t); err != nil { return err } if hub { address, err := netip.ParsePrefix(t.Address) if err != nil { return err } if _, err := i.place(ctx, nodeID, address.Addr().String()); err != nil { return err } } return nil } // 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 }