mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-06 22:22:08 +03:00
815c9c5772
The race job failed in TestAudit3ManagerEnsureActualSendersWithPersistentTraffic with "close called for canceled stream 14". The server parses one command per uni stream and then calls CancelRead, as the quinn reference server does on drop, so its STOP_SENDING can reach the client before the client's own Close and quic-go reports that Close as an error. The data was already read. Every test that wrote a command on a uni stream and required Close to succeed shared this race. closeUniStream accepts only a remote StreamError on the stream's context, so any other Close failure still fails the test.
199 lines
7.1 KiB
Go
199 lines
7.1 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()
|
|
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}},
|
|
}, &SocksRelay{Addr: relayAddr, Password: "lifecycle-socks-password"})
|
|
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)
|
|
}
|
|
|
|
registry.mu.Lock()
|
|
refreshedAt := association.lastActive
|
|
registry.mu.Unlock()
|
|
if !refreshedAt.After(oldActive) {
|
|
t.Fatal("successful downstream response did not refresh association activity")
|
|
}
|
|
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")
|
|
}
|
|
}
|