mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 23:35:00 +03:00
207c705300
TUIC traffic reached Xray through a loopback SOCKS relay that took no login, so Xray never learned which client a connection belonged to and user routing rules never matched TUIC (#6874). The relay now carries one account per client email, so per-user rules match on TCP and both UDP modes; admission stays with the native TUIC server. Adding, removing or renaming a TUIC client swaps only that inbound's relay instead of restarting Xray: new sessions wait behind a bounded gate while live streams and UDP associations continue, accounts are verified after the swap and rolled back on failure, one failing relay no longer strands the others or blocks unrelated live changes, and emails over SOCKS5's 255-byte limit are excluded with a warning.
213 lines
7.6 KiB
Go
213 lines
7.6 KiB
Go
package tuic
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"io"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
serverquic "github.com/apernet/quic-go"
|
|
"github.com/google/uuid"
|
|
clientquic "github.com/quic-go/quic-go"
|
|
)
|
|
|
|
func startLifecycleTestServer(t *testing.T, relayAddr, email string) (*Server, *clientquic.Conn, uuid.UUID, string) {
|
|
t.Helper()
|
|
return startLifecycleTestServerWithRelay(t, &SocksRelay{Addr: relayAddr, Password: "lifecycle-socks-password"}, email)
|
|
}
|
|
|
|
func startLifecycleTestServerWithRelay(t *testing.T, relay *SocksRelay, email string) (*Server, *clientquic.Conn, uuid.UUID, string) {
|
|
t.Helper()
|
|
cert, key := generateTestCert(t)
|
|
clientID := uuid.New()
|
|
password := "lifecycle-test-password"
|
|
server, err := NewServer(Instance{
|
|
Id: 99101, Tag: "lifecycle-test", Listen: "127.0.0.1", Port: 0,
|
|
Certificate: string(cert), PrivateKey: string(key), ALPN: []string{"h3"},
|
|
AuthenticationTimeout: 2, MaxIdleTime: 30,
|
|
Clients: []TuicClientSettings{{UUID: clientID.String(), Password: password, Email: email}},
|
|
}, relay)
|
|
if err != nil {
|
|
t.Fatalf("create TUIC server: %v", err)
|
|
}
|
|
if err := server.Start(); err != nil {
|
|
t.Fatalf("start TUIC server: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = server.Close() })
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
client, err := clientquic.DialAddr(ctx, server.packetConn.LocalAddr().String(),
|
|
&tls.Config{InsecureSkipVerify: true, NextProtos: []string{"h3"}},
|
|
&clientquic.Config{EnableDatagrams: true})
|
|
if err != nil {
|
|
t.Fatalf("dial TUIC server: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = client.CloseWithError(0, "test complete") })
|
|
|
|
tlsState := client.ConnectionState().TLS
|
|
token, err := tlsState.ExportKeyingMaterial(string(clientID[:]), []byte(password), 32)
|
|
if err != nil {
|
|
t.Fatalf("derive authentication token: %v", err)
|
|
}
|
|
stream, err := client.OpenUniStreamSync(ctx)
|
|
if err != nil {
|
|
t.Fatalf("open authentication stream: %v", err)
|
|
}
|
|
auth := append([]byte{ProtocolVersion, CmdAuthenticate}, clientID[:]...)
|
|
auth = append(auth, token...)
|
|
if _, err := stream.Write(auth); err != nil {
|
|
t.Fatalf("write authentication: %v", err)
|
|
}
|
|
closeUniStream(t, stream)
|
|
return server, client, clientID, password
|
|
}
|
|
|
|
func authenticatedServerConnection(t *testing.T, server *Server, clientID uuid.UUID) (*serverquic.Conn, *User) {
|
|
t.Helper()
|
|
id := [16]byte(clientID)
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
server.activeConnsMu.Lock()
|
|
for conn, user := range server.activeConns[id] {
|
|
server.activeConnsMu.Unlock()
|
|
return conn, user
|
|
}
|
|
server.activeConnsMu.Unlock()
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
t.Fatal("server did not register authenticated QUIC connection")
|
|
return nil, nil
|
|
}
|
|
|
|
func TestMalformedBiStreamDelayedFINReleasesReceiveCredit(t *testing.T) {
|
|
_, client, _, _ := startLifecycleTestServer(t, "127.0.0.1:1", "bidi-lifecycle@x")
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
for i := 0; i < 110; i++ {
|
|
openCtx, openCancel := context.WithTimeout(ctx, 700*time.Millisecond)
|
|
stream, err := client.OpenStreamSync(openCtx)
|
|
openCancel()
|
|
if err != nil {
|
|
t.Fatalf("bidirectional stream %d blocked after unsupported commands: %v", i+1, err)
|
|
}
|
|
if _, err := stream.Write([]byte{ProtocolVersion, 0xff}); err != nil {
|
|
t.Fatalf("write unsupported command %d: %v", i+1, err)
|
|
}
|
|
stream.SetReadDeadline(time.Now().Add(time.Second))
|
|
if _, err := io.Copy(io.Discard, stream); err != nil {
|
|
t.Fatalf("wait for unsupported stream %d to close: %v", i+1, err)
|
|
}
|
|
_ = stream.Close()
|
|
}
|
|
}
|
|
|
|
func TestDownstreamUDPResponseRefreshesAssociationIdleTime(t *testing.T) {
|
|
socksAddr, cleanup := startMockSocks5Server(t, "udp-lifecycle@x", "lifecycle-socks-password")
|
|
defer cleanup()
|
|
server, client, clientID, _ := startLifecycleTestServer(t, socksAddr, "udp-lifecycle@x")
|
|
serverConn, user := authenticatedServerConnection(t, server, clientID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
session, err := server.relay.DialUDP(ctx, user.Email)
|
|
if err != nil {
|
|
t.Fatalf("open SOCKS UDP session: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = session.Close() })
|
|
const associationID uint16 = 61244
|
|
oldActive := time.Now().Add(-udpAssociationIdleTimeout - time.Second)
|
|
association := &udpAssociation{
|
|
responseTransport: packetTransportDatagram,
|
|
relay: &udpRelaySession{relay: session, responseTransport: packetTransportDatagram},
|
|
lastActive: oldActive,
|
|
}
|
|
registry := newUdpAssociationRegistry(maxUdpRelayPacketSize)
|
|
registry.associations[associationID] = association
|
|
responseDone := make(chan struct{})
|
|
go func() {
|
|
defer close(responseDone)
|
|
server.relayUDPResponses(ctx, serverConn, user, associationID, association, registry, association.relay)
|
|
}()
|
|
|
|
target := &Address{Type: AddrTypeIPv4, IP: net.IPv4(8, 8, 8, 8), Port: 53}
|
|
if _, err := session.Send(target, []byte("seed")); err != nil {
|
|
t.Fatalf("send SOCKS seed datagram: %v", err)
|
|
}
|
|
response, err := client.ReceiveDatagram(ctx)
|
|
if err != nil {
|
|
t.Fatalf("receive echoed UDP response: %v", err)
|
|
}
|
|
if len(response) < 2 || response[0] != ProtocolVersion || response[1] != CmdPacket {
|
|
t.Fatalf("unexpected TUIC UDP response: %x", response)
|
|
}
|
|
reader := bytes.NewReader(response[2:])
|
|
header, err := ReadPacketHeader(reader)
|
|
if err != nil {
|
|
t.Fatalf("read response header: %v", err)
|
|
}
|
|
got, err := readPacketPayload(reader, header)
|
|
if err != nil || !bytes.Equal(got, []byte("seed")) {
|
|
t.Fatalf("echo response payload = %q, error=%v", got, err)
|
|
}
|
|
if header.AssocID != associationID {
|
|
t.Fatalf("response association id = %d, want %d", header.AssocID, associationID)
|
|
}
|
|
|
|
// Receiving the datagram can precede the sender's post-send activity update.
|
|
// Wait for that protected state before exercising idle reaping.
|
|
deadline := time.Now().Add(time.Second)
|
|
for {
|
|
registry.mu.Lock()
|
|
refreshedAt := association.lastActive
|
|
registry.mu.Unlock()
|
|
if refreshedAt.After(oldActive) {
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("successful downstream response did not refresh association activity")
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
registry.reapIdle(oldActive.Add(udpAssociationIdleTimeout + time.Second))
|
|
registry.mu.Lock()
|
|
remaining := registry.associations[associationID]
|
|
registry.mu.Unlock()
|
|
if remaining != association {
|
|
t.Fatal("association was reaped despite a recently delivered downstream response")
|
|
}
|
|
_ = session.Close()
|
|
select {
|
|
case <-responseDone:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("UDP response relay did not stop after SOCKS session closed")
|
|
}
|
|
}
|
|
|
|
func TestUdpAssociationTouchDoesNotRefreshReusedID(t *testing.T) {
|
|
registry := newUdpAssociationRegistry(maxUdpRelayPacketSize)
|
|
addr := &Address{Type: AddrTypeIPv4, IP: net.IPv4(8, 8, 8, 8), Port: 53}
|
|
header := &PacketHeader{AssocID: 17, PktID: 1, FragTotal: 1, FragID: 0, Size: 1, Addr: addr}
|
|
old, _, _, complete := registry.feed(packetTransportDatagram, header, []byte("x"))
|
|
if !complete {
|
|
t.Fatal("failed to create first association generation")
|
|
}
|
|
oldTime := old.lastActive
|
|
if !registry.dissociate(header.AssocID) {
|
|
t.Fatal("failed to dissociate first association generation")
|
|
}
|
|
newGeneration, _, _, complete := registry.feed(packetTransportDatagram, header, []byte("x"))
|
|
if !complete || newGeneration == old {
|
|
t.Fatal("failed to create replacement association generation")
|
|
}
|
|
if registry.touch(header.AssocID, old, oldTime.Add(time.Hour)) {
|
|
t.Fatal("late response refreshed a replacement association generation")
|
|
}
|
|
if !newGeneration.lastActive.Before(oldTime.Add(time.Hour)) {
|
|
t.Fatal("replacement association timestamp changed after stale touch")
|
|
}
|
|
}
|