diff --git a/constant/v2ray.go b/constant/v2ray.go index 2243d736..c3089a6c 100644 --- a/constant/v2ray.go +++ b/constant/v2ray.go @@ -1,8 +1,9 @@ package constant const ( - V2RayTransportTypeHTTP = "http" - V2RayTransportTypeWebsocket = "ws" - V2RayTransportTypeQUIC = "quic" - V2RayTransportTypeGRPC = "grpc" + V2RayTransportTypeHTTP = "http" + V2RayTransportTypeWebsocket = "ws" + V2RayTransportTypeQUIC = "quic" + V2RayTransportTypeGRPC = "grpc" + V2RayTransportTypeHTTPUpgrade = "httpupgrade" ) diff --git a/docs/configuration/shared/v2ray-transport.md b/docs/configuration/shared/v2ray-transport.md index 4b5b6f66..418ef28d 100644 --- a/docs/configuration/shared/v2ray-transport.md +++ b/docs/configuration/shared/v2ray-transport.md @@ -15,6 +15,7 @@ Available transports: * WebSocket * QUIC * gRPC +* HTTPUpgrade !!! 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. 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. diff --git a/docs/configuration/shared/v2ray-transport.zh.md b/docs/configuration/shared/v2ray-transport.zh.md index deab5589..2ea93562 100644 --- a/docs/configuration/shared/v2ray-transport.zh.md +++ b/docs/configuration/shared/v2ray-transport.zh.md @@ -14,6 +14,7 @@ V2Ray Transport 是 v2ray 发明的一组私有协议,并污染了其他协议 * WebSocket * QUIC * gRPC +* HTTPUpgrade !!! warning "与 v2ray-core 的区别" @@ -183,3 +184,32 @@ gRPC 服务名称。 如果启用,客户端传输即使没有活动连接也会发送 keepalive ping。如果禁用,则在没有活动连接时,将忽略 `idle_timeout` 和 `ping_timeout`,并且不会发送 keepalive ping。 默认禁用。 + +### HTTPUpgrade + +```json +{ + "type": "httpupgrade", + "host": "", + "path": "", + "headers": {} +} +``` + +#### host + +主机域名。 + +默认服务器将验证。 + +#### path + +HTTP 请求路径 + +默认服务器将验证。 + +#### headers + +HTTP 请求的额外标头。 + +默认服务器将写入响应。 diff --git a/option/v2ray_transport.go b/option/v2ray_transport.go index 54b0de79..63af28a3 100644 --- a/option/v2ray_transport.go +++ b/option/v2ray_transport.go @@ -7,11 +7,12 @@ import ( ) 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 @@ -29,6 +30,8 @@ func (o V2RayTransportOptions) MarshalJSON() ([]byte, error) { v = o.QUICOptions case C.V2RayTransportTypeGRPC: v = o.GRPCOptions + case C.V2RayTransportTypeHTTPUpgrade: + v = o.HTTPUpgradeOptions default: return nil, E.New("unknown transport type: " + o.Type) } @@ -50,6 +53,8 @@ func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error { v = &o.QUICOptions case C.V2RayTransportTypeGRPC: v = &o.GRPCOptions + case C.V2RayTransportTypeHTTPUpgrade: + v = &o.HTTPUpgradeOptions default: return E.New("unknown transport type: " + o.Type) } @@ -85,3 +90,9 @@ type V2RayGRPCOptions struct { PermitWithoutStream bool `json:"permit_without_stream,omitempty"` ForceLite bool `json:"-"` // for test } + +type V2RayHTTPUpgradeOptions struct { + Host string `json:"host,omitempty"` + Path string `json:"path,omitempty"` + Headers HTTPHeader `json:"headers,omitempty"` +} diff --git a/test/go.mod b/test/go.mod index 856f10f1..c499355e 100644 --- a/test/go.mod +++ b/test/go.mod @@ -11,7 +11,7 @@ require ( github.com/docker/go-connections v0.4.0 github.com/gofrs/uuid/v5 v5.0.0 github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460 - github.com/sagernet/sing v0.2.16-0.20231021090846-8002db54c028 + github.com/sagernet/sing v0.2.16-0.20231028125948-afcc9cb766c2 github.com/sagernet/sing-dns v0.1.10 github.com/sagernet/sing-quic v0.1.3-0.20231026034240-fa3d997246b6 github.com/sagernet/sing-shadowsocks v0.2.5 @@ -40,6 +40,8 @@ require ( github.com/go-chi/render v1.0.3 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/btree v1.1.2 // indirect @@ -75,13 +77,13 @@ require ( 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.17-0.20231026060825-efd9884154a6 // indirect + github.com/sagernet/sing-tun v0.1.17-0.20231030120513-2e85725657c1 // 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 github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 // indirect - github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e // indirect github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f // indirect + github.com/sagernet/ws v0.0.0-20231030053741-7d481eb31bed // indirect github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect diff --git a/test/go.sum b/test/go.sum index bd89d8b6..c5926055 100644 --- a/test/go.sum +++ b/test/go.sum @@ -43,6 +43,10 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M= github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -127,8 +131,8 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byL 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.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 v0.2.16-0.20231028125948-afcc9cb766c2 h1:PW18IgRodvppd09d4mewYM3Hedu3PtFERN8yOqkTVk0= +github.com/sagernet/sing v0.2.16-0.20231028125948-afcc9cb766c2/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= @@ -141,8 +145,8 @@ github.com/sagernet/sing-shadowsocks2 v0.1.4 h1:vht2M8t3m5DTgXR2j24KbYOygG5aOp+M 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.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-tun v0.1.17-0.20231030120513-2e85725657c1 h1:QxC+myHDZ0BnkIEqXE0lWUzfYEVlhhQdSCo7mOMm7x4= +github.com/sagernet/sing-tun v0.1.17-0.20231030120513-2e85725657c1/go.mod h1:4ACZp3C6TDSy1rsMrfwtSyLrKPtm9Wm2eKHwhYIojbU= 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= @@ -151,10 +155,10 @@ github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 h1:Px+hN4Vzgx+iCGV github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6/go.mod h1:zovq6vTvEM6ECiqE3Eeb9rpIylPpamPcmrJ9tv0Bt0M= github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 h1:kDUqhc9Vsk5HJuhfIATJ8oQwBmpOZJuozQG7Vk88lL4= github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM= -github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e h1:7uw2njHFGE+VpWamge6o56j2RWk4omF6uLKKxMmcWvs= -github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e/go.mod h1:45TUl8+gH4SIKr4ykREbxKWTxkDlSzFENzctB1dVRRY= github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f h1:Kvo8w8Y9lzFGB/7z09MJ3TR99TFtfI/IuY87Ygcycho= github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f/go.mod h1:mySs0abhpc/gLlvhoq7HP1RzOaRmIXVeZGCh++zoApk= +github.com/sagernet/ws v0.0.0-20231030053741-7d481eb31bed h1:90a510OeE9siSJoYsI8nSjPmA+u5ROMDts/ZkdNsuXY= +github.com/sagernet/ws v0.0.0-20231030053741-7d481eb31bed/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg= github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s= github.com/spyzhov/ajson v0.9.0 h1:tF46gJGOenYVj+k9K1U1XpCxVWhmiyY5PsVCAs1+OJ0= @@ -222,6 +226,7 @@ golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/test/v2ray_httpupgrade_test.go b/test/v2ray_httpupgrade_test.go new file mode 100644 index 00000000..9a79aa3a --- /dev/null +++ b/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, + }) + }) +} diff --git a/transport/v2ray/transport.go b/transport/v2ray/transport.go index 3481c852..deb8a7f0 100644 --- a/transport/v2ray/transport.go +++ b/transport/v2ray/transport.go @@ -8,6 +8,7 @@ import ( 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/v2rayhttpupgrade" "github.com/sagernet/sing-box/transport/v2raywebsocket" E "github.com/sagernet/sing/common/exceptions" 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) case C.V2RayTransportTypeGRPC: return NewGRPCServer(ctx, options.GRPCOptions, tlsConfig, handler) + case C.V2RayTransportTypeHTTPUpgrade: + return v2rayhttpupgrade.NewServer(ctx, options.HTTPUpgradeOptions, tlsConfig, handler) default: return nil, E.New("unknown transport type: " + options.Type) } @@ -50,13 +53,14 @@ func NewClientTransport(ctx context.Context, dialer N.Dialer, serverAddr M.Socks case C.V2RayTransportTypeGRPC: return NewGRPCClient(ctx, dialer, serverAddr, options.GRPCOptions, tlsConfig) case C.V2RayTransportTypeWebsocket: - return v2raywebsocket.NewClient(ctx, dialer, serverAddr, options.WebsocketOptions, tlsConfig), nil + return v2raywebsocket.NewClient(ctx, dialer, serverAddr, options.WebsocketOptions, tlsConfig) case C.V2RayTransportTypeQUIC: if tlsConfig == nil { return nil, C.ErrTLSRequired } return NewQUICClient(ctx, dialer, serverAddr, options.QUICOptions, tlsConfig) - + case C.V2RayTransportTypeHTTPUpgrade: + return v2rayhttpupgrade.NewClient(ctx, dialer, serverAddr, options.HTTPUpgradeOptions, tlsConfig) default: return nil, E.New("unknown transport type: " + options.Type) } diff --git a/transport/v2raygrpclite/client.go b/transport/v2raygrpclite/client.go index 8480ac53..588a8133 100644 --- a/transport/v2raygrpclite/client.go +++ b/transport/v2raygrpclite/client.go @@ -100,7 +100,7 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { conn.setup(nil, err) } else if response.StatusCode != 200 { response.Body.Close() - conn.setup(nil, E.New("unexpected status: ", response.StatusCode, " ", response.Status)) + conn.setup(nil, E.New("unexpected status: ", response.Status)) } else { conn.setup(response.Body, nil) } diff --git a/transport/v2raygrpclite/server.go b/transport/v2raygrpclite/server.go index a3025ca6..6d3e42eb 100644 --- a/transport/v2raygrpclite/server.go +++ b/transport/v2raygrpclite/server.go @@ -35,10 +35,6 @@ type Server struct { 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) { server := &Server{ 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)) } +func (s *Server) Network() []string { + return []string{N.NetworkTCP} +} + func (s *Server) Serve(listener net.Listener) error { if s.tlsConfig != nil { if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) { diff --git a/transport/v2rayhttp/client.go b/transport/v2rayhttp/client.go index d5e8a1f6..333784fc 100644 --- a/transport/v2rayhttp/client.go +++ b/transport/v2rayhttp/client.go @@ -81,7 +81,7 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt uri.Path = options.Path err := sHTTP.URLSetPath(&uri, options.Path) if err != nil { - return nil, E.New("failed to set path: " + err.Error()) + return nil, E.New("parse path: " + err.Error()) } client.url = &uri return client, nil @@ -143,7 +143,7 @@ func (c *Client) dialHTTP2(ctx context.Context) (net.Conn, error) { conn.Setup(nil, err) } else if response.StatusCode != 200 { response.Body.Close() - conn.Setup(nil, E.New("unexpected status: ", response.StatusCode, " ", response.Status)) + conn.Setup(nil, E.New("unexpected status: ", response.Status)) } else { conn.Setup(response.Body, nil) } diff --git a/transport/v2rayhttp/server.go b/transport/v2rayhttp/server.go index dcfb07a6..ef5fffcd 100644 --- a/transport/v2rayhttp/server.go +++ b/transport/v2rayhttp/server.go @@ -40,10 +40,6 @@ type Server struct { 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) { server := &Server{ 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)) } +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 { diff --git a/transport/v2rayhttpupgrade/client.go b/transport/v2rayhttpupgrade/client.go new file mode 100644 index 00000000..c10e1b8f --- /dev/null +++ b/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 +} diff --git a/transport/v2rayhttpupgrade/server.go b/transport/v2rayhttpupgrade/server.go new file mode 100644 index 00000000..30a065c8 --- /dev/null +++ b/transport/v2rayhttpupgrade/server.go @@ -0,0 +1,137 @@ +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 +} + +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 + } + 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 + } + response := &http.Response{ + StatusCode: 101, + Header: s.headers.Clone(), + } + response.Header.Set("Connection", "upgrade") + response.Header.Set("Upgrade", "websocket") + err = response.Write(conn) + if err != nil { + s.invalidRequest(writer, request, http.StatusInternalServerError, E.Cause(err, "write response 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)) +} diff --git a/transport/v2raywebsocket/client.go b/transport/v2raywebsocket/client.go index dbab463a..d330ae53 100644 --- a/transport/v2raywebsocket/client.go +++ b/transport/v2raywebsocket/client.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing-box/common/tls" 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" sHTTP "github.com/sagernet/sing/protocol/http" @@ -30,7 +31,7 @@ type Client struct { earlyDataHeaderName string } -func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayWebsocketOptions, tlsConfig tls.Config) adapter.V2RayClientTransport { +func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayWebsocketOptions, tlsConfig tls.Config) (*Client, error) { if tlsConfig != nil { if len(tlsConfig.NextProtos()) == 0 { tlsConfig.SetNextProtos([]string{"http/1.1"}) @@ -46,7 +47,7 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt requestURL.Path = options.Path err := sHTTP.URLSetPath(&requestURL, options.Path) if err != nil { - return nil + return nil, E.Cause(err, "parse path") } if !strings.HasPrefix(requestURL.Path, "/") { requestURL.Path = "/" + requestURL.Path @@ -63,7 +64,7 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt headers, options.MaxEarlyData, options.EarlyDataHeaderName, - } + }, nil } func (c *Client) dialContext(ctx context.Context, requestURL *url.URL, headers http.Header) (*WebsocketConn, error) {