浏览代码

Add support for v2ray http upgrade transport

世界 1 年之前
父节点
当前提交
6d24be23da

+ 5 - 4
constant/v2ray.go

@@ -1,8 +1,9 @@
 package constant
 package constant
 
 
 const (
 const (
-	V2RayTransportTypeHTTP      = "http"
-	V2RayTransportTypeWebsocket = "ws"
-	V2RayTransportTypeQUIC      = "quic"
-	V2RayTransportTypeGRPC      = "grpc"
+	V2RayTransportTypeHTTP        = "http"
+	V2RayTransportTypeWebsocket   = "ws"
+	V2RayTransportTypeQUIC        = "quic"
+	V2RayTransportTypeGRPC        = "grpc"
+	V2RayTransportTypeHTTPUpgrade = "httpupgrade"
 )
 )

+ 30 - 0
docs/configuration/shared/v2ray-transport.md

@@ -15,6 +15,7 @@ Available transports:
 * WebSocket
 * WebSocket
 * QUIC
 * QUIC
 * gRPC
 * gRPC
+* HTTPUpgrade
 
 
 !!! warning "Difference from v2ray-core"
 !!! warning "Difference from v2ray-core"
 
 
@@ -184,3 +185,32 @@ In standard gRPC client:
 If enabled, the client transport sends keepalive pings even with no active connections. If disabled, when there are no active connections, `idle_timeout` and `ping_timeout` will be ignored and no keepalive pings will be sent.
 If enabled, the client transport sends keepalive pings even with no active connections. If disabled, when there are no active connections, `idle_timeout` and `ping_timeout` will be ignored and no keepalive pings will be sent.
 
 
 Disabled by default.
 Disabled by default.
+
+### HTTPUpgrade
+
+```json
+{
+  "type": "httpupgrade",
+  "host": "",
+  "path": "",
+  "headers": {}
+}
+```
+
+#### host
+
+Host domain.
+
+The server will verify if not empty.
+
+#### path
+
+Path of HTTP request.
+
+The server will verify if not empty.
+
+#### headers
+
+Extra headers of HTTP request.
+
+The server will write in response if not empty.

+ 30 - 0
docs/configuration/shared/v2ray-transport.zh.md

@@ -14,6 +14,7 @@ V2Ray Transport 是 v2ray 发明的一组私有协议,并污染了其他协议
 * WebSocket
 * WebSocket
 * QUIC
 * QUIC
 * gRPC
 * gRPC
+* HTTPUpgrade
 
 
 !!! warning "与 v2ray-core 的区别"
 !!! warning "与 v2ray-core 的区别"
 
 
@@ -183,3 +184,32 @@ gRPC 服务名称。
 如果启用,客户端传输即使没有活动连接也会发送 keepalive ping。如果禁用,则在没有活动连接时,将忽略 `idle_timeout` 和 `ping_timeout`,并且不会发送 keepalive ping。
 如果启用,客户端传输即使没有活动连接也会发送 keepalive ping。如果禁用,则在没有活动连接时,将忽略 `idle_timeout` 和 `ping_timeout`,并且不会发送 keepalive ping。
 
 
 默认禁用。
 默认禁用。
+
+### HTTPUpgrade
+
+```json
+{
+  "type": "httpupgrade",
+  "host": "",
+  "path": "",
+  "headers": {}
+}
+```
+
+#### host
+
+主机域名。
+
+默认服务器将验证。
+
+#### path
+
+HTTP 请求路径
+
+默认服务器将验证。
+
+#### headers
+
+HTTP 请求的额外标头。
+
+默认服务器将写入响应。

+ 16 - 5
option/v2ray_transport.go

@@ -7,11 +7,12 @@ import (
 )
 )
 
 
 type _V2RayTransportOptions struct {
 type _V2RayTransportOptions struct {
-	Type             string                `json:"type,omitempty"`
-	HTTPOptions      V2RayHTTPOptions      `json:"-"`
-	WebsocketOptions V2RayWebsocketOptions `json:"-"`
-	QUICOptions      V2RayQUICOptions      `json:"-"`
-	GRPCOptions      V2RayGRPCOptions      `json:"-"`
+	Type               string                  `json:"type,omitempty"`
+	HTTPOptions        V2RayHTTPOptions        `json:"-"`
+	WebsocketOptions   V2RayWebsocketOptions   `json:"-"`
+	QUICOptions        V2RayQUICOptions        `json:"-"`
+	GRPCOptions        V2RayGRPCOptions        `json:"-"`
+	HTTPUpgradeOptions V2RayHTTPUpgradeOptions `json:"-"`
 }
 }
 
 
 type V2RayTransportOptions _V2RayTransportOptions
 type V2RayTransportOptions _V2RayTransportOptions
@@ -29,6 +30,8 @@ func (o V2RayTransportOptions) MarshalJSON() ([]byte, error) {
 		v = o.QUICOptions
 		v = o.QUICOptions
 	case C.V2RayTransportTypeGRPC:
 	case C.V2RayTransportTypeGRPC:
 		v = o.GRPCOptions
 		v = o.GRPCOptions
+	case C.V2RayTransportTypeHTTPUpgrade:
+		v = o.HTTPUpgradeOptions
 	default:
 	default:
 		return nil, E.New("unknown transport type: " + o.Type)
 		return nil, E.New("unknown transport type: " + o.Type)
 	}
 	}
@@ -50,6 +53,8 @@ func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error {
 		v = &o.QUICOptions
 		v = &o.QUICOptions
 	case C.V2RayTransportTypeGRPC:
 	case C.V2RayTransportTypeGRPC:
 		v = &o.GRPCOptions
 		v = &o.GRPCOptions
+	case C.V2RayTransportTypeHTTPUpgrade:
+		v = &o.HTTPUpgradeOptions
 	default:
 	default:
 		return E.New("unknown transport type: " + o.Type)
 		return E.New("unknown transport type: " + o.Type)
 	}
 	}
@@ -85,3 +90,9 @@ type V2RayGRPCOptions struct {
 	PermitWithoutStream bool     `json:"permit_without_stream,omitempty"`
 	PermitWithoutStream bool     `json:"permit_without_stream,omitempty"`
 	ForceLite           bool     `json:"-"` // for test
 	ForceLite           bool     `json:"-"` // for test
 }
 }
+
+type V2RayHTTPUpgradeOptions struct {
+	Host    string     `json:"host,omitempty"`
+	Path    string     `json:"path,omitempty"`
+	Headers HTTPHeader `json:"headers,omitempty"`
+}

+ 16 - 0
test/v2ray_httpupgrade_test.go

@@ -0,0 +1,16 @@
+package main
+
+import (
+	"testing"
+
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/option"
+)
+
+func TestV2RayHTTPUpgrade(t *testing.T) {
+	t.Run("self", func(t *testing.T) {
+		testV2RayTransportSelf(t, &option.V2RayTransportOptions{
+			Type: C.V2RayTransportTypeHTTPUpgrade,
+		})
+	})
+}

+ 5 - 1
transport/v2ray/transport.go

@@ -8,6 +8,7 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/transport/v2rayhttp"
 	"github.com/sagernet/sing-box/transport/v2rayhttp"
+	"github.com/sagernet/sing-box/transport/v2rayhttpupgrade"
 	"github.com/sagernet/sing-box/transport/v2raywebsocket"
 	"github.com/sagernet/sing-box/transport/v2raywebsocket"
 	E "github.com/sagernet/sing/common/exceptions"
 	E "github.com/sagernet/sing/common/exceptions"
 	M "github.com/sagernet/sing/common/metadata"
 	M "github.com/sagernet/sing/common/metadata"
@@ -35,6 +36,8 @@ func NewServerTransport(ctx context.Context, options option.V2RayTransportOption
 		return NewQUICServer(ctx, options.QUICOptions, tlsConfig, handler)
 		return NewQUICServer(ctx, options.QUICOptions, tlsConfig, handler)
 	case C.V2RayTransportTypeGRPC:
 	case C.V2RayTransportTypeGRPC:
 		return NewGRPCServer(ctx, options.GRPCOptions, tlsConfig, handler)
 		return NewGRPCServer(ctx, options.GRPCOptions, tlsConfig, handler)
+	case C.V2RayTransportTypeHTTPUpgrade:
+		return v2rayhttpupgrade.NewServer(ctx, options.HTTPUpgradeOptions, tlsConfig, handler)
 	default:
 	default:
 		return nil, E.New("unknown transport type: " + options.Type)
 		return nil, E.New("unknown transport type: " + options.Type)
 	}
 	}
@@ -56,7 +59,8 @@ func NewClientTransport(ctx context.Context, dialer N.Dialer, serverAddr M.Socks
 			return nil, C.ErrTLSRequired
 			return nil, C.ErrTLSRequired
 		}
 		}
 		return NewQUICClient(ctx, dialer, serverAddr, options.QUICOptions, tlsConfig)
 		return NewQUICClient(ctx, dialer, serverAddr, options.QUICOptions, tlsConfig)
-
+	case C.V2RayTransportTypeHTTPUpgrade:
+		return v2rayhttpupgrade.NewClient(ctx, dialer, serverAddr, options.HTTPUpgradeOptions, tlsConfig)
 	default:
 	default:
 		return nil, E.New("unknown transport type: " + options.Type)
 		return nil, E.New("unknown transport type: " + options.Type)
 	}
 	}

+ 1 - 1
transport/v2raygrpclite/client.go

@@ -100,7 +100,7 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
 			conn.setup(nil, err)
 			conn.setup(nil, err)
 		} else if response.StatusCode != 200 {
 		} else if response.StatusCode != 200 {
 			response.Body.Close()
 			response.Body.Close()
-			conn.setup(nil, E.New("unexpected status: ", response.StatusCode, " ", response.Status))
+			conn.setup(nil, E.New("unexpected status: ", response.Status))
 		} else {
 		} else {
 			conn.setup(response.Body, nil)
 			conn.setup(response.Body, nil)
 		}
 		}

+ 4 - 4
transport/v2raygrpclite/server.go

@@ -35,10 +35,6 @@ type Server struct {
 	path         string
 	path         string
 }
 }
 
 
-func (s *Server) Network() []string {
-	return []string{N.NetworkTCP}
-}
-
 func NewServer(ctx context.Context, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) {
 func NewServer(ctx context.Context, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) {
 	server := &Server{
 	server := &Server{
 		tlsConfig: tlsConfig,
 		tlsConfig: tlsConfig,
@@ -92,6 +88,10 @@ func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Reques
 	s.handler.NewError(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr))
 	s.handler.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 {
 func (s *Server) Serve(listener net.Listener) error {
 	if s.tlsConfig != nil {
 	if s.tlsConfig != nil {
 		if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) {
 		if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) {

+ 1 - 1
transport/v2rayhttp/client.go

@@ -143,7 +143,7 @@ func (c *Client) dialHTTP2(ctx context.Context) (net.Conn, error) {
 			conn.Setup(nil, err)
 			conn.Setup(nil, err)
 		} else if response.StatusCode != 200 {
 		} else if response.StatusCode != 200 {
 			response.Body.Close()
 			response.Body.Close()
-			conn.Setup(nil, E.New("unexpected status: ", response.StatusCode, " ", response.Status))
+			conn.Setup(nil, E.New("unexpected status: ", response.Status))
 		} else {
 		} else {
 			conn.Setup(response.Body, nil)
 			conn.Setup(response.Body, nil)
 		}
 		}

+ 4 - 4
transport/v2rayhttp/server.go

@@ -40,10 +40,6 @@ type Server struct {
 	headers    http.Header
 	headers    http.Header
 }
 }
 
 
-func (s *Server) Network() []string {
-	return []string{N.NetworkTCP}
-}
-
 func NewServer(ctx context.Context, options option.V2RayHTTPOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) {
 func NewServer(ctx context.Context, options option.V2RayHTTPOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) {
 	server := &Server{
 	server := &Server{
 		ctx:       ctx,
 		ctx:       ctx,
@@ -153,6 +149,10 @@ func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Reques
 	s.handler.NewError(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr))
 	s.handler.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 {
 func (s *Server) Serve(listener net.Listener) error {
 	if s.tlsConfig != nil {
 	if s.tlsConfig != nil {
 		if len(s.tlsConfig.NextProtos()) == 0 {
 		if len(s.tlsConfig.NextProtos()) == 0 {

+ 118 - 0
transport/v2rayhttpupgrade/client.go

@@ -0,0 +1,118 @@
+package v2rayhttpupgrade
+
+import (
+	std_bufio "bufio"
+	"context"
+	"net"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/tls"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common/buf"
+	"github.com/sagernet/sing/common/bufio"
+	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.V2RayClientTransport = (*Client)(nil)
+
+type Client struct {
+	dialer     N.Dialer
+	tlsConfig  tls.Config
+	serverAddr M.Socksaddr
+	requestURL url.URL
+	headers    http.Header
+	host       string
+}
+
+func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayHTTPUpgradeOptions, tlsConfig tls.Config) (*Client, error) {
+	if tlsConfig != nil {
+		if len(tlsConfig.NextProtos()) == 0 {
+			tlsConfig.SetNextProtos([]string{"http/1.1"})
+		}
+	}
+	var host string
+	if options.Host != "" {
+		host = options.Host
+	} else if tlsConfig != nil && tlsConfig.ServerName() != "" {
+		host = tlsConfig.ServerName()
+	} else {
+		host = serverAddr.String()
+	}
+	var requestURL url.URL
+	if tlsConfig == nil {
+		requestURL.Scheme = "http"
+	} else {
+		requestURL.Scheme = "https"
+	}
+	requestURL.Host = serverAddr.String()
+	requestURL.Path = options.Path
+	err := sHTTP.URLSetPath(&requestURL, options.Path)
+	if err != nil {
+		return nil, E.Cause(err, "parse path")
+	}
+	if !strings.HasPrefix(requestURL.Path, "/") {
+		requestURL.Path = "/" + requestURL.Path
+	}
+	headers := make(http.Header)
+	for key, value := range options.Headers {
+		headers[key] = value
+	}
+	return &Client{
+		dialer:     dialer,
+		tlsConfig:  tlsConfig,
+		serverAddr: serverAddr,
+		requestURL: requestURL,
+		headers:    headers,
+		host:       host,
+	}, nil
+}
+
+func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
+	conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.serverAddr)
+	if err != nil {
+		return nil, err
+	}
+	if c.tlsConfig != nil {
+		conn, err = tls.ClientHandshake(ctx, conn, c.tlsConfig)
+		if err != nil {
+			return nil, err
+		}
+	}
+	request := &http.Request{
+		Method: http.MethodGet,
+		URL:    &c.requestURL,
+		Header: c.headers.Clone(),
+		Host:   c.host,
+	}
+	request.Header.Set("Connection", "Upgrade")
+	request.Header.Set("Upgrade", "websocket")
+	err = request.Write(conn)
+	if err != nil {
+		return nil, err
+	}
+	bufReader := std_bufio.NewReader(conn)
+	response, err := http.ReadResponse(bufReader, request)
+	if err != nil {
+		return nil, err
+	}
+	if response.StatusCode != 101 ||
+		!strings.EqualFold(response.Header.Get("Connection"), "upgrade") ||
+		!strings.EqualFold(response.Header.Get("Upgrade"), "websocket") {
+		return nil, E.New("unexpected status: ", response.Status)
+	}
+	if bufReader.Buffered() > 0 {
+		buffer := buf.NewSize(bufReader.Buffered())
+		_, err = buffer.ReadFullFrom(bufReader, buffer.Len())
+		if err != nil {
+			return nil, err
+		}
+		conn = bufio.NewCachedConn(conn, buffer)
+	}
+	return conn, nil
+}

+ 139 - 0
transport/v2rayhttpupgrade/server.go

@@ -0,0 +1,139 @@
+package v2rayhttpupgrade
+
+import (
+	"context"
+	"net"
+	"net/http"
+	"os"
+	"strings"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/tls"
+	C "github.com/sagernet/sing-box/constant"
+	"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"
+	aTLS "github.com/sagernet/sing/common/tls"
+	sHttp "github.com/sagernet/sing/protocol/http"
+)
+
+var _ adapter.V2RayServerTransport = (*Server)(nil)
+
+type Server struct {
+	ctx        context.Context
+	tlsConfig  tls.ServerConfig
+	handler    adapter.V2RayServerTransportHandler
+	httpServer *http.Server
+	host       string
+	path       string
+	headers    http.Header
+}
+
+func NewServer(ctx context.Context, options option.V2RayHTTPUpgradeOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) {
+	server := &Server{
+		ctx:       ctx,
+		tlsConfig: tlsConfig,
+		handler:   handler,
+		host:      options.Host,
+		path:      options.Path,
+		headers:   options.Headers.Build(),
+	}
+	if !strings.HasPrefix(server.path, "/") {
+		server.path = "/" + server.path
+	}
+	server.httpServer = &http.Server{
+		Handler:           server,
+		ReadHeaderTimeout: C.TCPTimeout,
+		MaxHeaderBytes:    http.DefaultMaxHeaderBytes,
+		BaseContext: func(net.Listener) context.Context {
+			return ctx
+		},
+		TLSNextProto: make(map[string]func(*http.Server, *tls.STDConn, http.Handler)),
+	}
+	return server, nil
+}
+
+type httpFlusher interface {
+	FlushError() error
+}
+
+func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
+	host := request.Host
+	if len(s.host) > 0 && host != s.host {
+		s.invalidRequest(writer, request, http.StatusBadRequest, E.New("bad host: ", host))
+		return
+	}
+	if !strings.HasPrefix(request.URL.Path, s.path) {
+		s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path))
+		return
+	}
+	if request.Method != http.MethodGet {
+		s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad method: ", request.Method))
+		return
+	}
+	if !strings.EqualFold(request.Header.Get("Connection"), "upgrade") {
+		s.invalidRequest(writer, request, http.StatusNotFound, E.New("not a upgrade request"))
+		return
+	}
+	if !strings.EqualFold(request.Header.Get("Upgrade"), "websocket") {
+		s.invalidRequest(writer, request, http.StatusNotFound, E.New("not a websocket request"))
+		return
+	}
+	if request.Header.Get("Sec-WebSocket-Key") != "" {
+		s.invalidRequest(writer, request, http.StatusNotFound, E.New("real websocket request received"))
+		return
+	}
+	writer.Header().Set("Connection", "upgrade")
+	writer.Header().Set("Upgrade", "websocket")
+	writer.WriteHeader(http.StatusSwitchingProtocols)
+	if flusher, isFlusher := writer.(httpFlusher); isFlusher {
+		err := flusher.FlushError()
+		if err != nil {
+			s.invalidRequest(writer, request, http.StatusInternalServerError, E.New("flush response"))
+		}
+	}
+	hijacker, canHijack := writer.(http.Hijacker)
+	if !canHijack {
+		s.invalidRequest(writer, request, http.StatusInternalServerError, E.New("invalid connection, maybe HTTP/2"))
+		return
+	}
+	conn, _, err := hijacker.Hijack()
+	if err != nil {
+		s.invalidRequest(writer, request, http.StatusInternalServerError, E.Cause(err, "hijack failed"))
+		return
+	}
+	var metadata M.Metadata
+	metadata.Source = sHttp.SourceAddress(request)
+	s.handler.NewConnection(request.Context(), conn, metadata)
+}
+
+func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Request, statusCode int, err error) {
+	if statusCode > 0 {
+		writer.WriteHeader(statusCode)
+	}
+	s.handler.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.tlsConfig != nil {
+		if len(s.tlsConfig.NextProtos()) == 0 {
+			s.tlsConfig.SetNextProtos([]string{"http/1.1"})
+		}
+		listener = aTLS.NewListener(listener, s.tlsConfig)
+	}
+	return s.httpServer.Serve(listener)
+}
+
+func (s *Server) ServePacket(listener net.PacketConn) error {
+	return os.ErrInvalid
+}
+
+func (s *Server) Close() error {
+	return common.Close(common.PtrOrNil(s.httpServer))
+}