From d4b7e221f06b3ea2075d32d1bf376d40d069b64d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 22 Aug 2022 21:20:05 +0800 Subject: [PATCH] Add v2ray QUIC transport --- adapter/v2ray.go | 2 + constant/quic_stub.go | 4 ++ constant/v2ray.go | 1 + inbound/default.go | 12 ++++ inbound/hysteria_stub.go | 4 +- inbound/naive_quic_stub.go | 6 +- inbound/vmess.go | 32 +++++++--- option/v2ray_transport.go | 9 +++ outbound/hysteria_stub.go | 4 +- test/box_test.go | 2 +- test/hysteria_test.go | 4 +- test/trojan_test.go | 3 +- test/vmess_test.go | 3 +- test/vmess_transport_test.go | 11 +++- transport/hysteria/wrap.go | 9 +++ transport/v2ray/quic.go | 23 ++++++++ transport/v2ray/quic_stub.go | 23 ++++++++ transport/v2ray/transport.go | 4 ++ transport/v2raygrpc/server.go | 9 +++ transport/v2rayquic/client.go | 92 +++++++++++++++++++++++++++++ transport/v2rayquic/server.go | 95 ++++++++++++++++++++++++++++++ transport/v2raywebsocket/server.go | 9 +++ 22 files changed, 336 insertions(+), 25 deletions(-) create mode 100644 transport/v2ray/quic.go create mode 100644 transport/v2ray/quic_stub.go create mode 100644 transport/v2rayquic/client.go create mode 100644 transport/v2rayquic/server.go diff --git a/adapter/v2ray.go b/adapter/v2ray.go index 9509a450..724c4935 100644 --- a/adapter/v2ray.go +++ b/adapter/v2ray.go @@ -6,7 +6,9 @@ import ( ) type V2RayServerTransport interface { + Network() []string Serve(listener net.Listener) error + ServePacket(listener net.PacketConn) error Close() error } diff --git a/constant/quic_stub.go b/constant/quic_stub.go index e91b1379..7cf7a0a8 100644 --- a/constant/quic_stub.go +++ b/constant/quic_stub.go @@ -2,4 +2,8 @@ package constant +import E "github.com/sagernet/sing/common/exceptions" + const QUIC_AVAILABLE = false + +var ErrQUICNotIncluded = E.New(`QUIC is not included in this build, rebuild with -tags with_quic`) diff --git a/constant/v2ray.go b/constant/v2ray.go index cfffbb95..d5d5e7ba 100644 --- a/constant/v2ray.go +++ b/constant/v2ray.go @@ -3,4 +3,5 @@ package constant const ( V2RayTransportTypeGRPC = "grpc" V2RayTransportTypeWebsocket = "ws" + V2RayTransportTypeQUIC = "quic" ) diff --git a/inbound/default.go b/inbound/default.go index 74fb1f62..3150b03f 100644 --- a/inbound/default.go +++ b/inbound/default.go @@ -129,6 +129,18 @@ func (a *myInboundAdapter) ListenTCP() (*net.TCPListener, error) { return tcpListener, err } +func (a *myInboundAdapter) ListenUDP() (*net.UDPConn, error) { + bindAddr := M.SocksaddrFrom(netip.Addr(a.listenOptions.Listen), a.listenOptions.ListenPort) + udpConn, err := net.ListenUDP(M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.UDPAddr()) + if err != nil { + return nil, err + } + a.udpConn = udpConn + a.udpAddr = bindAddr + a.logger.Info("udp server started at ", udpConn.LocalAddr()) + return udpConn, err +} + func (a *myInboundAdapter) Close() error { var err error if a.clearSystemProxy != nil { diff --git a/inbound/hysteria_stub.go b/inbound/hysteria_stub.go index 5a619115..1a56a5b6 100644 --- a/inbound/hysteria_stub.go +++ b/inbound/hysteria_stub.go @@ -6,11 +6,11 @@ import ( "context" "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" - E "github.com/sagernet/sing/common/exceptions" ) func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (adapter.Inbound, error) { - return nil, E.New(`QUIC is not included in this build, rebuild with -tags with_quic`) + return nil, C.ErrQUICNotIncluded } diff --git a/inbound/naive_quic_stub.go b/inbound/naive_quic_stub.go index d7a2a811..8cc2ff8d 100644 --- a/inbound/naive_quic_stub.go +++ b/inbound/naive_quic_stub.go @@ -2,8 +2,10 @@ package inbound -import E "github.com/sagernet/sing/common/exceptions" +import ( + C "github.com/sagernet/sing-box/constant" +) func (n *Naive) configureHTTP3Listener(listenAddr string) error { - return E.New("QUIC is not included in this build, rebuild with -tags with_quic") + return C.ErrQUICNotIncluded } diff --git a/inbound/vmess.go b/inbound/vmess.go index 0ee493e2..a3f14f97 100644 --- a/inbound/vmess.go +++ b/inbound/vmess.go @@ -83,16 +83,30 @@ func (h *VMess) Start() error { if h.transport == nil { return h.myInboundAdapter.Start() } - tcpListener, err := h.myInboundAdapter.ListenTCP() - if err != nil { - return err - } - go func() { - sErr := h.transport.Serve(tcpListener) - if sErr != nil && !E.IsClosed(sErr) { - h.logger.Error("transport serve error: ", sErr) + if common.Contains(h.transport.Network(), N.NetworkTCP) { + tcpListener, err := h.myInboundAdapter.ListenTCP() + if err != nil { + return err } - }() + go func() { + sErr := h.transport.Serve(tcpListener) + if sErr != nil && !E.IsClosed(sErr) { + h.logger.Error("transport serve error: ", sErr) + } + }() + } + if common.Contains(h.transport.Network(), N.NetworkUDP) { + udpConn, err := h.myInboundAdapter.ListenUDP() + if err != nil { + return err + } + go func() { + sErr := h.transport.ServePacket(udpConn) + if sErr != nil && !E.IsClosed(sErr) { + h.logger.Error("transport serve error: ", sErr) + } + }() + } return nil } diff --git a/option/v2ray_transport.go b/option/v2ray_transport.go index 17a98a58..bc1567ff 100644 --- a/option/v2ray_transport.go +++ b/option/v2ray_transport.go @@ -10,6 +10,7 @@ type _V2RayTransportOptions struct { Type string `json:"type,omitempty"` GRPCOptions V2RayGRPCOptions `json:"-"` WebsocketOptions V2RayWebsocketOptions `json:"-"` + QUICOptions V2RayQUICOptions `json:"-"` } type V2RayTransportOptions _V2RayTransportOptions @@ -23,6 +24,8 @@ func (o V2RayTransportOptions) MarshalJSON() ([]byte, error) { v = o.GRPCOptions case C.V2RayTransportTypeWebsocket: v = o.WebsocketOptions + case C.V2RayTransportTypeQUIC: + v = o.QUICOptions default: return nil, E.New("unknown transport type: " + o.Type) } @@ -38,6 +41,10 @@ func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error { switch o.Type { case C.V2RayTransportTypeGRPC: v = &o.GRPCOptions + case C.V2RayTransportTypeWebsocket: + v = &o.WebsocketOptions + case C.V2RayTransportTypeQUIC: + v = &o.QUICOptions default: return E.New("unknown transport type: " + o.Type) } @@ -102,3 +109,5 @@ type V2RayWebsocketOptions struct { MaxEarlyData uint32 `json:"max_early_data,omitempty"` EarlyDataHeaderName string `json:"early_data_header_name,omitempty"` } + +type V2RayQUICOptions struct{} diff --git a/outbound/hysteria_stub.go b/outbound/hysteria_stub.go index ae2d62b4..62fae20c 100644 --- a/outbound/hysteria_stub.go +++ b/outbound/hysteria_stub.go @@ -6,11 +6,11 @@ import ( "context" "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" - E "github.com/sagernet/sing/common/exceptions" ) func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (adapter.Outbound, error) { - return nil, E.New(`QUIC is not included in this build, rebuild with -tags with_quic`) + return nil, C.ErrQUICNotIncluded } diff --git a/test/box_test.go b/test/box_test.go index d70caf70..a84023e7 100644 --- a/test/box_test.go +++ b/test/box_test.go @@ -59,7 +59,7 @@ func testTCP(t *testing.T, clientPort uint16, testPort uint16) { require.NoError(t, testPingPongWithConn(t, testPort, dialTCP)) } -func testSuitHy(t *testing.T, clientPort uint16, testPort uint16) { +func testSuitQUIC(t *testing.T, clientPort uint16, testPort uint16) { dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "") dialTCP := func() (net.Conn, error) { return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort)) diff --git a/test/hysteria_test.go b/test/hysteria_test.go index 3c9c6d2b..c757a0fd 100644 --- a/test/hysteria_test.go +++ b/test/hysteria_test.go @@ -83,7 +83,7 @@ func TestHysteriaSelf(t *testing.T) { }, }, }) - testSuitHy(t, clientPort, testPort) + testSuitQUIC(t, clientPort, testPort) } func TestHysteriaInbound(t *testing.T) { @@ -180,5 +180,5 @@ func TestHysteriaOutbound(t *testing.T) { }, }, }) - testSuitHy(t, clientPort, testPort) + testSuitQUIC(t, clientPort, testPort) } diff --git a/test/trojan_test.go b/test/trojan_test.go index f9d8260a..6445be67 100644 --- a/test/trojan_test.go +++ b/test/trojan_test.go @@ -59,8 +59,7 @@ func TestTrojanSelf(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ Log: &option.LogOptions{ - Level: "error", - Output: "stderr", + Level: "error", }, Inbounds: []option.Inbound{ { diff --git a/test/vmess_test.go b/test/vmess_test.go index f35942b0..e87aefbe 100644 --- a/test/vmess_test.go +++ b/test/vmess_test.go @@ -229,8 +229,7 @@ func testVMessOutboundWithV2Ray(t *testing.T, security string, uuid uuid.UUID, g func testVMessSelf(t *testing.T, security string, uuid uuid.UUID, alterId int, globalPadding bool, authenticatedLength bool) { startInstance(t, option.Options{ Log: &option.LogOptions{ - Level: "error", - Output: "stderr", + Level: "error", }, Inbounds: []option.Inbound{ { diff --git a/test/vmess_transport_test.go b/test/vmess_transport_test.go index 0f3681dd..ae050b55 100644 --- a/test/vmess_transport_test.go +++ b/test/vmess_transport_test.go @@ -45,14 +45,19 @@ func TestVMessWebscoketSelf(t *testing.T) { }) } +func TestVMessQUICSelf(t *testing.T) { + testVMessWebscoketSelf(t, &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeQUIC, + }) +} + func testVMessWebscoketSelf(t *testing.T, transport *option.V2RayTransportOptions) { user, err := uuid.DefaultGenerator.NewV4() require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ Log: &option.LogOptions{ - Level: "error", - Output: "stderr", + Level: "error", }, Inbounds: []option.Inbound{ { @@ -122,5 +127,5 @@ func testVMessWebscoketSelf(t *testing.T, transport *option.V2RayTransportOption }, }, }) - testSuit(t, clientPort, testPort) + testSuitQUIC(t, clientPort, testPort) } diff --git a/transport/hysteria/wrap.go b/transport/hysteria/wrap.go index c280cf1c..f8df1b3b 100644 --- a/transport/hysteria/wrap.go +++ b/transport/hysteria/wrap.go @@ -34,9 +34,18 @@ func (c *PacketConnWrapper) Upstream() any { } type StreamWrapper struct { + Conn quic.Connection quic.Stream } +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 } diff --git a/transport/v2ray/quic.go b/transport/v2ray/quic.go new file mode 100644 index 00000000..535864e7 --- /dev/null +++ b/transport/v2ray/quic.go @@ -0,0 +1,23 @@ +//go:build with_quic + +package v2ray + +import ( + "context" + "crypto/tls" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/v2rayquic" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func NewQUICServer(ctx context.Context, options option.V2RayQUICOptions, tlsConfig *tls.Config, handler N.TCPConnectionHandler, errorHandler E.Handler) (adapter.V2RayServerTransport, error) { + return v2rayquic.NewServer(ctx, options, tlsConfig, handler, errorHandler), nil +} + +func NewQUICClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayQUICOptions, tlsConfig *tls.Config) (adapter.V2RayClientTransport, error) { + return v2rayquic.NewClient(ctx, dialer, serverAddr, options, tlsConfig), nil +} diff --git a/transport/v2ray/quic_stub.go b/transport/v2ray/quic_stub.go new file mode 100644 index 00000000..d832e016 --- /dev/null +++ b/transport/v2ray/quic_stub.go @@ -0,0 +1,23 @@ +//go:build !with_quic + +package v2ray + +import ( + "context" + "crypto/tls" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func NewQUICServer(ctx context.Context, options option.V2RayQUICOptions, tlsConfig *tls.Config, handler N.TCPConnectionHandler, errorHandler E.Handler) (adapter.V2RayServerTransport, error) { + return nil, C.ErrQUICNotIncluded +} + +func NewQUICClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayQUICOptions, tlsConfig *tls.Config) (adapter.V2RayClientTransport, error) { + return nil, C.ErrQUICNotIncluded +} diff --git a/transport/v2ray/transport.go b/transport/v2ray/transport.go index fcdf9071..f136dbe8 100644 --- a/transport/v2ray/transport.go +++ b/transport/v2ray/transport.go @@ -22,6 +22,8 @@ func NewServerTransport(ctx context.Context, options option.V2RayTransportOption return NewGRPCServer(ctx, options.GRPCOptions, tlsConfig, handler) case C.V2RayTransportTypeWebsocket: return v2raywebsocket.NewServer(ctx, options.WebsocketOptions, tlsConfig, handler, errorHandler), nil + case C.V2RayTransportTypeQUIC: + return NewQUICServer(ctx, options.QUICOptions, tlsConfig, handler, errorHandler) default: return nil, E.New("unknown transport type: " + options.Type) } @@ -36,6 +38,8 @@ func NewClientTransport(ctx context.Context, dialer N.Dialer, serverAddr M.Socks return NewGRPCClient(ctx, dialer, serverAddr, options.GRPCOptions, tlsConfig) case C.V2RayTransportTypeWebsocket: return v2raywebsocket.NewClient(ctx, dialer, serverAddr, options.WebsocketOptions, tlsConfig), nil + case C.V2RayTransportTypeQUIC: + return NewQUICClient(ctx, dialer, serverAddr, options.QUICOptions, tlsConfig) default: return nil, E.New("unknown transport type: " + options.Type) } diff --git a/transport/v2raygrpc/server.go b/transport/v2raygrpc/server.go index ddae67a0..41ad0a70 100644 --- a/transport/v2raygrpc/server.go +++ b/transport/v2raygrpc/server.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "net" + "os" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/option" @@ -44,10 +45,18 @@ func (s *Server) Tun(server GunService_TunServer) error { func (s *Server) mustEmbedUnimplementedGunServiceServer() { } +func (s *Server) Network() []string { + return []string{N.NetworkTCP} +} + func (s *Server) Serve(listener net.Listener) error { return s.server.Serve(listener) } +func (s *Server) ServePacket(listener net.PacketConn) error { + return os.ErrInvalid +} + func (s *Server) Close() error { s.server.Stop() return nil diff --git a/transport/v2rayquic/client.go b/transport/v2rayquic/client.go new file mode 100644 index 00000000..06acaf14 --- /dev/null +++ b/transport/v2rayquic/client.go @@ -0,0 +1,92 @@ +package v2rayquic + +import ( + "context" + "crypto/tls" + "net" + "sync" + + "github.com/sagernet/quic-go" + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/hysteria" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +var _ adapter.V2RayClientTransport = (*Client)(nil) + +type Client struct { + ctx context.Context + dialer N.Dialer + serverAddr M.Socksaddr + tlsConfig *tls.Config + quicConfig *quic.Config + conn quic.Connection + connAccess sync.Mutex +} + +func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayQUICOptions, tlsConfig *tls.Config) adapter.V2RayClientTransport { + quicConfig := &quic.Config{ + DisablePathMTUDiscovery: !C.IsLinux && !C.IsWindows, + } + if len(tlsConfig.NextProtos) == 0 { + tlsConfig.NextProtos = []string{"h2", "http/1.1"} + } + return &Client{ + ctx: ctx, + dialer: dialer, + serverAddr: serverAddr, + tlsConfig: tlsConfig, + quicConfig: quicConfig, + } +} + +func (c *Client) offer() (quic.Connection, error) { + conn := c.conn + if conn != nil && !common.Done(conn.Context()) { + return conn, nil + } + c.connAccess.Lock() + defer c.connAccess.Unlock() + conn = c.conn + if conn != nil && !common.Done(conn.Context()) { + return conn, nil + } + conn, err := c.offerNew() + if err != nil { + return nil, err + } + c.conn = conn + return conn, nil +} + +func (c *Client) offerNew() (quic.Connection, error) { + udpConn, err := c.dialer.DialContext(c.ctx, "udp", c.serverAddr) + if err != nil { + return nil, err + } + var packetConn net.PacketConn + packetConn = bufio.NewUnbindPacketConn(udpConn) + quicConn, err := quic.Dial(packetConn, udpConn.RemoteAddr(), c.serverAddr.AddrString(), c.tlsConfig, c.quicConfig) + if err != nil { + packetConn.Close() + return nil, err + } + return quicConn, nil +} + +func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { + conn, err := c.offer() + if err != nil { + return nil, err + } + stream, err := conn.OpenStream() + if err != nil { + return nil, err + } + return &hysteria.StreamWrapper{Conn: conn, Stream: stream}, nil +} diff --git a/transport/v2rayquic/server.go b/transport/v2rayquic/server.go new file mode 100644 index 00000000..f376ccdc --- /dev/null +++ b/transport/v2rayquic/server.go @@ -0,0 +1,95 @@ +package v2rayquic + +import ( + "context" + "crypto/tls" + "net" + "os" + + "github.com/sagernet/quic-go" + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/hysteria" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +var _ adapter.V2RayServerTransport = (*Server)(nil) + +type Server struct { + ctx context.Context + tlsConfig *tls.Config + quicConfig *quic.Config + handler N.TCPConnectionHandler + errorHandler E.Handler + udpListener net.PacketConn + quicListener quic.Listener +} + +func NewServer(ctx context.Context, options option.V2RayQUICOptions, tlsConfig *tls.Config, handler N.TCPConnectionHandler, errorHandler E.Handler) *Server { + quicConfig := &quic.Config{ + DisablePathMTUDiscovery: !C.IsLinux && !C.IsWindows, + } + if len(tlsConfig.NextProtos) == 0 { + tlsConfig.NextProtos = []string{"h2", "http/1.1"} + } + server := &Server{ + ctx: ctx, + tlsConfig: tlsConfig, + quicConfig: quicConfig, + handler: handler, + errorHandler: errorHandler, + } + return server +} + +func (s *Server) Network() []string { + return []string{N.NetworkUDP} +} + +func (s *Server) Serve(listener net.Listener) error { + return os.ErrInvalid +} + +func (s *Server) ServePacket(listener net.PacketConn) error { + quicListener, err := quic.Listen(listener, s.tlsConfig, s.quicConfig) + if err != nil { + return err + } + s.udpListener = listener + s.quicListener = quicListener + go s.acceptLoop() + return nil +} + +func (s *Server) acceptLoop() { + for { + conn, err := s.quicListener.Accept(s.ctx) + if err != nil { + return + } + go func() { + hErr := s.streamAcceptLoop(conn) + if hErr != nil { + s.errorHandler.NewError(conn.Context(), hErr) + } + }() + } +} + +func (s *Server) streamAcceptLoop(conn quic.Connection) error { + for { + stream, err := conn.AcceptStream(s.ctx) + if err != nil { + return err + } + go s.handler.NewConnection(conn.Context(), &hysteria.StreamWrapper{Conn: conn, Stream: stream}, M.Metadata{}) + } +} + +func (s *Server) Close() error { + return common.Close(s.udpListener, s.quicListener) +} diff --git a/transport/v2raywebsocket/server.go b/transport/v2raywebsocket/server.go index d8239a3c..4c00f954 100644 --- a/transport/v2raywebsocket/server.go +++ b/transport/v2raywebsocket/server.go @@ -7,6 +7,7 @@ import ( "net" "net/http" "net/netip" + "os" "strings" "github.com/sagernet/sing-box/adapter" @@ -125,6 +126,10 @@ func (s *Server) badRequest(request *http.Request, err error) { s.errorHandler.NewError(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) } +func (s *Server) Network() []string { + return []string{N.NetworkTCP} +} + func (s *Server) Serve(listener net.Listener) error { if s.httpServer.TLSConfig == nil { return s.httpServer.Serve(listener) @@ -133,6 +138,10 @@ func (s *Server) Serve(listener net.Listener) error { } } +func (s *Server) ServePacket(listener net.PacketConn) error { + return os.ErrInvalid +} + func (s *Server) Close() error { return common.Close(s.httpServer) }