mirror of
https://github.com/bolucat/Archive.git
synced 2026-04-22 16:07:49 +08:00
Update On Tue Apr 14 21:27:45 CEST 2026
This commit is contained in:
@@ -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
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
+2
-2
@@ -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"
|
||||
|
||||
+6
-2
@@ -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(
|
||||
|
||||
+194
@@ -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>
|
||||
// )
|
||||
}
|
||||
+73
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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";
|
||||
+ };
|
||||
};
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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" +
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
"allowLoopbackIP": true
|
||||
}
|
||||
],
|
||||
"advancedSettings": {
|
||||
"userHintIsMandatory": true
|
||||
},
|
||||
"loggingLevel": "DEBUG",
|
||||
"mtu": 1400,
|
||||
"trafficPattern": {
|
||||
|
||||
@@ -24,5 +24,8 @@
|
||||
"allowLoopbackIP": true
|
||||
}
|
||||
],
|
||||
"advancedSettings": {
|
||||
"userHintIsMandatory": true
|
||||
},
|
||||
"loggingLevel": "INFO"
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
"allowLoopbackIP": true
|
||||
}
|
||||
],
|
||||
"advancedSettings": {
|
||||
"userHintIsMandatory": true
|
||||
},
|
||||
"loggingLevel": "INFO",
|
||||
"mtu": 1400
|
||||
}
|
||||
|
||||
Vendored
+2
-2
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user