Files
privatae-bastion/client-go/peers.go
T
Privatae 8822d5df85
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m51s
ci / fuzz (push) Successful in 2m55s
bastion 0.55.3
2026-10-25 23:16:45 +00:00

89 lines
4.4 KiB
Go

package main
// Peer announcement — how a machine introduces the hosts it serves.
//
// One daemon serves many hosts (config.go: one file per pairing under
// pairings/<host_name>.json). This box is therefore the ONLY party that knows
// the set: the platform cannot derive it, deliberately. Every pairing mints a
// fresh WireGuard keypair, and the "hardware fingerprint" is fresh random bytes
// per pairing (pair.go), so two pairings of one machine are unlinkable from the
// outside. That is the privacy property, and it is why discovery has to be the
// machine ANNOUNCING rather than the platform correlating.
//
// The rule: announcement is automatic and symmetric — pairing IS the
// consent, no approval step. Pair a bastion to a second host and both hosts
// learn of each other on the next heartbeat, which is what makes an IT team's
// hosts assemble themselves with zero steps. It is also how you find out that
// someone paired a host to a machine you own, so it only works if it is loud.
//
// WHAT TRAVELS: host name, handle, and when this box started serving it.
// NOTHING ELSE. Not keys, not grants, not context, not fingerprints. That line
// is load-bearing: multi-homing gives each pairing its own DCN dir precisely so
// that one entity's pinned key can never verify another entity's grants
// (config.go). Sharing a NAME is clear of that; carrying anything a peer could
// verify a grant with would be exactly the cross-host bleed that isolation
// exists to prevent.
// peerAnnounce is one OTHER host this machine serves, as told to a host on its
// own heartbeat. The receiving host treats it as a CLAIM by this bastion, not
// as proof, and it grants nothing.
type peerAnnounce struct {
HostName string `json:"host_name"`
Handle string `json:"handle"`
// State (anchor.go): active | pending | revoked. Without it a
// host that shares the box would show a PENDING host — one the anchor has
// not approved — as an established peer. Absent from bastions before it;
// the host reads absence as active, which is all those could have been.
State string `json:"state,omitempty"`
Since string `json:"since,omitempty"`
}
// peersFor returns every pairing on this box EXCEPT the one identified by
// selfHostName, newest-serving first by name order (loadPairings sorts).
//
// Read fresh on each beat rather than cached at start: `privatae-bastion pair`
// writes a new file into the same directory, and a peer the user just added
// must show up on the next heartbeat without restarting the daemon. That IS
// the popup — a cached list would make the feature arrive whenever the box
// next rebooted.
//
// Best-effort by design: any failure to read the directory returns nil, and the
// heartbeat simply carries no peers. A machine that cannot enumerate its own
// pairings must not fail its liveness beat over it.
func peersFor(selfHostName string) []peerAnnounce {
cfgs, err := loadPairings()
if err != nil {
return nil
}
out := make([]peerAnnounce, 0, len(cfgs))
for _, c := range cfgs {
// A pairing whose file this build refuses is never served, so it is
// no peer; pairingsReport (anchor.go) says why it is left out rather
// than sent as "invalid".
if c.HostName == "" || c.HostName == selfHostName || c.invalid != "" {
continue
}
// "Since" is first_seen when the pairing carries it (0.16.0+), else the
// file's mtime — the honest answer already on disk for older pairings,
// never faked with time.Now(). Omitted when neither can be read.
out = append(out, peerAnnounce{HostName: c.HostName, Handle: handleOf(c.HostName),
State: c.stateName(), Since: sinceOf(c)})
}
// EMPTY IS A REAL ANSWER, and it is NOT the same as nil.
//
// nil is returned above only when the pairing directory cannot be read: we
// do not know what this box serves, so we say nothing and the host keeps
// whatever it already has. An empty (non-nil) slice is this box saying "I
// enumerated my pairings and I serve only this host" — a genuine state
// change, and the one that happens when the user unpairs the last other
// host.
//
// Collapsing that into nil would make the change unsendable. The host keeps
// stored peers on an ABSENT key (its heartbeat upsert falls back to what it
// already holds when the key is missing) so that a bastion too old to
// report peers cannot retract them — so "none" arriving as "absent" would
// leave the Network app listing peers the machine no longer serves, with
// nothing that could ever clear them.
return out
}