| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069 |
- package tuic
- import (
- "bytes"
- "context"
- "crypto/tls"
- "encoding/binary"
- "errors"
- "fmt"
- "io"
- "net"
- "strings"
- "sync"
- "sync/atomic"
- "time"
- "github.com/apernet/quic-go"
- xraycongestion "github.com/xtls/xray-core/transport/internet/hysteria/congestion"
- "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr"
- "github.com/mhsanaei/3x-ui/v3/internal/logger"
- )
- // Server is an in-process native Go TUIC v5 server terminating QUIC
- // and bridging decrypted TCP/UDP into a local SOCKS5 inbound.
- type Server struct {
- id int
- tag atomic.Pointer[string]
- listenAddr string
- authTimeout time.Duration
- congestionControl atomic.Value
- logLevel atomic.Uint32
- maxUdpRelayPacketSize int
- users *UserRegistry
- relay *SocksRelay
- tlsConfig *tls.Config
- quicConfig *quic.Config
- quicListener *quic.Listener
- packetConn net.PacketConn
- lastOnline sync.Map // email string -> time.Time
- logThrottle sync.Map // event name -> *atomic.Int64 timestamp
- activeConnsMu sync.Mutex
- activeConns map[[16]byte]map[*quic.Conn]*User
- connectionsMu sync.Mutex
- connections map[*quic.Conn]struct{}
- ctx context.Context
- cancel context.CancelFunc
- wg sync.WaitGroup
- closed atomic.Bool
- running atomic.Bool
- }
- // NewServer creates a new TUIC v5 Server instance.
- func NewServer(inst Instance, relay *SocksRelay) (*Server, error) {
- if err := ValidateClients(inst.Clients); err != nil {
- return nil, err
- }
- if inst.Certificate == "" || inst.PrivateKey == "" {
- return nil, errors.New("tuic: certificate or private key missing")
- }
- tlsCert, err := loadCertificate(inst.Certificate, inst.PrivateKey)
- if err != nil {
- return nil, fmt.Errorf("tuic: load tls certificate: %w", err)
- }
- alpn := inst.ALPN
- if len(alpn) == 0 {
- alpn = []string{"h3", "spdy/3.1"}
- }
- tlsConfig := &tls.Config{
- Certificates: []tls.Certificate{tlsCert},
- NextProtos: alpn,
- }
- maxIdle := inst.MaxIdleTime
- if maxIdle <= 0 {
- maxIdle = 15
- }
- authTimeout := inst.AuthenticationTimeout
- if authTimeout <= 0 {
- authTimeout = 3
- }
- maxUdpSize := inst.MaxUdpRelayPacketSize
- if maxUdpSize <= 0 {
- maxUdpSize = 1500
- }
- if maxUdpSize > maxSafeUdpRelayPacketSize && maxUdpSize <= maxLegacyUdpRelayPacketSize {
- maxUdpSize = maxSafeUdpRelayPacketSize
- }
- if maxUdpSize > maxLegacyUdpRelayPacketSize {
- return nil, fmt.Errorf("tuic: max UDP relay packet size %d exceeds %d", maxUdpSize, maxSafeUdpRelayPacketSize)
- }
- quicConfig := &quic.Config{
- EnableDatagrams: true,
- MaxIdleTimeout: time.Duration(maxIdle) * time.Second,
- KeepAlivePeriod: time.Duration(maxIdle/2) * time.Second,
- Allow0RTT: inst.ZeroRTTHandshake,
- }
- registry := NewUserRegistry()
- registry.SetUsers(inst.Clients)
- ctx, cancel := context.WithCancel(context.Background())
- s := &Server{
- id: inst.Id,
- listenAddr: inst.BindTo(),
- authTimeout: time.Duration(authTimeout) * time.Second,
- maxUdpRelayPacketSize: maxUdpSize,
- users: registry,
- activeConns: make(map[[16]byte]map[*quic.Conn]*User),
- connections: make(map[*quic.Conn]struct{}),
- relay: relay,
- tlsConfig: tlsConfig,
- quicConfig: quicConfig,
- ctx: ctx,
- cancel: cancel,
- }
- s.updateRuntimeSettings(inst.Tag, inst.CongestionControl, inst.LogLevel)
- return s, nil
- }
- // Start opens the UDP socket and starts the QUIC listener.
- func (s *Server) Start() error {
- var lc net.ListenConfig
- pConn, err := lc.ListenPacket(s.ctx, "udp", s.listenAddr)
- if err != nil {
- return fmt.Errorf("tuic: listen packet on %s: %w", s.listenAddr, err)
- }
- s.packetConn = pConn
- ln, err := quic.Listen(pConn, s.tlsConfig, s.quicConfig)
- if err != nil {
- _ = pConn.Close()
- return fmt.Errorf("tuic: quic listen on %s: %w", s.listenAddr, err)
- }
- s.quicListener = ln
- s.running.Store(true)
- s.logf(tuicLogInfo, "listener started on %s", s.listenAddr)
- s.wg.Add(1)
- go s.acceptLoop()
- return nil
- }
- // IsRunning returns whether the server is currently accepting connections.
- func (s *Server) IsRunning() bool {
- return s.running.Load() && !s.closed.Load()
- }
- func (s *Server) updateRuntimeSettings(tag, controller, logLevel string) {
- tagCopy := tag
- s.tag.Store(&tagCopy)
- s.logLevel.Store(parseLogLevel(logLevel))
- normalized, valid := normalizeCongestionControl(controller)
- s.congestionControl.Store(normalized)
- if !valid {
- s.logf(tuicLogWarn, "unsupported congestion controller %q; using %s", controller, normalized)
- }
- }
- func (s *Server) UpdateRuntimeSettings(tag, controller, logLevel string) {
- s.updateRuntimeSettings(tag, controller, logLevel)
- }
- // applyCongestionControl installs Xray's BBR on an accepted connection; quic-go
- // itself only ships New Reno, so a CUBIC choice is served as New Reno.
- func (s *Server) applyCongestionControl(conn *quic.Conn) {
- controller, _ := s.congestionControl.Load().(string)
- switch controller {
- case "bbr":
- xraycongestion.UseBBR(conn, bbr.ProfileStandard)
- s.logf(tuicLogDebug, "applied bbr congestion controller")
- case "cubic":
- s.logf(tuicLogDebug, "cubic is not available; applied new_reno congestion controller")
- default:
- s.logf(tuicLogDebug, "applied new_reno congestion controller")
- }
- }
- func (s *Server) registerConn(user *User, conn *quic.Conn) {
- s.activeConnsMu.Lock()
- defer s.activeConnsMu.Unlock()
- 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)
- }
- func (s *Server) unregisterConn(user *User, conn *quic.Conn) {
- s.activeConnsMu.Lock()
- if conns := s.activeConns[user.UUID]; conns != nil {
- if registered, ok := conns[conn]; ok {
- delete(conns, conn)
- if registered == user {
- s.users.sessionEnded(user)
- }
- }
- if len(conns) == 0 {
- delete(s.activeConns, user.UUID)
- }
- }
- s.activeConnsMu.Unlock()
- }
- func (s *Server) closeUserConns(user *User) {
- s.activeConnsMu.Lock()
- conns := s.activeConns[user.UUID]
- var toClose []*quic.Conn
- for conn, registered := range conns {
- if registered == user {
- toClose = append(toClose, conn)
- }
- }
- s.activeConnsMu.Unlock()
- for _, conn := range toClose {
- _ = conn.CloseWithError(0x100, "tuic: user revoked")
- }
- }
- func (s *Server) closeAllConns() {
- s.connectionsMu.Lock()
- var all []*quic.Conn
- for conn := range s.connections {
- all = append(all, conn)
- }
- s.connectionsMu.Unlock()
- for _, conn := range all {
- _ = conn.CloseWithError(0x00, "tuic: server closed")
- }
- }
- // UpdateUsers updates the active users dynamically without restarting the listener,
- // and terminates active QUIC sessions for any revoked or disabled users.
- func (s *Server) UpdateUsers(clients []TuicClientSettings) {
- if err := ValidateClients(clients); err != nil {
- s.logLimited(tuicLogWarn, "users-invalid", 30*time.Second, "User update rejected: %v", err)
- return
- }
- revoked := s.users.SetUsers(clients)
- if len(revoked) > 0 {
- s.logf(tuicLogDebug, "Revoked %d user registrations", len(revoked))
- }
- for _, u := range revoked {
- s.closeUserConns(u)
- }
- }
- // GetActiveEmails returns emails that were active within the specified time window.
- func (s *Server) GetActiveEmails(window time.Duration) []string {
- now := time.Now()
- var active []string
- s.lastOnline.Range(func(key, value any) bool {
- email := key.(string)
- lastTime := value.(time.Time)
- if now.Sub(lastTime) <= window {
- active = append(active, email)
- }
- return true
- })
- return active
- }
- // CollectClientTraffic drains and returns traffic deltas for each client.
- func (s *Server) CollectClientTraffic() []ClientTrafficDelta {
- deltas := s.users.CollectTrafficDeltas()
- for i := range deltas {
- deltas[i].InboundID = s.id
- }
- return deltas
- }
- // CollectAllTraffic drains client deltas once and returns total up, down and individual client deltas.
- func (s *Server) CollectAllTraffic() (int64, int64, []ClientTrafficDelta) {
- deltas := s.CollectClientTraffic()
- var totalUp, totalDown int64
- for _, d := range deltas {
- totalUp += d.Up
- totalDown += d.Down
- }
- return totalUp, totalDown, deltas
- }
- func (s *Server) markActive(email string) {
- if email != "" {
- s.lastOnline.Store(email, time.Now())
- }
- }
- // AddTestTraffic adds byte counts to a client for testing purposes.
- func (s *Server) AddTestTraffic(email string, up, down int64) bool {
- s.markActive(email)
- return s.users.AddTestTraffic(email, up, down)
- }
- func (s *Server) acceptLoop() {
- defer s.wg.Done()
- for {
- conn, err := s.quicListener.Accept(s.ctx)
- if err != nil {
- if s.closed.Load() {
- return
- }
- s.running.Store(false)
- s.logf(tuicLogError, "QUIC listener stopped accepting connections: %v", err)
- _ = s.quicListener.Close()
- return
- }
- s.connectionsMu.Lock()
- if s.closed.Load() {
- s.connectionsMu.Unlock()
- _ = conn.CloseWithError(0x00, "tuic: server closed")
- return
- }
- s.connections[conn] = struct{}{}
- s.connectionsMu.Unlock()
- s.wg.Add(1)
- go func(c *quic.Conn) {
- defer s.wg.Done()
- s.handleConn(c)
- }(conn)
- }
- }
- func (s *Server) handleConn(conn *quic.Conn) {
- defer func() {
- s.connectionsMu.Lock()
- delete(s.connections, conn)
- s.connectionsMu.Unlock()
- }()
- s.applyCongestionControl(conn)
- sessCtx, sessCancel := context.WithCancel(s.ctx)
- stopConnWatch := context.AfterFunc(conn.Context(), sessCancel)
- defer stopConnWatch()
- var (
- authUser atomic.Pointer[User]
- authState atomic.Uint32 // 0 pending, 1 authenticated, 2 timed out
- authSignal = make(chan struct{})
- authOnce sync.Once
- udpAssociations = newUdpAssociationRegistry(s.maxUdpRelayPacketSize)
- )
- authTimer := time.AfterFunc(s.authTimeout, func() {
- if authState.CompareAndSwap(0, 2) {
- s.logf(tuicLogWarn, "client authentication timed out")
- sessCancel()
- _ = conn.CloseWithError(0x100, "tuic: authentication timeout")
- }
- })
- defer authTimer.Stop()
- authenticate := func(rawUUID [16]byte, token [32]byte) (*User, error) {
- tlsState := conn.ConnectionState().TLS
- return s.users.AuthenticateAndRegister(&tlsState, rawUUID, token, func(user *User) bool {
- if !authState.CompareAndSwap(0, 1) {
- return authState.Load() == 1 && authUser.Load() == user
- }
- authUser.Store(user)
- s.registerConn(user, conn)
- s.markActive(user.Email)
- authTimer.Stop()
- s.logf(tuicLogInfo, "client authenticated")
- authOnce.Do(func() { close(authSignal) })
- return true
- })
- }
- waitForAuth := func() (*User, error) {
- if authState.Load() == 1 {
- if u := authUser.Load(); u != nil {
- return u, nil
- }
- }
- if authState.Load() == 2 {
- return nil, errors.New("tuic: authentication timeout")
- }
- select {
- case <-authSignal:
- if u := authUser.Load(); u != nil {
- return u, nil
- }
- return nil, errors.New("tuic: authentication unavailable")
- case <-sessCtx.Done():
- return nil, sessCtx.Err()
- }
- }
- var relayWg sync.WaitGroup
- cleanup := func() {
- sessCancel()
- udpAssociations.closeAll()
- relayWg.Wait()
- if u := authUser.Load(); u != nil {
- s.unregisterConn(u, conn)
- }
- _ = conn.CloseWithError(0, "")
- }
- defer cleanup()
- var innerWg sync.WaitGroup
- // Loop 1: Unidirectional streams
- innerWg.Add(1)
- go func() {
- defer innerWg.Done()
- for {
- uniStream, err := conn.AcceptUniStream(sessCtx)
- if err != nil {
- return
- }
- innerWg.Add(1)
- go func(stream *quic.ReceiveStream) {
- defer innerWg.Done()
- s.handleUniStream(sessCtx, conn, stream, authenticate, waitForAuth, udpAssociations, &relayWg)
- }(uniStream)
- }
- }()
- // Loop 2: Bidirectional streams
- innerWg.Add(1)
- go func() {
- defer innerWg.Done()
- for {
- biStream, err := conn.AcceptStream(sessCtx)
- if err != nil {
- return
- }
- innerWg.Add(1)
- go func(stream *quic.Stream) {
- defer innerWg.Done()
- s.handleBiStream(sessCtx, conn, stream, authenticate, waitForAuth)
- }(biStream)
- }
- }()
- // Loop 3: Datagrams
- innerWg.Add(1)
- go func() {
- defer innerWg.Done()
- for {
- dgram, err := conn.ReceiveDatagram(sessCtx)
- if err != nil {
- return
- }
- s.handleDatagram(sessCtx, conn, dgram, waitForAuth, udpAssociations, &relayWg)
- }
- }()
- innerWg.Add(1)
- go func() {
- defer innerWg.Done()
- ticker := time.NewTicker(time.Minute)
- defer ticker.Stop()
- for {
- select {
- case <-ticker.C:
- udpAssociations.reapIdle(time.Now())
- case <-sessCtx.Done():
- return
- }
- }
- }()
- innerWg.Wait()
- }
- func (s *Server) handleUniStream(
- ctx context.Context,
- conn *quic.Conn,
- stream *quic.ReceiveStream,
- authenticate func([16]byte, [32]byte) (*User, error),
- waitForAuth func() (*User, error),
- udpAssociations *udpAssociationRegistry,
- relayWg *sync.WaitGroup,
- ) {
- defer stream.CancelRead(0)
- _, cmd, err := ReadCommand(stream)
- if err != nil {
- return
- }
- switch cmd {
- case CmdAuthenticate:
- var authData [16 + 32]byte
- if _, err := io.ReadFull(stream, authData[:]); err != nil {
- return
- }
- var rawUUID [16]byte
- var token [32]byte
- copy(rawUUID[:], authData[0:16])
- copy(token[:], authData[16:48])
- _, err := authenticate(rawUUID, token)
- if err != nil {
- s.logLimited(tuicLogWarn, "auth-rejected", 30*time.Second, "client authentication rejected")
- _ = conn.CloseWithError(0x100, "tuic: authentication failed")
- return
- }
- case CmdDissociate:
- if _, err := waitForAuth(); err != nil {
- return
- }
- var assocIDBytes [2]byte
- if _, err := io.ReadFull(stream, assocIDBytes[:]); err != nil {
- return
- }
- assocID := binary.BigEndian.Uint16(assocIDBytes[:])
- if udpAssociations.dissociate(assocID) {
- s.logf(tuicLogInfo, "UDP association %d closed", assocID)
- }
- case CmdPacket:
- user, err := waitForAuth()
- if err != nil {
- return
- }
- hdr, err := ReadPacketHeader(stream)
- if err != nil || int(hdr.Size) > s.maxUdpRelayPacketSize {
- s.logLimited(tuicLogWarn, "udp-malformed", 30*time.Second, "UDP packet rejected: malformed header or size limit")
- return
- }
- payload, err := readPacketPayload(stream, hdr)
- if err != nil {
- return
- }
- s.handlePacket(ctx, conn, user, hdr, payload, packetTransportStream, udpAssociations, relayWg)
- }
- }
- func (s *Server) handleBiStream(
- ctx context.Context,
- conn *quic.Conn,
- stream *quic.Stream,
- authenticate func([16]byte, [32]byte) (*User, error),
- waitForAuth func() (*User, error),
- ) {
- defer stream.Close()
- defer stream.CancelRead(0)
- _, cmd, err := ReadCommand(stream)
- if err != nil {
- return
- }
- switch cmd {
- case CmdAuthenticate:
- var authData [16 + 32]byte
- if _, err := io.ReadFull(stream, authData[:]); err != nil {
- return
- }
- var rawUUID [16]byte
- var token [32]byte
- copy(rawUUID[:], authData[0:16])
- copy(token[:], authData[16:48])
- _, err := authenticate(rawUUID, token)
- if err != nil {
- s.logLimited(tuicLogWarn, "auth-rejected", 30*time.Second, "client authentication rejected")
- _ = conn.CloseWithError(0x100, "tuic: authentication failed")
- return
- }
- case CmdConnect:
- user, err := waitForAuth()
- if err != nil {
- return
- }
- target, err := ReadAddress(stream)
- if err != nil {
- s.logLimited(tuicLogWarn, "tcp-relay", 30*time.Second, "TCP relay failed: malformed target address")
- return
- }
- s.markActive(user.Email)
- if !isPacketTarget(target) {
- s.logLimited(tuicLogWarn, "tcp-relay", 30*time.Second, "TCP relay failed: invalid target address")
- return
- }
- socksConn, err := s.relay.DialTCP(ctx, user.Email, target)
- if err != nil {
- s.logLimited(tuicLogWarn, "tcp-relay", 30*time.Second, "TCP relay failed: %v", err)
- return
- }
- s.logf(tuicLogInfo, "TCP relay started")
- PipeBiDirectionalContext(ctx, tcpRelayStream{stream}, socksConn, &user.Traffic.BytesUp, &user.Traffic.BytesDown)
- s.logf(tuicLogDebug, "TCP relay closed")
- }
- }
- type packetFragmentKey struct {
- assocID uint16
- pktID uint16
- transport uint8
- }
- const (
- packetTransportDatagram uint8 = iota
- packetTransportStream
- )
- type udpRelaySession struct {
- relay *SocksUDPSession
- responseTransport uint8
- }
- type packetReassembly struct {
- total uint8
- received uint8
- size int
- frags [][]byte
- addr *Address
- updatedAt time.Time
- }
- type packetReassembler struct {
- mu sync.Mutex
- maxPacketSize int
- packets map[packetFragmentKey]*packetReassembly
- }
- const (
- maxSafeUdpRelayPacketSize = maxSocksUdpDatagramSize - 262
- maxLegacyUdpRelayPacketSize = maxSocksUdpDatagramSize
- maxUdpRelayPacketSize = maxSafeUdpRelayPacketSize
- maxPendingPacketAssemblies = 32
- packetAssemblyTimeout = 10 * time.Second
- )
- func newPacketReassembler(maxPacketSize int) *packetReassembler {
- if maxPacketSize <= 0 || maxPacketSize > maxUdpRelayPacketSize {
- maxPacketSize = maxUdpRelayPacketSize
- }
- return &packetReassembler{
- maxPacketSize: maxPacketSize,
- packets: make(map[packetFragmentKey]*packetReassembly),
- }
- }
- func (pr *packetReassembler) feed(transport uint8, hdr *PacketHeader, payload []byte) (*Address, []byte, bool) {
- if hdr == nil || hdr.FragTotal == 0 || hdr.FragID >= hdr.FragTotal || int(hdr.Size) != len(payload) || len(payload) > pr.maxPacketSize {
- return nil, nil, false
- }
- pr.mu.Lock()
- defer pr.mu.Unlock()
- now := time.Now()
- pr.expireLocked(now)
- key := packetFragmentKey{assocID: hdr.AssocID, pktID: hdr.PktID, transport: transport}
- if hdr.FragTotal == 1 {
- if hdr.FragID != 0 || !isPacketTarget(hdr.Addr) {
- return nil, nil, false
- }
- delete(pr.packets, key)
- return hdr.Addr, payload, true
- }
- if (hdr.FragID == 0 && !isPacketTarget(hdr.Addr)) || (hdr.FragID != 0 && hdr.Addr != nil && hdr.Addr.Type != AddrTypeNone) {
- return nil, nil, false
- }
- entry, ok := pr.packets[key]
- if !ok {
- if len(pr.packets) >= maxPendingPacketAssemblies {
- return nil, nil, false
- }
- entry = &packetReassembly{
- total: hdr.FragTotal,
- frags: make([][]byte, hdr.FragTotal),
- updatedAt: now,
- }
- pr.packets[key] = entry
- } else if entry.total != hdr.FragTotal {
- delete(pr.packets, key)
- return nil, nil, false
- }
- fragment := entry.frags[hdr.FragID]
- if fragment != nil {
- if !bytes.Equal(fragment, payload) {
- delete(pr.packets, key)
- }
- return nil, nil, false
- }
- if entry.size+len(payload) > pr.maxPacketSize {
- delete(pr.packets, key)
- return nil, nil, false
- }
- entry.frags[hdr.FragID] = make([]byte, len(payload))
- copy(entry.frags[hdr.FragID], payload)
- entry.size += len(payload)
- entry.received++
- entry.updatedAt = now
- if hdr.FragID == 0 {
- entry.addr = hdr.Addr
- }
- if entry.received == entry.total {
- delete(pr.packets, key)
- if !isPacketTarget(entry.addr) {
- return nil, nil, false
- }
- assembled := make([]byte, 0, entry.size)
- for _, f := range entry.frags {
- assembled = append(assembled, f...)
- }
- return entry.addr, assembled, true
- }
- return nil, nil, false
- }
- func (pr *packetReassembler) expireLocked(now time.Time) {
- for key, entry := range pr.packets {
- if now.Sub(entry.updatedAt) > packetAssemblyTimeout {
- delete(pr.packets, key)
- }
- }
- }
- func (pr *packetReassembler) clearAssociation(assocID uint16) {
- pr.mu.Lock()
- defer pr.mu.Unlock()
- for key := range pr.packets {
- if key.assocID == assocID {
- delete(pr.packets, key)
- }
- }
- }
- func (pr *packetReassembler) clearAll() {
- pr.mu.Lock()
- pr.packets = make(map[packetFragmentKey]*packetReassembly)
- pr.mu.Unlock()
- }
- func isPacketTarget(addr *Address) bool {
- return addr != nil && addr.Type != AddrTypeNone
- }
- func readPacketPayload(r io.Reader, hdr *PacketHeader) ([]byte, error) {
- payload := make([]byte, int(hdr.Size))
- if _, err := io.ReadFull(r, payload); err != nil {
- return nil, err
- }
- return payload, nil
- }
- func (s *Server) handlePacket(
- ctx context.Context,
- conn *quic.Conn,
- user *User,
- hdr *PacketHeader,
- payload []byte,
- transport uint8,
- udpAssociations *udpAssociationRegistry,
- relayWg *sync.WaitGroup,
- ) {
- association, addr, fullPayload, complete := udpAssociations.feed(transport, hdr, payload)
- if association == nil {
- s.logLimited(tuicLogWarn, "udp-malformed", 30*time.Second, "UDP packet rejected")
- }
- if !complete {
- return
- }
- s.forwardUDPPacket(ctx, conn, user, hdr.AssocID, association, addr, fullPayload, udpAssociations, relayWg)
- }
- func (s *Server) handleDatagram(
- ctx context.Context,
- conn *quic.Conn,
- dgram []byte,
- waitForAuth func() (*User, error),
- udpAssociations *udpAssociationRegistry,
- relayWg *sync.WaitGroup,
- ) {
- if len(dgram) < 2 || dgram[0] != ProtocolVersion {
- return
- }
- cmd := dgram[1]
- switch cmd {
- case CmdHeartbeat:
- if u, _ := waitForAuth(); u != nil {
- s.markActive(u.Email)
- }
- case CmdPacket:
- user, err := waitForAuth()
- if err != nil {
- return
- }
- r := bytes.NewReader(dgram[2:])
- hdr, err := ReadPacketHeader(r)
- if err != nil || int(hdr.Size) > s.maxUdpRelayPacketSize {
- s.logLimited(tuicLogWarn, "udp-malformed", 30*time.Second, "UDP packet rejected: malformed header or size limit")
- return
- }
- payload, err := readPacketPayload(r, hdr)
- if err != nil || r.Len() != 0 {
- return
- }
- s.handlePacket(ctx, conn, user, hdr, payload, packetTransportDatagram, udpAssociations, relayWg)
- case CmdDissociate:
- if len(dgram) >= 4 {
- assocID := binary.BigEndian.Uint16(dgram[2:4])
- if udpAssociations.dissociate(assocID) {
- s.logf(tuicLogInfo, "UDP association %d closed", assocID)
- }
- }
- }
- }
- func (s *Server) forwardUDPPacket(
- ctx context.Context,
- conn *quic.Conn,
- user *User,
- assocID uint16,
- association *udpAssociation,
- target *Address,
- payload []byte,
- udpAssociations *udpAssociationRegistry,
- relayWg *sync.WaitGroup,
- ) {
- if len(payload) > s.maxUdpRelayPacketSize || !isPacketTarget(target) {
- return
- }
- if association == nil || len(payload) > s.maxUdpRelayPacketSize || !isPacketTarget(target) {
- return
- }
- association, created, err := udpAssociations.ensureRelay(ctx, assocID, association, user, s.relay)
- if err != nil {
- s.logLimited(tuicLogWarn, "udp-dial", 30*time.Second, "UDP relay could not be opened: %v", err)
- return
- }
- sess := association.relay
- if created {
- s.logf(tuicLogInfo, "UDP association %d started", assocID)
- relayWg.Add(1)
- go func() {
- defer relayWg.Done()
- s.relayUDPResponses(ctx, conn, user, assocID, association, udpAssociations, sess)
- }()
- }
- if _, err := sess.relay.Send(target, payload); err != nil {
- s.logLimited(tuicLogWarn, "udp-send", 30*time.Second, "UDP relay request failed: %v", err)
- return
- }
- user.Traffic.BytesUp.Add(int64(len(payload)))
- s.markActive(user.Email)
- }
- const (
- maxDatagramFragmentSize = 850
- maxStreamFragmentSize = 8 * 1024
- )
- func (s *Server) relayUDPResponses(
- ctx context.Context,
- conn *quic.Conn,
- user *User,
- assocID uint16,
- association *udpAssociation,
- associations *udpAssociationRegistry,
- sess *udpRelaySession,
- ) {
- defer associations.release(assocID, association)
- buf := make([]byte, s.maxUdpRelayPacketSize+263)
- var nextPktID uint16
- for {
- srcAddr, respPayload, err := sess.relay.Receive(buf)
- if err != nil {
- if ctx.Err() == nil && !sess.relay.closed.Load() {
- s.logLimited(tuicLogWarn, "udp-receive", 30*time.Second, "UDP relay receive failed: %v", err)
- }
- return
- }
- if len(respPayload) > s.maxUdpRelayPacketSize {
- continue
- }
- user.Traffic.BytesDown.Add(int64(len(respPayload)))
- s.markActive(user.Email)
- nextPktID++
- if err := s.sendUDPPacketFragments(ctx, conn, assocID, nextPktID, srcAddr, respPayload, sess.responseTransport); err != nil {
- if ctx.Err() == nil {
- s.logLimited(tuicLogWarn, "udp-response", 30*time.Second, "UDP relay response failed: %v", err)
- }
- return
- }
- associations.touch(assocID, association, time.Now())
- }
- }
- func (s *Server) sendUDPPacketFragments(
- ctx context.Context,
- conn *quic.Conn,
- assocID, pktID uint16,
- srcAddr *Address,
- payload []byte,
- transport uint8,
- ) error {
- if len(payload) > s.maxUdpRelayPacketSize || !isPacketTarget(srcAddr) {
- return fmt.Errorf("tuic: UDP response exceeds configured limit or has invalid source address")
- }
- fragmentSize := maxDatagramFragmentSize
- if transport == packetTransportStream {
- fragmentSize = maxStreamFragmentSize
- }
- fragmentTotal := (len(payload) + fragmentSize - 1) / fragmentSize
- if fragmentTotal == 0 {
- fragmentTotal = 1
- }
- if fragmentTotal > 255 {
- return fmt.Errorf("tuic: UDP response requires too many fragments: %d", fragmentTotal)
- }
- for i := 0; i < fragmentTotal; i++ {
- start := i * fragmentSize
- end := min(start+fragmentSize, len(payload))
- addr := (*Address)(nil)
- if i == 0 {
- addr = srcAddr
- }
- var frame bytes.Buffer
- if err := WritePacket(&frame, assocID, pktID, uint8(fragmentTotal), uint8(i), addr, payload[start:end]); err != nil {
- return err
- }
- if transport == packetTransportStream {
- stream, err := conn.OpenUniStreamSync(ctx)
- if err != nil {
- return err
- }
- if _, err := stream.Write(frame.Bytes()); err != nil {
- stream.CancelWrite(0)
- return err
- }
- if err := stream.Close(); err != nil {
- return err
- }
- continue
- }
- if err := conn.SendDatagram(frame.Bytes()); err != nil {
- return err
- }
- }
- return nil
- }
- // Close gracefully stops the server and releases all network resources.
- func (s *Server) Close() error {
- if s.closed.Swap(true) {
- return nil
- }
- s.running.Store(false)
- s.logf(tuicLogInfo, "listener stopped")
- s.cancel()
- var err error
- if s.quicListener != nil {
- err = s.quicListener.Close()
- }
- if s.packetConn != nil {
- _ = s.packetConn.Close()
- }
- s.closeAllConns()
- s.wg.Wait()
- return err
- }
- const (
- tuicLogDebug uint32 = iota
- tuicLogInfo
- tuicLogWarn
- tuicLogError
- )
- func parseLogLevel(level string) uint32 {
- switch strings.ToLower(strings.TrimSpace(level)) {
- case "debug":
- return tuicLogDebug
- case "warn", "warning":
- return tuicLogWarn
- case "error":
- return tuicLogError
- default:
- return tuicLogInfo
- }
- }
- func normalizeCongestionControl(controller string) (string, bool) {
- normalized, err := NormalizeCongestionControl(controller)
- if err != nil {
- return "new_reno", false
- }
- return normalized, true
- }
- func (s *Server) logf(level uint32, format string, args ...any) {
- if level < s.logLevel.Load() {
- return
- }
- tag := ""
- if value := s.tag.Load(); value != nil && *value != "" {
- tag = fmt.Sprintf(" (%s)", *value)
- }
- message := fmt.Sprintf("tuic: inbound %d%s: %s", s.id, tag, fmt.Sprintf(format, args...))
- switch level {
- case tuicLogDebug:
- logger.Debugf("%s", message)
- case tuicLogInfo:
- logger.Infof("%s", message)
- case tuicLogWarn:
- logger.Warningf("%s", message)
- case tuicLogError:
- logger.Errorf("%s", message)
- }
- }
- func (s *Server) logLimited(level uint32, key string, interval time.Duration, format string, args ...any) {
- if level < s.logLevel.Load() {
- return
- }
- value, _ := s.logThrottle.LoadOrStore(key, &atomic.Int64{})
- stamp := value.(*atomic.Int64)
- now := time.Now().UnixNano()
- last := stamp.Load()
- if last != 0 && time.Duration(now-last) < interval {
- return
- }
- if stamp.CompareAndSwap(last, now) {
- s.logf(level, format, args...)
- }
- }
- func loadCertificate(certInput, keyInput string) (tls.Certificate, error) {
- if strings.Contains(certInput, "-----BEGIN CERTIFICATE-----") {
- return tls.X509KeyPair([]byte(certInput), []byte(keyInput))
- }
- return tls.LoadX509KeyPair(certInput, keyInput)
- }
- // QUIC Close sends FIN but does not interrupt reads. Relay cancellation must
- // cancel reads too, while a normal EOF preserves the peer's half-close.
- type tcpRelayStream struct{ *quic.Stream }
- func (stream tcpRelayStream) Close() error {
- stream.CancelRead(0)
- return stream.Stream.Close()
- }
- func (stream tcpRelayStream) CloseWrite() error { return stream.Stream.Close() }
|