Ver código fonte

Update BBR and Hysteria congestion control & Migrate legacy Hysteria protocol to library

世界 2 anos atrás
pai
commit
31c294d998

+ 3 - 3
go.mod

@@ -24,12 +24,12 @@ require (
 	github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a
 	github.com/sagernet/gomobile v0.0.0-20230915142329-c6740b6d2950
 	github.com/sagernet/gvisor v0.0.0-20230930141345-5fef6f2e17ab
-	github.com/sagernet/quic-go v0.0.0-20230919101909-0cc6c5dcecee
+	github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460
 	github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
 	github.com/sagernet/sing v0.2.16-0.20231021090846-8002db54c028
 	github.com/sagernet/sing-dns v0.1.10
 	github.com/sagernet/sing-mux v0.1.3
-	github.com/sagernet/sing-quic v0.1.2
+	github.com/sagernet/sing-quic v0.1.3-0.20231026034240-fa3d997246b6
 	github.com/sagernet/sing-shadowsocks v0.2.5
 	github.com/sagernet/sing-shadowsocks2 v0.1.4
 	github.com/sagernet/sing-shadowtls v0.1.4
@@ -45,7 +45,6 @@ require (
 	go.uber.org/zap v1.26.0
 	go4.org/netipx v0.0.0-20230824141953-6213f710f925
 	golang.org/x/crypto v0.14.0
-	golang.org/x/exp v0.0.0-20231006140011-7918f672742d
 	golang.org/x/net v0.17.0
 	golang.org/x/sys v0.13.0
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
@@ -85,6 +84,7 @@ require (
 	github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
 	github.com/zeebo/blake3 v0.2.3 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
+	golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
 	golang.org/x/mod v0.13.0 // indirect
 	golang.org/x/text v0.13.0 // indirect
 	golang.org/x/time v0.3.0 // indirect

+ 4 - 4
go.sum

@@ -104,8 +104,8 @@ github.com/sagernet/gvisor v0.0.0-20230930141345-5fef6f2e17ab h1:u+xQoi/Yc6bNUvT
 github.com/sagernet/gvisor v0.0.0-20230930141345-5fef6f2e17ab/go.mod h1:3akUhSHSVtLuJaYcW5JPepUraBOW06Ibz2HKwaK5rOk=
 github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
 github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
-github.com/sagernet/quic-go v0.0.0-20230919101909-0cc6c5dcecee h1:ykuhl9jCS638N+jw1vC9AvT9bbQn6xRNScP2FWPV9dM=
-github.com/sagernet/quic-go v0.0.0-20230919101909-0cc6c5dcecee/go.mod h1:0CfhWwZAeXGYM9+Nkkw1zcQtFHQC8KWjbpeDv7pu8iw=
+github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460 h1:dAe4OIJAtE0nHOzTHhAReQteh3+sa63rvXbuIpbeOTY=
+github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460/go.mod h1:uJGpmJCOcMQqMlHKc3P1Vz6uygmpz4bPeVIoOhdVQnM=
 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
 github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
@@ -116,8 +116,8 @@ github.com/sagernet/sing-dns v0.1.10 h1:iIU7nRBlUYj+fF2TaktGIvRiTFFrHwSMedLQsvlT
 github.com/sagernet/sing-dns v0.1.10/go.mod h1:vtUimtf7Nq9EdvD5WTpfCr69KL1M7bcgOVKiYBiAY/c=
 github.com/sagernet/sing-mux v0.1.3 h1:fAf7PZa2A55mCeh0KKM02f1k2Y4vEmxuZZ/51ahkkLA=
 github.com/sagernet/sing-mux v0.1.3/go.mod h1:wGeIeiiFLx4HUM5LAg65wrNZ/X1muOimqK0PEhNbPi0=
-github.com/sagernet/sing-quic v0.1.2 h1:+u9CRf0KHi5HgXmJ3eB0CtqpWXtF0lx2QlWq+ZFZ+XY=
-github.com/sagernet/sing-quic v0.1.2/go.mod h1:H1TX0/y9UUM43wyaLQ+qjg2+o901ibYtwWX2rWG+a3o=
+github.com/sagernet/sing-quic v0.1.3-0.20231026034240-fa3d997246b6 h1:w+TUbIZKZFSdf/AUa/y33kY9xaLeNGz/tBNcNhqpqfg=
+github.com/sagernet/sing-quic v0.1.3-0.20231026034240-fa3d997246b6/go.mod h1:1M7xP4802K9Kz6BQ7LlA7UeCapWvWlH1Htmk2bAqkWc=
 github.com/sagernet/sing-shadowsocks v0.2.5 h1:qxIttos4xu6ii7MTVJYA8EFQR7Q3KG6xMqmLJIFtBaY=
 github.com/sagernet/sing-shadowsocks v0.2.5/go.mod h1:MGWGkcU2xW2G2mfArT9/QqpVLOGU+dBaahZCtPHdt7A=
 github.com/sagernet/sing-shadowsocks2 v0.1.4 h1:vht2M8t3m5DTgXR2j24KbYOygG5aOp+MUhpQnAux728=

+ 85 - 267
inbound/hysteria.go

@@ -4,104 +4,38 @@ package inbound
 
 import (
 	"context"
-	"sync"
+	"net"
 
-	"github.com/sagernet/quic-go"
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/humanize"
 	"github.com/sagernet/sing-box/common/tls"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing-box/transport/hysteria"
-	"github.com/sagernet/sing-quic"
-	hyCC "github.com/sagernet/sing-quic/hysteria2/congestion"
+	"github.com/sagernet/sing-quic/hysteria"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/auth"
 	E "github.com/sagernet/sing/common/exceptions"
-	F "github.com/sagernet/sing/common/format"
-	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
-
-	"golang.org/x/exp/slices"
 )
 
 var _ adapter.Inbound = (*Hysteria)(nil)
 
 type Hysteria struct {
 	myInboundAdapter
-	quicConfig   *quic.Config
 	tlsConfig    tls.ServerConfig
-	authKey      []string
-	authUser     []string
-	xplusKey     []byte
-	sendBPS      uint64
-	recvBPS      uint64
-	listener     qtls.Listener
-	udpAccess    sync.RWMutex
-	udpSessionId uint32
-	udpSessions  map[uint32]chan *hysteria.UDPMessage
-	udpDefragger hysteria.Defragger
+	service      *hysteria.Service[int]
+	userNameList []string
 }
 
 func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (*Hysteria, error) {
 	options.UDPFragmentDefault = true
-	quicConfig := &quic.Config{
-		InitialStreamReceiveWindow:     options.ReceiveWindowConn,
-		MaxStreamReceiveWindow:         options.ReceiveWindowConn,
-		InitialConnectionReceiveWindow: options.ReceiveWindowClient,
-		MaxConnectionReceiveWindow:     options.ReceiveWindowClient,
-		MaxIncomingStreams:             int64(options.MaxConnClient),
-		KeepAlivePeriod:                hysteria.KeepAlivePeriod,
-		DisablePathMTUDiscovery:        options.DisableMTUDiscovery || !(C.IsLinux || C.IsWindows),
-		EnableDatagrams:                true,
-	}
-	if options.ReceiveWindowConn == 0 {
-		quicConfig.InitialStreamReceiveWindow = hysteria.DefaultStreamReceiveWindow
-		quicConfig.MaxStreamReceiveWindow = hysteria.DefaultStreamReceiveWindow
-	}
-	if options.ReceiveWindowClient == 0 {
-		quicConfig.InitialConnectionReceiveWindow = hysteria.DefaultConnectionReceiveWindow
-		quicConfig.MaxConnectionReceiveWindow = hysteria.DefaultConnectionReceiveWindow
-	}
-	if quicConfig.MaxIncomingStreams == 0 {
-		quicConfig.MaxIncomingStreams = hysteria.DefaultMaxIncomingStreams
-	}
-	authKey := common.Map(options.Users, func(it option.HysteriaUser) string {
-		if len(it.Auth) > 0 {
-			return string(it.Auth)
-		} else {
-			return it.AuthString
-		}
-	})
-	authUser := common.Map(options.Users, func(it option.HysteriaUser) string {
-		return it.Name
-	})
-	var xplus []byte
-	if options.Obfs != "" {
-		xplus = []byte(options.Obfs)
-	}
-	var up, down uint64
-	if len(options.Up) > 0 {
-		up = hysteria.StringToBps(options.Up)
-		if up == 0 {
-			return nil, E.New("invalid up speed format: ", options.Up)
-		}
-	} else {
-		up = uint64(options.UpMbps) * hysteria.MbpsToBps
-	}
-	if len(options.Down) > 0 {
-		down = hysteria.StringToBps(options.Down)
-		if down == 0 {
-			return nil, E.New("invalid down speed format: ", options.Down)
-		}
-	} else {
-		down = uint64(options.DownMbps) * hysteria.MbpsToBps
-	}
-	if up < hysteria.MinSpeedBPS {
-		return nil, E.New("invalid up speed")
+	if options.TLS == nil || !options.TLS.Enabled {
+		return nil, C.ErrTLSRequired
 	}
-	if down < hysteria.MinSpeedBPS {
-		return nil, E.New("invalid down speed")
+	tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
+	if err != nil {
+		return nil, err
 	}
 	inbound := &Hysteria{
 		myInboundAdapter: myInboundAdapter{
@@ -113,224 +47,108 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL
 			tag:           tag,
 			listenOptions: options.ListenOptions,
 		},
-		quicConfig:  quicConfig,
-		authKey:     authKey,
-		authUser:    authUser,
-		xplusKey:    xplus,
-		sendBPS:     up,
-		recvBPS:     down,
-		udpSessions: make(map[uint32]chan *hysteria.UDPMessage),
-	}
-	if options.TLS == nil || !options.TLS.Enabled {
-		return nil, C.ErrTLSRequired
-	}
-	if len(options.TLS.ALPN) == 0 {
-		options.TLS.ALPN = []string{hysteria.DefaultALPN}
-	}
-	tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
-	if err != nil {
-		return nil, err
+		tlsConfig: tlsConfig,
 	}
-	inbound.tlsConfig = tlsConfig
-	return inbound, nil
-}
-
-func (h *Hysteria) Start() error {
-	packetConn, err := h.myInboundAdapter.ListenUDP()
-	if err != nil {
-		return err
-	}
-	if len(h.xplusKey) > 0 {
-		packetConn = hysteria.NewXPlusPacketConn(packetConn, h.xplusKey)
-		packetConn = &hysteria.PacketConnWrapper{PacketConn: packetConn}
-	}
-	err = h.tlsConfig.Start()
-	if err != nil {
-		return err
-	}
-	listener, err := qtls.Listen(packetConn, h.tlsConfig, h.quicConfig)
-	if err != nil {
-		return err
-	}
-	h.listener = listener
-	h.logger.Info("udp server started at ", listener.Addr())
-	go h.acceptLoop()
-	return nil
-}
-
-func (h *Hysteria) acceptLoop() {
-	for {
-		ctx := log.ContextWithNewID(h.ctx)
-		conn, err := h.listener.Accept(ctx)
+	var sendBps, receiveBps uint64
+	if len(options.Up) > 0 {
+		sendBps, err = humanize.ParseBytes(options.Up)
 		if err != nil {
-			return
+			return nil, E.Cause(err, "invalid up speed format: ", options.Up)
 		}
-		go func() {
-			hErr := h.accept(ctx, conn)
-			if hErr != nil {
-				conn.CloseWithError(0, "")
-				NewError(h.logger, ctx, E.Cause(hErr, "process connection from ", conn.RemoteAddr()))
-			}
-		}()
-	}
-}
-
-func (h *Hysteria) accept(ctx context.Context, conn quic.Connection) error {
-	controlStream, err := conn.AcceptStream(ctx)
-	if err != nil {
-		return err
-	}
-	clientHello, err := hysteria.ReadClientHello(controlStream)
-	if err != nil {
-		return err
+	} else {
+		sendBps = uint64(options.UpMbps) * hysteria.MbpsToBps
 	}
-	if len(h.authKey) > 0 {
-		userIndex := slices.Index(h.authKey, string(clientHello.Auth))
-		if userIndex == -1 {
-			err = hysteria.WriteServerHello(controlStream, hysteria.ServerHello{
-				Message: "wrong password",
-			})
-			return E.Errors(E.New("wrong password: ", string(clientHello.Auth)), err)
-		}
-		user := h.authUser[userIndex]
-		if user == "" {
-			user = F.ToString(userIndex)
-		} else {
-			ctx = auth.ContextWithUser(ctx, user)
+	if len(options.Down) > 0 {
+		receiveBps, err = humanize.ParseBytes(options.Down)
+		if receiveBps == 0 {
+			return nil, E.New("invalid down speed format: ", options.Down)
 		}
-		h.logger.InfoContext(ctx, "[", user, "] inbound connection from ", conn.RemoteAddr())
 	} else {
-		h.logger.InfoContext(ctx, "inbound connection from ", conn.RemoteAddr())
-	}
-	h.logger.DebugContext(ctx, "peer send speed: ", clientHello.SendBPS/1024/1024, " MBps, peer recv speed: ", clientHello.RecvBPS/1024/1024, " MBps")
-	if clientHello.SendBPS == 0 || clientHello.RecvBPS == 0 {
-		return E.New("invalid rate from client")
-	}
-	serverSendBPS, serverRecvBPS := clientHello.RecvBPS, clientHello.SendBPS
-	if h.sendBPS > 0 && serverSendBPS > h.sendBPS {
-		serverSendBPS = h.sendBPS
-	}
-	if h.recvBPS > 0 && serverRecvBPS > h.recvBPS {
-		serverRecvBPS = h.recvBPS
-	}
-	err = hysteria.WriteServerHello(controlStream, hysteria.ServerHello{
-		OK:      true,
-		SendBPS: serverSendBPS,
-		RecvBPS: serverRecvBPS,
+		receiveBps = uint64(options.DownMbps) * hysteria.MbpsToBps
+	}
+	service, err := hysteria.NewService[int](hysteria.ServiceOptions{
+		Context:       ctx,
+		Logger:        logger,
+		SendBPS:       sendBps,
+		ReceiveBPS:    receiveBps,
+		XPlusPassword: options.Obfs,
+		TLSConfig:     tlsConfig,
+		Handler:       adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, nil),
+
+		// Legacy options
+
+		ConnReceiveWindow:   options.ReceiveWindowConn,
+		StreamReceiveWindow: options.ReceiveWindowClient,
+		MaxIncomingStreams:  int64(options.MaxConnClient),
+		DisableMTUDiscovery: options.DisableMTUDiscovery,
 	})
 	if err != nil {
-		return err
+		return nil, err
 	}
-	conn.SetCongestionControl(hyCC.NewBrutalSender(serverSendBPS))
-	go h.udpRecvLoop(conn)
-	for {
-		var stream quic.Stream
-		stream, err = conn.AcceptStream(ctx)
-		if err != nil {
-			return err
+	userList := make([]int, 0, len(options.Users))
+	userNameList := make([]string, 0, len(options.Users))
+	userPasswordList := make([]string, 0, len(options.Users))
+	for index, user := range options.Users {
+		userList = append(userList, index)
+		userNameList = append(userNameList, user.Name)
+		var password string
+		if user.AuthString != "" {
+			password = user.AuthString
+		} else {
+			password = string(user.Auth)
 		}
-		go func() {
-			hErr := h.acceptStream(ctx, conn /*&hysteria.StreamWrapper{Stream: stream}*/, stream)
-			if hErr != nil {
-				stream.Close()
-				NewError(h.logger, ctx, E.Cause(hErr, "process stream from ", conn.RemoteAddr()))
-			}
-		}()
+		userPasswordList = append(userPasswordList, password)
 	}
+	service.UpdateUsers(userList, userPasswordList)
+	inbound.service = service
+	inbound.userNameList = userNameList
+	return inbound, nil
 }
 
-func (h *Hysteria) udpRecvLoop(conn quic.Connection) {
-	for {
-		packet, err := conn.ReceiveMessage(h.ctx)
-		if err != nil {
-			return
-		}
-		message, err := hysteria.ParseUDPMessage(packet)
-		if err != nil {
-			h.logger.Error("parse udp message: ", err)
-			continue
-		}
-		dfMsg := h.udpDefragger.Feed(message)
-		if dfMsg == nil {
-			continue
-		}
-		h.udpAccess.RLock()
-		ch, ok := h.udpSessions[dfMsg.SessionID]
-		if ok {
-			select {
-			case ch <- dfMsg:
-				// OK
-			default:
-				// Silently drop the message when the channel is full
-			}
-		}
-		h.udpAccess.RUnlock()
+func (h *Hysteria) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	ctx = log.ContextWithNewID(ctx)
+	metadata = h.createMetadata(conn, metadata)
+	userID, _ := auth.UserFromContext[int](ctx)
+	if userName := h.userNameList[userID]; userName != "" {
+		metadata.User = userName
+		h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination)
+	} else {
+		h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
 	}
+	return h.router.RouteConnection(ctx, conn, metadata)
 }
 
-func (h *Hysteria) acceptStream(ctx context.Context, conn quic.Connection, stream quic.Stream) error {
-	request, err := hysteria.ReadClientRequest(stream)
-	if err != nil {
-		return err
-	}
-	var metadata adapter.InboundContext
-	metadata.Inbound = h.tag
-	metadata.InboundType = C.TypeHysteria
-	metadata.InboundOptions = h.listenOptions.InboundOptions
-	metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap()
-	metadata.OriginDestination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap()
-	metadata.Destination = M.ParseSocksaddrHostPort(request.Host, request.Port).Unwrap()
-	metadata.User, _ = auth.UserFromContext[string](ctx)
-
-	if !request.UDP {
-		err = hysteria.WriteServerResponse(stream, hysteria.ServerResponse{
-			OK: true,
-		})
-		if err != nil {
-			return err
-		}
-		h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
-		return h.router.RouteConnection(ctx, hysteria.NewConn(stream, metadata.Destination, false), metadata)
+func (h *Hysteria) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	ctx = log.ContextWithNewID(ctx)
+	metadata = h.createPacketMetadata(conn, metadata)
+	userID, _ := auth.UserFromContext[int](ctx)
+	if userName := h.userNameList[userID]; userName != "" {
+		metadata.User = userName
+		h.logger.InfoContext(ctx, "[", userName, "] inbound packet connection to ", metadata.Destination)
 	} else {
 		h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
-		var id uint32
-		h.udpAccess.Lock()
-		id = h.udpSessionId
-		nCh := make(chan *hysteria.UDPMessage, 1024)
-		h.udpSessions[id] = nCh
-		h.udpSessionId += 1
-		h.udpAccess.Unlock()
-		err = hysteria.WriteServerResponse(stream, hysteria.ServerResponse{
-			OK:           true,
-			UDPSessionID: id,
-		})
+	}
+	return h.router.RoutePacketConnection(ctx, conn, metadata)
+}
+
+func (h *Hysteria) Start() error {
+	if h.tlsConfig != nil {
+		err := h.tlsConfig.Start()
 		if err != nil {
 			return err
 		}
-		packetConn := hysteria.NewPacketConn(conn, stream, id, metadata.Destination, nCh, common.Closer(func() error {
-			h.udpAccess.Lock()
-			if ch, ok := h.udpSessions[id]; ok {
-				close(ch)
-				delete(h.udpSessions, id)
-			}
-			h.udpAccess.Unlock()
-			return nil
-		}))
-		go packetConn.Hold()
-		return h.router.RoutePacketConnection(ctx, packetConn, metadata)
 	}
+	packetConn, err := h.myInboundAdapter.ListenUDP()
+	if err != nil {
+		return err
+	}
+	return h.service.Start(packetConn)
 }
 
 func (h *Hysteria) Close() error {
-	h.udpAccess.Lock()
-	for _, session := range h.udpSessions {
-		close(session)
-	}
-	h.udpSessions = make(map[uint32]chan *hysteria.UDPMessage)
-	h.udpAccess.Unlock()
 	return common.Close(
 		&h.myInboundAdapter,
-		h.listener,
 		h.tlsConfig,
+		common.PtrOrNil(h.service),
 	)
 }

+ 2 - 1
inbound/hysteria2.go

@@ -14,7 +14,7 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing-box/transport/hysteria"
+	"github.com/sagernet/sing-quic/hysteria"
 	"github.com/sagernet/sing-quic/hysteria2"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/auth"
@@ -32,6 +32,7 @@ type Hysteria2 struct {
 }
 
 func NewHysteria2(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (*Hysteria2, error) {
+	options.UDPFragmentDefault = true
 	if options.TLS == nil || !options.TLS.Enabled {
 		return nil, C.ErrTLSRequired
 	}

+ 52 - 272
outbound/hysteria.go

@@ -5,18 +5,16 @@ package outbound
 import (
 	"context"
 	"net"
-	"sync"
+	"os"
 
-	"github.com/sagernet/quic-go"
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/common/dialer"
+	"github.com/sagernet/sing-box/common/humanize"
 	"github.com/sagernet/sing-box/common/tls"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing-box/transport/hysteria"
-	"github.com/sagernet/sing-quic"
-	hyCC "github.com/sagernet/sing-quic/hysteria2/congestion"
+	"github.com/sagernet/sing-quic/hysteria"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/bufio"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -25,27 +23,13 @@ import (
 )
 
 var (
-	_ adapter.Outbound                = (*Hysteria)(nil)
-	_ adapter.InterfaceUpdateListener = (*Hysteria)(nil)
+	_ adapter.Outbound                = (*TUIC)(nil)
+	_ adapter.InterfaceUpdateListener = (*TUIC)(nil)
 )
 
 type Hysteria struct {
 	myOutboundAdapter
-	ctx          context.Context
-	dialer       N.Dialer
-	serverAddr   M.Socksaddr
-	tlsConfig    tls.Config
-	quicConfig   *quic.Config
-	authKey      []byte
-	xplusKey     []byte
-	sendBPS      uint64
-	recvBPS      uint64
-	connAccess   sync.Mutex
-	conn         quic.Connection
-	rawConn      net.Conn
-	udpAccess    sync.RWMutex
-	udpSessions  map[uint32]chan *hysteria.UDPMessage
-	udpDefragger hysteria.Defragger
+	client *hysteria.Client
 }
 
 func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (*Hysteria, error) {
@@ -57,252 +41,77 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL
 	if err != nil {
 		return nil, err
 	}
-	if len(tlsConfig.NextProtos()) == 0 {
-		tlsConfig.SetNextProtos([]string{hysteria.DefaultALPN})
-	}
-	quicConfig := &quic.Config{
-		InitialStreamReceiveWindow:     options.ReceiveWindowConn,
-		MaxStreamReceiveWindow:         options.ReceiveWindowConn,
-		InitialConnectionReceiveWindow: options.ReceiveWindow,
-		MaxConnectionReceiveWindow:     options.ReceiveWindow,
-		KeepAlivePeriod:                hysteria.KeepAlivePeriod,
-		DisablePathMTUDiscovery:        options.DisableMTUDiscovery,
-		EnableDatagrams:                true,
-	}
-	if options.ReceiveWindowConn == 0 {
-		quicConfig.InitialStreamReceiveWindow = hysteria.DefaultStreamReceiveWindow
-		quicConfig.MaxStreamReceiveWindow = hysteria.DefaultStreamReceiveWindow
-	}
-	if options.ReceiveWindow == 0 {
-		quicConfig.InitialConnectionReceiveWindow = hysteria.DefaultConnectionReceiveWindow
-		quicConfig.MaxConnectionReceiveWindow = hysteria.DefaultConnectionReceiveWindow
-	}
-	if quicConfig.MaxIncomingStreams == 0 {
-		quicConfig.MaxIncomingStreams = hysteria.DefaultMaxIncomingStreams
+	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	if err != nil {
+		return nil, err
 	}
-	var auth []byte
-	if len(options.Auth) > 0 {
-		auth = options.Auth
+	networkList := options.Network.Build()
+	var password string
+	if options.AuthString != "" {
+		password = options.AuthString
 	} else {
-		auth = []byte(options.AuthString)
-	}
-	var xplus []byte
-	if options.Obfs != "" {
-		xplus = []byte(options.Obfs)
+		password = string(options.Auth)
 	}
-	var up, down uint64
+	var sendBps, receiveBps uint64
 	if len(options.Up) > 0 {
-		up = hysteria.StringToBps(options.Up)
-		if up == 0 {
-			return nil, E.New("invalid up speed format: ", options.Up)
+		sendBps, err = humanize.ParseBytes(options.Up)
+		if err != nil {
+			return nil, E.Cause(err, "invalid up speed format: ", options.Up)
 		}
 	} else {
-		up = uint64(options.UpMbps) * hysteria.MbpsToBps
+		sendBps = uint64(options.UpMbps) * hysteria.MbpsToBps
 	}
 	if len(options.Down) > 0 {
-		down = hysteria.StringToBps(options.Down)
-		if down == 0 {
+		receiveBps, err = humanize.ParseBytes(options.Down)
+		if receiveBps == 0 {
 			return nil, E.New("invalid down speed format: ", options.Down)
 		}
 	} else {
-		down = uint64(options.DownMbps) * hysteria.MbpsToBps
-	}
-	if up < hysteria.MinSpeedBPS {
-		return nil, E.New("invalid up speed")
-	}
-	if down < hysteria.MinSpeedBPS {
-		return nil, E.New("invalid down speed")
-	}
-	outboundDialer, err := dialer.New(router, options.DialerOptions)
+		receiveBps = uint64(options.DownMbps) * hysteria.MbpsToBps
+	}
+	client, err := hysteria.NewClient(hysteria.ClientOptions{
+		Context:       ctx,
+		Dialer:        outboundDialer,
+		Logger:        logger,
+		ServerAddress: options.ServerOptions.Build(),
+		SendBPS:       sendBps,
+		ReceiveBPS:    receiveBps,
+		XPlusPassword: options.Obfs,
+		Password:      password,
+		TLSConfig:     tlsConfig,
+		UDPDisabled:   !common.Contains(networkList, N.NetworkUDP),
+
+		ConnReceiveWindow:   options.ReceiveWindowConn,
+		StreamReceiveWindow: options.ReceiveWindow,
+		DisableMTUDiscovery: options.DisableMTUDiscovery,
+	})
 	if err != nil {
 		return nil, err
 	}
 	return &Hysteria{
 		myOutboundAdapter: myOutboundAdapter{
 			protocol:     C.TypeHysteria,
-			network:      options.Network.Build(),
+			network:      networkList,
 			router:       router,
 			logger:       logger,
 			tag:          tag,
 			dependencies: withDialerDependency(options.DialerOptions),
 		},
-		ctx:        ctx,
-		dialer:     outboundDialer,
-		serverAddr: options.ServerOptions.Build(),
-		tlsConfig:  tlsConfig,
-		quicConfig: quicConfig,
-		authKey:    auth,
-		xplusKey:   xplus,
-		sendBPS:    up,
-		recvBPS:    down,
+		client: client,
 	}, nil
 }
 
-func (h *Hysteria) offer(ctx context.Context) (quic.Connection, error) {
-	conn := h.conn
-	if conn != nil && !common.Done(conn.Context()) {
-		return conn, nil
-	}
-	h.connAccess.Lock()
-	defer h.connAccess.Unlock()
-	h.udpAccess.Lock()
-	defer h.udpAccess.Unlock()
-	conn = h.conn
-	if conn != nil && !common.Done(conn.Context()) {
-		return conn, nil
-	}
-	common.Close(h.rawConn)
-	conn, err := h.offerNew(ctx)
-	if err != nil {
-		return nil, err
-	}
-	if common.Contains(h.network, N.NetworkUDP) {
-		for _, session := range h.udpSessions {
-			close(session)
-		}
-		h.udpSessions = make(map[uint32]chan *hysteria.UDPMessage)
-		h.udpDefragger = hysteria.Defragger{}
-		go h.udpRecvLoop(conn)
-	}
-	return conn, nil
-}
-
-func (h *Hysteria) offerNew(ctx context.Context) (quic.Connection, error) {
-	udpConn, err := h.dialer.DialContext(h.ctx, "udp", h.serverAddr)
-	if err != nil {
-		return nil, err
-	}
-	var packetConn net.PacketConn
-	packetConn = bufio.NewUnbindPacketConn(udpConn)
-	if h.xplusKey != nil {
-		packetConn = hysteria.NewXPlusPacketConn(packetConn, h.xplusKey)
-	}
-	packetConn = &hysteria.PacketConnWrapper{PacketConn: packetConn}
-	quicConn, err := qtls.Dial(h.ctx, packetConn, udpConn.RemoteAddr(), h.tlsConfig, h.quicConfig)
-	if err != nil {
-		packetConn.Close()
-		return nil, err
-	}
-	controlStream, err := quicConn.OpenStreamSync(ctx)
-	if err != nil {
-		packetConn.Close()
-		return nil, err
-	}
-	err = hysteria.WriteClientHello(controlStream, hysteria.ClientHello{
-		SendBPS: h.sendBPS,
-		RecvBPS: h.recvBPS,
-		Auth:    h.authKey,
-	})
-	if err != nil {
-		packetConn.Close()
-		return nil, err
-	}
-	serverHello, err := hysteria.ReadServerHello(controlStream)
-	if err != nil {
-		packetConn.Close()
-		return nil, err
-	}
-	if !serverHello.OK {
-		packetConn.Close()
-		return nil, E.New("remote error: ", serverHello.Message)
-	}
-	quicConn.SetCongestionControl(hyCC.NewBrutalSender(serverHello.RecvBPS))
-	h.conn = quicConn
-	h.rawConn = udpConn
-	return quicConn, nil
-}
-
-func (h *Hysteria) udpRecvLoop(conn quic.Connection) {
-	for {
-		packet, err := conn.ReceiveMessage(h.ctx)
-		if err != nil {
-			return
-		}
-		message, err := hysteria.ParseUDPMessage(packet)
-		if err != nil {
-			h.logger.Error("parse udp message: ", err)
-			continue
-		}
-		dfMsg := h.udpDefragger.Feed(message)
-		if dfMsg == nil {
-			continue
-		}
-		h.udpAccess.RLock()
-		ch, ok := h.udpSessions[dfMsg.SessionID]
-		if ok {
-			select {
-			case ch <- dfMsg:
-				// OK
-			default:
-				// Silently drop the message when the channel is full
-			}
-		}
-		h.udpAccess.RUnlock()
-	}
-}
-
-func (h *Hysteria) InterfaceUpdated() {
-	h.Close()
-	return
-}
-
-func (h *Hysteria) Close() error {
-	h.connAccess.Lock()
-	defer h.connAccess.Unlock()
-	h.udpAccess.Lock()
-	defer h.udpAccess.Unlock()
-	if h.conn != nil {
-		h.conn.CloseWithError(0, "")
-		h.rawConn.Close()
-	}
-	for _, session := range h.udpSessions {
-		close(session)
-	}
-	h.udpSessions = make(map[uint32]chan *hysteria.UDPMessage)
-	return nil
-}
-
-func (h *Hysteria) open(ctx context.Context, reconnect bool) (quic.Connection, quic.Stream, error) {
-	conn, err := h.offer(ctx)
-	if err != nil {
-		if nErr, ok := err.(net.Error); ok && !nErr.Temporary() && reconnect {
-			return h.open(ctx, false)
-		}
-		return nil, nil, err
-	}
-	stream, err := conn.OpenStream()
-	if err != nil {
-		if nErr, ok := err.(net.Error); ok && !nErr.Temporary() && reconnect {
-			return h.open(ctx, false)
-		}
-		return nil, nil, err
-	}
-	return conn, &hysteria.StreamWrapper{Stream: stream}, nil
-}
-
 func (h *Hysteria) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	switch N.NetworkName(network) {
 	case N.NetworkTCP:
 		h.logger.InfoContext(ctx, "outbound connection to ", destination)
-		_, stream, err := h.open(ctx, true)
-		if err != nil {
-			return nil, err
-		}
-		err = hysteria.WriteClientRequest(stream, hysteria.ClientRequest{
-			Host: destination.AddrString(),
-			Port: destination.Port,
-		})
-		if err != nil {
-			stream.Close()
-			return nil, err
-		}
-		return hysteria.NewConn(stream, destination, true), nil
+		return h.client.DialConn(ctx, destination)
 	case N.NetworkUDP:
 		conn, err := h.ListenPacket(ctx, destination)
 		if err != nil {
 			return nil, err
 		}
-		return conn.(*hysteria.PacketConn), nil
+		return bufio.NewBindPacketConn(conn, destination), nil
 	default:
 		return nil, E.New("unsupported network: ", network)
 	}
@@ -310,44 +119,7 @@ func (h *Hysteria) DialContext(ctx context.Context, network string, destination
 
 func (h *Hysteria) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
-	conn, stream, err := h.open(ctx, true)
-	if err != nil {
-		return nil, err
-	}
-	err = hysteria.WriteClientRequest(stream, hysteria.ClientRequest{
-		UDP:  true,
-		Host: destination.AddrString(),
-		Port: destination.Port,
-	})
-	if err != nil {
-		stream.Close()
-		return nil, err
-	}
-	var response *hysteria.ServerResponse
-	response, err = hysteria.ReadServerResponse(stream)
-	if err != nil {
-		stream.Close()
-		return nil, err
-	}
-	if !response.OK {
-		stream.Close()
-		return nil, E.New("remote error: ", response.Message)
-	}
-	h.udpAccess.Lock()
-	nCh := make(chan *hysteria.UDPMessage, 1024)
-	h.udpSessions[response.UDPSessionID] = nCh
-	h.udpAccess.Unlock()
-	packetConn := hysteria.NewPacketConn(conn, stream, response.UDPSessionID, destination, nCh, common.Closer(func() error {
-		h.udpAccess.Lock()
-		if ch, ok := h.udpSessions[response.UDPSessionID]; ok {
-			close(ch)
-			delete(h.udpSessions, response.UDPSessionID)
-		}
-		h.udpAccess.Unlock()
-		return nil
-	}))
-	go packetConn.Hold()
-	return packetConn, nil
+	return h.client.ListenPacket(ctx, destination)
 }
 
 func (h *Hysteria) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
@@ -357,3 +129,11 @@ func (h *Hysteria) NewConnection(ctx context.Context, conn net.Conn, metadata ad
 func (h *Hysteria) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
 	return NewPacketConnection(ctx, h, conn, metadata)
 }
+
+func (h *Hysteria) InterfaceUpdated() error {
+	return h.client.CloseWithError(E.New("network changed"))
+}
+
+func (h *Hysteria) Close() error {
+	return h.client.CloseWithError(os.ErrClosed)
+}

+ 1 - 1
outbound/hysteria2.go

@@ -13,7 +13,7 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing-box/transport/hysteria"
+	"github.com/sagernet/sing-quic/hysteria"
 	"github.com/sagernet/sing-quic/hysteria2"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/bufio"

+ 5 - 5
test/go.mod

@@ -10,10 +10,10 @@ require (
 	github.com/docker/docker v24.0.6+incompatible
 	github.com/docker/go-connections v0.4.0
 	github.com/gofrs/uuid/v5 v5.0.0
-	github.com/sagernet/quic-go v0.0.0-20230919101909-0cc6c5dcecee
-	github.com/sagernet/sing v0.2.15
+	github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460
+	github.com/sagernet/sing v0.2.16-0.20231021090846-8002db54c028
 	github.com/sagernet/sing-dns v0.1.10
-	github.com/sagernet/sing-quic v0.1.2
+	github.com/sagernet/sing-quic v0.1.3-0.20231026034240-fa3d997246b6
 	github.com/sagernet/sing-shadowsocks v0.2.5
 	github.com/sagernet/sing-shadowsocks2 v0.1.4
 	github.com/spyzhov/ajson v0.9.0
@@ -70,12 +70,12 @@ require (
 	github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect
 	github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a // indirect
 	github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect
-	github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2 // indirect
+	github.com/sagernet/gvisor v0.0.0-20230930141345-5fef6f2e17ab // indirect
 	github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect
 	github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 // indirect
 	github.com/sagernet/sing-mux v0.1.3 // indirect
 	github.com/sagernet/sing-shadowtls v0.1.4 // indirect
-	github.com/sagernet/sing-tun v0.1.16 // indirect
+	github.com/sagernet/sing-tun v0.1.17-0.20231026060825-efd9884154a6 // indirect
 	github.com/sagernet/sing-vmess v0.1.8 // indirect
 	github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 // indirect
 	github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 // indirect

+ 10 - 10
test/go.sum

@@ -117,32 +117,32 @@ github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a h1:wZHruBx
 github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a/go.mod h1:dNV1ZP9y3qx5ltULeKaQZTZWTLHflgW5DES+Ses7cMI=
 github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA=
 github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms=
-github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2 h1:dnkKrzapqtAwjTSWt6hdPrARORfoYvuUczynvRLrueo=
-github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2/go.mod h1:1JUiV7nGuf++YFm9eWZ8q2lrwHmhcUGzptMl/vL1+LA=
+github.com/sagernet/gvisor v0.0.0-20230930141345-5fef6f2e17ab h1:u+xQoi/Yc6bNUvTfrDD6HhGRybn2lzrhf5vmS+wb4Ho=
+github.com/sagernet/gvisor v0.0.0-20230930141345-5fef6f2e17ab/go.mod h1:3akUhSHSVtLuJaYcW5JPepUraBOW06Ibz2HKwaK5rOk=
 github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
 github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
-github.com/sagernet/quic-go v0.0.0-20230919101909-0cc6c5dcecee h1:ykuhl9jCS638N+jw1vC9AvT9bbQn6xRNScP2FWPV9dM=
-github.com/sagernet/quic-go v0.0.0-20230919101909-0cc6c5dcecee/go.mod h1:0CfhWwZAeXGYM9+Nkkw1zcQtFHQC8KWjbpeDv7pu8iw=
+github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460 h1:dAe4OIJAtE0nHOzTHhAReQteh3+sa63rvXbuIpbeOTY=
+github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460/go.mod h1:uJGpmJCOcMQqMlHKc3P1Vz6uygmpz4bPeVIoOhdVQnM=
 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
 github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
 github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
-github.com/sagernet/sing v0.2.15 h1:PFwyiMzkyJkq+YGOVznJUsRVOT6EoVxRGIsllLuvHXA=
-github.com/sagernet/sing v0.2.15/go.mod h1:AhNEHu0GXrpqkuzvTwvC8+j2cQUU/dh+zLEmq4C99pg=
+github.com/sagernet/sing v0.2.16-0.20231021090846-8002db54c028 h1:6GbQt7SC9y5Imrq5jDMbXDSaNiMhJ8KBjhjtQRuqQvE=
+github.com/sagernet/sing v0.2.16-0.20231021090846-8002db54c028/go.mod h1:AhNEHu0GXrpqkuzvTwvC8+j2cQUU/dh+zLEmq4C99pg=
 github.com/sagernet/sing-dns v0.1.10 h1:iIU7nRBlUYj+fF2TaktGIvRiTFFrHwSMedLQsvlTZCI=
 github.com/sagernet/sing-dns v0.1.10/go.mod h1:vtUimtf7Nq9EdvD5WTpfCr69KL1M7bcgOVKiYBiAY/c=
 github.com/sagernet/sing-mux v0.1.3 h1:fAf7PZa2A55mCeh0KKM02f1k2Y4vEmxuZZ/51ahkkLA=
 github.com/sagernet/sing-mux v0.1.3/go.mod h1:wGeIeiiFLx4HUM5LAg65wrNZ/X1muOimqK0PEhNbPi0=
-github.com/sagernet/sing-quic v0.1.2 h1:+u9CRf0KHi5HgXmJ3eB0CtqpWXtF0lx2QlWq+ZFZ+XY=
-github.com/sagernet/sing-quic v0.1.2/go.mod h1:H1TX0/y9UUM43wyaLQ+qjg2+o901ibYtwWX2rWG+a3o=
+github.com/sagernet/sing-quic v0.1.3-0.20231026034240-fa3d997246b6 h1:w+TUbIZKZFSdf/AUa/y33kY9xaLeNGz/tBNcNhqpqfg=
+github.com/sagernet/sing-quic v0.1.3-0.20231026034240-fa3d997246b6/go.mod h1:1M7xP4802K9Kz6BQ7LlA7UeCapWvWlH1Htmk2bAqkWc=
 github.com/sagernet/sing-shadowsocks v0.2.5 h1:qxIttos4xu6ii7MTVJYA8EFQR7Q3KG6xMqmLJIFtBaY=
 github.com/sagernet/sing-shadowsocks v0.2.5/go.mod h1:MGWGkcU2xW2G2mfArT9/QqpVLOGU+dBaahZCtPHdt7A=
 github.com/sagernet/sing-shadowsocks2 v0.1.4 h1:vht2M8t3m5DTgXR2j24KbYOygG5aOp+MUhpQnAux728=
 github.com/sagernet/sing-shadowsocks2 v0.1.4/go.mod h1:Mgdee99NxxNd5Zld3ixIs18yVs4x2dI2VTDDE1N14Wc=
 github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
 github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4=
-github.com/sagernet/sing-tun v0.1.16 h1:RHXYIVg6uacvdfbYMiPEz9VX5uu6mNrvP7u9yAH3oNc=
-github.com/sagernet/sing-tun v0.1.16/go.mod h1:S3q8GCjeyRniK+KLmo4XqKY0bS3x2UdKkKbqxT/Agl8=
+github.com/sagernet/sing-tun v0.1.17-0.20231026060825-efd9884154a6 h1:4yEXBqQoUgXj7qPSLD6lr+z9/KfsvixO9JUA2i5xnM8=
+github.com/sagernet/sing-tun v0.1.17-0.20231026060825-efd9884154a6/go.mod h1:w2+S+uWE94E/pQWSDdDdMIjwAEb645kuGPunr6ZllUg=
 github.com/sagernet/sing-vmess v0.1.8 h1:XVWad1RpTy9b5tPxdm5MCU8cGfrTGdR8qCq6HV2aCNc=
 github.com/sagernet/sing-vmess v0.1.8/go.mod h1:vhx32UNzTDUkNwOyIjcZQohre1CaytquC5mPplId8uA=
 github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 h1:HuE6xSwco/Xed8ajZ+coeYLmioq0Qp1/Z2zczFaV8as=

+ 2 - 2
test/hysteria_test.go

@@ -79,7 +79,7 @@ func TestHysteriaSelf(t *testing.T) {
 			},
 		},
 	})
-	testSuitSimple1(t, clientPort, testPort)
+	testSuit(t, clientPort, testPort)
 }
 
 func TestHysteriaInbound(t *testing.T) {
@@ -118,7 +118,7 @@ func TestHysteriaInbound(t *testing.T) {
 			caPem:                  "/etc/hysteria/ca.pem",
 		},
 	})
-	testSuitSimple1(t, clientPort, testPort)
+	testSuit(t, clientPort, testPort)
 }
 
 func TestHysteriaOutbound(t *testing.T) {

+ 0 - 65
transport/hysteria/frag.go

@@ -1,65 +0,0 @@
-package hysteria
-
-func FragUDPMessage(m UDPMessage, maxSize int) []UDPMessage {
-	if m.Size() <= maxSize {
-		return []UDPMessage{m}
-	}
-	fullPayload := m.Data
-	maxPayloadSize := maxSize - m.HeaderSize()
-	off := 0
-	fragID := uint8(0)
-	fragCount := uint8((len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize) // round up
-	var frags []UDPMessage
-	for off < len(fullPayload) {
-		payloadSize := len(fullPayload) - off
-		if payloadSize > maxPayloadSize {
-			payloadSize = maxPayloadSize
-		}
-		frag := m
-		frag.FragID = fragID
-		frag.FragCount = fragCount
-		frag.Data = fullPayload[off : off+payloadSize]
-		frags = append(frags, frag)
-		off += payloadSize
-		fragID++
-	}
-	return frags
-}
-
-type Defragger struct {
-	msgID uint16
-	frags []*UDPMessage
-	count uint8
-}
-
-func (d *Defragger) Feed(m UDPMessage) *UDPMessage {
-	if m.FragCount <= 1 {
-		return &m
-	}
-	if m.FragID >= m.FragCount {
-		// wtf is this?
-		return nil
-	}
-	if m.MsgID != d.msgID {
-		// new message, clear previous state
-		d.msgID = m.MsgID
-		d.frags = make([]*UDPMessage, m.FragCount)
-		d.count = 1
-		d.frags[m.FragID] = &m
-	} else if d.frags[m.FragID] == nil {
-		d.frags[m.FragID] = &m
-		d.count++
-		if int(d.count) == len(d.frags) {
-			// all fragments received, assemble
-			var data []byte
-			for _, frag := range d.frags {
-				data = append(data, frag.Data...)
-			}
-			m.Data = data
-			m.FragID = 0
-			m.FragCount = 1
-			return &m
-		}
-	}
-	return nil
-}

+ 0 - 539
transport/hysteria/protocol.go

@@ -1,539 +0,0 @@
-package hysteria
-
-import (
-	"bytes"
-	"encoding/binary"
-	"io"
-	"math/rand"
-	"net"
-	"os"
-	"time"
-
-	"github.com/sagernet/quic-go"
-	"github.com/sagernet/sing/common"
-	"github.com/sagernet/sing/common/buf"
-	E "github.com/sagernet/sing/common/exceptions"
-	M "github.com/sagernet/sing/common/metadata"
-)
-
-const (
-	MbpsToBps                      = 125000
-	MinSpeedBPS                    = 16384
-	DefaultStreamReceiveWindow     = 15728640 // 15 MB/s
-	DefaultConnectionReceiveWindow = 67108864 // 64 MB/s
-	DefaultMaxIncomingStreams      = 1024
-	DefaultALPN                    = "hysteria"
-	KeepAlivePeriod                = 10 * time.Second
-)
-
-const Version = 3
-
-type ClientHello struct {
-	SendBPS uint64
-	RecvBPS uint64
-	Auth    []byte
-}
-
-func WriteClientHello(stream io.Writer, hello ClientHello) error {
-	var requestLen int
-	requestLen += 1 // version
-	requestLen += 8 // sendBPS
-	requestLen += 8 // recvBPS
-	requestLen += 2 // auth len
-	requestLen += len(hello.Auth)
-	request := buf.NewSize(requestLen)
-	defer request.Release()
-	common.Must(
-		request.WriteByte(Version),
-		binary.Write(request, binary.BigEndian, hello.SendBPS),
-		binary.Write(request, binary.BigEndian, hello.RecvBPS),
-		binary.Write(request, binary.BigEndian, uint16(len(hello.Auth))),
-		common.Error(request.Write(hello.Auth)),
-	)
-	return common.Error(stream.Write(request.Bytes()))
-}
-
-func ReadClientHello(reader io.Reader) (*ClientHello, error) {
-	var version uint8
-	err := binary.Read(reader, binary.BigEndian, &version)
-	if err != nil {
-		return nil, err
-	}
-	if version != Version {
-		return nil, E.New("unsupported client version: ", version)
-	}
-	var clientHello ClientHello
-	err = binary.Read(reader, binary.BigEndian, &clientHello.SendBPS)
-	if err != nil {
-		return nil, err
-	}
-	err = binary.Read(reader, binary.BigEndian, &clientHello.RecvBPS)
-	if err != nil {
-		return nil, err
-	}
-	var authLen uint16
-	err = binary.Read(reader, binary.BigEndian, &authLen)
-	if err != nil {
-		return nil, err
-	}
-	clientHello.Auth = make([]byte, authLen)
-	_, err = io.ReadFull(reader, clientHello.Auth)
-	if err != nil {
-		return nil, err
-	}
-	return &clientHello, nil
-}
-
-type ServerHello struct {
-	OK      bool
-	SendBPS uint64
-	RecvBPS uint64
-	Message string
-}
-
-func ReadServerHello(stream io.Reader) (*ServerHello, error) {
-	var responseLen int
-	responseLen += 1 // ok
-	responseLen += 8 // sendBPS
-	responseLen += 8 // recvBPS
-	responseLen += 2 // message len
-	response := buf.NewSize(responseLen)
-	defer response.Release()
-	_, err := response.ReadFullFrom(stream, responseLen)
-	if err != nil {
-		return nil, err
-	}
-	var serverHello ServerHello
-	serverHello.OK = response.Byte(0) == 1
-	serverHello.SendBPS = binary.BigEndian.Uint64(response.Range(1, 9))
-	serverHello.RecvBPS = binary.BigEndian.Uint64(response.Range(9, 17))
-	messageLen := binary.BigEndian.Uint16(response.Range(17, 19))
-	if messageLen == 0 {
-		return &serverHello, nil
-	}
-	message := make([]byte, messageLen)
-	_, err = io.ReadFull(stream, message)
-	if err != nil {
-		return nil, err
-	}
-	serverHello.Message = string(message)
-	return &serverHello, nil
-}
-
-func WriteServerHello(stream io.Writer, hello ServerHello) error {
-	var responseLen int
-	responseLen += 1 // ok
-	responseLen += 8 // sendBPS
-	responseLen += 8 // recvBPS
-	responseLen += 2 // message len
-	responseLen += len(hello.Message)
-	response := buf.NewSize(responseLen)
-	defer response.Release()
-	if hello.OK {
-		common.Must(response.WriteByte(1))
-	} else {
-		common.Must(response.WriteByte(0))
-	}
-	common.Must(
-		binary.Write(response, binary.BigEndian, hello.SendBPS),
-		binary.Write(response, binary.BigEndian, hello.RecvBPS),
-		binary.Write(response, binary.BigEndian, uint16(len(hello.Message))),
-		common.Error(response.WriteString(hello.Message)),
-	)
-	return common.Error(stream.Write(response.Bytes()))
-}
-
-type ClientRequest struct {
-	UDP  bool
-	Host string
-	Port uint16
-}
-
-func ReadClientRequest(stream io.Reader) (*ClientRequest, error) {
-	var clientRequest ClientRequest
-	err := binary.Read(stream, binary.BigEndian, &clientRequest.UDP)
-	if err != nil {
-		return nil, err
-	}
-	var hostLen uint16
-	err = binary.Read(stream, binary.BigEndian, &hostLen)
-	if err != nil {
-		return nil, err
-	}
-	host := make([]byte, hostLen)
-	_, err = io.ReadFull(stream, host)
-	if err != nil {
-		return nil, err
-	}
-	clientRequest.Host = string(host)
-	err = binary.Read(stream, binary.BigEndian, &clientRequest.Port)
-	if err != nil {
-		return nil, err
-	}
-	return &clientRequest, nil
-}
-
-func WriteClientRequest(stream io.Writer, request ClientRequest) error {
-	var requestLen int
-	requestLen += 1 // udp
-	requestLen += 2 // host len
-	requestLen += len(request.Host)
-	requestLen += 2 // port
-	buffer := buf.NewSize(requestLen)
-	defer buffer.Release()
-	if request.UDP {
-		common.Must(buffer.WriteByte(1))
-	} else {
-		common.Must(buffer.WriteByte(0))
-	}
-	common.Must(
-		binary.Write(buffer, binary.BigEndian, uint16(len(request.Host))),
-		common.Error(buffer.WriteString(request.Host)),
-		binary.Write(buffer, binary.BigEndian, request.Port),
-	)
-	return common.Error(stream.Write(buffer.Bytes()))
-}
-
-type ServerResponse struct {
-	OK           bool
-	UDPSessionID uint32
-	Message      string
-}
-
-func ReadServerResponse(stream io.Reader) (*ServerResponse, error) {
-	var responseLen int
-	responseLen += 1 // ok
-	responseLen += 4 // udp session id
-	responseLen += 2 // message len
-	response := buf.NewSize(responseLen)
-	defer response.Release()
-	_, err := response.ReadFullFrom(stream, responseLen)
-	if err != nil {
-		return nil, err
-	}
-	var serverResponse ServerResponse
-	serverResponse.OK = response.Byte(0) == 1
-	serverResponse.UDPSessionID = binary.BigEndian.Uint32(response.Range(1, 5))
-	messageLen := binary.BigEndian.Uint16(response.Range(5, 7))
-	if messageLen == 0 {
-		return &serverResponse, nil
-	}
-	message := make([]byte, messageLen)
-	_, err = io.ReadFull(stream, message)
-	if err != nil {
-		return nil, err
-	}
-	serverResponse.Message = string(message)
-	return &serverResponse, nil
-}
-
-func WriteServerResponse(stream io.Writer, response ServerResponse) error {
-	var responseLen int
-	responseLen += 1 // ok
-	responseLen += 4 // udp session id
-	responseLen += 2 // message len
-	responseLen += len(response.Message)
-	buffer := buf.NewSize(responseLen)
-	defer buffer.Release()
-	if response.OK {
-		common.Must(buffer.WriteByte(1))
-	} else {
-		common.Must(buffer.WriteByte(0))
-	}
-	common.Must(
-		binary.Write(buffer, binary.BigEndian, response.UDPSessionID),
-		binary.Write(buffer, binary.BigEndian, uint16(len(response.Message))),
-		common.Error(buffer.WriteString(response.Message)),
-	)
-	return common.Error(stream.Write(buffer.Bytes()))
-}
-
-type UDPMessage struct {
-	SessionID uint32
-	Host      string
-	Port      uint16
-	MsgID     uint16 // doesn't matter when not fragmented, but must not be 0 when fragmented
-	FragID    uint8  // doesn't matter when not fragmented, starts at 0 when fragmented
-	FragCount uint8  // must be 1 when not fragmented
-	Data      []byte
-}
-
-func (m UDPMessage) HeaderSize() int {
-	return 4 + 2 + len(m.Host) + 2 + 2 + 1 + 1 + 2
-}
-
-func (m UDPMessage) Size() int {
-	return m.HeaderSize() + len(m.Data)
-}
-
-func ParseUDPMessage(packet []byte) (message UDPMessage, err error) {
-	reader := bytes.NewReader(packet)
-	err = binary.Read(reader, binary.BigEndian, &message.SessionID)
-	if err != nil {
-		return
-	}
-	var hostLen uint16
-	err = binary.Read(reader, binary.BigEndian, &hostLen)
-	if err != nil {
-		return
-	}
-	_, err = reader.Seek(int64(hostLen), io.SeekCurrent)
-	if err != nil {
-		return
-	}
-	if 6+int(hostLen) > len(packet) {
-		err = E.New("invalid host length")
-		return
-	}
-	message.Host = string(packet[6 : 6+hostLen])
-	err = binary.Read(reader, binary.BigEndian, &message.Port)
-	if err != nil {
-		return
-	}
-	err = binary.Read(reader, binary.BigEndian, &message.MsgID)
-	if err != nil {
-		return
-	}
-	err = binary.Read(reader, binary.BigEndian, &message.FragID)
-	if err != nil {
-		return
-	}
-	err = binary.Read(reader, binary.BigEndian, &message.FragCount)
-	if err != nil {
-		return
-	}
-	var dataLen uint16
-	err = binary.Read(reader, binary.BigEndian, &dataLen)
-	if err != nil {
-		return
-	}
-	if reader.Len() != int(dataLen) {
-		err = E.New("invalid data length")
-	}
-	dataOffset := int(reader.Size()) - reader.Len()
-	message.Data = packet[dataOffset:]
-	return
-}
-
-func WriteUDPMessage(conn quic.Connection, message UDPMessage) error {
-	var messageLen int
-	messageLen += 4 // session id
-	messageLen += 2 // host len
-	messageLen += len(message.Host)
-	messageLen += 2 // port
-	messageLen += 2 // msg id
-	messageLen += 1 // frag id
-	messageLen += 1 // frag count
-	messageLen += 2 // data len
-	messageLen += len(message.Data)
-	buffer := buf.NewSize(messageLen)
-	defer buffer.Release()
-	err := writeUDPMessage(conn, message, buffer)
-	if errSize, ok := err.(quic.ErrMessageTooLarge); ok {
-		// need to frag
-		message.MsgID = uint16(rand.Intn(0xFFFF)) + 1 // msgID must be > 0 when fragCount > 1
-		fragMsgs := FragUDPMessage(message, int(errSize))
-		for _, fragMsg := range fragMsgs {
-			buffer.FullReset()
-			err = writeUDPMessage(conn, fragMsg, buffer)
-			if err != nil {
-				return err
-			}
-		}
-		return nil
-	}
-	return err
-}
-
-func writeUDPMessage(conn quic.Connection, message UDPMessage, buffer *buf.Buffer) error {
-	common.Must(
-		binary.Write(buffer, binary.BigEndian, message.SessionID),
-		binary.Write(buffer, binary.BigEndian, uint16(len(message.Host))),
-		common.Error(buffer.WriteString(message.Host)),
-		binary.Write(buffer, binary.BigEndian, message.Port),
-		binary.Write(buffer, binary.BigEndian, message.MsgID),
-		binary.Write(buffer, binary.BigEndian, message.FragID),
-		binary.Write(buffer, binary.BigEndian, message.FragCount),
-		binary.Write(buffer, binary.BigEndian, uint16(len(message.Data))),
-		common.Error(buffer.Write(message.Data)),
-	)
-	return conn.SendMessage(buffer.Bytes())
-}
-
-var _ net.Conn = (*Conn)(nil)
-
-type Conn struct {
-	quic.Stream
-	destination      M.Socksaddr
-	needReadResponse bool
-}
-
-func NewConn(stream quic.Stream, destination M.Socksaddr, isClient bool) *Conn {
-	return &Conn{
-		Stream:           stream,
-		destination:      destination,
-		needReadResponse: isClient,
-	}
-}
-
-func (c *Conn) Read(p []byte) (n int, err error) {
-	if c.needReadResponse {
-		var response *ServerResponse
-		response, err = ReadServerResponse(c.Stream)
-		if err != nil {
-			c.Close()
-			return
-		}
-		if !response.OK {
-			c.Close()
-			return 0, E.New("remote error: ", response.Message)
-		}
-		c.needReadResponse = false
-	}
-	return c.Stream.Read(p)
-}
-
-func (c *Conn) LocalAddr() net.Addr {
-	return M.Socksaddr{}
-}
-
-func (c *Conn) RemoteAddr() net.Addr {
-	return c.destination.TCPAddr()
-}
-
-func (c *Conn) ReaderReplaceable() bool {
-	return !c.needReadResponse
-}
-
-func (c *Conn) WriterReplaceable() bool {
-	return true
-}
-
-func (c *Conn) Upstream() any {
-	return c.Stream
-}
-
-type PacketConn struct {
-	session     quic.Connection
-	stream      quic.Stream
-	sessionId   uint32
-	destination M.Socksaddr
-	msgCh       <-chan *UDPMessage
-	closer      io.Closer
-}
-
-func NewPacketConn(session quic.Connection, stream quic.Stream, sessionId uint32, destination M.Socksaddr, msgCh <-chan *UDPMessage, closer io.Closer) *PacketConn {
-	return &PacketConn{
-		session:     session,
-		stream:      stream,
-		sessionId:   sessionId,
-		destination: destination,
-		msgCh:       msgCh,
-		closer:      closer,
-	}
-}
-
-func (c *PacketConn) Hold() {
-	// Hold the stream until it's closed
-	buf := make([]byte, 1024)
-	for {
-		_, err := c.stream.Read(buf)
-		if err != nil {
-			break
-		}
-	}
-	_ = c.Close()
-}
-
-func (c *PacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
-	msg := <-c.msgCh
-	if msg == nil {
-		err = net.ErrClosed
-		return
-	}
-	err = common.Error(buffer.Write(msg.Data))
-	destination = M.ParseSocksaddrHostPort(msg.Host, msg.Port).Unwrap()
-	return
-}
-
-func (c *PacketConn) ReadPacketThreadSafe() (buffer *buf.Buffer, destination M.Socksaddr, err error) {
-	msg := <-c.msgCh
-	if msg == nil {
-		err = net.ErrClosed
-		return
-	}
-	buffer = buf.As(msg.Data)
-	destination = M.ParseSocksaddrHostPort(msg.Host, msg.Port).Unwrap()
-	return
-}
-
-func (c *PacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
-	return WriteUDPMessage(c.session, UDPMessage{
-		SessionID: c.sessionId,
-		Host:      destination.AddrString(),
-		Port:      destination.Port,
-		FragCount: 1,
-		Data:      buffer.Bytes(),
-	})
-}
-
-func (c *PacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
-	msg := <-c.msgCh
-	if msg == nil {
-		err = net.ErrClosed
-		return
-	}
-	n = copy(p, msg.Data)
-	destination := M.ParseSocksaddrHostPort(msg.Host, msg.Port)
-	if destination.IsFqdn() {
-		addr = destination
-	} else {
-		addr = destination.UDPAddr()
-	}
-	return
-}
-
-func (c *PacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
-	err = c.WritePacket(buf.As(p), M.SocksaddrFromNet(addr))
-	if err == nil {
-		n = len(p)
-	}
-	return
-}
-
-func (c *PacketConn) LocalAddr() net.Addr {
-	return M.Socksaddr{}
-}
-
-func (c *PacketConn) RemoteAddr() net.Addr {
-	return c.destination.UDPAddr()
-}
-
-func (c *PacketConn) SetDeadline(t time.Time) error {
-	return os.ErrInvalid
-}
-
-func (c *PacketConn) SetReadDeadline(t time.Time) error {
-	return os.ErrInvalid
-}
-
-func (c *PacketConn) SetWriteDeadline(t time.Time) error {
-	return os.ErrInvalid
-}
-
-func (c *PacketConn) NeedAdditionalReadDeadline() bool {
-	return true
-}
-
-func (c *PacketConn) Read(b []byte) (n int, err error) {
-	n, _, err = c.ReadFrom(b)
-	return
-}
-
-func (c *PacketConn) Write(b []byte) (n int, err error) {
-	return c.WriteTo(b, c.destination)
-}
-
-func (c *PacketConn) Close() error {
-	return common.Close(c.stream, c.closer)
-}

+ 0 - 36
transport/hysteria/speed.go

@@ -1,36 +0,0 @@
-package hysteria
-
-import (
-	"regexp"
-	"strconv"
-)
-
-func StringToBps(s string) uint64 {
-	if s == "" {
-		return 0
-	}
-	m := regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`).FindStringSubmatch(s)
-	if m == nil {
-		return 0
-	}
-	var n uint64
-	switch m[2] {
-	case "K":
-		n = 1 << 10
-	case "M":
-		n = 1 << 20
-	case "G":
-		n = 1 << 30
-	case "T":
-		n = 1 << 40
-	default:
-		n = 1
-	}
-	v, _ := strconv.ParseUint(m[1], 10, 64)
-	n = v * n
-	if m[3] == "b" {
-		// Bits, need to convert to bytes
-		n = n >> 3
-	}
-	return n
-}

+ 0 - 68
transport/hysteria/wrap.go

@@ -1,68 +0,0 @@
-package hysteria
-
-import (
-	"net"
-	"os"
-	"syscall"
-
-	"github.com/sagernet/quic-go"
-	"github.com/sagernet/sing/common"
-	"github.com/sagernet/sing/common/baderror"
-)
-
-type PacketConnWrapper struct {
-	net.PacketConn
-}
-
-func (c *PacketConnWrapper) SetReadBuffer(bytes int) error {
-	return common.MustCast[*net.UDPConn](c.PacketConn).SetReadBuffer(bytes)
-}
-
-func (c *PacketConnWrapper) SetWriteBuffer(bytes int) error {
-	return common.MustCast[*net.UDPConn](c.PacketConn).SetWriteBuffer(bytes)
-}
-
-func (c *PacketConnWrapper) SyscallConn() (syscall.RawConn, error) {
-	return common.MustCast[*net.UDPConn](c.PacketConn).SyscallConn()
-}
-
-func (c *PacketConnWrapper) File() (f *os.File, err error) {
-	return common.MustCast[*net.UDPConn](c.PacketConn).File()
-}
-
-func (c *PacketConnWrapper) Upstream() any {
-	return c.PacketConn
-}
-
-type StreamWrapper struct {
-	Conn quic.Connection
-	quic.Stream
-}
-
-func (s *StreamWrapper) Read(p []byte) (n int, err error) {
-	n, err = s.Stream.Read(p)
-	return n, baderror.WrapQUIC(err)
-}
-
-func (s *StreamWrapper) Write(p []byte) (n int, err error) {
-	n, err = s.Stream.Write(p)
-	return n, baderror.WrapQUIC(err)
-}
-
-func (s *StreamWrapper) LocalAddr() net.Addr {
-	return s.Conn.LocalAddr()
-}
-
-func (s *StreamWrapper) RemoteAddr() net.Addr {
-	return s.Conn.RemoteAddr()
-}
-
-func (s *StreamWrapper) Upstream() any {
-	return s.Stream
-}
-
-func (s *StreamWrapper) Close() error {
-	s.CancelRead(0)
-	s.Stream.Close()
-	return nil
-}

+ 0 - 118
transport/hysteria/xplus.go

@@ -1,118 +0,0 @@
-package hysteria
-
-import (
-	"crypto/sha256"
-	"math/rand"
-	"net"
-	"sync"
-	"time"
-
-	"github.com/sagernet/sing/common"
-	"github.com/sagernet/sing/common/buf"
-	"github.com/sagernet/sing/common/bufio"
-	M "github.com/sagernet/sing/common/metadata"
-	N "github.com/sagernet/sing/common/network"
-)
-
-const xplusSaltLen = 16
-
-func NewXPlusPacketConn(conn net.PacketConn, key []byte) net.PacketConn {
-	vectorisedWriter, isVectorised := bufio.CreateVectorisedPacketWriter(conn)
-	if isVectorised {
-		return &VectorisedXPlusConn{
-			XPlusPacketConn: XPlusPacketConn{
-				PacketConn: conn,
-				key:        key,
-				rand:       rand.New(rand.NewSource(time.Now().UnixNano())),
-			},
-			writer: vectorisedWriter,
-		}
-	} else {
-		return &XPlusPacketConn{
-			PacketConn: conn,
-			key:        key,
-			rand:       rand.New(rand.NewSource(time.Now().UnixNano())),
-		}
-	}
-}
-
-type XPlusPacketConn struct {
-	net.PacketConn
-	key        []byte
-	randAccess sync.Mutex
-	rand       *rand.Rand
-}
-
-func (c *XPlusPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
-	n, addr, err = c.PacketConn.ReadFrom(p)
-	if err != nil {
-		return
-	} else if n < xplusSaltLen {
-		n = 0
-		return
-	}
-	key := sha256.Sum256(append(c.key, p[:xplusSaltLen]...))
-	for i := range p[xplusSaltLen:] {
-		p[i] = p[xplusSaltLen+i] ^ key[i%sha256.Size]
-	}
-	n -= xplusSaltLen
-	return
-}
-
-func (c *XPlusPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
-	// can't use unsafe buffer on WriteTo
-	buffer := buf.NewSize(len(p) + xplusSaltLen)
-	defer buffer.Release()
-	salt := buffer.Extend(xplusSaltLen)
-	c.randAccess.Lock()
-	_, _ = c.rand.Read(salt)
-	c.randAccess.Unlock()
-	key := sha256.Sum256(append(c.key, salt...))
-	for i := range p {
-		common.Must(buffer.WriteByte(p[i] ^ key[i%sha256.Size]))
-	}
-	return c.PacketConn.WriteTo(buffer.Bytes(), addr)
-}
-
-func (c *XPlusPacketConn) Upstream() any {
-	return c.PacketConn
-}
-
-type VectorisedXPlusConn struct {
-	XPlusPacketConn
-	writer N.VectorisedPacketWriter
-}
-
-func (c *VectorisedXPlusConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
-	header := buf.NewSize(xplusSaltLen)
-	defer header.Release()
-	salt := header.Extend(xplusSaltLen)
-	c.randAccess.Lock()
-	_, _ = c.rand.Read(salt)
-	c.randAccess.Unlock()
-	key := sha256.Sum256(append(c.key, salt...))
-	for i := range p {
-		p[i] ^= key[i%sha256.Size]
-	}
-	return bufio.WriteVectorisedPacket(c.writer, [][]byte{header.Bytes(), p}, M.SocksaddrFromNet(addr))
-}
-
-func (c *VectorisedXPlusConn) WriteVectorisedPacket(buffers []*buf.Buffer, destination M.Socksaddr) error {
-	header := buf.NewSize(xplusSaltLen)
-	defer header.Release()
-	salt := header.Extend(xplusSaltLen)
-	c.randAccess.Lock()
-	_, _ = c.rand.Read(salt)
-	c.randAccess.Unlock()
-	key := sha256.Sum256(append(c.key, salt...))
-	var index int
-	for _, buffer := range buffers {
-		data := buffer.Bytes()
-		for i := range data {
-			data[i] ^= key[index%sha256.Size]
-			index++
-		}
-	}
-	buffers = append([]*buf.Buffer{header}, buffers...)
-	return c.writer.WriteVectorisedPacket(buffers, destination)
-}

+ 1 - 2
transport/v2rayquic/client.go

@@ -12,7 +12,6 @@ import (
 	"github.com/sagernet/sing-box/common/tls"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing-box/transport/hysteria"
 	"github.com/sagernet/sing-quic"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/bufio"
@@ -93,7 +92,7 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
 	if err != nil {
 		return nil, err
 	}
-	return &hysteria.StreamWrapper{Conn: conn, Stream: stream}, nil
+	return &StreamWrapper{Conn: conn, Stream: stream}, nil
 }
 
 func (c *Client) Close() error {

+ 1 - 2
transport/v2rayquic/server.go

@@ -12,7 +12,6 @@ import (
 	"github.com/sagernet/sing-box/common/tls"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing-box/transport/hysteria"
 	"github.com/sagernet/sing-quic"
 	"github.com/sagernet/sing/common"
 	M "github.com/sagernet/sing/common/metadata"
@@ -86,7 +85,7 @@ func (s *Server) streamAcceptLoop(conn quic.Connection) error {
 		if err != nil {
 			return err
 		}
-		go s.handler.NewConnection(conn.Context(), &hysteria.StreamWrapper{Conn: conn, Stream: stream}, M.Metadata{})
+		go s.handler.NewConnection(conn.Context(), &StreamWrapper{Conn: conn, Stream: stream}, M.Metadata{})
 	}
 }
 

+ 41 - 0
transport/v2rayquic/stream.go

@@ -0,0 +1,41 @@
+package v2rayquic
+
+import (
+	"net"
+
+	"github.com/sagernet/quic-go"
+	"github.com/sagernet/sing/common/baderror"
+)
+
+type StreamWrapper struct {
+	Conn quic.Connection
+	quic.Stream
+}
+
+func (s *StreamWrapper) Read(p []byte) (n int, err error) {
+	n, err = s.Stream.Read(p)
+	return n, baderror.WrapQUIC(err)
+}
+
+func (s *StreamWrapper) Write(p []byte) (n int, err error) {
+	n, err = s.Stream.Write(p)
+	return n, baderror.WrapQUIC(err)
+}
+
+func (s *StreamWrapper) LocalAddr() net.Addr {
+	return s.Conn.LocalAddr()
+}
+
+func (s *StreamWrapper) RemoteAddr() net.Addr {
+	return s.Conn.RemoteAddr()
+}
+
+func (s *StreamWrapper) Upstream() any {
+	return s.Stream
+}
+
+func (s *StreamWrapper) Close() error {
+	s.CancelRead(0)
+	s.Stream.Close()
+	return nil
+}