bastion 0.55.3
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m51s
ci / fuzz (push) Successful in 2m55s

This commit is contained in:
Privatae
2026-10-25 23:16:45 +00:00
parent 77b7853ae1
commit 8822d5df85
136 changed files with 8638 additions and 1641 deletions
+37 -22
View File
@@ -76,12 +76,20 @@ type Config struct {
// that its file does not hold yet (rotate.go, setUnsaved; guarded by
// rotation.mu). Unexported: never in the file.
savePending bool
// invalid is why loadPairings found this pairing's file unusable (an
// endpoint that is not one host:port), naming the file; "" for a usable
// one. Such a pairing is listed, so status shows it and unpair removes it,
// but it never serves: active() is false. Unexported: never in the file.
invalid string
}
const (
pairingActive = "active"
pairingPending = "pending"
pairingRevoked = "revoked"
// pairingInvalid is reported, never stored: the file holds a value this
// build refuses (Config.invalid).
pairingInvalid = "invalid"
// transportUnix is Config.Transport for a Host on this machine.
transportUnix = "unix"
@@ -103,11 +111,15 @@ func (c *Config) localTransport() bool { return c.unixTransport() || c.tcpTransp
// active is the ONE place the state field is interpreted: "" and "active" serve,
// anything else does not. run.go, the heartbeat and the mgmt lane all ask this.
func (c *Config) active() bool {
return c.State == "" || c.State == pairingActive
return c.invalid == "" && (c.State == "" || c.State == pairingActive)
}
// stateName is the state as reported: the empty legacy value reads "active".
// stateName is the state as reported: the empty legacy value reads "active",
// and a pairing whose file is unusable reads "invalid" whatever its state.
func (c *Config) stateName() string {
if c.invalid != "" {
return pairingInvalid
}
if c.State == "" {
return pairingActive
}
@@ -316,17 +328,36 @@ func (c *Config) updateOnDiskIf(apply func(disk *Config) error) error {
// updatePairingOnDisk is updateOnDisk for a caller that only has the host
// name (the mgmt handlers). Returns the pairing as written, or nil with
// errPairingGone when no such file exists.
//
// The copy is checked as loadPairings checks a file, so a caller that starts
// the pairing from it never serves one whose file this build refuses.
func updatePairingOnDisk(host string, apply func(disk *Config)) (*Config, error) {
pd, err := pairingsDir()
if err != nil {
return nil, err
}
c := &Config{HostName: host}
var out *Config
err := c.updateOnDisk(func(d *Config) {
err = c.updateOnDisk(func(d *Config) {
apply(d)
cp := *d
cp.invalid = invalidReason(&cp, filepath.Join(pd, host+".json"))
out = &cp
})
return out, err
}
// invalidReason is why the pairing c, read from file, is never served, naming
// the file, or "" when it may be served. It is the check the pairing response
// and the rotate directive get, for a file an older build saved from either.
func invalidReason(c *Config, file string) string {
if err := checkEndpoints(append([]string{c.Endpoint}, c.Endpoints...)...); err != nil {
return fmt.Sprintf("pairing %s: %v — it is not served; pair it again (`privatae-bastion pair --replace <token>`) or remove it (`privatae-bastion unpair %s`)",
file, err, c.HostName)
}
return ""
}
// unpairHost tears down and deletes one pairing: its file and its grant file.
// The context store and DCN dir are the USER's data and are left in place —
// deleting a pairing must not delete what the user put on this machine.
@@ -495,31 +526,15 @@ func loadPairings() ([]*Config, error) {
if err := json.Unmarshal(raw, &c); err != nil {
return nil, fmt.Errorf("pairing %s: %w", e.Name(), err)
}
// One such file must not stop every other pairing, status or unpair:
// it loads, never serves.
c.invalid = invalidReason(&c, filepath.Join(pd, e.Name()))
cfgs = append(cfgs, &c)
}
sort.Slice(cfgs, func(i, j int) bool { return cfgs[i].HostName < cfgs[j].HostName })
return cfgs, nil
}
// loadConfig keeps single-pairing call sites working: with exactly one pairing it
// returns it. With several it REFUSES — a straggler caller must name a host and
// fail safe, not silently act on an arbitrary one (that ambiguity is the same
// family of bug as the single-config unpair).
func loadConfig() (*Config, error) {
cfgs, err := loadPairings()
if err != nil {
return nil, err
}
switch len(cfgs) {
case 0:
return nil, fmt.Errorf("no pairings")
case 1:
return cfgs[0], nil
default:
return nil, fmt.Errorf("%d hosts are paired — this operation must name one (see --host)", len(cfgs))
}
}
// hostNames lists the paired host names (for error messages).
func hostNames(cfgs []*Config) []string {
names := make([]string, 0, len(cfgs))