break: replace muxed workers with 1:1 tunnels

Worker frames are now FrameType + payload; there is no stream id.
Each player gets its own worker conn. maxTunnels (default 256)
caps concurrent tunnels. The old maxConn pool size is ignored so
existing configs do not silently admit only a handful of players.

Resume, per-direction windows, the control session, and the
DATA-only shaper stay. A dropped worker still hangs that one
player and reattaches over a fresh conn.

Add a hub-side per-IP limiter for player intents only (default
8/s, burst 16, 64 concurrent). Unmatched hostnames consume a
token; Intent 17 is never counted. 0 disables each knob.
This commit is contained in:
iceBear67
2026-08-15 18:32:51 +08:00
parent da17140583
commit 4df2560331
27 changed files with 1174 additions and 856 deletions
+17 -20
View File
@@ -13,19 +13,19 @@ import (
// startClient builds and starts an in-process client against the hub, with the
// given mappings, returning the running client.
func startClient(t *testing.T, hubAddr, psk string, maxConn int, mappings []client.Mapping) *client.Client {
func startClient(t *testing.T, hubAddr, psk string, maxTunnels int, mappings []client.Mapping) *client.Client {
t.Helper()
return startClientWithPing(t, hubAddr, psk, maxConn, 20000, mappings)
return startClientWithPing(t, hubAddr, psk, maxTunnels, 20000, mappings)
}
// startClientWithPing is startClient with an explicit heartbeat interval, for
// tests that need liveness detection to trigger quickly.
func startClientWithPing(t *testing.T, hubAddr, psk string, maxConn, pingMs int, mappings []client.Mapping) *client.Client {
func startClientWithPing(t *testing.T, hubAddr, psk string, maxTunnels, pingMs int, mappings []client.Mapping) *client.Client {
t.Helper()
return startClientCfg(t, &client.Config{
Server: hubAddr,
PSK: psk,
MaxConn: maxConn,
MaxTunnels: maxTunnels,
PingIntervalMs: pingMs,
Mappings: mappings,
})
@@ -107,7 +107,7 @@ func TestRegexPatternMatch(t *testing.T) {
}
}
// TestLargeTransfer pushes a multi-megabyte payload both ways to exercise mux
// TestLargeTransfer pushes a multi-megabyte payload both ways to exercise
// framing and back-pressure.
func TestLargeTransfer(t *testing.T) {
const psk = "e2e-large"
@@ -147,18 +147,17 @@ func TestLargeTransfer(t *testing.T) {
}
}
// TestConcurrentStreamsUseMultipleConns confirms the least-loaded allocator
// grows the pool breadth-first: concurrent streams spread over several worker
// connections rather than stacking on one, without ever exceeding maxConn.
func TestConcurrentStreamsUseMultipleConns(t *testing.T) {
// TestEachPlayerGetsOwnWorker confirms the 1:1 rule: N concurrent players
// produce N worker connections, and the maxTunnels cap is honoured.
func TestEachPlayerGetsOwnWorker(t *testing.T) {
const psk = "e2e-concurrent"
const n = 20
const maxConn = 4
const n = 8
const maxTunnels = 16
port := freePort(t)
hubAddr := fmt.Sprintf("127.0.0.1:%d", port)
startHub(t, port, psk)
dest := newMockDest(t, modeEcho)
c := startClient(t, hubAddr, psk, maxConn, []client.Mapping{
c := startClient(t, hubAddr, psk, maxTunnels, []client.Mapping{
{Pattern: "mc.local", Destination: dest.addr},
})
@@ -168,8 +167,6 @@ func TestConcurrentStreamsUseMultipleConns(t *testing.T) {
_ = pc.Close()
}
}()
// Establish streams sequentially so allocation is deterministic; keep them
// all open to hold streams active.
for i := 0; i < n; i++ {
pc := dialPlayer(t, hubAddr, "mc.local")
playerEcho(t, pc, []byte(fmt.Sprintf("hello-%d", i)))
@@ -177,13 +174,13 @@ func TestConcurrentStreamsUseMultipleConns(t *testing.T) {
}
got := c.WorkerConnCount()
if got < 2 {
t.Fatalf("expected >=2 worker conns for %d concurrent streams, got %d", n, got)
if got != n {
t.Fatalf("expected %d worker conns for %d players, got %d", n, n, got)
}
if got > maxConn {
t.Fatalf("worker conns %d exceed maxConn %d", got, maxConn)
if got > maxTunnels {
t.Fatalf("worker conns %d exceed maxTunnels %d", got, maxTunnels)
}
t.Logf("%d concurrent streams spread over %d worker conn(s)", n, got)
t.Logf("%d players on %d worker conn(s)", n, got)
}
// TestProxyProtocol checks that the client prepends a correct HAProxy v2 header
@@ -278,7 +275,7 @@ func TestBadPSK(t *testing.T) {
cfg := &client.Config{
Server: hubAddr,
PSK: "totally-wrong",
MaxConn: 2,
MaxTunnels: 2,
PingIntervalMs: 20000,
Mappings: []client.Mapping{{Pattern: "mc.local", Destination: dest.addr}},
}