mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-11-10 13:14:06 +08:00
Rewrite HTTP proxy protocol
This commit is contained in:
49 files changed
+5306
-138
No files matched your search
@@ -0,0 +1,36 @@
|
||||
package badhttp
|
||||
|
||||
import (
|
||||
std_bufio "bufio"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
_ "unsafe"
|
||||
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
)
|
||||
|
||||
//go:linkname ReadRequest net/http.readRequest
|
||||
func ReadRequest(b *std_bufio.Reader) (req *http.Request, err error)
|
||||
|
||||
//go:linkname URLSetPath net/url.(*URL).setPath
|
||||
func URLSetPath(u *url.URL, p string) error
|
||||
|
||||
//go:linkname ParseBasicAuth net/http.parseBasicAuth
|
||||
func ParseBasicAuth(auth string) (username, password string, valid bool)
|
||||
|
||||
func SourceAddress(request *http.Request) M.Socksaddr {
|
||||
return ForwardedSource(request, M.ParseSocksaddr(request.RemoteAddr).Unwrap())
|
||||
}
|
||||
|
||||
func ForwardedSource(request *http.Request, source M.Socksaddr) M.Socksaddr {
|
||||
for _, value := range request.Header.Values("X-Forwarded-For") {
|
||||
for from := range strings.SplitSeq(value, ",") {
|
||||
address := M.ParseAddr(strings.TrimSpace(from))
|
||||
if address.IsValid() {
|
||||
return M.Socksaddr{Addr: address, Port: source.Port}.Unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
return source
|
||||
}
|
||||
@@ -46,21 +46,7 @@ func newHTTP3RoundTripper(
|
||||
if baseTLSConfig != nil {
|
||||
handshakeTimeout = baseTLSConfig.HandshakeTimeout()
|
||||
}
|
||||
quicConfig := &quic.Config{
|
||||
InitialStreamReceiveWindow: options.StreamReceiveWindow.Value(),
|
||||
MaxStreamReceiveWindow: options.StreamReceiveWindow.Value(),
|
||||
InitialConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(),
|
||||
MaxConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(),
|
||||
KeepAlivePeriod: time.Duration(options.KeepAlivePeriod),
|
||||
MaxIdleTimeout: time.Duration(options.IdleTimeout),
|
||||
DisablePathMTUDiscovery: options.DisablePathMTUDiscovery,
|
||||
}
|
||||
if options.InitialPacketSize > 0 {
|
||||
quicConfig.InitialPacketSize = uint16(options.InitialPacketSize)
|
||||
}
|
||||
if options.MaxConcurrentStreams > 0 {
|
||||
quicConfig.MaxIncomingStreams = int64(options.MaxConcurrentStreams)
|
||||
}
|
||||
quicConfig := NewQUICConfig(options)
|
||||
if handshakeTimeout > 0 {
|
||||
quicConfig.HandshakeIdleTimeout = handshakeTimeout
|
||||
}
|
||||
@@ -343,3 +329,22 @@ func (t *http3FallbackTransport) markH3Broken(authority string) {
|
||||
entry.until = time.Now().Add(entry.backoff)
|
||||
t.broken[authority] = entry
|
||||
}
|
||||
|
||||
func NewQUICConfig(options option.QUICOptions) *quic.Config {
|
||||
quicConfig := &quic.Config{
|
||||
InitialStreamReceiveWindow: options.StreamReceiveWindow.Value(),
|
||||
MaxStreamReceiveWindow: options.StreamReceiveWindow.Value(),
|
||||
InitialConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(),
|
||||
MaxConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(),
|
||||
KeepAlivePeriod: time.Duration(options.KeepAlivePeriod),
|
||||
MaxIdleTimeout: time.Duration(options.IdleTimeout),
|
||||
DisablePathMTUDiscovery: options.DisablePathMTUDiscovery,
|
||||
}
|
||||
if options.InitialPacketSize > 0 {
|
||||
quicConfig.InitialPacketSize = uint16(options.InitialPacketSize)
|
||||
}
|
||||
if options.MaxConcurrentStreams > 0 {
|
||||
quicConfig.MaxIncomingStreams = int64(options.MaxConcurrentStreams)
|
||||
}
|
||||
return quicConfig
|
||||
}
|
||||
@@ -7,14 +7,14 @@ import (
|
||||
"io"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/badhttp"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/protocol/http"
|
||||
)
|
||||
|
||||
func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
||||
request, err := http.ReadRequest(std_bufio.NewReader(reader))
|
||||
request, err := badhttp.ReadRequest(std_bufio.NewReader(reader))
|
||||
if err != nil {
|
||||
if errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/badhttp"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
@@ -24,7 +25,6 @@ import (
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
sHTTP "github.com/sagernet/sing/protocol/http"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
"golang.org/x/net/http2"
|
||||
@@ -93,7 +93,7 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
if path == "" {
|
||||
path = "/dns-query"
|
||||
}
|
||||
err = sHTTP.URLSetPath(&destinationURL, path)
|
||||
err = badhttp.URLSetPath(&destinationURL, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/badhttp"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
@@ -24,7 +25,6 @@ import (
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
sHTTP "github.com/sagernet/sing/protocol/http"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
@@ -87,7 +87,7 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
if path == "" {
|
||||
path = "/dns-query"
|
||||
}
|
||||
err = sHTTP.URLSetPath(&destinationURL, path)
|
||||
err = badhttp.URLSetPath(&destinationURL, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
... // Listen Fields
|
||||
|
||||
"version": [],
|
||||
"users": [
|
||||
{
|
||||
"username": "admin",
|
||||
@@ -14,7 +15,9 @@
|
||||
}
|
||||
],
|
||||
"tls": {},
|
||||
"set_system_proxy": false
|
||||
"set_system_proxy": false,
|
||||
|
||||
... // HTTP2 Fields / QUIC Fields
|
||||
}
|
||||
```
|
||||
|
||||
@@ -24,6 +27,18 @@ See [Listen Fields](/configuration/shared/listen/) for details.
|
||||
|
||||
### Fields
|
||||
|
||||
#### version
|
||||
|
||||
!!! question "Since sing-box 1.15.0"
|
||||
|
||||
List of HTTP versions to serve.
|
||||
|
||||
Available values: `1`, `2`, `3`.
|
||||
|
||||
`1` and `2` are used by default.
|
||||
|
||||
TLS is required for `3`.
|
||||
|
||||
#### tls
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
@@ -45,3 +60,19 @@ No authentication required if empty.
|
||||
To work on Android and Apple platforms without privileges, use tun.platform.http_proxy instead.
|
||||
|
||||
Automatically set system proxy configuration when start and clean up when stop.
|
||||
|
||||
### HTTP2 Fields
|
||||
|
||||
!!! question "Since sing-box 1.15.0"
|
||||
|
||||
When `version` contains `2`.
|
||||
|
||||
See [HTTP2 Fields](/configuration/shared/http2/) for details.
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
!!! question "Since sing-box 1.15.0"
|
||||
|
||||
When `version` contains `3`, [HTTP2 Fields](#http2-fields) are replaced by QUIC Fields.
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
... // 监听字段
|
||||
|
||||
"version": [],
|
||||
"users": [
|
||||
{
|
||||
"username": "admin",
|
||||
@@ -14,7 +15,9 @@
|
||||
}
|
||||
],
|
||||
"tls": {},
|
||||
"set_system_proxy": false
|
||||
"set_system_proxy": false,
|
||||
|
||||
... // HTTP2 字段 / QUIC 字段
|
||||
}
|
||||
```
|
||||
|
||||
@@ -24,6 +27,18 @@
|
||||
|
||||
### 字段
|
||||
|
||||
#### version
|
||||
|
||||
!!! question "自 sing-box 1.15.0 起"
|
||||
|
||||
提供的 HTTP 版本列表。
|
||||
|
||||
可用值:`1`、`2`、`3`。
|
||||
|
||||
默认为 `1` 和 `2`。
|
||||
|
||||
`3` 需要 TLS。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
@@ -44,4 +59,20 @@ HTTP 用户
|
||||
|
||||
要在无特权的 Android 和 iOS 上工作,请改用 tun.platform.http_proxy。
|
||||
|
||||
启动时自动设置系统代理,停止时自动清理。
|
||||
启动时自动设置系统代理,停止时自动清理。
|
||||
|
||||
### HTTP2 字段
|
||||
|
||||
!!! question "自 sing-box 1.15.0 起"
|
||||
|
||||
当 `version` 包含 `2` 时。
|
||||
|
||||
参阅 [HTTP2 字段](/zh/configuration/shared/http2/)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
!!! question "自 sing-box 1.15.0 起"
|
||||
|
||||
当 `version` 包含 `3` 时,[HTTP2 字段](#http2-字段) 被 QUIC 字段替代。
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/)。
|
||||
@@ -13,8 +13,11 @@
|
||||
"password": "admin",
|
||||
"path": "",
|
||||
"headers": {},
|
||||
"version": 0,
|
||||
"disable_version_fallback": false,
|
||||
"tls": {},
|
||||
|
||||
|
||||
... // HTTP2 Fields / QUIC Fields
|
||||
... // Dial Fields
|
||||
}
|
||||
```
|
||||
@@ -49,10 +52,46 @@ Path of HTTP request.
|
||||
|
||||
Extra headers of HTTP request.
|
||||
|
||||
#### version
|
||||
|
||||
!!! question "Since sing-box 1.15.0"
|
||||
|
||||
HTTP version.
|
||||
|
||||
Available values: `1`, `2`, `3`.
|
||||
|
||||
`2` is used by default, or `1` if `path` or the `Host` header is set.
|
||||
|
||||
`path` and the `Host` header are only available for `1`.
|
||||
|
||||
When `3`, [HTTP2 Fields](#http2-fields) are replaced by [QUIC Fields](#quic-fields).
|
||||
|
||||
#### disable_version_fallback
|
||||
|
||||
!!! question "Since sing-box 1.15.0"
|
||||
|
||||
Disable automatic fallback to lower HTTP version.
|
||||
|
||||
#### tls
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
|
||||
### HTTP2 Fields
|
||||
|
||||
!!! question "Since sing-box 1.15.0"
|
||||
|
||||
When `version` is `2` (default).
|
||||
|
||||
See [HTTP2 Fields](/configuration/shared/http2/) for details.
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
!!! question "Since sing-box 1.15.0"
|
||||
|
||||
When `version` is `3`.
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
@@ -13,8 +13,11 @@
|
||||
"password": "admin",
|
||||
"path": "",
|
||||
"headers": {},
|
||||
"version": 0,
|
||||
"disable_version_fallback": false,
|
||||
"tls": {},
|
||||
|
||||
... // HTTP2 字段 / QUIC 字段
|
||||
... // 拨号字段
|
||||
}
|
||||
```
|
||||
@@ -49,10 +52,46 @@ HTTP 请求路径。
|
||||
|
||||
HTTP 请求的额外标头。
|
||||
|
||||
#### version
|
||||
|
||||
!!! question "自 sing-box 1.15.0 起"
|
||||
|
||||
HTTP 版本。
|
||||
|
||||
可用值:`1`、`2`、`3`。
|
||||
|
||||
默认使用 `2`;设置了 `path` 或 `Host` 头时默认使用 `1`。
|
||||
|
||||
`path` 和 `Host` 头仅在 `1` 时可用。
|
||||
|
||||
当为 `3` 时,[HTTP2 字段](#http2-字段) 替换为 [QUIC 字段](#quic-字段)。
|
||||
|
||||
#### disable_version_fallback
|
||||
|
||||
!!! question "自 sing-box 1.15.0 起"
|
||||
|
||||
禁用自动回退到更低的 HTTP 版本。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### HTTP2 字段
|
||||
|
||||
!!! question "自 sing-box 1.15.0 起"
|
||||
|
||||
当 `version` 为 `2`(默认)时。
|
||||
|
||||
参阅 [HTTP2 字段](/zh/configuration/shared/http2/) 了解详情。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
!!! question "自 sing-box 1.15.0 起"
|
||||
|
||||
当 `version` 为 `3` 时。
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
||||
@@ -6019,8 +6019,68 @@
|
||||
"set_system_proxy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"version": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tls": {
|
||||
"$ref": "#/$defs/InboundTLSOptions"
|
||||
},
|
||||
"idle_timeout": {
|
||||
"$ref": "#/$defs/Duration"
|
||||
},
|
||||
"keep_alive_period": {
|
||||
"$ref": "#/$defs/Duration"
|
||||
},
|
||||
"stream_receive_window": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"connection_receive_window": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"max_concurrent_streams": {
|
||||
"type": "integer"
|
||||
},
|
||||
"initial_packet_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"disable_path_mtu_discovery": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -11229,6 +11289,55 @@
|
||||
},
|
||||
"headers": {
|
||||
"$ref": "#/$defs/HTTPHeader"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
},
|
||||
"disable_version_fallback": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"idle_timeout": {
|
||||
"$ref": "#/$defs/Duration"
|
||||
},
|
||||
"keep_alive_period": {
|
||||
"$ref": "#/$defs/Duration"
|
||||
},
|
||||
"stream_receive_window": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"connection_receive_window": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"max_concurrent_streams": {
|
||||
"type": "integer"
|
||||
},
|
||||
"initial_packet_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"disable_path_mtu_discovery": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport/quic"
|
||||
_ "github.com/sagernet/sing-box/protocol/http/quic"
|
||||
"github.com/sagernet/sing-box/protocol/hysteria"
|
||||
"github.com/sagernet/sing-box/protocol/hysteria2"
|
||||
_ "github.com/sagernet/sing-box/protocol/naive/quic"
|
||||
|
||||
+60
-23
@@ -1,7 +1,9 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"slices"
|
||||
|
||||
"github.com/sagernet/sing-box/schema"
|
||||
"github.com/sagernet/sing/common/byteformats"
|
||||
@@ -69,17 +71,17 @@ func (o HTTPClientOptions) MarshalJSON() ([]byte, error) {
|
||||
return badjson.MarshallObjects(_HTTPClientOptions(o), httpClientVariant(_HTTPClientOptions(o)))
|
||||
}
|
||||
|
||||
func (o *HTTPClientOptions) UnmarshalJSON(content []byte) error {
|
||||
func (o *HTTPClientOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error {
|
||||
if len(content) > 0 && content[0] == '"' {
|
||||
*o = HTTPClientOptions{}
|
||||
return json.Unmarshal(content, &o.Tag)
|
||||
}
|
||||
var options _HTTPClientOptions
|
||||
err := json.Unmarshal(content, &options)
|
||||
err := json.UnmarshalContext(ctx, content, &options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = unmarshalHTTPClientVersionOptions(content, &options, &options)
|
||||
err = unmarshalHTTPVersionOptions(ctx, content, &options, options.Version, &options.HTTP2Options, &options.HTTP3Options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -92,39 +94,74 @@ func (h HTTPClient) MarshalJSON() ([]byte, error) {
|
||||
return badjson.MarshallObjects(_HTTPClientOptions(h), httpClientVariant(_HTTPClientOptions(h)))
|
||||
}
|
||||
|
||||
func (h *HTTPClient) UnmarshalJSON(content []byte) error {
|
||||
err := json.Unmarshal(content, (*_HTTPClientOptions)(h))
|
||||
func (h *HTTPClient) UnmarshalJSONContext(ctx context.Context, content []byte) error {
|
||||
err := json.UnmarshalContext(ctx, content, (*_HTTPClientOptions)(h))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmarshalHTTPClientVersionOptions(content, (*_HTTPClientOptions)(h), (*_HTTPClientOptions)(h))
|
||||
return unmarshalHTTPVersionOptions(ctx, content, (*_HTTPClientOptions)(h), h.Version, &h.HTTP2Options, &h.HTTP3Options)
|
||||
}
|
||||
|
||||
func unmarshalHTTPClientVersionOptions(content []byte, baseStruct any, options *_HTTPClientOptions) error {
|
||||
switch options.Version {
|
||||
func unmarshalHTTPVersionOptions(ctx context.Context, content []byte, baseStruct any, version int, http2Options *HTTP2Options, http3Options *QUICOptions) error {
|
||||
switch version {
|
||||
case 1:
|
||||
return json.UnmarshalDisallowUnknownFields(content, baseStruct)
|
||||
return json.UnmarshalContextDisallowUnknownFields(ctx, content, baseStruct)
|
||||
case 0, 2:
|
||||
options.Version = 2
|
||||
return badjson.UnmarshallExcluded(content, baseStruct, &options.HTTP2Options)
|
||||
return badjson.UnmarshallExcludedContext(ctx, content, baseStruct, http2Options)
|
||||
case 3:
|
||||
return badjson.UnmarshallExcluded(content, baseStruct, &options.HTTP3Options)
|
||||
return badjson.UnmarshallExcludedContext(ctx, content, baseStruct, http3Options)
|
||||
default:
|
||||
return E.New("unknown HTTP version: ", options.Version)
|
||||
return E.New("unknown HTTP version: ", version)
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshalHTTPVersionsOptions(ctx context.Context, content []byte, baseStruct any, versions []int, http2Options *HTTP2Options, http3Options *QUICOptions) error {
|
||||
for _, version := range versions {
|
||||
if version < 1 || version > 3 {
|
||||
return E.New("unknown HTTP version: ", version)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case slices.Contains(versions, 3):
|
||||
err := badjson.UnmarshallExcludedContext(ctx, content, baseStruct, http3Options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*http2Options = http3Options.HTTP2Options
|
||||
return nil
|
||||
case slices.Contains(versions, 2):
|
||||
return badjson.UnmarshallExcludedContext(ctx, content, baseStruct, http2Options)
|
||||
default:
|
||||
return json.UnmarshalContextDisallowUnknownFields(ctx, content, baseStruct)
|
||||
}
|
||||
}
|
||||
|
||||
func httpVersionsVariant(versions []int, http2Options HTTP2Options, http3Options QUICOptions) any {
|
||||
switch {
|
||||
case slices.Contains(versions, 3):
|
||||
return http3Options
|
||||
case slices.Contains(versions, 2):
|
||||
return http2Options
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func httpVersionVariant(version int, http2Options HTTP2Options, http3Options QUICOptions) any {
|
||||
switch version {
|
||||
case 1:
|
||||
return nil
|
||||
case 0, 2:
|
||||
return http2Options
|
||||
case 3:
|
||||
return http3Options
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func httpClientVariant(options _HTTPClientOptions) any {
|
||||
switch options.Version {
|
||||
case 1:
|
||||
return nil
|
||||
case 0, 2:
|
||||
return options.HTTP2Options
|
||||
case 3:
|
||||
return options.HTTP3Options
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return httpVersionVariant(options.Version, options.HTTP2Options, options.HTTP3Options)
|
||||
}
|
||||
|
||||
func describeHTTPClientObject(builder schema.Builder) (*schema.Node, error) {
|
||||
|
||||
+85
-3
@@ -1,7 +1,13 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
"github.com/sagernet/sing-box/schema"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
)
|
||||
|
||||
@@ -19,6 +25,51 @@ type HTTPMixedInboundOptions struct {
|
||||
InboundTLSOptionsContainer
|
||||
}
|
||||
|
||||
type _HTTPInboundOptions struct {
|
||||
ListenOptions
|
||||
Users []auth.User `json:"users,omitempty"`
|
||||
DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"`
|
||||
SetSystemProxy bool `json:"set_system_proxy,omitempty"`
|
||||
Version badoption.Listable[int] `json:"version,omitempty" enum:"1,2,3"`
|
||||
InboundTLSOptionsContainer
|
||||
HTTP2Options HTTP2Options `json:"-"`
|
||||
HTTP3Options QUICOptions `json:"-"`
|
||||
}
|
||||
|
||||
type HTTPInboundOptions _HTTPInboundOptions
|
||||
|
||||
func (o HTTPInboundOptions) Versions() []int {
|
||||
if len(o.Version) > 0 {
|
||||
return o.Version
|
||||
}
|
||||
return []int{1, 2}
|
||||
}
|
||||
|
||||
func (o HTTPInboundOptions) MarshalJSON() ([]byte, error) {
|
||||
return badjson.MarshallObjects(_HTTPInboundOptions(o), httpVersionsVariant(o.Versions(), o.HTTP2Options, o.HTTP3Options))
|
||||
}
|
||||
|
||||
func (o *HTTPInboundOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error {
|
||||
err := json.UnmarshalContext(ctx, content, (*_HTTPInboundOptions)(o))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmarshalHTTPVersionsOptions(ctx, content, (*_HTTPInboundOptions)(o), o.Versions(), &o.HTTP2Options, &o.HTTP3Options)
|
||||
}
|
||||
|
||||
func (o HTTPInboundOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
|
||||
node := schema.StrictObject()
|
||||
err := builder.FlattenStruct(node, reflect.TypeFor[HTTPInboundOptions]())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = builder.FlattenStruct(node, reflect.TypeFor[QUICOptions]())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
type SOCKSOutboundOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
@@ -29,12 +80,43 @@ type SOCKSOutboundOptions struct {
|
||||
UDPOverTCP *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"`
|
||||
}
|
||||
|
||||
type HTTPOutboundOptions struct {
|
||||
type _HTTPOutboundOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
OutboundTLSOptionsContainer
|
||||
Path string `json:"path,omitempty"`
|
||||
Headers badoption.HTTPHeader `json:"headers,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Headers badoption.HTTPHeader `json:"headers,omitempty"`
|
||||
Version int `json:"version,omitempty" enum:"0,1,2,3"`
|
||||
DisableVersionFallback bool `json:"disable_version_fallback,omitempty"`
|
||||
HTTP2Options HTTP2Options `json:"-"`
|
||||
HTTP3Options QUICOptions `json:"-"`
|
||||
}
|
||||
|
||||
type HTTPOutboundOptions _HTTPOutboundOptions
|
||||
|
||||
func (o HTTPOutboundOptions) MarshalJSON() ([]byte, error) {
|
||||
return badjson.MarshallObjects(_HTTPOutboundOptions(o), httpVersionVariant(o.Version, o.HTTP2Options, o.HTTP3Options))
|
||||
}
|
||||
|
||||
func (o *HTTPOutboundOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error {
|
||||
err := json.UnmarshalContext(ctx, content, (*_HTTPOutboundOptions)(o))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmarshalHTTPVersionOptions(ctx, content, (*_HTTPOutboundOptions)(o), o.Version, &o.HTTP2Options, &o.HTTP3Options)
|
||||
}
|
||||
|
||||
func (o HTTPOutboundOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
|
||||
node := schema.StrictObject()
|
||||
err := builder.FlattenStruct(node, reflect.TypeFor[HTTPOutboundOptions]())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = builder.FlattenStruct(node, reflect.TypeFor[QUICOptions]())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
+80
-19
@@ -1,9 +1,11 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
std_bufio "bufio"
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
std_http "net/http"
|
||||
"slices"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
@@ -13,34 +15,62 @@ import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/transport/http"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/protocol/http"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
var ConfigureHTTP3ListenerFunc func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler std_http.Handler, tlsConfig tls.ServerConfig, options option.QUICOptions) (io.Closer, error)
|
||||
|
||||
func RegisterInbound(registry *inbound.Registry) {
|
||||
inbound.Register[option.HTTPMixedInboundOptions](registry, C.TypeHTTP, NewInbound)
|
||||
inbound.Register[option.HTTPInboundOptions](registry, C.TypeHTTP, NewInbound)
|
||||
}
|
||||
|
||||
var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
|
||||
|
||||
type Inbound struct {
|
||||
inbound.Adapter
|
||||
router adapter.ConnectionRouterEx
|
||||
logger log.ContextLogger
|
||||
listener *listener.Listener
|
||||
authenticator *auth.Authenticator
|
||||
tlsConfig tls.ServerConfig
|
||||
ctx context.Context
|
||||
router adapter.ConnectionRouterEx
|
||||
logger log.ContextLogger
|
||||
listener *listener.Listener
|
||||
server *http.Server
|
||||
tlsConfig tls.ServerConfig
|
||||
http3 bool
|
||||
quicOptions option.QUICOptions
|
||||
}
|
||||
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) (adapter.Inbound, error) {
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPInboundOptions) (adapter.Inbound, error) {
|
||||
versions := options.Versions()
|
||||
serveHTTP1 := slices.Contains(versions, 1)
|
||||
serveHTTP2 := slices.Contains(versions, 2)
|
||||
serveHTTP3 := slices.Contains(versions, 3)
|
||||
if serveHTTP3 && (options.TLS == nil || !options.TLS.Enabled) {
|
||||
return nil, E.New("TLS is required for HTTP/3")
|
||||
}
|
||||
if !serveHTTP1 && !serveHTTP2 && options.SetSystemProxy {
|
||||
return nil, E.New("set_system_proxy requires HTTP/1 or HTTP/2")
|
||||
}
|
||||
inbound := &Inbound{
|
||||
Adapter: inbound.NewAdapter(C.TypeHTTP, tag),
|
||||
router: uot.NewRouter(router, logger),
|
||||
logger: logger,
|
||||
authenticator: auth.NewAuthenticator(options.Users),
|
||||
Adapter: inbound.NewAdapter(C.TypeHTTP, tag),
|
||||
ctx: ctx,
|
||||
router: uot.NewRouter(router, logger),
|
||||
logger: logger,
|
||||
server: http.NewServer(http.ServerOptions{
|
||||
Authenticator: auth.NewAuthenticator(options.Users),
|
||||
Logger: logger,
|
||||
HTTP1: serveHTTP1,
|
||||
HTTP2: serveHTTP2,
|
||||
HTTP2Options: options.HTTP2Options,
|
||||
UDP: true,
|
||||
}),
|
||||
http3: serveHTTP3,
|
||||
quicOptions: options.HTTP3Options,
|
||||
}
|
||||
if options.TLS != nil {
|
||||
tlsConfig, err := tls.NewServerWithOptions(tls.ServerOptions{
|
||||
@@ -52,12 +82,26 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tlsConfig != nil && len(tlsConfig.NextProtos()) == 0 {
|
||||
var nextProtos []string
|
||||
if serveHTTP2 {
|
||||
nextProtos = append(nextProtos, http2.NextProtoTLS)
|
||||
}
|
||||
if serveHTTP1 {
|
||||
nextProtos = append(nextProtos, "http/1.1")
|
||||
}
|
||||
tlsConfig.SetNextProtos(nextProtos)
|
||||
}
|
||||
inbound.tlsConfig = tlsConfig
|
||||
}
|
||||
var network []string
|
||||
if serveHTTP1 || serveHTTP2 {
|
||||
network = []string{N.NetworkTCP}
|
||||
}
|
||||
inbound.listener = listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Network: []string{N.NetworkTCP},
|
||||
Network: network,
|
||||
Listen: options.ListenOptions,
|
||||
ConnectionHandler: inbound,
|
||||
SetSystemProxy: options.SetSystemProxy,
|
||||
@@ -82,6 +126,27 @@ func (h *Inbound) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
return err
|
||||
}
|
||||
scope.Add(h.listener.Close)
|
||||
if h.http3 {
|
||||
return h.startHTTP3(scope)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Inbound) startHTTP3(scope *adapter.Scope) error {
|
||||
if ConfigureHTTP3ListenerFunc == nil {
|
||||
return C.ErrQUICNotIncluded
|
||||
}
|
||||
if !slices.Contains(h.tlsConfig.NextProtos(), "h3") {
|
||||
h.tlsConfig.SetNextProtos(append([]string{"h3"}, h.tlsConfig.NextProtos()...))
|
||||
}
|
||||
var metadata adapter.InboundContext
|
||||
//nolint:staticcheck
|
||||
metadata.InboundDetour = h.listener.ListenOptions().Detour
|
||||
http3Server, err := ConfigureHTTP3ListenerFunc(h.ctx, h.logger, h.listener, h.server.HTTP3Handler(adapter.NewUpstreamHandler(metadata, h.newUserConnection, h.streamUserPacketConnection)), h.tlsConfig, h.quicOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scope.Add(http3Server.Close)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -95,11 +160,7 @@ func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata ada
|
||||
}
|
||||
conn = tlsConn
|
||||
}
|
||||
err := http.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, adapter.NewUpstreamHandler(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose)
|
||||
if err != nil {
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
|
||||
}
|
||||
h.server.ServeConnection(ctx, conn, http.NewReader(conn), adapter.NewUpstreamHandler(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose)
|
||||
}
|
||||
|
||||
func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
|
||||
+99
-16
@@ -3,7 +3,7 @@ package http
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"slices"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
@@ -12,21 +12,27 @@ import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/transport/http"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
sHTTP "github.com/sagernet/sing/protocol/http"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
func RegisterOutbound(registry *outbound.Registry) {
|
||||
outbound.Register[option.HTTPOutboundOptions](registry, C.TypeHTTP, NewOutbound)
|
||||
}
|
||||
|
||||
var _ adapter.OutboundWithMultiplex = (*Outbound)(nil)
|
||||
|
||||
type Outbound struct {
|
||||
outbound.Adapter
|
||||
logger logger.ContextLogger
|
||||
client *sHTTP.Client
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPOutboundOptions) (adapter.Outbound, error) {
|
||||
@@ -34,32 +40,109 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
detour, err := tls.NewDialerFromOptions(ctx, logger, outboundDialer, options.Server, common.PtrValueOrDefault(options.TLS))
|
||||
headers := options.Headers.Build()
|
||||
version := http.ResolveVersion(options.Version, options.Path, headers.Get("Host"))
|
||||
tlsOptions := common.PtrValueOrDefault(options.TLS)
|
||||
if version == 3 && !tlsOptions.Enabled {
|
||||
return nil, C.ErrTLSRequired
|
||||
}
|
||||
alpnIsDefault := tlsOptions.Enabled && len(tlsOptions.ALPN) == 0
|
||||
if alpnIsDefault {
|
||||
switch version {
|
||||
case 1:
|
||||
tlsOptions.ALPN = []string{"http/1.1"}
|
||||
default:
|
||||
tlsOptions.ALPN = []string{http2.NextProtoTLS, "http/1.1"}
|
||||
}
|
||||
}
|
||||
detour, err := tls.NewDialerFromOptions(ctx, logger, outboundDialer, options.Server, tlsOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var http1Detour N.Dialer
|
||||
if version >= 2 && (alpnIsDefault || slices.Contains(tlsOptions.ALPN, "http/1.1")) {
|
||||
http1TLSOptions := tlsOptions
|
||||
http1TLSOptions.ALPN = []string{"http/1.1"}
|
||||
http1Detour, err = tls.NewDialerFromOptions(ctx, logger, outboundDialer, options.Server, http1TLSOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var quicTLSConfig tls.Config
|
||||
if version == 3 {
|
||||
if alpnIsDefault {
|
||||
tlsOptions.ALPN = []string{"h3"}
|
||||
}
|
||||
quicTLSConfig, err = tls.NewClient(ctx, logger, options.Server, tlsOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
client, err := http.NewClient(http.ClientOptions{
|
||||
Dialer: detour,
|
||||
HTTP1Dialer: http1Detour,
|
||||
RawDialer: outboundDialer,
|
||||
TLSConfig: quicTLSConfig,
|
||||
Server: options.ServerOptions.Build(),
|
||||
Username: options.Username,
|
||||
Password: options.Password,
|
||||
Path: options.Path,
|
||||
Headers: headers,
|
||||
Version: version,
|
||||
DisableVersionFallback: options.DisableVersionFallback,
|
||||
HTTP2Options: options.HTTP2Options,
|
||||
HTTP3Options: options.HTTP3Options,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Outbound{
|
||||
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHTTP, tag, []string{N.NetworkTCP}, options.DialerOptions),
|
||||
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHTTP, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
|
||||
logger: logger,
|
||||
client: sHTTP.NewClient(sHTTP.Options{
|
||||
Dialer: detour,
|
||||
Server: options.ServerOptions.Build(),
|
||||
Username: options.Username,
|
||||
Password: options.Password,
|
||||
Path: options.Path,
|
||||
Headers: options.Headers.Build(),
|
||||
}),
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *Outbound) MultiplexEnabled() bool {
|
||||
return h.client.MultiplexEnabled()
|
||||
}
|
||||
|
||||
func (h *Outbound) InterfaceUpdated(ctx context.Context) {
|
||||
h.client.ResetConnections()
|
||||
}
|
||||
|
||||
func (h *Outbound) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
if stage != adapter.StartStateInitialize {
|
||||
return nil
|
||||
}
|
||||
scope.Add(h.client.Close)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
metadata.Outbound = h.Tag()
|
||||
metadata.Destination = destination
|
||||
h.logger.InfoContext(ctx, "outbound connection to ", destination)
|
||||
return h.client.DialContext(ctx, network, destination)
|
||||
switch N.NetworkName(network) {
|
||||
case N.NetworkTCP:
|
||||
h.logger.InfoContext(ctx, "outbound connection to ", destination)
|
||||
return h.client.DialContext(ctx, network, destination)
|
||||
case N.NetworkUDP:
|
||||
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||||
packetConn, err := h.client.ListenPacket(ctx, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bufio.NewBindPacketConn(packetConn, destination), nil
|
||||
default:
|
||||
return nil, E.Extend(N.ErrUnknownNetwork, network)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
return nil, os.ErrInvalid
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
metadata.Outbound = h.Tag()
|
||||
metadata.Destination = destination
|
||||
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||||
return h.client.ListenPacket(ctx, destination)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
//go:build with_quic
|
||||
|
||||
package quic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
"github.com/sagernet/sing-box/common/httpclient"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
boxHTTP "github.com/sagernet/sing-box/protocol/http"
|
||||
transportHTTP "github.com/sagernet/sing-box/transport/http"
|
||||
"github.com/sagernet/sing-quic"
|
||||
congestion_meta2 "github.com/sagernet/sing-quic/congestion_meta2"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
func init() {
|
||||
boxHTTP.ConfigureHTTP3ListenerFunc = func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.QUICOptions) (io.Closer, error) {
|
||||
err := qtls.ConfigureHTTP3(tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
udpConn, err := listener.ListenUDP()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
quicConfig := httpclient.NewQUICConfig(options)
|
||||
if quicConfig.MaxIncomingStreams == 0 {
|
||||
quicConfig.MaxIncomingStreams = 1 << 60
|
||||
}
|
||||
quicConfig.Allow0RTT = true
|
||||
quicConfig.DisablePathManager = true
|
||||
quicConfig.EnableDatagrams = true
|
||||
quicListener, err := qtls.ListenEarly(udpConn, tlsConfig, quicConfig)
|
||||
if err != nil {
|
||||
udpConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
http3Server := &http3.Server{
|
||||
Handler: handler,
|
||||
EnableDatagrams: true,
|
||||
ConnContext: func(ctx context.Context, conn *quic.Conn) context.Context {
|
||||
conn.SetCongestionControl(congestion_meta2.NewBbrSenderWithProfile(conn.InitialPacketSize(), congestion_meta2.ProfileStandard))
|
||||
return log.ContextWithNewID(ctx)
|
||||
},
|
||||
}
|
||||
go func() {
|
||||
serveErr := http3Server.ServeListener(quicListener)
|
||||
udpConn.Close()
|
||||
if serveErr != nil && !E.IsClosedOrCanceled(serveErr) {
|
||||
logger.Error("http3 server closed: ", serveErr)
|
||||
}
|
||||
}()
|
||||
return quicListener, nil
|
||||
}
|
||||
transportHTTP.HTTP3StreamFunc = func(ctx context.Context, writer http.ResponseWriter) (transportHTTP.DatagramStream, bool) {
|
||||
streamer, isStreamer := writer.(http3.HTTPStreamer)
|
||||
if !isStreamer {
|
||||
return nil, false
|
||||
}
|
||||
settingser, isSettingser := writer.(http3.Settingser)
|
||||
if !isSettingser {
|
||||
return nil, false
|
||||
}
|
||||
select {
|
||||
case <-settingser.ReceivedSettings():
|
||||
case <-ctx.Done():
|
||||
return nil, false
|
||||
}
|
||||
return &datagramStream{
|
||||
Stream: streamer.HTTPStream(),
|
||||
datagramsEnabled: settingser.Settings().EnableDatagrams,
|
||||
}, true
|
||||
}
|
||||
}
|
||||
|
||||
type datagramStream struct {
|
||||
*http3.Stream
|
||||
datagramsEnabled bool
|
||||
}
|
||||
|
||||
func (s *datagramStream) SendDatagram(payload []byte) error {
|
||||
if !s.datagramsEnabled {
|
||||
return transportHTTP.ErrDatagramUnsupported
|
||||
}
|
||||
err := s.Stream.SendDatagram(payload)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var tooLarge *quic.DatagramTooLargeError
|
||||
if errors.As(err, &tooLarge) {
|
||||
return transportHTTP.ErrDatagramUnsupported
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *datagramStream) Close() error {
|
||||
s.Stream.SetWriteDeadline(time.Now())
|
||||
s.Stream.CancelRead(0)
|
||||
return s.Stream.Close()
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/badhttp"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
@@ -18,7 +19,6 @@ import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
sHTTP "github.com/sagernet/sing/protocol/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
@@ -65,7 +65,7 @@ func NewRealmService(ctx context.Context, logger log.ContextLogger, tag string,
|
||||
chiRouter.Use(middleware.RequestSize(maxRequestBodyBytes))
|
||||
chiRouter.Use(func(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
logger.DebugContext(r.Context(), r.Method, " ", r.RequestURI, " ", sHTTP.SourceAddress(r))
|
||||
logger.DebugContext(r.Context(), r.Method, " ", r.RequestURI, " ", badhttp.SourceAddress(r))
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
})
|
||||
|
||||
+31
-19
@@ -1,7 +1,6 @@
|
||||
package mixed
|
||||
|
||||
import (
|
||||
std_bufio "bufio"
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
@@ -14,12 +13,12 @@ import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/transport/http"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/protocol/http"
|
||||
"github.com/sagernet/sing/protocol/socks"
|
||||
"github.com/sagernet/sing/protocol/socks/socks4"
|
||||
"github.com/sagernet/sing/protocol/socks/socks5"
|
||||
@@ -37,6 +36,7 @@ type Inbound struct {
|
||||
logger log.ContextLogger
|
||||
listener *listener.Listener
|
||||
authenticator *auth.Authenticator
|
||||
server *http.Server
|
||||
tlsConfig tls.ServerConfig
|
||||
udpTimeout time.Duration
|
||||
}
|
||||
@@ -55,6 +55,13 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
authenticator: auth.NewAuthenticator(options.Users),
|
||||
udpTimeout: udpTimeout,
|
||||
}
|
||||
inbound.server = http.NewServer(http.ServerOptions{
|
||||
Authenticator: inbound.authenticator,
|
||||
Logger: logger,
|
||||
HTTP1: true,
|
||||
HTTP2: true,
|
||||
UDP: true,
|
||||
})
|
||||
if options.TLS != nil {
|
||||
tlsConfig, err := tls.NewServerWithOptions(tls.ServerOptions{
|
||||
Context: ctx,
|
||||
@@ -99,35 +106,40 @@ func (h *Inbound) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
}
|
||||
|
||||
func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
err := h.newConnection(ctx, conn, metadata, onClose)
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
if err != nil {
|
||||
if E.IsClosedOrCanceled(err) {
|
||||
h.logger.DebugContext(ctx, "connection closed: ", err)
|
||||
} else {
|
||||
h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
|
||||
if h.tlsConfig != nil {
|
||||
tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig)
|
||||
if err != nil {
|
||||
return E.Cause(err, "TLS handshake")
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": TLS handshake"))
|
||||
return
|
||||
}
|
||||
conn = tlsConn
|
||||
}
|
||||
reader := std_bufio.NewReader(conn)
|
||||
reader := http.NewReader(conn)
|
||||
headerBytes, err := reader.Peek(1)
|
||||
if err != nil {
|
||||
return E.Cause(err, "peek first byte")
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
if E.IsClosedOrCanceled(err) {
|
||||
h.logger.DebugContext(ctx, "connection closed: ", err)
|
||||
} else {
|
||||
h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": peek first byte"))
|
||||
}
|
||||
return
|
||||
}
|
||||
handler := adapter.NewUpstreamHandler(metadata, h.newUserConnection, h.streamUserPacketConnection)
|
||||
switch headerBytes[0] {
|
||||
case socks4.Version, socks5.Version:
|
||||
return socks.HandleConnectionEx(ctx, conn, reader, h.authenticator, adapter.NewUpstreamHandler(metadata, h.newUserConnection, h.streamUserPacketConnection), h.listener, h.udpTimeout, metadata.Source, onClose)
|
||||
err = socks.HandleConnectionEx(ctx, conn, reader.Reader, h.authenticator, handler, h.listener, h.udpTimeout, metadata.Source, onClose)
|
||||
if err != nil {
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
if E.IsClosedOrCanceled(err) {
|
||||
h.logger.DebugContext(ctx, "connection closed: ", err)
|
||||
} else {
|
||||
h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
|
||||
}
|
||||
}
|
||||
default:
|
||||
return http.HandleConnectionEx(ctx, conn, reader, h.authenticator, adapter.NewUpstreamHandler(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose)
|
||||
h.server.ServeConnection(ctx, conn, reader, handler, metadata.Source, onClose)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/common/badhttp"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/common/uot"
|
||||
@@ -25,7 +26,6 @@ import (
|
||||
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"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c" //nolint:staticcheck
|
||||
@@ -155,7 +155,7 @@ func (n *Inbound) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
n.badRequest(ctx, request, E.New("missing naive padding"))
|
||||
return
|
||||
}
|
||||
userName, password, authOk := sHttp.ParseBasicAuth(request.Header.Get("Proxy-Authorization"))
|
||||
userName, password, authOk := badhttp.ParseBasicAuth(request.Header.Get("Proxy-Authorization"))
|
||||
if authOk {
|
||||
authOk = n.authenticator.Verify(userName, password)
|
||||
}
|
||||
@@ -175,7 +175,7 @@ func (n *Inbound) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
hostPort = request.Host
|
||||
}
|
||||
}
|
||||
source := sHttp.SourceAddress(request)
|
||||
source := badhttp.SourceAddress(request)
|
||||
destination := M.ParseSocksaddr(hostPort).Unwrap()
|
||||
|
||||
if hijacker, isHijacker := writer.(http.Hijacker); isHijacker {
|
||||
|
||||
@@ -3,9 +3,9 @@ package ssmapi
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/sagernet/sing-box/common/badhttp"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
sHTTP "github.com/sagernet/sing/protocol/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
@@ -29,7 +29,7 @@ func (s *APIServer) Route(r chi.Router) {
|
||||
r.Route("/server/v1", func(r chi.Router) {
|
||||
r.Use(func(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
s.logger.Debug(request.Method, " ", request.RequestURI, " ", sHTTP.SourceAddress(request))
|
||||
s.logger.Debug(request.Method, " ", request.RequestURI, " ", badhttp.SourceAddress(request))
|
||||
handler.ServeHTTP(writer, request)
|
||||
})
|
||||
})
|
||||
|
||||
+4
-3
@@ -411,9 +411,10 @@ func testLargeDataWithPacketConnSize(t *testing.T, port uint16, chunkSize int, p
|
||||
buf := make([]byte, 64*1024)
|
||||
|
||||
for i := 0; i < times; i++ {
|
||||
_, rAddr, err = l.ReadFrom(buf)
|
||||
if err != nil {
|
||||
t.Log(err.Error())
|
||||
var readErr error
|
||||
_, rAddr, readErr = l.ReadFrom(buf)
|
||||
if readErr != nil {
|
||||
t.Log(readErr.Error())
|
||||
return
|
||||
}
|
||||
hash := md5.Sum(buf[:chunkSize])
|
||||
|
||||
+3
-3
@@ -12,12 +12,12 @@ require (
|
||||
github.com/gofrs/uuid/v5 v5.5.1
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
github.com/sagernet/quic-go v0.61.0-sing-box-mod.7
|
||||
github.com/sagernet/sing v0.9.5-0.20260914135810-80bfa92a0607
|
||||
github.com/sagernet/sing v0.9.6-0.20260920093640-814d533a21ef
|
||||
github.com/sagernet/sing-openvpn v0.0.0-20260903200517-e060dda5b1f1
|
||||
github.com/sagernet/sing-quic v0.7.1-0.20260904135313-497364e8ee3e
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
||||
github.com/sagernet/sing-tun v0.9.4-0.20260917030658-12dce01d6d8d
|
||||
github.com/sagernet/sing-tun v0.9.6-0.20260919141142-a39eab51450b
|
||||
github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.5.0.20260916060041-a8fbeb4b0838
|
||||
github.com/spyzhov/ajson v0.9.4
|
||||
github.com/stretchr/testify v1.12.0
|
||||
@@ -157,7 +157,7 @@ require (
|
||||
github.com/sagernet/nftables v0.3.0-mod.4 // indirect
|
||||
github.com/sagernet/sing-anytls v0.0.0-20260904135308-cec2d74334be // indirect
|
||||
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 // indirect
|
||||
github.com/sagernet/sing-mux v0.3.7-0.20260905054442-91d1502591ce // indirect
|
||||
github.com/sagernet/sing-mux v0.3.9-0.20260927144857-0257b84c582a // indirect
|
||||
github.com/sagernet/sing-openconnect v0.0.0-20260903200519-8b89c968949d // indirect
|
||||
github.com/sagernet/sing-shadowtls v0.2.1 // indirect
|
||||
github.com/sagernet/sing-snell v0.0.0-20260904135315-bc5a12ac736f // indirect
|
||||
|
||||
+4
-4
@@ -331,14 +331,14 @@ github.com/sagernet/nftables v0.3.0-mod.4 h1:vnOtcDYeSXv2e5RoRuGH0lrpttQFJ8iC4IC
|
||||
github.com/sagernet/nftables v0.3.0-mod.4/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ=
|
||||
github.com/sagernet/quic-go v0.61.0-sing-box-mod.7 h1:sW2O+DoNF+my1PGlSZ3zG2N+fdxLR3DC0l4sXCM0lBE=
|
||||
github.com/sagernet/quic-go v0.61.0-sing-box-mod.7/go.mod h1:hmLC8GJPp+BrpBgrgJnacvI4fZGIhasuy1ROJJ8GR4E=
|
||||
github.com/sagernet/sing v0.9.5-0.20260914135810-80bfa92a0607 h1:YzA14QltKgRqFKEZYRP1JBL2meuf3Mx3DiDK/U5+/PA=
|
||||
github.com/sagernet/sing v0.9.5-0.20260914135810-80bfa92a0607/go.mod h1:K3Owt3xPhHugvlnlPPxZJ/exXdaJfEPOTNorGk4AXjo=
|
||||
github.com/sagernet/sing v0.9.6-0.20260920093640-814d533a21ef h1:xutL8qCHi73T2KBXc3AAF97qUFZJRAJAe43yCkqenR0=
|
||||
github.com/sagernet/sing v0.9.6-0.20260920093640-814d533a21ef/go.mod h1:K3Owt3xPhHugvlnlPPxZJ/exXdaJfEPOTNorGk4AXjo=
|
||||
github.com/sagernet/sing-anytls v0.0.0-20260904135308-cec2d74334be h1:0uMXu8kcJU0XLc2vfjGH6q6khmECCc5WYpD9dqlu1Xg=
|
||||
github.com/sagernet/sing-anytls v0.0.0-20260904135308-cec2d74334be/go.mod h1:VRKLnj0ObEsN228h+zuzf91p+6sjtYE8SBaLPVsBy5o=
|
||||
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6++yIa8XlDhxPkpR4p+7RUHVY2KTP9CPIGnWmOlO8=
|
||||
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg=
|
||||
github.com/sagernet/sing-mux v0.3.7-0.20260905054442-91d1502591ce h1:ay1pGbzMF4e7iggj+uESNhwMNPbZZajDghcdC4P92Dc=
|
||||
github.com/sagernet/sing-mux v0.3.7-0.20260905054442-91d1502591ce/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
|
||||
github.com/sagernet/sing-mux v0.3.9-0.20260927144857-0257b84c582a h1:VrlU4QE8Bh4kEeLgvvWLBtyTiuXqc6/5vf3B+xQCTRE=
|
||||
github.com/sagernet/sing-mux v0.3.9-0.20260927144857-0257b84c582a/go.mod h1:jricpwCOndDsTEM1TM3piJvhL/2Mm7OhLkm+JadJJ1Q=
|
||||
github.com/sagernet/sing-openconnect v0.0.0-20260903200519-8b89c968949d h1:Jt/Tb0boIr/xRr4sWr2vC/rrVlYNoumZITLEQpqZ8UE=
|
||||
github.com/sagernet/sing-openconnect v0.0.0-20260903200519-8b89c968949d/go.mod h1:4AKZLVcvY3r54UaK2Gbnm7aN8pOwdLz+y4EP0QFZ5Eg=
|
||||
github.com/sagernet/sing-openvpn v0.0.0-20260903200517-e060dda5b1f1 h1:Zn7+pNY5mX1e4DO8oKhTHz5sxqUKNdCWKbfZUtM26Qg=
|
||||
|
||||
@@ -0,0 +1,526 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
std_bufio "bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httptrace"
|
||||
"net/netip"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type forwardOrigin struct {
|
||||
*httptest.Server
|
||||
connections atomic.Int32
|
||||
}
|
||||
|
||||
func newForwardOrigin(t *testing.T) *forwardOrigin {
|
||||
origin := &forwardOrigin{}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/hello", func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.Header.Get("Proxy-Authorization") != "" || request.Header.Get("Proxy-Connection") != "" {
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if _, loaded := request.Header["User-Agent"]; loaded {
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
writer.Header().Set("X-Host", request.Host)
|
||||
writer.Write([]byte("hello"))
|
||||
})
|
||||
mux.HandleFunc("/echo", func(writer http.ResponseWriter, request *http.Request) {
|
||||
body, err := io.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
writer.Write(body)
|
||||
})
|
||||
mux.HandleFunc("/chunked", func(writer http.ResponseWriter, request *http.Request) {
|
||||
flusher := writer.(http.Flusher)
|
||||
for i := 0; i < 3; i++ {
|
||||
writer.Write([]byte("chunk"))
|
||||
flusher.Flush()
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/trailer", func(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.Header().Set("Trailer", "X-Checksum")
|
||||
writer.Write([]byte("hello"))
|
||||
writer.Header().Set("X-Checksum", "5d41402a")
|
||||
})
|
||||
mux.HandleFunc("/stream", func(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
writer.(http.Flusher).Flush()
|
||||
time.Sleep(2 * time.Second)
|
||||
writer.Write([]byte("late"))
|
||||
})
|
||||
mux.HandleFunc("/hints", func(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.Header().Set("Link", "</style.css>; rel=preload")
|
||||
writer.WriteHeader(http.StatusEarlyHints)
|
||||
writer.Write([]byte("body"))
|
||||
})
|
||||
mux.HandleFunc("/ws", func(writer http.ResponseWriter, request *http.Request) {
|
||||
conn, err := websocket.Accept(writer, request, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.CloseNow()
|
||||
messageType, message, err := conn.Read(request.Context())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
conn.Write(request.Context(), messageType, message)
|
||||
conn.Close(websocket.StatusNormalClosure, "")
|
||||
})
|
||||
origin.Server = httptest.NewUnstartedServer(mux)
|
||||
origin.Config.ConnState = func(conn net.Conn, state http.ConnState) {
|
||||
if state == http.StateNew {
|
||||
origin.connections.Add(1)
|
||||
}
|
||||
}
|
||||
origin.Start()
|
||||
t.Cleanup(origin.Close)
|
||||
return origin
|
||||
}
|
||||
|
||||
func (o *forwardOrigin) url(path string) string {
|
||||
return o.URL + path
|
||||
}
|
||||
|
||||
func (o *forwardOrigin) host() string {
|
||||
return strings.TrimPrefix(o.URL, "http://")
|
||||
}
|
||||
|
||||
func startForwardProxy(t *testing.T) {
|
||||
startInstance(t, option.Options{
|
||||
Inbounds: []option.Inbound{
|
||||
{
|
||||
Type: C.TypeMixed,
|
||||
Options: &option.HTTPMixedInboundOptions{
|
||||
ListenOptions: option.ListenOptions{
|
||||
Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
|
||||
ListenPort: clientPort,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: C.TypeHTTP,
|
||||
Options: &option.HTTPInboundOptions{
|
||||
ListenOptions: option.ListenOptions{
|
||||
Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
|
||||
ListenPort: serverPort,
|
||||
},
|
||||
Users: []auth.User{{Username: "sekai", Password: "password"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Outbounds: []option.Outbound{
|
||||
{
|
||||
Type: C.TypeDirect,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func proxyClient(t *testing.T, port uint16) *http.Client {
|
||||
proxyURL, err := url.Parse("http://127.0.0.1:" + strconv.Itoa(int(port)))
|
||||
require.NoError(t, err)
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyURL(proxyURL),
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
t.Cleanup(client.CloseIdleConnections)
|
||||
return client
|
||||
}
|
||||
|
||||
func TestHTTPForwardKeepAlive(t *testing.T) {
|
||||
startForwardProxy(t)
|
||||
origin := newForwardOrigin(t)
|
||||
client := proxyClient(t, clientPort)
|
||||
for i := 0; i < 3; i++ {
|
||||
request, err := http.NewRequest(http.MethodGet, origin.url("/hello"), nil)
|
||||
require.NoError(t, err)
|
||||
request.Header.Set("User-Agent", "")
|
||||
response, err := client.Do(request)
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
require.Equal(t, "hello", string(body))
|
||||
require.Equal(t, origin.host(), response.Header.Get("X-Host"))
|
||||
}
|
||||
response, err := client.Post(origin.url("/echo"), "application/octet-stream", strings.NewReader(strings.Repeat("payload", 1024)))
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, strings.Repeat("payload", 1024), string(body))
|
||||
request, err := http.NewRequest(http.MethodHead, origin.url("/hello"), nil)
|
||||
require.NoError(t, err)
|
||||
request.Header.Set("User-Agent", "")
|
||||
response, err = client.Do(request)
|
||||
require.NoError(t, err)
|
||||
response.Body.Close()
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
require.Equal(t, int64(5), response.ContentLength)
|
||||
require.Equal(t, int32(1), origin.connections.Load())
|
||||
}
|
||||
|
||||
func TestHTTPForwardChunked(t *testing.T) {
|
||||
startForwardProxy(t)
|
||||
origin := newForwardOrigin(t)
|
||||
client := proxyClient(t, clientPort)
|
||||
response, err := client.Get(origin.url("/chunked"))
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "chunkchunkchunk", string(body))
|
||||
require.Equal(t, []string{"chunked"}, response.TransferEncoding)
|
||||
response, err = client.Get(origin.url("/hello"))
|
||||
require.NoError(t, err)
|
||||
response.Body.Close()
|
||||
require.Equal(t, int32(1), origin.connections.Load())
|
||||
}
|
||||
|
||||
func TestHTTPForwardEarlyHints(t *testing.T) {
|
||||
startForwardProxy(t)
|
||||
origin := newForwardOrigin(t)
|
||||
client := proxyClient(t, clientPort)
|
||||
var hints atomic.Int32
|
||||
trace := &httptrace.ClientTrace{
|
||||
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
|
||||
if code == http.StatusEarlyHints && header.Get("Link") != "" {
|
||||
hints.Add(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
request, err := http.NewRequestWithContext(httptrace.WithClientTrace(context.Background(), trace), http.MethodGet, origin.url("/hints"), nil)
|
||||
require.NoError(t, err)
|
||||
response, err := client.Do(request)
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "body", string(body))
|
||||
require.Equal(t, int32(1), hints.Load())
|
||||
|
||||
conn, err := net.Dial("tcp", "127.0.0.1:"+strconv.Itoa(int(serverPort)))
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
_, err = conn.Write([]byte("GET " + origin.url("/hints") + " HTTP/1.1\r\nHost: " + origin.host() + "\r\nProxy-Authorization: " + proxyAuthorization + "\r\n\r\n"))
|
||||
require.NoError(t, err)
|
||||
reader := std_bufio.NewReader(conn)
|
||||
interim, err := http.ReadResponse(reader, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusEarlyHints, interim.StatusCode)
|
||||
require.Empty(t, interim.Header.Values("Content-Length"))
|
||||
require.Empty(t, interim.Header.Values("Transfer-Encoding"))
|
||||
final, err := http.ReadResponse(reader, nil)
|
||||
require.NoError(t, err)
|
||||
body, err = io.ReadAll(final.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "body", string(body))
|
||||
}
|
||||
|
||||
func TestHTTPForwardWebSocket(t *testing.T) {
|
||||
startForwardProxy(t)
|
||||
origin := newForwardOrigin(t)
|
||||
client := proxyClient(t, clientPort)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
conn, _, err := websocket.Dial(ctx, "ws://"+origin.host()+"/ws", &websocket.DialOptions{HTTPClient: client})
|
||||
require.NoError(t, err)
|
||||
defer conn.CloseNow()
|
||||
err = conn.Write(ctx, websocket.MessageText, []byte("ping"))
|
||||
require.NoError(t, err)
|
||||
messageType, message, err := conn.Read(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, websocket.MessageText, messageType)
|
||||
require.Equal(t, "ping", string(message))
|
||||
}
|
||||
|
||||
func TestHTTPForwardConnect(t *testing.T) {
|
||||
startForwardProxy(t)
|
||||
origin := httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.Write([]byte("secure"))
|
||||
}))
|
||||
defer origin.Close()
|
||||
client := proxyClient(t, clientPort)
|
||||
response, err := client.Get(origin.URL)
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "secure", string(body))
|
||||
}
|
||||
|
||||
func TestHTTPForwardLegacyClient(t *testing.T) {
|
||||
startForwardProxy(t)
|
||||
origin := newForwardOrigin(t)
|
||||
conn, err := net.Dial("tcp", "127.0.0.1:"+strconv.Itoa(int(clientPort)))
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
reader := std_bufio.NewReader(conn)
|
||||
for i := 0; i < 2; i++ {
|
||||
_, err = conn.Write([]byte("GET " + origin.url("/hello") + " HTTP/1.0\r\nHost: " + origin.host() + "\r\nUser-Agent: \r\nProxy-Connection: keep-alive\r\n\r\n"))
|
||||
require.NoError(t, err)
|
||||
response, err := http.ReadResponse(reader, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
require.Equal(t, int64(5), response.ContentLength)
|
||||
require.Equal(t, "keep-alive", response.Header.Get("Connection"))
|
||||
body, err := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello", string(body))
|
||||
}
|
||||
_, err = conn.Write([]byte("GET " + origin.url("/chunked") + " HTTP/1.0\r\nHost: " + origin.host() + "\r\nProxy-Connection: keep-alive\r\n\r\n"))
|
||||
require.NoError(t, err)
|
||||
response, err := http.ReadResponse(reader, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
require.Empty(t, response.TransferEncoding)
|
||||
require.Equal(t, int64(-1), response.ContentLength)
|
||||
require.True(t, response.Close)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "chunkchunkchunk", string(body))
|
||||
_, err = reader.ReadByte()
|
||||
require.ErrorIs(t, err, io.EOF)
|
||||
require.Equal(t, int32(1), origin.connections.Load())
|
||||
}
|
||||
|
||||
func TestHTTPForwardAuthRetry(t *testing.T) {
|
||||
startForwardProxy(t)
|
||||
origin := newForwardOrigin(t)
|
||||
conn, err := net.Dial("tcp", "127.0.0.1:"+strconv.Itoa(int(serverPort)))
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
reader := std_bufio.NewReader(conn)
|
||||
_, err = conn.Write([]byte("GET " + origin.url("/hello") + " HTTP/1.1\r\nHost: " + origin.host() + "\r\nUser-Agent: \r\n\r\n"))
|
||||
require.NoError(t, err)
|
||||
response, err := http.ReadResponse(reader, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusProxyAuthRequired, response.StatusCode)
|
||||
require.Contains(t, response.Header.Get("Proxy-Authenticate"), "Basic")
|
||||
require.False(t, response.Close)
|
||||
_, err = io.ReadAll(response.Body)
|
||||
require.NoError(t, err)
|
||||
response.Body.Close()
|
||||
request, err := http.NewRequest(http.MethodGet, origin.url("/hello"), nil)
|
||||
require.NoError(t, err)
|
||||
request.Header.Set("User-Agent", "")
|
||||
request.Header.Set("Proxy-Authorization", "Basic c2VrYWk6cGFzc3dvcmQ=")
|
||||
err = request.WriteProxy(conn)
|
||||
require.NoError(t, err)
|
||||
response, err = http.ReadResponse(reader, request)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello", string(body))
|
||||
_, err = conn.Write([]byte("CONNECT " + origin.host() + " HTTP/1.1\r\nHost: " + origin.host() + "\r\nProxy-Authorization: Basic c2VrYWk6cGFzc3dvcmQ=\r\n\r\n"))
|
||||
require.NoError(t, err)
|
||||
response, err = http.ReadResponse(reader, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
request, err = http.NewRequest(http.MethodGet, origin.url("/hello"), nil)
|
||||
require.NoError(t, err)
|
||||
request.Header.Set("User-Agent", "")
|
||||
err = request.Write(conn)
|
||||
require.NoError(t, err)
|
||||
response, err = http.ReadResponse(reader, request)
|
||||
require.NoError(t, err)
|
||||
body, err = io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello", string(body))
|
||||
}
|
||||
|
||||
func TestHTTPForwardCloseOnError(t *testing.T) {
|
||||
startForwardProxy(t)
|
||||
conn, err := net.Dial("tcp", "127.0.0.1:"+strconv.Itoa(int(serverPort)))
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
_, err = conn.Write([]byte("GET http://127.0.0.1:1/ HTTP/1.1\r\nHost: 127.0.0.1:1\r\nConnection: close\r\nProxy-Authorization: " + proxyAuthorization + "\r\n\r\n"))
|
||||
require.NoError(t, err)
|
||||
reader := std_bufio.NewReader(conn)
|
||||
response, err := http.ReadResponse(reader, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusBadGateway, response.StatusCode)
|
||||
require.True(t, response.Close)
|
||||
_, err = io.ReadAll(response.Body)
|
||||
require.NoError(t, err)
|
||||
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
_, err = reader.ReadByte()
|
||||
require.ErrorIs(t, err, io.EOF)
|
||||
}
|
||||
|
||||
func TestHTTPForwardUserIsolation(t *testing.T) {
|
||||
startInstance(t, option.Options{
|
||||
Inbounds: []option.Inbound{
|
||||
{
|
||||
Type: C.TypeHTTP,
|
||||
Options: &option.HTTPInboundOptions{
|
||||
ListenOptions: option.ListenOptions{
|
||||
Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
|
||||
ListenPort: serverPort,
|
||||
},
|
||||
Users: []auth.User{
|
||||
{Username: "sekai", Password: "password"},
|
||||
{Username: "blocked", Password: "password"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Outbounds: []option.Outbound{
|
||||
{Type: C.TypeDirect},
|
||||
{Type: C.TypeBlock, Tag: "block"},
|
||||
},
|
||||
Route: &option.RouteOptions{
|
||||
Rules: []option.Rule{
|
||||
{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultRule{
|
||||
RawDefaultRule: option.RawDefaultRule{
|
||||
AuthUser: []string{"blocked"},
|
||||
},
|
||||
RuleAction: option.RuleAction{
|
||||
Action: C.RuleActionTypeRoute,
|
||||
RouteOptions: option.RouteActionOptions{Outbound: "block"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
origin := newForwardOrigin(t)
|
||||
conn, err := net.Dial("tcp", "127.0.0.1:"+strconv.Itoa(int(serverPort)))
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
reader := std_bufio.NewReader(conn)
|
||||
for _, authorization := range []string{proxyAuthorization, "Basic YmxvY2tlZDpwYXNzd29yZA==", proxyAuthorization} {
|
||||
_, err = conn.Write([]byte("GET " + origin.url("/hello") + " HTTP/1.1\r\nHost: " + origin.host() + "\r\nUser-Agent: \r\nProxy-Authorization: " + authorization + "\r\n\r\n"))
|
||||
require.NoError(t, err)
|
||||
response, err := http.ReadResponse(reader, nil)
|
||||
require.NoError(t, err)
|
||||
_, err = io.ReadAll(response.Body)
|
||||
require.NoError(t, err)
|
||||
response.Body.Close()
|
||||
if authorization == proxyAuthorization {
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
} else {
|
||||
require.Equal(t, http.StatusBadGateway, response.StatusCode)
|
||||
}
|
||||
require.False(t, response.Close)
|
||||
}
|
||||
require.Equal(t, int32(2), origin.connections.Load())
|
||||
}
|
||||
|
||||
func startEarlyResponseOrigin(t *testing.T) *net.TCPAddr {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
stop := make(chan struct{})
|
||||
t.Cleanup(func() {
|
||||
close(stop)
|
||||
listener.Close()
|
||||
})
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer conn.Close()
|
||||
reader := std_bufio.NewReader(conn)
|
||||
_, err := reader.ReadString('\n')
|
||||
if err == nil {
|
||||
_, err = textproto.NewReader(reader).ReadMIMEHeader()
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
<-stop
|
||||
}()
|
||||
}
|
||||
}()
|
||||
return listener.Addr().(*net.TCPAddr)
|
||||
}
|
||||
|
||||
func startHugeHeaderOrigin(t *testing.T) *net.TCPAddr {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
listener.Close()
|
||||
})
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer conn.Close()
|
||||
_, err := http.ReadRequest(std_bufio.NewReader(conn))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = conn.Write([]byte("HTTP/1.1 200 OK\r\nX-Padding: "))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
padding := bytes.Repeat([]byte("a"), 64<<10)
|
||||
for {
|
||||
_, err = conn.Write(padding)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}()
|
||||
return listener.Addr().(*net.TCPAddr)
|
||||
}
|
||||
|
||||
func TestHTTPForwardHugeUpstreamHeader(t *testing.T) {
|
||||
startForwardProxy(t)
|
||||
origin := startHugeHeaderOrigin(t)
|
||||
client := proxyClient(t, clientPort)
|
||||
response, err := client.Get("http://" + origin.String() + "/")
|
||||
require.NoError(t, err)
|
||||
response.Body.Close()
|
||||
require.Equal(t, http.StatusBadGateway, response.StatusCode)
|
||||
}
|
||||
@@ -0,0 +1,630 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
std_bufio "bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sTLS "github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
sHTTP "github.com/sagernet/sing-box/transport/http"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
const proxyAuthorization = "Basic c2VrYWk6cGFzc3dvcmQ="
|
||||
|
||||
func startTLSHTTPInbound(t *testing.T, certPem string, keyPem string, versions []int, extraOutbounds []option.Outbound) {
|
||||
outbounds := append([]option.Outbound{{Type: C.TypeDirect}}, extraOutbounds...)
|
||||
startInstance(t, option.Options{
|
||||
Inbounds: []option.Inbound{
|
||||
{
|
||||
Type: C.TypeHTTP,
|
||||
Options: &option.HTTPInboundOptions{
|
||||
ListenOptions: option.ListenOptions{
|
||||
Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
|
||||
ListenPort: serverPort,
|
||||
},
|
||||
Version: versions,
|
||||
Users: []auth.User{{Username: "sekai", Password: "password"}},
|
||||
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
|
||||
TLS: &option.InboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: "example.org",
|
||||
CertificatePath: certPem,
|
||||
KeyPath: keyPem,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Outbounds: outbounds,
|
||||
})
|
||||
}
|
||||
|
||||
func dialHTTP2Proxy(t *testing.T, port uint16) *http2.ClientConn {
|
||||
tlsConn, err := tls.Dial("tcp", "127.0.0.1:"+strconv.Itoa(int(port)), &tls.Config{
|
||||
ServerName: "example.org",
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{http2.NextProtoTLS},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http2.NextProtoTLS, tlsConn.ConnectionState().NegotiatedProtocol)
|
||||
clientConn, err := (&http2.Transport{}).NewClientConn(tlsConn)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
clientConn.Close()
|
||||
})
|
||||
return clientConn
|
||||
}
|
||||
|
||||
type http2Tunnel struct {
|
||||
writer *io.PipeWriter
|
||||
response *http.Response
|
||||
}
|
||||
|
||||
func openHTTP2Tunnel(t *testing.T, clientConn *http2.ClientConn, host string, authorization string) *http2Tunnel {
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
request := &http.Request{
|
||||
Method: http.MethodConnect,
|
||||
URL: &url.URL{Host: host},
|
||||
Host: host,
|
||||
Header: make(http.Header),
|
||||
Body: pipeReader,
|
||||
}
|
||||
if authorization != "" {
|
||||
request.Header.Set("Proxy-Authorization", authorization)
|
||||
}
|
||||
response, err := clientConn.RoundTrip(request)
|
||||
require.NoError(t, err)
|
||||
return &http2Tunnel{writer: pipeWriter, response: response}
|
||||
}
|
||||
|
||||
func (t *http2Tunnel) close() {
|
||||
t.writer.Close()
|
||||
t.response.Body.Close()
|
||||
}
|
||||
|
||||
func TestHTTPInboundHTTP2(t *testing.T) {
|
||||
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||
startTLSHTTPInbound(t, certPem, keyPem, nil, nil)
|
||||
origin := newForwardOrigin(t)
|
||||
clientConn := dialHTTP2Proxy(t, serverPort)
|
||||
|
||||
rejected := openHTTP2Tunnel(t, clientConn, origin.host(), "")
|
||||
require.Equal(t, http.StatusProxyAuthRequired, rejected.response.StatusCode)
|
||||
require.Contains(t, rejected.response.Header.Get("Proxy-Authenticate"), "Basic")
|
||||
rejected.close()
|
||||
|
||||
first := openHTTP2Tunnel(t, clientConn, origin.host(), proxyAuthorization)
|
||||
require.Equal(t, http.StatusOK, first.response.StatusCode)
|
||||
second := openHTTP2Tunnel(t, clientConn, origin.host(), proxyAuthorization)
|
||||
require.Equal(t, http.StatusOK, second.response.StatusCode)
|
||||
for _, tunnel := range []*http2Tunnel{second, first} {
|
||||
_, err := tunnel.writer.Write([]byte("GET /hello HTTP/1.1\r\nHost: " + origin.host() + "\r\n\r\n"))
|
||||
require.NoError(t, err)
|
||||
response, err := http.ReadResponse(std_bufio.NewReader(tunnel.response.Body), nil)
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello", string(body))
|
||||
tunnel.close()
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(http.MethodGet, origin.url("/hello"), nil)
|
||||
require.NoError(t, err)
|
||||
request.Header.Set("User-Agent", "")
|
||||
request.Header.Set("Proxy-Authorization", proxyAuthorization)
|
||||
response, err := clientConn.RoundTrip(request)
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
require.Equal(t, "hello", string(body))
|
||||
require.Equal(t, origin.host(), response.Header.Get("X-Host"))
|
||||
|
||||
response, err = clientConn.RoundTrip(request)
|
||||
require.NoError(t, err)
|
||||
body, err = io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello", string(body))
|
||||
require.Equal(t, int32(4), origin.connections.Load())
|
||||
}
|
||||
|
||||
type http2ProxyServer struct {
|
||||
listener net.Listener
|
||||
connections atomic.Int32
|
||||
streams atomic.Int32
|
||||
}
|
||||
|
||||
func startHTTP2ProxyServer(t *testing.T, certPem string, keyPem string) *http2ProxyServer {
|
||||
certificate, err := tls.LoadX509KeyPair(certPem, keyPem)
|
||||
require.NoError(t, err)
|
||||
listener, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{
|
||||
Certificates: []tls.Certificate{certificate},
|
||||
NextProtos: []string{http2.NextProtoTLS},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
server := &http2ProxyServer{listener: listener}
|
||||
h2Server := &http2.Server{}
|
||||
handler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
server.streams.Add(1)
|
||||
if request.Method != http.MethodConnect || request.Header.Get("Proxy-Authorization") != proxyAuthorization {
|
||||
writer.WriteHeader(http.StatusProxyAuthRequired)
|
||||
return
|
||||
}
|
||||
conn, err := net.Dial("tcp", request.Host)
|
||||
if err != nil {
|
||||
writer.WriteHeader(http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
writer.(http.Flusher).Flush()
|
||||
go func() {
|
||||
io.Copy(conn, request.Body)
|
||||
conn.(*net.TCPConn).CloseWrite()
|
||||
}()
|
||||
buffer := make([]byte, 4096)
|
||||
for {
|
||||
n, err := conn.Read(buffer)
|
||||
if n > 0 {
|
||||
_, err = writer.Write(buffer[:n])
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
writer.(http.Flusher).Flush()
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
conn.Close()
|
||||
})
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
server.connections.Add(1)
|
||||
go func() {
|
||||
err = conn.(*tls.Conn).Handshake()
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
h2Server.ServeConn(conn, &http2.ServeConnOpts{Handler: handler})
|
||||
}()
|
||||
}
|
||||
}()
|
||||
t.Cleanup(func() {
|
||||
listener.Close()
|
||||
})
|
||||
return server
|
||||
}
|
||||
|
||||
func (s *http2ProxyServer) port() uint16 {
|
||||
return uint16(s.listener.Addr().(*net.TCPAddr).Port)
|
||||
}
|
||||
|
||||
func TestHTTPOutboundHTTP2(t *testing.T) {
|
||||
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||
proxyServer := startHTTP2ProxyServer(t, certPem, keyPem)
|
||||
startInstance(t, option.Options{
|
||||
Inbounds: []option.Inbound{
|
||||
{
|
||||
Type: C.TypeMixed,
|
||||
Options: &option.HTTPMixedInboundOptions{
|
||||
ListenOptions: option.ListenOptions{
|
||||
Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
|
||||
ListenPort: clientPort,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Outbounds: []option.Outbound{
|
||||
{
|
||||
Type: C.TypeHTTP,
|
||||
Options: &option.HTTPOutboundOptions{
|
||||
ServerOptions: option.ServerOptions{
|
||||
Server: "127.0.0.1",
|
||||
ServerPort: proxyServer.port(),
|
||||
},
|
||||
Username: "sekai",
|
||||
Password: "password",
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: "example.org",
|
||||
CertificatePath: certPem,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
origin := newForwardOrigin(t)
|
||||
for i := 0; i < 3; i++ {
|
||||
client := proxyClient(t, clientPort)
|
||||
request, err := http.NewRequest(http.MethodGet, origin.url("/hello"), nil)
|
||||
require.NoError(t, err)
|
||||
request.Header.Set("User-Agent", "")
|
||||
response, err := client.Do(request)
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello", string(body))
|
||||
client.CloseIdleConnections()
|
||||
}
|
||||
require.Equal(t, int32(1), proxyServer.connections.Load())
|
||||
require.Equal(t, int32(3), proxyServer.streams.Load())
|
||||
}
|
||||
|
||||
func TestHTTPSelfTLS(t *testing.T) {
|
||||
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||
startTLSHTTPInbound(t, certPem, keyPem, nil, nil)
|
||||
startInstance(t, option.Options{
|
||||
Inbounds: []option.Inbound{
|
||||
{
|
||||
Type: C.TypeMixed,
|
||||
Options: &option.HTTPMixedInboundOptions{
|
||||
ListenOptions: option.ListenOptions{
|
||||
Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
|
||||
ListenPort: clientPort,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Outbounds: []option.Outbound{
|
||||
{
|
||||
Type: C.TypeHTTP,
|
||||
Options: &option.HTTPOutboundOptions{
|
||||
ServerOptions: option.ServerOptions{
|
||||
Server: "127.0.0.1",
|
||||
ServerPort: serverPort,
|
||||
},
|
||||
Username: "sekai",
|
||||
Password: "password",
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: "example.org",
|
||||
CertificatePath: certPem,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
origin := newForwardOrigin(t)
|
||||
client := proxyClient(t, clientPort)
|
||||
for i := 0; i < 3; i++ {
|
||||
request, err := http.NewRequest(http.MethodGet, origin.url("/hello"), nil)
|
||||
require.NoError(t, err)
|
||||
request.Header.Set("User-Agent", "")
|
||||
response, err := client.Do(request)
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello", string(body))
|
||||
}
|
||||
require.Equal(t, int32(1), origin.connections.Load())
|
||||
}
|
||||
|
||||
func TestHTTPForwardEarlyResponse(t *testing.T) {
|
||||
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||
startTLSHTTPInbound(t, certPem, keyPem, nil, nil)
|
||||
origin := startEarlyResponseOrigin(t)
|
||||
clientConn := dialHTTP2Proxy(t, serverPort)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://"+origin.String()+"/upload", io.LimitReader(rand.Reader, 4<<20))
|
||||
require.NoError(t, err)
|
||||
request.ContentLength = 4 << 20
|
||||
request.Header.Set("Proxy-Authorization", proxyAuthorization)
|
||||
response, err := clientConn.RoundTrip(request)
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
require.Equal(t, "ok", string(body))
|
||||
}
|
||||
|
||||
func TestHTTPInboundHTTP2Cleartext(t *testing.T) {
|
||||
startInstance(t, option.Options{
|
||||
Inbounds: []option.Inbound{
|
||||
{
|
||||
Type: C.TypeHTTP,
|
||||
Options: &option.HTTPInboundOptions{
|
||||
ListenOptions: option.ListenOptions{
|
||||
Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
|
||||
ListenPort: serverPort,
|
||||
},
|
||||
Version: []int{2},
|
||||
Users: []auth.User{{Username: "sekai", Password: "password"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Outbounds: []option.Outbound{{Type: C.TypeDirect}},
|
||||
})
|
||||
origin := newForwardOrigin(t)
|
||||
http1Conn, err := net.Dial("tcp", "127.0.0.1:"+strconv.Itoa(int(serverPort)))
|
||||
require.NoError(t, err)
|
||||
defer http1Conn.Close()
|
||||
_, err = http1Conn.Write([]byte("CONNECT " + origin.host() + " HTTP/1.1\r\nHost: " + origin.host() + "\r\nProxy-Authorization: " + proxyAuthorization + "\r\n\r\n"))
|
||||
require.NoError(t, err)
|
||||
http1Response, err := http.ReadResponse(std_bufio.NewReader(http1Conn), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusHTTPVersionNotSupported, http1Response.StatusCode)
|
||||
require.True(t, http1Response.Close)
|
||||
|
||||
conn, err := net.Dial("tcp", "127.0.0.1:"+strconv.Itoa(int(serverPort)))
|
||||
require.NoError(t, err)
|
||||
clientConn, err := (&http2.Transport{AllowHTTP: true}).NewClientConn(conn)
|
||||
require.NoError(t, err)
|
||||
defer clientConn.Close()
|
||||
tunnel := openHTTP2Tunnel(t, clientConn, origin.host(), proxyAuthorization)
|
||||
require.Equal(t, http.StatusOK, tunnel.response.StatusCode)
|
||||
_, err = tunnel.writer.Write([]byte("GET /hello HTTP/1.1\r\nHost: " + origin.host() + "\r\n\r\n"))
|
||||
require.NoError(t, err)
|
||||
response, err := http.ReadResponse(std_bufio.NewReader(tunnel.response.Body), nil)
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello", string(body))
|
||||
tunnel.close()
|
||||
|
||||
forwardRequest, err := http.NewRequest(http.MethodGet, origin.url("/hello"), nil)
|
||||
require.NoError(t, err)
|
||||
forwardRequest.Header.Set("Proxy-Authorization", proxyAuthorization)
|
||||
forwardResponse, err := clientConn.RoundTrip(forwardRequest)
|
||||
require.NoError(t, err)
|
||||
forwardResponse.Body.Close()
|
||||
require.Equal(t, http.StatusBadRequest, forwardResponse.StatusCode)
|
||||
}
|
||||
|
||||
func TestHTTPOutboundHTTP2Deadline(t *testing.T) {
|
||||
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||
startTLSHTTPInbound(t, certPem, keyPem, nil, nil)
|
||||
origin := newForwardOrigin(t)
|
||||
detour, err := sTLS.NewDialerFromOptions(globalCtx, log.NewNOPFactory().Logger(), N.SystemDialer, "127.0.0.1", option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: "example.org",
|
||||
CertificatePath: certPem,
|
||||
ALPN: []string{http2.NextProtoTLS},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
client, err := sHTTP.NewClient(sHTTP.ClientOptions{
|
||||
Dialer: detour,
|
||||
Server: M.ParseSocksaddrHostPort("127.0.0.1", serverPort),
|
||||
Username: "sekai",
|
||||
Password: "password",
|
||||
Version: 2,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
conn, err := client.DialContext(context.Background(), N.NetworkTCP, M.ParseSocksaddr(origin.host()))
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
require.NoError(t, conn.SetDeadline(time.Now().Add(100*time.Millisecond)))
|
||||
_, err = conn.Read(make([]byte, 1))
|
||||
require.True(t, E.IsTimeout(err))
|
||||
require.NoError(t, conn.SetDeadline(time.Now().Add(5*time.Second)))
|
||||
_, err = conn.Write([]byte("GET /hello HTTP/1.1\r\nHost: " + origin.host() + "\r\n\r\n"))
|
||||
require.NoError(t, err)
|
||||
response, err := http.ReadResponse(std_bufio.NewReader(conn), nil)
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello", string(body))
|
||||
}
|
||||
|
||||
func TestHTTPOutboundHTTP2NoFallback(t *testing.T) {
|
||||
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||
startInstance(t, option.Options{
|
||||
Inbounds: []option.Inbound{
|
||||
{
|
||||
Type: C.TypeHTTP,
|
||||
Options: &option.HTTPInboundOptions{
|
||||
ListenOptions: option.ListenOptions{
|
||||
Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
|
||||
ListenPort: serverPort,
|
||||
},
|
||||
Version: []int{1},
|
||||
Users: []auth.User{{Username: "sekai", Password: "password"}},
|
||||
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
|
||||
TLS: &option.InboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: "example.org",
|
||||
CertificatePath: certPem,
|
||||
KeyPath: keyPem,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Outbounds: []option.Outbound{{Type: C.TypeDirect}},
|
||||
})
|
||||
startInstance(t, option.Options{
|
||||
Inbounds: []option.Inbound{
|
||||
{
|
||||
Type: C.TypeMixed,
|
||||
Options: &option.HTTPMixedInboundOptions{
|
||||
ListenOptions: option.ListenOptions{
|
||||
Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
|
||||
ListenPort: clientPort,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Outbounds: []option.Outbound{
|
||||
{
|
||||
Type: C.TypeHTTP,
|
||||
Options: &option.HTTPOutboundOptions{
|
||||
ServerOptions: option.ServerOptions{
|
||||
Server: "127.0.0.1",
|
||||
ServerPort: serverPort,
|
||||
},
|
||||
Username: "sekai",
|
||||
Password: "password",
|
||||
Version: 2,
|
||||
DisableVersionFallback: true,
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: "example.org",
|
||||
CertificatePath: certPem,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
origin := newForwardOrigin(t)
|
||||
client := proxyClient(t, clientPort)
|
||||
for i := 0; i < 2; i++ {
|
||||
response, err := client.Get(origin.url("/hello"))
|
||||
require.NoError(t, err)
|
||||
response.Body.Close()
|
||||
require.Equal(t, http.StatusBadGateway, response.StatusCode)
|
||||
}
|
||||
require.Equal(t, int32(0), origin.connections.Load())
|
||||
}
|
||||
|
||||
func startSwitchingProtocolsOrigin(t *testing.T) *net.TCPAddr {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
listener.Close()
|
||||
})
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer conn.Close()
|
||||
_, err := http.ReadRequest(std_bufio.NewReader(conn))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
conn.Write([]byte("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n"))
|
||||
}()
|
||||
}
|
||||
}()
|
||||
return listener.Addr().(*net.TCPAddr)
|
||||
}
|
||||
|
||||
func TestHTTPForwardUnexpectedSwitchingProtocols(t *testing.T) {
|
||||
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||
startTLSHTTPInbound(t, certPem, keyPem, nil, nil)
|
||||
origin := startSwitchingProtocolsOrigin(t)
|
||||
clientConn := dialHTTP2Proxy(t, serverPort)
|
||||
request, err := http.NewRequest(http.MethodGet, "http://"+origin.String()+"/", nil)
|
||||
require.NoError(t, err)
|
||||
request.Header.Set("Proxy-Authorization", proxyAuthorization)
|
||||
response, err := clientConn.RoundTrip(request)
|
||||
require.NoError(t, err)
|
||||
response.Body.Close()
|
||||
require.Equal(t, http.StatusBadGateway, response.StatusCode)
|
||||
}
|
||||
|
||||
func startTruncatedChunkedOrigin(t *testing.T) *net.TCPAddr {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
listener.Close()
|
||||
})
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer conn.Close()
|
||||
_, err := http.ReadRequest(std_bufio.NewReader(conn))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
conn.Write([]byte("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n"))
|
||||
}()
|
||||
}
|
||||
}()
|
||||
return listener.Addr().(*net.TCPAddr)
|
||||
}
|
||||
|
||||
func TestHTTPForwardHTTP2Responses(t *testing.T) {
|
||||
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||
startTLSHTTPInbound(t, certPem, keyPem, nil, nil)
|
||||
clientConn := dialHTTP2Proxy(t, serverPort)
|
||||
|
||||
origin := newForwardOrigin(t)
|
||||
headRequest, err := http.NewRequest(http.MethodHead, origin.url("/hello"), nil)
|
||||
require.NoError(t, err)
|
||||
headRequest.Header.Set("User-Agent", "")
|
||||
headRequest.Header.Set("Proxy-Authorization", proxyAuthorization)
|
||||
headResponse, err := clientConn.RoundTrip(headRequest)
|
||||
require.NoError(t, err)
|
||||
headResponse.Body.Close()
|
||||
require.Equal(t, http.StatusOK, headResponse.StatusCode)
|
||||
require.Equal(t, int64(5), headResponse.ContentLength)
|
||||
|
||||
trailerRequest, err := http.NewRequest(http.MethodGet, origin.url("/trailer"), nil)
|
||||
require.NoError(t, err)
|
||||
trailerRequest.Header.Set("Proxy-Authorization", proxyAuthorization)
|
||||
trailerResponse, err := clientConn.RoundTrip(trailerRequest)
|
||||
require.NoError(t, err)
|
||||
trailerBody, err := io.ReadAll(trailerResponse.Body)
|
||||
require.NoError(t, err)
|
||||
trailerResponse.Body.Close()
|
||||
require.Equal(t, "hello", string(trailerBody))
|
||||
require.Equal(t, "5d41402a", trailerResponse.Trailer.Get("X-Checksum"))
|
||||
|
||||
streamRequest, err := http.NewRequest(http.MethodGet, origin.url("/stream"), nil)
|
||||
require.NoError(t, err)
|
||||
streamRequest.Header.Set("Proxy-Authorization", proxyAuthorization)
|
||||
startTime := time.Now()
|
||||
streamResponse, err := clientConn.RoundTrip(streamRequest)
|
||||
require.NoError(t, err)
|
||||
require.Less(t, time.Since(startTime), time.Second)
|
||||
streamBody, err := io.ReadAll(streamResponse.Body)
|
||||
streamResponse.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "late", string(streamBody))
|
||||
|
||||
truncated := startTruncatedChunkedOrigin(t)
|
||||
request, err := http.NewRequest(http.MethodGet, "http://"+truncated.String()+"/", nil)
|
||||
require.NoError(t, err)
|
||||
request.Header.Set("Proxy-Authorization", proxyAuthorization)
|
||||
response, err := clientConn.RoundTrip(request)
|
||||
require.NoError(t, err)
|
||||
_, err = io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
//go:build with_quic
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
std_bufio "bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
sTLS "github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
sHTTP "github.com/sagernet/sing-box/transport/http"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/protocol/socks"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func dialHTTP3Proxy(t *testing.T, port uint16, enableDatagrams bool) *http3.ClientConn {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
quicConn, err := quic.DialAddrEarly(ctx, "127.0.0.1:"+strconv.Itoa(int(port)), &tls.Config{
|
||||
ServerName: "example.org",
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{http3.NextProtoH3},
|
||||
}, &quic.Config{EnableDatagrams: enableDatagrams})
|
||||
require.NoError(t, err)
|
||||
transport := &http3.Transport{EnableDatagrams: enableDatagrams}
|
||||
clientConn := transport.NewClientConn(quicConn)
|
||||
t.Cleanup(func() {
|
||||
clientConn.CloseWithError(0, "")
|
||||
transport.Close()
|
||||
})
|
||||
return clientConn
|
||||
}
|
||||
|
||||
func TestHTTPInboundHTTP3(t *testing.T) {
|
||||
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||
startTLSHTTPInbound(t, certPem, keyPem, []int{1, 2, 3}, nil)
|
||||
origin := newForwardOrigin(t)
|
||||
clientConn := dialHTTP3Proxy(t, serverPort, true)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
stream, err := clientConn.OpenRequestStream(ctx)
|
||||
require.NoError(t, err)
|
||||
err = stream.SendRequestHeader(&http.Request{
|
||||
Method: http.MethodConnect,
|
||||
URL: &url.URL{Host: origin.host()},
|
||||
Host: origin.host(),
|
||||
Header: http.Header{"Proxy-Authorization": []string{proxyAuthorization}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
response, err := stream.ReadResponse()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
_, err = stream.Write([]byte("GET /hello HTTP/1.1\r\nHost: " + origin.host() + "\r\n\r\n"))
|
||||
require.NoError(t, err)
|
||||
originResponse, err := http.ReadResponse(std_bufio.NewReader(stream), nil)
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(originResponse.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello", string(body))
|
||||
stream.Close()
|
||||
|
||||
forward, err := clientConn.OpenRequestStream(ctx)
|
||||
require.NoError(t, err)
|
||||
err = forward.SendRequestHeader(&http.Request{
|
||||
Method: http.MethodGet,
|
||||
URL: &url.URL{Scheme: "http", Host: origin.host(), Path: "/hello"},
|
||||
Host: origin.host(),
|
||||
Header: http.Header{
|
||||
"Proxy-Authorization": []string{proxyAuthorization},
|
||||
"User-Agent": nil,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, forward.Close())
|
||||
forwardResponse, err := forward.ReadResponse()
|
||||
require.NoError(t, err)
|
||||
body, err = io.ReadAll(forwardResponse.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, forwardResponse.StatusCode)
|
||||
require.Equal(t, "hello", string(body))
|
||||
forward.Close()
|
||||
|
||||
rejected, err := clientConn.OpenRequestStream(ctx)
|
||||
require.NoError(t, err)
|
||||
err = rejected.SendRequestHeader(&http.Request{
|
||||
Method: http.MethodGet,
|
||||
URL: &url.URL{Scheme: "https", Host: origin.host(), Path: "/hello"},
|
||||
Host: origin.host(),
|
||||
Header: http.Header{"Proxy-Authorization": []string{proxyAuthorization}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, rejected.Close())
|
||||
rejectedResponse, err := rejected.ReadResponse()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusBadRequest, rejectedResponse.StatusCode)
|
||||
}
|
||||
|
||||
func openHTTP3ConnectUDP(t *testing.T, ctx context.Context, clientConn *http3.ClientConn, path string) *http3.RequestStream {
|
||||
stream, err := clientConn.OpenRequestStream(ctx)
|
||||
require.NoError(t, err)
|
||||
err = stream.SendRequestHeader(&http.Request{
|
||||
Method: http.MethodConnect,
|
||||
Proto: "connect-udp",
|
||||
URL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "example.org",
|
||||
Path: path,
|
||||
},
|
||||
Host: "example.org",
|
||||
Header: http.Header{
|
||||
"Capsule-Protocol": []string{"?1"},
|
||||
"Proxy-Authorization": []string{proxyAuthorization},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
response, err := stream.ReadResponse()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
return stream
|
||||
}
|
||||
|
||||
func TestHTTPInboundConnectUDPHTTP3(t *testing.T) {
|
||||
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||
startTLSHTTPInbound(t, certPem, keyPem, []int{1, 2, 3}, nil)
|
||||
echo := startUDPEcho(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
clientConn := dialHTTP3Proxy(t, serverPort, true)
|
||||
stream := openHTTP3ConnectUDP(t, ctx, clientConn, connectUDPPath(echo))
|
||||
for i := 0; i < 3; i++ {
|
||||
err := stream.SendDatagram(append([]byte{0}, []byte("ping")...))
|
||||
require.NoError(t, err)
|
||||
datagram, err := stream.ReceiveDatagram(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, append([]byte{0}, []byte("ping")...), datagram)
|
||||
}
|
||||
writeDatagramCapsule(t, stream, []byte("capsule"))
|
||||
datagram, err := stream.ReceiveDatagram(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, append([]byte{0}, []byte("capsule")...), datagram)
|
||||
err = stream.SendDatagram(append([]byte{0x40, 0x00}, []byte("varint")...))
|
||||
require.NoError(t, err)
|
||||
datagram, err = stream.ReceiveDatagram(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, append([]byte{0}, []byte("varint")...), datagram)
|
||||
stream.Close()
|
||||
|
||||
capsuleConn := dialHTTP3Proxy(t, serverPort, false)
|
||||
capsuleStream := openHTTP3ConnectUDP(t, ctx, capsuleConn, connectUDPPath(echo))
|
||||
reader := std_bufio.NewReader(capsuleStream)
|
||||
for i := 0; i < 3; i++ {
|
||||
writeDatagramCapsule(t, capsuleStream, []byte("capsule"))
|
||||
require.Equal(t, "capsule", string(readDatagramCapsule(t, reader)))
|
||||
}
|
||||
capsuleStream.Close()
|
||||
}
|
||||
|
||||
func startHTTP3OnlyInbound(t *testing.T, certPem string, keyPem string) {
|
||||
startInstance(t, option.Options{
|
||||
Inbounds: []option.Inbound{
|
||||
{
|
||||
Type: C.TypeHTTP,
|
||||
Options: &option.HTTPInboundOptions{
|
||||
ListenOptions: option.ListenOptions{
|
||||
Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
|
||||
ListenPort: serverPort,
|
||||
},
|
||||
Version: []int{3},
|
||||
Users: []auth.User{{Username: "sekai", Password: "password"}},
|
||||
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
|
||||
TLS: &option.InboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: "example.org",
|
||||
CertificatePath: certPem,
|
||||
KeyPath: keyPem,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Outbounds: []option.Outbound{{Type: C.TypeDirect}},
|
||||
})
|
||||
}
|
||||
|
||||
func startHTTP3Outbound(t *testing.T, certPem string, disableFallback bool) {
|
||||
startInstance(t, option.Options{
|
||||
Inbounds: []option.Inbound{
|
||||
{
|
||||
Type: C.TypeMixed,
|
||||
Options: &option.HTTPMixedInboundOptions{
|
||||
ListenOptions: option.ListenOptions{
|
||||
Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
|
||||
ListenPort: clientPort,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Outbounds: []option.Outbound{
|
||||
{
|
||||
Type: C.TypeHTTP,
|
||||
Options: &option.HTTPOutboundOptions{
|
||||
ServerOptions: option.ServerOptions{
|
||||
Server: "127.0.0.1",
|
||||
ServerPort: serverPort,
|
||||
},
|
||||
Username: "sekai",
|
||||
Password: "password",
|
||||
Version: 3,
|
||||
DisableVersionFallback: disableFallback,
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: "example.org",
|
||||
CertificatePath: certPem,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTPOutboundHTTP3(t *testing.T) {
|
||||
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||
startHTTP3OnlyInbound(t, certPem, keyPem)
|
||||
_, err := net.DialTimeout("tcp", "127.0.0.1:"+strconv.Itoa(int(serverPort)), time.Second)
|
||||
require.Error(t, err)
|
||||
startHTTP3Outbound(t, certPem, true)
|
||||
origin := newForwardOrigin(t)
|
||||
client := proxyClient(t, clientPort)
|
||||
for i := 0; i < 3; i++ {
|
||||
request, err := http.NewRequest(http.MethodGet, origin.url("/hello"), nil)
|
||||
require.NoError(t, err)
|
||||
request.Header.Set("User-Agent", "")
|
||||
response, err := client.Do(request)
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello", string(body))
|
||||
}
|
||||
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "")
|
||||
dialUDP := func() (net.PacketConn, error) {
|
||||
return dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", testPort))
|
||||
}
|
||||
require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP))
|
||||
require.NoError(t, testLargeDataWithPacketConn(t, testPort, dialUDP))
|
||||
}
|
||||
|
||||
func TestHTTPOutboundHTTP3Fallback(t *testing.T) {
|
||||
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||
startInstance(t, option.Options{
|
||||
Inbounds: []option.Inbound{
|
||||
{
|
||||
Type: C.TypeHTTP,
|
||||
Options: &option.HTTPInboundOptions{
|
||||
ListenOptions: option.ListenOptions{
|
||||
Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
|
||||
ListenPort: serverPort,
|
||||
},
|
||||
Version: []int{1, 2},
|
||||
Users: []auth.User{{Username: "sekai", Password: "password"}},
|
||||
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
|
||||
TLS: &option.InboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: "example.org",
|
||||
CertificatePath: certPem,
|
||||
KeyPath: keyPem,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Outbounds: []option.Outbound{{Type: C.TypeDirect}},
|
||||
})
|
||||
startHTTP3Outbound(t, certPem, false)
|
||||
origin := newForwardOrigin(t)
|
||||
client := proxyClient(t, clientPort)
|
||||
request, err := http.NewRequest(http.MethodGet, origin.url("/hello"), nil)
|
||||
require.NoError(t, err)
|
||||
request.Header.Set("User-Agent", "")
|
||||
response, err := client.Do(request)
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello", string(body))
|
||||
}
|
||||
|
||||
func TestHTTPOutboundHTTP3NoFallback(t *testing.T) {
|
||||
_, certPem, _ := createSelfSignedCertificate(t, "example.org")
|
||||
startHTTP3Outbound(t, certPem, true)
|
||||
origin := newForwardOrigin(t)
|
||||
client := proxyClient(t, clientPort)
|
||||
request, err := http.NewRequest(http.MethodGet, origin.url("/hello"), nil)
|
||||
require.NoError(t, err)
|
||||
response, err := client.Do(request)
|
||||
require.NoError(t, err)
|
||||
response.Body.Close()
|
||||
require.Equal(t, http.StatusBadGateway, response.StatusCode)
|
||||
}
|
||||
|
||||
func TestHTTPOutboundHTTP3Cancel(t *testing.T) {
|
||||
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||
certificate, err := tls.LoadX509KeyPair(certPem, keyPem)
|
||||
require.NoError(t, err)
|
||||
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
|
||||
require.NoError(t, err)
|
||||
quicListener, err := quic.ListenEarly(udpConn, &tls.Config{
|
||||
Certificates: []tls.Certificate{certificate},
|
||||
NextProtos: []string{http3.NextProtoH3},
|
||||
}, &quic.Config{EnableDatagrams: true})
|
||||
require.NoError(t, err)
|
||||
server := &http3.Server{
|
||||
EnableDatagrams: true,
|
||||
Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
<-request.Context().Done()
|
||||
}),
|
||||
}
|
||||
go server.ServeListener(quicListener)
|
||||
t.Cleanup(func() {
|
||||
server.Close()
|
||||
udpConn.Close()
|
||||
})
|
||||
serverAddress := M.SocksaddrFromNet(udpConn.LocalAddr())
|
||||
tlsConfig, err := sTLS.NewClient(context.Background(), log.NewNOPFactory().Logger(), serverAddress.AddrString(), option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: "example.org",
|
||||
CertificatePath: certPem,
|
||||
ALPN: []string{http3.NextProtoH3},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
client, err := sHTTP.NewClient(sHTTP.ClientOptions{
|
||||
RawDialer: N.SystemDialer,
|
||||
TLSConfig: tlsConfig,
|
||||
Server: serverAddress,
|
||||
Version: 3,
|
||||
DisableVersionFallback: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
startTime := time.Now()
|
||||
_, err = client.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddrHostPort("127.0.0.1", testPort))
|
||||
require.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
require.Less(t, time.Since(startTime), 5*time.Second)
|
||||
packetCtx, packetCancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer packetCancel()
|
||||
_, err = client.ListenPacket(packetCtx, M.ParseSocksaddrHostPort("127.0.0.1", testPort))
|
||||
require.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHTTPJSONConfig(t *testing.T) {
|
||||
serverOptions, err := json.UnmarshalExtendedContext[option.Options](globalCtx, []byte(`{
|
||||
"inbounds": [{
|
||||
"type": "http",
|
||||
"listen": "127.0.0.1",
|
||||
"listen_port": `+strconv.Itoa(int(serverPort))+`,
|
||||
"users": [{"username": "sekai", "password": "password"}],
|
||||
"stream_receive_window": "1 MB"
|
||||
}],
|
||||
"outbounds": [{"type": "direct"}]
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
startInstance(t, serverOptions)
|
||||
clientOptions, err := json.UnmarshalExtendedContext[option.Options](globalCtx, []byte(`{
|
||||
"inbounds": [{
|
||||
"type": "mixed",
|
||||
"listen": "127.0.0.1",
|
||||
"listen_port": `+strconv.Itoa(int(clientPort))+`
|
||||
}],
|
||||
"outbounds": [{
|
||||
"type": "http",
|
||||
"server": "127.0.0.1",
|
||||
"server_port": `+strconv.Itoa(int(serverPort))+`,
|
||||
"username": "sekai",
|
||||
"password": "password",
|
||||
"path": "/proxy"
|
||||
}]
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
startInstance(t, clientOptions)
|
||||
origin := newForwardOrigin(t)
|
||||
client := proxyClient(t, clientPort)
|
||||
request, err := http.NewRequest(http.MethodGet, origin.url("/hello"), nil)
|
||||
require.NoError(t, err)
|
||||
request.Header.Set("User-Agent", "")
|
||||
response, err := client.Do(request)
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello", string(body))
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
std_bufio "bufio"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
sHTTP "github.com/sagernet/sing-box/transport/http"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/protocol/socks"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
func appendVarint(b []byte, value uint64) []byte {
|
||||
switch {
|
||||
case value < 1<<6:
|
||||
return append(b, byte(value))
|
||||
case value < 1<<14:
|
||||
return binary.BigEndian.AppendUint16(b, uint16(value)|0x4000)
|
||||
case value < 1<<30:
|
||||
return binary.BigEndian.AppendUint32(b, uint32(value)|0x80000000)
|
||||
default:
|
||||
return binary.BigEndian.AppendUint64(b, value|0xC000000000000000)
|
||||
}
|
||||
}
|
||||
|
||||
func readVarint(t *testing.T, reader *std_bufio.Reader) uint64 {
|
||||
first, err := reader.ReadByte()
|
||||
require.NoError(t, err)
|
||||
length := 1 << (first >> 6)
|
||||
value := uint64(first & 0x3f)
|
||||
for i := 1; i < length; i++ {
|
||||
next, err := reader.ReadByte()
|
||||
require.NoError(t, err)
|
||||
value = value<<8 | uint64(next)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func writeDatagramCapsule(t *testing.T, writer io.Writer, payload []byte) {
|
||||
capsule := appendVarint(nil, 0)
|
||||
capsule = appendVarint(capsule, uint64(1+len(payload)))
|
||||
capsule = append(capsule, 0)
|
||||
capsule = append(capsule, payload...)
|
||||
_, err := writer.Write(capsule)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func readDatagramCapsule(t *testing.T, reader *std_bufio.Reader) []byte {
|
||||
capsuleType := readVarint(t, reader)
|
||||
require.Equal(t, uint64(0), capsuleType)
|
||||
length := readVarint(t, reader)
|
||||
contextID, err := reader.ReadByte()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, byte(0), contextID)
|
||||
payload := make([]byte, length-1)
|
||||
_, err = io.ReadFull(reader, payload)
|
||||
require.NoError(t, err)
|
||||
return payload
|
||||
}
|
||||
|
||||
func startUDPEcho(t *testing.T) *net.UDPAddr {
|
||||
conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
conn.Close()
|
||||
})
|
||||
go func() {
|
||||
buffer := make([]byte, 65535)
|
||||
for {
|
||||
n, addr, err := conn.ReadFromUDP(buffer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
conn.WriteToUDP(buffer[:n], addr)
|
||||
}
|
||||
}()
|
||||
return conn.LocalAddr().(*net.UDPAddr)
|
||||
}
|
||||
|
||||
func connectUDPPath(target *net.UDPAddr) string {
|
||||
return "/.well-known/masque/udp/" + target.IP.String() + "/" + strconv.Itoa(target.Port) + "/"
|
||||
}
|
||||
|
||||
func TestHTTPInboundConnectUDP(t *testing.T) {
|
||||
startForwardProxy(t)
|
||||
echo := startUDPEcho(t)
|
||||
conn, err := net.Dial("tcp", "127.0.0.1:"+strconv.Itoa(int(serverPort)))
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
_, err = conn.Write([]byte("GET " + connectUDPPath(echo) + " HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: Upgrade\r\nUpgrade: connect-udp\r\nCapsule-Protocol: ?1\r\nProxy-Authorization: " + proxyAuthorization + "\r\n\r\n"))
|
||||
require.NoError(t, err)
|
||||
reader := std_bufio.NewReader(conn)
|
||||
response, err := http.ReadResponse(reader, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusSwitchingProtocols, response.StatusCode)
|
||||
require.Equal(t, "connect-udp", response.Header.Get("Upgrade"))
|
||||
for i := 0; i < 3; i++ {
|
||||
writeDatagramCapsule(t, conn, []byte("ping"))
|
||||
require.Equal(t, "ping", string(readDatagramCapsule(t, reader)))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPInboundConnectUDPHTTP2(t *testing.T) {
|
||||
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||
startTLSHTTPInbound(t, certPem, keyPem, nil, nil)
|
||||
echo := startUDPEcho(t)
|
||||
clientConn := dialHTTP2Proxy(t, serverPort)
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
request := &http.Request{
|
||||
Method: http.MethodConnect,
|
||||
URL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "example.org",
|
||||
Path: connectUDPPath(echo),
|
||||
},
|
||||
Host: "example.org",
|
||||
Header: http.Header{
|
||||
":protocol": []string{"connect-udp"},
|
||||
"Capsule-Protocol": []string{"?1"},
|
||||
"Proxy-Authorization": []string{proxyAuthorization},
|
||||
},
|
||||
Body: pipeReader,
|
||||
}
|
||||
response, err := clientConn.RoundTrip(request)
|
||||
require.NoError(t, err)
|
||||
defer response.Body.Close()
|
||||
defer pipeWriter.Close()
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
require.Equal(t, "?1", response.Header.Get("Capsule-Protocol"))
|
||||
reader := std_bufio.NewReader(response.Body)
|
||||
for i := 0; i < 3; i++ {
|
||||
writeDatagramCapsule(t, pipeWriter, []byte("ping"))
|
||||
require.Equal(t, "ping", string(readDatagramCapsule(t, reader)))
|
||||
}
|
||||
}
|
||||
|
||||
func testHTTPOutboundUDP(t *testing.T, tlsOptions *option.OutboundTLSOptions) {
|
||||
startInstance(t, option.Options{
|
||||
Inbounds: []option.Inbound{
|
||||
{
|
||||
Type: C.TypeMixed,
|
||||
Options: &option.HTTPMixedInboundOptions{
|
||||
ListenOptions: option.ListenOptions{
|
||||
Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
|
||||
ListenPort: clientPort,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Outbounds: []option.Outbound{
|
||||
{
|
||||
Type: C.TypeHTTP,
|
||||
Options: &option.HTTPOutboundOptions{
|
||||
ServerOptions: option.ServerOptions{
|
||||
Server: "127.0.0.1",
|
||||
ServerPort: serverPort,
|
||||
},
|
||||
Username: "sekai",
|
||||
Password: "password",
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: tlsOptions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "")
|
||||
dialUDP := func() (net.PacketConn, error) {
|
||||
return dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", testPort))
|
||||
}
|
||||
require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP))
|
||||
require.NoError(t, testLargeDataWithPacketConn(t, testPort, dialUDP))
|
||||
}
|
||||
|
||||
func TestHTTPOutboundUDP(t *testing.T) {
|
||||
startInstance(t, option.Options{
|
||||
Inbounds: []option.Inbound{
|
||||
{
|
||||
Type: C.TypeHTTP,
|
||||
Options: &option.HTTPInboundOptions{
|
||||
ListenOptions: option.ListenOptions{
|
||||
Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
|
||||
ListenPort: serverPort,
|
||||
},
|
||||
Users: []auth.User{{Username: "sekai", Password: "password"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Outbounds: []option.Outbound{
|
||||
{
|
||||
Type: C.TypeDirect,
|
||||
},
|
||||
},
|
||||
})
|
||||
testHTTPOutboundUDP(t, nil)
|
||||
}
|
||||
|
||||
func TestHTTPOutboundUDPHTTP2(t *testing.T) {
|
||||
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||
startTLSHTTPInbound(t, certPem, keyPem, nil, nil)
|
||||
testHTTPOutboundUDP(t, &option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: "example.org",
|
||||
CertificatePath: certPem,
|
||||
ALPN: []string{http2.NextProtoTLS},
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTPOutboundUDPIPv6Path(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
listener.Close()
|
||||
})
|
||||
pathErr := make(chan error, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
reader := std_bufio.NewReader(conn)
|
||||
request, err := http.ReadRequest(reader)
|
||||
if err != nil {
|
||||
pathErr <- err
|
||||
return
|
||||
}
|
||||
expectedPath := "/.well-known/masque/udp/::1/" + strconv.Itoa(int(testPort)) + "/"
|
||||
expectedRawPath := "/.well-known/masque/udp/%3A%3A1/" + strconv.Itoa(int(testPort)) + "/"
|
||||
if request.URL.Path != expectedPath || request.URL.RawPath != expectedRawPath {
|
||||
pathErr <- E.New("unexpected request URI: ", request.RequestURI)
|
||||
return
|
||||
}
|
||||
pathErr <- nil
|
||||
_, err = conn.Write([]byte("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: connect-udp\r\nCapsule-Protocol: ?1\r\n\r\n"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
io.Copy(conn, reader)
|
||||
}()
|
||||
startInstance(t, option.Options{
|
||||
Inbounds: []option.Inbound{
|
||||
{
|
||||
Type: C.TypeMixed,
|
||||
Options: &option.HTTPMixedInboundOptions{
|
||||
ListenOptions: option.ListenOptions{
|
||||
Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
|
||||
ListenPort: clientPort,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Outbounds: []option.Outbound{
|
||||
{
|
||||
Type: C.TypeHTTP,
|
||||
Options: &option.HTTPOutboundOptions{
|
||||
ServerOptions: option.ServerOptions{
|
||||
Server: "127.0.0.1",
|
||||
ServerPort: uint16(listener.Addr().(*net.TCPAddr).Port),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "")
|
||||
packetConn, err := dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("::1", testPort))
|
||||
require.NoError(t, err)
|
||||
defer packetConn.Close()
|
||||
destination := &net.UDPAddr{IP: net.IPv6loopback, Port: int(testPort)}
|
||||
_, err = packetConn.WriteTo([]byte("ping"), destination)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, <-pathErr)
|
||||
packetConn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
buffer := make([]byte, 64)
|
||||
n, _, err := packetConn.ReadFrom(buffer)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "ping", string(buffer[:n]))
|
||||
packetConn.SetReadDeadline(time.Now())
|
||||
_, _, err = packetConn.ReadFrom(buffer)
|
||||
require.True(t, E.IsTimeout(err))
|
||||
}
|
||||
|
||||
func TestHTTPOutboundUDPDialContext(t *testing.T) {
|
||||
startForwardProxy(t)
|
||||
echo := startUDPEcho(t)
|
||||
client, err := sHTTP.NewClient(sHTTP.ClientOptions{
|
||||
Server: M.ParseSocksaddrHostPort("127.0.0.1", serverPort),
|
||||
Username: "sekai",
|
||||
Password: "password",
|
||||
Version: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
packetConn, err := client.ListenPacket(ctx, M.SocksaddrFromNet(echo))
|
||||
require.NoError(t, err)
|
||||
defer packetConn.Close()
|
||||
cancel()
|
||||
packetConn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
_, err = packetConn.WriteTo([]byte("ping"), echo)
|
||||
require.NoError(t, err)
|
||||
buffer := make([]byte, 64)
|
||||
n, _, err := packetConn.ReadFrom(buffer)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "ping", string(buffer[:n]))
|
||||
packetConn.SetReadDeadline(time.Now())
|
||||
_, _, err = packetConn.ReadFrom(buffer)
|
||||
require.True(t, E.IsTimeout(err))
|
||||
}
|
||||
|
||||
func TestHTTPOutboundUDPStreamClosed(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
listener.Close()
|
||||
})
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer conn.Close()
|
||||
reader := std_bufio.NewReader(conn)
|
||||
_, err := http.ReadRequest(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = conn.Write([]byte("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: connect-udp\r\nCapsule-Protocol: ?1\r\n\r\n"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
io.ReadFull(reader, make([]byte, 7))
|
||||
}()
|
||||
}
|
||||
}()
|
||||
client, err := sHTTP.NewClient(sHTTP.ClientOptions{
|
||||
Server: M.SocksaddrFromNet(listener.Addr()),
|
||||
Version: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
packetConn, err := client.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", testPort))
|
||||
require.NoError(t, err)
|
||||
defer packetConn.Close()
|
||||
_, err = packetConn.WriteTo([]byte("ping"), &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: int(testPort)})
|
||||
require.NoError(t, err)
|
||||
packetConn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
_, _, err = packetConn.ReadFrom(make([]byte, 64))
|
||||
require.Error(t, err)
|
||||
require.False(t, E.IsTimeout(err))
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
func TestOptionsWrapper(t *testing.T) {
|
||||
inbound := option.Inbound{
|
||||
Type: C.TypeHTTP,
|
||||
Options: &option.HTTPMixedInboundOptions{
|
||||
Options: &option.HTTPInboundOptions{
|
||||
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
|
||||
TLS: &option.InboundTLSOptions{
|
||||
Enabled: true,
|
||||
@@ -26,5 +26,5 @@ func TestOptionsWrapper(t *testing.T) {
|
||||
require.NotNil(t, tlsOptions, "find inbound tls options")
|
||||
tlsOptions.Enabled = false
|
||||
tlsOptionsWrapper.ReplaceInboundTLSOptions(tlsOptions)
|
||||
require.False(t, inbound.Options.(*option.HTTPMixedInboundOptions).TLS.Enabled, "replace tls enabled")
|
||||
require.False(t, inbound.Options.(*option.HTTPInboundOptions).TLS.Enabled, "replace tls enabled")
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
std_bufio "bufio"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
const (
|
||||
capsuleTypeDatagram = 0
|
||||
capsuleHeadroom = 1 + 4 + 1
|
||||
maxCapsuleLength = 1 << 20
|
||||
)
|
||||
|
||||
func readVarint(reader *std_bufio.Reader) (uint64, int, error) {
|
||||
first, err := reader.ReadByte()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
length := 1 << (first >> 6)
|
||||
value := uint64(first & 0x3f)
|
||||
for i := 1; i < length; i++ {
|
||||
var next byte
|
||||
next, err = reader.ReadByte()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
value = value<<8 | uint64(next)
|
||||
}
|
||||
return value, length, nil
|
||||
}
|
||||
|
||||
func decodeVarint(b []byte) (uint64, int, bool) {
|
||||
if len(b) == 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
length := 1 << (b[0] >> 6)
|
||||
if len(b) < length {
|
||||
return 0, 0, false
|
||||
}
|
||||
value := uint64(b[0] & 0x3f)
|
||||
for i := 1; i < length; i++ {
|
||||
value = value<<8 | uint64(b[i])
|
||||
}
|
||||
return value, length, true
|
||||
}
|
||||
|
||||
func varintLen(value uint64) int {
|
||||
switch {
|
||||
case value < 1<<6:
|
||||
return 1
|
||||
case value < 1<<14:
|
||||
return 2
|
||||
case value < 1<<30:
|
||||
return 4
|
||||
default:
|
||||
return 8
|
||||
}
|
||||
}
|
||||
|
||||
func putVarint(b []byte, value uint64) int {
|
||||
switch varintLen(value) {
|
||||
case 1:
|
||||
b[0] = byte(value)
|
||||
return 1
|
||||
case 2:
|
||||
binary.BigEndian.PutUint16(b, uint16(value)|0x4000)
|
||||
return 2
|
||||
case 4:
|
||||
binary.BigEndian.PutUint32(b, uint32(value)|0x80000000)
|
||||
return 4
|
||||
default:
|
||||
binary.BigEndian.PutUint64(b, value|0xC000000000000000)
|
||||
return 8
|
||||
}
|
||||
}
|
||||
|
||||
func readDatagramCapsule(reader *std_bufio.Reader, buffer *buf.Buffer) error {
|
||||
for {
|
||||
capsuleType, _, err := readVarint(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
length, _, err := readVarint(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if length > maxCapsuleLength {
|
||||
return E.New("capsule too large: ", length)
|
||||
}
|
||||
if capsuleType != capsuleTypeDatagram {
|
||||
_, err = reader.Discard(int(length))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
contextID, contextLength, err := readVarint(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if uint64(contextLength) > length {
|
||||
return E.New("malformed datagram capsule")
|
||||
}
|
||||
payloadLength := int(length) - contextLength
|
||||
if contextID != 0 || payloadLength > buffer.FreeLen() {
|
||||
_, err = reader.Discard(payloadLength)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
_, err = buffer.ReadFullFrom(reader, payloadLength)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func prependContextID(buffer *buf.Buffer) *buf.Buffer {
|
||||
if buffer.Start() >= 1 {
|
||||
buffer.ExtendHeader(1)[0] = 0
|
||||
return buffer
|
||||
}
|
||||
datagram := buf.NewSize(1 + buffer.Len())
|
||||
datagram.WriteByte(0)
|
||||
datagram.Write(buffer.Bytes())
|
||||
buffer.Release()
|
||||
return datagram
|
||||
}
|
||||
|
||||
func writeDatagramCapsule(writer io.Writer, datagram *buf.Buffer) error {
|
||||
length := uint64(datagram.Len())
|
||||
headerLength := 1 + varintLen(length)
|
||||
var capsule *buf.Buffer
|
||||
if datagram.Start() >= headerLength {
|
||||
header := datagram.ExtendHeader(headerLength)
|
||||
header[0] = capsuleTypeDatagram
|
||||
putVarint(header[1:], length)
|
||||
capsule = datagram
|
||||
} else {
|
||||
capsule = buf.NewSize(headerLength + datagram.Len())
|
||||
header := capsule.Extend(headerLength)
|
||||
header[0] = capsuleTypeDatagram
|
||||
putVarint(header[1:], length)
|
||||
capsule.Write(datagram.Bytes())
|
||||
datagram.Release()
|
||||
}
|
||||
defer capsule.Release()
|
||||
_, err := writer.Write(capsule.Bytes())
|
||||
return err
|
||||
}
|
||||
|
||||
type capsuleConn struct {
|
||||
reader *std_bufio.Reader
|
||||
writer io.Writer
|
||||
upstream net.Conn
|
||||
destination M.Socksaddr
|
||||
writeAccess sync.Mutex
|
||||
}
|
||||
|
||||
func newCapsuleConn(reader *std_bufio.Reader, upstream net.Conn, destination M.Socksaddr) *capsuleConn {
|
||||
return &capsuleConn{
|
||||
reader: reader,
|
||||
writer: upstream,
|
||||
upstream: upstream,
|
||||
destination: destination,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *capsuleConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) {
|
||||
err := readDatagramCapsule(c.reader, buffer)
|
||||
if err != nil {
|
||||
return M.Socksaddr{}, err
|
||||
}
|
||||
return c.destination, nil
|
||||
}
|
||||
|
||||
func (c *capsuleConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||
c.writeAccess.Lock()
|
||||
defer c.writeAccess.Unlock()
|
||||
return writeDatagramCapsule(c.writer, prependContextID(buffer))
|
||||
}
|
||||
|
||||
func (c *capsuleConn) Close() error {
|
||||
return c.upstream.Close()
|
||||
}
|
||||
|
||||
func (c *capsuleConn) LocalAddr() net.Addr {
|
||||
return c.upstream.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *capsuleConn) SetDeadline(t time.Time) error {
|
||||
return c.upstream.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *capsuleConn) SetReadDeadline(t time.Time) error {
|
||||
return c.upstream.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *capsuleConn) SetWriteDeadline(t time.Time) error {
|
||||
return c.upstream.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (c *capsuleConn) NeedAdditionalReadDeadline() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *capsuleConn) FrontHeadroom() int {
|
||||
return capsuleHeadroom
|
||||
}
|
||||
|
||||
func (c *capsuleConn) Upstream() any {
|
||||
return c.upstream
|
||||
}
|
||||
|
||||
var _ N.PacketConn = (*capsuleConn)(nil)
|
||||
|
||||
type DatagramStream interface {
|
||||
io.ReadWriteCloser
|
||||
SendDatagram(payload []byte) error
|
||||
ReceiveDatagram(ctx context.Context) ([]byte, error)
|
||||
}
|
||||
|
||||
var (
|
||||
HTTP3StreamFunc func(ctx context.Context, writer http.ResponseWriter) (DatagramStream, bool)
|
||||
ErrDatagramUnsupported = E.New("datagram unsupported")
|
||||
)
|
||||
|
||||
type http3PacketConn struct {
|
||||
stream DatagramStream
|
||||
reader *std_bufio.Reader
|
||||
destination M.Socksaddr
|
||||
localAddr net.Addr
|
||||
packets chan *buf.Buffer
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
closeOnce sync.Once
|
||||
waitGroup sync.WaitGroup
|
||||
writeAccess sync.Mutex
|
||||
err error
|
||||
}
|
||||
|
||||
func newHTTP3PacketConn(stream DatagramStream, destination M.Socksaddr, localAddr net.Addr) *http3PacketConn {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
conn := &http3PacketConn{
|
||||
stream: stream,
|
||||
reader: std_bufio.NewReader(stream),
|
||||
destination: destination,
|
||||
localAddr: localAddr,
|
||||
packets: make(chan *buf.Buffer, 64),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
conn.waitGroup.Add(2)
|
||||
go conn.loopDatagram()
|
||||
go conn.loopCapsule()
|
||||
return conn
|
||||
}
|
||||
|
||||
func (c *http3PacketConn) loopDatagram() {
|
||||
defer c.waitGroup.Done()
|
||||
for {
|
||||
datagram, err := c.stream.ReceiveDatagram(c.ctx)
|
||||
if err != nil {
|
||||
c.closeWithError(err)
|
||||
return
|
||||
}
|
||||
contextID, contextLength, valid := decodeVarint(datagram)
|
||||
if !valid || contextID != 0 {
|
||||
continue
|
||||
}
|
||||
buffer := buf.NewSize(len(datagram) - contextLength)
|
||||
buffer.Write(datagram[contextLength:])
|
||||
select {
|
||||
case c.packets <- buffer:
|
||||
case <-c.ctx.Done():
|
||||
buffer.Release()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *http3PacketConn) loopCapsule() {
|
||||
defer c.waitGroup.Done()
|
||||
for {
|
||||
buffer := buf.NewPacket()
|
||||
err := readDatagramCapsule(c.reader, buffer)
|
||||
if err != nil {
|
||||
buffer.Release()
|
||||
c.closeWithError(err)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case c.packets <- buffer:
|
||||
case <-c.ctx.Done():
|
||||
buffer.Release()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *http3PacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) {
|
||||
for {
|
||||
select {
|
||||
case packet := <-c.packets:
|
||||
if packet.Len() > buffer.FreeLen() {
|
||||
packet.Release()
|
||||
continue
|
||||
}
|
||||
buffer.Write(packet.Bytes())
|
||||
packet.Release()
|
||||
return c.destination, nil
|
||||
case <-c.ctx.Done():
|
||||
return M.Socksaddr{}, c.err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *http3PacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||
datagram := prependContextID(buffer)
|
||||
err := c.stream.SendDatagram(datagram.Bytes())
|
||||
if err == nil {
|
||||
datagram.Release()
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, ErrDatagramUnsupported) {
|
||||
datagram.Release()
|
||||
return err
|
||||
}
|
||||
c.writeAccess.Lock()
|
||||
defer c.writeAccess.Unlock()
|
||||
return writeDatagramCapsule(c.stream, datagram)
|
||||
}
|
||||
|
||||
func (c *http3PacketConn) closeWithError(err error) {
|
||||
c.closeOnce.Do(func() {
|
||||
c.err = err
|
||||
c.cancel()
|
||||
c.stream.Close()
|
||||
go func() {
|
||||
c.waitGroup.Wait()
|
||||
for {
|
||||
select {
|
||||
case packet := <-c.packets:
|
||||
packet.Release()
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
func (c *http3PacketConn) Close() error {
|
||||
c.closeWithError(net.ErrClosed)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *http3PacketConn) wait(ctx context.Context) {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
case <-ctx.Done():
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *http3PacketConn) LocalAddr() net.Addr {
|
||||
return c.localAddr
|
||||
}
|
||||
|
||||
func (c *http3PacketConn) SetDeadline(t time.Time) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (c *http3PacketConn) SetReadDeadline(t time.Time) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (c *http3PacketConn) SetWriteDeadline(t time.Time) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (c *http3PacketConn) NeedAdditionalReadDeadline() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *http3PacketConn) FrontHeadroom() int {
|
||||
return capsuleHeadroom
|
||||
}
|
||||
|
||||
var _ N.PacketConn = (*http3PacketConn)(nil)
|
||||
@@ -0,0 +1,403 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
std_bufio "bufio"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/badhttp"
|
||||
"github.com/sagernet/sing-box/common/httpclient"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
"github.com/sagernet/sing/common/bufio/deadline"
|
||||
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"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
type tlsDialer interface {
|
||||
DialTLSContext(ctx context.Context, destination M.Socksaddr) (aTLS.Conn, error)
|
||||
}
|
||||
|
||||
type ClientOptions struct {
|
||||
Dialer N.Dialer
|
||||
HTTP1Dialer N.Dialer
|
||||
RawDialer N.Dialer
|
||||
TLSConfig aTLS.Config
|
||||
Server M.Socksaddr
|
||||
Username string
|
||||
Password string
|
||||
Path string
|
||||
Headers http.Header
|
||||
Version int
|
||||
DisableVersionFallback bool
|
||||
HTTP2Options option.HTTP2Options
|
||||
HTTP3Options option.QUICOptions
|
||||
}
|
||||
|
||||
type http3Client interface {
|
||||
DialContext(ctx context.Context, destination M.Socksaddr) (net.Conn, error)
|
||||
ListenPacket(ctx context.Context, destination M.Socksaddr) (N.PacketConn, error)
|
||||
ResetConnection()
|
||||
Close() error
|
||||
}
|
||||
|
||||
var NewHTTP3Client func(options ClientOptions, authorization string) (http3Client, error)
|
||||
|
||||
type Client struct {
|
||||
dialer N.Dialer
|
||||
http1Dialer N.Dialer
|
||||
tlsDialer tlsDialer
|
||||
server M.Socksaddr
|
||||
authorization string
|
||||
host string
|
||||
path string
|
||||
headers http.Header
|
||||
version int
|
||||
disableVersionFallback bool
|
||||
http2Transport *http2.Transport
|
||||
http2Access sync.Mutex
|
||||
http2Conns []*http2ClientConn
|
||||
http2Unsupported atomic.Bool
|
||||
http2ConnectUDPUnsupported atomic.Bool
|
||||
http3 http3Client
|
||||
http3Broken atomic.Int64
|
||||
http3Backoff atomic.Int64
|
||||
}
|
||||
|
||||
func NewClient(options ClientOptions) (*Client, error) {
|
||||
client := &Client{
|
||||
dialer: options.Dialer,
|
||||
http1Dialer: options.HTTP1Dialer,
|
||||
server: options.Server,
|
||||
path: options.Path,
|
||||
headers: options.Headers.Clone(),
|
||||
version: options.Version,
|
||||
disableVersionFallback: options.DisableVersionFallback,
|
||||
}
|
||||
if client.headers != nil {
|
||||
client.host = client.headers.Get("Host")
|
||||
client.headers.Del("Host")
|
||||
}
|
||||
if client.host != "" && client.path != "" {
|
||||
return nil, E.New("Host header and path are not allowed at the same time")
|
||||
}
|
||||
client.version = ResolveVersion(client.version, client.path, client.host)
|
||||
if client.version >= 2 && (client.path != "" || client.host != "") {
|
||||
return nil, E.New("path and Host header are only supported by HTTP/1")
|
||||
}
|
||||
if client.dialer == nil {
|
||||
client.dialer = N.SystemDialer
|
||||
}
|
||||
if client.http1Dialer == nil {
|
||||
client.http1Dialer = client.dialer
|
||||
}
|
||||
if dialer, isTLSDialer := options.Dialer.(tlsDialer); isTLSDialer && client.version >= 2 {
|
||||
client.tlsDialer = dialer
|
||||
http2Transport, err := httpclient.ConfigureHTTP2Transport(options.HTTP2Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
http2Transport.DisableCompression = true
|
||||
client.http2Transport = http2Transport
|
||||
} else if client.version == 2 && options.DisableVersionFallback {
|
||||
return nil, E.New("HTTP/2 requires TLS")
|
||||
}
|
||||
if client.version == 3 && NewHTTP3Client == nil {
|
||||
return nil, C.ErrQUICNotIncluded
|
||||
}
|
||||
if options.Username != "" {
|
||||
client.authorization = "Basic " + base64.StdEncoding.EncodeToString([]byte(options.Username+":"+options.Password))
|
||||
}
|
||||
if client.version == 3 {
|
||||
http3, err := NewHTTP3Client(options, client.authorization)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client.http3 = http3
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func ResolveVersion(version int, path string, host string) int {
|
||||
if version != 0 {
|
||||
return version
|
||||
}
|
||||
if path != "" || host != "" {
|
||||
return 1
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
func (c *Client) MultiplexEnabled() bool {
|
||||
return c.http3Available() || (c.tlsDialer != nil && !c.http2Unsupported.Load())
|
||||
}
|
||||
|
||||
func (c *Client) http3Available() bool {
|
||||
if c.http3 == nil {
|
||||
return false
|
||||
}
|
||||
brokenUntil := c.http3Broken.Load()
|
||||
return brokenUntil == 0 || time.Now().UnixNano() >= brokenUntil
|
||||
}
|
||||
|
||||
func (c *Client) markHTTP3Broken() {
|
||||
backoff := time.Duration(c.http3Backoff.Load())
|
||||
if backoff == 0 {
|
||||
backoff = http3BrokenBackoffInitial
|
||||
} else {
|
||||
backoff = min(backoff*2, http3BrokenBackoffMax)
|
||||
}
|
||||
c.http3Backoff.Store(int64(backoff))
|
||||
c.http3Broken.Store(time.Now().Add(backoff).UnixNano())
|
||||
}
|
||||
|
||||
func (c *Client) clearHTTP3Broken() {
|
||||
c.http3Broken.Store(0)
|
||||
c.http3Backoff.Store(0)
|
||||
}
|
||||
|
||||
func (c *Client) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
switch N.NetworkName(network) {
|
||||
case N.NetworkTCP:
|
||||
case N.NetworkUDP:
|
||||
return nil, os.ErrInvalid
|
||||
default:
|
||||
return nil, E.Extend(N.ErrUnknownNetwork, network)
|
||||
}
|
||||
if c.http3Available() {
|
||||
conn, err := c.http3.DialContext(ctx, destination)
|
||||
if err == nil {
|
||||
c.clearHTTP3Broken()
|
||||
return conn, nil
|
||||
}
|
||||
if c.disableVersionFallback || !errors.Is(err, ErrHTTP3Unavailable) {
|
||||
return nil, err
|
||||
}
|
||||
c.markHTTP3Broken()
|
||||
}
|
||||
if c.tlsDialer != nil && !c.http2Unsupported.Load() {
|
||||
clientConn, conn, err := c.acquireHTTP2(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if clientConn != nil {
|
||||
return c.connectHTTP2(ctx, clientConn, destination)
|
||||
}
|
||||
if c.disableVersionFallback {
|
||||
conn.Close()
|
||||
return nil, ErrHTTP2Unsupported
|
||||
}
|
||||
return c.connectAndClose(ctx, conn, destination)
|
||||
}
|
||||
conn, err := c.http1Dialer.DialContext(ctx, N.NetworkTCP, c.server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.connectAndClose(ctx, conn, destination)
|
||||
}
|
||||
|
||||
func (c *Client) closeHTTP2Locked() {
|
||||
for _, clientConn := range c.http2Conns {
|
||||
clientConn.Close()
|
||||
}
|
||||
c.http2Conns = nil
|
||||
}
|
||||
|
||||
func (c *Client) ResetConnections() {
|
||||
c.http2Access.Lock()
|
||||
c.closeHTTP2Locked()
|
||||
c.http2Access.Unlock()
|
||||
c.http2Unsupported.Store(false)
|
||||
c.http2ConnectUDPUnsupported.Store(false)
|
||||
if c.http3 != nil {
|
||||
c.http3.ResetConnection()
|
||||
c.clearHTTP3Broken()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) connect(ctx context.Context, conn net.Conn, destination M.Socksaddr) (net.Conn, error) {
|
||||
stop := context.AfterFunc(ctx, func() {
|
||||
conn.Close()
|
||||
})
|
||||
defer stop()
|
||||
request := &http.Request{
|
||||
Method: http.MethodConnect,
|
||||
Header: http.Header{
|
||||
"Proxy-Connection": []string{"Keep-Alive"},
|
||||
},
|
||||
}
|
||||
if c.host != "" && c.host != destination.Fqdn {
|
||||
request.Host = c.host
|
||||
request.URL = &url.URL{Opaque: destination.String()}
|
||||
} else {
|
||||
request.URL = &url.URL{Host: destination.String()}
|
||||
}
|
||||
if c.path != "" {
|
||||
err := badhttp.URLSetPath(request.URL, c.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
maps.Copy(request.Header, c.headers)
|
||||
if _, loaded := request.Header["User-Agent"]; !loaded {
|
||||
request.Header["User-Agent"] = nil
|
||||
}
|
||||
if c.authorization != "" {
|
||||
request.Header.Set("Proxy-Authorization", c.authorization)
|
||||
}
|
||||
err := request.Write(conn)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "write request")
|
||||
}
|
||||
reader := std_bufio.NewReader(conn)
|
||||
response, err := http.ReadResponse(reader, request)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read response")
|
||||
}
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, statusError(response)
|
||||
}
|
||||
if !stop() {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
if reader.Buffered() > 0 {
|
||||
buffer := buf.NewSize(reader.Buffered())
|
||||
_, err = buffer.ReadFullFrom(reader, buffer.FreeLen())
|
||||
if err != nil {
|
||||
buffer.Release()
|
||||
return nil, err
|
||||
}
|
||||
return bufio.NewCachedConn(conn, buffer), nil
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
c.http2Access.Lock()
|
||||
defer c.http2Access.Unlock()
|
||||
c.closeHTTP2Locked()
|
||||
if c.http3 != nil {
|
||||
c.http3.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
http3BrokenBackoffInitial = 5 * time.Second
|
||||
http3BrokenBackoffMax = 5 * time.Minute
|
||||
)
|
||||
|
||||
var (
|
||||
ErrHTTP2Unsupported = E.New("server does not support HTTP/2")
|
||||
ErrHTTP3Unavailable = E.New("HTTP/3 unavailable")
|
||||
errExtendedConnectUnsupported = E.New("server does not support HTTP/2 extended CONNECT")
|
||||
)
|
||||
|
||||
func statusError(response *http.Response) error {
|
||||
switch response.StatusCode {
|
||||
case http.StatusProxyAuthRequired:
|
||||
return E.New("authentication required")
|
||||
case http.StatusMethodNotAllowed:
|
||||
return E.New("method not allowed")
|
||||
default:
|
||||
return E.New("unexpected status: ", response.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
packetConn, err := c.listenPacket(ctx, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return deadline.NewPacketConn(bufio.NewNetPacketConn(&boundPacketConn{PacketConn: packetConn, destination: destination.Unwrap()})), nil
|
||||
}
|
||||
|
||||
func (c *Client) listenPacket(ctx context.Context, destination M.Socksaddr) (N.PacketConn, error) {
|
||||
if c.http3Available() {
|
||||
packetConn, err := c.http3.ListenPacket(ctx, destination)
|
||||
if err == nil {
|
||||
c.clearHTTP3Broken()
|
||||
return packetConn, nil
|
||||
}
|
||||
if c.disableVersionFallback || !errors.Is(err, ErrHTTP3Unavailable) {
|
||||
return nil, err
|
||||
}
|
||||
c.markHTTP3Broken()
|
||||
}
|
||||
if c.tlsDialer != nil && !c.http2Unsupported.Load() && !c.http2ConnectUDPUnsupported.Load() {
|
||||
clientConn, conn, err := c.acquireHTTP2(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if clientConn != nil {
|
||||
packetConn, connectErr := c.connectUDPHTTP2(ctx, clientConn, destination)
|
||||
if connectErr == nil {
|
||||
return packetConn, nil
|
||||
}
|
||||
if !errors.Is(connectErr, errExtendedConnectUnsupported) || c.disableVersionFallback {
|
||||
return nil, connectErr
|
||||
}
|
||||
c.http2ConnectUDPUnsupported.Store(true)
|
||||
} else if c.disableVersionFallback {
|
||||
conn.Close()
|
||||
return nil, ErrHTTP2Unsupported
|
||||
} else {
|
||||
return c.connectUDPHTTP1AndClose(ctx, conn, destination)
|
||||
}
|
||||
}
|
||||
conn, err := c.http1Dialer.DialContext(ctx, N.NetworkTCP, c.server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.connectUDPHTTP1AndClose(ctx, conn, destination)
|
||||
}
|
||||
|
||||
func (c *Client) connectUDPHTTP1AndClose(ctx context.Context, conn net.Conn, destination M.Socksaddr) (N.PacketConn, error) {
|
||||
packetConn, err := c.connectUDPHTTP1(ctx, conn, destination)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return packetConn, nil
|
||||
}
|
||||
|
||||
var _ N.Dialer = (*Client)(nil)
|
||||
|
||||
type boundPacketConn struct {
|
||||
N.PacketConn
|
||||
destination M.Socksaddr
|
||||
}
|
||||
|
||||
func (c *boundPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||
if destination.Unwrap() != c.destination {
|
||||
buffer.Release()
|
||||
return E.New("connect-udp: destination mismatch: ", destination)
|
||||
}
|
||||
return c.PacketConn.WritePacket(buffer, destination)
|
||||
}
|
||||
|
||||
func (c *boundPacketConn) NeedAdditionalReadDeadline() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *boundPacketConn) Upstream() any {
|
||||
return c.PacketConn
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/common/pipe"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
type http2ClientConn struct {
|
||||
*http2.ClientConn
|
||||
}
|
||||
|
||||
func (c *Client) acquireHTTP2(ctx context.Context) (*http2ClientConn, net.Conn, error) {
|
||||
c.http2Access.Lock()
|
||||
defer c.http2Access.Unlock()
|
||||
c.http2Conns = slices.DeleteFunc(c.http2Conns, func(it *http2ClientConn) bool { return it.State().Closed })
|
||||
for _, clientConn := range c.http2Conns {
|
||||
if clientConn.ReserveNewRequest() {
|
||||
return clientConn, nil, nil
|
||||
}
|
||||
}
|
||||
conn, err := c.tlsDialer.DialTLSContext(adapter.ContextForMultiplexSession(ctx), c.server)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if conn.ConnectionState().NegotiatedProtocol != http2.NextProtoTLS {
|
||||
if !c.disableVersionFallback {
|
||||
c.http2Unsupported.Store(true)
|
||||
}
|
||||
return nil, conn, nil
|
||||
}
|
||||
rawClientConn, err := c.http2Transport.NewClientConn(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, nil, E.Cause(err, "create HTTP/2 connection")
|
||||
}
|
||||
clientConn := &http2ClientConn{ClientConn: rawClientConn}
|
||||
clientConn.ReserveNewRequest()
|
||||
c.http2Conns = append(c.http2Conns, clientConn)
|
||||
return clientConn, nil, nil
|
||||
}
|
||||
|
||||
func (c *Client) connectHTTP2(ctx context.Context, clientConn *http2ClientConn, destination M.Socksaddr) (net.Conn, error) {
|
||||
request := &http.Request{
|
||||
Method: http.MethodConnect,
|
||||
URL: &url.URL{Host: destination.String()},
|
||||
Host: destination.String(),
|
||||
Header: c.headers.Clone(),
|
||||
}
|
||||
streamConn, err := c.roundTripHTTP2(ctx, clientConn, request, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newClientTunnelConn(streamConn), nil
|
||||
}
|
||||
|
||||
func (c *Client) roundTripHTTP2(ctx context.Context, clientConn *http2ClientConn, request *http.Request, destination M.Socksaddr) (*clientStreamConn, error) {
|
||||
pipeReader, pipeWriter := pipe.Pipe()
|
||||
streamCtx, cancel := context.WithCancel(context.Background())
|
||||
request.Body = pipeReader
|
||||
request = request.WithContext(streamCtx)
|
||||
if request.Header == nil {
|
||||
request.Header = make(http.Header)
|
||||
}
|
||||
if _, loaded := request.Header["User-Agent"]; !loaded {
|
||||
request.Header["User-Agent"] = nil
|
||||
}
|
||||
if c.authorization != "" {
|
||||
request.Header.Set("Proxy-Authorization", c.authorization)
|
||||
}
|
||||
stop := context.AfterFunc(ctx, cancel)
|
||||
response, err := clientConn.RoundTrip(request)
|
||||
stopped := stop()
|
||||
if err == nil && !stopped {
|
||||
response.Body.Close()
|
||||
err = ctx.Err()
|
||||
}
|
||||
if err != nil {
|
||||
cancel()
|
||||
pipeWriter.Close()
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
// golang.org/x/net/http2 has no exported sentinel for a peer that lacks
|
||||
// SETTINGS_ENABLE_CONNECT_PROTOCOL; it returns errExtendedConnectNotSupported
|
||||
// ("net/http: extended connect not supported by peer") from RoundTrip.
|
||||
if strings.Contains(err.Error(), "extended connect not supported") {
|
||||
return nil, E.Cause1(errExtendedConnectUnsupported, err)
|
||||
}
|
||||
return nil, E.Cause(err, "HTTP/2 CONNECT")
|
||||
}
|
||||
if response.StatusCode != http.StatusOK {
|
||||
response.Body.Close()
|
||||
cancel()
|
||||
pipeWriter.Close()
|
||||
return nil, statusError(response)
|
||||
}
|
||||
return &clientStreamConn{
|
||||
reader: response.Body,
|
||||
writer: pipeWriter,
|
||||
cancel: cancel,
|
||||
localAddr: M.Socksaddr{},
|
||||
remoteAddr: destination,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) connectAndClose(ctx context.Context, conn net.Conn, destination M.Socksaddr) (net.Conn, error) {
|
||||
result, err := c.connect(ctx, conn, destination)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
//go:build with_quic
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/httpclient"
|
||||
"github.com/sagernet/sing-quic"
|
||||
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"
|
||||
)
|
||||
|
||||
func init() {
|
||||
NewHTTP3Client = newHTTP3Client
|
||||
}
|
||||
|
||||
type http3ClientImpl struct {
|
||||
dialer N.Dialer
|
||||
tlsConfig aTLS.Config
|
||||
server M.Socksaddr
|
||||
authority string
|
||||
headers http.Header
|
||||
authorization string
|
||||
quicConfig *quic.Config
|
||||
transport *http3.Transport
|
||||
access sync.Mutex
|
||||
conn *http3.ClientConn
|
||||
rawConn net.Conn
|
||||
}
|
||||
|
||||
func newHTTP3Client(options ClientOptions, authorization string) (http3Client, error) {
|
||||
if options.TLSConfig == nil {
|
||||
return nil, E.New("HTTP/3 requires TLS")
|
||||
}
|
||||
dialer := options.RawDialer
|
||||
if dialer == nil {
|
||||
dialer = N.SystemDialer
|
||||
}
|
||||
quicConfig := httpclient.NewQUICConfig(options.HTTP3Options)
|
||||
quicConfig.EnableDatagrams = true
|
||||
headers := options.Headers.Clone()
|
||||
authority := options.Server.String()
|
||||
if headers != nil {
|
||||
if host := headers.Get("Host"); host != "" {
|
||||
authority = host
|
||||
}
|
||||
headers.Del("Host")
|
||||
}
|
||||
return &http3ClientImpl{
|
||||
dialer: dialer,
|
||||
tlsConfig: options.TLSConfig,
|
||||
server: options.Server,
|
||||
authority: authority,
|
||||
headers: headers,
|
||||
authorization: authorization,
|
||||
quicConfig: quicConfig,
|
||||
transport: &http3.Transport{EnableDatagrams: true, DisableCompression: true},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *http3ClientImpl) acquire(ctx context.Context) (*http3.ClientConn, error) {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
if c.conn != nil && c.conn.Context().Err() == nil {
|
||||
return c.conn, nil
|
||||
}
|
||||
if c.rawConn != nil {
|
||||
c.rawConn.Close()
|
||||
c.rawConn = nil
|
||||
}
|
||||
rawConn, err := c.dialer.DialContext(adapter.ContextForMultiplexSession(ctx), N.NetworkUDP, c.server)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
return nil, E.Cause1(ErrHTTP3Unavailable, err)
|
||||
}
|
||||
quicConn, err := qtls.DialEarly(ctx, rawConn, c.tlsConfig, c.quicConfig)
|
||||
if err != nil {
|
||||
rawConn.Close()
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
return nil, E.Cause1(ErrHTTP3Unavailable, err)
|
||||
}
|
||||
c.conn = c.transport.NewClientConn(quicConn)
|
||||
c.rawConn = rawConn
|
||||
return c.conn, nil
|
||||
}
|
||||
|
||||
func (c *http3ClientImpl) openStream(ctx context.Context, request *http.Request) (*http3.RequestStream, *http3.ClientConn, error) {
|
||||
clientConn, err := c.acquire(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
stream, err := clientConn.OpenRequestStream(ctx)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil, nil, ctx.Err()
|
||||
}
|
||||
return nil, nil, E.Cause1(ErrHTTP3Unavailable, err)
|
||||
}
|
||||
if request.Header == nil {
|
||||
request.Header = make(http.Header)
|
||||
}
|
||||
if _, loaded := request.Header["User-Agent"]; !loaded {
|
||||
request.Header["User-Agent"] = nil
|
||||
}
|
||||
if c.authorization != "" {
|
||||
request.Header.Set("Proxy-Authorization", c.authorization)
|
||||
}
|
||||
stop := context.AfterFunc(ctx, func() {
|
||||
stream.CancelRead(0)
|
||||
stream.CancelWrite(0)
|
||||
})
|
||||
var response *http.Response
|
||||
err = stream.SendRequestHeader(request)
|
||||
if err == nil {
|
||||
response, err = stream.ReadResponse()
|
||||
}
|
||||
if err == nil {
|
||||
select {
|
||||
case <-clientConn.ReceivedSettings():
|
||||
case <-clientConn.Context().Done():
|
||||
err = context.Cause(clientConn.Context())
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
}
|
||||
}
|
||||
if !stop() {
|
||||
err = ctx.Err()
|
||||
}
|
||||
if err != nil {
|
||||
stream.CancelRead(0)
|
||||
stream.Close()
|
||||
if ctx.Err() != nil {
|
||||
return nil, nil, ctx.Err()
|
||||
}
|
||||
return nil, nil, E.Cause(err, "HTTP/3 CONNECT")
|
||||
}
|
||||
if response.StatusCode != http.StatusOK {
|
||||
stream.CancelRead(0)
|
||||
stream.Close()
|
||||
return nil, nil, statusError(response)
|
||||
}
|
||||
return stream, clientConn, nil
|
||||
}
|
||||
|
||||
func (c *http3ClientImpl) DialContext(ctx context.Context, destination M.Socksaddr) (net.Conn, error) {
|
||||
stream, _, err := c.openStream(ctx, &http.Request{
|
||||
Method: http.MethodConnect,
|
||||
URL: &url.URL{Host: destination.String()},
|
||||
Host: destination.String(),
|
||||
Header: c.headers.Clone(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &http3StreamConn{stream: stream, remoteAddr: destination}, nil
|
||||
}
|
||||
|
||||
func (c *http3ClientImpl) ListenPacket(ctx context.Context, destination M.Socksaddr) (N.PacketConn, error) {
|
||||
header := c.headers.Clone()
|
||||
if header == nil {
|
||||
header = make(http.Header)
|
||||
}
|
||||
header.Set("Capsule-Protocol", "?1")
|
||||
stream, clientConn, err := c.openStream(ctx, &http.Request{
|
||||
Method: http.MethodConnect,
|
||||
Proto: connectUDPProtocol,
|
||||
URL: connectUDPURL(c.authority, destination),
|
||||
Host: c.authority,
|
||||
Header: header,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newHTTP3PacketConn(&http3RequestDatagramStream{stream: stream, datagramsEnabled: clientConn.Settings().EnableDatagrams}, destination, M.Socksaddr{}), nil
|
||||
}
|
||||
|
||||
func (c *http3ClientImpl) ResetConnection() {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
if c.conn != nil {
|
||||
c.conn.CloseWithError(0, "")
|
||||
c.conn = nil
|
||||
}
|
||||
if c.rawConn != nil {
|
||||
c.rawConn.Close()
|
||||
c.rawConn = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *http3ClientImpl) Close() error {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
if c.conn != nil {
|
||||
c.conn.CloseWithError(0, "")
|
||||
c.conn = nil
|
||||
}
|
||||
if c.rawConn != nil {
|
||||
c.rawConn.Close()
|
||||
c.rawConn = nil
|
||||
}
|
||||
return c.transport.Close()
|
||||
}
|
||||
|
||||
type http3StreamConn struct {
|
||||
stream *http3.RequestStream
|
||||
remoteAddr net.Addr
|
||||
writeAccess sync.Mutex
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
func (c *http3StreamConn) Read(p []byte) (int, error) {
|
||||
n, err := c.stream.Read(p)
|
||||
return n, c.wrapError(err)
|
||||
}
|
||||
|
||||
func (c *http3StreamConn) Write(p []byte) (int, error) {
|
||||
c.writeAccess.Lock()
|
||||
defer c.writeAccess.Unlock()
|
||||
n, err := c.stream.Write(p)
|
||||
return n, c.wrapError(err)
|
||||
}
|
||||
|
||||
func (c *http3StreamConn) wrapError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if c.closed.Load() {
|
||||
return net.ErrClosed
|
||||
}
|
||||
return qtls.WrapError(err)
|
||||
}
|
||||
|
||||
func (c *http3StreamConn) CloseWrite() error {
|
||||
c.writeAccess.Lock()
|
||||
defer c.writeAccess.Unlock()
|
||||
return c.stream.Close()
|
||||
}
|
||||
|
||||
func (c *http3StreamConn) Close() error {
|
||||
c.closed.Store(true)
|
||||
c.stream.SetWriteDeadline(time.Now())
|
||||
c.writeAccess.Lock()
|
||||
defer c.writeAccess.Unlock()
|
||||
c.stream.CancelRead(0)
|
||||
return c.stream.Close()
|
||||
}
|
||||
|
||||
func (c *http3StreamConn) LocalAddr() net.Addr {
|
||||
return M.Socksaddr{}
|
||||
}
|
||||
|
||||
func (c *http3StreamConn) RemoteAddr() net.Addr {
|
||||
return c.remoteAddr
|
||||
}
|
||||
|
||||
func (c *http3StreamConn) SetDeadline(t time.Time) error {
|
||||
return c.stream.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *http3StreamConn) SetReadDeadline(t time.Time) error {
|
||||
return c.stream.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *http3StreamConn) SetWriteDeadline(t time.Time) error {
|
||||
return c.stream.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (c *http3StreamConn) NeedAdditionalReadDeadline() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type http3RequestDatagramStream struct {
|
||||
stream *http3.RequestStream
|
||||
datagramsEnabled bool
|
||||
}
|
||||
|
||||
func (s *http3RequestDatagramStream) Read(p []byte) (int, error) {
|
||||
return s.stream.Read(p)
|
||||
}
|
||||
|
||||
func (s *http3RequestDatagramStream) Write(p []byte) (int, error) {
|
||||
return s.stream.Write(p)
|
||||
}
|
||||
|
||||
func (s *http3RequestDatagramStream) Close() error {
|
||||
s.stream.SetWriteDeadline(time.Now())
|
||||
s.stream.CancelRead(0)
|
||||
return s.stream.Close()
|
||||
}
|
||||
|
||||
func (s *http3RequestDatagramStream) SendDatagram(payload []byte) error {
|
||||
if !s.datagramsEnabled {
|
||||
return ErrDatagramUnsupported
|
||||
}
|
||||
err := s.stream.SendDatagram(payload)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var tooLarge *quic.DatagramTooLargeError
|
||||
if errors.As(err, &tooLarge) {
|
||||
return ErrDatagramUnsupported
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *http3RequestDatagramStream) ReceiveDatagram(ctx context.Context) ([]byte, error) {
|
||||
return s.stream.ReceiveDatagram(ctx)
|
||||
}
|
||||
|
||||
var (
|
||||
_ net.Conn = (*http3StreamConn)(nil)
|
||||
_ N.WriteCloser = (*http3StreamConn)(nil)
|
||||
_ DatagramStream = (*http3RequestDatagramStream)(nil)
|
||||
)
|
||||
@@ -0,0 +1,178 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
std_bufio "bufio"
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/transport/v2rayhttp"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
const (
|
||||
connectUDPProtocol = "connect-udp"
|
||||
connectUDPPathPrefix = "/.well-known/masque/udp/"
|
||||
)
|
||||
|
||||
func parseConnectUDPTarget(path string) (M.Socksaddr, bool) {
|
||||
rest, found := strings.CutPrefix(path, connectUDPPathPrefix)
|
||||
if !found {
|
||||
return M.Socksaddr{}, false
|
||||
}
|
||||
segments := strings.Split(strings.TrimSuffix(rest, "/"), "/")
|
||||
if len(segments) != 2 {
|
||||
return M.Socksaddr{}, false
|
||||
}
|
||||
host, err := url.PathUnescape(segments[0])
|
||||
if err != nil || host == "" {
|
||||
return M.Socksaddr{}, false
|
||||
}
|
||||
port, err := strconv.ParseUint(segments[1], 10, 16)
|
||||
if err != nil || port == 0 {
|
||||
return M.Socksaddr{}, false
|
||||
}
|
||||
destination := M.ParseSocksaddrHostPort(host, uint16(port)).Unwrap()
|
||||
if !destination.IsValid() {
|
||||
return M.Socksaddr{}, false
|
||||
}
|
||||
return destination, true
|
||||
}
|
||||
|
||||
func connectUDPURL(authority string, destination M.Socksaddr) *url.URL {
|
||||
host := destination.AddrString()
|
||||
port := "/" + strconv.Itoa(int(destination.Port)) + "/"
|
||||
requestURL := &url.URL{
|
||||
Path: connectUDPPathPrefix + host + port,
|
||||
RawPath: connectUDPPathPrefix + strings.ReplaceAll(url.PathEscape(host), ":", "%3A") + port,
|
||||
}
|
||||
if authority != "" {
|
||||
requestURL.Scheme = "https"
|
||||
requestURL.Host = authority
|
||||
}
|
||||
return requestURL
|
||||
}
|
||||
|
||||
func requestIsConnectUDP(request *http.Request) bool {
|
||||
return request.Method == http.MethodGet && requestIsUpgrade(request) && strings.EqualFold(request.Header.Get("Upgrade"), connectUDPProtocol)
|
||||
}
|
||||
|
||||
func (c *serverConn) serveConnectUDP(ctx context.Context, request *http.Request, source M.Socksaddr) (requestResult, error) {
|
||||
destination, valid := parseConnectUDPTarget(request.URL.EscapedPath())
|
||||
if !valid || !request.ProtoAtLeast(1, 1) {
|
||||
return c.reject(request, requestKeepAlive(request), http.StatusBadRequest, E.New("invalid connect-udp request: ", request.URL.Path))
|
||||
}
|
||||
_, err := c.conn.Write([]byte("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: connect-udp\r\nCapsule-Protocol: ?1\r\n\r\n"))
|
||||
if err != nil {
|
||||
return requestClose, E.Cause(err, "write response")
|
||||
}
|
||||
c.handler.NewPacketConnectionEx(ctx, newCapsuleConn(c.reader.Reader, c.conn, destination), source, destination, c.onClose)
|
||||
return requestHandedOff, nil
|
||||
}
|
||||
|
||||
func (h *httpHandler) serveConnectUDP(ctx context.Context, writer http.ResponseWriter, request *http.Request, source M.Socksaddr) {
|
||||
destination, valid := parseConnectUDPTarget(request.URL.EscapedPath())
|
||||
if !valid {
|
||||
h.server.logger.ErrorContext(ctx, "process connection from ", source, ": invalid connect-udp target: ", request.URL.Path)
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
writer.Header().Set("Capsule-Protocol", "?1")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
writer.(http.Flusher).Flush()
|
||||
if request.ProtoMajor == 3 && HTTP3StreamFunc != nil {
|
||||
stream, isDatagramStream := HTTP3StreamFunc(request.Context(), writer)
|
||||
if isDatagramStream {
|
||||
localAddr, _ := request.Context().Value(http.LocalAddrContextKey).(net.Addr)
|
||||
conn := newHTTP3PacketConn(stream, destination, localAddr)
|
||||
h.handler.NewPacketConnectionEx(ctx, conn, source, destination, nil)
|
||||
conn.wait(request.Context())
|
||||
return
|
||||
}
|
||||
}
|
||||
conn := v2rayhttp.NewHTTP2Wrapper(&v2rayhttp.ServerHTTPConn{
|
||||
HTTP2Conn: v2rayhttp.NewHTTPConn(request.Body, writer),
|
||||
Flusher: writer.(http.Flusher),
|
||||
})
|
||||
done := make(chan struct{})
|
||||
h.handler.NewPacketConnectionEx(ctx, newCapsuleConn(std_bufio.NewReader(conn), conn, destination), source, destination, N.OnceClose(func(it error) {
|
||||
close(done)
|
||||
}))
|
||||
<-done
|
||||
conn.CloseWrapper()
|
||||
}
|
||||
|
||||
func (c *Client) connectUDPHTTP1(ctx context.Context, conn net.Conn, destination M.Socksaddr) (N.PacketConn, error) {
|
||||
stop := context.AfterFunc(ctx, func() {
|
||||
conn.Close()
|
||||
})
|
||||
defer stop()
|
||||
request := &http.Request{
|
||||
Method: http.MethodGet,
|
||||
URL: connectUDPURL("", destination),
|
||||
Host: c.authority(),
|
||||
Header: c.headers.Clone(),
|
||||
}
|
||||
if request.Header == nil {
|
||||
request.Header = make(http.Header)
|
||||
}
|
||||
request.Header.Set("Connection", "Upgrade")
|
||||
request.Header.Set("Upgrade", connectUDPProtocol)
|
||||
request.Header.Set("Capsule-Protocol", "?1")
|
||||
if _, loaded := request.Header["User-Agent"]; !loaded {
|
||||
request.Header["User-Agent"] = nil
|
||||
}
|
||||
if c.authorization != "" {
|
||||
request.Header.Set("Proxy-Authorization", c.authorization)
|
||||
}
|
||||
err := request.Write(conn)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "write request")
|
||||
}
|
||||
reader := std_bufio.NewReader(conn)
|
||||
response, err := http.ReadResponse(reader, request)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read response")
|
||||
}
|
||||
if response.StatusCode != http.StatusSwitchingProtocols {
|
||||
return nil, statusError(response)
|
||||
}
|
||||
if !strings.EqualFold(response.Header.Get("Upgrade"), connectUDPProtocol) {
|
||||
return nil, E.New("unexpected upgrade protocol: ", response.Header.Get("Upgrade"))
|
||||
}
|
||||
if !stop() {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
return newCapsuleConn(reader, conn, destination), nil
|
||||
}
|
||||
|
||||
func (c *Client) connectUDPHTTP2(ctx context.Context, clientConn *http2ClientConn, destination M.Socksaddr) (N.PacketConn, error) {
|
||||
request := &http.Request{
|
||||
Method: http.MethodConnect,
|
||||
URL: connectUDPURL(c.authority(), destination),
|
||||
Host: c.authority(),
|
||||
Header: c.headers.Clone(),
|
||||
}
|
||||
if request.Header == nil {
|
||||
request.Header = make(http.Header)
|
||||
}
|
||||
request.Header.Set(":protocol", connectUDPProtocol)
|
||||
request.Header.Set("Capsule-Protocol", "?1")
|
||||
stream, err := c.roundTripHTTP2(ctx, clientConn, request, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newCapsuleConn(std_bufio.NewReader(stream), stream, destination), nil
|
||||
}
|
||||
|
||||
func (c *Client) authority() string {
|
||||
if c.host != "" {
|
||||
return c.host
|
||||
}
|
||||
return c.server.String()
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
_ "unsafe"
|
||||
|
||||
_ "golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
//go:linkname disableExtendedConnectProtocol golang.org/x/net/http2.disableExtendedConnectProtocol
|
||||
var disableExtendedConnectProtocol bool
|
||||
|
||||
func init() {
|
||||
disableExtendedConnectProtocol = false
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
std_bufio "bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
)
|
||||
|
||||
type upstreamConn struct {
|
||||
net.Conn
|
||||
reader *std_bufio.Reader
|
||||
limiter *readLimiter
|
||||
user string
|
||||
source M.Socksaddr
|
||||
destination M.Socksaddr
|
||||
done chan struct{}
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *upstreamConn) closed() bool {
|
||||
select {
|
||||
case <-c.done:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (c *upstreamConn) readResponse(request *http.Request) (*http.Response, error) {
|
||||
c.limiter.remaining = maxHeaderBytes
|
||||
response, err := http.ReadResponse(c.reader, request)
|
||||
c.limiter.remaining = -1
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (c *upstreamConn) closeErr() error {
|
||||
select {
|
||||
case <-c.done:
|
||||
return c.err
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *serverConn) openUpstream(ctx context.Context, source M.Socksaddr, destination M.Socksaddr) *upstreamConn {
|
||||
user, _ := auth.UserFromContext[string](ctx)
|
||||
if c.upstream != nil {
|
||||
if c.upstream.user == user && c.upstream.source == source && c.upstream.destination == destination && !c.upstream.closed() {
|
||||
return c.upstream
|
||||
}
|
||||
c.closeUpstream()
|
||||
}
|
||||
c.upstream = newUpstreamConn(ctx, c.handler, source, destination)
|
||||
c.upstream.user = user
|
||||
return c.upstream
|
||||
}
|
||||
|
||||
func (c *serverConn) serveForward(ctx context.Context, request *http.Request, source M.Socksaddr, upgrade bool) (requestResult, error) {
|
||||
destination, valid := forwardDestination(request)
|
||||
if !valid {
|
||||
return c.reject(request, requestKeepAlive(request), http.StatusBadRequest, E.New("invalid forward target: ", request.URL.String()))
|
||||
}
|
||||
keepAlive := requestKeepAlive(request)
|
||||
upgradeProtocol := request.Header.Get("Upgrade")
|
||||
removeHopByHopHeaders(request.Header)
|
||||
if upgrade {
|
||||
request.Header.Set("Connection", "Upgrade")
|
||||
request.Header.Set("Upgrade", upgradeProtocol)
|
||||
}
|
||||
if _, loaded := request.Header["User-Agent"]; !loaded {
|
||||
request.Header["User-Agent"] = nil
|
||||
}
|
||||
request.Close = false
|
||||
// Request.Write and Response.Write close the body, and net/http's (*body).Close
|
||||
// drains an unread body to EOF from the connection reader.
|
||||
if request.Body != http.NoBody {
|
||||
request.Body = io.NopCloser(request.Body)
|
||||
}
|
||||
|
||||
upstream := c.openUpstream(ctx, source, destination)
|
||||
writeDone := make(chan error, 1)
|
||||
go func() {
|
||||
err := request.Write(upstream)
|
||||
if err != nil {
|
||||
upstream.Close()
|
||||
}
|
||||
writeDone <- err
|
||||
}()
|
||||
|
||||
var response *http.Response
|
||||
for {
|
||||
var err error
|
||||
response, err = upstream.readResponse(request)
|
||||
if err != nil {
|
||||
c.closeUpstream()
|
||||
writeErr := c.finishRequestWrite(writeDone)
|
||||
err = E.Errors(upstream.closeErr(), E.Cause(err, "read upstream response"))
|
||||
if !keepAlive || (writeErr != nil && request.Body != http.NoBody) {
|
||||
return c.rejectAndClose(request, http.StatusBadGateway, err)
|
||||
}
|
||||
request.Body = http.NoBody
|
||||
return c.reject(request, true, http.StatusBadGateway, err)
|
||||
}
|
||||
if response.StatusCode >= 200 || response.StatusCode == http.StatusSwitchingProtocols {
|
||||
break
|
||||
}
|
||||
if request.ProtoMajor == 1 && request.ProtoMinor == 0 {
|
||||
continue
|
||||
}
|
||||
removeHopByHopHeaders(response.Header)
|
||||
err = writeInterimResponse(c.conn, request, response)
|
||||
if err != nil {
|
||||
c.closeUpstream()
|
||||
c.finishRequestWrite(writeDone)
|
||||
return requestClose, E.Cause(err, "write interim response")
|
||||
}
|
||||
}
|
||||
|
||||
response.ProtoMajor = request.ProtoMajor
|
||||
response.ProtoMinor = request.ProtoMinor
|
||||
if response.StatusCode == http.StatusSwitchingProtocols {
|
||||
if !upgrade {
|
||||
c.closeUpstream()
|
||||
c.finishRequestWrite(writeDone)
|
||||
return requestClose, E.New("unexpected 101 response")
|
||||
}
|
||||
return c.relayUpgrade(ctx, response, writeDone)
|
||||
}
|
||||
|
||||
upstreamClose := response.Close
|
||||
removeHopByHopHeaders(response.Header)
|
||||
if response.ContentLength == -1 && responseHasBody(request, response) {
|
||||
if request.ProtoMajor == 1 && request.ProtoMinor == 0 {
|
||||
keepAlive = false
|
||||
} else {
|
||||
response.TransferEncoding = []string{"chunked"}
|
||||
}
|
||||
}
|
||||
response.Close = !keepAlive
|
||||
if keepAlive && request.ProtoMajor == 1 && request.ProtoMinor == 0 {
|
||||
response.Header.Set("Connection", "keep-alive")
|
||||
response.Header.Set("Proxy-Connection", "keep-alive")
|
||||
}
|
||||
response.Body = io.NopCloser(response.Body)
|
||||
err := response.Write(c.conn)
|
||||
if err != nil {
|
||||
c.closeUpstream()
|
||||
c.finishRequestWrite(writeDone)
|
||||
return requestClose, E.Cause(err, "write response")
|
||||
}
|
||||
writeErr := c.finishRequestWrite(writeDone)
|
||||
if writeErr != nil {
|
||||
c.closeUpstream()
|
||||
if keepAlive && request.Body != http.NoBody && !c.discardBody(request.Body) {
|
||||
keepAlive = false
|
||||
}
|
||||
} else if upstreamClose {
|
||||
c.closeUpstream()
|
||||
}
|
||||
if !keepAlive {
|
||||
return requestClose, nil
|
||||
}
|
||||
return requestContinue, nil
|
||||
}
|
||||
|
||||
func (c *serverConn) finishRequestWrite(writeDone chan error) error {
|
||||
select {
|
||||
case err := <-writeDone:
|
||||
return err
|
||||
default:
|
||||
}
|
||||
c.conn.SetReadDeadline(time.Now().Add(discardBodyTimeout))
|
||||
timer := time.NewTimer(discardBodyTimeout)
|
||||
defer timer.Stop()
|
||||
var err error
|
||||
select {
|
||||
case err = <-writeDone:
|
||||
case <-timer.C:
|
||||
c.closeUpstream()
|
||||
err = <-writeDone
|
||||
if err == nil {
|
||||
err = E.New("request body timeout")
|
||||
}
|
||||
}
|
||||
c.conn.SetReadDeadline(time.Time{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *serverConn) relayUpgrade(ctx context.Context, response *http.Response, writeDone chan error) (requestResult, error) {
|
||||
response.Close = false
|
||||
err := response.Write(c.conn)
|
||||
if err != nil {
|
||||
c.closeUpstream()
|
||||
c.finishRequestWrite(writeDone)
|
||||
return requestClose, E.Cause(err, "write response")
|
||||
}
|
||||
err = c.finishRequestWrite(writeDone)
|
||||
if err != nil {
|
||||
c.closeUpstream()
|
||||
return requestClose, E.Cause(err, "write upgrade request")
|
||||
}
|
||||
upstream := c.upstream
|
||||
c.upstream = nil
|
||||
var upstreamConn net.Conn = upstream
|
||||
if upstream.reader.Buffered() > 0 {
|
||||
buffer := buf.NewSize(upstream.reader.Buffered())
|
||||
_, err = buffer.ReadFullFrom(upstream.reader, buffer.FreeLen())
|
||||
if err != nil {
|
||||
buffer.Release()
|
||||
upstream.Close()
|
||||
return requestClose, err
|
||||
}
|
||||
upstreamConn = bufio.NewCachedConn(upstream.Conn, buffer)
|
||||
}
|
||||
err = bufio.CopyConn(ctx, c.reader.cachedConn(c.conn), upstreamConn)
|
||||
return requestClose, err
|
||||
}
|
||||
|
||||
func responseHasBody(request *http.Request, response *http.Response) bool {
|
||||
if request.Method == http.MethodHead {
|
||||
return false
|
||||
}
|
||||
if response.StatusCode < 200 || response.StatusCode == http.StatusNoContent || response.StatusCode == http.StatusNotModified {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func writeInterimResponse(writer io.Writer, request *http.Request, response *http.Response) error {
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteString(F.ToString("HTTP/", request.ProtoMajor, ".", request.ProtoMinor, " ", response.StatusCode, " ", http.StatusText(response.StatusCode), "\r\n"))
|
||||
err := response.Header.Write(&buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buffer.WriteString("\r\n")
|
||||
_, err = writer.Write(buffer.Bytes())
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
|
||||
"golang.org/x/net/http/httpguts"
|
||||
)
|
||||
|
||||
func headerHasToken(header http.Header, name string, token string) bool {
|
||||
for _, value := range header.Values(name) {
|
||||
if httpguts.HeaderValuesContainsToken([]string{value}, token) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func removeHopByHopHeaders(header http.Header) {
|
||||
for _, value := range header.Values("Connection") {
|
||||
for token := range strings.SplitSeq(value, ",") {
|
||||
token = strings.TrimSpace(token)
|
||||
if token != "" {
|
||||
header.Del(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
header.Del("Connection")
|
||||
header.Del("Proxy-Connection")
|
||||
header.Del("Proxy-Authenticate")
|
||||
header.Del("Proxy-Authorization")
|
||||
header.Del("Keep-Alive")
|
||||
header.Del("TE")
|
||||
header.Del("Transfer-Encoding")
|
||||
header.Del("Upgrade")
|
||||
}
|
||||
|
||||
func requestKeepAlive(request *http.Request) bool {
|
||||
if request.ProtoMajor == 1 && request.ProtoMinor == 0 {
|
||||
return headerHasToken(request.Header, "Connection", "keep-alive") ||
|
||||
headerHasToken(request.Header, "Proxy-Connection", "keep-alive")
|
||||
}
|
||||
return !request.Close && !headerHasToken(request.Header, "Proxy-Connection", "close")
|
||||
}
|
||||
|
||||
func requestIsUpgrade(request *http.Request) bool {
|
||||
return request.Header.Get("Upgrade") != "" && headerHasToken(request.Header, "Connection", "upgrade")
|
||||
}
|
||||
|
||||
func parseAuthority(authority string, defaultPort uint16) M.Socksaddr {
|
||||
destination := M.ParseSocksaddr(authority)
|
||||
if destination.Port == 0 {
|
||||
destination.Port = defaultPort
|
||||
}
|
||||
return destination.Unwrap()
|
||||
}
|
||||
|
||||
func connectDestination(request *http.Request) M.Socksaddr {
|
||||
authority := request.URL.Host
|
||||
if authority == "" {
|
||||
authority = request.Host
|
||||
}
|
||||
return parseAuthority(authority, 443)
|
||||
}
|
||||
|
||||
func forwardDestination(request *http.Request) (M.Socksaddr, bool) {
|
||||
switch request.URL.Scheme {
|
||||
case "http", "ws":
|
||||
default:
|
||||
return M.Socksaddr{}, false
|
||||
}
|
||||
if request.URL.Host == "" {
|
||||
return M.Socksaddr{}, false
|
||||
}
|
||||
destination := parseAuthority(request.URL.Host, 80)
|
||||
if !destination.IsValid() {
|
||||
return M.Socksaddr{}, false
|
||||
}
|
||||
return destination, true
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
std_bufio "bufio"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var errHeaderTooLarge = E.New("request header too large")
|
||||
|
||||
type Reader struct {
|
||||
*std_bufio.Reader
|
||||
limiter *readLimiter
|
||||
}
|
||||
|
||||
func NewReader(conn net.Conn) *Reader {
|
||||
limiter := &readLimiter{reader: conn, remaining: -1}
|
||||
return &Reader{
|
||||
Reader: std_bufio.NewReader(limiter),
|
||||
limiter: limiter,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reader) setLimit(limit int64) {
|
||||
r.limiter.remaining = limit
|
||||
}
|
||||
|
||||
func (r *Reader) cachedConn(conn net.Conn) net.Conn {
|
||||
if r.Buffered() == 0 {
|
||||
return conn
|
||||
}
|
||||
buffer := buf.NewSize(r.Buffered())
|
||||
_, err := buffer.ReadFullFrom(r, buffer.FreeLen())
|
||||
if err != nil {
|
||||
buffer.Release()
|
||||
return conn
|
||||
}
|
||||
return bufio.NewCachedConn(conn, buffer)
|
||||
}
|
||||
|
||||
type readLimiter struct {
|
||||
reader io.Reader
|
||||
remaining int64
|
||||
}
|
||||
|
||||
func (r *readLimiter) Read(p []byte) (int, error) {
|
||||
if r.remaining < 0 {
|
||||
return r.reader.Read(p)
|
||||
}
|
||||
if r.remaining == 0 {
|
||||
return 0, errHeaderTooLarge
|
||||
}
|
||||
if int64(len(p)) > r.remaining {
|
||||
p = p[:r.remaining]
|
||||
}
|
||||
n, err := r.reader.Read(p)
|
||||
r.remaining -= int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
var _ io.Reader = (*readLimiter)(nil)
|
||||
@@ -0,0 +1,124 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
const (
|
||||
maxHeaderBytes = 1 << 20
|
||||
idleTimeout = 60 * time.Second
|
||||
maxDiscardBodyBytes = 256 << 10
|
||||
discardBodyTimeout = 5 * time.Second
|
||||
realm = "sing-box"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
N.TCPConnectionHandlerEx
|
||||
N.UDPConnectionHandlerEx
|
||||
}
|
||||
|
||||
type ServerOptions struct {
|
||||
Authenticator *auth.Authenticator
|
||||
Logger logger.ContextLogger
|
||||
HTTP1 bool
|
||||
HTTP2 bool
|
||||
HTTP2Options option.HTTP2Options
|
||||
UDP bool
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
authenticator *auth.Authenticator
|
||||
logger logger.ContextLogger
|
||||
http1 bool
|
||||
http2Server *http2.Server
|
||||
udp bool
|
||||
}
|
||||
|
||||
func NewServer(options ServerOptions) *Server {
|
||||
server := &Server{
|
||||
authenticator: options.Authenticator,
|
||||
logger: options.Logger,
|
||||
http1: options.HTTP1,
|
||||
udp: options.UDP,
|
||||
}
|
||||
if options.HTTP2 {
|
||||
server.http2Server = &http2.Server{
|
||||
IdleTimeout: idleTimeout,
|
||||
ReadIdleTimeout: time.Duration(options.HTTP2Options.KeepAlivePeriod),
|
||||
PingTimeout: time.Duration(options.HTTP2Options.IdleTimeout),
|
||||
MaxConcurrentStreams: uint32(max(options.HTTP2Options.MaxConcurrentStreams, 0)),
|
||||
MaxUploadBufferPerConnection: int32(min(options.HTTP2Options.ConnectionReceiveWindow.Value(), math.MaxInt32)),
|
||||
MaxUploadBufferPerStream: int32(min(options.HTTP2Options.StreamReceiveWindow.Value(), math.MaxInt32)),
|
||||
}
|
||||
if options.HTTP2Options.IdleTimeout > 0 {
|
||||
server.http2Server.IdleTimeout = time.Duration(options.HTTP2Options.IdleTimeout)
|
||||
}
|
||||
}
|
||||
return server
|
||||
}
|
||||
|
||||
func (s *Server) ServeConnection(ctx context.Context, conn net.Conn, reader *Reader, handler Handler, source M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
if s.http2Server != nil {
|
||||
conn.SetReadDeadline(time.Now().Add(idleTimeout))
|
||||
isHTTP2, err := reader.isHTTP2Preface()
|
||||
conn.SetReadDeadline(time.Time{})
|
||||
if err != nil {
|
||||
s.finishConnection(ctx, conn, source, onClose, E.Cause(err, "peek request"))
|
||||
return
|
||||
}
|
||||
if isHTTP2 {
|
||||
s.serveHTTP2(ctx, conn, reader, handler, source, onClose)
|
||||
return
|
||||
}
|
||||
}
|
||||
if !s.http1 {
|
||||
_, err := conn.Write([]byte("HTTP/1.1 505 HTTP Version Not Supported\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"))
|
||||
s.finishConnection(ctx, conn, source, onClose, err)
|
||||
return
|
||||
}
|
||||
connection := &serverConn{
|
||||
server: s,
|
||||
ctx: ctx,
|
||||
conn: conn,
|
||||
reader: reader,
|
||||
handler: handler,
|
||||
source: source,
|
||||
onClose: onClose,
|
||||
}
|
||||
connection.serve()
|
||||
}
|
||||
|
||||
func (s *Server) HTTP3Handler(handler Handler) http.Handler {
|
||||
return &httpHandler{
|
||||
server: s,
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) finishConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc, err error) {
|
||||
conn.Close()
|
||||
if err != nil {
|
||||
if E.IsClosedOrCanceled(err) || E.IsTimeout(err) {
|
||||
s.logger.DebugContext(ctx, "connection closed: ", err)
|
||||
err = nil
|
||||
} else {
|
||||
s.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", source))
|
||||
}
|
||||
}
|
||||
if onClose != nil {
|
||||
onClose(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/badhttp"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type requestResult uint8
|
||||
|
||||
const (
|
||||
requestContinue requestResult = iota
|
||||
requestClose
|
||||
requestHandedOff
|
||||
)
|
||||
|
||||
type serverConn struct {
|
||||
server *Server
|
||||
ctx context.Context
|
||||
conn net.Conn
|
||||
reader *Reader
|
||||
handler Handler
|
||||
source M.Socksaddr
|
||||
onClose N.CloseHandlerFunc
|
||||
upstream *upstreamConn
|
||||
}
|
||||
|
||||
func (c *serverConn) serve() {
|
||||
for {
|
||||
result, err := c.serveRequest()
|
||||
switch result {
|
||||
case requestContinue:
|
||||
continue
|
||||
case requestHandedOff:
|
||||
c.closeUpstream()
|
||||
return
|
||||
case requestClose:
|
||||
c.closeUpstream()
|
||||
c.server.finishConnection(c.ctx, c.conn, c.source, c.onClose, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *serverConn) serveRequest() (requestResult, error) {
|
||||
c.conn.SetReadDeadline(time.Now().Add(idleTimeout))
|
||||
c.reader.setLimit(maxHeaderBytes)
|
||||
request, err := badhttp.ReadRequest(c.reader.Reader)
|
||||
c.reader.setLimit(-1)
|
||||
c.conn.SetReadDeadline(time.Time{})
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, io.EOF), E.IsTimeout(err), E.IsClosed(err):
|
||||
return requestClose, err
|
||||
case errors.Is(err, errHeaderTooLarge):
|
||||
return c.rejectAndClose(nil, http.StatusRequestHeaderFieldsTooLarge, err)
|
||||
default:
|
||||
return c.rejectAndClose(nil, http.StatusBadRequest, E.Cause(err, "read request"))
|
||||
}
|
||||
}
|
||||
ctx := c.ctx
|
||||
if c.server.authenticator != nil {
|
||||
username, password, valid := badhttp.ParseBasicAuth(request.Header.Get("Proxy-Authorization"))
|
||||
if !valid || !c.server.authenticator.Verify(username, password) {
|
||||
var authErr error
|
||||
if !valid {
|
||||
authErr = E.New("authentication failed: missing or malformed Proxy-Authorization")
|
||||
} else {
|
||||
authErr = E.New("authentication failed: username=", username)
|
||||
}
|
||||
c.server.logger.ErrorContext(ctx, E.Cause(authErr, "process connection from ", c.source))
|
||||
return c.reject(request, requestKeepAlive(request), http.StatusProxyAuthRequired, authErr)
|
||||
}
|
||||
ctx = auth.ContextWithUser(ctx, username)
|
||||
}
|
||||
source := badhttp.ForwardedSource(request, c.source)
|
||||
switch {
|
||||
case request.Method == http.MethodConnect:
|
||||
return c.serveConnect(ctx, request, source)
|
||||
case c.server.udp && requestIsConnectUDP(request):
|
||||
return c.serveConnectUDP(ctx, request, source)
|
||||
case requestIsUpgrade(request):
|
||||
return c.serveForward(ctx, request, source, true)
|
||||
default:
|
||||
return c.serveForward(ctx, request, source, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *serverConn) serveConnect(ctx context.Context, request *http.Request, source M.Socksaddr) (requestResult, error) {
|
||||
destination := connectDestination(request)
|
||||
if !destination.IsValid() {
|
||||
return c.reject(request, requestKeepAlive(request), http.StatusBadRequest, E.New("invalid CONNECT target: ", request.URL.Host))
|
||||
}
|
||||
_, err := c.conn.Write([]byte(F.ToString("HTTP/", request.ProtoMajor, ".", request.ProtoMinor, " 200 Connection established\r\n\r\n")))
|
||||
if err != nil {
|
||||
return requestClose, E.Cause(err, "write response")
|
||||
}
|
||||
c.handler.NewConnectionEx(ctx, c.reader.cachedConn(c.conn), source, destination, c.onClose)
|
||||
return requestHandedOff, nil
|
||||
}
|
||||
|
||||
func (c *serverConn) reject(request *http.Request, keepAlive bool, statusCode int, cause error) (requestResult, error) {
|
||||
if keepAlive && request.Body != nil && request.Body != http.NoBody {
|
||||
keepAlive = c.discardBody(request.Body)
|
||||
}
|
||||
err := c.writeReject(request, statusCode, keepAlive)
|
||||
if err != nil {
|
||||
return requestClose, E.Errors(cause, err)
|
||||
}
|
||||
if keepAlive {
|
||||
return requestContinue, nil
|
||||
}
|
||||
return requestClose, cause
|
||||
}
|
||||
|
||||
func (c *serverConn) rejectAndClose(request *http.Request, statusCode int, cause error) (requestResult, error) {
|
||||
err := c.writeReject(request, statusCode, false)
|
||||
if err != nil {
|
||||
return requestClose, E.Errors(cause, err)
|
||||
}
|
||||
return requestClose, cause
|
||||
}
|
||||
|
||||
func (c *serverConn) writeReject(request *http.Request, statusCode int, keepAlive bool) error {
|
||||
response := &http.Response{
|
||||
StatusCode: statusCode,
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: make(http.Header),
|
||||
Request: request,
|
||||
Close: !keepAlive,
|
||||
}
|
||||
if request != nil {
|
||||
response.ProtoMajor = request.ProtoMajor
|
||||
response.ProtoMinor = request.ProtoMinor
|
||||
}
|
||||
if statusCode == http.StatusProxyAuthRequired {
|
||||
response.Header.Set("Proxy-Authenticate", `Basic realm="`+realm+`", charset="UTF-8"`)
|
||||
}
|
||||
if keepAlive && request.ProtoMajor == 1 && request.ProtoMinor == 0 {
|
||||
response.Header.Set("Connection", "keep-alive")
|
||||
response.Header.Set("Proxy-Connection", "keep-alive")
|
||||
}
|
||||
err := response.Write(c.conn)
|
||||
if err != nil {
|
||||
return E.Cause(err, "write response")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *serverConn) discardBody(body io.Reader) bool {
|
||||
c.conn.SetReadDeadline(time.Now().Add(discardBodyTimeout))
|
||||
_, err := io.CopyN(io.Discard, body, maxDiscardBodyBytes)
|
||||
c.conn.SetReadDeadline(time.Time{})
|
||||
return errors.Is(err, io.EOF)
|
||||
}
|
||||
|
||||
func (c *serverConn) closeUpstream() {
|
||||
if c.upstream != nil {
|
||||
c.upstream.Close()
|
||||
c.upstream = nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
std_bufio "bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/common/badhttp"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/transport/v2rayhttp"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
"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"
|
||||
"github.com/sagernet/sing/common/pipe"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
const http2Preface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
|
||||
|
||||
func (r *Reader) isHTTP2Preface() (bool, error) {
|
||||
head, err := r.Peek(3)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if string(head) != http2Preface[:3] {
|
||||
return false, nil
|
||||
}
|
||||
preface, err := r.Peek(len(http2Preface))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return string(preface) == http2Preface, nil
|
||||
}
|
||||
|
||||
type connectionStater interface {
|
||||
ConnectionState() tls.ConnectionState
|
||||
}
|
||||
|
||||
type statedConn struct {
|
||||
net.Conn
|
||||
stater connectionStater
|
||||
}
|
||||
|
||||
func (c *statedConn) ConnectionState() tls.ConnectionState {
|
||||
return c.stater.ConnectionState()
|
||||
}
|
||||
|
||||
func (s *Server) serveHTTP2(ctx context.Context, conn net.Conn, reader *Reader, handler Handler, source M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
serveConn := reader.cachedConn(conn)
|
||||
stater, isStater := conn.(connectionStater)
|
||||
if isStater && serveConn != conn {
|
||||
serveConn = &statedConn{Conn: serveConn, stater: stater}
|
||||
}
|
||||
s.http2Server.ServeConn(serveConn, &http2.ServeConnOpts{
|
||||
Context: ctx,
|
||||
BaseConfig: &http.Server{
|
||||
MaxHeaderBytes: maxHeaderBytes,
|
||||
},
|
||||
Handler: &httpHandler{
|
||||
server: s,
|
||||
handler: handler,
|
||||
source: source,
|
||||
plaintext: !isStater,
|
||||
},
|
||||
})
|
||||
if onClose != nil {
|
||||
onClose(nil)
|
||||
}
|
||||
}
|
||||
|
||||
type httpHandler struct {
|
||||
server *Server
|
||||
handler Handler
|
||||
source M.Socksaddr
|
||||
plaintext bool
|
||||
}
|
||||
|
||||
func (h *httpHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
ctx := log.ContextWithNewID(request.Context())
|
||||
connectionSource := h.source
|
||||
if !connectionSource.IsValid() {
|
||||
connectionSource = M.ParseSocksaddr(request.RemoteAddr).Unwrap()
|
||||
}
|
||||
if h.server.authenticator != nil {
|
||||
username, password, valid := badhttp.ParseBasicAuth(request.Header.Get("Proxy-Authorization"))
|
||||
if !valid || !h.server.authenticator.Verify(username, password) {
|
||||
var authErr error
|
||||
if !valid {
|
||||
authErr = E.New("authentication failed: missing or malformed Proxy-Authorization")
|
||||
} else {
|
||||
authErr = E.New("authentication failed: username=", username)
|
||||
}
|
||||
h.server.logger.ErrorContext(ctx, E.Cause(authErr, "process connection from ", connectionSource))
|
||||
writer.Header().Set("Proxy-Authenticate", `Basic realm="`+realm+`", charset="UTF-8"`)
|
||||
writer.WriteHeader(http.StatusProxyAuthRequired)
|
||||
return
|
||||
}
|
||||
ctx = auth.ContextWithUser(ctx, username)
|
||||
}
|
||||
source := badhttp.ForwardedSource(request, connectionSource)
|
||||
if request.Method == http.MethodConnect {
|
||||
protocol := request.Header.Get(":protocol")
|
||||
if protocol == "" && request.ProtoMajor == 3 && !strings.HasPrefix(request.Proto, "HTTP/") {
|
||||
protocol = request.Proto
|
||||
}
|
||||
switch {
|
||||
case protocol == "":
|
||||
h.serveConnect(ctx, writer, request, source)
|
||||
case protocol == connectUDPProtocol && h.server.udp:
|
||||
h.serveConnectUDP(ctx, writer, request, source)
|
||||
default:
|
||||
h.server.logger.ErrorContext(ctx, "process connection from ", source, ": unsupported CONNECT protocol: ", protocol)
|
||||
writer.WriteHeader(http.StatusNotImplemented)
|
||||
}
|
||||
return
|
||||
}
|
||||
h.serveForward(ctx, writer, request, source)
|
||||
}
|
||||
|
||||
func (h *httpHandler) serveConnect(ctx context.Context, writer http.ResponseWriter, request *http.Request, source M.Socksaddr) {
|
||||
destination := connectDestination(request)
|
||||
if !destination.IsValid() {
|
||||
h.server.logger.ErrorContext(ctx, "process connection from ", source, ": invalid CONNECT target: ", request.Host)
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
writer.(http.Flusher).Flush()
|
||||
conn := v2rayhttp.NewHTTP2Wrapper(&v2rayhttp.ServerHTTPConn{
|
||||
HTTP2Conn: v2rayhttp.NewHTTPConn(request.Body, writer),
|
||||
Flusher: writer.(http.Flusher),
|
||||
})
|
||||
done := make(chan struct{})
|
||||
h.handler.NewConnectionEx(ctx, conn, source, destination, N.OnceClose(func(it error) {
|
||||
close(done)
|
||||
}))
|
||||
<-done
|
||||
conn.CloseWrapper()
|
||||
}
|
||||
|
||||
func (h *httpHandler) serveForward(ctx context.Context, writer http.ResponseWriter, request *http.Request, source M.Socksaddr) {
|
||||
// golang.org/x/net/http2 builds the request URL from :path only (internal/httpcommon.NewServerRequest);
|
||||
// :scheme survives only as request.TLS, which it sets when :scheme is https and the served conn
|
||||
// exposes ConnectionState, so on a plaintext conn the scheme is unknowable. quic-go/http3 sets URL.Scheme.
|
||||
if request.URL.Scheme == "" {
|
||||
if h.plaintext {
|
||||
h.server.logger.ErrorContext(ctx, "process connection from ", source, ": forward request over cleartext HTTP/2")
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if request.TLS != nil {
|
||||
request.URL.Scheme = "https"
|
||||
} else {
|
||||
request.URL.Scheme = "http"
|
||||
}
|
||||
}
|
||||
request.URL.Host = request.Host
|
||||
destination, valid := forwardDestination(request)
|
||||
if !valid {
|
||||
h.server.logger.ErrorContext(ctx, "process connection from ", source, ": invalid forward target: ", request.URL.Scheme, "://", request.Host)
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
removeHopByHopHeaders(request.Header)
|
||||
if _, loaded := request.Header["User-Agent"]; !loaded {
|
||||
request.Header["User-Agent"] = nil
|
||||
}
|
||||
if request.ContentLength == 0 {
|
||||
request.Body = http.NoBody
|
||||
}
|
||||
request.Close = false
|
||||
|
||||
upstream := newUpstreamConn(ctx, h.handler, source, destination)
|
||||
stop := context.AfterFunc(request.Context(), func() {
|
||||
upstream.Close()
|
||||
})
|
||||
defer stop()
|
||||
writeDone := make(chan error, 1)
|
||||
go func() {
|
||||
err := request.Write(upstream)
|
||||
if err != nil {
|
||||
upstream.Close()
|
||||
}
|
||||
writeDone <- err
|
||||
}()
|
||||
defer func() {
|
||||
upstream.Close()
|
||||
request.Body.Close()
|
||||
<-writeDone
|
||||
}()
|
||||
|
||||
var response *http.Response
|
||||
for {
|
||||
var err error
|
||||
response, err = upstream.readResponse(request)
|
||||
if err != nil {
|
||||
upstream.Close()
|
||||
h.server.logger.ErrorContext(ctx, E.Cause(E.Errors(upstream.closeErr(), err), "process connection from ", source, ": read upstream response"))
|
||||
writer.WriteHeader(http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
if response.StatusCode >= 200 {
|
||||
break
|
||||
}
|
||||
if response.StatusCode == http.StatusSwitchingProtocols {
|
||||
upstream.Close()
|
||||
h.server.logger.ErrorContext(ctx, "process connection from ", source, ": unexpected 101 response")
|
||||
writer.WriteHeader(http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
removeHopByHopHeaders(response.Header)
|
||||
maps.Copy(writer.Header(), response.Header)
|
||||
writer.WriteHeader(response.StatusCode)
|
||||
clear(writer.Header())
|
||||
}
|
||||
removeHopByHopHeaders(response.Header)
|
||||
maps.Copy(writer.Header(), response.Header)
|
||||
if !responseHasBody(request, response) && request.Method != http.MethodHead {
|
||||
writer.Header().Del("Content-Length")
|
||||
}
|
||||
for name := range response.Trailer {
|
||||
writer.Header().Add("Trailer", name)
|
||||
}
|
||||
writer.WriteHeader(response.StatusCode)
|
||||
if !responseHasBody(request, response) {
|
||||
response.Body.Close()
|
||||
return
|
||||
}
|
||||
writer.(http.Flusher).Flush()
|
||||
_, err := bufio.Copy(flushWriter{writer}, response.Body)
|
||||
if err != nil {
|
||||
upstream.Close()
|
||||
response.Body.Close()
|
||||
h.server.logger.DebugContext(ctx, "process connection from ", source, ": relay response: ", err)
|
||||
panic(http.ErrAbortHandler)
|
||||
}
|
||||
response.Body.Close()
|
||||
maps.Copy(writer.Header(), response.Trailer)
|
||||
}
|
||||
|
||||
func newUpstreamConn(ctx context.Context, handler Handler, source M.Socksaddr, destination M.Socksaddr) *upstreamConn {
|
||||
serverSide, clientSide := pipe.Pipe()
|
||||
limiter := &readLimiter{reader: clientSide, remaining: -1}
|
||||
upstream := &upstreamConn{
|
||||
Conn: clientSide,
|
||||
reader: std_bufio.NewReader(limiter),
|
||||
limiter: limiter,
|
||||
source: source,
|
||||
destination: destination,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go handler.NewConnectionEx(ctx, serverSide, source, destination, N.OnceClose(func(it error) {
|
||||
upstream.err = it
|
||||
close(upstream.done)
|
||||
}))
|
||||
return upstream
|
||||
}
|
||||
|
||||
type flushWriter struct {
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
func (w flushWriter) Write(p []byte) (int, error) {
|
||||
n, err := w.ResponseWriter.Write(p)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
w.ResponseWriter.(http.Flusher).Flush()
|
||||
return n, nil
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common/baderror"
|
||||
"github.com/sagernet/sing/common/bufio/deadline"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type clientStreamConn struct {
|
||||
reader io.ReadCloser
|
||||
writer net.Conn
|
||||
cancel context.CancelFunc
|
||||
localAddr net.Addr
|
||||
remoteAddr net.Addr
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
func (c *clientStreamConn) Read(p []byte) (int, error) {
|
||||
n, err := c.reader.Read(p)
|
||||
return n, c.wrapError(err)
|
||||
}
|
||||
|
||||
func (c *clientStreamConn) Write(p []byte) (int, error) {
|
||||
n, err := c.writer.Write(p)
|
||||
return n, c.wrapError(err)
|
||||
}
|
||||
|
||||
func (c *clientStreamConn) wrapError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if c.closed.Load() || strings.Contains(err.Error(), "client connection force closed") {
|
||||
return net.ErrClosed
|
||||
}
|
||||
return baderror.WrapH2(err)
|
||||
}
|
||||
|
||||
func (c *clientStreamConn) CloseWrite() error {
|
||||
return c.writer.Close()
|
||||
}
|
||||
|
||||
func (c *clientStreamConn) Close() error {
|
||||
c.closed.Store(true)
|
||||
c.writer.Close()
|
||||
c.reader.Close()
|
||||
c.cancel()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clientStreamConn) LocalAddr() net.Addr {
|
||||
return c.localAddr
|
||||
}
|
||||
|
||||
func (c *clientStreamConn) RemoteAddr() net.Addr {
|
||||
return c.remoteAddr
|
||||
}
|
||||
|
||||
func (c *clientStreamConn) SetDeadline(t time.Time) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (c *clientStreamConn) SetReadDeadline(t time.Time) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (c *clientStreamConn) SetWriteDeadline(t time.Time) error {
|
||||
return c.writer.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (c *clientStreamConn) NeedAdditionalReadDeadline() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type clientTunnelConn struct {
|
||||
N.ExtendedConn
|
||||
streamConn *clientStreamConn
|
||||
}
|
||||
|
||||
func newClientTunnelConn(streamConn *clientStreamConn) *clientTunnelConn {
|
||||
return &clientTunnelConn{ExtendedConn: deadline.NewConn(streamConn), streamConn: streamConn}
|
||||
}
|
||||
|
||||
func (c *clientTunnelConn) CloseWrite() error {
|
||||
return c.streamConn.CloseWrite()
|
||||
}
|
||||
|
||||
func (c *clientTunnelConn) SetDeadline(t time.Time) error {
|
||||
return E.Errors(c.SetReadDeadline(t), c.SetWriteDeadline(t))
|
||||
}
|
||||
|
||||
func (c *clientTunnelConn) Upstream() any {
|
||||
return c.ExtendedConn
|
||||
}
|
||||
|
||||
var (
|
||||
_ net.Conn = (*clientStreamConn)(nil)
|
||||
_ N.WriteCloser = (*clientStreamConn)(nil)
|
||||
_ N.WriteCloser = (*clientTunnelConn)(nil)
|
||||
)
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/badhttp"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@@ -19,7 +20,6 @@ import (
|
||||
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"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c" //nolint:staticcheck
|
||||
@@ -83,7 +83,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
done := make(chan struct{})
|
||||
conn := v2rayhttp.NewHTTP2Wrapper(newGunConn(request.Body, writer, writer.(http.Flusher)))
|
||||
s.handler.NewConnectionEx(request.Context(), conn, sHttp.SourceAddress(request), M.Socksaddr{}, N.OnceClose(func(it error) {
|
||||
s.handler.NewConnectionEx(request.Context(), conn, badhttp.SourceAddress(request), M.Socksaddr{}, N.OnceClose(func(it error) {
|
||||
close(done)
|
||||
}))
|
||||
<-done
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/badhttp"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@@ -20,7 +21,6 @@ import (
|
||||
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"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
@@ -72,7 +72,7 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
|
||||
}
|
||||
requestURL.Host = serverAddr.String()
|
||||
requestURL.Path = options.Path
|
||||
err := sHTTP.URLSetPath(&requestURL, options.Path)
|
||||
err := badhttp.URLSetPath(&requestURL, options.Path)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse path")
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/badhttp"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -21,7 +22,6 @@ import (
|
||||
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"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c" //nolint:staticcheck
|
||||
@@ -103,7 +103,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
source := sHttp.SourceAddress(request)
|
||||
source := badhttp.SourceAddress(request)
|
||||
if h, ok := writer.(http.Hijacker); ok {
|
||||
if request.ContentLength > buf.MaxPooledBufferSize {
|
||||
s.invalidRequest(writer, request, http.StatusRequestEntityTooLarge, E.New("request body too large: ", request.ContentLength))
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/badhttp"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
@@ -16,7 +17,6 @@ import (
|
||||
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)
|
||||
@@ -57,7 +57,7 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
|
||||
}
|
||||
requestURL.Host = serverAddr.String()
|
||||
requestURL.Path = options.Path
|
||||
err := sHTTP.URLSetPath(&requestURL, options.Path)
|
||||
err := badhttp.URLSetPath(&requestURL, options.Path)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse path")
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/badhttp"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -21,7 +22,6 @@ import (
|
||||
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)
|
||||
@@ -125,7 +125,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
conn = bufio.NewCachedConn(conn, cache)
|
||||
}
|
||||
s.handler.NewConnectionEx(v2rayhttp.DupContext(request.Context()), conn, sHttp.SourceAddress(request), M.Socksaddr{}, nil)
|
||||
s.handler.NewConnectionEx(v2rayhttp.DupContext(request.Context()), conn, badhttp.SourceAddress(request), M.Socksaddr{}, nil)
|
||||
}
|
||||
|
||||
func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Request, statusCode int, err error) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/badhttp"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@@ -18,7 +19,6 @@ import (
|
||||
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"
|
||||
"github.com/sagernet/ws"
|
||||
)
|
||||
|
||||
@@ -48,7 +48,7 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
|
||||
}
|
||||
requestURL.Host = serverAddr.String()
|
||||
requestURL.Path = options.Path
|
||||
err := sHTTP.URLSetPath(&requestURL, options.Path)
|
||||
err := badhttp.URLSetPath(&requestURL, options.Path)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse path")
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/badhttp"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -22,7 +23,6 @@ import (
|
||||
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"
|
||||
"github.com/sagernet/ws"
|
||||
)
|
||||
|
||||
@@ -110,7 +110,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
s.invalidRequest(writer, request, 0, E.Cause(err, "upgrade websocket connection"))
|
||||
return
|
||||
}
|
||||
source := sHttp.SourceAddress(request)
|
||||
source := badhttp.SourceAddress(request)
|
||||
conn = NewConn(wsConn, source, ws.StateServerSide)
|
||||
if len(earlyData) > 0 {
|
||||
conn = bufio.NewCachedConn(conn, buf.As(earlyData))
|
||||
|
||||
Reference in new issue
Block a user