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 }