Carries MTU from the reported tunnel (mesh-host#28) through inventory, the overlay graph's TakeOver, into the generated config's [Interface]. A tuned path keeps its MTU across the takeover instead of regressing to 1420 and hanging transfers no ping would reveal. Two emit tests; a tunnel with no MTU writes no line.
422 lines
16 KiB
Go
422 lines
16 KiB
Go
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"`
|
|
// MTU is the found interface's, when it set one; the mesh's interface takes it over so a
|
|
// tuned path does not silently regress to the default (novox/hq: a taken tunnel carries its MTU).
|
|
MTU int `json:"mtu,omitempty"`
|
|
// 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
|
|
// Named is what the operator said this peer is, before it enrolled (novox/hq issue 112) —
|
|
// a statement the mesh records and cannot verify, which is why enrolment checks it.
|
|
Named 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, ''), coalesce(p.named, '')
|
|
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, &p.Named); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, p)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// NamePeer records the operator's statement that a carried address is a particular machine
|
|
// (novox/hq issue 112). Refused when nothing carried has that address, when a node of the mesh
|
|
// already has the name — the statement would collide with something verified — and when another
|
|
// peer was already named it. Naming an enrolled peer is refused too: its name is the node's now.
|
|
func (i *Inventory) NamePeer(ctx context.Context, address, name string) error {
|
|
peers, err := i.CarriedPeers(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var at *CarriedPeer
|
|
for idx := range peers {
|
|
if peers[idx].Address == address {
|
|
at = &peers[idx]
|
|
continue
|
|
}
|
|
if peers[idx].Named == name {
|
|
return fmt.Errorf("the carried peer at %s is already named %q — one machine per name",
|
|
peers[idx].Address, name)
|
|
}
|
|
}
|
|
if at == nil {
|
|
return fmt.Errorf("no carried peer has the address %s — `overlay show` lists them", address)
|
|
}
|
|
if at.EnrolledAs != "" {
|
|
return fmt.Errorf("the peer at %s enrolled as %q — its name is the node's now", address, at.EnrolledAs)
|
|
}
|
|
if _, err := i.NodeByName(ctx, name); err == nil {
|
|
return fmt.Errorf("%q is a node of this mesh — a carried peer cannot be named after one", name)
|
|
}
|
|
tag, err := i.store.Pool().Exec(ctx,
|
|
`update tunnel_peer set named = $2 where host(address) = $1`, address, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return fmt.Errorf("no carried peer has the address %s", address)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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
|
|
}
|