Rewrite HTTP proxy protocol

This commit is contained in:
世界 committed 2026-11-09 15:17:20 +00:00
1 parent d81cff5aa5
commit 67773d6bd4
49 files changed
+5306 -138

No files matched your search

+36
View File
@@ -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
}
+20 -15
View File
@@ -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
}
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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
}
+2 -2
View File
@@ -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
}
+32 -1
View File
@@ -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.
+33 -2
View File
@@ -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/)。
+40 -1
View File
@@ -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.
+39
View File
@@ -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/)。
+109
View File
@@ -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": [
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+111
View File
@@ -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()
}
+2 -2
View File
@@ -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
View File
@@ -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)
}
}
+3 -3
View File
@@ -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 {
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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=
+526
View File
@@ -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)
}
+630
View File
@@ -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)
}
+373
View File
@@ -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)
}
+56
View File
@@ -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))
}
+371
View File
@@ -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))
}
+2 -2
View File
@@ -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")
}
+402
View File
@@ -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)
+403
View File
@@ -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
}
+127
View File
@@ -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
}
+331
View File
@@ -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)
)
+178
View File
@@ -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()
}
+14
View File
@@ -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
}
+250
View File
@@ -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
}
+82
View File
@@ -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
}
+65
View File
@@ -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)
+124
View File
@@ -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)
}
}
+173
View File
@@ -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
}
}
+277
View File
@@ -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
}
+108
View File
@@ -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)
)
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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")
}
+2 -2
View File
@@ -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))
+2 -2
View File
@@ -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")
}
+2 -2
View File
@@ -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) {
+2 -2
View File
@@ -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")
}
+2 -2
View File
@@ -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))