diff --git a/constant/err.go b/constant/err.go new file mode 100644 index 00000000..881e155a --- /dev/null +++ b/constant/err.go @@ -0,0 +1,5 @@ +package constant + +import E "github.com/sagernet/sing/common/exceptions" + +var ErrTLSRequired = E.New("TLS required") diff --git a/constant/v2ray.go b/constant/v2ray.go index d5d5e7ba..2243d736 100644 --- a/constant/v2ray.go +++ b/constant/v2ray.go @@ -1,7 +1,8 @@ package constant const ( - V2RayTransportTypeGRPC = "grpc" + V2RayTransportTypeHTTP = "http" V2RayTransportTypeWebsocket = "ws" V2RayTransportTypeQUIC = "quic" + V2RayTransportTypeGRPC = "grpc" ) diff --git a/inbound/hysteria.go b/inbound/hysteria.go index 585f034d..47e4d352 100644 --- a/inbound/hysteria.go +++ b/inbound/hysteria.go @@ -114,7 +114,7 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL udpSessions: make(map[uint32]chan *hysteria.UDPMessage), } if options.TLS == nil || !options.TLS.Enabled { - return nil, errTLSRequired + return nil, C.ErrTLSRequired } if len(options.TLS.ALPN) == 0 { options.TLS.ALPN = []string{hysteria.DefaultALPN} diff --git a/inbound/naive.go b/inbound/naive.go index dc8e2cb8..e08d7367 100644 --- a/inbound/naive.go +++ b/inbound/naive.go @@ -44,8 +44,6 @@ type Naive struct { h3Server any } -var errTLSRequired = E.New("TLS required") - func NewNaive(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.NaiveInboundOptions) (*Naive, error) { inbound := &Naive{ ctx: ctx, @@ -57,7 +55,7 @@ func NewNaive(ctx context.Context, router adapter.Router, logger log.ContextLogg authenticator: auth.NewAuthenticator(options.Users), } if options.TLS == nil || !options.TLS.Enabled { - return nil, errTLSRequired + return nil, C.ErrTLSRequired } if len(options.Users) == 0 { return nil, E.New("missing users") diff --git a/inbound/vmess.go b/inbound/vmess.go index a3f14f97..eb04fbdf 100644 --- a/inbound/vmess.go +++ b/inbound/vmess.go @@ -63,9 +63,13 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg } } if options.Transport != nil { - inbound.transport, err = v2ray.NewServerTransport(ctx, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig.Config(), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newTransportConnection, nil, nil), inbound) + var tlsConfig *tls.Config + if inbound.tlsConfig != nil { + tlsConfig = inbound.tlsConfig.Config() + } + inbound.transport, err = v2ray.NewServerTransport(ctx, common.PtrValueOrDefault(options.Transport), tlsConfig, adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newTransportConnection, nil, nil), inbound) if err != nil { - return nil, err + return nil, E.Cause(err, "create server transport: ", options.Transport.Type) } } inbound.connHandler = inbound @@ -75,7 +79,7 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg func (h *VMess) Start() error { err := common.Start( h.service, - h.tlsConfig, + common.PtrOrNil(h.tlsConfig), ) if err != nil { return err diff --git a/option/v2ray_transport.go b/option/v2ray_transport.go index bc1567ff..6c45b178 100644 --- a/option/v2ray_transport.go +++ b/option/v2ray_transport.go @@ -8,9 +8,10 @@ import ( type _V2RayTransportOptions struct { Type string `json:"type,omitempty"` - GRPCOptions V2RayGRPCOptions `json:"-"` + HTTPOptions V2RayHTTPOptions `json:"-"` WebsocketOptions V2RayWebsocketOptions `json:"-"` QUICOptions V2RayQUICOptions `json:"-"` + GRPCOptions V2RayGRPCOptions `json:"-"` } type V2RayTransportOptions _V2RayTransportOptions @@ -20,12 +21,14 @@ func (o V2RayTransportOptions) MarshalJSON() ([]byte, error) { switch o.Type { case "": return nil, nil - case C.V2RayTransportTypeGRPC: - v = o.GRPCOptions + case C.V2RayTransportTypeHTTP: + v = o.HTTPOptions case C.V2RayTransportTypeWebsocket: v = o.WebsocketOptions case C.V2RayTransportTypeQUIC: v = o.QUICOptions + case C.V2RayTransportTypeGRPC: + v = o.GRPCOptions default: return nil, E.New("unknown transport type: " + o.Type) } @@ -39,12 +42,14 @@ func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error { } var v any switch o.Type { - case C.V2RayTransportTypeGRPC: - v = &o.GRPCOptions + case C.V2RayTransportTypeHTTP: + v = &o.HTTPOptions case C.V2RayTransportTypeWebsocket: v = &o.WebsocketOptions case C.V2RayTransportTypeQUIC: v = &o.QUICOptions + case C.V2RayTransportTypeGRPC: + v = &o.GRPCOptions default: return E.New("unknown transport type: " + o.Type) } @@ -55,52 +60,11 @@ func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error { return nil } -/*type _V2RayOutboundTransportOptions struct { - Type string `json:"type,omitempty"` - GRPCOptions V2RayGRPCOptions `json:"-"` - WebsocketOptions V2RayWebsocketOptions `json:"-"` -} - -type V2RayOutboundTransportOptions _V2RayOutboundTransportOptions - -func (o V2RayOutboundTransportOptions) MarshalJSON() ([]byte, error) { - var v any - switch o.Type { - case "": - return nil, nil - case C.V2RayTransportTypeGRPC: - v = o.GRPCOptions - case C.V2RayTransportTypeWebsocket: - v = o.WebsocketOptions - default: - return nil, E.New("unknown transport type: " + o.Type) - } - return MarshallObjects((_V2RayOutboundTransportOptions)(o), v) -} - -func (o *V2RayOutboundTransportOptions) UnmarshalJSON(bytes []byte) error { - err := json.Unmarshal(bytes, (*_V2RayOutboundTransportOptions)(o)) - if err != nil { - return err - } - var v any - switch o.Type { - case C.V2RayTransportTypeGRPC: - v = &o.GRPCOptions - case C.V2RayTransportTypeWebsocket: - v = &o.WebsocketOptions - default: - return E.New("unknown transport type: " + o.Type) - } - err = UnmarshallExcluded(bytes, (*_V2RayOutboundTransportOptions)(o), v) - if err != nil { - return E.Cause(err, "vmess transport options") - } - return nil -}*/ - -type V2RayGRPCOptions struct { - ServiceName string `json:"service_name,omitempty"` +type V2RayHTTPOptions struct { + Host Listable[string] `json:"host,omitempty"` + Path string `json:"path,omitempty"` + Method string `json:"method,omitempty"` + Headers map[string]string `json:"headers,omitempty"` } type V2RayWebsocketOptions struct { @@ -111,3 +75,7 @@ type V2RayWebsocketOptions struct { } type V2RayQUICOptions struct{} + +type V2RayGRPCOptions struct { + ServiceName string `json:"service_name,omitempty"` +} diff --git a/outbound/hysteria.go b/outbound/hysteria.go index 6fa13230..27b2d872 100644 --- a/outbound/hysteria.go +++ b/outbound/hysteria.go @@ -43,11 +43,9 @@ type Hysteria struct { udpDefragger hysteria.Defragger } -var errTLSRequired = E.New("TLS required") - func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (*Hysteria, error) { if options.TLS == nil || !options.TLS.Enabled { - return nil, errTLSRequired + return nil, C.ErrTLSRequired } tlsConfig, err := dialer.TLSConfig(options.Server, common.PtrValueOrDefault(options.TLS)) if err != nil { diff --git a/outbound/vmess.go b/outbound/vmess.go index 4e49599b..ec749616 100644 --- a/outbound/vmess.go +++ b/outbound/vmess.go @@ -64,7 +64,7 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg if options.Transport != nil { outbound.transport, err = v2ray.NewClientTransport(ctx, outbound.dialer, outbound.serverAddr, common.PtrValueOrDefault(options.Transport), outbound.tlsConfig) if err != nil { - return nil, err + return nil, E.Cause(err, "create client transport: ", options.Transport.Type) } } outbound.multiplexDialer, err = mux.NewClientWithOptions(ctx, (*vmessDialer)(outbound), common.PtrValueOrDefault(options.Multiplex)) diff --git a/test/vmess_transport_test.go b/test/vmess_transport_test.go index ae050b55..b5df62ce 100644 --- a/test/vmess_transport_test.go +++ b/test/vmess_transport_test.go @@ -45,13 +45,95 @@ func TestVMessWebscoketSelf(t *testing.T) { }) } -func TestVMessQUICSelf(t *testing.T) { +func TestVMessHTTPSelf(t *testing.T) { testVMessWebscoketSelf(t, &option.V2RayTransportOptions{ - Type: C.V2RayTransportTypeQUIC, + Type: C.V2RayTransportTypeHTTP, }) } 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", + }, + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + MixedOptions: option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeVMess, + VMessOptions: option.VMessInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: serverPort, + }, + Users: []option.VMessUser{ + { + Name: "sekai", + UUID: user.String(), + }, + }, + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + Transport: transport, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeVMess, + Tag: "vmess-out", + VMessOptions: option.VMessOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: user.String(), + Security: "zero", + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + Transport: transport, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + DefaultOptions: option.DefaultRule{ + Inbound: []string{"mixed-in"}, + Outbound: "vmess-out", + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestVMessQUICSelf(t *testing.T) { + transport := &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeQUIC, + } user, err := uuid.DefaultGenerator.NewV4() require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") diff --git a/transport/v2ray/transport.go b/transport/v2ray/transport.go index f136dbe8..9c726354 100644 --- a/transport/v2ray/transport.go +++ b/transport/v2ray/transport.go @@ -7,6 +7,7 @@ import ( "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/v2rayhttp" "github.com/sagernet/sing-box/transport/v2raywebsocket" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" @@ -18,12 +19,20 @@ func NewServerTransport(ctx context.Context, options option.V2RayTransportOption return nil, nil } switch options.Type { - case C.V2RayTransportTypeGRPC: - return NewGRPCServer(ctx, options.GRPCOptions, tlsConfig, handler) + case C.V2RayTransportTypeHTTP: + return v2rayhttp.NewServer(ctx, options.HTTPOptions, tlsConfig, handler, errorHandler), nil case C.V2RayTransportTypeWebsocket: return v2raywebsocket.NewServer(ctx, options.WebsocketOptions, tlsConfig, handler, errorHandler), nil case C.V2RayTransportTypeQUIC: + if tlsConfig == nil { + return nil, C.ErrTLSRequired + } return NewQUICServer(ctx, options.QUICOptions, tlsConfig, handler, errorHandler) + case C.V2RayTransportTypeGRPC: + if tlsConfig == nil { + return nil, C.ErrTLSRequired + } + return NewGRPCServer(ctx, options.GRPCOptions, tlsConfig, handler) default: return nil, E.New("unknown transport type: " + options.Type) } @@ -34,12 +43,24 @@ func NewClientTransport(ctx context.Context, dialer N.Dialer, serverAddr M.Socks return nil, nil } switch options.Type { + case C.V2RayTransportTypeHTTP: + if tlsConfig == nil { + return nil, C.ErrTLSRequired + } + return v2rayhttp.NewClient(ctx, dialer, serverAddr, options.HTTPOptions, tlsConfig), nil case C.V2RayTransportTypeGRPC: + if tlsConfig == nil { + return nil, C.ErrTLSRequired + } return NewGRPCClient(ctx, dialer, serverAddr, options.GRPCOptions, tlsConfig) case C.V2RayTransportTypeWebsocket: return v2raywebsocket.NewClient(ctx, dialer, serverAddr, options.WebsocketOptions, tlsConfig), nil case C.V2RayTransportTypeQUIC: + if tlsConfig == nil { + return nil, C.ErrTLSRequired + } return NewQUICClient(ctx, dialer, serverAddr, options.QUICOptions, tlsConfig) + default: return nil, E.New("unknown transport type: " + options.Type) } diff --git a/transport/v2rayhttp/client.go b/transport/v2rayhttp/client.go new file mode 100644 index 00000000..465a7adb --- /dev/null +++ b/transport/v2rayhttp/client.go @@ -0,0 +1,100 @@ +package v2rayhttp + +import ( + "context" + "crypto/tls" + "io" + "math/rand" + "net" + "net/http" + "net/url" + "strings" + + "github.com/sagernet/sing-box/adapter" + "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" +) + +var _ adapter.V2RayClientTransport = (*Client)(nil) + +type Client struct { + ctx context.Context + client *http.Client + url *url.URL + host []string + method string + headers http.Header +} + +func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayHTTPOptions, tlsConfig *tls.Config) adapter.V2RayClientTransport { + client := &Client{ + ctx: ctx, + host: options.Host, + method: options.Method, + headers: make(http.Header), + client: &http.Client{ + 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, + }, + }, + } + if client.method == "" { + client.method = "PUT" + } + var uri url.URL + if tlsConfig == nil { + uri.Scheme = "http" + } else { + uri.Scheme = "https" + } + uri.Host = serverAddr.String() + uri.Path = options.Path + if !strings.HasPrefix(uri.Path, "/") { + uri.Path = "/" + uri.Path + } + for key, value := range options.Headers { + client.headers.Set(key, value) + } + client.url = &uri + return client +} + +func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { + pipeInReader, pipeInWriter := io.Pipe() + request := &http.Request{ + Method: c.method, + Body: pipeInReader, + URL: c.url, + ProtoMajor: 2, + ProtoMinor: 0, + Proto: "HTTP/2", + Header: c.headers.Clone(), + } + switch hostLen := len(c.host); hostLen { + case 0: + case 1: + request.Host = c.host[0] + default: + request.Host = c.host[rand.Intn(hostLen)] + } + // Disable any compression method from server. + request.Header.Set("Accept-Encoding", "identity") + response, err := c.client.Do(request) // nolint: bodyclose + if err != nil { + pipeInWriter.Close() + return nil, err + } + if response.StatusCode != 200 { + return nil, E.New("unexpected status: ", response.StatusCode, " ", response.Status) + } + return &HTTPConn{ + response.Body, + pipeInWriter, + }, nil +} diff --git a/transport/v2rayhttp/conn.go b/transport/v2rayhttp/conn.go new file mode 100644 index 00000000..61d2ad01 --- /dev/null +++ b/transport/v2rayhttp/conn.go @@ -0,0 +1,61 @@ +package v2rayhttp + +import ( + "io" + "net" + "net/http" + "os" + "time" + + "github.com/sagernet/sing/common" +) + +type HTTPConn struct { + reader io.Reader + writer io.Writer +} + +func (c *HTTPConn) Read(b []byte) (n int, err error) { + return c.reader.Read(b) +} + +func (c *HTTPConn) Write(b []byte) (n int, err error) { + return c.writer.Write(b) +} + +func (c *HTTPConn) Close() error { + return common.Close(c.reader, c.writer) +} + +func (c *HTTPConn) LocalAddr() net.Addr { + return nil +} + +func (c *HTTPConn) RemoteAddr() net.Addr { + return nil +} + +func (c *HTTPConn) SetDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *HTTPConn) SetReadDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *HTTPConn) SetWriteDeadline(t time.Time) error { + return os.ErrInvalid +} + +type ServerHTTPConn struct { + HTTPConn + flusher http.Flusher +} + +func (c *ServerHTTPConn) Write(b []byte) (n int, err error) { + n, err = c.writer.Write(b) + if err == nil { + c.flusher.Flush() + } + return +} diff --git a/transport/v2rayhttp/server.go b/transport/v2rayhttp/server.go new file mode 100644 index 00000000..a42c5d49 --- /dev/null +++ b/transport/v2rayhttp/server.go @@ -0,0 +1,146 @@ +package v2rayhttp + +import ( + "context" + "crypto/tls" + "net" + "net/http" + "os" + "strings" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "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" +) + +var _ adapter.V2RayServerTransport = (*Server)(nil) + +type Server struct { + ctx context.Context + handler N.TCPConnectionHandler + errorHandler E.Handler + httpServer *http.Server + host []string + path string + method string + headers http.Header +} + +func (s *Server) Network() []string { + return []string{N.NetworkTCP} +} + +func NewServer(ctx context.Context, options option.V2RayHTTPOptions, tlsConfig *tls.Config, handler N.TCPConnectionHandler, errorHandler E.Handler) *Server { + server := &Server{ + ctx: ctx, + handler: handler, + errorHandler: errorHandler, + host: options.Host, + path: options.Path, + method: options.Method, + headers: make(http.Header), + } + if server.method == "" { + server.method = "PUT" + } + if !strings.HasPrefix(server.path, "/") { + server.path = "/" + server.path + } + for key, value := range options.Headers { + server.headers.Set(key, value) + } + server.httpServer = &http.Server{ + Handler: server, + ReadHeaderTimeout: C.TCPTimeout, + MaxHeaderBytes: http.DefaultMaxHeaderBytes, + TLSConfig: tlsConfig, + } + return server +} + +func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + host := request.Host + if len(s.host) > 0 && !common.Contains(s.host, host) { + writer.WriteHeader(http.StatusBadRequest) + s.badRequest(request, E.New("bad host: ", host)) + return + } + if !strings.HasPrefix(request.URL.Path, s.path) { + writer.WriteHeader(http.StatusNotFound) + s.badRequest(request, E.New("bad path: ", request.URL.Path)) + return + } + if request.Method != s.method { + writer.WriteHeader(http.StatusNotFound) + s.badRequest(request, E.New("bad method: ", request.Method)) + return + } + + writer.Header().Set("Cache-Control", "no-store") + + for key, values := range s.headers { + for _, value := range values { + writer.Header().Set(key, value) + } + } + + writer.WriteHeader(http.StatusOK) + if f, ok := writer.(http.Flusher); ok { + f.Flush() + } + + if h, ok := writer.(http.Hijacker); ok { + conn, reader, err := h.Hijack() + if err != nil { + writer.WriteHeader(http.StatusInternalServerError) + s.badRequest(request, E.Cause(err, "hijack conn")) + return + } + if reader.Available() > 0 { + buffer := buf.NewSize(reader.Available()) + _, err = buffer.ReadFullFrom(reader, buffer.FreeLen()) + if err != nil { + writer.WriteHeader(http.StatusInternalServerError) + s.badRequest(request, E.Cause(err, "read cached data")) + return + } + conn = bufio.NewCachedConn(conn, buffer) + } + s.handler.NewConnection(request.Context(), conn, M.Metadata{}) + } else { + conn := &ServerHTTPConn{ + HTTPConn{ + request.Body, + writer, + }, + writer.(http.Flusher), + } + s.handler.NewConnection(request.Context(), conn, M.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)) +} diff --git a/transport/v2raywebsocket/server.go b/transport/v2raywebsocket/server.go index 4c00f954..9b30f9bf 100644 --- a/transport/v2raywebsocket/server.go +++ b/transport/v2raywebsocket/server.go @@ -143,5 +143,5 @@ func (s *Server) ServePacket(listener net.PacketConn) error { } func (s *Server) Close() error { - return common.Close(s.httpServer) + return common.Close(common.PtrOrNil(s.httpServer)) }