package hysteria import ( "context" go_tls "crypto/tls" "net/http" "net/url" "reflect" "runtime" "strconv" "sync" "time" "github.com/apernet/quic-go" "github.com/apernet/quic-go/http3" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net/cnc" "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/finalmask" "github.com/xtls/xray-core/transport/internet/hysteria/congestion" "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" "github.com/xtls/xray-core/transport/internet/hysteria/udphop" "github.com/xtls/xray-core/transport/internet/stat" "github.com/xtls/xray-core/transport/internet/tls" ) type client struct { sync.Mutex dest net.Destination config *Config tlsConfig *go_tls.Config socketConfig *internet.SocketConfig udpmaskManager *finalmask.UdpmaskManager quicParams *internet.QuicParams conn *quic.Conn tr *quic.Transport pktConn net.PacketConn udpSM *udpSessionManager } func (c *client) status() status { if c.conn == nil { return StatusNull } select { case <-c.conn.Context().Done(): return StatusInactive default: return StatusActive } } func (c *client) close() { c.conn.CloseWithError(closeErrCodeOK, "") c.tr.Close() c.pktConn.Close() c.conn = nil c.tr = nil c.pktConn = nil c.udpSM = nil } func (c *client) dial() error { status := c.status() if status == StatusActive { return nil } if status == StatusInactive { c.close() } quicParams := c.quicParams if quicParams == nil { quicParams = &internet.QuicParams{ BbrProfile: string(bbr.ProfileStandard), UdpHop: &internet.UdpHop{}, } } quicConfig := &quic.Config{ InitialStreamReceiveWindow: quicParams.InitStreamReceiveWindow, MaxStreamReceiveWindow: quicParams.MaxStreamReceiveWindow, InitialConnectionReceiveWindow: quicParams.InitConnReceiveWindow, MaxConnectionReceiveWindow: quicParams.MaxConnReceiveWindow, MaxIdleTimeout: time.Duration(quicParams.MaxIdleTimeout) * time.Second, KeepAlivePeriod: time.Duration(quicParams.KeepAlivePeriod) * time.Second, DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery || (runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin"), EnableDatagrams: true, MaxDatagramFrameSize: MaxDatagramFrameSize, OmitMaxDatagramFrameSize: time.Now().After(time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)), DisablePathManager: true, } if quicParams.InitStreamReceiveWindow == 0 { quicConfig.InitialStreamReceiveWindow = 8388608 } if quicParams.MaxStreamReceiveWindow == 0 { quicConfig.MaxStreamReceiveWindow = 8388608 } if quicParams.InitConnReceiveWindow == 0 { quicConfig.InitialConnectionReceiveWindow = 8388608 * 5 / 2 } if quicParams.MaxConnReceiveWindow == 0 { quicConfig.MaxConnectionReceiveWindow = 8388608 * 5 / 2 } if quicParams.MaxIdleTimeout == 0 { quicConfig.MaxIdleTimeout = 30 * time.Second } // if quicParams.KeepAlivePeriod == 0 { // quicConfig.KeepAlivePeriod = 10 * time.Second // } var pktConn net.PacketConn var udpAddr *net.UDPAddr var err error udpAddr, err = net.ResolveUDPAddr("udp", c.dest.NetAddr()) if err != nil { return err } if len(quicParams.UdpHop.Ports) > 0 { pktConn, err = udphop.NewUDPHopPacketConn(udphop.ToAddrs(udpAddr.IP, quicParams.UdpHop.Ports), time.Duration(quicParams.UdpHop.IntervalMin)*time.Second, time.Duration(quicParams.UdpHop.IntervalMax)*time.Second, c.udpHopDialer) if err != nil { return err } } else { conn, err := internet.DialSystem(context.Background(), c.dest, c.socketConfig) if err != nil { return err } switch c := conn.(type) { case *internet.PacketConnWrapper: pktConn = c.PacketConn case *net.UDPConn: pktConn = c case *cnc.Connection: pktConn = &internet.FakePacketConn{Conn: c} default: panic(reflect.TypeOf(c)) } } if c.udpmaskManager != nil { newConn, err := c.udpmaskManager.WrapPacketConnClient(pktConn) if err != nil { pktConn.Close() return errors.New("mask err").Base(err) } pktConn = newConn } tr := &quic.Transport{Conn: pktConn} var conn *quic.Conn rt := &http3.Transport{ TLSClientConfig: c.tlsConfig, QUICConfig: quicConfig, Dial: func(ctx context.Context, _ string, tlsCfg *go_tls.Config, cfg *quic.Config) (*quic.Conn, error) { qc, err := tr.DialEarly(ctx, udpAddr, tlsCfg, cfg) if err != nil { return nil, err } conn = qc return qc, nil }, } req := &http.Request{ Method: http.MethodPost, URL: &url.URL{ Scheme: "https", Host: URLHost, Path: URLPath, }, Header: http.Header{ RequestHeaderAuth: []string{c.config.Auth}, CommonHeaderCCRX: []string{strconv.FormatUint(quicParams.BrutalDown, 10)}, CommonHeaderPadding: []string{AuthRequestPadding.String()}, }, } resp, err := rt.RoundTrip(req) if err != nil { if conn != nil { _ = conn.CloseWithError(closeErrCodeProtocolError, "") } _ = tr.Close() _ = pktConn.Close() return err } if resp.StatusCode != StatusAuthOK { _ = conn.CloseWithError(closeErrCodeProtocolError, "") _ = tr.Close() _ = pktConn.Close() return errors.New("auth failed code ", resp.StatusCode) } _ = resp.Body.Close() // udp, _ := strconv.ParseBool(resp.Header.Get(ResponseHeaderUDPEnabled)) down, _ := strconv.ParseUint(resp.Header.Get(CommonHeaderCCRX), 10, 64) switch quicParams.Congestion { case "reno": case "bbr": congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile)) case "", "brutal": if quicParams.BrutalUp == 0 || down == 0 { congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile)) } else { congestion.UseBrutal(conn, min(quicParams.BrutalUp, down)) } case "force-brutal": congestion.UseBrutal(conn, quicParams.BrutalUp) default: panic(quicParams.Congestion) } c.pktConn = pktConn c.tr = tr c.conn = conn c.udpSM = &udpSessionManager{ conn: conn, m: make(map[uint32]*InterConn), next: 1, } go c.udpSM.run() return nil } func (c *client) tcp() (stat.Connection, error) { c.Lock() defer c.Unlock() err := c.dial() if err != nil { return nil, err } stream, err := c.conn.OpenStream() if err != nil { return nil, err } return &interConn{ stream: stream, local: c.conn.LocalAddr(), remote: c.conn.RemoteAddr(), client: true, }, nil } func (c *client) udp() (stat.Connection, error) { c.Lock() defer c.Unlock() err := c.dial() if err != nil { return nil, err } return c.udpSM.udp() } func (c *client) clean() { c.Lock() if c.status() == StatusInactive { c.close() } c.Unlock() } func (c *client) udpHopDialer(addr *net.UDPAddr) (net.PacketConn, error) { conn, err := internet.DialSystem(context.Background(), net.UDPDestination(net.IPAddress(addr.IP), net.Port(addr.Port)), c.socketConfig) if err != nil { errors.LogInfoInner(context.Background(), err, "skip hop: failed to dial to dest") return nil, errors.New("failed to dial to dest").Base(err) } var pktConn net.PacketConn switch c := conn.(type) { case *internet.PacketConnWrapper: pktConn = c.PacketConn case *net.UDPConn: pktConn = c default: errors.LogInfo(context.Background(), "skip hop: invalid conn ", reflect.TypeOf(c)) conn.Close() return nil, errors.New("invalid conn ", reflect.TypeOf(c)) } return pktConn, nil } type dialerConf struct { net.Destination *internet.MemoryStreamConfig } type clientManager struct { sync.RWMutex m map[dialerConf]*client } func (m *clientManager) clean() { ticker := time.NewTicker(idleCleanupInterval) for range ticker.C { m.RLock() for _, c := range m.m { c.clean() } m.RUnlock() } } var manager *clientManager var initmanager sync.Once func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) { tlsConfig := tls.ConfigFromStreamSettings(streamSettings) if tlsConfig == nil { return nil, errors.New("tls config is nil") } datagram := DatagramFromContext(ctx) dest.Network = net.Network_UDP initmanager.Do(func() { manager = &clientManager{ m: make(map[dialerConf]*client), } go manager.clean() }) manager.RLock() c := manager.m[dialerConf{dest, streamSettings}] manager.RUnlock() if c == nil { manager.Lock() c = manager.m[dialerConf{dest, streamSettings}] if c == nil { c = &client{ dest: dest, config: streamSettings.ProtocolSettings.(*Config), tlsConfig: tlsConfig.GetTLSConfig(), socketConfig: streamSettings.SocketSettings, udpmaskManager: streamSettings.UdpmaskManager, quicParams: streamSettings.QuicParams, } manager.m[dialerConf{dest, streamSettings}] = c } manager.Unlock() } if datagram { return c.udp() } return c.tcp() } func init() { common.Must(internet.RegisterTransportDialer(protocolName, Dial)) }