feat(poller): add port pool for WinBox tunnel allocation
Implements PortPool with mutex-protected allocation, bind verification to skip ports already in use by the OS, and release-for-reuse semantics. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
63
poller/internal/tunnel/portpool.go
Normal file
63
poller/internal/tunnel/portpool.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// PortPool tracks available ports in a fixed range for WinBox tunnel allocation.
|
||||
type PortPool struct {
|
||||
mu sync.Mutex
|
||||
used []bool
|
||||
base int
|
||||
count int
|
||||
}
|
||||
|
||||
func NewPortPool(min, max int) *PortPool {
|
||||
count := max - min + 1
|
||||
return &PortPool{
|
||||
used: make([]bool, count),
|
||||
base: min,
|
||||
count: count,
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate returns the next free port, verifying it can actually be bound.
|
||||
// Returns error if all ports are exhausted.
|
||||
func (pp *PortPool) Allocate() (int, error) {
|
||||
pp.mu.Lock()
|
||||
defer pp.mu.Unlock()
|
||||
|
||||
for i := 0; i < pp.count; i++ {
|
||||
if pp.used[i] {
|
||||
continue
|
||||
}
|
||||
port := pp.base + i
|
||||
if !canBind(port) {
|
||||
continue
|
||||
}
|
||||
pp.used[i] = true
|
||||
return port, nil
|
||||
}
|
||||
return 0, fmt.Errorf("no ports available in range %d-%d", pp.base, pp.base+pp.count-1)
|
||||
}
|
||||
|
||||
// Release returns a port to the pool.
|
||||
func (pp *PortPool) Release(port int) {
|
||||
pp.mu.Lock()
|
||||
defer pp.mu.Unlock()
|
||||
idx := port - pp.base
|
||||
if idx >= 0 && idx < pp.count {
|
||||
pp.used[idx] = false
|
||||
}
|
||||
}
|
||||
|
||||
func canBind(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
|
||||
}
|
||||
82
poller/internal/tunnel/portpool_test.go
Normal file
82
poller/internal/tunnel/portpool_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPortPool_Allocate(t *testing.T) {
|
||||
pp := NewPortPool(49000, 49002) // 3 ports: 49000, 49001, 49002
|
||||
p1, err := pp.Allocate()
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, p1, 49000)
|
||||
assert.LessOrEqual(t, p1, 49002)
|
||||
}
|
||||
|
||||
func TestPortPool_AllocateAll(t *testing.T) {
|
||||
pp := NewPortPool(49000, 49002)
|
||||
ports := make(map[int]bool)
|
||||
for i := 0; i < 3; i++ {
|
||||
p, err := pp.Allocate()
|
||||
require.NoError(t, err)
|
||||
ports[p] = true
|
||||
}
|
||||
assert.Len(t, ports, 3)
|
||||
}
|
||||
|
||||
func TestPortPool_Exhausted(t *testing.T) {
|
||||
pp := NewPortPool(49000, 49001)
|
||||
_, _ = pp.Allocate()
|
||||
_, _ = pp.Allocate()
|
||||
_, err := pp.Allocate()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no ports available")
|
||||
}
|
||||
|
||||
func TestPortPool_Release(t *testing.T) {
|
||||
pp := NewPortPool(49000, 49000) // single port
|
||||
p, _ := pp.Allocate()
|
||||
pp.Release(p)
|
||||
p2, err := pp.Allocate()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, p, p2)
|
||||
}
|
||||
|
||||
func TestPortPool_ConcurrentAccess(t *testing.T) {
|
||||
pp := NewPortPool(49000, 49099) // 100 ports
|
||||
var wg sync.WaitGroup
|
||||
allocated := make(chan int, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
p, err := pp.Allocate()
|
||||
if err == nil {
|
||||
allocated <- p
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
close(allocated)
|
||||
ports := make(map[int]bool)
|
||||
for p := range allocated {
|
||||
assert.False(t, ports[p], "duplicate port allocated: %d", p)
|
||||
ports[p] = true
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortPool_BindVerification(t *testing.T) {
|
||||
// Occupy a port, then verify Allocate skips it
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:49050")
|
||||
require.NoError(t, err)
|
||||
defer ln.Close()
|
||||
|
||||
pp := NewPortPool(49050, 49051)
|
||||
p, err := pp.Allocate()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 49051, p) // should skip 49050 since it's occupied
|
||||
}
|
||||
Reference in New Issue
Block a user