From d880289b0632c842751e01bd7d6428ae435f6483 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Sun, 5 Jul 2026 22:41:14 +0200 Subject: [PATCH] fix(felhomsshd): persist claimed port in agent StateDir (non-root can't write /etc) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6 --- internal/felhomsshd/claim.go | 28 +--------------------------- internal/felhomsshd/manager.go | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/internal/felhomsshd/claim.go b/internal/felhomsshd/claim.go index 844010e..884193c 100644 --- a/internal/felhomsshd/claim.go +++ b/internal/felhomsshd/claim.go @@ -1,11 +1,6 @@ package felhomsshd -import ( - "fmt" - "os" - "strconv" - "strings" -) +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. @@ -42,24 +37,3 @@ func claimPort(candidates []int, isFree portProbe, readPersisted func() (int, bo } return 0, ErrPortsExhausted } - -// readPortFile parses PortFile → (port, ok). A missing/garbage file → (0,false). -func readPortFile() (int, bool) { - raw, err := os.ReadFile(PortFile) - if err != nil { - return 0, false - } - p, err := strconv.Atoi(strings.TrimSpace(string(raw))) - if err != nil || p < 1 || p > 65535 { - return 0, false - } - return p, true -} - -// writePortFile persists the claimed port (0644 — a port is not a secret). -func writePortFile(port int) error { - if err := os.MkdirAll(ConfDir, 0o755); err != nil { - return err - } - return os.WriteFile(PortFile, []byte(strconv.Itoa(port)+"\n"), 0o644) -} diff --git a/internal/felhomsshd/manager.go b/internal/felhomsshd/manager.go index 2f7268a..b132d3f 100644 --- a/internal/felhomsshd/manager.go +++ b/internal/felhomsshd/manager.go @@ -74,6 +74,28 @@ func (m *Manager) Port() int { return m.port } func (m *Manager) sshdDir() string { return filepath.Join(m.stateDir, "felhom-sshd") } func (m *Manager) stagedConfPath() string { return filepath.Join(m.sshdDir(), stagedConfName) } +func (m *Manager) portFilePath() string { return filepath.Join(m.sshdDir(), "port") } + +// readPort/writePort persist the claimed port in the AGENT-OWNED state dir (the agent is non-root and +// cannot write the root-owned /etc/felhom-sshd). A port is not a secret. +func (m *Manager) readPort() (int, bool) { + raw, err := os.ReadFile(m.portFilePath()) + if err != nil { + return 0, false + } + p, err := strconv.Atoi(strings.TrimSpace(string(raw))) + if err != nil || p < 1 || p > 65535 { + return 0, false + } + return p, true +} + +func (m *Manager) writePort(port int) error { + if err := os.MkdirAll(m.sshdDir(), 0o700); err != nil { + return err + } + return os.WriteFile(m.portFilePath(), []byte(strconv.Itoa(port)+"\n"), 0o600) +} // Apply claims the port, renders the config, reconciles the running unit, and (when the desired-state // block carries it) installs the operator's authorized_keys. Idempotent: no change → at most an @@ -81,7 +103,7 @@ func (m *Manager) stagedConfPath() string { return filepath.Join(m.sshdDir(), st // operator login/belt inputs are skipped. Returns the claimed port (0 on a claim/exhaustion error) — // the caller (belt sync) needs it. func (m *Manager) Apply(ctx context.Context, block *hub.WireWireguard) (int, error) { - port, err := claimPort(Candidates, m.isFree, readPortFile, writePortFile) + port, err := claimPort(Candidates, m.isFree, m.readPort, m.writePort) if err != nil { m.logger.Error("felhomsshd: port claim failed", "err", err) return 0, err