瀏覽代碼

Add gun-lite gRPC implementation (#44)

Hellojack 3 年之前
父節點
當前提交
de2453fce9

+ 1 - 0
option/v2ray_transport.go

@@ -78,4 +78,5 @@ type V2RayQUICOptions struct{}
 
 type V2RayGRPCOptions struct {
 	ServiceName string `json:"service_name,omitempty"`
+	ForceLite   bool   `json:"-"` // for test
 }

+ 61 - 8
test/v2ray_transport_test.go

@@ -20,6 +20,52 @@ func TestV2RayGRPCSelf(t *testing.T) {
 	})
 }
 
+func TestV2RayGRPCLite(t *testing.T) {
+	t.Run("server", func(t *testing.T) {
+		testV2RayTransportSelfWith(t, &option.V2RayTransportOptions{
+			Type: C.V2RayTransportTypeGRPC,
+			GRPCOptions: option.V2RayGRPCOptions{
+				ServiceName: "TunService",
+				ForceLite:   true,
+			},
+		}, &option.V2RayTransportOptions{
+			Type: C.V2RayTransportTypeGRPC,
+			GRPCOptions: option.V2RayGRPCOptions{
+				ServiceName: "TunService",
+			},
+		})
+	})
+	t.Run("client", func(t *testing.T) {
+		testV2RayTransportSelfWith(t, &option.V2RayTransportOptions{
+			Type: C.V2RayTransportTypeGRPC,
+			GRPCOptions: option.V2RayGRPCOptions{
+				ServiceName: "TunService",
+			},
+		}, &option.V2RayTransportOptions{
+			Type: C.V2RayTransportTypeGRPC,
+			GRPCOptions: option.V2RayGRPCOptions{
+				ServiceName: "TunService",
+				ForceLite:   true,
+			},
+		})
+	})
+	t.Run("self", func(t *testing.T) {
+		testV2RayTransportSelfWith(t, &option.V2RayTransportOptions{
+			Type: C.V2RayTransportTypeGRPC,
+			GRPCOptions: option.V2RayGRPCOptions{
+				ServiceName: "TunService",
+				ForceLite:   true,
+			},
+		}, &option.V2RayTransportOptions{
+			Type: C.V2RayTransportTypeGRPC,
+			GRPCOptions: option.V2RayGRPCOptions{
+				ServiceName: "TunService",
+				ForceLite:   true,
+			},
+		})
+	})
+}
+
 func TestV2RayWebscoketSelf(t *testing.T) {
 	t.Run("basic", func(t *testing.T) {
 		testV2RayTransportSelf(t, &option.V2RayTransportOptions{
@@ -48,6 +94,9 @@ func TestV2RayWebscoketSelf(t *testing.T) {
 func TestV2RayHTTPSelf(t *testing.T) {
 	testV2RayTransportSelf(t, &option.V2RayTransportOptions{
 		Type: C.V2RayTransportTypeHTTP,
+		HTTPOptions: option.V2RayHTTPOptions{
+			Method: "POST",
+		},
 	})
 }
 
@@ -58,15 +107,19 @@ func TestV2RayHTTPPlainSelf(t *testing.T) {
 }
 
 func testV2RayTransportSelf(t *testing.T, transport *option.V2RayTransportOptions) {
+	testV2RayTransportSelfWith(t, transport, transport)
+}
+
+func testV2RayTransportSelfWith(t *testing.T, server, client *option.V2RayTransportOptions) {
 	t.Run("vmess", func(t *testing.T) {
-		testVMessTransportSelf(t, transport)
+		testVMessTransportSelf(t, server, client)
 	})
 	t.Run("trojan", func(t *testing.T) {
-		testTrojanTransportSelf(t, transport)
+		testTrojanTransportSelf(t, server, client)
 	})
 }
 
-func testVMessTransportSelf(t *testing.T, transport *option.V2RayTransportOptions) {
+func testVMessTransportSelf(t *testing.T, server *option.V2RayTransportOptions, client *option.V2RayTransportOptions) {
 	user, err := uuid.DefaultGenerator.NewV4()
 	require.NoError(t, err)
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
@@ -104,7 +157,7 @@ func testVMessTransportSelf(t *testing.T, transport *option.V2RayTransportOption
 						CertificatePath: certPem,
 						KeyPath:         keyPem,
 					},
-					Transport: transport,
+					Transport: server,
 				},
 			},
 		},
@@ -127,7 +180,7 @@ func testVMessTransportSelf(t *testing.T, transport *option.V2RayTransportOption
 						ServerName:      "example.org",
 						CertificatePath: certPem,
 					},
-					Transport: transport,
+					Transport: client,
 				},
 			},
 		},
@@ -145,7 +198,7 @@ func testVMessTransportSelf(t *testing.T, transport *option.V2RayTransportOption
 	testSuit(t, clientPort, testPort)
 }
 
-func testTrojanTransportSelf(t *testing.T, transport *option.V2RayTransportOptions) {
+func testTrojanTransportSelf(t *testing.T, server *option.V2RayTransportOptions, client *option.V2RayTransportOptions) {
 	user, err := uuid.DefaultGenerator.NewV4()
 	require.NoError(t, err)
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
@@ -183,7 +236,7 @@ func testTrojanTransportSelf(t *testing.T, transport *option.V2RayTransportOptio
 						CertificatePath: certPem,
 						KeyPath:         keyPem,
 					},
-					Transport: transport,
+					Transport: server,
 				},
 			},
 		},
@@ -205,7 +258,7 @@ func testTrojanTransportSelf(t *testing.T, transport *option.V2RayTransportOptio
 						ServerName:      "example.org",
 						CertificatePath: certPem,
 					},
-					Transport: transport,
+					Transport: client,
 				},
 			},
 		},

+ 9 - 1
transport/v2ray/grpc.go

@@ -9,14 +9,22 @@ import (
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/transport/v2raygrpc"
+	"github.com/sagernet/sing-box/transport/v2raygrpclite"
+	E "github.com/sagernet/sing/common/exceptions"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 )
 
-func NewGRPCServer(ctx context.Context, options option.V2RayGRPCOptions, tlsConfig *tls.Config, handler N.TCPConnectionHandler) (adapter.V2RayServerTransport, error) {
+func NewGRPCServer(ctx context.Context, options option.V2RayGRPCOptions, tlsConfig *tls.Config, handler N.TCPConnectionHandler, errorHandler E.Handler) (adapter.V2RayServerTransport, error) {
+	if options.ForceLite {
+		return v2raygrpclite.NewServer(ctx, options, tlsConfig, handler, errorHandler), nil
+	}
 	return v2raygrpc.NewServer(ctx, options, tlsConfig, handler), nil
 }
 
 func NewGRPCClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayGRPCOptions, tlsConfig *tls.Config) (adapter.V2RayClientTransport, error) {
+	if options.ForceLite {
+		return v2raygrpclite.NewClient(ctx, dialer, serverAddr, options, tlsConfig), nil
+	}
 	return v2raygrpc.NewClient(ctx, dialer, serverAddr, options, tlsConfig), nil
 }

+ 4 - 5
transport/v2ray/grpc_stub.go → transport/v2ray/grpc_lite.go

@@ -8,17 +8,16 @@ import (
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-box/transport/v2raygrpclite"
 	E "github.com/sagernet/sing/common/exceptions"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 )
 
-var errGRPCNotIncluded = E.New("gRPC is not included in this build, rebuild with -tags with_grpc")
-
-func NewGRPCServer(ctx context.Context, options option.V2RayGRPCOptions, tlsConfig *tls.Config, handler N.TCPConnectionHandler) (adapter.V2RayServerTransport, error) {
-	return nil, errGRPCNotIncluded
+func NewGRPCServer(ctx context.Context, options option.V2RayGRPCOptions, tlsConfig *tls.Config, handler N.TCPConnectionHandler, errorHandler E.Handler) (adapter.V2RayServerTransport, error) {
+	return v2raygrpclite.NewServer(ctx, options, tlsConfig, handler, errorHandler), nil
 }
 
 func NewGRPCClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayGRPCOptions, tlsConfig *tls.Config) (adapter.V2RayClientTransport, error) {
-	return nil, errGRPCNotIncluded
+	return v2raygrpclite.NewClient(ctx, dialer, serverAddr, options, tlsConfig), nil
 }

+ 1 - 1
transport/v2ray/transport.go

@@ -29,7 +29,7 @@ func NewServerTransport(ctx context.Context, options option.V2RayTransportOption
 		}
 		return NewQUICServer(ctx, options.QUICOptions, tlsConfig, handler, errorHandler)
 	case C.V2RayTransportTypeGRPC:
-		return NewGRPCServer(ctx, options.GRPCOptions, tlsConfig, handler)
+		return NewGRPCServer(ctx, options.GRPCOptions, tlsConfig, handler, errorHandler)
 	default:
 		return nil, E.New("unknown transport type: " + options.Type)
 	}

+ 77 - 0
transport/v2raygrpclite/client.go

@@ -0,0 +1,77 @@
+package v2raygrpclite
+
+import (
+	"context"
+	"crypto/tls"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+	"net/url"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/option"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+)
+
+var _ adapter.V2RayClientTransport = (*Client)(nil)
+
+var defaultClientHeader = http.Header{
+	"Content-Type": []string{"application/grpc"},
+	"User-Agent":   []string{"grpc-go/1.48.0"},
+	"TE":           []string{"trailers"},
+}
+
+type Client struct {
+	ctx        context.Context
+	dialer     N.Dialer
+	serverAddr M.Socksaddr
+	transport  *http.Transport
+	options    option.V2RayGRPCOptions
+	url        *url.URL
+}
+
+func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayGRPCOptions, tlsConfig *tls.Config) adapter.V2RayClientTransport {
+	return &Client{
+		ctx:        ctx,
+		dialer:     dialer,
+		serverAddr: serverAddr,
+		options:    options,
+		transport: &http.Transport{
+			DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+				return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
+			},
+			ForceAttemptHTTP2: true,
+			TLSClientConfig:   tlsConfig,
+		},
+		url: &url.URL{
+			Scheme: "https",
+			Host:   serverAddr.String(),
+			Path:   fmt.Sprintf("/%s/Tun", url.QueryEscape(options.ServiceName)),
+		},
+	}
+}
+
+func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
+	pipeInReader, pipeInWriter := io.Pipe()
+	request := &http.Request{
+		Method:     http.MethodPost,
+		Body:       pipeInReader,
+		URL:        c.url,
+		Proto:      "HTTP/2",
+		ProtoMajor: 2,
+		Header:     defaultClientHeader,
+	}
+	request = request.WithContext(ctx)
+	conn := newLateGunConn(pipeInWriter)
+	go func() {
+		response, err := c.transport.RoundTrip(request)
+		if err == nil {
+			conn.setup(response.Body, nil)
+		} else {
+			conn.setup(nil, err)
+		}
+	}()
+	return conn, nil
+}

+ 168 - 0
transport/v2raygrpclite/conn.go

@@ -0,0 +1,168 @@
+package v2raygrpclite
+
+import (
+	std_bufio "bufio"
+	"bytes"
+	"encoding/binary"
+	"io"
+	"net"
+	"net/http"
+	"os"
+	"time"
+
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/buf"
+	"github.com/sagernet/sing/common/bufio"
+	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/rw"
+)
+
+// kanged from: https://github.com/Qv2ray/gun-lite
+
+var _ net.Conn = (*GunConn)(nil)
+
+type GunConn struct {
+	reader        *std_bufio.Reader
+	writer        io.Writer
+	flusher       http.Flusher
+	create        chan struct{}
+	err           error
+	readRemaining int
+}
+
+func newGunConn(reader io.Reader, writer io.Writer, flusher http.Flusher) *GunConn {
+	return &GunConn{
+		reader:  std_bufio.NewReader(reader),
+		writer:  writer,
+		flusher: flusher,
+	}
+}
+
+func newLateGunConn(writer io.Writer) *GunConn {
+	return &GunConn{
+		create: make(chan struct{}),
+		writer: writer,
+	}
+}
+
+func (c *GunConn) setup(reader io.Reader, err error) {
+	c.reader = std_bufio.NewReader(reader)
+	c.err = err
+	close(c.create)
+}
+
+func (c *GunConn) Read(b []byte) (n int, err error) {
+	n, err = c.read(b)
+	return n, wrapError(err)
+}
+
+func (c *GunConn) read(b []byte) (n int, err error) {
+	if c.reader == nil {
+		<-c.create
+		if c.err != nil {
+			return 0, c.err
+		}
+	}
+
+	if c.readRemaining > 0 {
+		if len(b) > c.readRemaining {
+			b = b[:c.readRemaining]
+		}
+		n, err = c.reader.Read(b)
+		c.readRemaining -= n
+		return
+	}
+
+	_, err = c.reader.Discard(6)
+	if err != nil {
+		return
+	}
+
+	dataLen, err := binary.ReadUvarint(c.reader)
+	if err != nil {
+		return
+	}
+
+	readLen := int(dataLen)
+	c.readRemaining = readLen
+	if len(b) > readLen {
+		b = b[:readLen]
+	}
+
+	n, err = c.reader.Read(b)
+	c.readRemaining -= n
+	return
+}
+
+func (c *GunConn) Write(b []byte) (n int, err error) {
+	protobufHeader := [1 + binary.MaxVarintLen64]byte{0x0A}
+	varuintLen := binary.PutUvarint(protobufHeader[1:], uint64(len(b)))
+	grpcHeader := buf.Get(5)
+	grpcPayloadLen := uint32(1 + varuintLen + len(b))
+	binary.BigEndian.PutUint32(grpcHeader[1:5], grpcPayloadLen)
+	_, err = bufio.Copy(c.writer, io.MultiReader(bytes.NewReader(grpcHeader), bytes.NewReader(protobufHeader[:varuintLen+1]), bytes.NewReader(b)))
+	buf.Put(grpcHeader)
+	if f, ok := c.writer.(http.Flusher); ok {
+		f.Flush()
+	}
+	return len(b), wrapError(err)
+}
+
+func uLen(x uint64) int {
+	i := 0
+	for x >= 0x80 {
+		x >>= 7
+		i++
+	}
+	return i + 1
+}
+
+func (c *GunConn) WriteBuffer(buffer *buf.Buffer) error {
+	defer buffer.Release()
+	dataLen := buffer.Len()
+	varLen := uLen(uint64(dataLen))
+	header := buffer.ExtendHeader(6 + varLen)
+	binary.BigEndian.PutUint32(header[1:5], uint32(1+varLen+dataLen))
+	header[5] = 0x0A
+	binary.PutUvarint(header[6:], uint64(dataLen))
+	err := rw.WriteBytes(c.writer, buffer.Bytes())
+	if c.flusher != nil {
+		c.flusher.Flush()
+	}
+	return wrapError(err)
+}
+
+func (c *GunConn) FrontHeadroom() int {
+	return 6 + binary.MaxVarintLen64
+}
+
+func (c *GunConn) Close() error {
+	return common.Close(c.reader, c.writer)
+}
+
+func (c *GunConn) LocalAddr() net.Addr {
+	return nil
+}
+
+func (c *GunConn) RemoteAddr() net.Addr {
+	return nil
+}
+
+func (c *GunConn) SetDeadline(t time.Time) error {
+	return os.ErrInvalid
+}
+
+func (c *GunConn) SetReadDeadline(t time.Time) error {
+	return os.ErrInvalid
+}
+
+func (c *GunConn) SetWriteDeadline(t time.Time) error {
+	return os.ErrInvalid
+}
+
+func wrapError(err error) error {
+	if E.IsMulti(err, io.ErrUnexpectedEOF) {
+		return io.EOF
+	}
+	return err
+}

+ 96 - 0
transport/v2raygrpclite/server.go

@@ -0,0 +1,96 @@
+package v2raygrpclite
+
+import (
+	"context"
+	"crypto/tls"
+	"fmt"
+	"net"
+	"net/http"
+	"net/url"
+	"os"
+	"strings"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/option"
+	"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"
+	sHttp "github.com/sagernet/sing/protocol/http"
+)
+
+var _ adapter.V2RayServerTransport = (*Server)(nil)
+
+type Server struct {
+	handler      N.TCPConnectionHandler
+	errorHandler E.Handler
+	httpServer   *http.Server
+	path         string
+}
+
+func (s *Server) Network() []string {
+	return []string{N.NetworkTCP}
+}
+
+func NewServer(ctx context.Context, options option.V2RayGRPCOptions, tlsConfig *tls.Config, handler N.TCPConnectionHandler, errorHandler E.Handler) *Server {
+	server := &Server{
+		handler:      handler,
+		errorHandler: errorHandler,
+		path:         fmt.Sprintf("/%s/Tun", url.QueryEscape(options.ServiceName)),
+	}
+	if tlsConfig != nil {
+		if !common.Contains(tlsConfig.NextProtos, "h2") {
+			tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2")
+		}
+	}
+	server.httpServer = &http.Server{
+		Handler:   server,
+		TLSConfig: tlsConfig,
+	}
+	return server
+}
+
+func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
+	if request.URL.Path != s.path {
+		writer.WriteHeader(http.StatusNotFound)
+		s.badRequest(request, E.New("bad path: ", request.URL.Path))
+		return
+	}
+	if request.Method != http.MethodPost {
+		writer.WriteHeader(http.StatusNotFound)
+		s.badRequest(request, E.New("bad method: ", request.Method))
+		return
+	}
+	if ct := request.Header.Get("Content-Type"); !strings.HasPrefix(ct, "application/grpc") {
+		writer.WriteHeader(http.StatusNotFound)
+		s.badRequest(request, E.New("bad content type: ", ct))
+		return
+	}
+	writer.Header().Set("Content-Type", "application/grpc")
+	writer.Header().Set("TE", "trailers")
+	writer.WriteHeader(http.StatusOK)
+	var metadata M.Metadata
+	metadata.Source = sHttp.SourceAddress(request)
+	conn := newGunConn(request.Body, writer, writer.(http.Flusher))
+	s.handler.NewConnection(request.Context(), conn, metadata)
+}
+
+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) Serve(listener net.Listener) error {
+	if s.httpServer.TLSConfig == nil {
+		return s.httpServer.Serve(listener)
+	} else {
+		return s.httpServer.ServeTLS(listener, "", "")
+	}
+}
+
+func (s *Server) ServePacket(listener net.PacketConn) error {
+	return os.ErrInvalid
+}
+
+func (s *Server) Close() error {
+	return common.Close(common.PtrOrNil(s.httpServer))
+}

+ 1 - 0
transport/v2rayhttp/client.go

@@ -141,6 +141,7 @@ func (c *Client) dialHTTP2(ctx context.Context) (net.Conn, error) {
 		return nil, err
 	}
 	if response.StatusCode != 200 {
+		pipeInWriter.Close()
 		return nil, E.New("unexpected status: ", response.StatusCode, " ", response.Status)
 	}
 	return &HTTPConn{