소스 검색

fix(tuic): evict the oldest relay flow instead of refusing new clients

udpRelay.flowFor returned "max relay flows reached" once the table held
maxRelayFlows entries, and only the idle sweep (every minute, two-minute
cutoff) freed slots. One host sending a single datagram from each of 4096
source ports therefore locked every new TUIC client out of the inbound for
up to two minutes, repeatably. A full table now evicts the flow last seen
longest ago, which under such a flood is one of the junk flows, and the
newcomer is admitted. TestUDPRelayFullTableAdmitsNewClient fails on the
refusing code with a read timeout for the third client.
Sanaei 17 시간 전
부모
커밋
5815254fc3
2개의 변경된 파일45개의 추가작업 그리고 7개의 파일을 삭제
  1. 22 7
      internal/tuic/relay.go
  2. 23 0
      internal/tuic/relay_test.go

+ 22 - 7
internal/tuic/relay.go

@@ -17,15 +17,13 @@ const (
 	maxRelayFlows     = 4096
 )
 
-// udpRelay owns an inbound's public UDP port and forwards each client's
-// datagrams to the sidecar on loopback, which is the only place the panel can
-// count the inbound's bytes: upstream tuic-server exposes no stats API and
-// its socket syscalls never reach /proc/<pid>/io. Per-client attribution stays
-// impossible because QUIC payloads are opaque.
+// udpRelay owns an inbound's public UDP port and counts the bytes it forwards to
+// the sidecar on loopback: tuic-server has no stats API and /proc/io stays at 0.
 type udpRelay struct {
 	public    *net.UDPConn
 	upstream  *net.UDPAddr
 	idle      time.Duration
+	maxFlows  int
 	up        atomic.Int64
 	down      atomic.Int64
 	mu        sync.Mutex
@@ -56,6 +54,7 @@ func startUDPRelay(bind string, upstream *net.UDPAddr, idle time.Duration) (*udp
 		public:   public,
 		upstream: upstream,
 		idle:     idle,
+		maxFlows: maxRelayFlows,
 		flows:    make(map[string]*relayFlow),
 		done:     make(chan struct{}),
 	}
@@ -136,8 +135,8 @@ func (r *udpRelay) flowFor(client *net.UDPAddr) (*relayFlow, error) {
 		f.lastSeen.Store(now)
 		return f, nil
 	}
-	if len(r.flows) >= maxRelayFlows {
-		return nil, errors.New("tuic: max relay flows reached")
+	if len(r.flows) >= r.maxFlows {
+		r.evictLeastRecentLocked()
 	}
 	conn, err := net.DialUDP("udp", nil, r.upstream)
 	if err != nil {
@@ -153,6 +152,22 @@ func (r *udpRelay) flowFor(client *net.UDPAddr) (*relayFlow, error) {
 	return f, nil
 }
 
+// Refusing a newcomer at the cap let 4096 junk datagrams lock every new client
+// out until the sweep; the flow last seen longest ago is the junk one.
+func (r *udpRelay) evictLeastRecentLocked() {
+	var oldestKey string
+	oldest := int64(-1)
+	for key, f := range r.flows {
+		if seen := f.lastSeen.Load(); oldest < 0 || seen < oldest {
+			oldest, oldestKey = seen, key
+		}
+	}
+	if f, ok := r.flows[oldestKey]; ok {
+		_ = f.conn.Close()
+		delete(r.flows, oldestKey)
+	}
+}
+
 func (r *udpRelay) pump(f *relayFlow) {
 	defer r.wg.Done()
 	buf := make([]byte, 65535)

+ 23 - 0
internal/tuic/relay_test.go

@@ -125,3 +125,26 @@ func TestUDPRelayRefusesFlowsAfterClose(t *testing.T) {
 		t.Fatalf("%d flow(s) registered after Close", n)
 	}
 }
+
+func TestUDPRelayFullTableAdmitsNewClient(t *testing.T) {
+	relay, err := startUDPRelay("127.0.0.1:0", doublingEcho(t), relayFlowIdle)
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(relay.Close)
+	relay.mu.Lock()
+	relay.maxFlows = 2
+	relay.mu.Unlock()
+
+	roundTrip(t, relay, []byte("a"))
+	roundTrip(t, relay, []byte("b"))
+	if got := roundTrip(t, relay, []byte("c")); got != 2 {
+		t.Fatalf("third client reply = %d bytes, want 2: a full table must evict, not refuse", got)
+	}
+	relay.mu.Lock()
+	n := len(relay.flows)
+	relay.mu.Unlock()
+	if n != 2 {
+		t.Fatalf("flow table holds %d flows after admitting a third client, want the cap of 2", n)
+	}
+}