Update On Tue Apr 14 21:27:45 CEST 2026

This commit is contained in:
github-action[bot]
2026-04-14 21:27:46 +02:00
parent 28b138e57c
commit 878c0ddfbf
294 changed files with 10548 additions and 15273 deletions
+1
View File
@@ -1329,3 +1329,4 @@ Update On Fri Apr 10 21:07:29 CEST 2026
Update On Sat Apr 11 20:57:57 CEST 2026
Update On Sun Apr 12 21:03:12 CEST 2026
Update On Mon Apr 13 21:25:44 CEST 2026
Update On Tue Apr 14 21:27:36 CEST 2026
+2 -2
View File
@@ -381,7 +381,7 @@ jobs:
EOF
- name: Upload Prerelease
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
if: ${{ success() }}
with:
tag_name: Prerelease-${{ github.ref_name }}
@@ -443,7 +443,7 @@ jobs:
working-directory: bin
- name: Upload Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
if: ${{ success() }}
with:
tag_name: ${{ github.event.inputs.version }}
+6 -7
View File
@@ -13,7 +13,7 @@ import (
"github.com/metacubex/mihomo/component/ca"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
tuicCommon "github.com/metacubex/mihomo/transport/tuic/common"
"github.com/metacubex/mihomo/transport/tuic/common"
"github.com/metacubex/quic-go"
qtls "github.com/metacubex/sing-quic"
@@ -22,10 +22,6 @@ import (
"github.com/metacubex/tls"
)
func init() {
hysteria2.SetCongestionController = tuicCommon.SetCongestionController
}
const minHopInterval = 5
const defaultHopInterval = 30
@@ -56,6 +52,7 @@ type Hysteria2Option struct {
PrivateKey string `proxy:"private-key,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
BBRProfile string `proxy:"bbr-profile,omitempty"`
UdpMTU int `proxy:"udp-mtu,omitempty"`
// quic-go special config
@@ -184,7 +181,6 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
TLSConfig: tlsClientConfig,
QUICConfig: quicConfig,
UDPDisabled: false,
CWND: option.CWND,
UdpMTU: option.UdpMTU,
ServerAddress: M.ParseSocksaddr(addr),
PacketListener: outbound.dialer,
@@ -193,8 +189,11 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
if err != nil {
return nil, nil, err
}
return tuicCommon.DialQuic(ctx, addr, outbound.DialOptions(), dialer, tlsCfg, cfg, early)
return common.DialQuic(ctx, addr, outbound.DialOptions(), dialer, tlsCfg, cfg, early)
}),
SetBBRCongestion: func(quicConn *quic.Conn) {
common.SetCongestionController(quicConn, "bbr", option.CWND, option.BBRProfile)
},
}
var serverPorts []uint16
+2 -1
View File
@@ -69,6 +69,7 @@ type MasqueOption struct {
CongestionController string `proxy:"congestion-controller,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
BBRProfile string `proxy:"bbr-profile,omitempty"`
RemoteDnsResolve bool `proxy:"remote-dns-resolve,omitempty"`
Dns []string `proxy:"dns,omitempty"`
@@ -262,7 +263,7 @@ func (w *Masque) run(ctx context.Context) error {
if err != nil {
return err
}
common.SetCongestionController(quicConn, w.option.CongestionController, w.option.CWND)
common.SetCongestionController(quicConn, w.option.CongestionController, w.option.CWND, w.option.BBRProfile)
closer, ipConn, err = masque.ConnectTunnel(ctx, quicConn, w.uri)
if err != nil {
@@ -38,6 +38,7 @@ type TrustTunnelOption struct {
Quic bool `proxy:"quic,omitempty"`
CongestionController string `proxy:"congestion-controller,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
BBRProfile string `proxy:"bbr-profile,omitempty"`
// reuse options
MaxConnections int `proxy:"max-connections,omitempty"`
MinStreams int `proxy:"min-streams,omitempty"`
@@ -110,6 +111,7 @@ func NewTrustTunnel(option TrustTunnelOption) (*TrustTunnel, error) {
QUIC: option.Quic,
QUICCongestionControl: option.CongestionController,
QUICCwnd: option.CWND,
QUICBBRProfile: option.BBRProfile,
HealthCheck: option.HealthCheck,
MaxConnections: option.MaxConnections,
MinStreams: option.MinStreams,
+2 -1
View File
@@ -52,6 +52,7 @@ type TuicOption struct {
FastOpen bool `proxy:"fast-open,omitempty"`
MaxOpenStreams int `proxy:"max-open-streams,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
BBRProfile string `proxy:"bbr-profile,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
Certificate string `proxy:"certificate,omitempty"`
@@ -113,7 +114,7 @@ func (t *Tuic) dial(ctx context.Context) (quicConn *quic.Conn, err error) {
if err != nil {
return nil, err
}
common.SetCongestionController(quicConn, t.option.CongestionController, t.option.CWND)
common.SetCongestionController(quicConn, t.option.CongestionController, t.option.CWND, t.option.BBRProfile)
return quicConn, nil
}
+36 -32
View File
@@ -79,15 +79,16 @@ type VlessOption struct {
}
type XHTTPOptions struct {
Path string `proxy:"path,omitempty"`
Host string `proxy:"host,omitempty"`
Mode string `proxy:"mode,omitempty"`
Headers map[string]string `proxy:"headers,omitempty"`
NoGRPCHeader bool `proxy:"no-grpc-header,omitempty"`
XPaddingBytes string `proxy:"x-padding-bytes,omitempty"`
ScMaxEachPostBytes string `proxy:"sc-max-each-post-bytes,omitempty"`
ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX
DownloadSettings *XHTTPDownloadSettings `proxy:"download-settings,omitempty"`
Path string `proxy:"path,omitempty"`
Host string `proxy:"host,omitempty"`
Mode string `proxy:"mode,omitempty"`
Headers map[string]string `proxy:"headers,omitempty"`
NoGRPCHeader bool `proxy:"no-grpc-header,omitempty"`
XPaddingBytes string `proxy:"x-padding-bytes,omitempty"`
ScMaxEachPostBytes string `proxy:"sc-max-each-post-bytes,omitempty"`
ScMinPostsIntervalMs string `proxy:"sc-min-posts-interval-ms,omitempty"`
ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX
DownloadSettings *XHTTPDownloadSettings `proxy:"download-settings,omitempty"`
}
type XHTTPReuseSettings struct {
@@ -101,13 +102,14 @@ type XHTTPReuseSettings struct {
type XHTTPDownloadSettings struct {
// xhttp part
Path *string `proxy:"path,omitempty"`
Host *string `proxy:"host,omitempty"`
Headers *map[string]string `proxy:"headers,omitempty"`
NoGRPCHeader *bool `proxy:"no-grpc-header,omitempty"`
XPaddingBytes *string `proxy:"x-padding-bytes,omitempty"`
ScMaxEachPostBytes *string `proxy:"sc-max-each-post-bytes,omitempty"`
ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX
Path *string `proxy:"path,omitempty"`
Host *string `proxy:"host,omitempty"`
Headers *map[string]string `proxy:"headers,omitempty"`
NoGRPCHeader *bool `proxy:"no-grpc-header,omitempty"`
XPaddingBytes *string `proxy:"x-padding-bytes,omitempty"`
ScMaxEachPostBytes *string `proxy:"sc-max-each-post-bytes,omitempty"`
ScMinPostsIntervalMs *string `proxy:"sc-min-posts-interval-ms,omitempty"`
ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX
// proxy part
Server *string `proxy:"server,omitempty"`
Port *int `proxy:"port,omitempty"`
@@ -544,14 +546,15 @@ func NewVless(option VlessOption) (*Vless, error) {
}
cfg := &xhttp.Config{
Host: requestHost,
Path: v.option.XHTTPOpts.Path,
Mode: v.option.XHTTPOpts.Mode,
Headers: v.option.XHTTPOpts.Headers,
NoGRPCHeader: v.option.XHTTPOpts.NoGRPCHeader,
XPaddingBytes: v.option.XHTTPOpts.XPaddingBytes,
ScMaxEachPostBytes: v.option.XHTTPOpts.ScMaxEachPostBytes,
ReuseConfig: reuseCfg,
Host: requestHost,
Path: v.option.XHTTPOpts.Path,
Mode: v.option.XHTTPOpts.Mode,
Headers: v.option.XHTTPOpts.Headers,
NoGRPCHeader: v.option.XHTTPOpts.NoGRPCHeader,
XPaddingBytes: v.option.XHTTPOpts.XPaddingBytes,
ScMaxEachPostBytes: v.option.XHTTPOpts.ScMaxEachPostBytes,
ScMinPostsIntervalMs: v.option.XHTTPOpts.ScMinPostsIntervalMs,
ReuseConfig: reuseCfg,
}
makeTransport := func() http.RoundTripper {
@@ -658,14 +661,15 @@ func NewVless(option VlessOption) (*Vless, error) {
}
cfg.DownloadConfig = &xhttp.Config{
Host: downloadHost,
Path: lo.FromPtrOr(ds.Path, v.option.XHTTPOpts.Path),
Mode: v.option.XHTTPOpts.Mode,
Headers: lo.FromPtrOr(ds.Headers, v.option.XHTTPOpts.Headers),
NoGRPCHeader: lo.FromPtrOr(ds.NoGRPCHeader, v.option.XHTTPOpts.NoGRPCHeader),
XPaddingBytes: lo.FromPtrOr(ds.XPaddingBytes, v.option.XHTTPOpts.XPaddingBytes),
ScMaxEachPostBytes: lo.FromPtrOr(ds.ScMaxEachPostBytes, v.option.XHTTPOpts.ScMaxEachPostBytes),
ReuseConfig: downloadReuseCfg,
Host: downloadHost,
Path: lo.FromPtrOr(ds.Path, v.option.XHTTPOpts.Path),
Mode: v.option.XHTTPOpts.Mode,
Headers: lo.FromPtrOr(ds.Headers, v.option.XHTTPOpts.Headers),
NoGRPCHeader: lo.FromPtrOr(ds.NoGRPCHeader, v.option.XHTTPOpts.NoGRPCHeader),
XPaddingBytes: lo.FromPtrOr(ds.XPaddingBytes, v.option.XHTTPOpts.XPaddingBytes),
ScMaxEachPostBytes: lo.FromPtrOr(ds.ScMaxEachPostBytes, v.option.XHTTPOpts.ScMaxEachPostBytes),
ScMinPostsIntervalMs: lo.FromPtrOr(ds.ScMinPostsIntervalMs, v.option.XHTTPOpts.ScMinPostsIntervalMs),
ReuseConfig: downloadReuseCfg,
}
makeDownloadTransport = func() http.RoundTripper {
+9
View File
@@ -826,6 +826,7 @@ proxies: # socks5
# no-grpc-header: false
# x-padding-bytes: "100-1000"
# sc-max-each-post-bytes: 1000000
# sc-min-posts-interval-ms: 30
# reuse-settings: # aka XMUX
# max-concurrency: "16-32"
# max-connections: "0"
@@ -842,6 +843,7 @@ proxies: # socks5
# no-grpc-header: false
# x-padding-bytes: "100-1000"
# sc-max-each-post-bytes: 1000000
# sc-min-posts-interval-ms: 30
# reuse-settings: # aka XMUX
# max-concurrency: "16-32"
# max-connections: "0"
@@ -986,6 +988,7 @@ proxies: # socks5
# up 和 down 均不写或为 0 则使用 BBR 流控
# up: "30 Mbps" # 若不写单位,默认为 Mbps
# down: "200 Mbps" # 若不写单位,默认为 Mbps
# bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard"
password: yourpassword
# obfs: salamander # 默认为空,如果填写则开启 obfs,目前仅支持 salamander
# obfs-password: yourpassword
@@ -1116,6 +1119,7 @@ proxies: # socks5
udp-relay-mode: native # Available: "native", "quic". Default: "native"
# congestion-controller: bbr # Available: "cubic", "new_reno", "bbr". Default: "cubic"
# cwnd: 10 # default: 32
# bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard"
# max-udp-relay-packet-size: 1500
# fast-open: true
# skip-cert-verify: true
@@ -1234,6 +1238,7 @@ proxies: # socks5
### quic options
# quic: true # 默认为false
# congestion-controller: bbr
# bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard"
### reuse options
# max-connections: 8 # Maximum connections. Conflict with max-streams.
# min-streams: 5 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams.
@@ -1649,6 +1654,7 @@ listeners:
# dC5jb20AAA==
# -----END ECH KEYS-----
# congestion-controller: bbr
# bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard"
# max-idle-time: 15000
# authentication-timeout: 1000
# alpn:
@@ -1681,6 +1687,7 @@ listeners:
# host: ""
# mode: auto # Available: "stream-one", "stream-up" or "packet-up"
# no-sse-header: false
# sc-max-buffered-posts: 30
# sc-stream-up-server-secs: "20-80"
# sc-max-each-post-bytes: 1000000
# -------------------------
@@ -1881,6 +1888,7 @@ listeners:
private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径
network: ["tcp", "udp"] # http2+http3
congestion-controller: bbr
# bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard"
# 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空
# client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify"
# client-auth-cert: string # 证书 PEM 格式,或者 证书的路径
@@ -1956,6 +1964,7 @@ listeners:
# certificate: ./server.crt
# private-key: ./server.key
# congestion-controller: bbr
# bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard"
# max-idle-time: 15000
# authentication-timeout: 1000
# alpn:
+1 -1
View File
@@ -30,7 +30,7 @@ require (
github.com/metacubex/restls-client-go v0.1.7
github.com/metacubex/sing v0.5.7
github.com/metacubex/sing-mux v0.3.6
github.com/metacubex/sing-quic v0.0.0-20260413140653-b900ea123319
github.com/metacubex/sing-quic v0.0.0-20260414034501-3ea3410d197a
github.com/metacubex/sing-shadowsocks v0.2.12
github.com/metacubex/sing-shadowsocks2 v0.2.7
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2
+2 -2
View File
@@ -127,8 +127,8 @@ github.com/metacubex/sing v0.5.7 h1:8OC+fhKFSv/l9ehEhJRaZZAOuthfZo68SteBVLe8QqM=
github.com/metacubex/sing v0.5.7/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing-mux v0.3.6 h1:AIjxEz7pq+5DVoV9N5/xaQXz3cMSZR9PBvMRP9X3+A0=
github.com/metacubex/sing-mux v0.3.6/go.mod h1:8bT7ZKT3clRrJjYc/x5CRYibC1TX/bK73a3r3+2E+Fc=
github.com/metacubex/sing-quic v0.0.0-20260413140653-b900ea123319 h1:zVqpKLZR/K7IiAVKs53RCEAXpVe9re7t5GVOHeL3Pzo=
github.com/metacubex/sing-quic v0.0.0-20260413140653-b900ea123319/go.mod h1:6ayFGfzzBE85csgQkM3gf4neFq6s0losHlPRSxY+nuk=
github.com/metacubex/sing-quic v0.0.0-20260414034501-3ea3410d197a h1:977o0ZYYbiQAGuOxql7Q6UN3rEy59OyAE0tELq4gZfI=
github.com/metacubex/sing-quic v0.0.0-20260414034501-3ea3410d197a/go.mod h1:6ayFGfzzBE85csgQkM3gf4neFq6s0losHlPRSxY+nuk=
github.com/metacubex/sing-shadowsocks v0.2.12 h1:Wqzo8bYXrK5aWqxu/TjlTnYZzAKtKsaFQBdr6IHFaBE=
github.com/metacubex/sing-shadowsocks v0.2.12/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU=
github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6wVj3PPBVhor3A=
+4
View File
@@ -122,6 +122,7 @@ type tuicServerSchema struct {
ALPN *[]string `yaml:"alpn" json:"alpn,omitempty"`
MaxUdpRelayPacketSize *int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"`
CWND *int `yaml:"cwnd" json:"cwnd,omitempty"`
BBRProfile *string `yaml:"bbr-profile" json:"bbr-profile,omitempty"`
}
func getConfigs(w http.ResponseWriter, r *http.Request) {
@@ -305,6 +306,9 @@ func pointerOrDefaultTuicServer(p *tuicServerSchema, def LC.TuicServer) LC.TuicS
if p.CWND != nil {
def.CWND = *p.CWND
}
if p.BBRProfile != nil {
def.BBRProfile = *p.BBRProfile
}
}
return def
}
+1
View File
@@ -24,6 +24,7 @@ type Hysteria2Server struct {
IgnoreClientBandwidth bool `yaml:"ignore-client-bandwidth" json:"ignore-client-bandwidth,omitempty"`
Masquerade string `yaml:"masquerade" json:"masquerade,omitempty"`
CWND int `yaml:"cwnd" json:"cwnd,omitempty"`
BBRProfile string `yaml:"bbr-profile" json:"bbr-profile,omitempty"`
UdpMTU int `yaml:"udp-mtu" json:"udp-mtu,omitempty"`
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
@@ -16,6 +16,7 @@ type TrustTunnelServer struct {
Network []string `yaml:"network" json:"network,omitempty"`
CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"`
CWND int `yaml:"cwnd" json:"cwnd,omitempty"`
BBRProfile string `yaml:"bbr-profile" json:"bbr-profile,omitempty"`
}
func (t TrustTunnelServer) String() string {
+1
View File
@@ -23,6 +23,7 @@ type TuicServer struct {
MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"`
MaxDatagramFrameSize int `yaml:"max-datagram-frame-size" json:"max-datagram-frame-size,omitempty"`
CWND int `yaml:"cwnd" json:"cwnd,omitempty"`
BBRProfile string `yaml:"bbr-profile" json:"bbr-profile,omitempty"`
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
}
+1
View File
@@ -36,6 +36,7 @@ type XHTTPConfig struct {
Mode string
NoSSEHeader bool
ScStreamUpServerSecs string
ScMaxBufferedPosts string
ScMaxEachPostBytes string
}
@@ -13,7 +13,6 @@ import (
"strconv"
"sync"
"testing"
"time"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/common/pool"
@@ -186,11 +185,6 @@ func NewHttpTestTunnel() *TestTunnel {
DialContext: func(context.Context, string, string) (net.Conn, error) {
return instance, nil
},
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
// for our self-signed cert
TLSClientConfig: tlsClientConfig.Clone(),
// open http2
@@ -198,7 +192,6 @@ func NewHttpTestTunnel() *TestTunnel {
}
client := http.Client{
Timeout: 30 * time.Second,
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
+2
View File
@@ -26,6 +26,7 @@ type Hysteria2Option struct {
IgnoreClientBandwidth bool `inbound:"ignore-client-bandwidth,omitempty"`
Masquerade string `inbound:"masquerade,omitempty"`
CWND int `inbound:"cwnd,omitempty"`
BBRProfile string `inbound:"bbr-profile,omitempty"`
UdpMTU int `inbound:"udp-mtu,omitempty"`
MuxOption MuxOption `inbound:"mux-option,omitempty"`
@@ -73,6 +74,7 @@ func NewHysteria2(options *Hysteria2Option) (*Hysteria2, error) {
IgnoreClientBandwidth: options.IgnoreClientBandwidth,
Masquerade: options.Masquerade,
CWND: options.CWND,
BBRProfile: options.BBRProfile,
UdpMTU: options.UdpMTU,
MuxOption: options.MuxOption.Build(),
// quic-go special config
@@ -2,6 +2,7 @@ package inbound_test
import (
"net/netip"
"runtime"
"testing"
"github.com/metacubex/mihomo/adapter/outbound"
@@ -166,6 +167,10 @@ func TestInboundSudoku_CustomTable(t *testing.T) {
}
func TestInboundSudoku_HTTPMaskMode(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("temporarily skipped on windows due to intermittent failures; tracked in PR")
}
key := "test_key_http_mask_mode"
for _, mode := range []string{"ws", "stream", "poll", "auto"} {
@@ -20,6 +20,7 @@ type TrustTunnelOption struct {
Network []string `inbound:"network,omitempty"`
CongestionController string `inbound:"congestion-controller,omitempty"`
CWND int `inbound:"cwnd,omitempty"`
BBRProfile string `inbound:"bbr-profile,omitempty"`
}
func (o TrustTunnelOption) Equal(config C.InboundConfig) bool {
@@ -57,6 +58,7 @@ func NewTrustTunnel(options *TrustTunnelOption) (*TrustTunnel, error) {
Network: options.Network,
CongestionController: options.CongestionController,
CWND: options.CWND,
BBRProfile: options.BBRProfile,
},
}, nil
}
+2
View File
@@ -24,6 +24,7 @@ type TuicOption struct {
ALPN []string `inbound:"alpn,omitempty"`
MaxUdpRelayPacketSize int `inbound:"max-udp-relay-packet-size,omitempty"`
CWND int `inbound:"cwnd,omitempty"`
BBRProfile string `inbound:"bbr-profile,omitempty"`
MuxOption MuxOption `inbound:"mux-option,omitempty"`
}
@@ -62,6 +63,7 @@ func NewTuic(options *TuicOption) (*Tuic, error) {
ALPN: options.ALPN,
MaxUdpRelayPacketSize: options.MaxUdpRelayPacketSize,
CWND: options.CWND,
BBRProfile: options.BBRProfile,
MuxOption: options.MuxOption.Build(),
},
}, nil
+2
View File
@@ -37,6 +37,7 @@ type XHTTPConfig struct {
Mode string `inbound:"mode,omitempty"`
NoSSEHeader bool `inbound:"no-sse-header,omitempty"`
ScStreamUpServerSecs string `inbound:"sc-stream-up-server-secs,omitempty"`
ScMaxBufferedPosts string `inbound:"sc-max-buffered-posts,omitempty"`
ScMaxEachPostBytes string `inbound:"sc-max-each-post-bytes,omitempty"`
}
@@ -47,6 +48,7 @@ func (o XHTTPConfig) Build() LC.XHTTPConfig {
Mode: o.Mode,
NoSSEHeader: o.NoSSEHeader,
ScStreamUpServerSecs: o.ScStreamUpServerSecs,
ScMaxBufferedPosts: o.ScMaxBufferedPosts,
ScMaxEachPostBytes: o.ScMaxEachPostBytes,
}
}
+4 -1
View File
@@ -20,6 +20,7 @@ import (
"github.com/metacubex/mihomo/listener/sing"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/ntp"
"github.com/metacubex/mihomo/transport/tuic/common"
"github.com/metacubex/http"
"github.com/metacubex/http/httputil"
@@ -171,8 +172,10 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi
UDPTimeout: sing.UDPTimeout,
Handler: h,
MasqueradeHandler: masqueradeHandler,
CWND: config.CWND,
UdpMTU: config.UdpMTU,
SetBBRCongestion: func(quicConn *quic.Conn) {
common.SetCongestionController(quicConn, "bbr", config.CWND, config.BBRProfile)
},
})
if err != nil {
return nil, err
+1
View File
@@ -161,6 +161,7 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition)
Mode: config.XHTTPConfig.Mode,
NoSSEHeader: config.XHTTPConfig.NoSSEHeader,
ScStreamUpServerSecs: config.XHTTPConfig.ScStreamUpServerSecs,
ScMaxBufferedPosts: config.XHTTPConfig.ScMaxBufferedPosts,
ScMaxEachPostBytes: config.XHTTPConfig.ScMaxEachPostBytes,
},
ConnHandler: func(conn net.Conn) {
@@ -135,6 +135,7 @@ func New(config LC.TrustTunnelServer, tunnel C.Tunnel, additions ...inbound.Addi
ICMPHandler: nil,
QUICCongestionControl: config.CongestionController,
QUICCwnd: config.CWND,
QUICBBRProfile: config.BBRProfile,
})
service.UpdateUsers(config.Users)
err = service.Start(tcpListener, udpConn, tlsConfig)
+1
View File
@@ -155,6 +155,7 @@ func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) (
AuthenticationTimeout: time.Duration(config.AuthenticationTimeout) * time.Millisecond,
MaxUdpRelayPacketSize: config.MaxUdpRelayPacketSize,
CWND: config.CWND,
BBRProfile: config.BBRProfile,
}
if len(config.Token) > 0 {
tokens := make([][32]byte, len(config.Token))
+2 -1
View File
@@ -34,6 +34,7 @@ type ClientOptions struct {
QUIC bool
QUICCongestionControl string
QUICCwnd int
QUICBBRProfile string
HealthCheck bool
MaxConnections int
MinStreams int
@@ -67,7 +68,7 @@ func NewClient(ctx context.Context, options ClientOptions) (client *Client, err
} else if !slices.Contains(options.TLSConfig.NextProtos, "h3") {
return nil, errors.New("require alpn h3")
}
err = client.quicRoundTripper(options.TLSConfig, options.QUICCongestionControl, options.QUICCwnd)
err = client.quicRoundTripper(options.TLSConfig, options.QUICCongestionControl, options.QUICCwnd, options.QUICBBRProfile)
if err != nil {
return nil, err
}
+3 -3
View File
@@ -15,7 +15,7 @@ import (
"github.com/metacubex/tls"
)
func (c *Client) quicRoundTripper(tlsConfig *vmess.TLSConfig, congestionControlName string, cwnd int) error {
func (c *Client) quicRoundTripper(tlsConfig *vmess.TLSConfig, congestionControlName string, cwnd int, bbrProfile string) error {
stdConfig, err := tlsConfig.ToStdConfig()
if err != nil {
return err
@@ -38,7 +38,7 @@ func (c *Client) quicRoundTripper(tlsConfig *vmess.TLSConfig, congestionControlN
if err != nil {
return nil, err
}
common.SetCongestionController(quicConn, congestionControlName, cwnd)
common.SetCongestionController(quicConn, congestionControlName, cwnd, bbrProfile)
return quicConn, nil
},
}
@@ -60,7 +60,7 @@ func (s *Service) configHTTP3Server(tlsConfig *tls.Config, udpConn net.PacketCon
Handler: s,
IdleTimeout: DefaultSessionTimeout,
ConnContext: func(ctx context.Context, conn *quic.Conn) context.Context {
common.SetCongestionController(conn, s.quicCongestionControl, s.quicCwnd)
common.SetCongestionController(conn, s.quicCongestionControl, s.quicCwnd, s.quicBBRProfile)
return ctx
},
}
+8 -4
View File
@@ -7,6 +7,7 @@ import (
"time"
"github.com/metacubex/mihomo/common/httputils"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/http"
"github.com/metacubex/http/h2c"
@@ -18,13 +19,13 @@ import (
E "github.com/metacubex/sing/common/exceptions"
"github.com/metacubex/sing/common/logger"
M "github.com/metacubex/sing/common/metadata"
N "github.com/metacubex/sing/common/network"
"github.com/metacubex/sing/common/network"
"github.com/metacubex/tls"
)
type Handler interface {
N.TCPConnectionHandler
N.UDPConnectionHandler
network.TCPConnectionHandler
network.UDPConnectionHandler
}
type ICMPHandler interface {
@@ -38,6 +39,7 @@ type ServiceOptions struct {
ICMPHandler ICMPHandler
QUICCongestionControl string
QUICCwnd int
QUICBBRProfile string
}
type Service struct {
@@ -48,6 +50,7 @@ type Service struct {
icmpHandler ICMPHandler
quicCongestionControl string
quicCwnd int
quicBBRProfile string
httpServer *http.Server
h2Server *http.Http2Server
h3Server *http3.Server
@@ -64,6 +67,7 @@ func NewService(options ServiceOptions) *Service {
icmpHandler: options.ICMPHandler,
quicCongestionControl: options.QUICCongestionControl,
quicCwnd: options.QUICCwnd,
quicBBRProfile: options.QUICBBRProfile,
}
}
@@ -224,7 +228,7 @@ func (s *Service) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
}
httputils.SetAddrFromRequest(&conn.NetAddr, request)
conn.setUp(request.Body, nil)
_ = s.handler.NewConnection(ctx, conn, M.Metadata{
_ = s.handler.NewConnection(ctx, N.NewDeadlineConn(conn), M.Metadata{
Protocol: "trusttunnel",
Source: M.ParseSocksaddr(request.RemoteAddr),
Destination: M.ParseSocksaddr(request.Host).Unwrap(),
@@ -13,7 +13,7 @@ const (
DefaultConnectionReceiveWindow = 67108864 // 64 MB/s
)
func SetCongestionController(quicConn *quic.Conn, cc string, cwnd int) {
func SetCongestionController(quicConn *quic.Conn, cc string, cwnd int, profile string) {
if cwnd == 0 {
cwnd = 32
}
@@ -47,6 +47,7 @@ func SetCongestionController(quicConn *quic.Conn, cc string, cwnd int) {
congestionv2.NewBbrSender(
congestionv2.GetInitialPacketSize(quicConn),
c.ByteCount(cwnd),
congestionv2.Profile(profile),
),
)
}
@@ -27,16 +27,13 @@ const (
invalidPacketNumber = -1
initialCongestionWindowPackets = 32
minCongestionWindowPackets = 4
// Constants based on TCP defaults.
// The minimum CWND to ensure delayed acks don't reduce bandwidth measurements.
// Does not inflate the pacing rate.
defaultMinimumCongestionWindow = 4 * congestion.ByteCount(congestion.InitialPacketSize)
// The gain used for the STARTUP, equal to 2/ln(2).
defaultHighGain = 2.885
// The newly derived gain for STARTUP, equal to 4 * ln(2)
derivedHighGain = 2.773
// The newly derived CWND gain for STARTUP, 2.
derivedHighCWNDGain = 2.0
)
@@ -63,7 +60,6 @@ const (
// Flag.
defaultStartupFullLossCount = 8
quicBbr2DefaultLossThreshold = 0.02
maxBbrBurstPackets = 10
)
type bbrMode int
@@ -94,6 +90,63 @@ const (
bbrRecoveryStateGrowth
)
type Profile string
const (
ProfileConservative Profile = "conservative"
ProfileStandard Profile = "standard"
ProfileAggressive Profile = "aggressive"
)
type profileConfig struct {
highGain float64
highCwndGain float64
congestionWindowGainConstant float64
numStartupRtts int64
drainToTarget bool
detectOvershooting bool
bytesLostMultiplier uint8
enableAckAggregationStartup bool
expireAckAggregationStartup bool
enableOverestimateAvoidance bool
reduceExtraAckedOnBandwidthIncrease bool
}
func configForProfile(profile Profile) profileConfig {
switch profile {
case ProfileConservative:
return profileConfig{
highGain: 2.25,
highCwndGain: 1.75,
congestionWindowGainConstant: 1.75,
numStartupRtts: 2,
drainToTarget: true,
detectOvershooting: true,
bytesLostMultiplier: 1,
enableOverestimateAvoidance: true,
reduceExtraAckedOnBandwidthIncrease: true,
}
case ProfileAggressive:
return profileConfig{
highGain: 3.0,
highCwndGain: 2.25,
congestionWindowGainConstant: 2.5,
numStartupRtts: 4,
bytesLostMultiplier: 2,
enableAckAggregationStartup: true,
expireAckAggregationStartup: true,
}
default:
return profileConfig{
highGain: defaultHighGain,
highCwndGain: derivedHighCWNDGain,
congestionWindowGainConstant: 2.0,
numStartupRtts: roundTripsWithoutGrowthBeforeExitingStartup,
bytesLostMultiplier: 2,
}
}
}
type bbrSender struct {
rttStats congestion.RTTStatsProvider
pacer *Pacer
@@ -141,6 +194,9 @@ type bbrSender struct {
// The smallest value the |congestion_window_| can achieve.
minCongestionWindow congestion.ByteCount
// The BBR profile used by the sender.
profile Profile
// The pacing gain applied during the STARTUP phase.
highGain float64
@@ -245,11 +301,13 @@ var _ congestion.CongestionControl = &bbrSender{}
func NewBbrSender(
initialMaxDatagramSize congestion.ByteCount,
initialCongestionWindowPackets congestion.ByteCount,
profile Profile,
) *bbrSender {
return newBbrSender(
initialMaxDatagramSize,
initialCongestionWindowPackets*initialMaxDatagramSize,
congestion.MaxCongestionWindowPackets*initialMaxDatagramSize,
profile,
)
}
@@ -257,6 +315,7 @@ func newBbrSender(
initialMaxDatagramSize,
initialCongestionWindow,
initialMaxCongestionWindow congestion.ByteCount,
profile Profile,
) *bbrSender {
b := &bbrSender{
mode: bbrModeStartup,
@@ -267,9 +326,10 @@ func newBbrSender(
congestionWindow: initialCongestionWindow,
initialCongestionWindow: initialCongestionWindow,
maxCongestionWindow: initialMaxCongestionWindow,
minCongestionWindow: defaultMinimumCongestionWindow,
minCongestionWindow: minCongestionWindowForMaxDatagramSize(initialMaxDatagramSize),
profile: ProfileStandard,
highGain: defaultHighGain,
highCwndGain: defaultHighGain,
highCwndGain: derivedHighCWNDGain,
drainGain: 1.0 / defaultHighGain,
pacingGain: 1.0,
congestionWindowGain: 1.0,
@@ -284,20 +344,57 @@ func newBbrSender(
maxDatagramSize: initialMaxDatagramSize,
}
b.pacer = NewPacer(b.bandwidthForPacer)
/*
if b.tracer != nil {
b.lastState = logging.CongestionStateStartup
b.tracer.UpdatedCongestionState(logging.CongestionStateStartup)
}
*/
b.applyProfile(profile)
b.enterStartupMode()
b.setHighCwndGain(derivedHighCWNDGain)
return b
}
func (b *bbrSender) applyProfile(profile Profile) {
cfg := configForProfile(profile)
b.profile = profile
b.highGain = cfg.highGain
b.highCwndGain = cfg.highCwndGain
b.drainGain = 1.0 / cfg.highGain
b.congestionWindowGainConstant = cfg.congestionWindowGainConstant
b.numStartupRtts = cfg.numStartupRtts
b.drainToTarget = cfg.drainToTarget
b.detectOvershooting = cfg.detectOvershooting
b.bytesLostMultiplierWhileDetectingOvershooting = cfg.bytesLostMultiplier
b.enableAckAggregationDuringStartup = cfg.enableAckAggregationStartup
b.expireAckAggregationInStartup = cfg.expireAckAggregationStartup
if cfg.enableOverestimateAvoidance {
b.sampler.EnableOverestimateAvoidance()
}
b.sampler.SetReduceExtraAckedOnBandwidthIncrease(cfg.reduceExtraAckedOnBandwidthIncrease)
}
func minCongestionWindowForMaxDatagramSize(maxDatagramSize congestion.ByteCount) congestion.ByteCount {
return minCongestionWindowPackets * maxDatagramSize
}
func scaleByteWindowForDatagramSize(window, oldMaxDatagramSize, newMaxDatagramSize congestion.ByteCount) congestion.ByteCount {
if oldMaxDatagramSize == newMaxDatagramSize {
return window
}
return congestion.ByteCount(uint64(window) * uint64(newMaxDatagramSize) / uint64(oldMaxDatagramSize))
}
func (b *bbrSender) rescalePacketSizedWindows(maxDatagramSize congestion.ByteCount) {
oldMaxDatagramSize := b.maxDatagramSize
b.maxDatagramSize = maxDatagramSize
b.initialCongestionWindow = scaleByteWindowForDatagramSize(b.initialCongestionWindow, oldMaxDatagramSize, maxDatagramSize)
b.maxCongestionWindow = scaleByteWindowForDatagramSize(b.maxCongestionWindow, oldMaxDatagramSize, maxDatagramSize)
b.minCongestionWindow = minCongestionWindowForMaxDatagramSize(maxDatagramSize)
b.cwndToCalculateMinPacingRate = scaleByteWindowForDatagramSize(b.cwndToCalculateMinPacingRate, oldMaxDatagramSize, maxDatagramSize)
b.maxCongestionWindowWithNetworkParametersAdjusted = scaleByteWindowForDatagramSize(
b.maxCongestionWindowWithNetworkParametersAdjusted,
oldMaxDatagramSize,
maxDatagramSize,
)
}
func (b *bbrSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) {
b.rttStats = provider
}
@@ -330,8 +427,6 @@ func (b *bbrSender) OnPacketSent(
}
b.sampler.OnPacketSent(sentTime, packetNumber, bytes, bytesInFlight, isRetransmittable)
b.maybeAppLimited(bytesInFlight)
}
// CanSend implements the SendAlgorithm interface.
@@ -364,11 +459,18 @@ func (b *bbrSender) SetMaxDatagramSize(s congestion.ByteCount) {
if s < b.maxDatagramSize {
panic(fmt.Sprintf("congestion BUG: decreased max datagram size from %d to %d", b.maxDatagramSize, s))
}
cwndIsMinCwnd := b.congestionWindow == b.minCongestionWindow
b.maxDatagramSize = s
if cwndIsMinCwnd {
oldMinCongestionWindow := b.minCongestionWindow
oldInitialCongestionWindow := b.initialCongestionWindow
b.rescalePacketSizedWindows(s)
switch b.congestionWindow {
case oldMinCongestionWindow:
b.congestionWindow = b.minCongestionWindow
case oldInitialCongestionWindow:
b.congestionWindow = b.initialCongestionWindow
default:
b.congestionWindow = Min(b.maxCongestionWindow, Max(b.congestionWindow, b.minCongestionWindow))
}
b.recoveryWindow = Min(b.maxCongestionWindow, Max(b.recoveryWindow, b.minCongestionWindow))
b.pacer.SetMaxDatagramSize(s)
}
@@ -411,6 +513,8 @@ func (b *bbrSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, even
// packet in lost_packets.
var lastPacketSendState sendTimeState
b.maybeAppLimited(priorInFlight)
// Update bytesInFlight
b.bytesInFlight = priorInFlight
for _, p := range ackedPackets {
@@ -508,22 +612,6 @@ func (b *bbrSender) PacingRate() Bandwidth {
return b.pacingRate
}
func (b *bbrSender) hasGoodBandwidthEstimateForResumption() bool {
return b.hasNonAppLimitedSample()
}
func (b *bbrSender) hasNonAppLimitedSample() bool {
return b.hasNoAppLimitedSample
}
// Sets the pacing gain used in STARTUP. Must be greater than 1.
func (b *bbrSender) setHighGain(highGain float64) {
b.highGain = highGain
if b.mode == bbrModeStartup {
b.pacingGain = highGain
}
}
// Sets the CWND gain used in STARTUP. Must be greater than 1.
func (b *bbrSender) setHighCwndGain(highCwndGain float64) {
b.highCwndGain = highCwndGain
@@ -532,11 +620,6 @@ func (b *bbrSender) setHighCwndGain(highCwndGain float64) {
}
}
// Sets the gain used in DRAIN. Must be less than 1.
func (b *bbrSender) setDrainGain(drainGain float64) {
b.drainGain = drainGain
}
// Get the current bandwidth estimate. Note that Bandwidth is in bits per second.
func (b *bbrSender) bandwidthEstimate() Bandwidth {
return b.maxBandwidth.GetBest()
@@ -697,12 +780,7 @@ func (b *bbrSender) checkIfFullBandwidthReached(lastPacketSendState *sendTimeSta
}
func (b *bbrSender) maybeAppLimited(bytesInFlight congestion.ByteCount) {
congestionWindow := b.GetCongestionWindow()
if bytesInFlight >= congestionWindow {
return
}
availableBytes := congestionWindow - bytesInFlight
if availableBytes > maxBbrBurstPackets*b.maxDatagramSize {
if bytesInFlight < b.getTargetCongestionWindow(1) {
b.sampler.OnAppLimited()
}
}
@@ -729,7 +807,7 @@ func (b *bbrSender) maybeEnterOrExitProbeRtt(now monotime.Time, isRoundStart, mi
b.pacingGain = 1.0
// Do not decide on the time to exit PROBE_RTT until the |bytes_in_flight|
// is at the target small value.
b.exitProbeRttAt = monotime.Time(0)
b.exitProbeRttAt = 0
}
if b.mode == bbrModeProbeRtt {
@@ -1,7 +1,6 @@
package congestion
import (
"math"
"time"
"github.com/metacubex/quic-go/congestion"
@@ -9,7 +8,8 @@ import (
)
const (
maxBurstPackets = 10
maxBurstPackets = 10
maxBurstPacingDelayMultiplier = 4
)
// Pacer implements a token bucket pacing algorithm.
@@ -52,22 +52,27 @@ func (p *Pacer) Budget(now monotime.Time) congestion.ByteCount {
func (p *Pacer) maxBurstSize() congestion.ByteCount {
return Max(
congestion.ByteCount((congestion.MinPacingDelay+time.Millisecond).Nanoseconds())*p.getBandwidth()/1e9,
congestion.ByteCount((maxBurstPacingDelayMultiplier*congestion.MinPacingDelay).Nanoseconds())*p.getBandwidth()/1e9,
maxBurstPackets*p.maxDatagramSize,
)
}
// TimeUntilSend returns when the next packet should be sent.
// It returns the zero value of monotime.Time if a packet can be sent immediately.
// It returns the zero value if a packet can be sent immediately.
func (p *Pacer) TimeUntilSend() monotime.Time {
if p.budgetAtLastSent >= p.maxDatagramSize {
return monotime.Time(0)
return 0
}
return p.lastSentTime.Add(Max(
congestion.MinPacingDelay,
time.Duration(math.Ceil(float64(p.maxDatagramSize-p.budgetAtLastSent)*1e9/
float64(p.getBandwidth())))*time.Nanosecond,
))
diff := 1e9 * uint64(p.maxDatagramSize-p.budgetAtLastSent)
bw := uint64(p.getBandwidth())
// We might need to round up this value.
// Otherwise, we might have a budget (slightly) smaller than the datagram size when the timer expires.
d := diff / bw
// this is effectively a math.Ceil, but using only integer math
if diff%bw > 0 {
d++
}
return p.lastSentTime.Add(Max(congestion.MinPacingDelay, time.Duration(d)*time.Nanosecond))
}
func (p *Pacer) SetMaxDatagramSize(s congestion.ByteCount) {
@@ -152,7 +152,7 @@ func (p *packetNumberIndexedQueue[T]) EntrySlotsUsed() int {
return p.entries.Len()
}
// LastPacket returns packet number of the first entry in the queue.
// FirstPacket returns packet number of the first entry in the queue.
func (p *packetNumberIndexedQueue[T]) FirstPacket() (packetNumber congestion.PacketNumber) {
return p.firstPacket
}
+2 -1
View File
@@ -33,6 +33,7 @@ type ServerOption struct {
AuthenticationTimeout time.Duration
MaxUdpRelayPacketSize int
CWND int
BBRProfile string
}
type Server struct {
@@ -48,7 +49,7 @@ func (s *Server) Serve() error {
if err != nil {
return err
}
common.SetCongestionController(conn, s.CongestionController, s.CWND)
common.SetCongestionController(conn, s.CongestionController, s.CWND, s.BBRProfile)
h := &serverHandler{
Server: s,
quicConn: conn,
+87 -48
View File
@@ -1,6 +1,7 @@
package xhttp
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
@@ -39,36 +40,71 @@ type DialQUICFunc func(ctx context.Context, cfg *quic.Config) (*quic.Conn, error
type TransportMaker func() http.RoundTripper
type PacketUpWriter struct {
ctx context.Context
cancel context.CancelFunc
cfg *Config
scMaxEachPostBytes Range
sessionID string
transport http.RoundTripper
writeMu sync.Mutex
seq uint64
ctx context.Context
cancel context.CancelFunc
cfg *Config
scMaxEachPostBytes int
scMinPostsIntervalMs Range
sessionID string
transport http.RoundTripper
writeMu sync.Mutex
writeCond sync.Cond
seq uint64
buf []byte
timer *time.Timer
flushErr error
}
func (c *PacketUpWriter) Write(b []byte) (int, error) {
c.writeMu.Lock()
defer c.writeMu.Unlock()
scMaxEachPostBytes := c.scMaxEachPostBytes.Rand()
if len(b) < scMaxEachPostBytes {
return c.write(b)
if err := c.flushErr; err != nil {
return 0, err
}
var n int
for start := 0; start < len(b); start += scMaxEachPostBytes {
end := start + scMaxEachPostBytes
if end > len(b) {
end = len(b)
data := bytes.NewBuffer(b)
for data.Len() > 0 {
if c.timer == nil { // start a timer to flush the buffer
c.timer = time.AfterFunc(time.Duration(c.scMinPostsIntervalMs.Rand())*time.Millisecond, c.flush)
}
_n, err := c.write(b[start:end])
n += _n
if err != nil {
return n, err
c.buf = append(c.buf, data.Next(c.scMaxEachPostBytes-len(c.buf))...) // let buffer fill up to scMaxEachPostBytes
if len(c.buf) >= c.scMaxEachPostBytes { // too much data in buffer, wait the flush complete
c.writeCond.Wait()
if err := c.flushErr; err != nil {
return 0, err
}
}
}
return n, nil
return len(b), nil
}
func (c *PacketUpWriter) flush() {
c.writeMu.Lock()
defer c.writeMu.Unlock()
defer c.writeCond.Broadcast() // wake up the waited Write() call
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
if c.flushErr != nil {
return
}
if len(c.buf) == 0 {
return
}
_, err := c.write(c.buf)
c.buf = c.buf[:0] // reset buffer
if err != nil {
c.flushErr = err
return
}
}
func (c *PacketUpWriter) write(b []byte) (int, error) {
@@ -106,6 +142,15 @@ func (c *PacketUpWriter) write(b []byte) (int, error) {
}
func (c *PacketUpWriter) Close() error {
ch := make(chan struct{})
go func() { // flush in the background
defer close(ch)
c.flush()
}()
select {
case <-ch:
case <-time.After(time.Second):
}
c.cancel()
httputils.CloseTransport(c.transport)
return nil
@@ -185,6 +230,7 @@ type Client struct {
mode string
cfg *Config
scMaxEachPostBytes Range
scMinPostsIntervalMs Range
makeTransport TransportMaker
makeDownloadTransport TransportMaker
uploadManager *ReuseManager
@@ -202,12 +248,17 @@ func NewClient(cfg *Config, makeTransport TransportMaker, makeDownloadTransport
if err != nil {
return nil, err
}
scMinPostsIntervalMs, err := cfg.GetNormalizedScMinPostsIntervalMs()
if err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(context.Background())
client := &Client{
mode: mode,
cfg: cfg,
scMaxEachPostBytes: scMaxEachPostBytes,
scMinPostsIntervalMs: scMinPostsIntervalMs,
makeTransport: makeTransport,
makeDownloadTransport: makeDownloadTransport,
ctx: ctx,
@@ -218,6 +269,7 @@ func NewClient(cfg *Config, makeTransport TransportMaker, makeDownloadTransport
if err != nil {
return nil, err
}
client.makeTransport = client.uploadManager.GetTransport
if cfg.DownloadConfig != nil {
if makeDownloadTransport == nil {
return nil, fmt.Errorf("xhttp: download manager requires download transport maker")
@@ -226,6 +278,7 @@ func NewClient(cfg *Config, makeTransport TransportMaker, makeDownloadTransport
if err != nil {
return nil, err
}
client.makeDownloadTransport = client.downloadManager.GetTransport
}
}
return client, nil
@@ -268,26 +321,10 @@ type onlyRoundTripper struct {
}
func (c *Client) getTransport() (uploadTransport http.RoundTripper, downloadTransport http.RoundTripper, err error) {
if c.uploadManager == nil {
uploadTransport = c.makeTransport()
downloadTransport = onlyRoundTripper{uploadTransport}
if c.makeDownloadTransport != nil {
downloadTransport = c.makeDownloadTransport()
}
} else {
uploadTransport, err = c.uploadManager.GetTransport()
if err != nil {
return
}
downloadTransport = onlyRoundTripper{uploadTransport}
if c.downloadManager != nil {
downloadTransport, err = c.downloadManager.GetTransport()
if err != nil {
httputils.CloseTransport(uploadTransport)
return
}
}
uploadTransport = c.makeTransport()
downloadTransport = onlyRoundTripper{uploadTransport}
if c.makeDownloadTransport != nil {
downloadTransport = c.makeDownloadTransport()
}
return
}
@@ -469,14 +506,16 @@ func (c *Client) DialPacketUp() (net.Conn, error) {
writerCtx, writerCancel := context.WithCancel(c.ctx)
writer := &PacketUpWriter{
ctx: writerCtx,
cancel: writerCancel,
cfg: c.cfg,
scMaxEachPostBytes: c.scMaxEachPostBytes,
sessionID: sessionID,
transport: uploadTransport,
seq: 0,
ctx: writerCtx,
cancel: writerCancel,
cfg: c.cfg,
scMaxEachPostBytes: c.scMaxEachPostBytes.Rand(),
scMinPostsIntervalMs: c.scMinPostsIntervalMs,
sessionID: sessionID,
transport: uploadTransport,
seq: 0,
}
writer.writeCond = sync.Cond{L: &writer.writeMu}
conn := &Conn{writer: writer}
downloadReq, err := http.NewRequestWithContext(
+25 -1
View File
@@ -20,7 +20,9 @@ type Config struct {
XPaddingBytes string
NoSSEHeader bool // server only
ScStreamUpServerSecs string // server only
ScMaxBufferedPosts string // server only
ScMaxEachPostBytes string
ScMinPostsIntervalMs string
ReuseConfig *ReuseConfig
DownloadConfig *Config
}
@@ -109,8 +111,19 @@ func (c *Config) GetNormalizedScStreamUpServerSecs() (Range, error) {
return r, nil
}
func (c *Config) GetNormalizedScMaxBufferedPosts() (Range, error) {
r, err := ParseRange(c.ScMaxBufferedPosts, "30")
if err != nil {
return Range{}, fmt.Errorf("invalid sc-max-buffered-posts: %w", err)
}
if r.Max == 0 {
return Range{}, fmt.Errorf("invalid sc-max-buffered-posts: must be greater than zero")
}
return r, nil
}
func (c *Config) GetNormalizedScMaxEachPostBytes() (Range, error) {
r, err := ParseRange(c.ScStreamUpServerSecs, "1000000")
r, err := ParseRange(c.ScMaxEachPostBytes, "1000000")
if err != nil {
return Range{}, fmt.Errorf("invalid sc-max-each-post-bytes: %w", err)
}
@@ -120,6 +133,17 @@ func (c *Config) GetNormalizedScMaxEachPostBytes() (Range, error) {
return r, nil
}
func (c *Config) GetNormalizedScMinPostsIntervalMs() (Range, error) {
r, err := ParseRange(c.ScMinPostsIntervalMs, "30")
if err != nil {
return Range{}, fmt.Errorf("invalid sc-min-posts-interval-ms: %w", err)
}
if r.Max == 0 {
return Range{}, fmt.Errorf("invalid sc-min-posts-interval-ms: must be greater than zero")
}
return r, nil
}
type Range struct {
Min int
Max int
+19 -18
View File
@@ -1,7 +1,6 @@
package xhttp
import (
"fmt"
"sync"
"sync/atomic"
"time"
@@ -23,20 +22,19 @@ type reuseEntry struct {
closed atomic.Bool
}
func (e *reuseEntry) isClosed() bool {
return e.closed.Load()
func (entry *reuseEntry) isClosed() bool {
return entry.closed.Load()
}
func (e *reuseEntry) close() {
if !e.closed.CompareAndSwap(false, true) {
func (entry *reuseEntry) close() {
if !entry.closed.CompareAndSwap(false, true) {
return
}
httputils.CloseTransport(e.transport)
httputils.CloseTransport(entry.transport)
}
type ReuseTransport struct {
entry *reuseEntry
manager *ReuseManager
removed atomic.Bool
}
@@ -48,7 +46,7 @@ func (rt *ReuseTransport) Close() error {
if !rt.removed.CompareAndSwap(false, true) {
return nil
}
rt.manager.release(rt.entry)
rt.entry.release()
return nil
}
@@ -121,7 +119,7 @@ func (m *ReuseManager) cleanupLocked(now time.Time) {
m.entries = kept
}
func (m *ReuseManager) release(entry *reuseEntry) {
func (entry *reuseEntry) release() {
if entry == nil {
return
}
@@ -163,11 +161,14 @@ func (m *ReuseManager) pickLocked() *reuseEntry {
return best
}
func (m *ReuseManager) canCreateLocked() bool {
if m.maxConnections <= 0 {
func (m *ReuseManager) shouldCreateLocked() bool {
if len(m.entries) == 0 {
return true
}
return len(m.entries) < m.maxConnections
if m.maxConnections > 0 {
return len(m.entries) < m.maxConnections
}
return false
}
func (m *ReuseManager) newEntryLocked(transport http.RoundTripper, now time.Time) *reuseEntry {
@@ -189,7 +190,7 @@ func (m *ReuseManager) newEntryLocked(transport http.RoundTripper, now time.Time
return entry
}
func (m *ReuseManager) GetTransport() (*ReuseTransport, error) {
func (m *ReuseManager) GetTransport() http.RoundTripper {
now := time.Now()
m.mu.Lock()
@@ -197,13 +198,13 @@ func (m *ReuseManager) GetTransport() (*ReuseTransport, error) {
m.cleanupLocked(now)
entry := m.pickLocked()
var entry *reuseEntry
if !m.shouldCreateLocked() {
entry = m.pickLocked()
}
reused := entry != nil
if entry == nil {
if !m.canCreateLocked() {
return nil, fmt.Errorf("manager: no available connection")
}
transport := m.maker()
entry = m.newEntryLocked(transport, now)
}
@@ -217,5 +218,5 @@ func (m *ReuseManager) GetTransport() (*ReuseTransport, error) {
entry.leftRequests.Add(-1)
}
return &ReuseTransport{entry: entry, manager: m}, nil
return &ReuseTransport{entry: entry}
}
+43 -68
View File
@@ -23,8 +23,8 @@ func makeTestTransportFactory(counter *atomic.Int64) TransportMaker {
}
}
func transportID(rt http.RoundTripper) int64 {
return rt.(*testRoundTripper).id
func transportID(rt *ReuseTransport) int64 {
return rt.entry.transport.(*testRoundTripper).id
}
func TestManagerReuseSameEntry(t *testing.T) {
@@ -39,19 +39,13 @@ func TestManagerReuseSameEntry(t *testing.T) {
t.Fatal(err)
}
transport1, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
id1 := transportID(transport1.entry.transport)
transport1 := manager.GetTransport().(*ReuseTransport)
id1 := transportID(transport1)
transport1.Close()
transport2, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
id2 := transportID(transport2.entry.transport)
transport2 := manager.GetTransport().(*ReuseTransport)
id2 := transportID(transport2)
if id1 != id2 {
t.Fatalf("expected same transport to be reused, got %d and %d", id1, id2)
@@ -65,7 +59,7 @@ func TestManagerRespectMaxConnections(t *testing.T) {
var created atomic.Int64
manager, err := NewReuseManager(&ReuseConfig{
MaxConcurrency: "1",
MaxConcurrency: "2",
MaxConnections: "2",
HMaxRequestTimes: "100",
}, makeTestTransportFactory(&created))
@@ -73,36 +67,38 @@ func TestManagerRespectMaxConnections(t *testing.T) {
t.Fatal(err)
}
transport1, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
if transport1 == nil {
t.Fatal("expected first entry")
transport1 := manager.GetTransport().(*ReuseTransport)
id1 := transportID(transport1)
transport2 := manager.GetTransport().(*ReuseTransport)
id2 := transportID(transport2)
transport3 := manager.GetTransport().(*ReuseTransport)
id3 := transportID(transport3)
transport4 := manager.GetTransport().(*ReuseTransport)
id4 := transportID(transport4)
transport5 := manager.GetTransport().(*ReuseTransport)
id5 := transportID(transport5)
if id1 == id2 {
t.Fatal("expected the second transport to be new")
}
transport2, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
if transport2 == nil {
t.Fatal("expected second entry")
if id3 != id1 && id3 != id2 {
t.Fatal("expected the third transport to be reused")
}
if transport1.entry == transport2.entry {
t.Fatal("expected different entries for first two allocations")
if id4 != id1 && id4 != id2 {
t.Fatal("expected the fourth transport to be reused")
}
transport3, err := manager.GetTransport()
if err == nil {
t.Fatal("expected error when max-connections reached and all entries are at max-concurrency")
}
if transport3 != nil {
t.Fatal("expected nil entry on allocation failure")
if id5 == id1 || id5 == id2 {
t.Fatal("expected the fifth transport to be new")
}
transport1.Close()
transport2.Close()
transport3.Close()
transport4.Close()
transport5.Close()
manager.Close()
}
@@ -118,19 +114,13 @@ func TestManagerRotateOnRequestLimit(t *testing.T) {
t.Fatal(err)
}
transport1, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
id1 := transportID(transport1.entry.transport)
transport1 := manager.GetTransport().(*ReuseTransport)
id1 := transportID(transport1)
transport1.Close()
transport2, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
id2 := transportID(transport2.entry.transport)
transport2 := manager.GetTransport().(*ReuseTransport)
id2 := transportID(transport2)
if id1 == id2 {
t.Fatalf("expected new transport after request limit, got same id %d", id1)
@@ -153,20 +143,14 @@ func TestManagerRotateOnReusableSecs(t *testing.T) {
t.Fatal(err)
}
transport1, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
id1 := transportID(transport1.entry.transport)
transport1 := manager.GetTransport().(*ReuseTransport)
id1 := transportID(transport1)
time.Sleep(1100 * time.Millisecond)
transport1.Close()
transport2, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
id2 := transportID(transport2.entry.transport)
transport2 := manager.GetTransport().(*ReuseTransport)
id2 := transportID(transport2)
if id1 == id2 {
t.Fatalf("expected new transport after reusable timeout, got same id %d", id1)
@@ -189,19 +173,13 @@ func TestManagerRotateOnConnReuseLimit(t *testing.T) {
t.Fatal(err)
}
transport1, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
id1 := transportID(transport1.entry.transport)
transport1 := manager.GetTransport().(*ReuseTransport)
id1 := transportID(transport1)
transport1.Close()
transport2, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
id2 := transportID(transport2.entry.transport)
transport2 := manager.GetTransport().(*ReuseTransport)
id2 := transportID(transport2)
if id1 != id2 {
t.Fatalf("expected first reuse to use same transport, got %d and %d", id1, id2)
@@ -209,11 +187,8 @@ func TestManagerRotateOnConnReuseLimit(t *testing.T) {
transport2.Close()
transport3, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
id3 := transportID(transport3.entry.transport)
transport3 := manager.GetTransport().(*ReuseTransport)
id3 := transportID(transport3)
if id3 == id2 {
t.Fatalf("expected new transport after c-max-reuse-times limit, got same id %d", id3)
+12 -5
View File
@@ -80,9 +80,9 @@ type httpSession struct {
once sync.Once
}
func newHTTPSession() *httpSession {
func newHTTPSession(maxPackets int) *httpSession {
return &httpSession{
uploadQueue: NewUploadQueue(),
uploadQueue: NewUploadQueue(maxPackets),
connected: make(chan struct{}),
}
}
@@ -100,6 +100,7 @@ type requestHandler struct {
scMaxEachPostBytes Range
scStreamUpServerSecs Range
scMaxBufferedPosts Range
mu sync.Mutex
sessions map[string]*httpSession
@@ -114,6 +115,10 @@ func NewServerHandler(opt ServerOption) (http.Handler, error) {
if err != nil {
return nil, err
}
scMaxBufferedPosts, err := opt.Config.GetNormalizedScMaxBufferedPosts()
if err != nil {
return nil, err
}
// using h2c.NewHandler to ensure we can work in plain http2
// and some tls conn is not *tls.Conn (like *reality.Conn)
return h2c.NewHandler(&requestHandler{
@@ -122,6 +127,7 @@ func NewServerHandler(opt ServerOption) (http.Handler, error) {
httpHandler: opt.HttpHandler,
scMaxEachPostBytes: scMaxEachPostBytes,
scStreamUpServerSecs: scStreamUpServerSecs,
scMaxBufferedPosts: scMaxBufferedPosts,
sessions: map[string]*httpSession{},
}, &http.Http2Server{
IdleTimeout: 30 * time.Second,
@@ -137,7 +143,7 @@ func (h *requestHandler) getOrCreateSession(sessionID string) *httpSession {
return s
}
s = newHTTPSession()
s = newHTTPSession(h.scMaxBufferedPosts.Max)
h.sessions[sessionID] = s
return s
}
@@ -371,10 +377,11 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
if err := session.uploadQueue.Push(Packet{
err = session.uploadQueue.Push(Packet{
Seq: seq,
Payload: body,
}); err != nil {
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
+34 -14
View File
@@ -13,20 +13,24 @@ type Packet struct {
}
type UploadQueue struct {
mu sync.Mutex
cond *sync.Cond
packets map[uint64][]byte
nextSeq uint64
buf []byte
closed bool
reader io.ReadCloser
mu sync.Mutex
condPushed sync.Cond
condPopped sync.Cond
packets map[uint64][]byte
nextSeq uint64
buf []byte
closed bool
maxPackets int
reader io.ReadCloser
}
func NewUploadQueue() *UploadQueue {
func NewUploadQueue(maxPackets int) *UploadQueue {
q := &UploadQueue{
packets: make(map[uint64][]byte),
packets: make(map[uint64][]byte, maxPackets),
maxPackets: maxPackets,
}
q.cond = sync.NewCond(&q.mu)
q.condPushed = sync.Cond{L: &q.mu}
q.condPopped = sync.Cond{L: &q.mu}
return q
}
@@ -44,12 +48,19 @@ func (q *UploadQueue) Push(p Packet) error {
if p.Reader != nil {
q.reader = p.Reader
q.cond.Broadcast()
q.condPushed.Broadcast()
return nil
}
for len(q.packets) > q.maxPackets {
q.condPopped.Wait() // wait for the reader to read the packets
if q.closed {
return io.ErrClosedPipe
}
}
q.packets[p.Seq] = p.Payload
q.cond.Broadcast()
q.condPushed.Broadcast()
return nil
}
@@ -68,6 +79,7 @@ func (q *UploadQueue) Read(b []byte) (int, error) {
delete(q.packets, q.nextSeq)
q.nextSeq++
q.buf = payload
q.condPopped.Broadcast()
continue
}
@@ -81,7 +93,14 @@ func (q *UploadQueue) Read(b []byte) (int, error) {
return 0, io.EOF
}
q.cond.Wait()
if len(q.packets) > q.maxPackets {
q.mu.Unlock()
// the "reassembly buffer" is too large, and we want to constrain memory usage somehow.
// let's tear down the connection and hope the application retries.
return 0, errors.New("packet queue is too large")
}
q.condPushed.Wait()
}
}
@@ -94,6 +113,7 @@ func (q *UploadQueue) Close() error {
err = q.reader.Close()
}
q.closed = true
q.cond.Broadcast()
q.condPushed.Broadcast()
q.condPopped.Broadcast()
return err
}
@@ -104,6 +104,22 @@
"settings_nyanpasu_logs": "Log Settings",
"settings_nyanpasu_max_log_files_label": "Max Log Files",
"settings_nyanpasu_app_log_level_label": "App Log Level",
"settings_nyanpasu_tray": "System Tray",
"settings_nyanpasu_tray_type": "Tray Menu Type",
"settings_nyanpasu_tray_type_normal": "Normal Menu",
"settings_nyanpasu_tray_type_hidden": "Hidden Menu",
"settings_nyanpasu_tray_type_submenu": "Submenu",
"settings_nyanpasu_tray_icon": "Tray Icon",
"settings_nyanpasu_tray_icon_normal": "No Proxy",
"settings_nyanpasu_tray_icon_tun": "TUN Mode",
"settings_nyanpasu_tray_icon_system_proxy": "System Proxy",
"settings_nyanpasu_tray_icon_edit": "Edit Icon",
"settings_nyanpasu_tray_icon_reset": "Reset",
"settings_nyanpasu_tray_icon_set_failed": "Failed to set tray icon, please try again",
"settings_nyanpasu_tray_icon_set_success": "Tray icon set successfully",
"settings_nyanpasu_tray_icon_reset_failed": "Failed to reset tray icon, please try again",
"settings_nyanpasu_tray_icon_reset_success": "Tray icon reset successfully",
"settings_nyanpasu_tray_icon_loading": "Setting tray icon...",
"settings_label_system": "System Settings",
"settings_label_system_description": "Proxy mode, proxy bypass, auto start, silent start and more.",
"settings_label_user_interface": "User Interface",
@@ -104,6 +104,22 @@
"settings_nyanpasu_logs": "Настройки журналов",
"settings_nyanpasu_max_log_files_label": "Максимальное количество файлов журнала",
"settings_nyanpasu_app_log_level_label": "Уровень логирования приложения",
"settings_nyanpasu_tray": "Системный трей",
"settings_nyanpasu_tray_type": "Тип меню трея",
"settings_nyanpasu_tray_type_normal": "Обычное меню",
"settings_nyanpasu_tray_type_hidden": "Скрытое меню",
"settings_nyanpasu_tray_type_submenu": "Подменю",
"settings_nyanpasu_tray_icon": "Иконка трея",
"settings_nyanpasu_tray_icon_normal": "Без прокси",
"settings_nyanpasu_tray_icon_tun": "TUN режим",
"settings_nyanpasu_tray_icon_system_proxy": "Системный прокси",
"settings_nyanpasu_tray_icon_edit": "Изменить иконку",
"settings_nyanpasu_tray_icon_reset": "Сбросить по умолчанию",
"settings_nyanpasu_tray_icon_set_failed": "Не удалось установить иконку трея",
"settings_nyanpasu_tray_icon_set_success": "Иконка трея успешно установлена",
"settings_nyanpasu_tray_icon_reset_failed": "Не удалось сбросить иконку трея",
"settings_nyanpasu_tray_icon_reset_success": "Иконка трея успешно сброшена",
"settings_nyanpasu_tray_icon_loading": "Установка иконки трея...",
"settings_label_system": "Системные настройки",
"settings_label_system_description": "Режим прокси, обход прокси, автозапуск, старт без отображения окна и т.д.",
"settings_label_user_interface": "Интерфейс пользователя",
@@ -104,6 +104,22 @@
"settings_nyanpasu_logs": "日志设置",
"settings_nyanpasu_max_log_files_label": "最大日志文件数量",
"settings_nyanpasu_app_log_level_label": "应用日志级别",
"settings_nyanpasu_tray": "系统托盘",
"settings_nyanpasu_tray_type": "托盘菜单类型",
"settings_nyanpasu_tray_type_normal": "一级菜单",
"settings_nyanpasu_tray_type_hidden": "隐藏菜单",
"settings_nyanpasu_tray_type_submenu": "子菜单",
"settings_nyanpasu_tray_icon": "托盘图标",
"settings_nyanpasu_tray_icon_normal": "无代理",
"settings_nyanpasu_tray_icon_tun": "TUN 模式",
"settings_nyanpasu_tray_icon_system_proxy": "系统代理",
"settings_nyanpasu_tray_icon_edit": "更换图标",
"settings_nyanpasu_tray_icon_reset": "重置默认",
"settings_nyanpasu_tray_icon_set_failed": "设置托盘图标失败",
"settings_nyanpasu_tray_icon_set_success": "设置托盘图标成功",
"settings_nyanpasu_tray_icon_reset_failed": "重置托盘图标失败",
"settings_nyanpasu_tray_icon_reset_success": "重置托盘图标成功",
"settings_nyanpasu_tray_icon_loading": "正在设置托盘图标...",
"settings_label_system": "系统设置",
"settings_label_system_description": "代理模式、代理绕过、开机自启、静默启动等设置",
"settings_label_user_interface": "用户界面",
@@ -104,6 +104,22 @@
"settings_nyanpasu_logs": "日誌設置",
"settings_nyanpasu_max_log_files_label": "最大日誌文件數量",
"settings_nyanpasu_app_log_level_label": "應用日誌級別",
"settings_nyanpasu_tray": "系統托盤",
"settings_nyanpasu_tray_type": "托盤菜單類型",
"settings_nyanpasu_tray_type_normal": "一级菜單",
"settings_nyanpasu_tray_type_hidden": "隱藏菜單",
"settings_nyanpasu_tray_type_submenu": "子菜單",
"settings_nyanpasu_tray_icon": "托盤圖標",
"settings_nyanpasu_tray_icon_normal": "無代理",
"settings_nyanpasu_tray_icon_tun": "TUN 模式",
"settings_nyanpasu_tray_icon_system_proxy": "系統代理",
"settings_nyanpasu_tray_icon_edit": "更換圖標",
"settings_nyanpasu_tray_icon_reset": "重置默認",
"settings_nyanpasu_tray_icon_set_failed": "設置托盤圖標失敗",
"settings_nyanpasu_tray_icon_reset_success": "重置托盤圖標成功",
"settings_nyanpasu_tray_icon_reset_failed": "重置托盤圖標失敗",
"settings_nyanpasu_tray_icon_set_success": "設置托盤圖標成功",
"settings_nyanpasu_tray_icon_loading": "正在設置托盤圖標...",
"settings_label_system": "系統設置",
"settings_label_system_description": "代理模式、代理繞過、開機自啟、靜默啟動等設定",
"settings_label_user_interface": "使用者介面",
@@ -11,6 +11,7 @@ import { useControllableState } from '@radix-ui/react-use-controllable-state'
const MotionContent = ({
children,
className,
style,
...props
}: ComponentProps<typeof motion.div>) => {
return (
@@ -24,6 +25,7 @@ const MotionContent = ({
)}
style={{
maxHeight: 'var(--radix-popper-available-height)',
...style,
}}
initial={{
opacity: 0,
@@ -52,8 +54,11 @@ const MotionContent = ({
)
}
type WidthType = 'auto' | 'full'
const DropdownMenuContext = createContext<{
open: boolean
width: WidthType
} | null>(null)
const useDropdownMenuContext = () => {
@@ -69,11 +74,14 @@ const useDropdownMenuContext = () => {
}
export const DropdownMenu = ({
width = 'auto',
open: inputOpen,
defaultOpen,
onOpenChange,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Root>) => {
}: ComponentProps<typeof DropdownMenuPrimitive.Root> & {
width?: WidthType
}) => {
const [open, setOpen] = useControllableState({
prop: inputOpen,
defaultProp: defaultOpen ?? false,
@@ -81,7 +89,12 @@ export const DropdownMenu = ({
})
return (
<DropdownMenuContext.Provider value={{ open }}>
<DropdownMenuContext.Provider
value={{
open,
width,
}}
>
<DropdownMenuPrimitive.Root
{...props}
open={open}
@@ -228,14 +241,24 @@ export const DropdownMenuContent = ({
className,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Content>) => {
const { open } = useDropdownMenuContext()
const { open, width } = useDropdownMenuContext()
return (
<AnimatePresence initial={false}>
{open && (
<DropdownMenuPrimitive.Portal forceMount>
<DropdownMenuPrimitive.Content {...props} asChild>
<MotionContent className={className}>{children}</MotionContent>
<MotionContent
className={className}
style={{
width:
width === 'full'
? 'var(--radix-popper-anchor-width)'
: undefined,
}}
>
{children}
</MotionContent>
</DropdownMenuPrimitive.Content>
</DropdownMenuPrimitive.Portal>
)}
@@ -2,9 +2,9 @@ import { ComponentProps, useMemo } from 'react'
import { useServerPort } from '@nyanpasu/interface'
import { LazyImage } from '@nyanpasu/ui'
export default function Image({
export function CacheImage({
icon,
...porps
...props
}: Omit<ComponentProps<typeof LazyImage>, 'src'> & {
icon: string
}) {
@@ -22,5 +22,20 @@ export default function Image({
return `http://localhost:${serverPort}/cache/icon?url=${btoa(src)}`
}, [src, serverPort])
return <LazyImage src={cachedUrl} {...porps} />
return <LazyImage src={cachedUrl} {...props} />
}
export function TrayImage({
mode,
version,
...props
}: Omit<ComponentProps<typeof LazyImage>, 'src'> & {
mode: 'system_proxy' | 'tun' | 'normal'
version?: number
}) {
const serverPort = useServerPort()
const src = `http://localhost:${serverPort}/tray/icon?mode=${mode}${version !== undefined ? `&v=${version}` : ''}`
return <LazyImage src={src} {...props} />
}
@@ -223,7 +223,8 @@ export type SelectValuePlaceholderLegendVariants = VariantProps<
export const selectContentVariants = cva(
[
'relative w-full overflow-auto rounded shadow-container z-50',
'relative w-full overflow-auto rounded z-50',
'shadow shadow-zinc-300 dark:shadow-zinc-900',
'bg-inverse-on-surface dark:bg-surface',
'dark:text-on-surface',
],
@@ -464,9 +465,21 @@ export const SelectContent = ({
width: 'var(--radix-popper-anchor-width)',
maxHeight: 'var(--radix-popper-available-height)',
}}
initial={{ opacity: 0, scaleY: 0.9, transformOrigin: 'top' }}
animate={{ opacity: 1, scaleY: 1, transformOrigin: 'top' }}
exit={{ opacity: 0, scaleY: 0.9, transformOrigin: 'top' }}
initial={{
opacity: 0,
scaleY: 0.9,
transformOrigin: 'var(--radix-popper-transform-origin)',
}}
animate={{
opacity: 1,
scaleY: 1,
transformOrigin: 'var(--radix-popper-transform-origin)',
}}
exit={{
opacity: 0,
scaleY: 0.9,
transformOrigin: 'var(--radix-popper-transform-origin)',
}}
transition={{
type: 'spring',
bounce: 0,
@@ -1,3 +1,4 @@
import { motion, type Transition } from 'framer-motion'
import { Tooltip as TooltipPrimitive } from 'radix-ui'
import * as React from 'react'
import { cn } from '@nyanpasu/ui'
@@ -33,37 +34,27 @@ export function TooltipTrigger({
export function TooltipContent({
className,
sideOffset = 0,
sideOffset = 4,
children,
disableArrow = false,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content> & {
disableArrow?: boolean
}) {
}: React.ComponentProps<typeof TooltipPrimitive.Content> & {}) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
'bg-surface-variant text-on-surface',
'z-50 w-fit min-w-12 text-center',
'rounded-full px-3 py-1.5 text-xs text-balance',
'shadow-outline/30 dark:shadow-surface-variant/20 shadow-sm',
'dark:text-on-surface',
'bg-mixed-background/30',
'backdrop-blur',
'dark:shadow-inverse-on-surface/50 shadow-inverse-surface/30 shadow-sm',
className,
)}
{...props}
>
{children}
{!disableArrow && (
<TooltipPrimitive.Arrow
className={cn(
'fill-surface-variant z-50',
'h-2.5 w-4 translate-y-[-6px] rounded-xl',
)}
/>
)}
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
@@ -1,5 +1,5 @@
import { Button } from '@/components/ui/button'
import Image from '@/components/ui/image'
import { CacheImage } from '@/components/ui/image'
import { useClashProxies } from '@nyanpasu/interface'
import { cn } from '@nyanpasu/ui'
import { Link, useLocation } from '@tanstack/react-router'
@@ -40,7 +40,7 @@ export default function ProxiesNavigate() {
<div className="flex items-center gap-2.5">
{group.icon && (
<div className="size-8">
<Image
<CacheImage
icon={group.icon}
className="size-8"
loadingClassName="rounded-full"
@@ -1,5 +1,5 @@
import { useMemo } from 'react'
import Image from '@/components/ui/image'
import { CacheImage } from '@/components/ui/image'
import { useClashProxies } from '@nyanpasu/interface'
import { cn } from '@nyanpasu/ui'
@@ -15,7 +15,11 @@ export default function ProxyIcon({ groupName }: { groupName: string }) {
}, [groupName, proxies])
return icon ? (
<Image className="size-6" loadingClassName="rounded-full" icon={icon} />
<CacheImage
className="size-6"
loadingClassName="rounded-full"
icon={icon}
/>
) : (
<div
className={cn(
@@ -0,0 +1,194 @@
import ArrowForwardIosRounded from '~icons/material-symbols/arrow-forward-ios-rounded'
import DeviceResetRounded from '~icons/material-symbols/device-reset-rounded'
import { AnimatePresence, motion } from 'framer-motion'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { TrayImage } from '@/components/ui/image'
import { CircularProgress } from '@/components/ui/progress'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { useLockFn } from '@/hooks/use-lock-fn'
import { m } from '@/paraglide/messages'
import { message } from '@/utils/notification'
import { commands, unwrapResult } from '@nyanpasu/interface'
import { cn } from '@nyanpasu/ui'
import { useQuery } from '@tanstack/react-query'
import { open } from '@tauri-apps/plugin-dialog'
import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card'
enum TrayIconMode {
normal = 'normal',
tun = 'tun',
system_proxy = 'system_proxy',
}
const TrayIconItem = ({ mode }: { mode: TrayIconMode }) => {
const [iconVersion, setIconVersion] = useState(0)
const isIconSet = useQuery({
queryKey: ['trayIcon', mode],
queryFn: async () => {
const path = await commands.isTrayIconSet(mode)
const result = unwrapResult(path)
return result !== null
},
})
const [isLoading, setIsLoading] = useState(false)
const handleChangeIcon = useLockFn(async () => {
try {
const selected = await open({
directory: false,
multiple: false,
filters: [
{
name: 'Images',
extensions: ['png', 'jpg', 'jpeg', 'bmp', 'ico'],
},
],
})
if (Array.isArray(selected)) {
throw new Error('Not Support')
} else if (selected === null) {
return null
}
setIsLoading(true)
await commands.setTrayIcon(mode, selected)
await isIconSet.refetch()
setIconVersion((prev) => prev + 1)
message(m.settings_nyanpasu_tray_icon_set_success(), {
kind: 'info',
})
} catch (e) {
console.error(e)
message(m.settings_nyanpasu_tray_icon_set_failed(), {
kind: 'error',
})
} finally {
setIsLoading(false)
}
})
const handleResetIcon = useLockFn(async () => {
try {
// null means reset
await commands.setTrayIcon(mode, null)
await isIconSet.refetch()
setIconVersion((prev) => prev + 1)
message(m.settings_nyanpasu_tray_icon_reset_success(), {
kind: 'info',
})
} catch (e) {
console.error(e)
message(m.settings_nyanpasu_tray_icon_reset_failed(), {
kind: 'error',
})
}
})
const messages = {
[TrayIconMode.normal]: m.settings_nyanpasu_tray_icon_normal(),
[TrayIconMode.tun]: m.settings_nyanpasu_tray_icon_tun(),
[TrayIconMode.system_proxy]: m.settings_nyanpasu_tray_icon_system_proxy(),
}
return (
<SettingsCard
className="relative"
data-mode={mode}
data-is-set={isIconSet.data}
>
<AnimatePresence initial={false}>
{isLoading && (
<motion.div
data-slot="core-manager-card-mask"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={cn(
'bg-primary/10 absolute inset-0 z-50 backdrop-blur-3xl',
'flex items-center justify-center gap-4',
)}
>
<CircularProgress className="size-8" indeterminate />
<p className="text-sm">{m.settings_nyanpasu_tray_icon_loading()}</p>
</motion.div>
)}
</AnimatePresence>
<SettingsCardContent className="flex-row items-center" asChild>
<Button
className="text-on-surface! h-auto w-full rounded-none px-5 text-left text-base"
onClick={handleChangeIcon}
>
<TrayImage className="size-12" mode={mode} version={iconVersion} />
<div className="flex-1 text-base font-semibold">{messages[mode]}</div>
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="raised"
className="hover:bg-inverse-on-surface"
icon
onClick={(e) => {
e.stopPropagation()
handleResetIcon()
}}
asChild
>
<span>
<DeviceResetRounded />
</span>
</Button>
</TooltipTrigger>
<TooltipContent>
{m.settings_nyanpasu_tray_icon_reset()}
</TooltipContent>
</Tooltip>
<span className="text-sm">
{m.settings_nyanpasu_tray_icon_edit()}
</span>
<ArrowForwardIosRounded />
</div>
</Button>
</SettingsCardContent>
</SettingsCard>
)
}
export default function TrayIconConfig() {
return Object.values(TrayIconMode).map((mode) => (
<TrayIconItem key={mode} mode={mode} />
))
// return (
// <SettingsCard>
// {/* <SettingsCardContent className="gap-4">
// <div className="flex items-center justify-between">
// <span>{m.settings_nyanpasu_tray_icon()}</span>
// </div>
// </SettingsCardContent> */}
// <SettingsCardContent className="grid grid-cols-1 gap-3 pb-5 sm:grid-cols-3">
// {Object.values(TrayIconMode).map((mode) => (
// <TrayIconItem key={mode} mode={mode} />
// ))}
// </SettingsCardContent>
// </SettingsCard>
// )
}
@@ -0,0 +1,73 @@
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useLockFn } from '@/hooks/use-lock-fn'
import { m } from '@/paraglide/messages'
import { ArrowForwardIosRounded } from '@mui/icons-material'
import { ProxiesSelectorMode, useSetting } from '@nyanpasu/interface'
import {
ItemContainer,
ItemLabel,
ItemLabelDescription,
ItemLabelText,
SettingsCard,
SettingsCardContent,
} from '../../_modules/settings-card'
export default function TrayProxiesSelector() {
const { value, upsert } = useSetting('clash_tray_selector')
const handleChange = useLockFn(async (mode: ProxiesSelectorMode) => {
await upsert(mode)
})
const messages = {
normal: m.settings_nyanpasu_tray_type_normal(),
hidden: m.settings_nyanpasu_tray_type_hidden(),
submenu: m.settings_nyanpasu_tray_type_submenu(),
} satisfies Record<ProxiesSelectorMode, string>
return (
<SettingsCard data-slot="tray-proxies-selector">
<DropdownMenu width="full">
<DropdownMenuTrigger asChild>
<SettingsCardContent
data-slot="tray-proxies-selector-trigger"
asChild
>
<Button className="text-on-surface! h-auto w-full rounded-none px-5 text-left text-base">
<ItemContainer>
<ItemLabel>
<ItemLabelText>
{m.settings_nyanpasu_tray_type()}
</ItemLabelText>
<ItemLabelDescription>
{value ? messages[value] : null}
</ItemLabelDescription>
</ItemLabel>
<ArrowForwardIosRounded />
</ItemContainer>
</Button>
</SettingsCardContent>
</DropdownMenuTrigger>
<DropdownMenuContent>
{Object.entries(messages).map(([key, value]) => (
<DropdownMenuItem
key={key}
onSelect={() => handleChange(key as ProxiesSelectorMode)}
>
{value}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</SettingsCard>
)
}
@@ -9,12 +9,14 @@ import {
import { SettingsTitle } from '../_modules/settings-title'
import LogFileConfig from './_modules/log-file-config'
import LogLevelSelector from './_modules/log-level-selector'
import TrayIconConfig from './_modules/tray-icon-config'
import TrayProxiesSelector from './_modules/tray-proxies-selector'
export const Route = createFileRoute('/(main)/main/settings/nyanpasu')({
component: RouteComponent,
})
const AppSettings = () => {
const LogSettings = () => {
return (
<div data-slot="app-settings-container">
<SettingsLabel>{m.settings_nyanpasu_logs()}</SettingsLabel>
@@ -32,13 +34,29 @@ const AppSettings = () => {
)
}
const TraySettings = () => {
return (
<div data-slot="app-settings-container">
<SettingsLabel>{m.settings_nyanpasu_tray()}</SettingsLabel>
<SettingsGroup>
<TrayProxiesSelector />
<TrayIconConfig />
</SettingsGroup>
</div>
)
}
function RouteComponent() {
return (
<>
<SettingsTitle>{m.settings_label_nyanpasu()}</SettingsTitle>
<div className="space-y-4 px-4 pb-4">
<AppSettings />
<LogSettings />
<TraySettings />
</div>
</>
)
+4 -4
View File
@@ -2,10 +2,10 @@
"manifest_version": 1,
"latest": {
"mihomo": "v1.19.23",
"mihomo_alpha": "alpha-0495d29",
"clash_rs": "v0.9.6",
"mihomo_alpha": "Not Found",
"clash_rs": "v0.9.7",
"clash_premium": "2023-09-05-gdcc8d87",
"clash_rs_alpha": "0.9.6-alpha+sha.1f9db91"
"clash_rs_alpha": "0.9.7-alpha+sha.1b5afe6"
},
"arch_template": {
"mihomo": {
@@ -69,5 +69,5 @@
"linux-armv7hf": "clash-rs-armv7-unknown-linux-gnueabihf"
}
},
"updated_at": "2026-04-12T22:23:34.622Z"
"updated_at": "2026-04-13T22:29:12.908Z"
}
@@ -788,7 +788,7 @@
<GIC_SPI 216 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 217 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "ring0", "ring1", "ring2", "ring3";
status = "okay";
status = "disabled";
};
afe: audio-controller@11210000 {
@@ -1498,7 +1498,7 @@
<GIC_SPI 216 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 217 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "ring0", "ring1", "ring2", "ring3";
status = "okay";
status = "disabled";
};
};
@@ -1654,7 +1654,7 @@
<GIC_SPI 216 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 217 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "ring0", "ring1", "ring2", "ring3";
status = "okay";
status = "disabled";
};
};
@@ -228,7 +228,7 @@ Work-in-progress patch to complete mt7988a.dtsi
+ <GIC_SPI 216 IRQ_TYPE_LEVEL_HIGH>,
+ <GIC_SPI 217 IRQ_TYPE_LEVEL_HIGH>;
+ interrupt-names = "ring0", "ring1", "ring2", "ring3";
+ status = "okay";
+ status = "disabled";
+ };
};
+3 -2
View File
@@ -95,8 +95,9 @@ func (ms *mieruServer) Start() error {
if err != nil {
return err
}
ms.mux.SetTrafficPattern(trafficPattern)
ms.mux.SetServerUsers(appctlcommon.UserListToMap(ms.config.Config.GetUsers()))
ms.mux.SetTrafficPattern(trafficPattern).
SetServerUsers(appctlcommon.UserListToMap(ms.config.Config.GetUsers())).
SetServerUserHintIsMandatory(ms.config.Config.GetAdvancedSettings().GetUserHintIsMandatory())
mtu := common.DefaultMTU
if ms.config.Config.GetMtu() != 0 {
mtu = int(ms.config.Config.GetMtu())
+17 -5
View File
@@ -250,8 +250,11 @@ type ServerAdvancedSettings struct {
// Examples: 30s, 5m, 2h.
// If empty, the default interval is used.
MetricsLoggingInterval *string `protobuf:"bytes,2,opt,name=metricsLoggingInterval,proto3,oneof" json:"metricsLoggingInterval,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
// If true, proxy clients must provide user hint,
// otherwise proxy server will refuse to decrypt the network packets.
UserHintIsMandatory *bool `protobuf:"varint,3,opt,name=userHintIsMandatory,proto3,oneof" json:"userHintIsMandatory,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ServerAdvancedSettings) Reset() {
@@ -291,6 +294,13 @@ func (x *ServerAdvancedSettings) GetMetricsLoggingInterval() string {
return ""
}
func (x *ServerAdvancedSettings) GetUserHintIsMandatory() bool {
if x != nil && x.UserHintIsMandatory != nil {
return *x.UserHintIsMandatory
}
return false
}
type Egress struct {
state protoimpl.MessageState `protogen:"open.v1"`
// A list of proxies.
@@ -567,10 +577,12 @@ const file_appctl_proto_servercfg_proto_rawDesc = "" +
"\x04_mtuB\t\n" +
"\a_egressB\x06\n" +
"\x04_dnsB\x11\n" +
"\x0f_trafficPattern\"v\n" +
"\x0f_trafficPattern\"\xc5\x01\n" +
"\x16ServerAdvancedSettings\x12;\n" +
"\x16metricsLoggingInterval\x18\x02 \x01(\tH\x00R\x16metricsLoggingInterval\x88\x01\x01B\x19\n" +
"\x17_metricsLoggingIntervalJ\x04\b\x01\x10\x02\"m\n" +
"\x16metricsLoggingInterval\x18\x02 \x01(\tH\x00R\x16metricsLoggingInterval\x88\x01\x01\x125\n" +
"\x13userHintIsMandatory\x18\x03 \x01(\bH\x01R\x13userHintIsMandatory\x88\x01\x01B\x19\n" +
"\x17_metricsLoggingIntervalB\x16\n" +
"\x14_userHintIsMandatoryJ\x04\b\x01\x10\x02\"m\n" +
"\x06Egress\x123\n" +
"\aproxies\x18\x01 \x03(\v2\x19.mieru.appctl.EgressProxyR\aproxies\x12.\n" +
"\x05rules\x18\x02 \x03(\v2\x18.mieru.appctl.EgressRuleR\x05rules\"\xa4\x02\n" +
+4
View File
@@ -57,6 +57,10 @@ message ServerAdvancedSettings {
// Examples: 30s, 5m, 2h.
// If empty, the default interval is used.
optional string metricsLoggingInterval = 2;
// If true, proxy clients must provide user hint,
// otherwise proxy server will refuse to decrypt the network packets.
optional bool userHintIsMandatory = 3;
}
message Egress {
+5 -1
View File
@@ -123,7 +123,8 @@ func (s *serverManagementService) Start(ctx context.Context, req *emptypb.Empty)
}
mux := protocol.NewMux(false).
SetTrafficPattern(trafficPattern).
SetServerUsers(appctlcommon.UserListToMap(config.GetUsers()))
SetServerUsers(appctlcommon.UserListToMap(config.GetUsers())).
SetServerUserHintIsMandatory(config.GetAdvancedSettings().GetUserHintIsMandatory())
mtu := common.DefaultMTU
if config.GetMtu() != 0 {
mtu = int(config.GetMtu())
@@ -253,6 +254,9 @@ func (s *serverManagementService) Reload(ctx context.Context, req *emptypb.Empty
// Adjust users.
mux.SetServerUsers(appctlcommon.UserListToMap(config.GetUsers()))
// Adjust advanced settings: user hint is mandatory.
mux.SetServerUserHintIsMandatory(config.GetAdvancedSettings().GetUserHintIsMandatory())
}
log.Infof("completed Reload request from RPC caller")
return &emptypb.Empty{}, nil
+2 -1
View File
@@ -491,7 +491,8 @@ var serverRunFunc = func(s []string) error {
}
mux := protocol.NewMux(false).
SetTrafficPattern(trafficPattern).
SetServerUsers(appctlcommon.UserListToMap(config.GetUsers()))
SetServerUsers(appctlcommon.UserListToMap(config.GetUsers())).
SetServerUserHintIsMandatory(config.GetAdvancedSettings().GetUserHintIsMandatory())
appctl.SetServerMuxRef(mux)
mtu := common.DefaultMTU
if config.GetMtu() != 0 {
+24 -9
View File
@@ -70,7 +70,8 @@ type Mux struct {
multiplexFactor int
// ---- server only fields ----
users map[string]*appctlpb.User
users map[string]*appctlpb.User
userHintIsMandatory bool
}
var _ net.Listener = &Mux{}
@@ -263,6 +264,18 @@ func (m *Mux) SetServerUsers(users map[string]*appctlpb.User) *Mux {
return m
}
// SetServerUserHintIsMandatory sets whether the user hint is mandatory.
func (m *Mux) SetServerUserHintIsMandatory(userHintIsMandatory bool) *Mux {
m.mu.Lock()
defer m.mu.Unlock()
if m.isClient {
panic("Can't set server user hint is mandatory in client mux")
}
m.userHintIsMandatory = userHintIsMandatory
log.Infof("Mux user hint is mandatory is set to %v", m.userHintIsMandatory)
return m
}
func (m *Mux) Accept() (net.Conn, error) {
select {
case <-m.acceptErr:
@@ -535,10 +548,11 @@ func (m *Mux) acceptUnderlayLoop(ctx context.Context, properties UnderlayPropert
trafficPattern = m.trafficPattern.Effective()
}
underlay := &PacketUnderlay{
baseUnderlay: *newBaseUnderlay(false, properties.MTU(), trafficPattern),
conn: conn,
sessionCleanTicker: time.NewTicker(sessionCleanInterval),
users: m.users,
baseUnderlay: *newBaseUnderlay(false, properties.MTU(), trafficPattern),
conn: conn,
sessionCleanTicker: time.NewTicker(sessionCleanInterval),
users: m.users,
userHintIsMandatory: m.userHintIsMandatory,
}
log.Infof("Created new server underlay %v", underlay)
m.mu.Lock()
@@ -601,10 +615,11 @@ func (m *Mux) acceptTCPUnderlay(rawListener net.Listener, properties UnderlayPro
func (m *Mux) serverWrapTCPConn(rawConn net.Conn, mtu int, users map[string]*appctlpb.User, trafficPattern *appctlpb.TrafficPattern) Underlay {
return &StreamUnderlay{
baseUnderlay: *newBaseUnderlay(false, mtu, trafficPattern),
conn: rawConn,
sessionCleanTicker: time.NewTicker(sessionCleanInterval),
users: users,
baseUnderlay: *newBaseUnderlay(false, mtu, trafficPattern),
conn: rawConn,
sessionCleanTicker: time.NewTicker(sessionCleanInterval),
users: users,
userHintIsMandatory: m.userHintIsMandatory,
}
}
+24 -23
View File
@@ -54,7 +54,8 @@ type PacketUnderlay struct {
block cipher.BlockCipher
// ---- server fields ----
users map[string]*appctlpb.User
users map[string]*appctlpb.User
userHintIsMandatory bool
}
var _ Underlay = &PacketUnderlay{}
@@ -395,51 +396,51 @@ func (u *PacketUnderlay) readOneSegment() (*segment, net.Addr, error) {
}
return true
})
if !decrypted {
// This is a new session.
cipher.ServerIterateDecrypt.Add(1)
// First, try to narrow down the user using the nonce hint.
var hintUser *appctlpb.User
var hintUsers []*appctlpb.User
for _, user := range u.users {
if cipher.CheckUserFromHint([]byte(user.GetName()), nonce) {
hintUser = user
break
hintUsers = append(hintUsers, user)
}
}
if hintUser != nil {
for _, hintUser := range hintUsers {
cipher.ServerHintMatchDecrypt.Add(1)
var password []byte
password, err = hex.DecodeString(hintUser.GetHashedPassword())
if err != nil {
log.Debugf("Unable to decode hashed password %q from user %q", hintUser.GetHashedPassword(), hintUser.GetName())
continue
}
if len(password) == 0 {
password = cipher.HashPassword([]byte(hintUser.GetPassword()), []byte(hintUser.GetName()))
}
blockCipher, decryptedMeta, err = cipher.TryDecrypt(encryptedMeta, password, true)
if err == nil {
decrypted = true
blockCipher.SetBlockContext(cipher.BlockContext{
UserName: hintUser.GetName(),
})
if u.trafficPattern != nil {
blockCipher.SetNoncePattern(u.trafficPattern.GetNonce())
}
break
} else {
if len(password) == 0 {
password = cipher.HashPassword([]byte(hintUser.GetPassword()), []byte(hintUser.GetName()))
}
blockCipher, decryptedMeta, err = cipher.TryDecrypt(encryptedMeta, password, true)
if err == nil {
decrypted = true
blockCipher.SetBlockContext(cipher.BlockContext{
UserName: hintUser.GetName(),
})
if u.trafficPattern != nil {
blockCipher.SetNoncePattern(u.trafficPattern.GetNonce())
}
} else {
cipher.ServerFailedHintMatchDecrypt.Add(1)
}
cipher.ServerFailedHintMatchDecrypt.Add(1)
}
}
}
if !decrypted {
if !decrypted && !u.userHintIsMandatory {
// Fallback: try all registered users.
// This handles old clients without hints and hint hash collisions.
for _, user := range u.users {
var password []byte
password, err = hex.DecodeString(user.GetHashedPassword())
if err != nil {
log.Debugf("Unable to decode hashed password %q from user %q", user.GetHashedPassword(), user.GetName())
continue
}
if len(password) == 0 {
+19 -17
View File
@@ -56,7 +56,8 @@ type StreamUnderlay struct {
block cipher.BlockCipher
// ---- server fields ----
users map[string]*appctlpb.User
users map[string]*appctlpb.User
userHintIsMandatory bool
}
var _ Underlay = &StreamUnderlay{}
@@ -662,32 +663,33 @@ func (t *StreamUnderlay) serverInitRecvBlockCipherAndDecryptMetadata(encryptedMe
var matchedUserName string
// First, try to narrow down the user using the nonce hint.
var hintUser *appctlpb.User
var hintUsers []*appctlpb.User
for _, user := range t.users {
if cipher.CheckUserFromHint([]byte(user.GetName()), nonce) {
hintUser = user
break
hintUsers = append(hintUsers, user)
}
}
if hintUser != nil {
for _, hintUser := range hintUsers {
cipher.ServerHintMatchDecrypt.Add(1)
password, err := hex.DecodeString(hintUser.GetHashedPassword())
if err != nil {
log.Debugf("Unable to decode hashed password %q from user %q", hintUser.GetHashedPassword(), hintUser.GetName())
continue
}
if len(password) == 0 {
password = cipher.HashPassword([]byte(hintUser.GetPassword()), []byte(hintUser.GetName()))
}
matchedBlock, _, err = cipher.TryDecrypt(encryptedMeta, password, true)
if err == nil {
if len(password) == 0 {
password = cipher.HashPassword([]byte(hintUser.GetPassword()), []byte(hintUser.GetName()))
}
matchedBlock, _, err = cipher.TryDecrypt(encryptedMeta, password, true)
if err == nil {
matchedUserName = hintUser.GetName()
} else {
cipher.ServerFailedHintMatchDecrypt.Add(1)
}
matchedUserName = hintUser.GetName()
break
} else {
cipher.ServerFailedHintMatchDecrypt.Add(1)
}
}
// Fallback: try all registered users.
// This handles old clients without hints and hint hash collisions.
if matchedBlock == nil {
if matchedBlock == nil && !t.userHintIsMandatory {
// Fallback: try all registered users.
for _, user := range t.users {
password, err := hex.DecodeString(user.GetHashedPassword())
if err != nil {
+3
View File
@@ -24,6 +24,9 @@
"allowLoopbackIP": true
}
],
"advancedSettings": {
"userHintIsMandatory": true
},
"loggingLevel": "DEBUG",
"mtu": 1400,
"trafficPattern": {
+3
View File
@@ -24,5 +24,8 @@
"allowLoopbackIP": true
}
],
"advancedSettings": {
"userHintIsMandatory": true
},
"loggingLevel": "INFO"
}
+3
View File
@@ -24,6 +24,9 @@
"allowLoopbackIP": true
}
],
"advancedSettings": {
"userHintIsMandatory": true
},
"loggingLevel": "INFO",
"mtu": 1400
}
+2 -2
View File
@@ -381,7 +381,7 @@ jobs:
EOF
- name: Upload Prerelease
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
if: ${{ success() }}
with:
tag_name: Prerelease-${{ github.ref_name }}
@@ -443,7 +443,7 @@ jobs:
working-directory: bin
- name: Upload Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
if: ${{ success() }}
with:
tag_name: ${{ github.event.inputs.version }}
+6 -7
View File
@@ -13,7 +13,7 @@ import (
"github.com/metacubex/mihomo/component/ca"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
tuicCommon "github.com/metacubex/mihomo/transport/tuic/common"
"github.com/metacubex/mihomo/transport/tuic/common"
"github.com/metacubex/quic-go"
qtls "github.com/metacubex/sing-quic"
@@ -22,10 +22,6 @@ import (
"github.com/metacubex/tls"
)
func init() {
hysteria2.SetCongestionController = tuicCommon.SetCongestionController
}
const minHopInterval = 5
const defaultHopInterval = 30
@@ -56,6 +52,7 @@ type Hysteria2Option struct {
PrivateKey string `proxy:"private-key,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
BBRProfile string `proxy:"bbr-profile,omitempty"`
UdpMTU int `proxy:"udp-mtu,omitempty"`
// quic-go special config
@@ -184,7 +181,6 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
TLSConfig: tlsClientConfig,
QUICConfig: quicConfig,
UDPDisabled: false,
CWND: option.CWND,
UdpMTU: option.UdpMTU,
ServerAddress: M.ParseSocksaddr(addr),
PacketListener: outbound.dialer,
@@ -193,8 +189,11 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
if err != nil {
return nil, nil, err
}
return tuicCommon.DialQuic(ctx, addr, outbound.DialOptions(), dialer, tlsCfg, cfg, early)
return common.DialQuic(ctx, addr, outbound.DialOptions(), dialer, tlsCfg, cfg, early)
}),
SetBBRCongestion: func(quicConn *quic.Conn) {
common.SetCongestionController(quicConn, "bbr", option.CWND, option.BBRProfile)
},
}
var serverPorts []uint16
+2 -1
View File
@@ -69,6 +69,7 @@ type MasqueOption struct {
CongestionController string `proxy:"congestion-controller,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
BBRProfile string `proxy:"bbr-profile,omitempty"`
RemoteDnsResolve bool `proxy:"remote-dns-resolve,omitempty"`
Dns []string `proxy:"dns,omitempty"`
@@ -262,7 +263,7 @@ func (w *Masque) run(ctx context.Context) error {
if err != nil {
return err
}
common.SetCongestionController(quicConn, w.option.CongestionController, w.option.CWND)
common.SetCongestionController(quicConn, w.option.CongestionController, w.option.CWND, w.option.BBRProfile)
closer, ipConn, err = masque.ConnectTunnel(ctx, quicConn, w.uri)
if err != nil {
+2
View File
@@ -38,6 +38,7 @@ type TrustTunnelOption struct {
Quic bool `proxy:"quic,omitempty"`
CongestionController string `proxy:"congestion-controller,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
BBRProfile string `proxy:"bbr-profile,omitempty"`
// reuse options
MaxConnections int `proxy:"max-connections,omitempty"`
MinStreams int `proxy:"min-streams,omitempty"`
@@ -110,6 +111,7 @@ func NewTrustTunnel(option TrustTunnelOption) (*TrustTunnel, error) {
QUIC: option.Quic,
QUICCongestionControl: option.CongestionController,
QUICCwnd: option.CWND,
QUICBBRProfile: option.BBRProfile,
HealthCheck: option.HealthCheck,
MaxConnections: option.MaxConnections,
MinStreams: option.MinStreams,
+2 -1
View File
@@ -52,6 +52,7 @@ type TuicOption struct {
FastOpen bool `proxy:"fast-open,omitempty"`
MaxOpenStreams int `proxy:"max-open-streams,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
BBRProfile string `proxy:"bbr-profile,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
Certificate string `proxy:"certificate,omitempty"`
@@ -113,7 +114,7 @@ func (t *Tuic) dial(ctx context.Context) (quicConn *quic.Conn, err error) {
if err != nil {
return nil, err
}
common.SetCongestionController(quicConn, t.option.CongestionController, t.option.CWND)
common.SetCongestionController(quicConn, t.option.CongestionController, t.option.CWND, t.option.BBRProfile)
return quicConn, nil
}
+36 -32
View File
@@ -79,15 +79,16 @@ type VlessOption struct {
}
type XHTTPOptions struct {
Path string `proxy:"path,omitempty"`
Host string `proxy:"host,omitempty"`
Mode string `proxy:"mode,omitempty"`
Headers map[string]string `proxy:"headers,omitempty"`
NoGRPCHeader bool `proxy:"no-grpc-header,omitempty"`
XPaddingBytes string `proxy:"x-padding-bytes,omitempty"`
ScMaxEachPostBytes string `proxy:"sc-max-each-post-bytes,omitempty"`
ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX
DownloadSettings *XHTTPDownloadSettings `proxy:"download-settings,omitempty"`
Path string `proxy:"path,omitempty"`
Host string `proxy:"host,omitempty"`
Mode string `proxy:"mode,omitempty"`
Headers map[string]string `proxy:"headers,omitempty"`
NoGRPCHeader bool `proxy:"no-grpc-header,omitempty"`
XPaddingBytes string `proxy:"x-padding-bytes,omitempty"`
ScMaxEachPostBytes string `proxy:"sc-max-each-post-bytes,omitempty"`
ScMinPostsIntervalMs string `proxy:"sc-min-posts-interval-ms,omitempty"`
ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX
DownloadSettings *XHTTPDownloadSettings `proxy:"download-settings,omitempty"`
}
type XHTTPReuseSettings struct {
@@ -101,13 +102,14 @@ type XHTTPReuseSettings struct {
type XHTTPDownloadSettings struct {
// xhttp part
Path *string `proxy:"path,omitempty"`
Host *string `proxy:"host,omitempty"`
Headers *map[string]string `proxy:"headers,omitempty"`
NoGRPCHeader *bool `proxy:"no-grpc-header,omitempty"`
XPaddingBytes *string `proxy:"x-padding-bytes,omitempty"`
ScMaxEachPostBytes *string `proxy:"sc-max-each-post-bytes,omitempty"`
ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX
Path *string `proxy:"path,omitempty"`
Host *string `proxy:"host,omitempty"`
Headers *map[string]string `proxy:"headers,omitempty"`
NoGRPCHeader *bool `proxy:"no-grpc-header,omitempty"`
XPaddingBytes *string `proxy:"x-padding-bytes,omitempty"`
ScMaxEachPostBytes *string `proxy:"sc-max-each-post-bytes,omitempty"`
ScMinPostsIntervalMs *string `proxy:"sc-min-posts-interval-ms,omitempty"`
ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX
// proxy part
Server *string `proxy:"server,omitempty"`
Port *int `proxy:"port,omitempty"`
@@ -544,14 +546,15 @@ func NewVless(option VlessOption) (*Vless, error) {
}
cfg := &xhttp.Config{
Host: requestHost,
Path: v.option.XHTTPOpts.Path,
Mode: v.option.XHTTPOpts.Mode,
Headers: v.option.XHTTPOpts.Headers,
NoGRPCHeader: v.option.XHTTPOpts.NoGRPCHeader,
XPaddingBytes: v.option.XHTTPOpts.XPaddingBytes,
ScMaxEachPostBytes: v.option.XHTTPOpts.ScMaxEachPostBytes,
ReuseConfig: reuseCfg,
Host: requestHost,
Path: v.option.XHTTPOpts.Path,
Mode: v.option.XHTTPOpts.Mode,
Headers: v.option.XHTTPOpts.Headers,
NoGRPCHeader: v.option.XHTTPOpts.NoGRPCHeader,
XPaddingBytes: v.option.XHTTPOpts.XPaddingBytes,
ScMaxEachPostBytes: v.option.XHTTPOpts.ScMaxEachPostBytes,
ScMinPostsIntervalMs: v.option.XHTTPOpts.ScMinPostsIntervalMs,
ReuseConfig: reuseCfg,
}
makeTransport := func() http.RoundTripper {
@@ -658,14 +661,15 @@ func NewVless(option VlessOption) (*Vless, error) {
}
cfg.DownloadConfig = &xhttp.Config{
Host: downloadHost,
Path: lo.FromPtrOr(ds.Path, v.option.XHTTPOpts.Path),
Mode: v.option.XHTTPOpts.Mode,
Headers: lo.FromPtrOr(ds.Headers, v.option.XHTTPOpts.Headers),
NoGRPCHeader: lo.FromPtrOr(ds.NoGRPCHeader, v.option.XHTTPOpts.NoGRPCHeader),
XPaddingBytes: lo.FromPtrOr(ds.XPaddingBytes, v.option.XHTTPOpts.XPaddingBytes),
ScMaxEachPostBytes: lo.FromPtrOr(ds.ScMaxEachPostBytes, v.option.XHTTPOpts.ScMaxEachPostBytes),
ReuseConfig: downloadReuseCfg,
Host: downloadHost,
Path: lo.FromPtrOr(ds.Path, v.option.XHTTPOpts.Path),
Mode: v.option.XHTTPOpts.Mode,
Headers: lo.FromPtrOr(ds.Headers, v.option.XHTTPOpts.Headers),
NoGRPCHeader: lo.FromPtrOr(ds.NoGRPCHeader, v.option.XHTTPOpts.NoGRPCHeader),
XPaddingBytes: lo.FromPtrOr(ds.XPaddingBytes, v.option.XHTTPOpts.XPaddingBytes),
ScMaxEachPostBytes: lo.FromPtrOr(ds.ScMaxEachPostBytes, v.option.XHTTPOpts.ScMaxEachPostBytes),
ScMinPostsIntervalMs: lo.FromPtrOr(ds.ScMinPostsIntervalMs, v.option.XHTTPOpts.ScMinPostsIntervalMs),
ReuseConfig: downloadReuseCfg,
}
makeDownloadTransport = func() http.RoundTripper {
+9
View File
@@ -826,6 +826,7 @@ proxies: # socks5
# no-grpc-header: false
# x-padding-bytes: "100-1000"
# sc-max-each-post-bytes: 1000000
# sc-min-posts-interval-ms: 30
# reuse-settings: # aka XMUX
# max-concurrency: "16-32"
# max-connections: "0"
@@ -842,6 +843,7 @@ proxies: # socks5
# no-grpc-header: false
# x-padding-bytes: "100-1000"
# sc-max-each-post-bytes: 1000000
# sc-min-posts-interval-ms: 30
# reuse-settings: # aka XMUX
# max-concurrency: "16-32"
# max-connections: "0"
@@ -986,6 +988,7 @@ proxies: # socks5
# up 和 down 均不写或为 0 则使用 BBR 流控
# up: "30 Mbps" # 若不写单位,默认为 Mbps
# down: "200 Mbps" # 若不写单位,默认为 Mbps
# bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard"
password: yourpassword
# obfs: salamander # 默认为空,如果填写则开启 obfs,目前仅支持 salamander
# obfs-password: yourpassword
@@ -1116,6 +1119,7 @@ proxies: # socks5
udp-relay-mode: native # Available: "native", "quic". Default: "native"
# congestion-controller: bbr # Available: "cubic", "new_reno", "bbr". Default: "cubic"
# cwnd: 10 # default: 32
# bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard"
# max-udp-relay-packet-size: 1500
# fast-open: true
# skip-cert-verify: true
@@ -1234,6 +1238,7 @@ proxies: # socks5
### quic options
# quic: true # 默认为false
# congestion-controller: bbr
# bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard"
### reuse options
# max-connections: 8 # Maximum connections. Conflict with max-streams.
# min-streams: 5 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams.
@@ -1649,6 +1654,7 @@ listeners:
# dC5jb20AAA==
# -----END ECH KEYS-----
# congestion-controller: bbr
# bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard"
# max-idle-time: 15000
# authentication-timeout: 1000
# alpn:
@@ -1681,6 +1687,7 @@ listeners:
# host: ""
# mode: auto # Available: "stream-one", "stream-up" or "packet-up"
# no-sse-header: false
# sc-max-buffered-posts: 30
# sc-stream-up-server-secs: "20-80"
# sc-max-each-post-bytes: 1000000
# -------------------------
@@ -1881,6 +1888,7 @@ listeners:
private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径
network: ["tcp", "udp"] # http2+http3
congestion-controller: bbr
# bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard"
# 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空
# client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify"
# client-auth-cert: string # 证书 PEM 格式,或者 证书的路径
@@ -1956,6 +1964,7 @@ listeners:
# certificate: ./server.crt
# private-key: ./server.key
# congestion-controller: bbr
# bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard"
# max-idle-time: 15000
# authentication-timeout: 1000
# alpn:
+1 -1
View File
@@ -30,7 +30,7 @@ require (
github.com/metacubex/restls-client-go v0.1.7
github.com/metacubex/sing v0.5.7
github.com/metacubex/sing-mux v0.3.6
github.com/metacubex/sing-quic v0.0.0-20260413140653-b900ea123319
github.com/metacubex/sing-quic v0.0.0-20260414034501-3ea3410d197a
github.com/metacubex/sing-shadowsocks v0.2.12
github.com/metacubex/sing-shadowsocks2 v0.2.7
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2
+2 -2
View File
@@ -127,8 +127,8 @@ github.com/metacubex/sing v0.5.7 h1:8OC+fhKFSv/l9ehEhJRaZZAOuthfZo68SteBVLe8QqM=
github.com/metacubex/sing v0.5.7/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing-mux v0.3.6 h1:AIjxEz7pq+5DVoV9N5/xaQXz3cMSZR9PBvMRP9X3+A0=
github.com/metacubex/sing-mux v0.3.6/go.mod h1:8bT7ZKT3clRrJjYc/x5CRYibC1TX/bK73a3r3+2E+Fc=
github.com/metacubex/sing-quic v0.0.0-20260413140653-b900ea123319 h1:zVqpKLZR/K7IiAVKs53RCEAXpVe9re7t5GVOHeL3Pzo=
github.com/metacubex/sing-quic v0.0.0-20260413140653-b900ea123319/go.mod h1:6ayFGfzzBE85csgQkM3gf4neFq6s0losHlPRSxY+nuk=
github.com/metacubex/sing-quic v0.0.0-20260414034501-3ea3410d197a h1:977o0ZYYbiQAGuOxql7Q6UN3rEy59OyAE0tELq4gZfI=
github.com/metacubex/sing-quic v0.0.0-20260414034501-3ea3410d197a/go.mod h1:6ayFGfzzBE85csgQkM3gf4neFq6s0losHlPRSxY+nuk=
github.com/metacubex/sing-shadowsocks v0.2.12 h1:Wqzo8bYXrK5aWqxu/TjlTnYZzAKtKsaFQBdr6IHFaBE=
github.com/metacubex/sing-shadowsocks v0.2.12/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU=
github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6wVj3PPBVhor3A=
+4
View File
@@ -122,6 +122,7 @@ type tuicServerSchema struct {
ALPN *[]string `yaml:"alpn" json:"alpn,omitempty"`
MaxUdpRelayPacketSize *int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"`
CWND *int `yaml:"cwnd" json:"cwnd,omitempty"`
BBRProfile *string `yaml:"bbr-profile" json:"bbr-profile,omitempty"`
}
func getConfigs(w http.ResponseWriter, r *http.Request) {
@@ -305,6 +306,9 @@ func pointerOrDefaultTuicServer(p *tuicServerSchema, def LC.TuicServer) LC.TuicS
if p.CWND != nil {
def.CWND = *p.CWND
}
if p.BBRProfile != nil {
def.BBRProfile = *p.BBRProfile
}
}
return def
}
+1
View File
@@ -24,6 +24,7 @@ type Hysteria2Server struct {
IgnoreClientBandwidth bool `yaml:"ignore-client-bandwidth" json:"ignore-client-bandwidth,omitempty"`
Masquerade string `yaml:"masquerade" json:"masquerade,omitempty"`
CWND int `yaml:"cwnd" json:"cwnd,omitempty"`
BBRProfile string `yaml:"bbr-profile" json:"bbr-profile,omitempty"`
UdpMTU int `yaml:"udp-mtu" json:"udp-mtu,omitempty"`
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
+1
View File
@@ -16,6 +16,7 @@ type TrustTunnelServer struct {
Network []string `yaml:"network" json:"network,omitempty"`
CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"`
CWND int `yaml:"cwnd" json:"cwnd,omitempty"`
BBRProfile string `yaml:"bbr-profile" json:"bbr-profile,omitempty"`
}
func (t TrustTunnelServer) String() string {
+1
View File
@@ -23,6 +23,7 @@ type TuicServer struct {
MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"`
MaxDatagramFrameSize int `yaml:"max-datagram-frame-size" json:"max-datagram-frame-size,omitempty"`
CWND int `yaml:"cwnd" json:"cwnd,omitempty"`
BBRProfile string `yaml:"bbr-profile" json:"bbr-profile,omitempty"`
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
}
+1
View File
@@ -36,6 +36,7 @@ type XHTTPConfig struct {
Mode string
NoSSEHeader bool
ScStreamUpServerSecs string
ScMaxBufferedPosts string
ScMaxEachPostBytes string
}
-7
View File
@@ -13,7 +13,6 @@ import (
"strconv"
"sync"
"testing"
"time"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/common/pool"
@@ -186,11 +185,6 @@ func NewHttpTestTunnel() *TestTunnel {
DialContext: func(context.Context, string, string) (net.Conn, error) {
return instance, nil
},
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
// for our self-signed cert
TLSClientConfig: tlsClientConfig.Clone(),
// open http2
@@ -198,7 +192,6 @@ func NewHttpTestTunnel() *TestTunnel {
}
client := http.Client{
Timeout: 30 * time.Second,
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
+2
View File
@@ -26,6 +26,7 @@ type Hysteria2Option struct {
IgnoreClientBandwidth bool `inbound:"ignore-client-bandwidth,omitempty"`
Masquerade string `inbound:"masquerade,omitempty"`
CWND int `inbound:"cwnd,omitempty"`
BBRProfile string `inbound:"bbr-profile,omitempty"`
UdpMTU int `inbound:"udp-mtu,omitempty"`
MuxOption MuxOption `inbound:"mux-option,omitempty"`
@@ -73,6 +74,7 @@ func NewHysteria2(options *Hysteria2Option) (*Hysteria2, error) {
IgnoreClientBandwidth: options.IgnoreClientBandwidth,
Masquerade: options.Masquerade,
CWND: options.CWND,
BBRProfile: options.BBRProfile,
UdpMTU: options.UdpMTU,
MuxOption: options.MuxOption.Build(),
// quic-go special config
+5
View File
@@ -2,6 +2,7 @@ package inbound_test
import (
"net/netip"
"runtime"
"testing"
"github.com/metacubex/mihomo/adapter/outbound"
@@ -166,6 +167,10 @@ func TestInboundSudoku_CustomTable(t *testing.T) {
}
func TestInboundSudoku_HTTPMaskMode(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("temporarily skipped on windows due to intermittent failures; tracked in PR")
}
key := "test_key_http_mask_mode"
for _, mode := range []string{"ws", "stream", "poll", "auto"} {
+2
View File
@@ -20,6 +20,7 @@ type TrustTunnelOption struct {
Network []string `inbound:"network,omitempty"`
CongestionController string `inbound:"congestion-controller,omitempty"`
CWND int `inbound:"cwnd,omitempty"`
BBRProfile string `inbound:"bbr-profile,omitempty"`
}
func (o TrustTunnelOption) Equal(config C.InboundConfig) bool {
@@ -57,6 +58,7 @@ func NewTrustTunnel(options *TrustTunnelOption) (*TrustTunnel, error) {
Network: options.Network,
CongestionController: options.CongestionController,
CWND: options.CWND,
BBRProfile: options.BBRProfile,
},
}, nil
}
+2
View File
@@ -24,6 +24,7 @@ type TuicOption struct {
ALPN []string `inbound:"alpn,omitempty"`
MaxUdpRelayPacketSize int `inbound:"max-udp-relay-packet-size,omitempty"`
CWND int `inbound:"cwnd,omitempty"`
BBRProfile string `inbound:"bbr-profile,omitempty"`
MuxOption MuxOption `inbound:"mux-option,omitempty"`
}
@@ -62,6 +63,7 @@ func NewTuic(options *TuicOption) (*Tuic, error) {
ALPN: options.ALPN,
MaxUdpRelayPacketSize: options.MaxUdpRelayPacketSize,
CWND: options.CWND,
BBRProfile: options.BBRProfile,
MuxOption: options.MuxOption.Build(),
},
}, nil
+2
View File
@@ -37,6 +37,7 @@ type XHTTPConfig struct {
Mode string `inbound:"mode,omitempty"`
NoSSEHeader bool `inbound:"no-sse-header,omitempty"`
ScStreamUpServerSecs string `inbound:"sc-stream-up-server-secs,omitempty"`
ScMaxBufferedPosts string `inbound:"sc-max-buffered-posts,omitempty"`
ScMaxEachPostBytes string `inbound:"sc-max-each-post-bytes,omitempty"`
}
@@ -47,6 +48,7 @@ func (o XHTTPConfig) Build() LC.XHTTPConfig {
Mode: o.Mode,
NoSSEHeader: o.NoSSEHeader,
ScStreamUpServerSecs: o.ScStreamUpServerSecs,
ScMaxBufferedPosts: o.ScMaxBufferedPosts,
ScMaxEachPostBytes: o.ScMaxEachPostBytes,
}
}
+4 -1
View File
@@ -20,6 +20,7 @@ import (
"github.com/metacubex/mihomo/listener/sing"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/ntp"
"github.com/metacubex/mihomo/transport/tuic/common"
"github.com/metacubex/http"
"github.com/metacubex/http/httputil"
@@ -171,8 +172,10 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi
UDPTimeout: sing.UDPTimeout,
Handler: h,
MasqueradeHandler: masqueradeHandler,
CWND: config.CWND,
UdpMTU: config.UdpMTU,
SetBBRCongestion: func(quicConn *quic.Conn) {
common.SetCongestionController(quicConn, "bbr", config.CWND, config.BBRProfile)
},
})
if err != nil {
return nil, err
+1
View File
@@ -161,6 +161,7 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition)
Mode: config.XHTTPConfig.Mode,
NoSSEHeader: config.XHTTPConfig.NoSSEHeader,
ScStreamUpServerSecs: config.XHTTPConfig.ScStreamUpServerSecs,
ScMaxBufferedPosts: config.XHTTPConfig.ScMaxBufferedPosts,
ScMaxEachPostBytes: config.XHTTPConfig.ScMaxEachPostBytes,
},
ConnHandler: func(conn net.Conn) {
+1
View File
@@ -135,6 +135,7 @@ func New(config LC.TrustTunnelServer, tunnel C.Tunnel, additions ...inbound.Addi
ICMPHandler: nil,
QUICCongestionControl: config.CongestionController,
QUICCwnd: config.CWND,
QUICBBRProfile: config.BBRProfile,
})
service.UpdateUsers(config.Users)
err = service.Start(tcpListener, udpConn, tlsConfig)
+1
View File
@@ -155,6 +155,7 @@ func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) (
AuthenticationTimeout: time.Duration(config.AuthenticationTimeout) * time.Millisecond,
MaxUdpRelayPacketSize: config.MaxUdpRelayPacketSize,
CWND: config.CWND,
BBRProfile: config.BBRProfile,
}
if len(config.Token) > 0 {
tokens := make([][32]byte, len(config.Token))
+2 -1
View File
@@ -34,6 +34,7 @@ type ClientOptions struct {
QUIC bool
QUICCongestionControl string
QUICCwnd int
QUICBBRProfile string
HealthCheck bool
MaxConnections int
MinStreams int
@@ -67,7 +68,7 @@ func NewClient(ctx context.Context, options ClientOptions) (client *Client, err
} else if !slices.Contains(options.TLSConfig.NextProtos, "h3") {
return nil, errors.New("require alpn h3")
}
err = client.quicRoundTripper(options.TLSConfig, options.QUICCongestionControl, options.QUICCwnd)
err = client.quicRoundTripper(options.TLSConfig, options.QUICCongestionControl, options.QUICCwnd, options.QUICBBRProfile)
if err != nil {
return nil, err
}
+3 -3
View File
@@ -15,7 +15,7 @@ import (
"github.com/metacubex/tls"
)
func (c *Client) quicRoundTripper(tlsConfig *vmess.TLSConfig, congestionControlName string, cwnd int) error {
func (c *Client) quicRoundTripper(tlsConfig *vmess.TLSConfig, congestionControlName string, cwnd int, bbrProfile string) error {
stdConfig, err := tlsConfig.ToStdConfig()
if err != nil {
return err
@@ -38,7 +38,7 @@ func (c *Client) quicRoundTripper(tlsConfig *vmess.TLSConfig, congestionControlN
if err != nil {
return nil, err
}
common.SetCongestionController(quicConn, congestionControlName, cwnd)
common.SetCongestionController(quicConn, congestionControlName, cwnd, bbrProfile)
return quicConn, nil
},
}
@@ -60,7 +60,7 @@ func (s *Service) configHTTP3Server(tlsConfig *tls.Config, udpConn net.PacketCon
Handler: s,
IdleTimeout: DefaultSessionTimeout,
ConnContext: func(ctx context.Context, conn *quic.Conn) context.Context {
common.SetCongestionController(conn, s.quicCongestionControl, s.quicCwnd)
common.SetCongestionController(conn, s.quicCongestionControl, s.quicCwnd, s.quicBBRProfile)
return ctx
},
}
+8 -4
View File
@@ -7,6 +7,7 @@ import (
"time"
"github.com/metacubex/mihomo/common/httputils"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/http"
"github.com/metacubex/http/h2c"
@@ -18,13 +19,13 @@ import (
E "github.com/metacubex/sing/common/exceptions"
"github.com/metacubex/sing/common/logger"
M "github.com/metacubex/sing/common/metadata"
N "github.com/metacubex/sing/common/network"
"github.com/metacubex/sing/common/network"
"github.com/metacubex/tls"
)
type Handler interface {
N.TCPConnectionHandler
N.UDPConnectionHandler
network.TCPConnectionHandler
network.UDPConnectionHandler
}
type ICMPHandler interface {
@@ -38,6 +39,7 @@ type ServiceOptions struct {
ICMPHandler ICMPHandler
QUICCongestionControl string
QUICCwnd int
QUICBBRProfile string
}
type Service struct {
@@ -48,6 +50,7 @@ type Service struct {
icmpHandler ICMPHandler
quicCongestionControl string
quicCwnd int
quicBBRProfile string
httpServer *http.Server
h2Server *http.Http2Server
h3Server *http3.Server
@@ -64,6 +67,7 @@ func NewService(options ServiceOptions) *Service {
icmpHandler: options.ICMPHandler,
quicCongestionControl: options.QUICCongestionControl,
quicCwnd: options.QUICCwnd,
quicBBRProfile: options.QUICBBRProfile,
}
}
@@ -224,7 +228,7 @@ func (s *Service) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
}
httputils.SetAddrFromRequest(&conn.NetAddr, request)
conn.setUp(request.Body, nil)
_ = s.handler.NewConnection(ctx, conn, M.Metadata{
_ = s.handler.NewConnection(ctx, N.NewDeadlineConn(conn), M.Metadata{
Protocol: "trusttunnel",
Source: M.ParseSocksaddr(request.RemoteAddr),
Destination: M.ParseSocksaddr(request.Host).Unwrap(),
+2 -1
View File
@@ -13,7 +13,7 @@ const (
DefaultConnectionReceiveWindow = 67108864 // 64 MB/s
)
func SetCongestionController(quicConn *quic.Conn, cc string, cwnd int) {
func SetCongestionController(quicConn *quic.Conn, cc string, cwnd int, profile string) {
if cwnd == 0 {
cwnd = 32
}
@@ -47,6 +47,7 @@ func SetCongestionController(quicConn *quic.Conn, cc string, cwnd int) {
congestionv2.NewBbrSender(
congestionv2.GetInitialPacketSize(quicConn),
c.ByteCount(cwnd),
congestionv2.Profile(profile),
),
)
}
+126 -48
View File
@@ -27,16 +27,13 @@ const (
invalidPacketNumber = -1
initialCongestionWindowPackets = 32
minCongestionWindowPackets = 4
// Constants based on TCP defaults.
// The minimum CWND to ensure delayed acks don't reduce bandwidth measurements.
// Does not inflate the pacing rate.
defaultMinimumCongestionWindow = 4 * congestion.ByteCount(congestion.InitialPacketSize)
// The gain used for the STARTUP, equal to 2/ln(2).
defaultHighGain = 2.885
// The newly derived gain for STARTUP, equal to 4 * ln(2)
derivedHighGain = 2.773
// The newly derived CWND gain for STARTUP, 2.
derivedHighCWNDGain = 2.0
)
@@ -63,7 +60,6 @@ const (
// Flag.
defaultStartupFullLossCount = 8
quicBbr2DefaultLossThreshold = 0.02
maxBbrBurstPackets = 10
)
type bbrMode int
@@ -94,6 +90,63 @@ const (
bbrRecoveryStateGrowth
)
type Profile string
const (
ProfileConservative Profile = "conservative"
ProfileStandard Profile = "standard"
ProfileAggressive Profile = "aggressive"
)
type profileConfig struct {
highGain float64
highCwndGain float64
congestionWindowGainConstant float64
numStartupRtts int64
drainToTarget bool
detectOvershooting bool
bytesLostMultiplier uint8
enableAckAggregationStartup bool
expireAckAggregationStartup bool
enableOverestimateAvoidance bool
reduceExtraAckedOnBandwidthIncrease bool
}
func configForProfile(profile Profile) profileConfig {
switch profile {
case ProfileConservative:
return profileConfig{
highGain: 2.25,
highCwndGain: 1.75,
congestionWindowGainConstant: 1.75,
numStartupRtts: 2,
drainToTarget: true,
detectOvershooting: true,
bytesLostMultiplier: 1,
enableOverestimateAvoidance: true,
reduceExtraAckedOnBandwidthIncrease: true,
}
case ProfileAggressive:
return profileConfig{
highGain: 3.0,
highCwndGain: 2.25,
congestionWindowGainConstant: 2.5,
numStartupRtts: 4,
bytesLostMultiplier: 2,
enableAckAggregationStartup: true,
expireAckAggregationStartup: true,
}
default:
return profileConfig{
highGain: defaultHighGain,
highCwndGain: derivedHighCWNDGain,
congestionWindowGainConstant: 2.0,
numStartupRtts: roundTripsWithoutGrowthBeforeExitingStartup,
bytesLostMultiplier: 2,
}
}
}
type bbrSender struct {
rttStats congestion.RTTStatsProvider
pacer *Pacer
@@ -141,6 +194,9 @@ type bbrSender struct {
// The smallest value the |congestion_window_| can achieve.
minCongestionWindow congestion.ByteCount
// The BBR profile used by the sender.
profile Profile
// The pacing gain applied during the STARTUP phase.
highGain float64
@@ -245,11 +301,13 @@ var _ congestion.CongestionControl = &bbrSender{}
func NewBbrSender(
initialMaxDatagramSize congestion.ByteCount,
initialCongestionWindowPackets congestion.ByteCount,
profile Profile,
) *bbrSender {
return newBbrSender(
initialMaxDatagramSize,
initialCongestionWindowPackets*initialMaxDatagramSize,
congestion.MaxCongestionWindowPackets*initialMaxDatagramSize,
profile,
)
}
@@ -257,6 +315,7 @@ func newBbrSender(
initialMaxDatagramSize,
initialCongestionWindow,
initialMaxCongestionWindow congestion.ByteCount,
profile Profile,
) *bbrSender {
b := &bbrSender{
mode: bbrModeStartup,
@@ -267,9 +326,10 @@ func newBbrSender(
congestionWindow: initialCongestionWindow,
initialCongestionWindow: initialCongestionWindow,
maxCongestionWindow: initialMaxCongestionWindow,
minCongestionWindow: defaultMinimumCongestionWindow,
minCongestionWindow: minCongestionWindowForMaxDatagramSize(initialMaxDatagramSize),
profile: ProfileStandard,
highGain: defaultHighGain,
highCwndGain: defaultHighGain,
highCwndGain: derivedHighCWNDGain,
drainGain: 1.0 / defaultHighGain,
pacingGain: 1.0,
congestionWindowGain: 1.0,
@@ -284,20 +344,57 @@ func newBbrSender(
maxDatagramSize: initialMaxDatagramSize,
}
b.pacer = NewPacer(b.bandwidthForPacer)
/*
if b.tracer != nil {
b.lastState = logging.CongestionStateStartup
b.tracer.UpdatedCongestionState(logging.CongestionStateStartup)
}
*/
b.applyProfile(profile)
b.enterStartupMode()
b.setHighCwndGain(derivedHighCWNDGain)
return b
}
func (b *bbrSender) applyProfile(profile Profile) {
cfg := configForProfile(profile)
b.profile = profile
b.highGain = cfg.highGain
b.highCwndGain = cfg.highCwndGain
b.drainGain = 1.0 / cfg.highGain
b.congestionWindowGainConstant = cfg.congestionWindowGainConstant
b.numStartupRtts = cfg.numStartupRtts
b.drainToTarget = cfg.drainToTarget
b.detectOvershooting = cfg.detectOvershooting
b.bytesLostMultiplierWhileDetectingOvershooting = cfg.bytesLostMultiplier
b.enableAckAggregationDuringStartup = cfg.enableAckAggregationStartup
b.expireAckAggregationInStartup = cfg.expireAckAggregationStartup
if cfg.enableOverestimateAvoidance {
b.sampler.EnableOverestimateAvoidance()
}
b.sampler.SetReduceExtraAckedOnBandwidthIncrease(cfg.reduceExtraAckedOnBandwidthIncrease)
}
func minCongestionWindowForMaxDatagramSize(maxDatagramSize congestion.ByteCount) congestion.ByteCount {
return minCongestionWindowPackets * maxDatagramSize
}
func scaleByteWindowForDatagramSize(window, oldMaxDatagramSize, newMaxDatagramSize congestion.ByteCount) congestion.ByteCount {
if oldMaxDatagramSize == newMaxDatagramSize {
return window
}
return congestion.ByteCount(uint64(window) * uint64(newMaxDatagramSize) / uint64(oldMaxDatagramSize))
}
func (b *bbrSender) rescalePacketSizedWindows(maxDatagramSize congestion.ByteCount) {
oldMaxDatagramSize := b.maxDatagramSize
b.maxDatagramSize = maxDatagramSize
b.initialCongestionWindow = scaleByteWindowForDatagramSize(b.initialCongestionWindow, oldMaxDatagramSize, maxDatagramSize)
b.maxCongestionWindow = scaleByteWindowForDatagramSize(b.maxCongestionWindow, oldMaxDatagramSize, maxDatagramSize)
b.minCongestionWindow = minCongestionWindowForMaxDatagramSize(maxDatagramSize)
b.cwndToCalculateMinPacingRate = scaleByteWindowForDatagramSize(b.cwndToCalculateMinPacingRate, oldMaxDatagramSize, maxDatagramSize)
b.maxCongestionWindowWithNetworkParametersAdjusted = scaleByteWindowForDatagramSize(
b.maxCongestionWindowWithNetworkParametersAdjusted,
oldMaxDatagramSize,
maxDatagramSize,
)
}
func (b *bbrSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) {
b.rttStats = provider
}
@@ -330,8 +427,6 @@ func (b *bbrSender) OnPacketSent(
}
b.sampler.OnPacketSent(sentTime, packetNumber, bytes, bytesInFlight, isRetransmittable)
b.maybeAppLimited(bytesInFlight)
}
// CanSend implements the SendAlgorithm interface.
@@ -364,11 +459,18 @@ func (b *bbrSender) SetMaxDatagramSize(s congestion.ByteCount) {
if s < b.maxDatagramSize {
panic(fmt.Sprintf("congestion BUG: decreased max datagram size from %d to %d", b.maxDatagramSize, s))
}
cwndIsMinCwnd := b.congestionWindow == b.minCongestionWindow
b.maxDatagramSize = s
if cwndIsMinCwnd {
oldMinCongestionWindow := b.minCongestionWindow
oldInitialCongestionWindow := b.initialCongestionWindow
b.rescalePacketSizedWindows(s)
switch b.congestionWindow {
case oldMinCongestionWindow:
b.congestionWindow = b.minCongestionWindow
case oldInitialCongestionWindow:
b.congestionWindow = b.initialCongestionWindow
default:
b.congestionWindow = Min(b.maxCongestionWindow, Max(b.congestionWindow, b.minCongestionWindow))
}
b.recoveryWindow = Min(b.maxCongestionWindow, Max(b.recoveryWindow, b.minCongestionWindow))
b.pacer.SetMaxDatagramSize(s)
}
@@ -411,6 +513,8 @@ func (b *bbrSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, even
// packet in lost_packets.
var lastPacketSendState sendTimeState
b.maybeAppLimited(priorInFlight)
// Update bytesInFlight
b.bytesInFlight = priorInFlight
for _, p := range ackedPackets {
@@ -508,22 +612,6 @@ func (b *bbrSender) PacingRate() Bandwidth {
return b.pacingRate
}
func (b *bbrSender) hasGoodBandwidthEstimateForResumption() bool {
return b.hasNonAppLimitedSample()
}
func (b *bbrSender) hasNonAppLimitedSample() bool {
return b.hasNoAppLimitedSample
}
// Sets the pacing gain used in STARTUP. Must be greater than 1.
func (b *bbrSender) setHighGain(highGain float64) {
b.highGain = highGain
if b.mode == bbrModeStartup {
b.pacingGain = highGain
}
}
// Sets the CWND gain used in STARTUP. Must be greater than 1.
func (b *bbrSender) setHighCwndGain(highCwndGain float64) {
b.highCwndGain = highCwndGain
@@ -532,11 +620,6 @@ func (b *bbrSender) setHighCwndGain(highCwndGain float64) {
}
}
// Sets the gain used in DRAIN. Must be less than 1.
func (b *bbrSender) setDrainGain(drainGain float64) {
b.drainGain = drainGain
}
// Get the current bandwidth estimate. Note that Bandwidth is in bits per second.
func (b *bbrSender) bandwidthEstimate() Bandwidth {
return b.maxBandwidth.GetBest()
@@ -697,12 +780,7 @@ func (b *bbrSender) checkIfFullBandwidthReached(lastPacketSendState *sendTimeSta
}
func (b *bbrSender) maybeAppLimited(bytesInFlight congestion.ByteCount) {
congestionWindow := b.GetCongestionWindow()
if bytesInFlight >= congestionWindow {
return
}
availableBytes := congestionWindow - bytesInFlight
if availableBytes > maxBbrBurstPackets*b.maxDatagramSize {
if bytesInFlight < b.getTargetCongestionWindow(1) {
b.sampler.OnAppLimited()
}
}
@@ -729,7 +807,7 @@ func (b *bbrSender) maybeEnterOrExitProbeRtt(now monotime.Time, isRoundStart, mi
b.pacingGain = 1.0
// Do not decide on the time to exit PROBE_RTT until the |bytes_in_flight|
// is at the target small value.
b.exitProbeRttAt = monotime.Time(0)
b.exitProbeRttAt = 0
}
if b.mode == bbrModeProbeRtt {
+15 -10
View File
@@ -1,7 +1,6 @@
package congestion
import (
"math"
"time"
"github.com/metacubex/quic-go/congestion"
@@ -9,7 +8,8 @@ import (
)
const (
maxBurstPackets = 10
maxBurstPackets = 10
maxBurstPacingDelayMultiplier = 4
)
// Pacer implements a token bucket pacing algorithm.
@@ -52,22 +52,27 @@ func (p *Pacer) Budget(now monotime.Time) congestion.ByteCount {
func (p *Pacer) maxBurstSize() congestion.ByteCount {
return Max(
congestion.ByteCount((congestion.MinPacingDelay+time.Millisecond).Nanoseconds())*p.getBandwidth()/1e9,
congestion.ByteCount((maxBurstPacingDelayMultiplier*congestion.MinPacingDelay).Nanoseconds())*p.getBandwidth()/1e9,
maxBurstPackets*p.maxDatagramSize,
)
}
// TimeUntilSend returns when the next packet should be sent.
// It returns the zero value of monotime.Time if a packet can be sent immediately.
// It returns the zero value if a packet can be sent immediately.
func (p *Pacer) TimeUntilSend() monotime.Time {
if p.budgetAtLastSent >= p.maxDatagramSize {
return monotime.Time(0)
return 0
}
return p.lastSentTime.Add(Max(
congestion.MinPacingDelay,
time.Duration(math.Ceil(float64(p.maxDatagramSize-p.budgetAtLastSent)*1e9/
float64(p.getBandwidth())))*time.Nanosecond,
))
diff := 1e9 * uint64(p.maxDatagramSize-p.budgetAtLastSent)
bw := uint64(p.getBandwidth())
// We might need to round up this value.
// Otherwise, we might have a budget (slightly) smaller than the datagram size when the timer expires.
d := diff / bw
// this is effectively a math.Ceil, but using only integer math
if diff%bw > 0 {
d++
}
return p.lastSentTime.Add(Max(congestion.MinPacingDelay, time.Duration(d)*time.Nanosecond))
}
func (p *Pacer) SetMaxDatagramSize(s congestion.ByteCount) {
@@ -152,7 +152,7 @@ func (p *packetNumberIndexedQueue[T]) EntrySlotsUsed() int {
return p.entries.Len()
}
// LastPacket returns packet number of the first entry in the queue.
// FirstPacket returns packet number of the first entry in the queue.
func (p *packetNumberIndexedQueue[T]) FirstPacket() (packetNumber congestion.PacketNumber) {
return p.firstPacket
}

Some files were not shown because too many files have changed in this diff Show More