d880289b06
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
40 lines
1.5 KiB
Go
40 lines
1.5 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, mirroring the spike's shell algorithm:
|
|
// - if a persisted port exists AND is still free → keep it (idempotent, no thrash),
|
|
// - else the FIRST free candidate → persist + return,
|
|
// - else ErrPortsExhausted (LOUD).
|
|
//
|
|
// persist writes PortFile; 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 && isFree(cur) {
|
|
return cur, nil
|
|
}
|
|
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
|
|
}
|