Fix V2Ray transport path validation behavior

This commit is contained in:
世界 2023-12-23 10:54:25 +08:00
parent a0cab4f563
commit 082e3fb8df
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
5 changed files with 76 additions and 40 deletions

View file

@ -53,9 +53,15 @@ The client will choose randomly and the server will verify if not empty.
#### path #### path
!!! warning
V2Ray's documentation says that the path between the server and the client must be consistent,
but the actual code allows the client to add any suffix to the path.
sing-box uses the same behavior as V2Ray, but note that the behavior does not exist in `WebSocket` and `HTTPUpgrade` transport.
Path of HTTP request. Path of HTTP request.
The server will verify if not empty. The server will verify.
#### method #### method
@ -77,7 +83,10 @@ Specifies the time until idle clients should be closed with a GOAWAY frame. PING
In HTTP2 client: In HTTP2 client:
Specifies the period of time after which a health check will be performed using a ping frame if no frames have been received on the connection. Please note that a ping response is considered a received frame, so if there is no other traffic on the connection, the health check will be executed every interval. If the value is zero, no health check will be performed. Specifies the period of time after which a health check will be performed using a ping frame if no frames have been
received on the connection.Please note that a ping response is considered a received frame, so if there is no other
traffic on the connection, the health check will be executed every interval. If the value is zero, no health check will
be performed.
Zero is used by default. Zero is used by default.
@ -85,7 +94,9 @@ Zero is used by default.
In HTTP2 client: In HTTP2 client:
Specifies the timeout duration after sending a PING frame, within which a response must be received. If a response to the PING frame is not received within the specified timeout duration, the connection will be closed. The default timeout duration is 15 seconds. Specifies the timeout duration after sending a PING frame, within which a response must be received.
If a response to the PING frame is not received within the specified timeout duration, the connection will be closed.
The default timeout duration is 15 seconds.
### WebSocket ### WebSocket
@ -103,12 +114,14 @@ Specifies the timeout duration after sending a PING frame, within which a respon
Path of HTTP request. Path of HTTP request.
The server will verify if not empty. The server will verify.
#### headers #### headers
Extra headers of HTTP request. Extra headers of HTTP request.
The server will write in response if not empty.
#### max_early_data #### max_early_data
Allowed payload size is in the request. Enabled if not zero. Allowed payload size is in the request. Enabled if not zero.
@ -162,7 +175,8 @@ Service name of gRPC.
In standard gRPC server/client: In standard gRPC server/client:
If the transport doesn't see any activity after a duration of this time, it pings the client to check if the connection is still active. If the transport doesn't see any activity after a duration of this time,
it pings the client to check if the connection is still active.
In default gRPC server/client: In default gRPC server/client:
@ -172,7 +186,8 @@ It has the same behavior as the corresponding setting in HTTP transport.
In standard gRPC server/client: In standard gRPC server/client:
The timeout that after performing a keepalive check, the client will wait for activity. If no activity is detected, the connection will be closed. The timeout that after performing a keepalive check, the client will wait for activity.
If no activity is detected, the connection will be closed.
In default gRPC server/client: In default gRPC server/client:
@ -182,7 +197,9 @@ It has the same behavior as the corresponding setting in HTTP transport.
In standard gRPC client: 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.
@ -207,7 +224,7 @@ The server will verify if not empty.
Path of HTTP request. Path of HTTP request.
The server will verify if not empty. The server will verify.
#### headers #### headers

View file

@ -48,25 +48,30 @@ V2Ray Transport 是 v2ray 发明的一组私有协议,并污染了其他协议
主机域名列表。 主机域名列表。
客户端将随机选择,默认服务器将验证。 如果设置,客户端将随机选择,服务器将验证。
#### path #### path
!!! warning
V2Ray 文档称服务端和客户端的路径必须一致,但实际代码允许客户端向路径添加任何后缀。
sing-box 使用与 V2Ray 相同的行为,但请注意,该行为在 `WebSocket``HTTPUpgrade` 传输层中不存在。
HTTP 请求路径 HTTP 请求路径
默认服务器将验证。 服务器将验证。
#### method #### method
HTTP 请求方法 HTTP 请求方法
默认服务器将验证。 如果设置,服务器将验证。
#### headers #### headers
HTTP 请求的额外标头 HTTP 请求的额外标头
默认服务器将写入响应。 如果设置,服务器将写入响应。
#### idle_timeout #### idle_timeout
@ -102,11 +107,13 @@ HTTP 请求的额外标头
HTTP 请求路径 HTTP 请求路径
默认服务器将验证。 服务器将验证。
#### headers #### headers
HTTP 请求的额外标头。 HTTP 请求的额外标头
如果设置,服务器将写入响应。
#### max_early_data #### max_early_data
@ -200,16 +207,16 @@ gRPC 服务名称。
主机域名。 主机域名。
默认服务器将验证。 服务器将验证。
#### path #### path
HTTP 请求路径 HTTP 请求路径
默认服务器将验证。 服务器将验证。
#### headers #### headers
HTTP 请求的额外标头。 HTTP 请求的额外标头。
默认服务器将写入响应。 如果设置,服务器将写入响应。

View file

@ -7,6 +7,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@ -28,7 +29,7 @@ type Client struct {
serverAddr M.Socksaddr serverAddr M.Socksaddr
transport http.RoundTripper transport http.RoundTripper
http2 bool http2 bool
url *url.URL requestURL url.URL
host []string host []string
method string method string
headers http.Header headers http.Header
@ -58,33 +59,35 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
}, },
} }
} }
client := &Client{ if options.Method == "" {
options.Method = http.MethodPut
}
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
}
return &Client{
ctx: ctx, ctx: ctx,
dialer: dialer, dialer: dialer,
serverAddr: serverAddr, serverAddr: serverAddr,
requestURL: requestURL,
host: options.Host, host: options.Host,
method: options.Method, method: options.Method,
headers: options.Headers.Build(), headers: options.Headers.Build(),
transport: transport, transport: transport,
http2: tlsConfig != nil, http2: tlsConfig != nil,
} }, nil
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
err := sHTTP.URLSetPath(&uri, options.Path)
if err != nil {
return nil, E.Cause(err, "parse path")
}
client.url = &uri
return client, nil
} }
func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
@ -103,7 +106,7 @@ func (c *Client) dialHTTP(ctx context.Context) (net.Conn, error) {
request := &http.Request{ request := &http.Request{
Method: c.method, Method: c.method,
URL: c.url, URL: &c.requestURL,
Header: c.headers.Clone(), Header: c.headers.Clone(),
} }
switch hostLen := len(c.host); hostLen { switch hostLen := len(c.host); hostLen {
@ -123,7 +126,7 @@ func (c *Client) dialHTTP2(ctx context.Context) (net.Conn, error) {
request := &http.Request{ request := &http.Request{
Method: c.method, Method: c.method,
Body: pipeInReader, Body: pipeInReader,
URL: c.url, URL: &c.requestURL,
Header: c.headers.Clone(), Header: c.headers.Clone(),
} }
request = request.WithContext(ctx) request = request.WithContext(ctx)

View file

@ -65,7 +65,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
s.invalidRequest(writer, request, http.StatusBadRequest, E.New("bad host: ", host)) s.invalidRequest(writer, request, http.StatusBadRequest, E.New("bad host: ", host))
return return
} }
if !strings.HasPrefix(request.URL.Path, s.path) { if request.URL.Path != s.path {
s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path)) s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path))
return return
} }

View file

@ -33,6 +33,7 @@ type Server struct {
path string path string
maxEarlyData uint32 maxEarlyData uint32
earlyDataHeaderName string earlyDataHeaderName string
upgrader ws.HTTPUpgrader
} }
func NewServer(ctx context.Context, options option.V2RayWebsocketOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { func NewServer(ctx context.Context, options option.V2RayWebsocketOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) {
@ -43,6 +44,10 @@ func NewServer(ctx context.Context, options option.V2RayWebsocketOptions, tlsCon
path: options.Path, path: options.Path,
maxEarlyData: options.MaxEarlyData, maxEarlyData: options.MaxEarlyData,
earlyDataHeaderName: options.EarlyDataHeaderName, earlyDataHeaderName: options.EarlyDataHeaderName,
upgrader: ws.HTTPUpgrader{
Timeout: C.TCPTimeout,
Header: options.Headers.Build(),
},
} }
if !strings.HasPrefix(server.path, "/") { if !strings.HasPrefix(server.path, "/") {
server.path = "/" + server.path server.path = "/" + server.path
@ -79,6 +84,10 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
return return
} }
} else { } else {
if request.URL.Path != s.path {
s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path))
return
}
earlyDataStr := request.Header.Get(s.earlyDataHeaderName) earlyDataStr := request.Header.Get(s.earlyDataHeaderName)
if earlyDataStr != "" { if earlyDataStr != "" {
earlyData, err = base64.RawURLEncoding.DecodeString(earlyDataStr) earlyData, err = base64.RawURLEncoding.DecodeString(earlyDataStr)