mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 03:05:02 +03:00
fix(tuic): bound one client's connections and UDP relays
The UDP association cap applied per QUIC connection, and nothing limited how many connections one credential could authenticate, so a single client could hold enough UDP relays (each a host port) to exhaust the host's ephemeral range. A client now gets at most 32 connections and 512 open UDP relays across all of them; past that a connection is refused at authentication and a relay is not opened.
This commit is contained in:
@@ -17,6 +17,13 @@ var (
|
||||
ErrInvalidTLSState = errors.New("tuic: TLS connection state not available")
|
||||
)
|
||||
|
||||
// One credential's limits across all its connections: each UDP relay holds a host port, so
|
||||
// a per-connection cap alone lets one client take the host's whole ephemeral range.
|
||||
const (
|
||||
maxUserSessions = 32
|
||||
maxUserUdpRelays = 512
|
||||
)
|
||||
|
||||
// User represents a configured TUIC client for authentication and billing.
|
||||
type User struct {
|
||||
TrafficID int
|
||||
@@ -26,6 +33,8 @@ type User struct {
|
||||
Email string
|
||||
Traffic *UserTraffic
|
||||
sessions atomic.Int64
|
||||
// udpRelays counts the open UDP relays over all the client's connections.
|
||||
udpRelays atomic.Int64
|
||||
}
|
||||
|
||||
type UserTraffic struct {
|
||||
|
||||
+10
-2
@@ -196,14 +196,18 @@ func (s *Server) applyCongestionControl(conn *quic.Conn) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) registerConn(user *User, conn *quic.Conn) {
|
||||
func (s *Server) registerConn(user *User, conn *quic.Conn) bool {
|
||||
s.activeConnsMu.Lock()
|
||||
defer s.activeConnsMu.Unlock()
|
||||
if user.sessions.Load() >= maxUserSessions {
|
||||
return false
|
||||
}
|
||||
if s.activeConns[user.UUID] == nil {
|
||||
s.activeConns[user.UUID] = make(map[*quic.Conn]*User)
|
||||
}
|
||||
s.activeConns[user.UUID][conn] = user
|
||||
user.sessions.Add(1)
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Server) unregisterConn(user *User, conn *quic.Conn) {
|
||||
@@ -379,8 +383,12 @@ func (s *Server) handleConn(conn *quic.Conn) {
|
||||
if !authState.CompareAndSwap(0, 1) {
|
||||
return authState.Load() == 1 && authUser.Load() == user
|
||||
}
|
||||
if !s.registerConn(user, conn) {
|
||||
authState.Store(2)
|
||||
s.logLimited(tuicLogWarn, "tuic-sessions", 30*time.Second, "client refused: more than %d connections", maxUserSessions)
|
||||
return false
|
||||
}
|
||||
authUser.Store(user)
|
||||
s.registerConn(user, conn)
|
||||
s.markActive(user.Email)
|
||||
authTimer.Stop()
|
||||
s.logf(tuicLogInfo, "client authenticated")
|
||||
|
||||
@@ -180,6 +180,8 @@ type SocksUDPSession struct {
|
||||
targetEP net.Addr
|
||||
user string
|
||||
closed atomic.Bool
|
||||
// onClose runs once when the session closes, releasing its place in a budget.
|
||||
onClose func()
|
||||
}
|
||||
|
||||
// DialUDP establishes a SOCKS5 UDP ASSOCIATE tunnel to Xray.
|
||||
@@ -312,6 +314,9 @@ func (s *SocksUDPSession) Close() error {
|
||||
if s.closed.Swap(true) {
|
||||
return nil
|
||||
}
|
||||
if s.onClose != nil {
|
||||
s.onClose()
|
||||
}
|
||||
_ = s.udpConn.Close()
|
||||
return s.ctrl.Close()
|
||||
}
|
||||
|
||||
@@ -252,3 +252,37 @@ func TestDissociateStopsAResponseRelayBlockedOnTheClient(t *testing.T) {
|
||||
t.Fatal("the response relay is still running after its association was dissociated")
|
||||
}
|
||||
}
|
||||
|
||||
// The per-connection association cap means nothing if one credential may open any number
|
||||
// of connections: each UDP relay holds a host port, so the host's whole range ran out.
|
||||
func TestOneClientsSessionsAndUDPRelaysAreBounded(t *testing.T) {
|
||||
socksAddr, cleanup := startMockSocks5Server(t, "udp-bound@x", "lifecycle-socks-password")
|
||||
defer cleanup()
|
||||
server, _, clientID, _ := startLifecycleTestServer(t, socksAddr, "udp-bound@x")
|
||||
_, user := authenticatedServerConnection(t, server, clientID)
|
||||
|
||||
for range maxUserSessions + 5 {
|
||||
server.registerConn(user, nil)
|
||||
}
|
||||
if got := user.sessions.Load(); got > maxUserSessions {
|
||||
t.Fatalf("one client holds %d sessions, want at most %d", got, maxUserSessions)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
registry := newUdpAssociationRegistry(maxUdpRelayPacketSize)
|
||||
user.udpRelays.Store(maxUserUdpRelays)
|
||||
association := &udpAssociation{responseTransport: packetTransportDatagram, lastActive: time.Now()}
|
||||
registry.associations[1] = association
|
||||
if _, _, err := registry.ensureRelay(ctx, 1, association, user, server.relay); err == nil {
|
||||
t.Fatal("a client past its UDP relay budget opened another relay")
|
||||
}
|
||||
user.udpRelays.Store(0)
|
||||
if _, created, err := registry.ensureRelay(ctx, 1, association, user, server.relay); err != nil || !created {
|
||||
t.Fatalf("ensureRelay under budget = %v, %v", created, err)
|
||||
}
|
||||
registry.dissociate(1)
|
||||
if got := user.udpRelays.Load(); got != 0 {
|
||||
t.Fatalf("closed relay still counted: %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ const (
|
||||
|
||||
var errUdpAssociationClosed = errors.New("tuic: UDP association is closed")
|
||||
|
||||
var errUserUdpRelayBudget = errors.New("tuic: the client has too many open UDP relays")
|
||||
|
||||
type udpAssociation struct {
|
||||
responseTransport uint8
|
||||
relay *udpRelaySession
|
||||
@@ -98,10 +100,16 @@ func (r *udpAssociationRegistry) ensureRelay(ctx context.Context, assocID uint16
|
||||
}
|
||||
r.mu.Unlock()
|
||||
|
||||
if user.udpRelays.Add(1) > maxUserUdpRelays {
|
||||
user.udpRelays.Add(-1)
|
||||
return nil, false, errUserUdpRelayBudget
|
||||
}
|
||||
socksSession, err := relay.DialUDP(ctx, user.Email)
|
||||
if err != nil {
|
||||
user.udpRelays.Add(-1)
|
||||
return nil, false, err
|
||||
}
|
||||
socksSession.onClose = func() { user.udpRelays.Add(-1) }
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
Reference in New Issue
Block a user