Files
admin b99d02b7a9 fix(felhomsshd): keep the persisted port unconditionally (no self-listen flip-flop)
On a continuous reconcile felhom-sshd itself listens on the claimed port, so
re-probing isFree(persisted) found it 'busy' by our own daemon and thrashed to
another candidate every tick. A persisted port is ours — keep it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-05 22:43:22 +02:00

43 lines
1.9 KiB
Go

package felhomsshd
import "fmt"
// Candidates is the ordered OOB-port candidate list (spike §2). First free wins; NEVER :22 or a
// random port. Package-var (not const) so tests can shrink it.
var Candidates = []int{8822, 2222, 8022, 62222}
// ErrPortsExhausted is returned when every candidate is busy — a LOUD failure [SF-4/trap 6], never a
// silent fallback to :22 or a random high port.
var ErrPortsExhausted = fmt.Errorf("felhomsshd: all candidate OOB ports are busy — refusing to fall back to :22 or a random port")
// portProbe reports whether a TCP port is free (nothing listening AND a real bind succeeds). Injected
// for tests; production impl = probeFree (ss + net.Listen).
type portProbe func(port int) bool
// claimPort returns the OOB port:
// - if a port is already PERSISTED (and != 22) → keep it unconditionally. It is OUR port; on a
// continuous reconcile felhom-sshd is itself LISTENING on it, so re-probing with isFree would
// (wrongly) find it "busy" by our own daemon and thrash to another candidate every tick. Once
// claimed, the port is stable (the belt @ssh_port and the operator's known port depend on it).
// - else the FIRST free candidate → persist + return (isFree = ss-empty AND a real bind succeeds).
// - else ErrPortsExhausted (LOUD — never :22 or a random port).
//
// persist writes the port file; readPersisted reads it. isFree is the probe. All injected for tests.
func claimPort(candidates []int, isFree portProbe, readPersisted func() (int, bool), persist func(int) error) (int, error) {
if cur, ok := readPersisted(); ok && cur != 22 {
return cur, nil // persisted = ours; keep it (no thrash — felhom-sshd holds it)
}
for _, p := range candidates {
if p == 22 {
continue // defensive: never :22
}
if isFree(p) {
if err := persist(p); err != nil {
return 0, fmt.Errorf("felhomsshd: persist claimed port %d: %w", p, err)
}
return p, nil
}
}
return 0, ErrPortsExhausted
}