Files
redapricot/e2e/slowstream_test.go
T
iceBear67 4df2560331 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.
2026-08-15 18:32:51 +08:00

130 lines
4.0 KiB
Go

package e2e
import (
crand "crypto/rand"
"fmt"
"net"
"testing"
"time"
"github.com/iceBear67/redapricot/client"
"github.com/iceBear67/redapricot/client/wire"
)
// echoRounds pushes payload through the tunnel `rounds` times on one player
// connection, failing the test if any round stalls.
func echoRounds(t *testing.T, hubAddr string, rounds, size int) {
t.Helper()
fast := dialPlayer(t, hubAddr, "mc.local")
defer fast.Close()
payload := make([]byte, size)
for i := range payload {
payload[i] = byte(i*13 + 5)
}
for i := 0; i < rounds; i++ {
playerEcho(t, fast, payload)
}
}
// TestSlowPlayerDoesNotStallOthers: a player that stops reading while megabytes
// are echoed back to it must not stall another player. Flow control and the
// 1:1 worker model keep a jammed tunnel from taking anyone else with it.
func TestSlowPlayerDoesNotStallOthers(t *testing.T) {
const psk = "e2e-slowplayer"
port := freePort(t)
hubAddr := fmt.Sprintf("127.0.0.1:%d", port)
startHub(t, port, psk)
dest := newMockDest(t, modeEcho)
startClient(t, hubAddr, psk, 2, []client.Mapping{
{Pattern: "mc.local", Destination: dest.addr},
})
// Slow player: 4 MiB goes out, gets echoed back, and is never read. The
// write blocks once buffers fill; the error on test-end close is expected.
slow := dialPlayer(t, hubAddr, "mc.local")
defer slow.Close()
go func() {
_, _ = slow.Write(make([]byte, 4*1024*1024))
}()
// Let the slow stream jam: its flow-control window fills and stays full.
time.Sleep(1 * time.Second)
// The fast player has its own worker conn and must still round-trip.
echoRounds(t, hubAddr, 10, 8*1024)
}
// TestSlowDestinationDoesNotStallOthers: a destination that never reads must
// only stall its own stream. Before the async delivery queue, the client wrote
// to destinations inline in the worker readLoop, so one blocked destination
// froze every stream on the connection.
func TestSlowDestinationDoesNotStallOthers(t *testing.T) {
const psk = "e2e-slowdest"
port := freePort(t)
hubAddr := fmt.Sprintf("127.0.0.1:%d", port)
startHub(t, port, psk)
dest := newMockDest(t, modeEcho)
hole := newMockDest(t, modeBlackhole)
startClient(t, hubAddr, psk, 2, []client.Mapping{
{Pattern: "mc.local", Destination: dest.addr},
{Pattern: "hole.local", Destination: hole.addr},
})
// This player's destination never reads: the client-side write jams after
// kernel buffers fill, with the stream's queue bounded by its window.
stuck := dialPlayer(t, hubAddr, "hole.local")
defer stuck.Close()
go func() {
_, _ = stuck.Write(make([]byte, 2*1024*1024))
}()
time.Sleep(1 * time.Second)
echoRounds(t, hubAddr, 10, 8*1024)
}
// TestLegacyClientRejected: per-stream flow control is mandatory. A client that
// performs the old session establishment — a Rekey message without the trailing
// feature flags — must be closed by the hub before SessionReady.
func TestLegacyClientRejected(t *testing.T) {
const psk = "e2e-legacyreject"
port := freePort(t)
hubAddr := fmt.Sprintf("127.0.0.1:%d", port)
startHub(t, port, psk)
conn, err := net.DialTimeout("tcp", hubAddr, 5*time.Second)
if err != nil {
t.Fatalf("dial hub: %v", err)
}
defer conn.Close()
pskBytes := []byte(psk)
hs := wire.BuildHandshake(767, wire.PSKAddress(pskBytes), 25565, 17)
if _, err := conn.Write(hs); err != nil {
t.Fatalf("handshake: %v", err)
}
fc := wire.NewFramedConn(conn,
wire.CipherFor(pskBytes, wire.DirS2C),
wire.CipherFor(pskBytes, wire.DirC2S),
)
rnd := make([]byte, 16)
if _, err := crand.Read(rnd); err != nil {
t.Fatal(err)
}
// Pre-flow-control Rekey: Magic|RandLen|Rand|Timestamp with no flags.
legacyRekey := wire.NewWriter().
U8(0x01). // control-session magic
VarInt(len(rnd)).
Bytes(rnd).
I64(time.Now().UnixMilli()).
Out()
if err := fc.WriteFrame(legacyRekey); err != nil {
t.Fatalf("rekey: %v", err)
}
_ = conn.SetReadDeadline(time.Now().Add(10 * time.Second))
if payload, err := fc.ReadFrame(); err == nil {
t.Fatalf("expected the hub to close a legacy session, got frame %v", payload)
}
}