Browse Source

Migrate multiplex and UoT server to inbound & Add tcp-brutal support for multiplex

世界 2 năm trước cách đây
mục cha
commit
1b71e52e90
65 tập tin đã thay đổi với 997 bổ sung176 xóa
  1. 104 0
      adapter/conn_router.go
  2. 1 4
      adapter/router.go
  3. 22 1
      common/mux/client.go
  4. 0 14
      common/mux/protocol.go
  5. 65 0
      common/mux/router.go
  6. 32 0
      common/mux/v2ray_legacy.go
  7. 53 0
      common/uot/router.go
  8. 3 0
      constant/speed.go
  9. 1 1
      docs/configuration/inbound/hysteria2.zh.md
  10. 10 3
      docs/configuration/inbound/shadowsocks.md
  11. 11 4
      docs/configuration/inbound/shadowsocks.zh.md
  12. 5 0
      docs/configuration/inbound/trojan.md
  13. 5 0
      docs/configuration/inbound/trojan.zh.md
  14. 1 1
      docs/configuration/inbound/tuic.zh.md
  15. 5 0
      docs/configuration/inbound/vless.md
  16. 5 0
      docs/configuration/inbound/vless.zh.md
  17. 5 0
      docs/configuration/inbound/vmess.md
  18. 5 0
      docs/configuration/inbound/vmess.zh.md
  19. 1 1
      docs/configuration/outbound/hysteria2.zh.md
  20. 1 1
      docs/configuration/outbound/shadowsocks.md
  21. 1 1
      docs/configuration/outbound/shadowsocks.zh.md
  22. 1 1
      docs/configuration/outbound/trojan.md
  23. 1 1
      docs/configuration/outbound/trojan.zh.md
  24. 1 1
      docs/configuration/outbound/tuic.zh.md
  25. 5 0
      docs/configuration/outbound/vless.md
  26. 5 0
      docs/configuration/outbound/vless.zh.md
  27. 2 2
      docs/configuration/outbound/vmess.md
  28. 1 1
      docs/configuration/outbound/vmess.zh.md
  29. 30 5
      docs/configuration/shared/multiplex.md
  30. 30 5
      docs/configuration/shared/multiplex.zh.md
  31. 28 0
      docs/configuration/shared/tcp-brutal.md
  32. 28 0
      docs/configuration/shared/tcp-brutal.zh.md
  33. 1 1
      go.mod
  34. 2 2
      go.sum
  35. 1 1
      inbound/default.go
  36. 2 1
      inbound/http.go
  37. 2 1
      inbound/mixed.go
  38. 2 1
      inbound/naive.go
  39. 10 2
      inbound/shadowsocks.go
  40. 9 5
      inbound/shadowsocks_multi.go
  41. 8 1
      inbound/shadowsocks_relay.go
  42. 2 1
      inbound/socks.go
  43. 5 0
      inbound/trojan.go
  44. 2 1
      inbound/tuic.go
  45. 8 2
      inbound/vless.go
  46. 9 2
      inbound/vmess.go
  47. 1 0
      mkdocs.yml
  48. 23 0
      option/multiplex.go
  49. 0 9
      option/outbound.go
  50. 8 7
      option/shadowsocks.go
  51. 5 5
      option/simple.go
  52. 6 5
      option/trojan.go
  53. 11 10
      option/vless.go
  54. 14 13
      option/vmess.go
  55. 2 2
      outbound/shadowsocks.go
  56. 1 1
      outbound/socks.go
  57. 1 1
      outbound/trojan.go
  58. 1 1
      outbound/vless.go
  59. 1 1
      outbound/vmess.go
  60. 5 22
      route/router.go
  61. 346 0
      test/brutal_test.go
  62. 9 7
      test/go.mod
  63. 19 14
      test/go.sum
  64. 12 9
      test/mux_test.go
  65. 1 1
      test/shadowsocks_test.go

+ 104 - 0
adapter/conn_router.go

@@ -0,0 +1,104 @@
+package adapter
+
+import (
+	"context"
+	"net"
+
+	"github.com/sagernet/sing/common/logger"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+)
+
+type ConnectionRouter interface {
+	RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
+	RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
+}
+
+func NewRouteHandler(
+	metadata InboundContext,
+	router ConnectionRouter,
+	logger logger.ContextLogger,
+) UpstreamHandlerAdapter {
+	return &routeHandlerWrapper{
+		metadata: metadata,
+		router:   router,
+		logger:   logger,
+	}
+}
+
+func NewRouteContextHandler(
+	router ConnectionRouter,
+	logger logger.ContextLogger,
+) UpstreamHandlerAdapter {
+	return &routeContextHandlerWrapper{
+		router: router,
+		logger: logger,
+	}
+}
+
+var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
+
+type routeHandlerWrapper struct {
+	metadata InboundContext
+	router   ConnectionRouter
+	logger   logger.ContextLogger
+}
+
+func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
+	myMetadata := w.metadata
+	if metadata.Source.IsValid() {
+		myMetadata.Source = metadata.Source
+	}
+	if metadata.Destination.IsValid() {
+		myMetadata.Destination = metadata.Destination
+	}
+	return w.router.RouteConnection(ctx, conn, myMetadata)
+}
+
+func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
+	myMetadata := w.metadata
+	if metadata.Source.IsValid() {
+		myMetadata.Source = metadata.Source
+	}
+	if metadata.Destination.IsValid() {
+		myMetadata.Destination = metadata.Destination
+	}
+	return w.router.RoutePacketConnection(ctx, conn, myMetadata)
+}
+
+func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) {
+	w.logger.ErrorContext(ctx, err)
+}
+
+var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
+
+type routeContextHandlerWrapper struct {
+	router ConnectionRouter
+	logger logger.ContextLogger
+}
+
+func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
+	myMetadata := ContextFrom(ctx)
+	if metadata.Source.IsValid() {
+		myMetadata.Source = metadata.Source
+	}
+	if metadata.Destination.IsValid() {
+		myMetadata.Destination = metadata.Destination
+	}
+	return w.router.RouteConnection(ctx, conn, *myMetadata)
+}
+
+func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
+	myMetadata := ContextFrom(ctx)
+	if metadata.Source.IsValid() {
+		myMetadata.Source = metadata.Source
+	}
+	if metadata.Destination.IsValid() {
+		myMetadata.Destination = metadata.Destination
+	}
+	return w.router.RoutePacketConnection(ctx, conn, *myMetadata)
+}
+
+func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) {
+	w.logger.ErrorContext(ctx, err)
+}

+ 1 - 4
adapter/router.go

@@ -2,14 +2,12 @@ package adapter
 
 import (
 	"context"
-	"net"
 	"net/netip"
 
 	"github.com/sagernet/sing-box/common/geoip"
 	"github.com/sagernet/sing-dns"
 	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing/common/control"
-	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/service"
 
 	mdns "github.com/miekg/dns"
@@ -24,8 +22,7 @@ type Router interface {
 
 	FakeIPStore() FakeIPStore
 
-	RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
-	RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
+	ConnectionRouter
 
 	GeoIPReader() *geoip.Reader
 	LoadGeosite(code string) (Rule, error)

+ 22 - 1
common/mux/client.go

@@ -1,21 +1,42 @@
 package mux
 
 import (
+	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-mux"
+	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/logger"
 	N "github.com/sagernet/sing/common/network"
 )
 
-func NewClientWithOptions(dialer N.Dialer, options option.MultiplexOptions) (*Client, error) {
+type Client = mux.Client
+
+func NewClientWithOptions(dialer N.Dialer, logger logger.Logger, options option.OutboundMultiplexOptions) (*Client, error) {
 	if !options.Enabled {
 		return nil, nil
 	}
+	var brutalOptions mux.BrutalOptions
+	if options.Brutal != nil && options.Brutal.Enabled {
+		brutalOptions = mux.BrutalOptions{
+			Enabled:    true,
+			SendBPS:    uint64(options.Brutal.UpMbps * C.MbpsToBps),
+			ReceiveBPS: uint64(options.Brutal.DownMbps * C.MbpsToBps),
+		}
+		if brutalOptions.SendBPS < mux.BrutalMinSpeedBPS {
+			return nil, E.New("brutal: invalid upload speed")
+		}
+		if brutalOptions.ReceiveBPS < mux.BrutalMinSpeedBPS {
+			return nil, E.New("brutal: invalid download speed")
+		}
+	}
 	return mux.NewClient(mux.Options{
 		Dialer:         dialer,
+		Logger:         logger,
 		Protocol:       options.Protocol,
 		MaxConnections: options.MaxConnections,
 		MinStreams:     options.MinStreams,
 		MaxStreams:     options.MaxStreams,
 		Padding:        options.Padding,
+		Brutal:         brutalOptions,
 	})
 }

+ 0 - 14
common/mux/protocol.go

@@ -1,14 +0,0 @@
-package mux
-
-import (
-	"github.com/sagernet/sing-mux"
-)
-
-type (
-	Client = mux.Client
-)
-
-var (
-	Destination      = mux.Destination
-	HandleConnection = mux.HandleConnection
-)

+ 65 - 0
common/mux/router.go

@@ -0,0 +1,65 @@
+package mux
+
+import (
+	"context"
+	"net"
+
+	"github.com/sagernet/sing-box/adapter"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-mux"
+	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/logger"
+	N "github.com/sagernet/sing/common/network"
+)
+
+type Router struct {
+	router  adapter.ConnectionRouter
+	service *mux.Service
+}
+
+func NewRouterWithOptions(router adapter.ConnectionRouter, logger logger.ContextLogger, options option.InboundMultiplexOptions) (adapter.ConnectionRouter, error) {
+	if !options.Enabled {
+		return router, nil
+	}
+	var brutalOptions mux.BrutalOptions
+	if options.Brutal != nil && options.Brutal.Enabled {
+		brutalOptions = mux.BrutalOptions{
+			Enabled:    true,
+			SendBPS:    uint64(options.Brutal.UpMbps * C.MbpsToBps),
+			ReceiveBPS: uint64(options.Brutal.DownMbps * C.MbpsToBps),
+		}
+		if brutalOptions.SendBPS < mux.BrutalMinSpeedBPS {
+			return nil, E.New("brutal: invalid upload speed")
+		}
+		if brutalOptions.ReceiveBPS < mux.BrutalMinSpeedBPS {
+			return nil, E.New("brutal: invalid download speed")
+		}
+	}
+	service, err := mux.NewService(mux.ServiceOptions{
+		NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context {
+			return log.ContextWithNewID(ctx)
+		},
+		Logger:  logger,
+		Handler: adapter.NewRouteContextHandler(router, logger),
+		Padding: options.Padding,
+		Brutal:  brutalOptions,
+	})
+	if err != nil {
+		return nil, err
+	}
+	return &Router{router, service}, nil
+}
+
+func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	if metadata.Destination == mux.Destination {
+		return r.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, adapter.UpstreamMetadata(metadata))
+	} else {
+		return r.router.RouteConnection(ctx, conn, metadata)
+	}
+}
+
+func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	return r.router.RoutePacketConnection(ctx, conn, metadata)
+}

+ 32 - 0
common/mux/v2ray_legacy.go

@@ -0,0 +1,32 @@
+package mux
+
+import (
+	"context"
+	"net"
+
+	"github.com/sagernet/sing-box/adapter"
+	vmess "github.com/sagernet/sing-vmess"
+	"github.com/sagernet/sing/common/logger"
+	N "github.com/sagernet/sing/common/network"
+)
+
+type V2RayLegacyRouter struct {
+	router adapter.ConnectionRouter
+	logger logger.ContextLogger
+}
+
+func NewV2RayLegacyRouter(router adapter.ConnectionRouter, logger logger.ContextLogger) adapter.ConnectionRouter {
+	return &V2RayLegacyRouter{router, logger}
+}
+
+func (r *V2RayLegacyRouter) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	if metadata.Destination.Fqdn == vmess.MuxDestination.Fqdn {
+		r.logger.InfoContext(ctx, "inbound legacy multiplex connection")
+		return vmess.HandleMuxConnection(ctx, conn, adapter.NewRouteHandler(metadata, r.router, r.logger))
+	}
+	return r.router.RouteConnection(ctx, conn, metadata)
+}
+
+func (r *V2RayLegacyRouter) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	return r.router.RoutePacketConnection(ctx, conn, metadata)
+}

+ 53 - 0
common/uot/router.go

@@ -0,0 +1,53 @@
+package uot
+
+import (
+	"context"
+	"net"
+	"net/netip"
+
+	"github.com/sagernet/sing-box/adapter"
+	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"
+	"github.com/sagernet/sing/common/uot"
+)
+
+var _ adapter.ConnectionRouter = (*Router)(nil)
+
+type Router struct {
+	router adapter.ConnectionRouter
+	logger logger.ContextLogger
+}
+
+func NewRouter(router adapter.ConnectionRouter, logger logger.ContextLogger) *Router {
+	return &Router{router, logger}
+}
+
+func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	switch metadata.Destination.Fqdn {
+	case uot.MagicAddress:
+		request, err := uot.ReadRequest(conn)
+		if err != nil {
+			return E.Cause(err, "read UoT request")
+		}
+		if request.IsConnect {
+			r.logger.InfoContext(ctx, "inbound UoT connect connection to ", request.Destination)
+		} else {
+			r.logger.InfoContext(ctx, "inbound UoT connection to ", request.Destination)
+		}
+		metadata.Domain = metadata.Destination.Fqdn
+		metadata.Destination = request.Destination
+		return r.router.RoutePacketConnection(ctx, uot.NewConn(conn, *request), metadata)
+	case uot.LegacyMagicAddress:
+		r.logger.InfoContext(ctx, "inbound legacy UoT connection")
+		metadata.Domain = metadata.Destination.Fqdn
+		metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()}
+		return r.RoutePacketConnection(ctx, uot.NewConn(conn, uot.Request{}), metadata)
+	}
+	return r.router.RouteConnection(ctx, conn, metadata)
+}
+
+func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	return r.router.RoutePacketConnection(ctx, conn, metadata)
+}

+ 3 - 0
constant/speed.go

@@ -0,0 +1,3 @@
+package constant
+
+const MbpsToBps = 125000

+ 1 - 1
docs/configuration/inbound/hysteria2.zh.md

@@ -62,7 +62,7 @@ Hysteria 用户
 
 #### ignore_client_bandwidth
 
-命令客户端使用 BBR 流量控制算法而不是 Hysteria CC。
+命令客户端使用 BBR 拥塞控制算法而不是 Hysteria CC。
 
 与 `up_mbps` 和 `down_mbps` 冲突。
 

+ 10 - 3
docs/configuration/inbound/shadowsocks.md

@@ -8,7 +8,8 @@
   ... // Listen Fields
 
   "method": "2022-blake3-aes-128-gcm",
-  "password": "8JCsPssfgS8tiRwiMlhARg=="
+  "password": "8JCsPssfgS8tiRwiMlhARg==",
+  "multiplex": {}
 }
 ```
 
@@ -23,7 +24,8 @@
       "name": "sekai",
       "password": "PCD2Z4o12bKUoFa3cC97Hw=="
     }
-  ]
+  ],
+  "multiplex": {}
 }
 ```
 
@@ -41,7 +43,8 @@
       "server_port": 8080,
       "password": "PCD2Z4o12bKUoFa3cC97Hw=="
     }
-  ]
+  ],
+  "multiplex": {}
 }
 ```
 
@@ -82,3 +85,7 @@ Both if empty.
 | none          | /                                              |
 | 2022 methods  | `sing-box generate rand --base64 <Key Length>` |
 | other methods | any string                                     |
+
+#### multiplex
+
+See [Multiplex](/configuration/shared/multiplex#inbound) for details.

+ 11 - 4
docs/configuration/inbound/shadowsocks.zh.md

@@ -8,7 +8,8 @@
   ... // 监听字段
 
   "method": "2022-blake3-aes-128-gcm",
-  "password": "8JCsPssfgS8tiRwiMlhARg=="
+  "password": "8JCsPssfgS8tiRwiMlhARg==",
+  "multiplex": {}
 }
 ```
 
@@ -23,7 +24,8 @@
       "name": "sekai",
       "password": "PCD2Z4o12bKUoFa3cC97Hw=="
     }
-  ]
+  ],
+  "multiplex": {}
 }
 ```
 
@@ -41,7 +43,8 @@
       "server_port": 8080,
       "password": "PCD2Z4o12bKUoFa3cC97Hw=="
     }
-  ]
+  ],
+  "multiplex": {}
 }
 ```
 
@@ -81,4 +84,8 @@ See [Listen Fields](/configuration/shared/listen) for details.
 |---------------|------------------------------------------|
 | none          | /                                        |
 | 2022 methods  | `sing-box generate rand --base64 <密钥长度>` |
-| other methods | 任意字符串                                    |
+| other methods | 任意字符串                                    |
+
+#### multiplex
+
+参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。

+ 5 - 0
docs/configuration/inbound/trojan.md

@@ -24,6 +24,7 @@
       "server_port": 8081
     }
   },
+  "multiplex": {},
   "transport": {}
 }
 ```
@@ -58,6 +59,10 @@ Fallback server configuration for specified ALPN.
 
 If not empty, TLS fallback requests with ALPN not in this table will be rejected.
 
+#### multiplex
+
+See [Multiplex](/configuration/shared/multiplex#inbound) for details.
+
 #### transport
 
 V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport).

+ 5 - 0
docs/configuration/inbound/trojan.zh.md

@@ -24,6 +24,7 @@
       "server_port": 8081
     }
   },
+  "multiplex": {},
   "transport": {}
 }
 ```
@@ -60,6 +61,10 @@ TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
 
 如果不为空,ALPN 不在此列表中的 TLS 回退请求将被拒绝。
 
+#### multiplex
+
+参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
+
 #### transport
 
 V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport)。

+ 1 - 1
docs/configuration/inbound/tuic.zh.md

@@ -48,7 +48,7 @@ TUIC 用户密码
 
 #### congestion_control
 
-QUIC 流量控制算法
+QUIC 拥塞控制算法
 
 可选值: `cubic`, `new_reno`, `bbr`
 

+ 5 - 0
docs/configuration/inbound/vless.md

@@ -15,6 +15,7 @@
     }
   ],
   "tls": {},
+  "multiplex": {},
   "transport": {}
 }
 ```
@@ -49,6 +50,10 @@ Available values:
 
 TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
 
+#### multiplex
+
+See [Multiplex](/configuration/shared/multiplex#inbound) for details.
+
 #### transport
 
 V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport).

+ 5 - 0
docs/configuration/inbound/vless.zh.md

@@ -15,6 +15,7 @@
     }
   ],
   "tls": {},
+  "multiplex": {},
   "transport": {}
 }
 ```
@@ -49,6 +50,10 @@ VLESS 子协议。
 
 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
 
+#### multiplex
+
+参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
+
 #### transport
 
 V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport)。

+ 5 - 0
docs/configuration/inbound/vmess.md

@@ -15,6 +15,7 @@
     }
   ],
   "tls": {},
+  "multiplex": {},
   "transport": {}
 }
 ```
@@ -44,6 +45,10 @@ VMess users.
 
 TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
 
+#### multiplex
+
+See [Multiplex](/configuration/shared/multiplex#inbound) for details.
+
 #### transport
 
 V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport).

+ 5 - 0
docs/configuration/inbound/vmess.zh.md

@@ -15,6 +15,7 @@
     }
   ],
   "tls": {},
+  "multiplex": {},
   "transport": {}
 }
 ```
@@ -44,6 +45,10 @@ VMess 用户。
 
 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
 
+#### multiplex
+
+参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
+
 #### transport
 
 V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport)。

+ 1 - 1
docs/configuration/outbound/hysteria2.zh.md

@@ -44,7 +44,7 @@
 
 最大带宽。
 
-如果为空,将使用 BBR 流量控制算法而不是 Hysteria CC。
+如果为空,将使用 BBR 拥塞控制算法而不是 Hysteria CC。
 
 #### obfs.type
 

+ 1 - 1
docs/configuration/outbound/shadowsocks.md

@@ -95,7 +95,7 @@ Conflict with `multiplex`.
 
 #### multiplex
 
-Multiplex configuration, see [Multiplex](/configuration/shared/multiplex).
+See [Multiplex](/configuration/shared/multiplex#outbound) for details.
 
 ### Dial Fields
 

+ 1 - 1
docs/configuration/outbound/shadowsocks.zh.md

@@ -95,7 +95,7 @@ UDP over TCP 配置。
 
 #### multiplex
 
-多路复用配置, 参阅 [多路复用](/zh/configuration/shared/multiplex)。
+参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
 
 ### 拨号字段
 

+ 1 - 1
docs/configuration/outbound/trojan.md

@@ -51,7 +51,7 @@ TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
 
 #### multiplex
 
-Multiplex configuration, see [Multiplex](/configuration/shared/multiplex).
+See [Multiplex](/configuration/shared/multiplex#outbound) for details.
 
 #### transport
 

+ 1 - 1
docs/configuration/outbound/trojan.zh.md

@@ -51,7 +51,7 @@ TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
 
 #### multiplex
 
-多路复用配置, 参阅 [多路复用](/zh/configuration/shared/multiplex)。
+参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
 
 #### transport
 

+ 1 - 1
docs/configuration/outbound/tuic.zh.md

@@ -51,7 +51,7 @@ TUIC 用户密码
 
 #### congestion_control
 
-QUIC 流量控制算法
+QUIC 拥塞控制算法
 
 可选值: `cubic`, `new_reno`, `bbr`
 

+ 5 - 0
docs/configuration/outbound/vless.md

@@ -12,6 +12,7 @@
   "network": "tcp",
   "tls": {},
   "packet_encoding": "",
+  "multiplex": {},
   "transport": {},
 
   ... // Dial Fields
@@ -68,6 +69,10 @@ UDP packet encoding, xudp is used by default.
 | packetaddr | Supported by v2ray 5+ |
 | xudp       | Supported by xray     |
 
+#### multiplex
+
+See [Multiplex](/configuration/shared/multiplex#outbound) for details.
+
 #### transport
 
 V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport).

+ 5 - 0
docs/configuration/outbound/vless.zh.md

@@ -12,6 +12,7 @@
   "network": "tcp",
   "tls": {},
   "packet_encoding": "",
+  "multiplex": {},
   "transport": {},
   
   ... // 拨号字段
@@ -68,6 +69,10 @@ UDP 包编码,默认使用 xudp。
 | packetaddr | 由 v2ray 5+ 支持 |
 | xudp       | 由 xray 支持     |
 
+#### multiplex
+
+参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
+
 #### transport
 
 V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport)。

+ 2 - 2
docs/configuration/outbound/vmess.md

@@ -15,8 +15,8 @@
   "network": "tcp",
   "tls": {},
   "packet_encoding": "",
-  "multiplex": {},
   "transport": {},
+  "multiplex": {},
 
   ... // Dial Fields
 }
@@ -96,7 +96,7 @@ UDP packet encoding.
 
 #### multiplex
 
-Multiplex configuration, see [Multiplex](/configuration/shared/multiplex).
+See [Multiplex](/configuration/shared/multiplex#outbound) for details.
 
 #### transport
 

+ 1 - 1
docs/configuration/outbound/vmess.zh.md

@@ -96,7 +96,7 @@ UDP 包编码。
 
 #### multiplex
 
-多路复用配置, 参阅 [多路复用](/zh/configuration/shared/multiplex)。
+参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
 
 #### transport
 

+ 30 - 5
docs/configuration/shared/multiplex.md

@@ -1,8 +1,14 @@
-### Server Requirements
+### Inbound
 
-`sing-box` :)
+```json
+{
+  "enabled": true,
+  "padding": false,
+  "brutal": {}
+}
+```
 
-### Structure
+### Outbound
 
 ```json
 {
@@ -11,11 +17,27 @@
   "max_connections": 4,
   "min_streams": 4,
   "max_streams": 0,
-  "padding": false
+  "padding": false,
+  "brutal": {}
 }
 ```
 
-### Fields
+
+### Inbound Fields
+
+#### enabled
+
+Enable multiplex support.
+
+#### padding
+
+If enabled, non-padded connections will be rejected.
+
+#### brutal
+
+See [TCP Brutal](/configuration/shared/tcp-brutal) for details.
+
+### Outbound Fields
 
 #### enabled
 
@@ -59,3 +81,6 @@ Conflict with `max_connections` and `min_streams`.
 
 Enable padding.
 
+#### brutal
+
+See [TCP Brutal](/configuration/shared/tcp-brutal) for details.

+ 30 - 5
docs/configuration/shared/multiplex.zh.md

@@ -1,8 +1,14 @@
-### 服务器要求
+### 入站
 
-`sing-box` :)
+```json
+{
+  "enabled": true,
+  "padding": false,
+  "brutal": {}
+}
+```
 
-### 结构
+### 出站
 
 ```json
 {
@@ -10,11 +16,27 @@
   "protocol": "smux",
   "max_connections": 4,
   "min_streams": 4,
-  "max_streams": 0
+  "max_streams": 0,
+  "padding": false,
+  "brutal": {}
 }
 ```
 
-### 字段
+### 入站字段
+
+#### enabled
+
+启用多路复用支持。
+
+#### padding
+
+如果启用,将拒绝非填充连接。
+
+#### brutal
+
+参阅 [TCP Brutal](/zh/configuration/shared/tcp-brutal)。
+
+### 出站字段
 
 #### enabled
 
@@ -58,3 +80,6 @@
 
 启用填充。
 
+#### brutal
+
+参阅 [TCP Brutal](/zh/configuration/shared/tcp-brutal)。

+ 28 - 0
docs/configuration/shared/tcp-brutal.md

@@ -0,0 +1,28 @@
+### Server Requirements
+
+* Linux
+* `brutal` congestion control algorithm kernel module installed
+
+See [tcp-brutal](https://github.com/apernet/tcp-brutal) for details.
+
+### Structure
+
+```json
+{
+  "enabled": true,
+  "up_mbps": 100,
+  "down_mbps": 100
+}
+```
+
+### Fields
+
+#### enabled
+
+Enable TCP Brutal congestion control algorithm。
+
+#### up_mbps, down_mbps
+
+==Required==
+
+Upload and download bandwidth, in Mbps.

+ 28 - 0
docs/configuration/shared/tcp-brutal.zh.md

@@ -0,0 +1,28 @@
+### 服务器要求
+
+* Linux
+* `brutal` 拥塞控制算法内核模块已安装
+
+参阅 [tcp-brutal](https://github.com/apernet/tcp-brutal)。
+
+### 结构
+
+```json
+{
+  "enabled": true,
+  "up_mbps": 100,
+  "down_mbps": 100
+}
+```
+
+### 字段
+
+#### enabled
+
+启用 TCP Brutal 拥塞控制算法。
+
+#### up_mbps, down_mbps
+
+==必填==
+
+上传和下载带宽,以 Mbps 为单位。

+ 1 - 1
go.mod

@@ -28,7 +28,7 @@ require (
 	github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
 	github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc
 	github.com/sagernet/sing-dns v0.1.11
-	github.com/sagernet/sing-mux v0.1.4
+	github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07
 	github.com/sagernet/sing-quic v0.1.5-0.20231123150204-077075e9b6ad
 	github.com/sagernet/sing-shadowsocks v0.2.5
 	github.com/sagernet/sing-shadowsocks2 v0.1.5

+ 2 - 2
go.sum

@@ -118,8 +118,8 @@ github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc h1:vESVuxHgbd2EzH
 github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
 github.com/sagernet/sing-dns v0.1.11 h1:PPrMCVVrAeR3f5X23I+cmvacXJ+kzuyAsBiWyUKhGSE=
 github.com/sagernet/sing-dns v0.1.11/go.mod h1:zJ/YjnYB61SYE+ubMcMqVdpaSvsyQ2iShQGO3vuLvvE=
-github.com/sagernet/sing-mux v0.1.4 h1:BPNPOQr6HkXG3iY/BrfvUKUl+A7gYsGKVSxvoR3PO50=
-github.com/sagernet/sing-mux v0.1.4/go.mod h1:dKvcu/sb3fZ88uGv9vzAqUej6J4W+pHu5GqjRuFwAWs=
+github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 h1:ncKb5tVOsCQgCsv6UpsA0jinbNb5OQ5GMPJlyQP3EHM=
+github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07/go.mod h1:u/MZf32xPG8jEKe3t+xUV67EBnKtDtCaPhsJQOQGUYU=
 github.com/sagernet/sing-quic v0.1.5-0.20231123150204-077075e9b6ad h1:PyMeM7c5xbrMbqGkIOMo6m2ip8o7TP0JONfDWs17rzg=
 github.com/sagernet/sing-quic v0.1.5-0.20231123150204-077075e9b6ad/go.mod h1:aXHVP+osF3w5wJzoWZbJSrX3ceJiU9QMd0KPnKV6C/o=
 github.com/sagernet/sing-shadowsocks v0.2.5 h1:qxIttos4xu6ii7MTVJYA8EFQR7Q3KG6xMqmLJIFtBaY=

+ 1 - 1
inbound/default.go

@@ -22,7 +22,7 @@ type myInboundAdapter struct {
 	protocol         string
 	network          []string
 	ctx              context.Context
-	router           adapter.Router
+	router           adapter.ConnectionRouter
 	logger           log.ContextLogger
 	tag              string
 	listenOptions    option.ListenOptions

+ 2 - 1
inbound/http.go

@@ -8,6 +8,7 @@ import (
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/common/tls"
+	"github.com/sagernet/sing-box/common/uot"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
@@ -35,7 +36,7 @@ func NewHTTP(ctx context.Context, router adapter.Router, logger log.ContextLogge
 			protocol:       C.TypeHTTP,
 			network:        []string{N.NetworkTCP},
 			ctx:            ctx,
-			router:         router,
+			router:         uot.NewRouter(router, logger),
 			logger:         logger,
 			tag:            tag,
 			listenOptions:  options.ListenOptions,

+ 2 - 1
inbound/mixed.go

@@ -7,6 +7,7 @@ import (
 	"os"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/uot"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
@@ -37,7 +38,7 @@ func NewMixed(ctx context.Context, router adapter.Router, logger log.ContextLogg
 			protocol:       C.TypeMixed,
 			network:        []string{N.NetworkTCP},
 			ctx:            ctx,
-			router:         router,
+			router:         uot.NewRouter(router, logger),
 			logger:         logger,
 			tag:            tag,
 			listenOptions:  options.ListenOptions,

+ 2 - 1
inbound/naive.go

@@ -13,6 +13,7 @@ import (
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/common/tls"
+	"github.com/sagernet/sing-box/common/uot"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/include"
 	"github.com/sagernet/sing-box/log"
@@ -43,7 +44,7 @@ func NewNaive(ctx context.Context, router adapter.Router, logger log.ContextLogg
 			protocol:      C.TypeNaive,
 			network:       options.Network.Build(),
 			ctx:           ctx,
-			router:        router,
+			router:        uot.NewRouter(router, logger),
 			logger:        logger,
 			tag:           tag,
 			listenOptions: options.ListenOptions,

+ 10 - 2
inbound/shadowsocks.go

@@ -6,6 +6,8 @@ import (
 	"os"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/mux"
+	"github.com/sagernet/sing-box/common/uot"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
@@ -48,21 +50,27 @@ func newShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
 			protocol:      C.TypeShadowsocks,
 			network:       options.Network.Build(),
 			ctx:           ctx,
-			router:        router,
+			router:        uot.NewRouter(router, logger),
 			logger:        logger,
 			tag:           tag,
 			listenOptions: options.ListenOptions,
 		},
 	}
+
 	inbound.connHandler = inbound
 	inbound.packetHandler = inbound
+	var err error
+	inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex))
+	if err != nil {
+		return nil, err
+	}
+
 	var udpTimeout int64
 	if options.UDPTimeout != 0 {
 		udpTimeout = options.UDPTimeout
 	} else {
 		udpTimeout = int64(C.UDPTimeout.Seconds())
 	}
-	var err error
 	switch {
 	case options.Method == shadowsocks.MethodNone:
 		inbound.service = shadowsocks.NewNoneService(options.UDPTimeout, inbound.upstreamContextHandler())

+ 9 - 5
inbound/shadowsocks_multi.go

@@ -6,6 +6,8 @@ import (
 	"os"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/mux"
+	"github.com/sagernet/sing-box/common/uot"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
@@ -38,7 +40,7 @@ func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log.
 			protocol:      C.TypeShadowsocks,
 			network:       options.Network.Build(),
 			ctx:           ctx,
-			router:        router,
+			router:        uot.NewRouter(router, logger),
 			logger:        logger,
 			tag:           tag,
 			listenOptions: options.ListenOptions,
@@ -46,16 +48,18 @@ func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log.
 	}
 	inbound.connHandler = inbound
 	inbound.packetHandler = inbound
+	var err error
+	inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex))
+	if err != nil {
+		return nil, err
+	}
 	var udpTimeout int64
 	if options.UDPTimeout != 0 {
 		udpTimeout = options.UDPTimeout
 	} else {
 		udpTimeout = int64(C.UDPTimeout.Seconds())
 	}
-	var (
-		service shadowsocks.MultiService[int]
-		err     error
-	)
+	var service shadowsocks.MultiService[int]
 	if common.Contains(shadowaead_2022.List, options.Method) {
 		service, err = shadowaead_2022.NewMultiServiceWithPassword[int](
 			options.Method,

+ 8 - 1
inbound/shadowsocks_relay.go

@@ -6,6 +6,8 @@ import (
 	"os"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/mux"
+	"github.com/sagernet/sing-box/common/uot"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
@@ -34,7 +36,7 @@ func newShadowsocksRelay(ctx context.Context, router adapter.Router, logger log.
 			protocol:      C.TypeShadowsocks,
 			network:       options.Network.Build(),
 			ctx:           ctx,
-			router:        router,
+			router:        uot.NewRouter(router, logger),
 			logger:        logger,
 			tag:           tag,
 			listenOptions: options.ListenOptions,
@@ -43,6 +45,11 @@ func newShadowsocksRelay(ctx context.Context, router adapter.Router, logger log.
 	}
 	inbound.connHandler = inbound
 	inbound.packetHandler = inbound
+	var err error
+	inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex))
+	if err != nil {
+		return nil, err
+	}
 	var udpTimeout int64
 	if options.UDPTimeout != 0 {
 		udpTimeout = options.UDPTimeout

+ 2 - 1
inbound/socks.go

@@ -6,6 +6,7 @@ import (
 	"os"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/uot"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
@@ -30,7 +31,7 @@ func NewSocks(ctx context.Context, router adapter.Router, logger log.ContextLogg
 			protocol:      C.TypeSOCKS,
 			network:       []string{N.NetworkTCP},
 			ctx:           ctx,
-			router:        router,
+			router:        uot.NewRouter(router, logger),
 			logger:        logger,
 			tag:           tag,
 			listenOptions: options.ListenOptions,

+ 5 - 0
inbound/trojan.go

@@ -6,6 +6,7 @@ import (
 	"os"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/mux"
 	"github.com/sagernet/sing-box/common/tls"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
@@ -94,6 +95,10 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog
 			return nil, E.Cause(err, "create server transport: ", options.Transport.Type)
 		}
 	}
+	inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex))
+	if err != nil {
+		return nil, err
+	}
 	inbound.service = service
 	inbound.connHandler = inbound
 	return inbound, nil

+ 2 - 1
inbound/tuic.go

@@ -9,6 +9,7 @@ import (
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/common/tls"
+	"github.com/sagernet/sing-box/common/uot"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
@@ -44,7 +45,7 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge
 			protocol:      C.TypeTUIC,
 			network:       []string{N.NetworkUDP},
 			ctx:           ctx,
-			router:        router,
+			router:        uot.NewRouter(router, logger),
 			logger:        logger,
 			tag:           tag,
 			listenOptions: options.ListenOptions,

+ 8 - 2
inbound/vless.go

@@ -6,7 +6,9 @@ import (
 	"os"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/mux"
 	"github.com/sagernet/sing-box/common/tls"
+	"github.com/sagernet/sing-box/common/uot"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
@@ -42,7 +44,7 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg
 			protocol:      C.TypeVLESS,
 			network:       []string{N.NetworkTCP},
 			ctx:           ctx,
-			router:        router,
+			router:        uot.NewRouter(router, logger),
 			logger:        logger,
 			tag:           tag,
 			listenOptions: options.ListenOptions,
@@ -50,6 +52,11 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg
 		ctx:   ctx,
 		users: options.Users,
 	}
+	var err error
+	inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex))
+	if err != nil {
+		return nil, err
+	}
 	service := vless.NewService[int](logger, adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound))
 	service.UpdateUsers(common.MapIndexed(inbound.users, func(index int, _ option.VLESSUser) int {
 		return index
@@ -59,7 +66,6 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg
 		return it.Flow
 	}))
 	inbound.service = service
-	var err error
 	if options.TLS != nil {
 		inbound.tlsConfig, err = tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
 		if err != nil {

+ 9 - 2
inbound/vmess.go

@@ -6,7 +6,9 @@ import (
 	"os"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/mux"
 	"github.com/sagernet/sing-box/common/tls"
+	"github.com/sagernet/sing-box/common/uot"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
@@ -42,7 +44,7 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg
 			protocol:      C.TypeVMess,
 			network:       []string{N.NetworkTCP},
 			ctx:           ctx,
-			router:        router,
+			router:        uot.NewRouter(router, logger),
 			logger:        logger,
 			tag:           tag,
 			listenOptions: options.ListenOptions,
@@ -50,6 +52,11 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg
 		ctx:   ctx,
 		users: options.Users,
 	}
+	var err error
+	inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex))
+	if err != nil {
+		return nil, err
+	}
 	var serviceOptions []vmess.ServiceOption
 	if timeFunc := ntp.TimeFuncFromContext(ctx); timeFunc != nil {
 		serviceOptions = append(serviceOptions, vmess.ServiceWithTimeFunc(timeFunc))
@@ -59,7 +66,7 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg
 	}
 	service := vmess.NewService[int](adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound), serviceOptions...)
 	inbound.service = service
-	err := service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.VMessUser) int {
+	err = service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.VMessUser) int {
 		return index
 	}), common.Map(options.Users, func(it option.VMessUser) string {
 		return it.UUID

+ 1 - 0
mkdocs.yml

@@ -75,6 +75,7 @@ nav:
           - Multiplex: configuration/shared/multiplex.md
           - V2Ray Transport: configuration/shared/v2ray-transport.md
           - UDP over TCP: configuration/shared/udp-over-tcp.md
+          - TCP Brutal: configuration/shared/tcp-brutal.md
       - Inbound:
           - configuration/inbound/index.md
           - Direct: configuration/inbound/direct.md

+ 23 - 0
option/multiplex.go

@@ -0,0 +1,23 @@
+package option
+
+type InboundMultiplexOptions struct {
+	Enabled bool           `json:"enabled,omitempty"`
+	Padding bool           `json:"padding,omitempty"`
+	Brutal  *BrutalOptions `json:"brutal,omitempty"`
+}
+
+type OutboundMultiplexOptions struct {
+	Enabled        bool           `json:"enabled,omitempty"`
+	Protocol       string         `json:"protocol,omitempty"`
+	MaxConnections int            `json:"max_connections,omitempty"`
+	MinStreams     int            `json:"min_streams,omitempty"`
+	MaxStreams     int            `json:"max_streams,omitempty"`
+	Padding        bool           `json:"padding,omitempty"`
+	Brutal         *BrutalOptions `json:"brutal,omitempty"`
+}
+
+type BrutalOptions struct {
+	Enabled  bool `json:"enabled,omitempty"`
+	UpMbps   int  `json:"up_mbps,omitempty"`
+	DownMbps int  `json:"down_mbps,omitempty"`
+}

+ 0 - 9
option/outbound.go

@@ -154,12 +154,3 @@ type ServerOptions struct {
 func (o ServerOptions) Build() M.Socksaddr {
 	return M.ParseSocksaddrHostPort(o.Server, o.ServerPort)
 }
-
-type MultiplexOptions struct {
-	Enabled        bool   `json:"enabled,omitempty"`
-	Protocol       string `json:"protocol,omitempty"`
-	MaxConnections int    `json:"max_connections,omitempty"`
-	MinStreams     int    `json:"min_streams,omitempty"`
-	MaxStreams     int    `json:"max_streams,omitempty"`
-	Padding        bool   `json:"padding,omitempty"`
-}

+ 8 - 7
option/shadowsocks.go

@@ -7,6 +7,7 @@ type ShadowsocksInboundOptions struct {
 	Password     string                   `json:"password,omitempty"`
 	Users        []ShadowsocksUser        `json:"users,omitempty"`
 	Destinations []ShadowsocksDestination `json:"destinations,omitempty"`
+	Multiplex    *InboundMultiplexOptions `json:"multiplex,omitempty"`
 }
 
 type ShadowsocksUser struct {
@@ -23,11 +24,11 @@ type ShadowsocksDestination struct {
 type ShadowsocksOutboundOptions struct {
 	DialerOptions
 	ServerOptions
-	Method            string             `json:"method"`
-	Password          string             `json:"password"`
-	Plugin            string             `json:"plugin,omitempty"`
-	PluginOptions     string             `json:"plugin_opts,omitempty"`
-	Network           NetworkList        `json:"network,omitempty"`
-	UDPOverTCPOptions *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"`
-	MultiplexOptions  *MultiplexOptions  `json:"multiplex,omitempty"`
+	Method        string                    `json:"method"`
+	Password      string                    `json:"password"`
+	Plugin        string                    `json:"plugin,omitempty"`
+	PluginOptions string                    `json:"plugin_opts,omitempty"`
+	Network       NetworkList               `json:"network,omitempty"`
+	UDPOverTCP    *UDPOverTCPOptions        `json:"udp_over_tcp,omitempty"`
+	Multiplex     *OutboundMultiplexOptions `json:"multiplex,omitempty"`
 }

+ 5 - 5
option/simple.go

@@ -17,11 +17,11 @@ type HTTPMixedInboundOptions struct {
 type SocksOutboundOptions struct {
 	DialerOptions
 	ServerOptions
-	Version           string             `json:"version,omitempty"`
-	Username          string             `json:"username,omitempty"`
-	Password          string             `json:"password,omitempty"`
-	Network           NetworkList        `json:"network,omitempty"`
-	UDPOverTCPOptions *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"`
+	Version    string             `json:"version,omitempty"`
+	Username   string             `json:"username,omitempty"`
+	Password   string             `json:"password,omitempty"`
+	Network    NetworkList        `json:"network,omitempty"`
+	UDPOverTCP *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"`
 }
 
 type HTTPOutboundOptions struct {

+ 6 - 5
option/trojan.go

@@ -6,6 +6,7 @@ type TrojanInboundOptions struct {
 	TLS             *InboundTLSOptions        `json:"tls,omitempty"`
 	Fallback        *ServerOptions            `json:"fallback,omitempty"`
 	FallbackForALPN map[string]*ServerOptions `json:"fallback_for_alpn,omitempty"`
+	Multiplex       *InboundMultiplexOptions  `json:"multiplex,omitempty"`
 	Transport       *V2RayTransportOptions    `json:"transport,omitempty"`
 }
 
@@ -17,9 +18,9 @@ type TrojanUser struct {
 type TrojanOutboundOptions struct {
 	DialerOptions
 	ServerOptions
-	Password  string                 `json:"password"`
-	Network   NetworkList            `json:"network,omitempty"`
-	TLS       *OutboundTLSOptions    `json:"tls,omitempty"`
-	Multiplex *MultiplexOptions      `json:"multiplex,omitempty"`
-	Transport *V2RayTransportOptions `json:"transport,omitempty"`
+	Password  string                    `json:"password"`
+	Network   NetworkList               `json:"network,omitempty"`
+	TLS       *OutboundTLSOptions       `json:"tls,omitempty"`
+	Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
+	Transport *V2RayTransportOptions    `json:"transport,omitempty"`
 }

+ 11 - 10
option/vless.go

@@ -2,9 +2,10 @@ package option
 
 type VLESSInboundOptions struct {
 	ListenOptions
-	Users     []VLESSUser            `json:"users,omitempty"`
-	TLS       *InboundTLSOptions     `json:"tls,omitempty"`
-	Transport *V2RayTransportOptions `json:"transport,omitempty"`
+	Users     []VLESSUser              `json:"users,omitempty"`
+	TLS       *InboundTLSOptions       `json:"tls,omitempty"`
+	Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"`
+	Transport *V2RayTransportOptions   `json:"transport,omitempty"`
 }
 
 type VLESSUser struct {
@@ -16,11 +17,11 @@ type VLESSUser struct {
 type VLESSOutboundOptions struct {
 	DialerOptions
 	ServerOptions
-	UUID           string                 `json:"uuid"`
-	Flow           string                 `json:"flow,omitempty"`
-	Network        NetworkList            `json:"network,omitempty"`
-	TLS            *OutboundTLSOptions    `json:"tls,omitempty"`
-	Multiplex      *MultiplexOptions      `json:"multiplex,omitempty"`
-	Transport      *V2RayTransportOptions `json:"transport,omitempty"`
-	PacketEncoding *string                `json:"packet_encoding,omitempty"`
+	UUID           string                    `json:"uuid"`
+	Flow           string                    `json:"flow,omitempty"`
+	Network        NetworkList               `json:"network,omitempty"`
+	TLS            *OutboundTLSOptions       `json:"tls,omitempty"`
+	Multiplex      *OutboundMultiplexOptions `json:"multiplex,omitempty"`
+	Transport      *V2RayTransportOptions    `json:"transport,omitempty"`
+	PacketEncoding *string                   `json:"packet_encoding,omitempty"`
 }

+ 14 - 13
option/vmess.go

@@ -2,9 +2,10 @@ package option
 
 type VMessInboundOptions struct {
 	ListenOptions
-	Users     []VMessUser            `json:"users,omitempty"`
-	TLS       *InboundTLSOptions     `json:"tls,omitempty"`
-	Transport *V2RayTransportOptions `json:"transport,omitempty"`
+	Users     []VMessUser              `json:"users,omitempty"`
+	TLS       *InboundTLSOptions       `json:"tls,omitempty"`
+	Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"`
+	Transport *V2RayTransportOptions   `json:"transport,omitempty"`
 }
 
 type VMessUser struct {
@@ -16,14 +17,14 @@ type VMessUser struct {
 type VMessOutboundOptions struct {
 	DialerOptions
 	ServerOptions
-	UUID                string                 `json:"uuid"`
-	Security            string                 `json:"security"`
-	AlterId             int                    `json:"alter_id,omitempty"`
-	GlobalPadding       bool                   `json:"global_padding,omitempty"`
-	AuthenticatedLength bool                   `json:"authenticated_length,omitempty"`
-	Network             NetworkList            `json:"network,omitempty"`
-	TLS                 *OutboundTLSOptions    `json:"tls,omitempty"`
-	PacketEncoding      string                 `json:"packet_encoding,omitempty"`
-	Multiplex           *MultiplexOptions      `json:"multiplex,omitempty"`
-	Transport           *V2RayTransportOptions `json:"transport,omitempty"`
+	UUID                string                    `json:"uuid"`
+	Security            string                    `json:"security"`
+	AlterId             int                       `json:"alter_id,omitempty"`
+	GlobalPadding       bool                      `json:"global_padding,omitempty"`
+	AuthenticatedLength bool                      `json:"authenticated_length,omitempty"`
+	Network             NetworkList               `json:"network,omitempty"`
+	TLS                 *OutboundTLSOptions       `json:"tls,omitempty"`
+	PacketEncoding      string                    `json:"packet_encoding,omitempty"`
+	Multiplex           *OutboundMultiplexOptions `json:"multiplex,omitempty"`
+	Transport           *V2RayTransportOptions    `json:"transport,omitempty"`
 }

+ 2 - 2
outbound/shadowsocks.go

@@ -62,9 +62,9 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
 			return nil, err
 		}
 	}
-	uotOptions := common.PtrValueOrDefault(options.UDPOverTCPOptions)
+	uotOptions := common.PtrValueOrDefault(options.UDPOverTCP)
 	if !uotOptions.Enabled {
-		outbound.multiplexDialer, err = mux.NewClientWithOptions((*shadowsocksDialer)(outbound), common.PtrValueOrDefault(options.MultiplexOptions))
+		outbound.multiplexDialer, err = mux.NewClientWithOptions((*shadowsocksDialer)(outbound), logger, common.PtrValueOrDefault(options.Multiplex))
 		if err != nil {
 			return nil, err
 		}

+ 1 - 1
outbound/socks.go

@@ -54,7 +54,7 @@ func NewSocks(router adapter.Router, logger log.ContextLogger, tag string, optio
 		client:  socks.NewClient(outboundDialer, options.ServerOptions.Build(), version, options.Username, options.Password),
 		resolve: version == socks.Version4,
 	}
-	uotOptions := common.PtrValueOrDefault(options.UDPOverTCPOptions)
+	uotOptions := common.PtrValueOrDefault(options.UDPOverTCP)
 	if uotOptions.Enabled {
 		outbound.uotClient = &uot.Client{
 			Dialer:  outbound.client,

+ 1 - 1
outbound/trojan.go

@@ -62,7 +62,7 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog
 			return nil, E.Cause(err, "create client transport: ", options.Transport.Type)
 		}
 	}
-	outbound.multiplexDialer, err = mux.NewClientWithOptions((*trojanDialer)(outbound), common.PtrValueOrDefault(options.Multiplex))
+	outbound.multiplexDialer, err = mux.NewClientWithOptions((*trojanDialer)(outbound), logger, common.PtrValueOrDefault(options.Multiplex))
 	if err != nil {
 		return nil, err
 	}

+ 1 - 1
outbound/vless.go

@@ -81,7 +81,7 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg
 	if err != nil {
 		return nil, err
 	}
-	outbound.multiplexDialer, err = mux.NewClientWithOptions((*vlessDialer)(outbound), common.PtrValueOrDefault(options.Multiplex))
+	outbound.multiplexDialer, err = mux.NewClientWithOptions((*vlessDialer)(outbound), logger, common.PtrValueOrDefault(options.Multiplex))
 	if err != nil {
 		return nil, err
 	}

+ 1 - 1
outbound/vmess.go

@@ -64,7 +64,7 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg
 			return nil, E.Cause(err, "create client transport: ", options.Transport.Type)
 		}
 	}
-	outbound.multiplexDialer, err = mux.NewClientWithOptions((*vmessDialer)(outbound), common.PtrValueOrDefault(options.Multiplex))
+	outbound.multiplexDialer, err = mux.NewClientWithOptions((*vmessDialer)(outbound), logger, common.PtrValueOrDefault(options.Multiplex))
 	if err != nil {
 		return nil, err
 	}

+ 5 - 22
route/router.go

@@ -16,7 +16,6 @@ import (
 	"github.com/sagernet/sing-box/common/dialer"
 	"github.com/sagernet/sing-box/common/geoip"
 	"github.com/sagernet/sing-box/common/geosite"
-	"github.com/sagernet/sing-box/common/mux"
 	"github.com/sagernet/sing-box/common/process"
 	"github.com/sagernet/sing-box/common/sniff"
 	C "github.com/sagernet/sing-box/constant"
@@ -27,6 +26,7 @@ import (
 	"github.com/sagernet/sing-box/outbound"
 	"github.com/sagernet/sing-box/transport/fakeip"
 	"github.com/sagernet/sing-dns"
+	mux "github.com/sagernet/sing-mux"
 	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing-vmess"
 	"github.com/sagernet/sing/common"
@@ -606,30 +606,13 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
 	metadata.Network = N.NetworkTCP
 	switch metadata.Destination.Fqdn {
 	case mux.Destination.Fqdn:
-		r.logger.InfoContext(ctx, "inbound multiplex connection")
-		handler := adapter.NewUpstreamHandler(metadata, r.RouteConnection, r.RoutePacketConnection, r)
-		return mux.HandleConnection(ctx, handler, r.logger, conn, adapter.UpstreamMetadata(metadata))
+		return E.New("global multiplex is deprecated since sing-box v1.7.0, enable multiplex in inbound options instead.")
 	case vmess.MuxDestination.Fqdn:
-		r.logger.InfoContext(ctx, "inbound legacy multiplex connection")
-		return vmess.HandleMuxConnection(ctx, conn, adapter.NewUpstreamHandler(metadata, r.RouteConnection, r.RoutePacketConnection, r))
+		return E.New("global multiplex (v2ray legacy) not supported since sing-box v1.7.0.")
 	case uot.MagicAddress:
-		request, err := uot.ReadRequest(conn)
-		if err != nil {
-			return E.Cause(err, "read UoT request")
-		}
-		if request.IsConnect {
-			r.logger.InfoContext(ctx, "inbound UoT connect connection to ", request.Destination)
-		} else {
-			r.logger.InfoContext(ctx, "inbound UoT connection to ", request.Destination)
-		}
-		metadata.Domain = metadata.Destination.Fqdn
-		metadata.Destination = request.Destination
-		return r.RoutePacketConnection(ctx, uot.NewConn(conn, *request), metadata)
+		return E.New("global UoT not supported since sing-box v1.7.0.")
 	case uot.LegacyMagicAddress:
-		r.logger.InfoContext(ctx, "inbound legacy UoT connection")
-		metadata.Domain = metadata.Destination.Fqdn
-		metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()}
-		return r.RoutePacketConnection(ctx, uot.NewConn(conn, uot.Request{}), metadata)
+		return E.New("global UoT (legacy) not supported since sing-box v1.7.0.")
 	}
 
 	if r.fakeIPStore != nil && r.fakeIPStore.Contains(metadata.Destination.Addr) {

+ 346 - 0
test/brutal_test.go

@@ -0,0 +1,346 @@
+package main
+
+import (
+	"net/netip"
+	"testing"
+
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
+
+	"github.com/gofrs/uuid/v5"
+)
+
+func TestBrutalShadowsocks(t *testing.T) {
+	method := shadowaead_2022.List[0]
+	password := mkBase64(t, 16)
+	startInstance(t, option.Options{
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeMixed,
+				Tag:  "mixed-in",
+				MixedOptions: option.HTTPMixedInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.NewListenAddress(netip.IPv4Unspecified()),
+						ListenPort: clientPort,
+					},
+				},
+			},
+			{
+				Type: C.TypeShadowsocks,
+				ShadowsocksOptions: option.ShadowsocksInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.NewListenAddress(netip.IPv4Unspecified()),
+						ListenPort: serverPort,
+					},
+					Method:   method,
+					Password: password,
+					Multiplex: &option.InboundMultiplexOptions{
+						Enabled: true,
+						Brutal: &option.BrutalOptions{
+							Enabled:  true,
+							UpMbps:   100,
+							DownMbps: 100,
+						},
+					},
+				},
+			},
+		},
+		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeDirect,
+			},
+			{
+				Type: C.TypeShadowsocks,
+				Tag:  "ss-out",
+				ShadowsocksOptions: option.ShadowsocksOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: serverPort,
+					},
+					Method:   method,
+					Password: password,
+					Multiplex: &option.OutboundMultiplexOptions{
+						Enabled:  true,
+						Protocol: "smux",
+						Padding:  true,
+						Brutal: &option.BrutalOptions{
+							Enabled:  true,
+							UpMbps:   100,
+							DownMbps: 100,
+						},
+					},
+				},
+			},
+		},
+		Route: &option.RouteOptions{
+			Rules: []option.Rule{
+				{
+					DefaultOptions: option.DefaultRule{
+						Inbound:  []string{"mixed-in"},
+						Outbound: "ss-out",
+					},
+				},
+			},
+		},
+	})
+	testSuit(t, clientPort, testPort)
+}
+
+func TestBrutalTrojan(t *testing.T) {
+	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
+	password := mkBase64(t, 16)
+	startInstance(t, option.Options{
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeMixed,
+				Tag:  "mixed-in",
+				MixedOptions: option.HTTPMixedInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.NewListenAddress(netip.IPv4Unspecified()),
+						ListenPort: clientPort,
+					},
+				},
+			},
+			{
+				Type: C.TypeTrojan,
+				TrojanOptions: option.TrojanInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.NewListenAddress(netip.IPv4Unspecified()),
+						ListenPort: serverPort,
+					},
+					Users: []option.TrojanUser{{Password: password}},
+					Multiplex: &option.InboundMultiplexOptions{
+						Enabled: true,
+						Brutal: &option.BrutalOptions{
+							Enabled:  true,
+							UpMbps:   100,
+							DownMbps: 100,
+						},
+					},
+					TLS: &option.InboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+						KeyPath:         keyPem,
+					},
+				},
+			},
+		},
+		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeDirect,
+			},
+			{
+				Type: C.TypeTrojan,
+				Tag:  "ss-out",
+				TrojanOptions: option.TrojanOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: serverPort,
+					},
+					Password: password,
+					Multiplex: &option.OutboundMultiplexOptions{
+						Enabled:  true,
+						Protocol: "yamux",
+						Padding:  true,
+						Brutal: &option.BrutalOptions{
+							Enabled:  true,
+							UpMbps:   100,
+							DownMbps: 100,
+						},
+					},
+					TLS: &option.OutboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+					},
+				},
+			},
+		},
+		Route: &option.RouteOptions{
+			Rules: []option.Rule{
+				{
+					DefaultOptions: option.DefaultRule{
+						Inbound:  []string{"mixed-in"},
+						Outbound: "ss-out",
+					},
+				},
+			},
+		},
+	})
+	testSuit(t, clientPort, testPort)
+}
+
+func TestBrutalVMess(t *testing.T) {
+	user, _ := uuid.NewV4()
+	startInstance(t, option.Options{
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeMixed,
+				Tag:  "mixed-in",
+				MixedOptions: option.HTTPMixedInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.NewListenAddress(netip.IPv4Unspecified()),
+						ListenPort: clientPort,
+					},
+				},
+			},
+			{
+				Type: C.TypeVMess,
+				VMessOptions: option.VMessInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.NewListenAddress(netip.IPv4Unspecified()),
+						ListenPort: serverPort,
+					},
+					Users: []option.VMessUser{{UUID: user.String()}},
+					Multiplex: &option.InboundMultiplexOptions{
+						Enabled: true,
+						Brutal: &option.BrutalOptions{
+							Enabled:  true,
+							UpMbps:   100,
+							DownMbps: 100,
+						},
+					},
+				},
+			},
+		},
+		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeDirect,
+			},
+			{
+				Type: C.TypeVMess,
+				Tag:  "ss-out",
+				VMessOptions: option.VMessOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: serverPort,
+					},
+					UUID: user.String(),
+					Multiplex: &option.OutboundMultiplexOptions{
+						Enabled:  true,
+						Protocol: "h2mux",
+						Padding:  true,
+						Brutal: &option.BrutalOptions{
+							Enabled:  true,
+							UpMbps:   100,
+							DownMbps: 100,
+						},
+					},
+				},
+			},
+		},
+		Route: &option.RouteOptions{
+			Rules: []option.Rule{
+				{
+					DefaultOptions: option.DefaultRule{
+						Inbound:  []string{"mixed-in"},
+						Outbound: "ss-out",
+					},
+				},
+			},
+		},
+	})
+	testSuit(t, clientPort, testPort)
+}
+
+func TestBrutalVLESS(t *testing.T) {
+	user, _ := uuid.NewV4()
+	startInstance(t, option.Options{
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeMixed,
+				Tag:  "mixed-in",
+				MixedOptions: option.HTTPMixedInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.NewListenAddress(netip.IPv4Unspecified()),
+						ListenPort: clientPort,
+					},
+				},
+			},
+			{
+				Type: C.TypeVLESS,
+				VLESSOptions: option.VLESSInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.NewListenAddress(netip.IPv4Unspecified()),
+						ListenPort: serverPort,
+					},
+					Users: []option.VLESSUser{{UUID: user.String()}},
+					Multiplex: &option.InboundMultiplexOptions{
+						Enabled: true,
+						Brutal: &option.BrutalOptions{
+							Enabled:  true,
+							UpMbps:   100,
+							DownMbps: 100,
+						},
+					},
+					TLS: &option.InboundTLSOptions{
+						Enabled:    true,
+						ServerName: "google.com",
+						Reality: &option.InboundRealityOptions{
+							Enabled: true,
+							Handshake: option.InboundRealityHandshakeOptions{
+								ServerOptions: option.ServerOptions{
+									Server:     "google.com",
+									ServerPort: 443,
+								},
+							},
+							ShortID:    []string{"0123456789abcdef"},
+							PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc",
+						},
+					},
+				},
+			},
+		},
+		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeDirect,
+			},
+			{
+				Type: C.TypeVLESS,
+				Tag:  "ss-out",
+				VLESSOptions: option.VLESSOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: serverPort,
+					},
+					UUID: user.String(),
+					TLS: &option.OutboundTLSOptions{
+						Enabled:    true,
+						ServerName: "google.com",
+						Reality: &option.OutboundRealityOptions{
+							Enabled:   true,
+							ShortID:   "0123456789abcdef",
+							PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0",
+						},
+						UTLS: &option.OutboundUTLSOptions{
+							Enabled: true,
+						},
+					},
+					Multiplex: &option.OutboundMultiplexOptions{
+						Enabled:  true,
+						Protocol: "h2mux",
+						Padding:  true,
+						Brutal: &option.BrutalOptions{
+							Enabled:  true,
+							UpMbps:   100,
+							DownMbps: 100,
+						},
+					},
+				},
+			},
+		},
+		Route: &option.RouteOptions{
+			Rules: []option.Rule{
+				{
+					DefaultOptions: option.DefaultRule{
+						Inbound:  []string{"mixed-in"},
+						Outbound: "ss-out",
+					},
+				},
+			},
+		},
+	})
+	testSuit(t, clientPort, testPort)
+}

+ 9 - 7
test/go.mod

@@ -11,11 +11,11 @@ require (
 	github.com/docker/go-connections v0.4.0
 	github.com/gofrs/uuid/v5 v5.0.0
 	github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460
-	github.com/sagernet/sing v0.2.17
+	github.com/sagernet/sing v0.2.18-0.20231119032432-6a556bfa50cc
 	github.com/sagernet/sing-dns v0.1.11
 	github.com/sagernet/sing-quic v0.1.4
 	github.com/sagernet/sing-shadowsocks v0.2.5
-	github.com/sagernet/sing-shadowsocks2 v0.1.4
+	github.com/sagernet/sing-shadowsocks2 v0.1.5
 	github.com/spyzhov/ajson v0.9.0
 	github.com/stretchr/testify v1.8.4
 	go.uber.org/goleak v1.3.0
@@ -40,6 +40,8 @@ require (
 	github.com/go-chi/render v1.0.3 // indirect
 	github.com/go-ole/go-ole v1.3.0 // indirect
 	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
+	github.com/gobwas/httphead v0.1.0 // indirect
+	github.com/gobwas/pool v0.2.1 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/google/btree v1.1.2 // indirect
@@ -70,18 +72,18 @@ require (
 	github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect
 	github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a // indirect
 	github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect
-	github.com/sagernet/gvisor v0.0.0-20230930141345-5fef6f2e17ab // indirect
+	github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 // indirect
 	github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect
 	github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 // indirect
-	github.com/sagernet/sing-mux v0.1.4 // indirect
+	github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 // indirect
 	github.com/sagernet/sing-shadowtls v0.1.4 // indirect
-	github.com/sagernet/sing-tun v0.1.20 // indirect
+	github.com/sagernet/sing-tun v0.1.21-0.20231119035513-f6ea97c5af71 // indirect
 	github.com/sagernet/sing-vmess v0.1.8 // indirect
 	github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 // indirect
 	github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 // indirect
 	github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 // indirect
-	github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e // indirect
 	github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f // indirect
+	github.com/sagernet/ws v0.0.0-20231030053741-7d481eb31bed // indirect
 	github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect
 	github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
 	github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
@@ -94,7 +96,7 @@ require (
 	golang.org/x/mod v0.14.0 // indirect
 	golang.org/x/sys v0.14.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
-	golang.org/x/time v0.3.0 // indirect
+	golang.org/x/time v0.4.0 // indirect
 	golang.org/x/tools v0.15.0 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
 	google.golang.org/grpc v1.59.0 // indirect

+ 19 - 14
test/go.sum

@@ -43,6 +43,10 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
 github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
+github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
+github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
+github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
+github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
 github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
 github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -117,8 +121,8 @@ github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a h1:wZHruBx
 github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a/go.mod h1:dNV1ZP9y3qx5ltULeKaQZTZWTLHflgW5DES+Ses7cMI=
 github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA=
 github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms=
-github.com/sagernet/gvisor v0.0.0-20230930141345-5fef6f2e17ab h1:u+xQoi/Yc6bNUvTfrDD6HhGRybn2lzrhf5vmS+wb4Ho=
-github.com/sagernet/gvisor v0.0.0-20230930141345-5fef6f2e17ab/go.mod h1:3akUhSHSVtLuJaYcW5JPepUraBOW06Ibz2HKwaK5rOk=
+github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 h1:dSPgjIw0CT6ISLeEh8Q20dZMBMFCcEceo23+LncRcNQ=
+github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930/go.mod h1:JpKHkOYgh4wLwrX2BhH3ZIvCvazCkTnPeEcmigZJfHY=
 github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
 github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
 github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460 h1:dAe4OIJAtE0nHOzTHhAReQteh3+sa63rvXbuIpbeOTY=
@@ -127,22 +131,22 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byL
 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
 github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
 github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
-github.com/sagernet/sing v0.2.17 h1:vMPKb3MV0Aa5ws4dCJkRI8XEjrsUcDn810czd0FwmzI=
-github.com/sagernet/sing v0.2.17/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
+github.com/sagernet/sing v0.2.18-0.20231119032432-6a556bfa50cc h1:dmU0chO0QrBpARo8sqyOc+mvPLW+qux4ca16kb2WIc8=
+github.com/sagernet/sing v0.2.18-0.20231119032432-6a556bfa50cc/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
 github.com/sagernet/sing-dns v0.1.11 h1:PPrMCVVrAeR3f5X23I+cmvacXJ+kzuyAsBiWyUKhGSE=
 github.com/sagernet/sing-dns v0.1.11/go.mod h1:zJ/YjnYB61SYE+ubMcMqVdpaSvsyQ2iShQGO3vuLvvE=
-github.com/sagernet/sing-mux v0.1.4 h1:BPNPOQr6HkXG3iY/BrfvUKUl+A7gYsGKVSxvoR3PO50=
-github.com/sagernet/sing-mux v0.1.4/go.mod h1:dKvcu/sb3fZ88uGv9vzAqUej6J4W+pHu5GqjRuFwAWs=
+github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 h1:ncKb5tVOsCQgCsv6UpsA0jinbNb5OQ5GMPJlyQP3EHM=
+github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07/go.mod h1:u/MZf32xPG8jEKe3t+xUV67EBnKtDtCaPhsJQOQGUYU=
 github.com/sagernet/sing-quic v0.1.4 h1:F5KRGXMXKQEmP8VrzVollf9HWcRqggcuG9nRCL+5IJ8=
 github.com/sagernet/sing-quic v0.1.4/go.mod h1:aXHVP+osF3w5wJzoWZbJSrX3ceJiU9QMd0KPnKV6C/o=
 github.com/sagernet/sing-shadowsocks v0.2.5 h1:qxIttos4xu6ii7MTVJYA8EFQR7Q3KG6xMqmLJIFtBaY=
 github.com/sagernet/sing-shadowsocks v0.2.5/go.mod h1:MGWGkcU2xW2G2mfArT9/QqpVLOGU+dBaahZCtPHdt7A=
-github.com/sagernet/sing-shadowsocks2 v0.1.4 h1:vht2M8t3m5DTgXR2j24KbYOygG5aOp+MUhpQnAux728=
-github.com/sagernet/sing-shadowsocks2 v0.1.4/go.mod h1:Mgdee99NxxNd5Zld3ixIs18yVs4x2dI2VTDDE1N14Wc=
+github.com/sagernet/sing-shadowsocks2 v0.1.5 h1:JDeAJ4ZWlYZ7F6qEVdDKPhQEangxKw/JtmU+i/YfCYE=
+github.com/sagernet/sing-shadowsocks2 v0.1.5/go.mod h1:KF65y8lI5PGHyMgRZGYXYsH9ilgRc/yr+NYbSNGuBm4=
 github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
 github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4=
-github.com/sagernet/sing-tun v0.1.20 h1:vYWo/w6fkKc8I1WP/IB8eBWZVsGIC6eoEoNR6XqEDlY=
-github.com/sagernet/sing-tun v0.1.20/go.mod h1:6kkPL/u9tWcLFfu55VbwMDnO++17cUihSmImkZjdZro=
+github.com/sagernet/sing-tun v0.1.21-0.20231119035513-f6ea97c5af71 h1:WQi0TwhjbSNFFbxybIgAUSjVvo7uWSsLD28ldoM2avY=
+github.com/sagernet/sing-tun v0.1.21-0.20231119035513-f6ea97c5af71/go.mod h1:hyzA4gDWbeg2SXklqPDswBKa//QcjlZqKw9aPcNdQ9A=
 github.com/sagernet/sing-vmess v0.1.8 h1:XVWad1RpTy9b5tPxdm5MCU8cGfrTGdR8qCq6HV2aCNc=
 github.com/sagernet/sing-vmess v0.1.8/go.mod h1:vhx32UNzTDUkNwOyIjcZQohre1CaytquC5mPplId8uA=
 github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 h1:HuE6xSwco/Xed8ajZ+coeYLmioq0Qp1/Z2zczFaV8as=
@@ -151,10 +155,10 @@ github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 h1:Px+hN4Vzgx+iCGV
 github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6/go.mod h1:zovq6vTvEM6ECiqE3Eeb9rpIylPpamPcmrJ9tv0Bt0M=
 github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 h1:kDUqhc9Vsk5HJuhfIATJ8oQwBmpOZJuozQG7Vk88lL4=
 github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM=
-github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e h1:7uw2njHFGE+VpWamge6o56j2RWk4omF6uLKKxMmcWvs=
-github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e/go.mod h1:45TUl8+gH4SIKr4ykREbxKWTxkDlSzFENzctB1dVRRY=
 github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f h1:Kvo8w8Y9lzFGB/7z09MJ3TR99TFtfI/IuY87Ygcycho=
 github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f/go.mod h1:mySs0abhpc/gLlvhoq7HP1RzOaRmIXVeZGCh++zoApk=
+github.com/sagernet/ws v0.0.0-20231030053741-7d481eb31bed h1:90a510OeE9siSJoYsI8nSjPmA+u5ROMDts/ZkdNsuXY=
+github.com/sagernet/ws v0.0.0-20231030053741-7d481eb31bed/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
 github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg=
 github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s=
 github.com/spyzhov/ajson v0.9.0 h1:tF46gJGOenYVj+k9K1U1XpCxVWhmiyY5PsVCAs1+OJ0=
@@ -222,6 +226,7 @@ golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
 golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -231,8 +236,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
-golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY=
+golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=

+ 12 - 9
test/mux_test.go

@@ -18,7 +18,7 @@ var muxProtocols = []string{
 }
 
 func TestVMessSMux(t *testing.T) {
-	testVMessMux(t, option.MultiplexOptions{
+	testVMessMux(t, option.OutboundMultiplexOptions{
 		Enabled:  true,
 		Protocol: "smux",
 	})
@@ -27,7 +27,7 @@ func TestVMessSMux(t *testing.T) {
 func TestShadowsocksMux(t *testing.T) {
 	for _, protocol := range muxProtocols {
 		t.Run(protocol, func(t *testing.T) {
-			testShadowsocksMux(t, option.MultiplexOptions{
+			testShadowsocksMux(t, option.OutboundMultiplexOptions{
 				Enabled:  true,
 				Protocol: protocol,
 			})
@@ -36,7 +36,7 @@ func TestShadowsocksMux(t *testing.T) {
 }
 
 func TestShadowsockH2Mux(t *testing.T) {
-	testShadowsocksMux(t, option.MultiplexOptions{
+	testShadowsocksMux(t, option.OutboundMultiplexOptions{
 		Enabled:  true,
 		Protocol: "h2mux",
 		Padding:  true,
@@ -44,14 +44,14 @@ func TestShadowsockH2Mux(t *testing.T) {
 }
 
 func TestShadowsockSMuxPadding(t *testing.T) {
-	testShadowsocksMux(t, option.MultiplexOptions{
+	testShadowsocksMux(t, option.OutboundMultiplexOptions{
 		Enabled:  true,
 		Protocol: "smux",
 		Padding:  true,
 	})
 }
 
-func testShadowsocksMux(t *testing.T, options option.MultiplexOptions) {
+func testShadowsocksMux(t *testing.T, options option.OutboundMultiplexOptions) {
 	method := shadowaead_2022.List[0]
 	password := mkBase64(t, 16)
 	startInstance(t, option.Options{
@@ -90,9 +90,9 @@ func testShadowsocksMux(t *testing.T, options option.MultiplexOptions) {
 						Server:     "127.0.0.1",
 						ServerPort: serverPort,
 					},
-					Method:           method,
-					Password:         password,
-					MultiplexOptions: &options,
+					Method:    method,
+					Password:  password,
+					Multiplex: &options,
 				},
 			},
 		},
@@ -110,7 +110,7 @@ func testShadowsocksMux(t *testing.T, options option.MultiplexOptions) {
 	testSuit(t, clientPort, testPort)
 }
 
-func testVMessMux(t *testing.T, options option.MultiplexOptions) {
+func testVMessMux(t *testing.T, options option.OutboundMultiplexOptions) {
 	user, _ := uuid.NewV4()
 	startInstance(t, option.Options{
 		Inbounds: []option.Inbound{
@@ -136,6 +136,9 @@ func testVMessMux(t *testing.T, options option.MultiplexOptions) {
 							UUID: user.String(),
 						},
 					},
+					Multiplex: &option.InboundMultiplexOptions{
+						Enabled: true,
+					},
 				},
 			},
 		},

+ 1 - 1
test/shadowsocks_test.go

@@ -249,7 +249,7 @@ func TestShadowsocksUoT(t *testing.T) {
 					},
 					Method:   method,
 					Password: password,
-					UDPOverTCPOptions: &option.UDPOverTCPOptions{
+					UDPOverTCP: &option.UDPOverTCPOptions{
 						Enabled: true,
 					},
 				},