Adopt the tunnel as a mesh fact, refuse a mismatched takeover, and rekey after enrolment
Review of the ADR 0105 build (hq ADR 0105). Four things it got wrong and one path it lacked: - A predecessor spoke's tunnel names one peer, the hub, routed the whole range; recording refused it and the whole enrolment failed. Range-routed peers are skipped now — only the hub's peers are ever carried. - The range and the carried peers were conditions on the node being adopted, so converging the hub would have renumbered the mesh and dropped the peers still reaching it. They are facts of the tunnel record now, mode aside; the takeover alone is declared to an adopted node. Converging the hub is refused while a carried peer has not enrolled, naming it. - A push composed a takeover for a hub whose address or endpoint disagreed with the tunnel, which would have the host stop the found interface and raise the mesh's where no peer listens. The graph refuses to compose it, naming both and the placement that fixes it. - The host's account said taken or not; "found down and the mesh's not up" read as not taken. Three states now, and an account on every takeover. - A hub that enrolled before this feature holds a key of its own, and re-enrolling would rotate every key the mesh sealed credentials to. A node now rekeys in a report, signed with its identity key over the key it leaves, the key it takes and the tunnel; the mesh verifies against the live key, refuses a stale or foreign proof, records key and tunnel, and moves a hub to the tunnel's address. `overlay show` names the path for a hub that found no tunnel. Also: a carried IPv6 peer is routed /128, and identity.ForTest exists so the link can be tested against a real identity store.
This commit is contained in:
@@ -60,13 +60,24 @@ type Carried struct {
|
||||
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"`
|
||||
// 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 {
|
||||
@@ -94,9 +105,13 @@ func (i *Inventory) RecordTunnel(ctx context.Context, nodeID string, t Tunnel) e
|
||||
}
|
||||
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)
|
||||
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",
|
||||
@@ -131,22 +146,19 @@ func (i *Inventory) RecordTunnel(ctx context.Context, nodeID string, t Tunnel) e
|
||||
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) {
|
||||
// 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, nil
|
||||
return a, true
|
||||
}
|
||||
p, err := netip.ParsePrefix(allowed)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("%q is not an address", allowed)
|
||||
if err != nil || !p.IsSingleIP() {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
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
|
||||
return p.Addr(), true
|
||||
}
|
||||
|
||||
func shortKey(key string) string {
|
||||
@@ -198,20 +210,22 @@ func (i *Inventory) tunnelPeers(ctx context.Context, nodeID string) ([]TunnelPee
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
// 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
|
||||
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)
|
||||
`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
|
||||
}
|
||||
@@ -222,7 +236,7 @@ func (i *Inventory) AdoptedTunnel(ctx context.Context) (Tunnel, string, bool, er
|
||||
if err != nil {
|
||||
return Tunnel{}, "", false, err
|
||||
}
|
||||
if !adopted || key == nil || *key != t.PublicKey {
|
||||
if key == nil || *key != t.PublicKey {
|
||||
return t, name, false, nil
|
||||
}
|
||||
return t, name, true, nil
|
||||
@@ -235,7 +249,7 @@ 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
|
||||
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`)
|
||||
@@ -254,33 +268,81 @@ func (i *Inventory) CarriedPeers(ctx context.Context) ([]CarriedPeer, error) {
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// Tunnels is every adopted node's found tunnel by node name, for the ones whose overlay key is the
|
||||
// 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]Tunnel, error) {
|
||||
func (i *Inventory) Tunnels(ctx context.Context) (map[string]FoundTunnel, 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'`)
|
||||
`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]Tunnel{}
|
||||
out := map[string]FoundTunnel{}
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var raw []byte
|
||||
if err := rows.Scan(&name, &raw); err != nil {
|
||||
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] = t
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user