package agent import ( "fmt" "math/rand/v2" "net" "sync" ) const ( portRangeMin = 10000 portRangeMax = 60000 maxRetries = 10 ) // PortAllocator manages host port allocation for route-based deployments. // It tracks allocated ports within the agent session to avoid double-allocation. type PortAllocator struct { mu sync.Mutex allocated map[int]bool } // NewPortAllocator creates a new PortAllocator. func NewPortAllocator() *PortAllocator { return &PortAllocator{ allocated: make(map[int]bool), } } // Allocate picks a free port in range [10000, 60000). // It tries random ports, checks availability with net.Listen, and retries up to 10 times. func (pa *PortAllocator) Allocate() (int, error) { pa.mu.Lock() defer pa.mu.Unlock() for range maxRetries { port := portRangeMin + rand.IntN(portRangeMax-portRangeMin) //nolint:gosec // port selection, not security if pa.allocated[port] { continue } if !isPortFree(port) { continue } pa.allocated[port] = true return port, nil } return 0, fmt.Errorf("failed to allocate port after %d attempts", maxRetries) } // Release marks a port as available again. func (pa *PortAllocator) Release(port int) { pa.mu.Lock() defer pa.mu.Unlock() delete(pa.allocated, port) } // isPortFree checks if a TCP port is available by attempting to listen on it. func isPortFree(port int) bool { ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port)) if err != nil { return false } _ = ln.Close() return true }