mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 15:47:14 +00:00
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.
This commit is contained in:
+22
-7
@@ -17,15 +17,13 @@ const (
|
|||||||
maxRelayFlows = 4096
|
maxRelayFlows = 4096
|
||||||
)
|
)
|
||||||
|
|
||||||
// udpRelay owns an inbound's public UDP port and forwards each client's
|
// udpRelay owns an inbound's public UDP port and counts the bytes it forwards to
|
||||||
// datagrams to the sidecar on loopback, which is the only place the panel can
|
// the sidecar on loopback: tuic-server has no stats API and /proc/io stays at 0.
|
||||||
// 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.
|
|
||||||
type udpRelay struct {
|
type udpRelay struct {
|
||||||
public *net.UDPConn
|
public *net.UDPConn
|
||||||
upstream *net.UDPAddr
|
upstream *net.UDPAddr
|
||||||
idle time.Duration
|
idle time.Duration
|
||||||
|
maxFlows int
|
||||||
up atomic.Int64
|
up atomic.Int64
|
||||||
down atomic.Int64
|
down atomic.Int64
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@@ -56,6 +54,7 @@ func startUDPRelay(bind string, upstream *net.UDPAddr, idle time.Duration) (*udp
|
|||||||
public: public,
|
public: public,
|
||||||
upstream: upstream,
|
upstream: upstream,
|
||||||
idle: idle,
|
idle: idle,
|
||||||
|
maxFlows: maxRelayFlows,
|
||||||
flows: make(map[string]*relayFlow),
|
flows: make(map[string]*relayFlow),
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
@@ -136,8 +135,8 @@ func (r *udpRelay) flowFor(client *net.UDPAddr) (*relayFlow, error) {
|
|||||||
f.lastSeen.Store(now)
|
f.lastSeen.Store(now)
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
if len(r.flows) >= maxRelayFlows {
|
if len(r.flows) >= r.maxFlows {
|
||||||
return nil, errors.New("tuic: max relay flows reached")
|
r.evictLeastRecentLocked()
|
||||||
}
|
}
|
||||||
conn, err := net.DialUDP("udp", nil, r.upstream)
|
conn, err := net.DialUDP("udp", nil, r.upstream)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -153,6 +152,22 @@ func (r *udpRelay) flowFor(client *net.UDPAddr) (*relayFlow, error) {
|
|||||||
return f, nil
|
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) {
|
func (r *udpRelay) pump(f *relayFlow) {
|
||||||
defer r.wg.Done()
|
defer r.wg.Done()
|
||||||
buf := make([]byte, 65535)
|
buf := make([]byte, 65535)
|
||||||
|
|||||||
@@ -125,3 +125,26 @@ func TestUDPRelayRefusesFlowsAfterClose(t *testing.T) {
|
|||||||
t.Fatalf("%d flow(s) registered after Close", n)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user