Update On Wed Apr 8 21:30:19 CEST 2026

This commit is contained in:
github-action[bot]
2026-04-08 21:30:20 +02:00
parent 3c2aac940c
commit e1ca2df947
174 changed files with 7998 additions and 91798 deletions
+1
View File
@@ -1323,3 +1323,4 @@ Update On Sat Apr 4 20:56:37 CEST 2026
Update On Sun Apr 5 20:57:38 CEST 2026
Update On Mon Apr 6 21:13:19 CEST 2026
Update On Tue Apr 7 21:18:54 CEST 2026
Update On Wed Apr 8 21:30:11 CEST 2026
+2 -2
View File
@@ -58,8 +58,8 @@ subprojects {
minSdk = 21
targetSdk = 35
versionName = "2.11.25"
versionCode = 211025
versionName = "2.11.26"
versionCode = 211026
resValue("string", "release_name", "v$versionName")
resValue("integer", "release_code", "$versionCode")
@@ -1,36 +0,0 @@
From b8f897a9da7a82ad8584a22284ceac61262fcb7e Mon Sep 17 00:00:00 2001
From: Jorropo <jorropo.pgm@gmail.com>
Date: Sun, 22 Feb 2026 01:47:45 +0100
Subject: [PATCH] runtime: fix value of ENOSYS on mips from 38 to 89
Fixes #77731
Change-Id: Iaca444e2d5f9e19fd2de38414b357b41471a668c
---
diff --git a/src/runtime/defs_linux_mips64x.go b/src/runtime/defs_linux_mips64x.go
index 7449d2c..4d0f103 100644
--- a/src/runtime/defs_linux_mips64x.go
+++ b/src/runtime/defs_linux_mips64x.go
@@ -12,7 +12,7 @@
_EINTR = 0x4
_EAGAIN = 0xb
_ENOMEM = 0xc
- _ENOSYS = 0x26
+ _ENOSYS = 0x59
_PROT_NONE = 0x0
_PROT_READ = 0x1
diff --git a/src/runtime/defs_linux_mipsx.go b/src/runtime/defs_linux_mipsx.go
index 5a446e0..b8da4d0 100644
--- a/src/runtime/defs_linux_mipsx.go
+++ b/src/runtime/defs_linux_mipsx.go
@@ -12,7 +12,7 @@
_EINTR = 0x4
_EAGAIN = 0xb
_ENOMEM = 0xc
- _ENOSYS = 0x26
+ _ENOSYS = 0x59
_PROT_NONE = 0x0
_PROT_READ = 0x1
@@ -1,265 +0,0 @@
From 1a44be4cecdc742ac6cce9825f9ffc19857c99f3 Mon Sep 17 00:00:00 2001
From: database64128 <free122448@hotmail.com>
Date: Mon, 9 Mar 2026 16:25:16 +0800
Subject: [PATCH] [release-branch.go1.26] internal/poll: move rsan to heap on
windows
According to https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsarecvfrom,
the memory pointed to by lpFromlen must remain available during the
overlapped I/O, and therefore cannot be allocated on the stack.
CL 685417 moved the rsan field out of the operation struct and placed
it on stack, which violates the above requirement and causes stack
corruption.
Unfortunately, it is no longer possible to cleanly revert CL 685417.
Instead of attempting to revert it, this CL bundles rsan together
with rsa in the same sync.Pool. The new wsaRsa struct is still in the
same size class, so no additional overhead is introduced by this
change.
Fixes #78041.
Change-Id: I5ffbccb332515116ddc03fb7c40ffc9293cad2ab
Reviewed-on: https://go-review.googlesource.com/c/go/+/753040
Reviewed-by: Quim Muntal <quimmuntal@gmail.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
Commit-Queue: Cherry Mui <cherryyz@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-on: https://go-review.googlesource.com/c/go/+/753480
Reviewed-by: Mark Freeman <markfreeman@google.com>
---
src/internal/poll/fd_windows.go | 94 +++++++++++++++++++++------------
1 file changed, 59 insertions(+), 35 deletions(-)
diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
index 2ba967f990982f..26319548e3c310 100644
--- a/src/internal/poll/fd_windows.go
+++ b/src/internal/poll/fd_windows.go
@@ -149,7 +149,7 @@ var wsaMsgPool = sync.Pool{
// newWSAMsg creates a new WSAMsg with the provided parameters.
// Use [freeWSAMsg] to free it.
-func newWSAMsg(p []byte, oob []byte, flags int, unconnected bool) *windows.WSAMsg {
+func newWSAMsg(p []byte, oob []byte, flags int, rsa *wsaRsa) *windows.WSAMsg {
// The returned object can't be allocated in the stack because it is accessed asynchronously
// by Windows in between several system calls. If the stack frame is moved while that happens,
// then Windows may access invalid memory.
@@ -166,34 +166,46 @@ func newWSAMsg(p []byte, oob []byte, flags int, unconnected bool) *windows.WSAMs
}
}
msg.Flags = uint32(flags)
- if unconnected {
- msg.Name = wsaRsaPool.Get().(*syscall.RawSockaddrAny)
- msg.Namelen = int32(unsafe.Sizeof(syscall.RawSockaddrAny{}))
+ if rsa != nil {
+ msg.Name = &rsa.name
+ msg.Namelen = rsa.namelen
}
return msg
}
func freeWSAMsg(msg *windows.WSAMsg) {
// Clear pointers to buffers so they can be released by garbage collector.
+ msg.Name = nil
+ msg.Namelen = 0
msg.Buffers.Len = 0
msg.Buffers.Buf = nil
msg.Control.Len = 0
msg.Control.Buf = nil
- if msg.Name != nil {
- *msg.Name = syscall.RawSockaddrAny{}
- wsaRsaPool.Put(msg.Name)
- msg.Name = nil
- msg.Namelen = 0
- }
wsaMsgPool.Put(msg)
}
+// wsaRsa bundles a [syscall.RawSockaddrAny] with its length for efficient caching.
+//
+// When used by WSARecvFrom, wsaRsa must be on the heap. See
+// https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsarecvfrom.
+type wsaRsa struct {
+ name syscall.RawSockaddrAny
+ namelen int32
+}
+
var wsaRsaPool = sync.Pool{
New: func() any {
- return new(syscall.RawSockaddrAny)
+ return new(wsaRsa)
},
}
+func newWSARsa() *wsaRsa {
+ rsa := wsaRsaPool.Get().(*wsaRsa)
+ rsa.name = syscall.RawSockaddrAny{}
+ rsa.namelen = int32(unsafe.Sizeof(syscall.RawSockaddrAny{}))
+ return rsa
+}
+
var operationPool = sync.Pool{
New: func() any {
return new(operation)
@@ -739,19 +751,18 @@ func (fd *FD) ReadFrom(buf []byte) (int, syscall.Sockaddr, error) {
fd.pin('r', &buf[0])
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
+ rsa := newWSARsa()
defer wsaRsaPool.Put(rsa)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
- rsan := int32(unsafe.Sizeof(*rsa))
var flags uint32
- err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, rsa, &rsan, &o.o, nil)
+ err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, &rsa.name, &rsa.namelen, &o.o, nil)
return qty, err
})
err = fd.eofError(n, err)
if err != nil {
return n, nil, err
}
- sa, _ := rsa.Sockaddr()
+ sa, _ := rsa.name.Sockaddr()
return n, sa, nil
}
@@ -770,19 +781,18 @@ func (fd *FD) ReadFromInet4(buf []byte, sa4 *syscall.SockaddrInet4) (int, error)
fd.pin('r', &buf[0])
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
+ rsa := newWSARsa()
defer wsaRsaPool.Put(rsa)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
- rsan := int32(unsafe.Sizeof(*rsa))
var flags uint32
- err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, rsa, &rsan, &o.o, nil)
+ err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, &rsa.name, &rsa.namelen, &o.o, nil)
return qty, err
})
err = fd.eofError(n, err)
if err != nil {
return n, err
}
- rawToSockaddrInet4(rsa, sa4)
+ rawToSockaddrInet4(&rsa.name, sa4)
return n, err
}
@@ -801,19 +811,18 @@ func (fd *FD) ReadFromInet6(buf []byte, sa6 *syscall.SockaddrInet6) (int, error)
fd.pin('r', &buf[0])
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
+ rsa := newWSARsa()
defer wsaRsaPool.Put(rsa)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
- rsan := int32(unsafe.Sizeof(*rsa))
var flags uint32
- err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, rsa, &rsan, &o.o, nil)
+ err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, &rsa.name, &rsa.namelen, &o.o, nil)
return qty, err
})
err = fd.eofError(n, err)
if err != nil {
return n, err
}
- rawToSockaddrInet6(rsa, sa6)
+ rawToSockaddrInet6(&rsa.name, sa6)
return n, err
}
@@ -1373,7 +1382,9 @@ func (fd *FD) ReadMsg(p []byte, oob []byte, flags int) (int, int, int, syscall.S
p = p[:maxRW]
}
- msg := newWSAMsg(p, oob, flags, true)
+ rsa := newWSARsa()
+ defer wsaRsaPool.Put(rsa)
+ msg := newWSAMsg(p, oob, flags, rsa)
defer freeWSAMsg(msg)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
@@ -1398,7 +1409,9 @@ func (fd *FD) ReadMsgInet4(p []byte, oob []byte, flags int, sa4 *syscall.Sockadd
p = p[:maxRW]
}
- msg := newWSAMsg(p, oob, flags, true)
+ rsa := newWSARsa()
+ defer wsaRsaPool.Put(rsa)
+ msg := newWSAMsg(p, oob, flags, rsa)
defer freeWSAMsg(msg)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
@@ -1422,7 +1435,9 @@ func (fd *FD) ReadMsgInet6(p []byte, oob []byte, flags int, sa6 *syscall.Sockadd
p = p[:maxRW]
}
- msg := newWSAMsg(p, oob, flags, true)
+ rsa := newWSARsa()
+ defer wsaRsaPool.Put(rsa)
+ msg := newWSAMsg(p, oob, flags, rsa)
defer freeWSAMsg(msg)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
@@ -1446,15 +1461,18 @@ func (fd *FD) WriteMsg(p []byte, oob []byte, sa syscall.Sockaddr) (int, int, err
}
defer fd.writeUnlock()
- msg := newWSAMsg(p, oob, 0, sa != nil)
- defer freeWSAMsg(msg)
+ var rsa *wsaRsa
if sa != nil {
+ rsa = newWSARsa()
+ defer wsaRsaPool.Put(rsa)
var err error
- msg.Namelen, err = sockaddrToRaw(msg.Name, sa)
+ rsa.namelen, err = sockaddrToRaw(&rsa.name, sa)
if err != nil {
return 0, 0, err
}
}
+ msg := newWSAMsg(p, oob, 0, rsa)
+ defer freeWSAMsg(msg)
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
return qty, err
@@ -1473,11 +1491,14 @@ func (fd *FD) WriteMsgInet4(p []byte, oob []byte, sa *syscall.SockaddrInet4) (in
}
defer fd.writeUnlock()
- msg := newWSAMsg(p, oob, 0, sa != nil)
- defer freeWSAMsg(msg)
+ var rsa *wsaRsa
if sa != nil {
- msg.Namelen = sockaddrInet4ToRaw(msg.Name, sa)
+ rsa = newWSARsa()
+ defer wsaRsaPool.Put(rsa)
+ rsa.namelen = sockaddrInet4ToRaw(&rsa.name, sa)
}
+ msg := newWSAMsg(p, oob, 0, rsa)
+ defer freeWSAMsg(msg)
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
return qty, err
@@ -1496,11 +1517,14 @@ func (fd *FD) WriteMsgInet6(p []byte, oob []byte, sa *syscall.SockaddrInet6) (in
}
defer fd.writeUnlock()
- msg := newWSAMsg(p, oob, 0, sa != nil)
- defer freeWSAMsg(msg)
+ var rsa *wsaRsa
if sa != nil {
- msg.Namelen = sockaddrInet6ToRaw(msg.Name, sa)
+ rsa = newWSARsa()
+ defer wsaRsaPool.Put(rsa)
+ rsa.namelen = sockaddrInet6ToRaw(&rsa.name, sa)
}
+ msg := newWSAMsg(p, oob, 0, rsa)
+ defer freeWSAMsg(msg)
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
return qty, err
@@ -32,7 +32,7 @@ jobs:
- { goos: linux, goarch: '386', go386: sse2, output: '386', debian: i386, rpm: i386}
- { goos: linux, goarch: '386', go386: softfloat, output: '386-softfloat' }
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible} # old style file name will be removed in next released
- { goos: linux, goarch: amd64, goamd64: v3, output: amd64, debian: amd64, rpm: x86_64, pacman: x86_64}
- { goos: linux, goarch: amd64, goamd64: v3, output: amd64, debian: amd64, rpm: x86_64, pacman: x86_64, tarball: tarball}
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-v1, debian: amd64, rpm: x86_64, pacman: x86_64, test: test }
- { goos: linux, goarch: amd64, goamd64: v2, output: amd64-v2, debian: amd64, rpm: x86_64, pacman: x86_64}
- { goos: linux, goarch: amd64, goamd64: v3, output: amd64-v3, debian: amd64, rpm: x86_64, pacman: x86_64}
@@ -186,20 +186,6 @@ jobs:
- name: Verify Go env
run: go env
# TODO: remove after issue77731 fixed, see: https://github.com/golang/go/issues/77731
- name: Fix issue77731 for Golang1.26
if: ${{ matrix.jobs.goversion == '' }}
run: |
cd $(go env GOROOT)
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/issue77731.patch
# TODO: remove after issue77975 fixed, see: https://github.com/golang/go/issues/77975
- name: Fix issue77975 for Golang1.26
if: ${{ matrix.jobs.goversion == '' }}
run: |
cd $(go env GOROOT)
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/issue77975.patch
# TODO: remove after issue77930 fixed, see: https://github.com/golang/go/issues/77930
- name: Fix issue77930 for Golang1.26
if: ${{ matrix.jobs.goversion == '' }}
@@ -312,6 +298,21 @@ jobs:
--architecture ${{ matrix.jobs.pacman }} \
mihomo=/usr/bin/mihomo
- name: Pack Golang Toolchain
if: ${{ matrix.jobs.goversion == '' && matrix.jobs.tarball == 'tarball' }}
run: |
mkdir -p $GITHUB_WORKSPACE/toolchain/go
cp -r $(go env GOROOT)/* $GITHUB_WORKSPACE/toolchain/go/
cd $GITHUB_WORKSPACE/toolchain/
tar -czf $GITHUB_WORKSPACE/toolchain.tar.gz .
rm -rf $GITHUB_WORKSPACE/toolchain
- name: Pack GoMod Vendor
if: ${{ matrix.jobs.goversion == '' && matrix.jobs.tarball == 'tarball' }}
run: |
go mod vendor
tar -czf $GITHUB_WORKSPACE/vendor.tar.gz vendor
- name: Save version
run: |
echo ${VERSION} > version.txt
@@ -327,6 +328,8 @@ jobs:
mihomo*.rpm
mihomo*.pkg.tar.zst
mihomo*.zip
toolchain.tar.gz
vendor.tar.gz
version.txt
checksums.txt
@@ -22,6 +22,7 @@ jobs:
- 'windows-latest' # amd64 windows
- 'macos-latest' # arm64 macos
- 'ubuntu-24.04-arm' # arm64 linux
- 'windows-11-arm' # arm64 windows
- 'macos-15-intel' # amd64 macos
go-version:
- '1.26'
@@ -56,13 +57,6 @@ jobs:
- name: Verify Go env
run: go env
# TODO: remove after issue77975 fixed, see: https://github.com/golang/go/issues/77975
- name: Fix issue77975 for Golang1.26
if: ${{ matrix.go-version == '1.26' }}
run: |
cd $(go env GOROOT)
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/issue77975.patch
- name: Remove inbound test for macOS
if: ${{ runner.os == 'macOS' }}
run: |
@@ -27,7 +27,7 @@ type Trojan struct {
hexPassword [trojan.KeyLength]byte
// for gun mux
gunTransport *gun.Transport
gunClient *gun.Client
realityConfig *tlsC.RealityConfig
echConfig *ech.Config
@@ -115,7 +115,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts)
case "grpc":
break // already handle in gun transport
break // already handle in dialContext
default:
// default tcp network
// handle TLS
@@ -175,7 +175,7 @@ func (t *Trojan) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C
func (t *Trojan) dialContext(ctx context.Context) (c net.Conn, err error) {
switch t.option.Network {
case "grpc": // gun transport
return t.gunTransport.Dial()
return t.gunClient.Dial()
default:
}
return t.dialer.DialContext(ctx, "tcp", t.addr)
@@ -236,10 +236,13 @@ func (t *Trojan) ProxyInfo() C.ProxyInfo {
// Close implements C.ProxyAdapter
func (t *Trojan) Close() error {
if t.gunTransport != nil {
return t.gunTransport.Close()
var errs []error
if t.gunClient != nil {
if err := t.gunClient.Close(); err != nil {
errs = append(errs, err)
}
}
return nil
return errors.Join(errs...)
}
func NewTrojan(option TrojanOption) (*Trojan, error) {
@@ -320,7 +323,14 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
PingInterval: option.GrpcOpts.PingInterval,
}
t.gunTransport = gun.NewTransport(dialFn, tlsConfig, gunConfig)
t.gunClient = gun.NewClient(
func() *gun.Transport {
return gun.NewTransport(dialFn, tlsConfig, gunConfig)
},
option.GrpcOpts.MaxConnections,
option.GrpcOpts.MinStreams,
option.GrpcOpts.MaxStreams,
)
}
return t, nil
@@ -14,7 +14,7 @@ import (
type TrustTunnel struct {
*Base
client *trusttunnel.Client
client *trusttunnel.PoolClient
option *TrustTunnelOption
}
@@ -35,10 +35,14 @@ type TrustTunnelOption struct {
PrivateKey string `proxy:"private-key,omitempty"`
UDP bool `proxy:"udp,omitempty"`
HealthCheck bool `proxy:"health-check,omitempty"`
// quic options
Quic bool `proxy:"quic,omitempty"`
CongestionController string `proxy:"congestion-controller,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
// reuse options
MaxConnections int `proxy:"max-connections,omitempty"`
MinStreams int `proxy:"min-streams,omitempty"`
MaxStreams int `proxy:"max-streams,omitempty"`
}
func (t *TrustTunnel) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
@@ -114,6 +118,9 @@ func NewTrustTunnel(option TrustTunnelOption) (*TrustTunnel, error) {
QUICCongestionControl: option.CongestionController,
QUICCwnd: option.CWND,
HealthCheck: option.HealthCheck,
MaxConnections: option.MaxConnections,
MinStreams: option.MinStreams,
MaxStreams: option.MaxStreams,
}
echConfig, err := option.ECHOpts.Parse()
if err != nil {
@@ -134,7 +141,7 @@ func NewTrustTunnel(option TrustTunnelOption) (*TrustTunnel, error) {
}
tOption.TLSConfig = tlsConfig
client, err := trusttunnel.NewClient(context.TODO(), tOption)
client, err := trusttunnel.NewPoolClient(context.TODO(), tOption)
if err != nil {
return nil, err
}
@@ -36,7 +36,7 @@ type Vless struct {
encryption *encryption.ClientInstance
// for gun mux
gunTransport *gun.Transport
gunClient *gun.Client
// for xhttp
xhttpClient *xhttp.Client
@@ -76,22 +76,34 @@ 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"`
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 int `proxy:"sc-max-each-post-bytes,omitempty"`
ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX
DownloadSettings *XHTTPDownloadSettings `proxy:"download-settings,omitempty"`
}
type XHTTPReuseSettings struct {
MaxConnections string `proxy:"max-connections,omitempty"`
MaxConcurrency string `proxy:"max-concurrency,omitempty"`
CMaxReuseTimes string `proxy:"c-max-reuse-times,omitempty"`
HMaxRequestTimes string `proxy:"h-max-request-times,omitempty"`
HMaxReusableSecs string `proxy:"h-max-reusable-secs,omitempty"`
}
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"`
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 *int `proxy:"sc-max-each-post-bytes,omitempty"`
ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX
// proxy part
Server *string `proxy:"server,omitempty"`
Port *int `proxy:"port,omitempty"`
@@ -133,7 +145,6 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
wsOpts.TLS = true
wsOpts.TLSConfig, err = ca.GetTLSConfig(ca.Option{
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
ServerName: host,
InsecureSkipVerify: v.option.SkipCertVerify,
NextProtos: []string{"http/1.1"},
@@ -187,9 +198,9 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c, err = vmess.StreamH2Conn(ctx, c, h2Opts)
case "grpc":
break // already handle in gun transport
break // already handle in dialContext
case "xhttp":
break // already handle in xhttp client
break // already handle in dialContext
default:
// default tcp network
// handle TLS
@@ -271,7 +282,7 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne
func (v *Vless) dialContext(ctx context.Context) (c net.Conn, err error) {
switch v.option.Network {
case "grpc": // gun transport
return v.gunTransport.Dial()
return v.gunClient.Dial()
case "xhttp":
return v.xhttpClient.Dial()
default:
@@ -349,8 +360,8 @@ func (v *Vless) ProxyInfo() C.ProxyInfo {
// Close implements C.ProxyAdapter
func (v *Vless) Close() error {
var errs []error
if v.gunTransport != nil {
if err := v.gunTransport.Close(); err != nil {
if v.gunClient != nil {
if err := v.gunClient.Close(); err != nil {
errs = append(errs, err)
}
}
@@ -496,7 +507,14 @@ func NewVless(option VlessOption) (*Vless, error) {
}
}
v.gunTransport = gun.NewTransport(dialFn, tlsConfig, gunConfig)
v.gunClient = gun.NewClient(
func() *gun.Transport {
return gun.NewTransport(dialFn, tlsConfig, gunConfig)
},
option.GrpcOpts.MaxConnections,
option.GrpcOpts.MinStreams,
option.GrpcOpts.MaxStreams,
)
case "xhttp":
requestHost := v.option.XHTTPOpts.Host
if requestHost == "" {
@@ -507,13 +525,26 @@ func NewVless(option VlessOption) (*Vless, error) {
}
}
var reuseCfg *xhttp.ReuseConfig
if option.XHTTPOpts.ReuseSettings != nil {
reuseCfg = &xhttp.ReuseConfig{
MaxConnections: option.XHTTPOpts.ReuseSettings.MaxConnections,
MaxConcurrency: option.XHTTPOpts.ReuseSettings.MaxConcurrency,
CMaxReuseTimes: option.XHTTPOpts.ReuseSettings.CMaxReuseTimes,
HMaxRequestTimes: option.XHTTPOpts.ReuseSettings.HMaxRequestTimes,
HMaxReusableSecs: option.XHTTPOpts.ReuseSettings.HMaxReusableSecs,
}
}
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,
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,
}
makeTransport := func() http.RoundTripper {
@@ -524,6 +555,7 @@ func NewVless(option VlessOption) (*Vless, error) {
func(ctx context.Context, raw net.Conn, isH2 bool) (net.Conn, error) {
return v.streamTLSConn(ctx, raw, isH2)
},
v.option.ALPN,
)
}
var makeDownloadTransport func() http.RoundTripper
@@ -569,13 +601,26 @@ func NewVless(option VlessOption) (*Vless, error) {
}
}
downloadReuseCfg := reuseCfg
if ds.ReuseSettings != nil {
downloadReuseCfg = &xhttp.ReuseConfig{
MaxConnections: ds.ReuseSettings.MaxConnections,
MaxConcurrency: ds.ReuseSettings.MaxConcurrency,
CMaxReuseTimes: ds.ReuseSettings.CMaxReuseTimes,
HMaxRequestTimes: ds.ReuseSettings.HMaxRequestTimes,
HMaxReusableSecs: ds.ReuseSettings.HMaxReusableSecs,
}
}
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),
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,
}
makeDownloadTransport = func() http.RoundTripper {
@@ -612,6 +657,7 @@ func NewVless(option VlessOption) (*Vless, error) {
return conn, nil
},
downloadALPN,
)
}
}
@@ -34,7 +34,7 @@ type Vmess struct {
option *VmessOption
// for gun mux
gunTransport *gun.Transport
gunClient *gun.Client
realityConfig *tlsC.RealityConfig
echConfig *ech.Config
@@ -86,6 +86,9 @@ type GrpcOptions struct {
GrpcServiceName string `proxy:"grpc-service-name,omitempty"`
GrpcUserAgent string `proxy:"grpc-user-agent,omitempty"`
PingInterval int `proxy:"ping-interval,omitempty"`
MaxConnections int `proxy:"max-connections,omitempty"`
MinStreams int `proxy:"min-streams,omitempty"`
MaxStreams int `proxy:"max-streams,omitempty"`
}
type WSOptions struct {
@@ -145,24 +148,9 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c, err = mihomoVMess.StreamWebsocketConn(ctx, c, wsOpts)
case "http":
// readability first, so just copy default TLS logic
if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := &mihomoVMess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
}
if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName
}
c, err = mihomoVMess.StreamTLSConn(ctx, c, tlsOpts)
if err != nil {
return nil, err
}
c, err = v.streamTLSConn(ctx, c, false)
if err != nil {
return nil, err
}
host, _, _ := net.SplitHostPort(v.addr)
@@ -175,23 +163,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c = mihomoVMess.StreamHTTPConn(c, httpOpts)
case "h2":
host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := mihomoVMess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
Certificate: v.option.Certificate,
PrivateKey: v.option.PrivateKey,
NextProtos: []string{"h2"},
ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig,
}
if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName
}
c, err = mihomoVMess.StreamTLSConn(ctx, c, &tlsOpts)
c, err = v.streamTLSConn(ctx, c, true)
if err != nil {
return nil, err
}
@@ -203,29 +175,11 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c, err = mihomoVMess.StreamH2Conn(ctx, c, h2Opts)
case "grpc":
break // already handle in gun transport
break // already handle in dialContext
default:
// default tcp network
// handle TLS
if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := &mihomoVMess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
Certificate: v.option.Certificate,
PrivateKey: v.option.PrivateKey,
ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
}
if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName
}
c, err = mihomoVMess.StreamTLSConn(ctx, c, tlsOpts)
}
c, err = v.streamTLSConn(ctx, c, false)
}
if err != nil {
@@ -290,10 +244,40 @@ func (v *Vmess) streamConnContext(ctx context.Context, c net.Conn, metadata *C.M
return
}
func (v *Vmess) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error) {
if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := mihomoVMess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
Certificate: v.option.Certificate,
PrivateKey: v.option.PrivateKey,
ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
}
if isH2 {
tlsOpts.NextProtos = []string{"h2"}
}
if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName
}
return mihomoVMess.StreamTLSConn(ctx, conn, &tlsOpts)
}
return conn, nil
}
func (v *Vmess) dialContext(ctx context.Context) (c net.Conn, err error) {
switch v.option.Network {
case "grpc": // gun transport
return v.gunTransport.Dial()
return v.gunClient.Dial()
default:
}
return v.dialer.DialContext(ctx, "tcp", v.addr)
@@ -350,10 +334,13 @@ func (v *Vmess) ProxyInfo() C.ProxyInfo {
// Close implements C.ProxyAdapter
func (v *Vmess) Close() error {
if v.gunTransport != nil {
return v.gunTransport.Close()
var errs []error
if v.gunClient != nil {
if err := v.gunClient.Close(); err != nil {
errs = append(errs, err)
}
}
return nil
return errors.Join(errs...)
}
// SupportUOT implements C.ProxyAdapter
@@ -457,7 +444,14 @@ func NewVmess(option VmessOption) (*Vmess, error) {
}
}
v.gunTransport = gun.NewTransport(dialFn, tlsConfig, gunConfig)
v.gunClient = gun.NewClient(
func() *gun.Transport {
return gun.NewTransport(dialFn, tlsConfig, gunConfig)
},
option.GrpcOpts.MaxConnections,
option.GrpcOpts.MinStreams,
option.GrpcOpts.MaxStreams,
)
}
return v, nil
@@ -1,6 +1,7 @@
package convert
import (
"encoding/json"
"errors"
"fmt"
"net/url"
@@ -134,6 +135,137 @@ func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy m
grpcOpts := make(map[string]any)
grpcOpts["grpc-service-name"] = query.Get("serviceName")
proxy["grpc-opts"] = grpcOpts
case "xhttp":
proxy["network"] = "xhttp"
xhttpOpts := make(map[string]any)
if path := query.Get("path"); path != "" {
xhttpOpts["path"] = path
}
if host := query.Get("host"); host != "" {
xhttpOpts["host"] = host
}
if mode := query.Get("mode"); mode != "" {
xhttpOpts["mode"] = mode
}
if extra := query.Get("extra"); extra != "" {
var extraMap map[string]any
if err := json.Unmarshal([]byte(extra), &extraMap); err == nil {
parseXHTTPExtra(extraMap, xhttpOpts)
}
}
proxy["xhttp-opts"] = xhttpOpts
}
return nil
}
// parseXHTTPExtra maps xray-core extra JSON fields to mihomo xhttp-opts fields.
func parseXHTTPExtra(extra map[string]any, opts map[string]any) {
// xmuxToReuse converts an xmux map to mihomo reuse-settings.
xmuxToReuse := func(xmux map[string]any) map[string]any {
reuse := make(map[string]any)
set := func(src, dst string) {
if v, ok := xmux[src]; ok {
switch val := v.(type) {
case string:
if val != "" {
reuse[dst] = val
}
case float64:
reuse[dst] = strconv.FormatInt(int64(val), 10)
}
}
}
set("maxConnections", "max-connections")
set("maxConcurrency", "max-concurrency")
set("cMaxReuseTimes", "c-max-reuse-times")
set("hMaxRequestTimes", "h-max-request-times")
set("hMaxReusableSecs", "h-max-reusable-secs")
return reuse
}
if v, ok := extra["noGRPCHeader"].(bool); ok && v {
opts["no-grpc-header"] = true
}
if v, ok := extra["xPaddingBytes"].(string); ok && v != "" {
opts["x-padding-bytes"] = v
}
// xmux in root extra → reuse-settings
if xmuxAny, ok := extra["xmux"].(map[string]any); ok && len(xmuxAny) > 0 {
if reuse := xmuxToReuse(xmuxAny); len(reuse) > 0 {
opts["reuse-settings"] = reuse
}
}
if dsAny, ok := extra["downloadSettings"].(map[string]any); ok {
ds := make(map[string]any)
if addr, ok := dsAny["address"].(string); ok && addr != "" {
ds["server"] = addr
}
if port, ok := dsAny["port"].(float64); ok {
ds["port"] = int(port)
}
if sec, ok := dsAny["security"].(string); ok && strings.ToLower(sec) == "tls" {
ds["tls"] = true
}
if tlsAny, ok := dsAny["tlsSettings"].(map[string]any); ok {
if sn, ok := tlsAny["serverName"].(string); ok && sn != "" {
ds["servername"] = sn
}
if fp, ok := tlsAny["fingerprint"].(string); ok && fp != "" {
ds["client-fingerprint"] = fp
}
if alpnAny, ok := tlsAny["alpn"].([]any); ok && len(alpnAny) > 0 {
alpnList := make([]string, 0, len(alpnAny))
for _, a := range alpnAny {
if s, ok := a.(string); ok {
alpnList = append(alpnList, s)
}
}
if len(alpnList) > 0 {
ds["alpn"] = alpnList
}
}
}
if xhttpAny, ok := dsAny["xhttpSettings"].(map[string]any); ok {
if path, ok := xhttpAny["path"].(string); ok && path != "" {
ds["path"] = path
}
if host, ok := xhttpAny["host"].(string); ok && host != "" {
ds["host"] = host
}
if v, ok := xhttpAny["noGRPCHeader"].(bool); ok && v {
ds["no-grpc-header"] = true
}
if v, ok := xhttpAny["xPaddingBytes"].(string); ok && v != "" {
ds["x-padding-bytes"] = v
}
// xmux inside downloadSettings.xhttpSettings.extra → download-settings.reuse-settings
if dsExtraAny, ok := xhttpAny["extra"].(map[string]any); ok {
if xmuxAny, ok := dsExtraAny["xmux"].(map[string]any); ok && len(xmuxAny) > 0 {
if reuse := xmuxToReuse(xmuxAny); len(reuse) > 0 {
ds["reuse-settings"] = reuse
}
}
}
}
if len(ds) > 0 {
opts["download-settings"] = ds
}
}
}
@@ -308,6 +308,18 @@ func BuildRemovedX25519MLKEM768HandshakeState(c *UConn) error {
return nil
}
func GetTLSConnectionState(conn net.Conn) (tlsState tls.ConnectionState) {
switch tlsConn := conn.(type) {
case interface{ ConnectionState() tls.ConnectionState }:
state := tlsConn.ConnectionState()
return state
case interface{ ConnectionState() utls.ConnectionState }:
state := tlsConn.ConnectionState()
return tlsConnectionState(state)
}
return
}
var globalFingerprint string
func SetGlobalFingerprint(fingerprint string) {
@@ -302,6 +302,8 @@ type RawTun struct {
IncludeAndroidUser []int `yaml:"include-android-user" json:"include-android-user,omitempty"`
IncludePackage []string `yaml:"include-package" json:"include-package,omitempty"`
ExcludePackage []string `yaml:"exclude-package" json:"exclude-package,omitempty"`
IncludeMACAddress []string `yaml:"include-mac-address" json:"include-mac-address,omitempty"`
ExcludeMACAddress []string `yaml:"exclude-mac-address" json:"exclude-mac-address,omitempty"`
EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"`
UDPTimeout int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"`
DisableICMPForwarding bool `yaml:"disable-icmp-forwarding" json:"disable-icmp-forwarding,omitempty"`
@@ -1680,6 +1682,8 @@ func parseTun(rawTun RawTun, dns *DNS, general *General) error {
IncludeAndroidUser: rawTun.IncludeAndroidUser,
IncludePackage: rawTun.IncludePackage,
ExcludePackage: rawTun.ExcludePackage,
IncludeMACAddress: rawTun.IncludeMACAddress,
ExcludeMACAddress: rawTun.ExcludeMACAddress,
EndpointIndependentNat: rawTun.EndpointIndependentNat,
UDPTimeout: rawTun.UDPTimeout,
DisableICMPForwarding: rawTun.DisableICMPForwarding,
@@ -171,6 +171,10 @@ tun:
#- 1000
# exclude-uid-range: # 排除路由的的用户范围
# - 1000:9999
# include-mac-address:
# - 00:11:22:33:44:55
# exclude-mac-address:
# - 00:11:22:33:44:55
# Android 用户和应用规则仅在 Android 下被支持
# 并且需要 auto-route
@@ -670,6 +674,9 @@ proxies: # socks5
grpc-service-name: "example"
# grpc-user-agent: "grpc-go/1.36.0"
# ping-interval: 0 # 默认关闭,单位为秒
# max-connections: 1 # Maximum connections. Conflict with max-streams.
# min-streams: 0 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams.
# max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams.
# ip-version: ipv4
# vless
@@ -761,6 +768,9 @@ proxies: # socks5
grpc-service-name: "grpc"
# grpc-user-agent: "grpc-go/1.36.0"
# ping-interval: 0 # 默认关闭,单位为秒
# max-connections: 1 # Maximum connections. Conflict with max-streams.
# min-streams: 0 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams.
# max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams.
reality-opts:
public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE
@@ -816,6 +826,13 @@ proxies: # socks5
# X-Forwarded-For: ""
# no-grpc-header: false
# x-padding-bytes: "100-1000"
# sc-max-each-post-bytes: 1000000
# reuse-settings: # aka XMUX
# max-connections: "16-32"
# max-concurrency: "0"
# c-max-reuse-times: "0"
# h-max-request-times: "600-900"
# h-max-reusable-secs: "1800-3000"
# download-settings:
# ## xhttp part
# path: "/"
@@ -824,6 +841,13 @@ proxies: # socks5
# X-Forwarded-For: ""
# no-grpc-header: false
# x-padding-bytes: "100-1000"
# sc-max-each-post-bytes: 1000000
# reuse-settings: # aka XMUX
# max-connections: "16-32"
# max-concurrency: "0"
# c-max-reuse-times: "0"
# h-max-request-times: "600-900"
# h-max-reusable-secs: "1800-3000"
# ## proxy part
# server: server
# port: 443
@@ -884,6 +908,9 @@ proxies: # socks5
grpc-service-name: "example"
# grpc-user-agent: "grpc-go/1.36.0"
# ping-interval: 0 # 默认关闭,单位为秒
# max-connections: 1 # Maximum connections. Conflict with max-streams.
# min-streams: 0 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams.
# max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams.
- name: trojan-ws
server: server
@@ -1187,8 +1214,13 @@ proxies: # socks5
# alpn:
# - h2
# skip-cert-verify: true
### quic options
# quic: true # 默认为false
# congestion-controller: bbr
### reuse options
# max-connections: 1 # Maximum connections. Conflict with max-streams.
# min-streams: 0 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams.
# max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams.
# dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理
- name: "dns-out"
@@ -1627,6 +1659,13 @@ listeners:
flow: xtls-rprx-vision
# ws-path: "/" # 如果不为空则开启 websocket 传输层
# grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层
# xhttp-config: # 如果不为空则开启 xhttp 传输层
# path: "/"
# host: ""
# mode: auto # Available: "stream-one", "stream-up" or "packet-up"
# no-sse-header: false
# sc-stream-up-server-secs: "20-80"
# sc-max-each-post-bytes: 1000000
# -------------------------
# vless encryption服务端配置:
# (原生外观 / 只 XOR 公钥 / 全随机数。1-RTT 每次下发随机 300 到 600 秒的 ticket 以便 0-RTT 复用 / 只允许 1-RTT
@@ -1867,6 +1906,10 @@ listeners:
# - 1000
# exclude-uid-range: # 排除路由的的用户范围
# - 1000:99999
# include-mac-address:
# - 00:11:22:33:44:55
# exclude-mac-address:
# - 00:11:22:33:44:55
# Android 用户和应用规则仅在 Android 下被支持
# 并且需要 auto-route
@@ -21,7 +21,7 @@ require (
github.com/metacubex/edwards25519 v1.2.0
github.com/metacubex/fswatch v0.1.1
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
github.com/metacubex/http v0.1.0
github.com/metacubex/http v0.1.1
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604
github.com/metacubex/mhurl v0.1.0
github.com/metacubex/mlkem v0.1.0
@@ -34,12 +34,12 @@ require (
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
github.com/metacubex/sing-tun v0.4.16
github.com/metacubex/sing-tun v0.4.17
github.com/metacubex/sing-vmess v0.2.5
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443
github.com/metacubex/tls v0.1.4
github.com/metacubex/tls v0.1.5
github.com/metacubex/utls v1.8.4
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f
github.com/mroth/weightedrand/v2 v2.1.0
@@ -103,8 +103,8 @@ github.com/metacubex/hkdf v0.1.0 h1:fPA6VzXK8cU1foc/TOmGCDmSa7pZbxlnqhl3RNsthaA=
github.com/metacubex/hkdf v0.1.0/go.mod h1:3seEfds3smgTAXqUGn+tgEJH3uXdsUjOiduG/2EtvZ4=
github.com/metacubex/hpke v0.1.0 h1:gu2jUNhraehWi0P/z5HX2md3d7L1FhPQE6/Q0E9r9xQ=
github.com/metacubex/hpke v0.1.0/go.mod h1:vfDm6gfgrwlXUxKDkWbcE44hXtmc1uxLDm2BcR11b3U=
github.com/metacubex/http v0.1.0 h1:Jcy0I9zKjYijSUaksZU34XEe2xNdoFkgUTB7z7K5q0o=
github.com/metacubex/http v0.1.0/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg=
github.com/metacubex/http v0.1.1 h1:zVea0zOoaZvxe51EvJMRWGNQv6MvWqJhkZSuoAjOjVw=
github.com/metacubex/http v0.1.1/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg=
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 h1:hJwCVlE3ojViC35MGHB+FBr8TuIf3BUFn2EQ1VIamsI=
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604/go.mod h1:lpmN3m269b3V5jFCWtffqBLS4U3QQoIid9ugtO+OhVc=
github.com/metacubex/mhurl v0.1.0 h1:ZdW4Zxe3j3uJ89gNytOazHu6kbHn5owutN/VfXOI8GE=
@@ -135,8 +135,8 @@ github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6w
github.com/metacubex/sing-shadowsocks2 v0.2.7/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
github.com/metacubex/sing-tun v0.4.16 h1:LosAe4A6TOPVcD7T1ReV9D2r5501woIXXZiim3D0RRg=
github.com/metacubex/sing-tun v0.4.16/go.mod h1:L/TjQY5JEGy8nvsuYmy/XgMFMCPiF0+AWSFCYfS6r9w=
github.com/metacubex/sing-tun v0.4.17 h1:ehzvPLyxG1vmjaKVeB0aEK1eqhR3reEzdbqQfM3+5XA=
github.com/metacubex/sing-tun v0.4.17/go.mod h1:L/TjQY5JEGy8nvsuYmy/XgMFMCPiF0+AWSFCYfS6r9w=
github.com/metacubex/sing-vmess v0.2.5 h1:m9Zt5I27lB9fmLMZfism9sH2LcnAfShZfwSkf6/KJoE=
github.com/metacubex/sing-vmess v0.2.5/go.mod h1:AwtlzUgf8COe9tRYAKqWZ+leDH7p5U98a0ZUpYehl8Q=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=
@@ -145,8 +145,8 @@ github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 h1:DK2l6m2Fc85H2Bhi
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141/go.mod h1:/yI4OiGOSn0SURhZdJF3CbtPg3nwK700bG8TZLMBvAg=
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 h1:H6TnfM12tOoTizYE/qBHH3nEuibIelmHI+BVSxVJr8o=
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/tls v0.1.4 h1:Gm5GrkyMUh52gYOMIAQ1kHIym4v4M3Qb87Wsmd8Kpdc=
github.com/metacubex/tls v0.1.4/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM=
github.com/metacubex/tls v0.1.5 h1:ECcB83dj+zadnhlKcLnUUf1Sq6+vU0f/zoyU0+9oPTc=
github.com/metacubex/tls v0.1.5/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM=
github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg=
github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f h1:FGBPRb1zUabhPhDrlKEjQ9lgIwQ6cHL4x8M9lrERhbk=
@@ -93,6 +93,8 @@ type tunSchema struct {
IncludeAndroidUser *[]int `yaml:"include-android-user" json:"include-android-user,omitempty"`
IncludePackage *[]string `yaml:"include-package" json:"include-package,omitempty"`
ExcludePackage *[]string `yaml:"exclude-package" json:"exclude-package,omitempty"`
IncludeMACAddress *[]string `yaml:"include-mac-address" json:"include-mac-address,omitempty"`
ExcludeMACAddress *[]string `yaml:"exclude-mac-address" json:"exclude-mac-address,omitempty"`
EndpointIndependentNat *bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"`
UDPTimeout *int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"`
FileDescriptor *int `yaml:"file-descriptor" json:"file-descriptor"`
@@ -242,6 +244,12 @@ func pointerOrDefaultTun(p *tunSchema, def LC.Tun) LC.Tun {
if p.ExcludePackage != nil {
def.ExcludePackage = *p.ExcludePackage
}
if p.IncludeMACAddress != nil {
def.IncludeMACAddress = *p.IncludeMACAddress
}
if p.ExcludeMACAddress != nil {
def.ExcludeMACAddress = *p.ExcludeMACAddress
}
if p.EndpointIndependentNat != nil {
def.EndpointIndependentNat = *p.EndpointIndependentNat
}
@@ -47,6 +47,8 @@ type Tun struct {
IncludeAndroidUser []int `yaml:"include-android-user" json:"include-android-user,omitempty"`
IncludePackage []string `yaml:"include-package" json:"include-package,omitempty"`
ExcludePackage []string `yaml:"exclude-package" json:"exclude-package,omitempty"`
IncludeMACAddress []string `yaml:"include-mac-address" json:"include-mac-address,omitempty"`
ExcludeMACAddress []string `yaml:"exclude-mac-address" json:"exclude-mac-address,omitempty"`
EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"`
UDPTimeout int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"`
DisableICMPForwarding bool `yaml:"disable-icmp-forwarding" json:"disable-icmp-forwarding,omitempty"`
@@ -80,6 +82,8 @@ func (t *Tun) Sort() {
slices.Sort(t.IncludeAndroidUser)
slices.Sort(t.IncludePackage)
slices.Sort(t.ExcludePackage)
slices.Sort(t.IncludeMACAddress)
slices.Sort(t.ExcludeMACAddress)
slices.SortFunc(t.Inet4RouteAddress, netipx.ComparePrefix)
slices.SortFunc(t.Inet6RouteAddress, netipx.ComparePrefix)
@@ -185,6 +189,12 @@ func (t *Tun) Equal(other Tun) bool {
if !slices.Equal(t.ExcludePackage, other.ExcludePackage) {
return false
}
if !slices.Equal(t.IncludeMACAddress, other.IncludeMACAddress) {
return false
}
if !slices.Equal(t.ExcludeMACAddress, other.ExcludeMACAddress) {
return false
}
if t.EndpointIndependentNat != other.EndpointIndependentNat {
return false
}
@@ -31,9 +31,12 @@ type VlessServer struct {
}
type XHTTPConfig struct {
Path string
Host string
Mode string
Path string
Host string
Mode string
NoSSEHeader bool
ScStreamUpServerSecs string
ScMaxEachPostBytes int
}
func (t VlessServer) String() string {
@@ -43,11 +43,10 @@ func HandleConn(c net.Conn, tunnel C.Tunnel, store auth.AuthStore, additions ...
conn := N.NewBufferedConn(c)
authenticator := store.Authenticator()
keepAlive := true
trusted := authenticator == nil // disable authenticate if lru is nil
lastUser := ""
for keepAlive {
for {
peekMutex.Lock()
request, err := ReadRequest(conn.Reader())
peekMutex.Unlock()
@@ -57,13 +56,12 @@ func HandleConn(c net.Conn, tunnel C.Tunnel, store auth.AuthStore, additions ...
request.RemoteAddr = conn.RemoteAddr().String()
keepAlive = strings.TrimSpace(strings.ToLower(request.Header.Get("Proxy-Connection"))) == "keep-alive"
keepAlive := strings.TrimSpace(strings.ToLower(request.Header.Get("Proxy-Connection"))) == "keep-alive"
var resp *http.Response
var user string
resp, user = authenticate(request, authenticator) // always call authenticate function to get user
trusted = trusted || resp == nil
resp, user := authenticate(request, authenticator) // always call authenticate function to get user
if resp == nil {
trusted = true
}
additions[inUserIdx] = inbound.WithInUser(user)
if trusted {
@@ -130,16 +128,21 @@ func HandleConn(c net.Conn, tunnel C.Tunnel, store auth.AuthStore, additions ...
removeHopByHopHeaders(resp.Header)
}
if keepAlive {
if !keepAlive {
resp.Close = true // close connection if keep-alive is not set
}
if keepAlive && resp.ContentLength > 0 {
resp.Close = false // don't need to close connection if content length is positive numbers
}
if !resp.Close {
resp.Header.Set("Proxy-Connection", "keep-alive")
resp.Header.Set("Connection", "keep-alive")
resp.Header.Set("Keep-Alive", "timeout=4")
}
resp.Close = !keepAlive
err = resp.Write(conn)
if err != nil {
if err != nil || resp.Close {
break // close connection
}
}
@@ -5,6 +5,7 @@ import (
"encoding/base64"
"net"
"net/netip"
"runtime"
"strings"
"testing"
@@ -166,6 +167,9 @@ func TestInboundShadowSocks_ShadowTlsv3(t *testing.T) {
}
func TestInboundShadowSocks_KcpTun(t *testing.T) {
if runtime.GOOS == "windows" && strings.HasPrefix(runtime.Version(), "go1.20") {
t.Skip("skip kcptun test on windows go1.20")
}
inboundOptions := inbound.ShadowSocksOption{
KcpTun: inbound.KcpTun{
Enable: true,
@@ -48,6 +48,8 @@ type TunOption struct {
IncludeAndroidUser []int `inbound:"include-android-user,omitempty"`
IncludePackage []string `inbound:"include-package,omitempty"`
ExcludePackage []string `inbound:"exclude-package,omitempty"`
IncludeMACAddress []string `inbound:"include-mac-address,omitempty"`
ExcludeMACAddress []string `inbound:"exclude-mac-address,omitempty"`
EndpointIndependentNat bool `inbound:"endpoint-independent-nat,omitempty"`
UDPTimeout int64 `inbound:"udp-timeout,omitempty"`
DisableICMPForwarding bool `inbound:"disable-icmp-forwarding,omitempty"`
@@ -123,6 +125,8 @@ func NewTun(options *TunOption) (*Tun, error) {
IncludeAndroidUser: options.IncludeAndroidUser,
IncludePackage: options.IncludePackage,
ExcludePackage: options.ExcludePackage,
IncludeMACAddress: options.IncludeMACAddress,
ExcludeMACAddress: options.ExcludeMACAddress,
EndpointIndependentNat: options.EndpointIndependentNat,
UDPTimeout: options.UDPTimeout,
DisableICMPForwarding: options.DisableICMPForwarding,
@@ -32,16 +32,22 @@ type VlessUser struct {
}
type XHTTPConfig struct {
Path string `inbound:"path,omitempty"`
Host string `inbound:"host,omitempty"`
Mode string `inbound:"mode,omitempty"`
Path string `inbound:"path,omitempty"`
Host string `inbound:"host,omitempty"`
Mode string `inbound:"mode,omitempty"`
NoSSEHeader bool `inbound:"no-sse-header,omitempty"`
ScStreamUpServerSecs string `inbound:"sc-stream-up-server-secs,omitempty"`
ScMaxEachPostBytes int `inbound:"sc-max-each-post-bytes,omitempty"`
}
func (o XHTTPConfig) Build() LC.XHTTPConfig {
return LC.XHTTPConfig{
Path: o.Path,
Host: o.Host,
Mode: o.Mode,
Path: o.Path,
Host: o.Host,
Mode: o.Mode,
NoSSEHeader: o.NoSSEHeader,
ScStreamUpServerSecs: o.ScStreamUpServerSecs,
ScMaxEachPostBytes: o.ScMaxEachPostBytes,
}
}
@@ -342,105 +342,168 @@ func TestInboundVless_Reality_Grpc(t *testing.T) {
}
func TestInboundVless_XHTTP(t *testing.T) {
inboundOptions := inbound.VlessOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
XHTTPConfig: inbound.XHTTPConfig{
Path: "/vless-xhttp",
Host: "example.com",
Mode: "auto",
},
testCases := []struct {
mode string
}{
{mode: "auto"},
{mode: "stream-one"},
{mode: "stream-up"},
{mode: "packet-up"},
}
outboundOptions := outbound.VlessOption{
TLS: true,
Fingerprint: tlsFingerprint,
Network: "xhttp",
XHTTPOpts: outbound.XHTTPOptions{
Path: "/vless-xhttp",
Host: "example.com",
Mode: "auto",
},
}
testInboundVlessTLS(t, inboundOptions, outboundOptions, false)
}
func TestInboundVless_Reality_XHTTP(t *testing.T) {
inboundOptions := inbound.VlessOption{
RealityConfig: inbound.RealityConfig{
Dest: net.JoinHostPort(realityDest, "443"),
PrivateKey: realityPrivateKey,
ShortID: []string{realityShortid},
ServerNames: []string{realityDest},
},
XHTTPConfig: inbound.XHTTPConfig{
Mode: "auto",
},
}
outboundOptions := outbound.VlessOption{
TLS: true,
ServerName: realityDest,
RealityOpts: outbound.RealityOptions{
PublicKey: realityPublickey,
ShortID: realityShortid,
},
ClientFingerprint: "chrome",
Network: "xhttp",
XHTTPOpts: outbound.XHTTPOptions{
Mode: "auto",
},
}
testInboundVless(t, inboundOptions, outboundOptions)
}
func TestInboundVless_XHTTP_DownloadSettings(t *testing.T) {
for _, mode := range []string{"stream-up", "packet-up"} {
t.Run(mode, func(t *testing.T) {
inboundOptions := inbound.VlessOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
XHTTPConfig: inbound.XHTTPConfig{
Path: "/vless-xhttp",
Host: "example.com",
Mode: mode,
},
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.mode, func(t *testing.T) {
getConfig := func() (inbound.VlessOption, outbound.VlessOption) {
inboundOptions := inbound.VlessOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
XHTTPConfig: inbound.XHTTPConfig{
Path: "/vless-xhttp",
Host: "example.com",
Mode: testCase.mode,
},
}
outboundOptions := outbound.VlessOption{
TLS: true,
Fingerprint: tlsFingerprint,
ServerName: "example.org",
ClientFingerprint: "chrome",
Network: "xhttp",
XHTTPOpts: outbound.XHTTPOptions{
Path: "/vless-xhttp",
Host: "example.com",
Mode: testCase.mode,
},
}
return inboundOptions, outboundOptions
}
outboundOptions := outbound.VlessOption{
TLS: true,
Fingerprint: tlsFingerprint,
ServerName: "example.org",
ClientFingerprint: "chrome",
Network: "xhttp",
XHTTPOpts: outbound.XHTTPOptions{
Path: "/vless-xhttp",
Host: "example.com",
Mode: mode,
DownloadSettings: &outbound.XHTTPDownloadSettings{},
},
}
testInboundVlessTLS(t, inboundOptions, outboundOptions, false)
t.Run("nosplit", func(t *testing.T) {
t.Run("single", func(t *testing.T) {
inboundOptions, outboundOptions := getConfig()
testInboundVlessTLS(t, inboundOptions, outboundOptions, false)
})
t.Run("reuse", func(t *testing.T) {
inboundOptions, outboundOptions := getConfig()
testInboundVlessTLS(t, inboundOptions, withXHTTPReuse(outboundOptions), false)
})
})
t.Run("split", func(t *testing.T) {
if testCase.mode == "stream-one" { // stream-one not supported download settings
return
}
t.Run("single", func(t *testing.T) {
inboundOptions, outboundOptions := getConfig()
outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{}
testInboundVlessTLS(t, inboundOptions, outboundOptions, false)
})
t.Run("reuse", func(t *testing.T) {
inboundOptions, outboundOptions := getConfig()
outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{}
testInboundVlessTLS(t, inboundOptions, withXHTTPReuse(outboundOptions), false)
})
})
})
}
}
func TestInboundVless_XHTTP_StreamUp(t *testing.T) {
inboundOptions := inbound.VlessOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
XHTTPConfig: inbound.XHTTPConfig{
Path: "/vless-xhttp",
Host: "example.com",
Mode: "stream-up",
},
func TestInboundVless_XHTTP_Reality(t *testing.T) {
testCases := []struct {
mode string
}{
{mode: "auto"},
{mode: "stream-one"},
{mode: "stream-up"},
{mode: "packet-up"},
}
outboundOptions := outbound.VlessOption{
TLS: true,
Fingerprint: tlsFingerprint,
Network: "xhttp",
XHTTPOpts: outbound.XHTTPOptions{
Path: "/vless-xhttp",
Host: "example.com",
Mode: "stream-up",
},
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.mode, func(t *testing.T) {
getConfig := func() (inbound.VlessOption, outbound.VlessOption) {
inboundOptions := inbound.VlessOption{
RealityConfig: inbound.RealityConfig{
Dest: net.JoinHostPort(realityDest, "443"),
PrivateKey: realityPrivateKey,
ShortID: []string{realityShortid},
ServerNames: []string{realityDest},
},
XHTTPConfig: inbound.XHTTPConfig{
Path: "/vless-xhttp",
Host: "example.com",
Mode: testCase.mode,
},
}
outboundOptions := outbound.VlessOption{
TLS: true,
ServerName: realityDest,
RealityOpts: outbound.RealityOptions{
PublicKey: realityPublickey,
ShortID: realityShortid,
},
ClientFingerprint: "chrome",
Network: "xhttp",
XHTTPOpts: outbound.XHTTPOptions{
Path: "/vless-xhttp",
Host: "example.com",
Mode: testCase.mode,
},
}
return inboundOptions, outboundOptions
}
t.Run("nosplit", func(t *testing.T) {
t.Run("single", func(t *testing.T) {
inboundOptions, outboundOptions := getConfig()
testInboundVless(t, inboundOptions, outboundOptions)
})
t.Run("reuse", func(t *testing.T) {
inboundOptions, outboundOptions := getConfig()
testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions))
})
})
t.Run("split", func(t *testing.T) {
if testCase.mode == "stream-one" { // stream-one not supported download settings
return
}
t.Run("single", func(t *testing.T) {
inboundOptions, outboundOptions := getConfig()
outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{}
testInboundVless(t, inboundOptions, outboundOptions)
})
t.Run("reuse", func(t *testing.T) {
inboundOptions, outboundOptions := getConfig()
outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{}
testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions))
})
})
})
}
testInboundVlessTLS(t, inboundOptions, outboundOptions, false)
}
func withXHTTPReuse(out outbound.VlessOption) outbound.VlessOption {
out.XHTTPOpts.ReuseSettings = &outbound.XHTTPReuseSettings{
MaxConnections: "0",
MaxConcurrency: "16-32",
CMaxReuseTimes: "0",
HMaxRequestTimes: "600-900",
HMaxReusableSecs: "1800-3000",
}
if out.XHTTPOpts.DownloadSettings != nil {
out.XHTTPOpts.DownloadSettings.ReuseSettings = &outbound.XHTTPReuseSettings{
MaxConnections: "0",
MaxConcurrency: "16-32",
CMaxReuseTimes: "0",
HMaxRequestTimes: "600-900",
HMaxReusableSecs: "1800-3000",
}
}
return out
}
@@ -19,9 +19,6 @@ import (
)
func (h *ListenerHandler) ShouldHijackDns(targetAddr netip.AddrPort) bool {
if targetAddr.Addr().IsLoopback() && targetAddr.Port() == 53 { // cause by system stack
return true
}
for _, addrPort := range h.DnsAddrPorts {
if addrPort == targetAddr || (addrPort.Addr().IsUnspecified() && targetAddr.Port() == 53) {
return true
@@ -251,6 +251,22 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis
return nil, E.Cause(err, "parse exclude_dst_port_range")
}
}
var includeMACAddress []net.HardwareAddr
for _, mac := range options.IncludeMACAddress {
addr, err := net.ParseMAC(mac)
if err != nil {
return nil, E.Cause(err, "parse include_mac_address")
}
includeMACAddress = append(includeMACAddress, addr)
}
var excludeMACAddress []net.HardwareAddr
for _, mac := range options.ExcludeMACAddress {
addr, err := net.ParseMAC(mac)
if err != nil {
return nil, E.Cause(err, "parse exclude_mac_address")
}
excludeMACAddress = append(excludeMACAddress, addr)
}
var dnsAdds []netip.AddrPort
@@ -390,6 +406,8 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis
IncludeAndroidUser: options.IncludeAndroidUser,
IncludePackage: options.IncludePackage,
ExcludePackage: options.ExcludePackage,
IncludeMACAddress: includeMACAddress,
ExcludeMACAddress: excludeMACAddress,
FileDescriptor: options.FileDescriptor,
InterfaceMonitor: defaultInterfaceMonitor,
EXP_RecvMsgX: options.RecvMsgX,
@@ -155,14 +155,22 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition)
}
if config.XHTTPConfig.Path != "" || config.XHTTPConfig.Host != "" || config.XHTTPConfig.Mode != "" {
httpServer.Handler = xhttp.NewServerHandler(xhttp.ServerOption{
Path: config.XHTTPConfig.Path,
Host: config.XHTTPConfig.Host,
Mode: config.XHTTPConfig.Mode,
Config: xhttp.Config{
Host: config.XHTTPConfig.Host,
Path: config.XHTTPConfig.Path,
Mode: config.XHTTPConfig.Mode,
NoSSEHeader: config.XHTTPConfig.NoSSEHeader,
ScStreamUpServerSecs: config.XHTTPConfig.ScStreamUpServerSecs,
ScMaxEachPostBytes: config.XHTTPConfig.ScMaxEachPostBytes,
},
ConnHandler: func(conn net.Conn) {
sl.HandleConn(conn, tunnel, additions...)
},
HttpHandler: httpServer.Handler,
})
if !slices.Contains(tlsConfig.NextProtos, "http/1.1") {
tlsConfig.NextProtos = append([]string{"http/1.1"}, tlsConfig.NextProtos...)
}
if !slices.Contains(tlsConfig.NextProtos, "h2") {
tlsConfig.NextProtos = append([]string{"h2"}, tlsConfig.NextProtos...)
}
@@ -13,6 +13,7 @@ import (
"net/url"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/metacubex/mihomo/common/buf"
@@ -51,6 +52,7 @@ type Conn struct {
closeMutex sync.Mutex
closed bool
onClose func()
// deadlines
deadline *time.Timer
@@ -103,7 +105,7 @@ func (g *Conn) read(b []byte) (n int, err error) {
size = len(b)
}
n, err = io.ReadFull(g.reader, b[:size])
n, err = g.reader.Read(b[:size])
g.remain -= n
return
}
@@ -112,6 +114,9 @@ func (g *Conn) read(b []byte) (n int, err error) {
var discard [6]byte
_, err = io.ReadFull(g.reader, discard[:])
if err != nil {
if err == io.ErrUnexpectedEOF {
err = io.EOF
}
return 0, err
}
@@ -206,6 +211,10 @@ func (g *Conn) Close() error {
}
}
if g.onClose != nil {
g.onClose()
}
return errors.Join(errorArr...)
}
@@ -237,6 +246,7 @@ type Transport struct {
ctx context.Context
cancel context.CancelFunc
closeOnce sync.Once
count atomic.Int64
}
func (t *Transport) Close() error {
@@ -267,19 +277,10 @@ func NewTransport(dialFn DialFn, tlsConfig *vmess.TLSConfig, gunCfg *Config) *Tr
}
if tlsConfig.Reality == nil { // reality doesn't return the negotiated ALPN
switch tlsConn := conn.(type) {
case interface{ ConnectionState() tls.ConnectionState }:
state := tlsConn.ConnectionState()
if p := state.NegotiatedProtocol; p != http.Http2NextProtoTLS {
_ = conn.Close()
return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http.Http2NextProtoTLS)
}
case interface{ ConnectionState() tlsC.ConnectionState }:
state := tlsConn.ConnectionState()
if p := state.NegotiatedProtocol; p != http.Http2NextProtoTLS {
_ = conn.Close()
return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http.Http2NextProtoTLS)
}
state := tlsC.GetTLSConnectionState(conn)
if p := state.NegotiatedProtocol; p != http.Http2NextProtoTLS {
_ = conn.Close()
return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http.Http2NextProtoTLS)
}
}
return conn, nil
@@ -355,6 +356,9 @@ func (t *Transport) Dial() (net.Conn, error) {
writer: writer,
}
t.count.Add(1)
conn.onClose = func() { t.count.Add(-1) }
go conn.Init()
// ensure conn.initOnce.Do has been called before return
@@ -364,6 +368,78 @@ func (t *Transport) Dial() (net.Conn, error) {
return conn, nil
}
type Client struct {
mutex sync.Mutex
maxConnections int
minStreams int
maxStreams int
transports []*Transport
maker func() *Transport
}
func NewClient(maker func() *Transport, maxConnections, minStreams, maxStreams int) *Client {
if maxConnections == 0 && minStreams == 0 && maxStreams == 0 {
maxConnections = 1
}
return &Client{
maxConnections: maxConnections,
minStreams: minStreams,
maxStreams: maxStreams,
maker: maker,
}
}
func (c *Client) Dial() (net.Conn, error) {
return c.getTransport().Dial()
}
func (c *Client) Close() error {
c.mutex.Lock()
defer c.mutex.Unlock()
var errs []error
for _, t := range c.transports {
if err := t.Close(); err != nil {
errs = append(errs, err)
}
}
c.transports = nil
return errors.Join(errs...)
}
func (c *Client) getTransport() *Transport {
c.mutex.Lock()
defer c.mutex.Unlock()
var transport *Transport
for _, t := range c.transports {
if transport == nil || t.count.Load() < transport.count.Load() {
transport = t
}
}
if transport == nil {
return c.newTransportLocked()
}
numStreams := int(transport.count.Load())
if numStreams == 0 {
return transport
}
if c.maxConnections > 0 {
if len(c.transports) >= c.maxConnections || numStreams < c.minStreams {
return transport
}
} else {
if c.maxStreams > 0 && numStreams < c.maxStreams {
return transport
}
}
return c.newTransportLocked()
}
func (c *Client) newTransportLocked() *Transport {
transport := c.maker()
c.transports = append(c.transports, transport)
return transport
}
func StreamGunWithConn(conn net.Conn, tlsConfig *vmess.TLSConfig, gunCfg *Config) (net.Conn, error) {
dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
return conn, nil
@@ -112,11 +112,6 @@ func (w *h2ConnWrapper) CloseWrapper() {
w.closed = true
}
func (w *h2ConnWrapper) Close() error {
w.CloseWrapper()
return w.ExtendedConn.Close()
}
func (w *h2ConnWrapper) Upstream() any {
return w.ExtendedConn
}
@@ -66,13 +66,7 @@ func WriteKIPMessage(w io.Writer, typ byte, payload []byte) error {
hdr[3] = typ
binary.BigEndian.PutUint16(hdr[4:], uint16(len(payload)))
if err := writeFull(w, hdr[:]); err != nil {
return err
}
if len(payload) == 0 {
return nil
}
return writeFull(w, payload)
return writeAllChunks(w, hdr[:], payload)
}
func ReadKIPMessage(r io.Reader) (*KIPMessage, error) {
@@ -173,16 +173,10 @@ func (s *Session) sendFrame(frameType byte, streamID uint32, payload []byte) err
s.writeMu.Lock()
defer s.writeMu.Unlock()
if err := writeFull(s.conn, header[:]); err != nil {
if err := writeAllChunks(s.conn, header[:], payload); err != nil {
s.closeWithError(err)
return err
}
if len(payload) > 0 {
if err := writeFull(s.conn, payload); err != nil {
s.closeWithError(err)
return err
}
}
return nil
}
@@ -315,17 +309,6 @@ func (s *Session) readLoop() {
}
}
func writeFull(w io.Writer, b []byte) error {
for len(b) > 0 {
n, err := w.Write(b)
if err != nil {
return err
}
b = b[n:]
}
return nil
}
func trimASCII(b []byte) string {
i := 0
j := len(b)
@@ -0,0 +1,19 @@
package multiplex
import "io"
func writeAllChunks(w io.Writer, chunks ...[]byte) error {
for _, chunk := range chunks {
for len(chunk) > 0 {
n, err := w.Write(chunk)
if err != nil {
return err
}
if n == 0 {
return io.ErrShortWrite
}
chunk = chunk[n:]
}
}
return nil
}
@@ -3,12 +3,9 @@ package sudoku
import (
"bufio"
"bytes"
crypto_rand "crypto/rand"
"encoding/binary"
"errors"
"math/rand"
"net"
"sync"
"sync/atomic"
)
const IOBufferSize = 32 * 1024
@@ -45,14 +42,17 @@ type Conn struct {
table *Table
reader *bufio.Reader
recorder *bytes.Buffer
recording bool
recording atomic.Bool
recordLock sync.Mutex
rawBuf []byte
pendingData []byte
hintBuf []byte
pendingData pendingBuffer
hintBuf [4]byte
hintCount int
writeMu sync.Mutex
writeBuf []byte
rng *rand.Rand
rng randomSource
paddingThreshold uint64
}
@@ -77,33 +77,28 @@ func (sc *Conn) CloseRead() error {
}
func NewConn(c net.Conn, table *Table, pMin, pMax int, record bool) *Conn {
var seedBytes [8]byte
if _, err := crypto_rand.Read(seedBytes[:]); err != nil {
binary.BigEndian.PutUint64(seedBytes[:], uint64(rand.Int63()))
}
seed := int64(binary.BigEndian.Uint64(seedBytes[:]))
localRng := rand.New(rand.NewSource(seed))
localRng := newSeededRand()
sc := &Conn{
Conn: c,
table: table,
reader: bufio.NewReaderSize(c, IOBufferSize),
rawBuf: make([]byte, IOBufferSize),
pendingData: make([]byte, 0, 4096),
hintBuf: make([]byte, 0, 4),
pendingData: newPendingBuffer(4096),
writeBuf: make([]byte, 0, 4096),
rng: localRng,
paddingThreshold: pickPaddingThreshold(localRng, pMin, pMax),
}
if record {
sc.recorder = new(bytes.Buffer)
sc.recording = true
sc.recording.Store(true)
}
return sc
}
func (sc *Conn) StopRecording() {
sc.recordLock.Lock()
sc.recording = false
sc.recording.Store(false)
sc.recorder = nil
sc.recordLock.Unlock()
}
@@ -137,74 +132,50 @@ func (sc *Conn) Write(p []byte) (n int, err error) {
return 0, nil
}
outCapacity := len(p) * 6
out := make([]byte, 0, outCapacity)
pads := sc.table.PaddingPool
padLen := len(pads)
sc.writeMu.Lock()
defer sc.writeMu.Unlock()
for _, b := range p {
if shouldPad(sc.rng, sc.paddingThreshold) {
out = append(out, pads[sc.rng.Intn(padLen)])
}
puzzles := sc.table.EncodeTable[b]
puzzle := puzzles[sc.rng.Intn(len(puzzles))]
perm := perm4[sc.rng.Intn(len(perm4))]
for _, idx := range perm {
if shouldPad(sc.rng, sc.paddingThreshold) {
out = append(out, pads[sc.rng.Intn(padLen)])
}
out = append(out, puzzle[idx])
}
}
if shouldPad(sc.rng, sc.paddingThreshold) {
out = append(out, pads[sc.rng.Intn(padLen)])
}
return len(p), writeFull(sc.Conn, out)
sc.writeBuf = encodeSudokuPayload(sc.writeBuf[:0], sc.table, sc.rng, sc.paddingThreshold, p)
return len(p), writeFull(sc.Conn, sc.writeBuf)
}
func (sc *Conn) Read(p []byte) (n int, err error) {
if len(sc.pendingData) > 0 {
n = copy(p, sc.pendingData)
if n == len(sc.pendingData) {
sc.pendingData = sc.pendingData[:0]
} else {
sc.pendingData = sc.pendingData[n:]
}
if n, ok := drainPending(p, &sc.pendingData); ok {
return n, nil
}
for {
if len(sc.pendingData) > 0 {
if sc.pendingData.available() > 0 {
break
}
nr, rErr := sc.reader.Read(sc.rawBuf)
if nr > 0 {
chunk := sc.rawBuf[:nr]
sc.recordLock.Lock()
if sc.recording {
sc.recorder.Write(chunk)
if sc.recording.Load() {
sc.recordLock.Lock()
if sc.recording.Load() && sc.recorder != nil {
sc.recorder.Write(chunk)
}
sc.recordLock.Unlock()
}
sc.recordLock.Unlock()
layout := sc.table.layout
for _, b := range chunk {
if !sc.table.layout.isHint(b) {
if !layout.hintTable[b] {
continue
}
sc.hintBuf = append(sc.hintBuf, b)
if len(sc.hintBuf) == 4 {
key := packHintsToKey([4]byte{sc.hintBuf[0], sc.hintBuf[1], sc.hintBuf[2], sc.hintBuf[3]})
sc.hintBuf[sc.hintCount] = b
sc.hintCount++
if sc.hintCount == len(sc.hintBuf) {
key := packHintsToKey(sc.hintBuf)
val, ok := sc.table.DecodeMap[key]
if !ok {
return 0, errors.New("INVALID_SUDOKU_MAP_MISS")
return 0, ErrInvalidSudokuMapMiss
}
sc.pendingData = append(sc.pendingData, val)
sc.hintBuf = sc.hintBuf[:0]
sc.pendingData.appendByte(val)
sc.hintCount = 0
}
}
}
@@ -212,16 +183,11 @@ func (sc *Conn) Read(p []byte) (n int, err error) {
if rErr != nil {
return 0, rErr
}
if len(sc.pendingData) > 0 {
if sc.pendingData.available() > 0 {
break
}
}
n = copy(p, sc.pendingData)
if n == len(sc.pendingData) {
sc.pendingData = sc.pendingData[:0]
} else {
sc.pendingData = sc.pendingData[n:]
}
n, _ = drainPending(p, &sc.pendingData)
return n, nil
}
@@ -0,0 +1,36 @@
package sudoku
func encodeSudokuPayload(dst []byte, table *Table, rng randomSource, paddingThreshold uint64, p []byte) []byte {
if len(p) == 0 {
return dst[:0]
}
outCapacity := len(p)*6 + 1
if cap(dst) < outCapacity {
dst = make([]byte, 0, outCapacity)
}
out := dst[:0]
pads := table.PaddingPool
padLen := len(pads)
for _, b := range p {
if shouldPad(rng, paddingThreshold) {
out = append(out, pads[rng.Intn(padLen)])
}
puzzles := table.EncodeTable[b]
puzzle := puzzles[rng.Intn(len(puzzles))]
perm := perm4[rng.Intn(len(perm4))]
for _, idx := range perm {
if shouldPad(rng, paddingThreshold) {
out = append(out, pads[rng.Intn(padLen)])
}
out = append(out, puzzle[idx])
}
}
if shouldPad(rng, paddingThreshold) {
out = append(out, pads[rng.Intn(padLen)])
}
return out
}
@@ -14,17 +14,30 @@ type byteLayout struct {
padMarker byte
paddingPool []byte
encodeHint func(val, pos byte) byte
encodeGroup func(group byte) byte
decodeGroup func(b byte) (byte, bool)
hintTable [256]bool
encodeHint [4][16]byte
encodeGroup [64]byte
decodeGroup [256]byte
groupValid [256]bool
}
func (l *byteLayout) isHint(b byte) bool {
if (b & l.hintMask) == l.hintValue {
return true
return l != nil && l.hintTable[b]
}
func (l *byteLayout) hintByte(val, pos byte) byte {
return l.encodeHint[val&0x03][pos&0x0F]
}
func (l *byteLayout) groupByte(group byte) byte {
return l.encodeGroup[group&0x3F]
}
func (l *byteLayout) decodePackedGroup(b byte) (byte, bool) {
if l == nil {
return 0, false
}
// ASCII layout maps the single non-printable marker (0x7F) to '\n' on the wire.
return l.name == "ascii" && b == '\n'
return l.decodeGroup[b], l.groupValid[b]
}
// resolveLayout picks the byte layout for a single traffic direction.
@@ -50,38 +63,44 @@ func newASCIILayout() *byteLayout {
for i := 0; i < 32; i++ {
padding = append(padding, byte(0x20+i))
}
return &byteLayout{
layout := &byteLayout{
name: "ascii",
hintMask: 0x40,
hintValue: 0x40,
padMarker: 0x3F,
paddingPool: padding,
encodeHint: func(val, pos byte) byte {
b := 0x40 | ((val & 0x03) << 4) | (pos & 0x0F)
// Avoid DEL (0x7F) in prefer_ascii mode; map it to '\n' to reduce fingerprint.
if b == 0x7F {
return '\n'
}
return b
},
encodeGroup: func(group byte) byte {
b := 0x40 | (group & 0x3F)
// Avoid DEL (0x7F) in prefer_ascii mode; map it to '\n' to reduce fingerprint.
if b == 0x7F {
return '\n'
}
return b
},
decodeGroup: func(b byte) (byte, bool) {
if b == '\n' {
return 0x3F, true
}
if (b & 0x40) == 0 {
return 0, false
}
return b & 0x3F, true
},
}
for val := 0; val < 4; val++ {
for pos := 0; pos < 16; pos++ {
b := byte(0x40 | (byte(val) << 4) | byte(pos))
if b == 0x7F {
b = '\n'
}
layout.encodeHint[val][pos] = b
}
}
for group := 0; group < 64; group++ {
b := byte(0x40 | byte(group))
if b == 0x7F {
b = '\n'
}
layout.encodeGroup[group] = b
}
for b := 0; b < 256; b++ {
wire := byte(b)
if (wire & 0x40) == 0x40 {
layout.hintTable[wire] = true
layout.decodeGroup[wire] = wire & 0x3F
layout.groupValid[wire] = true
}
}
layout.hintTable['\n'] = true
layout.decodeGroup['\n'] = 0x3F
layout.groupValid['\n'] = true
return layout
}
func newEntropyLayout() *byteLayout {
@@ -90,26 +109,35 @@ func newEntropyLayout() *byteLayout {
padding = append(padding, byte(0x80+i))
padding = append(padding, byte(0x10+i))
}
return &byteLayout{
layout := &byteLayout{
name: "entropy",
hintMask: 0x90,
hintValue: 0x00,
padMarker: 0x80,
paddingPool: padding,
encodeHint: func(val, pos byte) byte {
return ((val & 0x03) << 5) | (pos & 0x0F)
},
encodeGroup: func(group byte) byte {
v := group & 0x3F
return ((v & 0x30) << 1) | (v & 0x0F)
},
decodeGroup: func(b byte) (byte, bool) {
if (b & 0x90) != 0 {
return 0, false
}
return ((b >> 1) & 0x30) | (b & 0x0F), true
},
}
for val := 0; val < 4; val++ {
for pos := 0; pos < 16; pos++ {
layout.encodeHint[val][pos] = (byte(val) << 5) | byte(pos)
}
}
for group := 0; group < 64; group++ {
v := byte(group)
layout.encodeGroup[group] = ((v & 0x30) << 1) | (v & 0x0F)
}
for b := 0; b < 256; b++ {
wire := byte(b)
if (wire & 0x90) != 0 {
continue
}
layout.hintTable[wire] = true
layout.decodeGroup[wire] = ((wire >> 1) & 0x30) | (wire & 0x0F)
layout.groupValid[wire] = true
}
return layout
}
func newCustomLayout(pattern string) (*byteLayout, error) {
@@ -162,26 +190,6 @@ func newCustomLayout(pattern string) (*byteLayout, error) {
return out
}
decodeGroup := func(b byte) (byte, bool) {
if (b & xMask) != xMask {
return 0, false
}
var val, pos byte
if b&(1<<pBits[0]) != 0 {
val |= 0x02
}
if b&(1<<pBits[1]) != 0 {
val |= 0x01
}
for i, bit := range vBits {
if b&(1<<bit) != 0 {
pos |= 1 << (3 - uint8(i))
}
}
group := (val << 4) | (pos & 0x0F)
return group, true
}
paddingSet := make(map[byte]struct{})
var padding []byte
for drop := range xBits {
@@ -202,20 +210,46 @@ func newCustomLayout(pattern string) (*byteLayout, error) {
return nil, fmt.Errorf("custom table produced empty padding pool")
}
return &byteLayout{
layout := &byteLayout{
name: fmt.Sprintf("custom(%s)", cleaned),
hintMask: xMask,
hintValue: xMask,
padMarker: padding[0],
paddingPool: padding,
encodeHint: func(val, pos byte) byte {
return encodeBits(val, pos, -1)
},
encodeGroup: func(group byte) byte {
val := (group >> 4) & 0x03
pos := group & 0x0F
return encodeBits(val, pos, -1)
},
decodeGroup: decodeGroup,
}, nil
}
for val := 0; val < 4; val++ {
for pos := 0; pos < 16; pos++ {
layout.encodeHint[val][pos] = encodeBits(byte(val), byte(pos), -1)
}
}
for group := 0; group < 64; group++ {
val := byte(group>>4) & 0x03
pos := byte(group) & 0x0F
layout.encodeGroup[group] = encodeBits(val, pos, -1)
}
for b := 0; b < 256; b++ {
wire := byte(b)
if (wire & xMask) != xMask {
continue
}
layout.hintTable[wire] = true
var val, pos byte
if wire&(1<<pBits[0]) != 0 {
val |= 0x02
}
if wire&(1<<pBits[1]) != 0 {
val |= 0x01
}
for i, bit := range vBits {
if wire&(1<<bit) != 0 {
pos |= 1 << (3 - uint8(i))
}
}
layout.decodeGroup[wire] = (val << 4) | pos
layout.groupValid[wire] = true
}
return layout, nil
}
@@ -2,10 +2,7 @@ package sudoku
import (
"bufio"
crypto_rand "crypto/rand"
"encoding/binary"
"io"
"math/rand"
"net"
"sync"
)
@@ -25,7 +22,7 @@ type PackedConn struct {
// Read-side buffers.
rawBuf []byte
pendingData []byte
pendingData pendingBuffer
// Write-side state.
writeMu sync.Mutex
@@ -38,7 +35,7 @@ type PackedConn struct {
readBits int
// Padding selection matches Conn's threshold-based model.
rng *rand.Rand
rng randomSource
paddingThreshold uint64
padMarker byte
padPool []byte
@@ -65,19 +62,14 @@ func (pc *PackedConn) CloseRead() error {
}
func NewPackedConn(c net.Conn, table *Table, pMin, pMax int) *PackedConn {
var seedBytes [8]byte
if _, err := crypto_rand.Read(seedBytes[:]); err != nil {
binary.BigEndian.PutUint64(seedBytes[:], uint64(rand.Int63()))
}
seed := int64(binary.BigEndian.Uint64(seedBytes[:]))
localRng := rand.New(rand.NewSource(seed))
localRng := newSeededRand()
pc := &PackedConn{
Conn: c,
table: table,
reader: bufio.NewReaderSize(c, IOBufferSize),
rawBuf: make([]byte, IOBufferSize),
pendingData: make([]byte, 0, 4096),
pendingData: newPendingBuffer(4096),
writeBuf: make([]byte, 0, 4096),
rng: localRng,
paddingThreshold: pickPaddingThreshold(localRng, pMin, pMax),
@@ -104,7 +96,7 @@ func (pc *PackedConn) maybeAddPadding(out []byte) []byte {
func (pc *PackedConn) appendGroup(out []byte, group byte) []byte {
out = pc.maybeAddPadding(out)
return append(out, pc.encodeGroup(group))
return append(out, pc.table.layout.groupByte(group))
}
func (pc *PackedConn) appendForcedPadding(out []byte) []byte {
@@ -156,19 +148,6 @@ func (pc *PackedConn) writeProtectedPrefix(out []byte, p []byte) ([]byte, int) {
return out, limit
}
func (pc *PackedConn) drainPendingData(dst []byte) int {
n := copy(dst, pc.pendingData)
if n == len(pc.pendingData) {
pc.pendingData = pc.pendingData[:0]
return n
}
remaining := len(pc.pendingData) - n
copy(pc.pendingData, pc.pendingData[n:])
pc.pendingData = pc.pendingData[:remaining]
return n
}
func (pc *PackedConn) Write(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
@@ -282,7 +261,7 @@ func (pc *PackedConn) Flush() error {
pc.bitBuf = 0
pc.bitCount = 0
out = append(out, pc.encodeGroup(group&0x3F))
out = append(out, pc.table.layout.groupByte(group&0x3F))
out = append(out, pc.padMarker)
}
@@ -301,14 +280,17 @@ func writeFull(w io.Writer, b []byte) error {
if err != nil {
return err
}
if n == 0 {
return io.ErrShortWrite
}
b = b[n:]
}
return nil
}
func (pc *PackedConn) Read(p []byte) (int, error) {
if len(pc.pendingData) > 0 {
return pc.drainPendingData(p), nil
if n, ok := drainPending(p, &pc.pendingData); ok {
return n, nil
}
for {
@@ -320,7 +302,7 @@ func (pc *PackedConn) Read(p []byte) (int, error) {
layout := pc.table.layout
for _, b := range pc.rawBuf[:nr] {
if !layout.isHint(b) {
if !layout.hintTable[b] {
if b == padMarker {
rBuf = 0
rBits = 0
@@ -328,7 +310,7 @@ func (pc *PackedConn) Read(p []byte) (int, error) {
continue
}
group, ok := layout.decodeGroup(b)
group, ok := layout.decodePackedGroup(b)
if !ok {
return 0, ErrInvalidSudokuMapMiss
}
@@ -339,7 +321,12 @@ func (pc *PackedConn) Read(p []byte) (int, error) {
if rBits >= 8 {
rBits -= 8
val := byte(rBuf >> rBits)
pc.pendingData = append(pc.pendingData, val)
pc.pendingData.appendByte(val)
if rBits == 0 {
rBuf = 0
} else {
rBuf &= (uint64(1) << rBits) - 1
}
}
}
@@ -352,24 +339,21 @@ func (pc *PackedConn) Read(p []byte) (int, error) {
pc.readBitBuf = 0
pc.readBits = 0
}
if len(pc.pendingData) > 0 {
if pc.pendingData.available() > 0 {
break
}
return 0, rErr
}
if len(pc.pendingData) > 0 {
if pc.pendingData.available() > 0 {
break
}
}
return pc.drainPendingData(p), nil
n, _ := drainPending(p, &pc.pendingData)
return n, nil
}
func (pc *PackedConn) getPaddingByte() byte {
return pc.padPool[pc.rng.Intn(len(pc.padPool))]
}
func (pc *PackedConn) encodeGroup(group byte) byte {
return pc.table.layout.encodeGroup(group)
}
@@ -1,10 +1,8 @@
package sudoku
import "math/rand"
const probOne = uint64(1) << 32
func pickPaddingThreshold(r *rand.Rand, pMin, pMax int) uint64 {
func pickPaddingThreshold(r randomSource, pMin, pMax int) uint64 {
if r == nil {
return 0
}
@@ -30,7 +28,7 @@ func pickPaddingThreshold(r *rand.Rand, pMin, pMax int) uint64 {
return min + (u * (max - min) >> 32)
}
func shouldPad(r *rand.Rand, threshold uint64) bool {
func shouldPad(r randomSource, threshold uint64) bool {
if threshold == 0 {
return false
}
@@ -0,0 +1,57 @@
package sudoku
type pendingBuffer struct {
data []byte
off int
}
func newPendingBuffer(capacity int) pendingBuffer {
return pendingBuffer{data: make([]byte, 0, capacity)}
}
func (p *pendingBuffer) available() int {
if p == nil {
return 0
}
return len(p.data) - p.off
}
func (p *pendingBuffer) reset() {
if p == nil {
return
}
p.data = p.data[:0]
p.off = 0
}
func (p *pendingBuffer) ensureAppendCapacity(extra int) {
if p == nil || extra <= 0 || p.off == 0 {
return
}
if cap(p.data)-len(p.data) >= extra {
return
}
unread := len(p.data) - p.off
copy(p.data[:unread], p.data[p.off:])
p.data = p.data[:unread]
p.off = 0
}
func (p *pendingBuffer) appendByte(b byte) {
p.ensureAppendCapacity(1)
p.data = append(p.data, b)
}
func drainPending(dst []byte, pending *pendingBuffer) (int, bool) {
if pending == nil || pending.available() == 0 {
return 0, false
}
n := copy(dst, pending.data[pending.off:])
pending.off += n
if pending.off == len(pending.data) {
pending.reset()
}
return n, true
}
@@ -0,0 +1,56 @@
package sudoku
import (
crypto_rand "crypto/rand"
"encoding/binary"
"time"
)
type randomSource interface {
Uint32() uint32
Uint64() uint64
Intn(n int) int
}
type sudokuRand struct {
state uint64
}
func newSeededRand() *sudokuRand {
seed := time.Now().UnixNano()
var seedBytes [8]byte
if _, err := crypto_rand.Read(seedBytes[:]); err == nil {
seed = int64(binary.BigEndian.Uint64(seedBytes[:]))
}
return newSudokuRand(seed)
}
func newSudokuRand(seed int64) *sudokuRand {
state := uint64(seed)
if state == 0 {
state = 0x9e3779b97f4a7c15
}
return &sudokuRand{state: state}
}
func (r *sudokuRand) Uint64() uint64 {
if r == nil {
return 0
}
r.state += 0x9e3779b97f4a7c15
z := r.state
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9
z = (z ^ (z >> 27)) * 0x94d049bb133111eb
return z ^ (z >> 31)
}
func (r *sudokuRand) Uint32() uint32 {
return uint32(r.Uint64() >> 32)
}
func (r *sudokuRand) Intn(n int) int {
if n <= 1 {
return 0
}
return int((uint64(r.Uint32()) * uint64(n)) >> 32)
}
@@ -146,7 +146,7 @@ func newSingleDirectionTable(key string, mode string, customPattern string) (*Ta
if matchCount == 1 {
// 唯一确定,生成最终编码字节
for i, p := range rawParts {
currentHints[i] = t.layout.encodeHint(p.val-1, p.pos)
currentHints[i] = t.layout.hintByte(p.val-1, p.pos)
}
t.EncodeTable[byteVal] = append(t.EncodeTable[byteVal], currentHints)
@@ -8,7 +8,6 @@ import (
"io"
"net"
"net/netip"
"strconv"
"sync"
"time"
@@ -37,42 +36,15 @@ func WriteDatagram(w io.Writer, addr string, payload []byte) error {
binary.BigEndian.PutUint16(header[:2], uint16(len(addrBuf)))
binary.BigEndian.PutUint16(header[2:], uint16(len(payload)))
if err := writeFull(w, header[:]); err != nil {
return err
}
if err := writeFull(w, addrBuf); err != nil {
return err
}
return writeFull(w, payload)
return writeAllChunks(w, header[:], addrBuf, payload)
}
// ReadDatagram parses a single UDP datagram frame from the reliable stream.
func ReadDatagram(r io.Reader) (string, []byte, error) {
var header [4]byte
if _, err := io.ReadFull(r, header[:]); err != nil {
return "", nil, err
}
addrLen := int(binary.BigEndian.Uint16(header[:2]))
payloadLen := int(binary.BigEndian.Uint16(header[2:]))
if addrLen <= 0 || addrLen > maxUoTPayload {
return "", nil, fmt.Errorf("invalid address length: %d", addrLen)
}
if payloadLen < 0 || payloadLen > maxUoTPayload {
return "", nil, fmt.Errorf("invalid payload length: %d", payloadLen)
}
addrBuf := make([]byte, addrLen)
if _, err := io.ReadFull(r, addrBuf); err != nil {
return "", nil, err
}
addr, err := DecodeAddress(bytes.NewReader(addrBuf))
addr, payloadLen, err := readDatagramHeaderAndAddress(r)
if err != nil {
return "", nil, fmt.Errorf("decode address: %w", err)
return "", nil, err
}
payload := make([]byte, payloadLen)
if _, err := io.ReadFull(r, payload); err != nil {
return "", nil, err
@@ -93,26 +65,29 @@ func NewUoTPacketConn(conn net.Conn) *UoTPacketConn {
func (c *UoTPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
for {
addrStr, payload, err := ReadDatagram(c.conn)
addrStr, payloadLen, err := readDatagramHeaderAndAddress(c.conn)
if err != nil {
return 0, nil, err
}
if len(payload) > len(p) {
udpAddr, err := parseDatagramUDPAddr(addrStr)
if payloadLen > len(p) {
if discardErr := discardBytes(c.conn, payloadLen); discardErr != nil {
return 0, nil, discardErr
}
return 0, nil, io.ErrShortBuffer
}
host, port, _ := net.SplitHostPort(addrStr)
portInt, _ := strconv.ParseUint(port, 10, 16)
ip, err := netip.ParseAddr(host)
if err != nil { // disallow domain addr at here, just ignore
if err != nil {
if discardErr := discardBytes(c.conn, payloadLen); discardErr != nil {
return 0, nil, discardErr
}
log.Debugln("[Sudoku][UoT] discard datagram with invalid address %s: %v", addrStr, err)
continue
}
udpAddr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip.Unmap(), uint16(portInt)))
copy(p, payload)
return len(payload), udpAddr, nil
if _, err := io.ReadFull(c.conn, p[:payloadLen]); err != nil {
return 0, nil, err
}
return payloadLen, udpAddr, nil
}
}
@@ -147,3 +122,46 @@ func (c *UoTPacketConn) SetReadDeadline(t time.Time) error {
func (c *UoTPacketConn) SetWriteDeadline(t time.Time) error {
return c.conn.SetWriteDeadline(t)
}
func readDatagramHeaderAndAddress(r io.Reader) (string, int, error) {
var header [4]byte
if _, err := io.ReadFull(r, header[:]); err != nil {
return "", 0, err
}
addrLen := int(binary.BigEndian.Uint16(header[:2]))
payloadLen := int(binary.BigEndian.Uint16(header[2:]))
if addrLen <= 0 || addrLen > maxUoTPayload {
return "", 0, fmt.Errorf("invalid address length: %d", addrLen)
}
if payloadLen < 0 || payloadLen > maxUoTPayload {
return "", 0, fmt.Errorf("invalid payload length: %d", payloadLen)
}
addrBuf := make([]byte, addrLen)
if _, err := io.ReadFull(r, addrBuf); err != nil {
return "", 0, err
}
addr, err := DecodeAddress(bytes.NewReader(addrBuf))
if err != nil {
return "", 0, fmt.Errorf("decode address: %w", err)
}
return addr, payloadLen, nil
}
func parseDatagramUDPAddr(addr string) (*net.UDPAddr, error) {
addrPort, err := netip.ParseAddrPort(addr)
if err != nil {
return nil, err
}
return net.UDPAddrFromAddrPort(netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port())), nil
}
func discardBytes(r io.Reader, n int) error {
if n <= 0 {
return nil
}
_, err := io.CopyN(io.Discard, r, int64(n))
return err
}
@@ -0,0 +1,19 @@
package sudoku
import "io"
func writeAllChunks(w io.Writer, chunks ...[]byte) error {
for _, chunk := range chunks {
for len(chunk) > 0 {
n, err := w.Write(chunk)
if err != nil {
return err
}
if n == 0 {
return io.ErrShortWrite
}
chunk = chunk[n:]
}
}
return nil
}
@@ -9,9 +9,11 @@ import (
"net/netip"
"net/url"
"sync"
"sync/atomic"
"time"
"github.com/metacubex/mihomo/common/httputils"
"github.com/metacubex/mihomo/common/once"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/vmess"
@@ -33,6 +35,9 @@ type ClientOptions struct {
QUICCongestionControl string
QUICCwnd int
HealthCheck bool
MaxConnections int
MinStreams int
MaxStreams int
}
type Client struct {
@@ -45,6 +50,7 @@ type Client struct {
startOnce sync.Once
healthCheck bool
healthCheckTimer *time.Timer
count atomic.Int64
}
func NewClient(ctx context.Context, options ClientOptions) (client *Client, err error) {
@@ -134,6 +140,10 @@ func (c *Client) roundTrip(request *http.Request, conn *httpConn) {
writer: pipeWriter,
created: make(chan struct{}),
}
c.count.Add(1)
conn.closeFn = once.OnceFunc(func() {
c.count.Add(-1)
})
ctx, cancel := context.WithCancel(c.ctx) // requestCtx must alive during conn not closed
conn.cancelFn = cancel // cancel ctx when conn closed
go func() {
@@ -245,3 +255,108 @@ func (c *Client) HealthCheck(ctx context.Context) error {
}
return nil
}
type PoolClient struct {
mutex sync.Mutex
maxConnections int
minStreams int
maxStreams int
ctx context.Context
options ClientOptions
clients []*Client
}
func NewPoolClient(ctx context.Context, options ClientOptions) (*PoolClient, error) {
maxConnections := options.MaxConnections
minStreams := options.MinStreams
maxStreams := options.MaxStreams
if maxConnections == 0 && minStreams == 0 && maxStreams == 0 {
maxConnections = 1
}
client, err := NewClient(ctx, options) // reserve one client and verify the configuration
if err != nil {
return nil, err
}
return &PoolClient{
maxConnections: maxConnections,
minStreams: minStreams,
maxStreams: maxStreams,
ctx: ctx,
options: options,
clients: []*Client{client},
}, nil
}
func (c *PoolClient) Dial(ctx context.Context, host string) (net.Conn, error) {
transport, err := c.getClient()
if err != nil {
return nil, err
}
return transport.Dial(ctx, host)
}
func (c *PoolClient) ListenPacket(ctx context.Context) (net.PacketConn, error) {
transport, err := c.getClient()
if err != nil {
return nil, err
}
return transport.ListenPacket(ctx)
}
func (c *PoolClient) ListenICMP(ctx context.Context) (*IcmpConn, error) {
transport, err := c.getClient()
if err != nil {
return nil, err
}
return transport.ListenICMP(ctx)
}
func (c *PoolClient) Close() error {
c.mutex.Lock()
defer c.mutex.Unlock()
var errs []error
for _, t := range c.clients {
if err := t.Close(); err != nil {
errs = append(errs, err)
}
}
c.clients = nil
return errors.Join(errs...)
}
func (c *PoolClient) getClient() (*Client, error) {
c.mutex.Lock()
defer c.mutex.Unlock()
var transport *Client
for _, t := range c.clients {
if transport == nil || t.count.Load() < transport.count.Load() {
transport = t
}
}
if transport == nil {
return c.newTransportLocked()
}
numStreams := int(transport.count.Load())
if numStreams == 0 {
return transport, nil
}
if c.maxConnections > 0 {
if len(c.clients) >= c.maxConnections || numStreams < c.minStreams {
return transport, nil
}
} else {
if c.maxStreams > 0 && numStreams < c.maxStreams {
return transport, nil
}
}
return c.newTransportLocked()
}
func (c *PoolClient) newTransportLocked() (*Client, error) {
transport, err := NewClient(c.ctx, c.options)
if err != nil {
return nil, err
}
c.clients = append(c.clients, transport)
return transport, nil
}
@@ -98,6 +98,7 @@ type httpConn struct {
created chan struct{}
createErr error
cancelFn func()
closeFn func()
httputils.NetAddr
// deadlines
@@ -129,6 +130,9 @@ func (h *httpConn) Close() error {
if h.cancelFn != nil {
h.cancelFn()
}
if h.closeFn != nil {
h.closeFn()
}
return errors.Join(errorArr...)
}
@@ -4,6 +4,7 @@ import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"net"
@@ -24,6 +25,7 @@ type TransportMaker func() http.RoundTripper
type PacketUpWriter struct {
ctx context.Context
cancel context.CancelFunc
cfg *Config
sessionID string
transport http.RoundTripper
@@ -34,7 +36,26 @@ type PacketUpWriter struct {
func (c *PacketUpWriter) Write(b []byte) (int, error) {
c.writeMu.Lock()
defer c.writeMu.Unlock()
scMaxEachPostBytes := c.cfg.GetNormalizedScMaxEachPostBytes()
if len(b) < scMaxEachPostBytes {
return c.write(b)
}
var n int
for start := 0; start < len(b); start += scMaxEachPostBytes {
end := start + scMaxEachPostBytes
if end > len(b) {
end = len(b)
}
_n, err := c.write(b[start:end])
n += _n
if err != nil {
return n, err
}
}
return n, nil
}
func (c *PacketUpWriter) write(b []byte) (int, error) {
u := url.URL{
Scheme: "https",
Host: c.cfg.Host,
@@ -69,11 +90,12 @@ func (c *PacketUpWriter) Write(b []byte) (int, error) {
}
func (c *PacketUpWriter) Close() error {
c.cancel()
httputils.CloseTransport(c.transport)
return nil
}
func NewTransport(dialRaw DialRawFunc, wrapTLS WrapTLSFunc) http.RoundTripper {
func NewTransport(dialRaw DialRawFunc, wrapTLS WrapTLSFunc, alpn []string) http.RoundTripper {
return &http.Http2Transport{
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
raw, err := dialRaw(ctx)
@@ -91,12 +113,14 @@ func NewTransport(dialRaw DialRawFunc, wrapTLS WrapTLSFunc) http.RoundTripper {
}
type Client struct {
ctx context.Context
cancel context.CancelFunc
mode string
cfg *Config
makeTransport TransportMaker
makeDownloadTransport TransportMaker
ctx context.Context
cancel context.CancelFunc
uploadManager *ReuseManager
downloadManager *ReuseManager
}
func NewClient(cfg *Config, makeTransport TransportMaker, makeDownloadTransport TransportMaker, hasReality bool) (*Client, error) {
@@ -107,14 +131,50 @@ func NewClient(cfg *Config, makeTransport TransportMaker, makeDownloadTransport
return nil, fmt.Errorf("xhttp mode %s is not implemented yet", mode)
}
ctx, cancel := context.WithCancel(context.Background())
return &Client{
client := &Client{
mode: mode,
cfg: cfg,
makeTransport: makeTransport,
makeDownloadTransport: makeDownloadTransport,
ctx: ctx,
cancel: cancel,
}, nil
}
if cfg.ReuseConfig != nil {
var err error
client.uploadManager, err = NewReuseManager(cfg.ReuseConfig, makeTransport)
if err != nil {
return nil, err
}
if cfg.DownloadConfig != nil {
if makeDownloadTransport == nil {
return nil, fmt.Errorf("xhttp: download manager requires download transport maker")
}
client.downloadManager, err = NewReuseManager(cfg.DownloadConfig.ReuseConfig, makeDownloadTransport)
if err != nil {
return nil, err
}
}
}
return client, nil
}
func (c *Client) Close() error {
c.cancel()
var errs []error
if c.uploadManager != nil {
err := c.uploadManager.Close()
if err != nil {
errs = append(errs, err)
}
}
if c.downloadManager != nil {
err := c.downloadManager.Close()
if err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
func (c *Client) Dial() (net.Conn, error) {
@@ -130,13 +190,41 @@ func (c *Client) Dial() (net.Conn, error) {
}
}
func (c *Client) Close() error {
c.cancel()
return nil
// onlyRoundTripper is a wrapper that prevents the underlying transport from being closed.
type onlyRoundTripper struct {
http.RoundTripper
}
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
}
}
}
return
}
func (c *Client) DialStreamOne() (net.Conn, error) {
transport := c.makeTransport()
transport, _, err := c.getTransport()
if err != nil {
return nil, err
}
requestURL := url.URL{
Scheme: "https",
@@ -151,6 +239,7 @@ func (c *Client) DialStreamOne() (net.Conn, error) {
if err != nil {
_ = pr.Close()
_ = pw.Close()
httputils.CloseTransport(transport)
return nil, err
}
req.Host = c.cfg.Host
@@ -158,6 +247,7 @@ func (c *Client) DialStreamOne() (net.Conn, error) {
if err := c.cfg.FillStreamRequest(req, ""); err != nil {
_ = pr.Close()
_ = pw.Close()
httputils.CloseTransport(transport)
return nil, err
}
@@ -185,10 +275,9 @@ func (c *Client) DialStreamOne() (net.Conn, error) {
}
func (c *Client) DialStreamUp() (net.Conn, error) {
uploadTransport := c.makeTransport()
downloadTransport := uploadTransport
if c.makeDownloadTransport != nil {
downloadTransport = c.makeDownloadTransport()
uploadTransport, downloadTransport, err := c.getTransport()
if err != nil {
return nil, err
}
downloadCfg := c.cfg
@@ -221,17 +310,13 @@ func (c *Client) DialStreamUp() (net.Conn, error) {
)
if err != nil {
httputils.CloseTransport(uploadTransport)
if downloadTransport != uploadTransport {
httputils.CloseTransport(downloadTransport)
}
httputils.CloseTransport(downloadTransport)
return nil, err
}
if err := downloadCfg.FillDownloadRequest(downloadReq, sessionID); err != nil {
httputils.CloseTransport(uploadTransport)
if downloadTransport != uploadTransport {
httputils.CloseTransport(downloadTransport)
}
httputils.CloseTransport(downloadTransport)
return nil, err
}
downloadReq.Host = downloadCfg.Host
@@ -239,17 +324,13 @@ func (c *Client) DialStreamUp() (net.Conn, error) {
downloadResp, err := downloadTransport.RoundTrip(downloadReq)
if err != nil {
httputils.CloseTransport(uploadTransport)
if downloadTransport != uploadTransport {
httputils.CloseTransport(downloadTransport)
}
httputils.CloseTransport(downloadTransport)
return nil, err
}
if downloadResp.StatusCode != http.StatusOK {
_ = downloadResp.Body.Close()
httputils.CloseTransport(uploadTransport)
if downloadTransport != uploadTransport {
httputils.CloseTransport(downloadTransport)
}
httputils.CloseTransport(downloadTransport)
return nil, fmt.Errorf("xhttp stream-up download bad status: %s", downloadResp.Status)
}
@@ -264,9 +345,7 @@ func (c *Client) DialStreamUp() (net.Conn, error) {
_ = pr.Close()
_ = pw.Close()
httputils.CloseTransport(uploadTransport)
if downloadTransport != uploadTransport {
httputils.CloseTransport(downloadTransport)
}
httputils.CloseTransport(downloadTransport)
return nil, err
}
@@ -275,9 +354,7 @@ func (c *Client) DialStreamUp() (net.Conn, error) {
_ = pr.Close()
_ = pw.Close()
httputils.CloseTransport(uploadTransport)
if downloadTransport != uploadTransport {
httputils.CloseTransport(downloadTransport)
}
httputils.CloseTransport(downloadTransport)
return nil, err
}
uploadReq.Host = c.cfg.Host
@@ -300,19 +377,16 @@ func (c *Client) DialStreamUp() (net.Conn, error) {
conn.onClose = func() {
_ = pr.Close()
httputils.CloseTransport(uploadTransport)
if downloadTransport != uploadTransport {
httputils.CloseTransport(downloadTransport)
}
httputils.CloseTransport(downloadTransport)
}
return conn, nil
}
func (c *Client) DialPacketUp() (net.Conn, error) {
uploadTransport := c.makeTransport()
downloadTransport := uploadTransport
if c.makeDownloadTransport != nil {
downloadTransport = c.makeDownloadTransport()
uploadTransport, downloadTransport, err := c.getTransport()
if err != nil {
return nil, err
}
downloadCfg := c.cfg
@@ -327,8 +401,10 @@ func (c *Client) DialPacketUp() (net.Conn, error) {
Path: downloadCfg.NormalizedPath(),
}
writerCtx, writerCancel := context.WithCancel(c.ctx)
writer := &PacketUpWriter{
ctx: c.ctx,
ctx: writerCtx,
cancel: writerCancel,
cfg: c.cfg,
sessionID: sessionID,
transport: uploadTransport,
@@ -336,32 +412,41 @@ func (c *Client) DialPacketUp() (net.Conn, error) {
}
conn := &Conn{writer: writer}
downloadReq, err := http.NewRequestWithContext(httputils.NewAddrContext(&conn.NetAddr, c.ctx), http.MethodGet, downloadURL.String(), nil)
downloadReq, err := http.NewRequestWithContext(
httputils.NewAddrContext(&conn.NetAddr, c.ctx),
http.MethodGet,
downloadURL.String(),
nil,
)
if err != nil {
httputils.CloseTransport(uploadTransport)
httputils.CloseTransport(downloadTransport)
return nil, err
}
if err := downloadCfg.FillDownloadRequest(downloadReq, sessionID); err != nil {
httputils.CloseTransport(uploadTransport)
httputils.CloseTransport(downloadTransport)
return nil, err
}
downloadReq.Host = downloadCfg.Host
resp, err := downloadTransport.RoundTrip(downloadReq)
if err != nil {
httputils.CloseTransport(uploadTransport)
httputils.CloseTransport(downloadTransport)
return nil, err
}
if resp.StatusCode != http.StatusOK {
_ = resp.Body.Close()
httputils.CloseTransport(uploadTransport)
if downloadTransport != uploadTransport {
httputils.CloseTransport(downloadTransport)
}
httputils.CloseTransport(downloadTransport)
return nil, fmt.Errorf("xhttp packet-up download bad status: %s", resp.Status)
}
conn.reader = resp.Body
conn.onClose = func() {
if downloadTransport != uploadTransport {
httputils.CloseTransport(downloadTransport)
}
// uploadTransport already closed by writer
httputils.CloseTransport(downloadTransport)
}
return conn, nil
@@ -12,22 +12,25 @@ import (
)
type Config struct {
Host string
Path string
Mode string
Headers map[string]string
NoGRPCHeader bool
XPaddingBytes string
DownloadConfig *Config
Host string
Path string
Mode string
Headers map[string]string
NoGRPCHeader bool
XPaddingBytes string
NoSSEHeader bool // server only
ScStreamUpServerSecs string // server only
ScMaxEachPostBytes int
ReuseConfig *ReuseConfig
DownloadConfig *Config
}
type DownloadConfig struct {
Host string
Path string
Mode string
ServerName string
ClientFingerprint string
SkipCertVerify bool
type ReuseConfig struct {
MaxConnections string
MaxConcurrency string
CMaxReuseTimes string
HMaxRequestTimes string
HMaxReusableSecs string
}
func (c *Config) NormalizedMode() string {
@@ -115,6 +118,38 @@ func (c *Config) RandomPadding() (string, error) {
return strings.Repeat("X", n), nil
}
func (c *Config) GetNormalizedScStreamUpServerSecs() (int, error) {
scStreamUpServerSecs := c.ScStreamUpServerSecs
if scStreamUpServerSecs == "" {
scStreamUpServerSecs = "20-80"
}
minVal, maxVal, err := parseRange(scStreamUpServerSecs)
if err != nil {
return 0, err
}
if minVal < 0 || maxVal < minVal {
return 0, fmt.Errorf("invalid sc-stream-up-server-secs range: %s", scStreamUpServerSecs)
}
if maxVal == 0 {
return 0, nil
}
n := minVal
if maxVal > minVal {
n = minVal + rand.Intn(maxVal-minVal+1)
}
return n, nil
}
func (c *Config) GetNormalizedScMaxEachPostBytes() int {
if c.ScMaxEachPostBytes == 0 {
return 1000000
}
return c.ScMaxEachPostBytes
}
func parseRange(s string) (int, int, error) {
parts := strings.Split(strings.TrimSpace(s), "-")
if len(parts) == 1 {
@@ -139,6 +174,67 @@ func parseRange(s string) (int, int, error) {
return minVal, maxVal, nil
}
func resolveRangeValue(s string, fallback int) (int, error) {
if strings.TrimSpace(s) == "" {
return fallback, nil
}
minVal, maxVal, err := parseRange(s)
if err != nil {
return 0, err
}
if minVal < 0 || maxVal < minVal {
return 0, fmt.Errorf("invalid range: %s", s)
}
if minVal == maxVal {
return minVal, nil
}
return minVal + rand.Intn(maxVal-minVal+1), nil
}
func (c *ReuseConfig) ResolveManagerConfig() (int, int, error) {
if c == nil {
return 0, 0, nil
}
maxConnections, err := resolveRangeValue(c.MaxConnections, 0)
if err != nil {
return 0, 0, fmt.Errorf("invalid max-connections: %w", err)
}
maxConcurrency, err := resolveRangeValue(c.MaxConcurrency, 0)
if err != nil {
return 0, 0, fmt.Errorf("invalid max-concurrency: %w", err)
}
return maxConnections, maxConcurrency, nil
}
func (c *ReuseConfig) ResolveEntryConfig() (int, int, int, error) {
if c == nil {
return 0, 0, 0, nil
}
hMaxRequestTimes, err := resolveRangeValue(c.HMaxRequestTimes, 0)
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid h-max-request-times: %w", err)
}
hMaxReusableSecs, err := resolveRangeValue(c.HMaxReusableSecs, 0)
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid h-max-reusable-secs: %w", err)
}
cMaxReuseTimes, err := resolveRangeValue(c.CMaxReuseTimes, 0)
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid c-max-reuse-times: %w", err)
}
return hMaxRequestTimes, hMaxReusableSecs, cMaxReuseTimes, nil
}
func (c *Config) FillStreamRequest(req *http.Request, sessionID string) error {
req.Header = c.RequestHeader()
@@ -0,0 +1,218 @@
package xhttp
import (
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/metacubex/mihomo/common/httputils"
"github.com/metacubex/http"
)
type reuseEntry struct {
transport http.RoundTripper
openUsage atomic.Int32
leftRequests atomic.Int32
reuseCount atomic.Int32
maxReuseTimes int32
unreusableAt time.Time
closed atomic.Bool
}
func (e *reuseEntry) isClosed() bool {
return e.closed.Load()
}
func (e *reuseEntry) close() {
if !e.closed.CompareAndSwap(false, true) {
return
}
httputils.CloseTransport(e.transport)
}
type ReuseTransport struct {
entry *reuseEntry
manager *ReuseManager
removed atomic.Bool
}
func (rt *ReuseTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return rt.entry.transport.RoundTrip(req)
}
func (rt *ReuseTransport) Close() error {
if !rt.removed.CompareAndSwap(false, true) {
return nil
}
rt.manager.release(rt.entry)
return nil
}
var _ http.RoundTripper = (*ReuseTransport)(nil)
type ReuseManager struct {
cfg *ReuseConfig
maxConnections int
maxConcurrency int
maker TransportMaker
mu sync.Mutex
entries []*reuseEntry
}
func NewReuseManager(cfg *ReuseConfig, makeTransport TransportMaker) (*ReuseManager, error) {
if cfg == nil {
return nil, nil
}
connections, concurrency, err := cfg.ResolveManagerConfig()
if err != nil {
return nil, err
}
_, _, _, err = cfg.ResolveEntryConfig() // check if config is valid
if err != nil {
return nil, err
}
return &ReuseManager{
cfg: cfg,
maxConnections: connections,
maxConcurrency: concurrency,
maker: makeTransport,
entries: make([]*reuseEntry, 0),
}, nil
}
func (m *ReuseManager) Close() error {
if m == nil {
return nil
}
m.mu.Lock()
defer m.mu.Unlock()
for _, entry := range m.entries {
entry.close()
}
m.entries = nil
return nil
}
func (m *ReuseManager) cleanupLocked(now time.Time) {
kept := m.entries[:0]
for _, entry := range m.entries {
if entry.isClosed() {
continue
}
if entry.leftRequests.Load() <= 0 && entry.openUsage.Load() == 0 {
entry.close()
continue
}
if !entry.unreusableAt.IsZero() && now.After(entry.unreusableAt) && entry.openUsage.Load() == 0 {
entry.close()
continue
}
kept = append(kept, entry)
}
m.entries = kept
}
func (m *ReuseManager) release(entry *reuseEntry) {
if entry == nil {
return
}
remaining := entry.openUsage.Add(-1)
if remaining < 0 {
entry.openUsage.Store(0)
remaining = 0
}
if remaining == 0 {
now := time.Now()
if entry.leftRequests.Load() <= 0 ||
(entry.maxReuseTimes > 0 && entry.reuseCount.Load() >= entry.maxReuseTimes) ||
(!entry.unreusableAt.IsZero() && now.After(entry.unreusableAt)) {
entry.close()
}
}
}
func (m *ReuseManager) pickLocked() *reuseEntry {
var best *reuseEntry
for _, entry := range m.entries {
if entry.isClosed() {
continue
}
if entry.leftRequests.Load() <= 0 {
continue
}
if entry.maxReuseTimes > 0 && entry.reuseCount.Load() >= entry.maxReuseTimes {
continue
}
if m.maxConcurrency > 0 && int(entry.openUsage.Load()) >= m.maxConcurrency {
continue
}
if best == nil || entry.openUsage.Load() < best.openUsage.Load() {
best = entry
}
}
return best
}
func (m *ReuseManager) canCreateLocked() bool {
if m.maxConnections <= 0 {
return true
}
return len(m.entries) < m.maxConnections
}
func (m *ReuseManager) newEntryLocked(transport http.RoundTripper, now time.Time) *reuseEntry {
entry := &reuseEntry{transport: transport}
hMaxRequestTimes, hMaxReusableSecs, cMaxReuseTimes, _ := m.cfg.ResolveEntryConfig() // error already checked in [NewReuseManager]
if hMaxRequestTimes > 0 {
entry.leftRequests.Store(int32(hMaxRequestTimes))
} else {
entry.leftRequests.Store(1<<30 - 1)
}
if hMaxReusableSecs > 0 {
entry.unreusableAt = now.Add(time.Duration(hMaxReusableSecs) * time.Second)
}
if cMaxReuseTimes > 0 {
entry.maxReuseTimes = int32(cMaxReuseTimes)
}
m.entries = append(m.entries, entry)
return entry
}
func (m *ReuseManager) GetTransport() (*ReuseTransport, error) {
now := time.Now()
m.mu.Lock()
defer m.mu.Unlock()
m.cleanupLocked(now)
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)
}
if reused {
entry.reuseCount.Add(1)
}
entry.openUsage.Add(1)
if entry.leftRequests.Load() > 0 {
entry.leftRequests.Add(-1)
}
return &ReuseTransport{entry: entry, manager: m}, nil
}
@@ -0,0 +1,224 @@
package xhttp
import (
"sync/atomic"
"testing"
"time"
"github.com/metacubex/http"
)
type testRoundTripper struct {
id int64
}
func (t *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
panic("not used in reuse manager unit tests")
}
func makeTestTransportFactory(counter *atomic.Int64) TransportMaker {
return func() http.RoundTripper {
id := counter.Add(1)
return &testRoundTripper{id: id}
}
}
func transportID(rt http.RoundTripper) int64 {
return rt.(*testRoundTripper).id
}
func TestManagerReuseSameEntry(t *testing.T) {
var created atomic.Int64
manager, err := NewReuseManager(&ReuseConfig{
MaxConnections: "1",
MaxConcurrency: "1",
HMaxRequestTimes: "10",
}, makeTestTransportFactory(&created))
if err != nil {
t.Fatal(err)
}
transport1, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
id1 := transportID(transport1.entry.transport)
transport1.Close()
transport2, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
id2 := transportID(transport2.entry.transport)
if id1 != id2 {
t.Fatalf("expected same transport to be reused, got %d and %d", id1, id2)
}
transport2.Close()
manager.Close()
}
func TestManagerRespectMaxConnections(t *testing.T) {
var created atomic.Int64
manager, err := NewReuseManager(&ReuseConfig{
MaxConnections: "2",
MaxConcurrency: "1",
HMaxRequestTimes: "100",
}, makeTestTransportFactory(&created))
if err != nil {
t.Fatal(err)
}
transport1, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
if transport1 == nil {
t.Fatal("expected first entry")
}
transport2, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
if transport2 == nil {
t.Fatal("expected second entry")
}
if transport1.entry == transport2.entry {
t.Fatal("expected different entries for first two allocations")
}
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")
}
transport1.Close()
transport2.Close()
manager.Close()
}
func TestManagerRotateOnRequestLimit(t *testing.T) {
var created atomic.Int64
manager, err := NewReuseManager(&ReuseConfig{
MaxConnections: "1",
MaxConcurrency: "1",
HMaxRequestTimes: "1",
}, makeTestTransportFactory(&created))
if err != nil {
t.Fatal(err)
}
transport1, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
id1 := transportID(transport1.entry.transport)
transport1.Close()
transport2, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
id2 := transportID(transport2.entry.transport)
if id1 == id2 {
t.Fatalf("expected new transport after request limit, got same id %d", id1)
}
transport2.Close()
manager.Close()
}
func TestManagerRotateOnReusableSecs(t *testing.T) {
var created atomic.Int64
manager, err := NewReuseManager(&ReuseConfig{
MaxConnections: "1",
MaxConcurrency: "1",
HMaxRequestTimes: "100",
HMaxReusableSecs: "1",
}, makeTestTransportFactory(&created))
if err != nil {
t.Fatal(err)
}
transport1, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
id1 := transportID(transport1.entry.transport)
time.Sleep(1100 * time.Millisecond)
transport1.Close()
transport2, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
id2 := transportID(transport2.entry.transport)
if id1 == id2 {
t.Fatalf("expected new transport after reusable timeout, got same id %d", id1)
}
transport2.Close()
manager.Close()
}
func TestManagerRotateOnConnReuseLimit(t *testing.T) {
var created atomic.Int64
manager, err := NewReuseManager(&ReuseConfig{
MaxConnections: "1",
MaxConcurrency: "1",
CMaxReuseTimes: "1",
HMaxRequestTimes: "100",
}, makeTestTransportFactory(&created))
if err != nil {
t.Fatal(err)
}
transport1, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
id1 := transportID(transport1.entry.transport)
transport1.Close()
transport2, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
id2 := transportID(transport2.entry.transport)
if id1 != id2 {
t.Fatalf("expected first reuse to use same transport, got %d and %d", id1, id2)
}
transport2.Close()
transport3, err := manager.GetTransport()
if err != nil {
t.Fatal(err)
}
id3 := transportID(transport3.entry.transport)
if id3 == id2 {
t.Fatalf("expected new transport after c-max-reuse-times limit, got same id %d", id3)
}
transport3.Close()
manager.Close()
}
@@ -16,9 +16,7 @@ import (
)
type ServerOption struct {
Path string
Host string
Mode string
Config
ConnHandler func(net.Conn)
HttpHandler http.Handler
}
@@ -96,9 +94,7 @@ func (s *httpSession) markConnected() {
}
type requestHandler struct {
path string
host string
mode string
config Config
connHandler func(net.Conn)
httpHandler http.Handler
@@ -107,23 +103,10 @@ type requestHandler struct {
}
func NewServerHandler(opt ServerOption) http.Handler {
path := opt.Path
if path == "" {
path = "/"
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
if !strings.HasSuffix(path, "/") {
path += "/"
}
// 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{
path: path,
host: opt.Host,
mode: opt.Mode,
config: opt.Config,
connHandler: opt.ConnHandler,
httpHandler: opt.HttpHandler,
sessions: map[string]*httpSession{},
@@ -162,27 +145,76 @@ func (h *requestHandler) getSession(sessionID string) *httpSession {
return h.sessions[sessionID]
}
func (h *requestHandler) normalizedMode() string {
if h.config.Mode == "" {
return "auto"
}
return h.config.Mode
}
func (h *requestHandler) allowStreamOne() bool {
switch h.normalizedMode() {
case "auto", "stream-one", "stream-up":
return true
default:
return false
}
}
func (h *requestHandler) allowSessionDownload() bool {
switch h.normalizedMode() {
case "auto", "stream-up", "packet-up":
return true
default:
return false
}
}
func (h *requestHandler) allowStreamUpUpload() bool {
switch h.normalizedMode() {
case "auto", "stream-up":
return true
default:
return false
}
}
func (h *requestHandler) allowPacketUpUpload() bool {
switch h.normalizedMode() {
case "auto", "packet-up":
return true
default:
return false
}
}
func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h.httpHandler != nil && !strings.HasPrefix(r.URL.Path, h.path) {
path := h.config.NormalizedPath()
if h.httpHandler != nil && !strings.HasPrefix(r.URL.Path, path) {
h.httpHandler.ServeHTTP(w, r)
return
}
if h.host != "" && !equalHost(r.Host, h.host) {
if h.config.Host != "" && !equalHost(r.Host, h.config.Host) {
http.NotFound(w, r)
return
}
if !strings.HasPrefix(r.URL.Path, h.path) {
if !strings.HasPrefix(r.URL.Path, path) {
http.NotFound(w, r)
return
}
rest := strings.TrimPrefix(r.URL.Path, h.path)
rest := strings.TrimPrefix(r.URL.Path, path)
parts := splitNonEmpty(rest)
// stream-one: POST /path
if r.Method == http.MethodPost && len(parts) == 0 {
if !h.allowStreamOne() {
http.NotFound(w, r)
return
}
w.Header().Set("X-Accel-Buffering", "no")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusOK)
@@ -208,14 +240,27 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
// packet-up download: GET /path/{session}
// stream-up/packet-up download: GET /path/{session}
if r.Method == http.MethodGet && len(parts) == 1 {
if !h.allowSessionDownload() {
http.NotFound(w, r)
return
}
sessionID := parts[0]
session := h.getOrCreateSession(sessionID)
session.markConnected()
// magic header instructs nginx + apache to not buffer response body
w.Header().Set("X-Accel-Buffering", "no")
// A web-compliant header telling all middleboxes to disable caching.
// Should be able to prevent overloading the cache, or stop CDNs from
// teeing the response stream into their cache, causing slowdowns.
w.Header().Set("Cache-Control", "no-store")
if !h.config.NoSSEHeader {
// magic header to make the HTTP middle box consider this as SSE to disable buffer
w.Header().Set("Content-Type", "text/event-stream")
}
w.WriteHeader(http.StatusOK)
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
@@ -244,6 +289,11 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// stream-up upload: POST /path/{session}
if r.Method == http.MethodPost && len(parts) == 1 {
if !h.allowStreamUpUpload() {
http.NotFound(w, r)
return
}
sessionID := parts[0]
session := h.getSession(sessionID)
if session == nil {
@@ -251,38 +301,63 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
buf := make([]byte, 32*1024)
var seq uint64
for {
n, err := r.Body.Read(buf)
if n > 0 {
if pushErr := session.uploadQueue.Push(Packet{
Seq: seq,
Payload: buf[:n],
}); pushErr != nil {
http.Error(w, pushErr.Error(), http.StatusInternalServerError)
return
}
seq++
}
if err == io.EOF {
break
}
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
httpSC := newHTTPServerConn(w, r.Body)
err := session.uploadQueue.Push(Packet{
Reader: httpSC,
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// magic header instructs nginx + apache to not buffer response body
w.Header().Set("X-Accel-Buffering", "no")
// A web-compliant header telling all middleboxes to disable caching.
// Should be able to prevent overloading the cache, or stop CDNs from
// teeing the response stream into their cache, causing slowdowns.
w.Header().Set("Cache-Control", "no-store")
if !h.config.NoSSEHeader {
// magic header to make the HTTP middle box consider this as SSE to disable buffer
w.Header().Set("Content-Type", "text/event-stream")
}
w.WriteHeader(http.StatusOK)
referrer := r.Header.Get("Referer")
if referrer != "" {
go func() {
for {
scStreamUpServerSecs, _ := h.config.GetNormalizedScStreamUpServerSecs()
if scStreamUpServerSecs == 0 {
break
}
paddingValue, _ := h.config.RandomPadding()
if paddingValue == "" {
break
}
_, err = httpSC.Write([]byte(paddingValue))
if err != nil {
break
}
time.Sleep(time.Duration(scStreamUpServerSecs) * time.Second)
}
}()
}
select {
case <-r.Context().Done():
case <-httpSC.Wait():
}
_ = httpSC.Close()
return
}
// packet-up upload: POST /path/{session}/{seq}
if r.Method == http.MethodPost && len(parts) == 2 {
if !h.allowPacketUpUpload() {
http.NotFound(w, r)
return
}
sessionID := parts[0]
seq, err := strconv.ParseUint(parts[1], 10, 64)
if err != nil {
@@ -296,7 +371,13 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
body, err := io.ReadAll(r.Body)
scMaxEachPostBytes := int64(h.config.GetNormalizedScMaxEachPostBytes())
if r.ContentLength > scMaxEachPostBytes {
http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
return
}
body, err := io.ReadAll(io.LimitReader(r.Body, scMaxEachPostBytes+1))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@@ -0,0 +1,99 @@
package xhttp
import (
"io"
"net"
"testing"
"github.com/metacubex/http"
"github.com/metacubex/http/httptest"
"github.com/stretchr/testify/assert"
)
func TestServerHandlerModeRestrictions(t *testing.T) {
testCases := []struct {
name string
mode string
method string
target string
wantStatus int
}{
{
name: "StreamOneAcceptsStreamOne",
mode: "stream-one",
method: http.MethodPost,
target: "https://example.com/xhttp/",
wantStatus: http.StatusOK,
},
{
name: "StreamOneRejectsSessionDownload",
mode: "stream-one",
method: http.MethodGet,
target: "https://example.com/xhttp/session",
wantStatus: http.StatusNotFound,
},
{
name: "StreamUpAcceptsStreamOne",
mode: "stream-up",
method: http.MethodPost,
target: "https://example.com/xhttp/",
wantStatus: http.StatusOK,
},
{
name: "StreamUpAllowsDownloadEndpoint",
mode: "stream-up",
method: http.MethodGet,
target: "https://example.com/xhttp/session",
wantStatus: http.StatusOK,
},
{
name: "StreamUpRejectsPacketUpload",
mode: "stream-up",
method: http.MethodPost,
target: "https://example.com/xhttp/session/0",
wantStatus: http.StatusNotFound,
},
{
name: "PacketUpAllowsDownloadEndpoint",
mode: "packet-up",
method: http.MethodGet,
target: "https://example.com/xhttp/session",
wantStatus: http.StatusOK,
},
{
name: "PacketUpRejectsStreamOne",
mode: "packet-up",
method: http.MethodPost,
target: "https://example.com/xhttp/",
wantStatus: http.StatusNotFound,
},
{
name: "PacketUpRejectsStreamUpUpload",
mode: "packet-up",
method: http.MethodPost,
target: "https://example.com/xhttp/session",
wantStatus: http.StatusNotFound,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
handler := NewServerHandler(ServerOption{
Config: Config{
Path: "/xhttp",
Mode: testCase.mode,
},
ConnHandler: func(conn net.Conn) {
_ = conn.Close()
},
})
req := httptest.NewRequest(testCase.method, testCase.target, io.NopCloser(http.NoBody))
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
assert.Equal(t, testCase.wantStatus, recorder.Result().StatusCode)
})
}
}
@@ -1,6 +1,7 @@
package xhttp
import (
"errors"
"io"
"sync"
)
@@ -8,6 +9,7 @@ import (
type Packet struct {
Seq uint64
Payload []byte
Reader io.ReadCloser
}
type uploadQueue struct {
@@ -17,6 +19,7 @@ type uploadQueue struct {
nextSeq uint64
buf []byte
closed bool
reader io.ReadCloser
}
func NewUploadQueue() *uploadQueue {
@@ -35,6 +38,16 @@ func (q *uploadQueue) Push(p Packet) error {
return io.ErrClosedPipe
}
if q.reader != nil {
return errors.New("uploadQueue.reader already exists")
}
if p.Reader != nil {
q.reader = p.Reader
q.cond.Broadcast()
return nil
}
cp := make([]byte, len(p.Payload))
copy(cp, p.Payload)
q.packets[p.Seq] = cp
@@ -44,12 +57,12 @@ func (q *uploadQueue) Push(p Packet) error {
func (q *uploadQueue) Read(b []byte) (int, error) {
q.mu.Lock()
defer q.mu.Unlock()
for {
if len(q.buf) > 0 {
n := copy(b, q.buf)
q.buf = q.buf[n:]
q.mu.Unlock()
return n, nil
}
@@ -60,7 +73,13 @@ func (q *uploadQueue) Read(b []byte) (int, error) {
continue
}
if reader := q.reader; reader != nil {
q.mu.Unlock() // unlock before calling q.reader.Read
return reader.Read(b)
}
if q.closed {
q.mu.Unlock()
return 0, io.EOF
}
@@ -72,7 +91,11 @@ func (q *uploadQueue) Close() error {
q.mu.Lock()
defer q.mu.Unlock()
var err error
if q.reader != nil {
err = q.reader.Close()
}
q.closed = true
q.cond.Broadcast()
return nil
return err
}
@@ -51,7 +51,7 @@ require (
github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8 // indirect
github.com/metacubex/hkdf v0.1.0 // indirect
github.com/metacubex/hpke v0.1.0 // indirect
github.com/metacubex/http v0.1.0 // indirect
github.com/metacubex/http v0.1.1 // indirect
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 // indirect
github.com/metacubex/mhurl v0.1.0 // indirect
github.com/metacubex/mihomo v1.7.0 // indirect
@@ -67,12 +67,12 @@ require (
github.com/metacubex/sing-shadowsocks v0.2.12 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.7 // indirect
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 // indirect
github.com/metacubex/sing-tun v0.4.16 // indirect
github.com/metacubex/sing-tun v0.4.17 // indirect
github.com/metacubex/sing-vmess v0.2.5 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f // indirect
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 // indirect
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 // indirect
github.com/metacubex/tls v0.1.4 // indirect
github.com/metacubex/tls v0.1.5 // indirect
github.com/metacubex/utls v1.8.4 // indirect
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f // indirect
github.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49 // indirect
@@ -98,8 +98,8 @@ github.com/metacubex/hkdf v0.1.0 h1:fPA6VzXK8cU1foc/TOmGCDmSa7pZbxlnqhl3RNsthaA=
github.com/metacubex/hkdf v0.1.0/go.mod h1:3seEfds3smgTAXqUGn+tgEJH3uXdsUjOiduG/2EtvZ4=
github.com/metacubex/hpke v0.1.0 h1:gu2jUNhraehWi0P/z5HX2md3d7L1FhPQE6/Q0E9r9xQ=
github.com/metacubex/hpke v0.1.0/go.mod h1:vfDm6gfgrwlXUxKDkWbcE44hXtmc1uxLDm2BcR11b3U=
github.com/metacubex/http v0.1.0 h1:Jcy0I9zKjYijSUaksZU34XEe2xNdoFkgUTB7z7K5q0o=
github.com/metacubex/http v0.1.0/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg=
github.com/metacubex/http v0.1.1 h1:zVea0zOoaZvxe51EvJMRWGNQv6MvWqJhkZSuoAjOjVw=
github.com/metacubex/http v0.1.1/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg=
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 h1:hJwCVlE3ojViC35MGHB+FBr8TuIf3BUFn2EQ1VIamsI=
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604/go.mod h1:lpmN3m269b3V5jFCWtffqBLS4U3QQoIid9ugtO+OhVc=
github.com/metacubex/mhurl v0.1.0 h1:ZdW4Zxe3j3uJ89gNytOazHu6kbHn5owutN/VfXOI8GE=
@@ -130,8 +130,8 @@ github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6w
github.com/metacubex/sing-shadowsocks2 v0.2.7/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
github.com/metacubex/sing-tun v0.4.16 h1:LosAe4A6TOPVcD7T1ReV9D2r5501woIXXZiim3D0RRg=
github.com/metacubex/sing-tun v0.4.16/go.mod h1:L/TjQY5JEGy8nvsuYmy/XgMFMCPiF0+AWSFCYfS6r9w=
github.com/metacubex/sing-tun v0.4.17 h1:ehzvPLyxG1vmjaKVeB0aEK1eqhR3reEzdbqQfM3+5XA=
github.com/metacubex/sing-tun v0.4.17/go.mod h1:L/TjQY5JEGy8nvsuYmy/XgMFMCPiF0+AWSFCYfS6r9w=
github.com/metacubex/sing-vmess v0.2.5 h1:m9Zt5I27lB9fmLMZfism9sH2LcnAfShZfwSkf6/KJoE=
github.com/metacubex/sing-vmess v0.2.5/go.mod h1:AwtlzUgf8COe9tRYAKqWZ+leDH7p5U98a0ZUpYehl8Q=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=
@@ -140,8 +140,8 @@ github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 h1:DK2l6m2Fc85H2Bhi
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141/go.mod h1:/yI4OiGOSn0SURhZdJF3CbtPg3nwK700bG8TZLMBvAg=
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 h1:H6TnfM12tOoTizYE/qBHH3nEuibIelmHI+BVSxVJr8o=
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/tls v0.1.4 h1:Gm5GrkyMUh52gYOMIAQ1kHIym4v4M3Qb87Wsmd8Kpdc=
github.com/metacubex/tls v0.1.4/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM=
github.com/metacubex/tls v0.1.5 h1:ECcB83dj+zadnhlKcLnUUf1Sq6+vU0f/zoyU0+9oPTc=
github.com/metacubex/tls v0.1.5/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM=
github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg=
github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f h1:FGBPRb1zUabhPhDrlKEjQ9lgIwQ6cHL4x8M9lrERhbk=
@@ -56,7 +56,7 @@ require (
github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8 // indirect
github.com/metacubex/hkdf v0.1.0 // indirect
github.com/metacubex/hpke v0.1.0 // indirect
github.com/metacubex/http v0.1.0 // indirect
github.com/metacubex/http v0.1.1 // indirect
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 // indirect
github.com/metacubex/mhurl v0.1.0 // indirect
github.com/metacubex/mlkem v0.1.0 // indirect
@@ -71,12 +71,12 @@ require (
github.com/metacubex/sing-shadowsocks v0.2.12 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.7 // indirect
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 // indirect
github.com/metacubex/sing-tun v0.4.16 // indirect
github.com/metacubex/sing-tun v0.4.17 // indirect
github.com/metacubex/sing-vmess v0.2.5 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f // indirect
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 // indirect
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 // indirect
github.com/metacubex/tls v0.1.4 // indirect
github.com/metacubex/tls v0.1.5 // indirect
github.com/metacubex/utls v1.8.4 // indirect
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f // indirect
github.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49 // indirect
@@ -98,8 +98,8 @@ github.com/metacubex/hkdf v0.1.0 h1:fPA6VzXK8cU1foc/TOmGCDmSa7pZbxlnqhl3RNsthaA=
github.com/metacubex/hkdf v0.1.0/go.mod h1:3seEfds3smgTAXqUGn+tgEJH3uXdsUjOiduG/2EtvZ4=
github.com/metacubex/hpke v0.1.0 h1:gu2jUNhraehWi0P/z5HX2md3d7L1FhPQE6/Q0E9r9xQ=
github.com/metacubex/hpke v0.1.0/go.mod h1:vfDm6gfgrwlXUxKDkWbcE44hXtmc1uxLDm2BcR11b3U=
github.com/metacubex/http v0.1.0 h1:Jcy0I9zKjYijSUaksZU34XEe2xNdoFkgUTB7z7K5q0o=
github.com/metacubex/http v0.1.0/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg=
github.com/metacubex/http v0.1.1 h1:zVea0zOoaZvxe51EvJMRWGNQv6MvWqJhkZSuoAjOjVw=
github.com/metacubex/http v0.1.1/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg=
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 h1:hJwCVlE3ojViC35MGHB+FBr8TuIf3BUFn2EQ1VIamsI=
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604/go.mod h1:lpmN3m269b3V5jFCWtffqBLS4U3QQoIid9ugtO+OhVc=
github.com/metacubex/mhurl v0.1.0 h1:ZdW4Zxe3j3uJ89gNytOazHu6kbHn5owutN/VfXOI8GE=
@@ -130,8 +130,8 @@ github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6w
github.com/metacubex/sing-shadowsocks2 v0.2.7/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
github.com/metacubex/sing-tun v0.4.16 h1:LosAe4A6TOPVcD7T1ReV9D2r5501woIXXZiim3D0RRg=
github.com/metacubex/sing-tun v0.4.16/go.mod h1:L/TjQY5JEGy8nvsuYmy/XgMFMCPiF0+AWSFCYfS6r9w=
github.com/metacubex/sing-tun v0.4.17 h1:ehzvPLyxG1vmjaKVeB0aEK1eqhR3reEzdbqQfM3+5XA=
github.com/metacubex/sing-tun v0.4.17/go.mod h1:L/TjQY5JEGy8nvsuYmy/XgMFMCPiF0+AWSFCYfS6r9w=
github.com/metacubex/sing-vmess v0.2.5 h1:m9Zt5I27lB9fmLMZfism9sH2LcnAfShZfwSkf6/KJoE=
github.com/metacubex/sing-vmess v0.2.5/go.mod h1:AwtlzUgf8COe9tRYAKqWZ+leDH7p5U98a0ZUpYehl8Q=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=
@@ -140,8 +140,8 @@ github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 h1:DK2l6m2Fc85H2Bhi
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141/go.mod h1:/yI4OiGOSn0SURhZdJF3CbtPg3nwK700bG8TZLMBvAg=
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 h1:H6TnfM12tOoTizYE/qBHH3nEuibIelmHI+BVSxVJr8o=
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/tls v0.1.4 h1:Gm5GrkyMUh52gYOMIAQ1kHIym4v4M3Qb87Wsmd8Kpdc=
github.com/metacubex/tls v0.1.4/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM=
github.com/metacubex/tls v0.1.5 h1:ECcB83dj+zadnhlKcLnUUf1Sq6+vU0f/zoyU0+9oPTc=
github.com/metacubex/tls v0.1.5/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM=
github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg=
github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f h1:FGBPRb1zUabhPhDrlKEjQ9lgIwQ6cHL4x8M9lrERhbk=
-36
View File
@@ -1,36 +0,0 @@
From b8f897a9da7a82ad8584a22284ceac61262fcb7e Mon Sep 17 00:00:00 2001
From: Jorropo <jorropo.pgm@gmail.com>
Date: Sun, 22 Feb 2026 01:47:45 +0100
Subject: [PATCH] runtime: fix value of ENOSYS on mips from 38 to 89
Fixes #77731
Change-Id: Iaca444e2d5f9e19fd2de38414b357b41471a668c
---
diff --git a/src/runtime/defs_linux_mips64x.go b/src/runtime/defs_linux_mips64x.go
index 7449d2c..4d0f103 100644
--- a/src/runtime/defs_linux_mips64x.go
+++ b/src/runtime/defs_linux_mips64x.go
@@ -12,7 +12,7 @@
_EINTR = 0x4
_EAGAIN = 0xb
_ENOMEM = 0xc
- _ENOSYS = 0x26
+ _ENOSYS = 0x59
_PROT_NONE = 0x0
_PROT_READ = 0x1
diff --git a/src/runtime/defs_linux_mipsx.go b/src/runtime/defs_linux_mipsx.go
index 5a446e0..b8da4d0 100644
--- a/src/runtime/defs_linux_mipsx.go
+++ b/src/runtime/defs_linux_mipsx.go
@@ -12,7 +12,7 @@
_EINTR = 0x4
_EAGAIN = 0xb
_ENOMEM = 0xc
- _ENOSYS = 0x26
+ _ENOSYS = 0x59
_PROT_NONE = 0x0
_PROT_READ = 0x1
-265
View File
@@ -1,265 +0,0 @@
From 1a44be4cecdc742ac6cce9825f9ffc19857c99f3 Mon Sep 17 00:00:00 2001
From: database64128 <free122448@hotmail.com>
Date: Mon, 9 Mar 2026 16:25:16 +0800
Subject: [PATCH] [release-branch.go1.26] internal/poll: move rsan to heap on
windows
According to https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsarecvfrom,
the memory pointed to by lpFromlen must remain available during the
overlapped I/O, and therefore cannot be allocated on the stack.
CL 685417 moved the rsan field out of the operation struct and placed
it on stack, which violates the above requirement and causes stack
corruption.
Unfortunately, it is no longer possible to cleanly revert CL 685417.
Instead of attempting to revert it, this CL bundles rsan together
with rsa in the same sync.Pool. The new wsaRsa struct is still in the
same size class, so no additional overhead is introduced by this
change.
Fixes #78041.
Change-Id: I5ffbccb332515116ddc03fb7c40ffc9293cad2ab
Reviewed-on: https://go-review.googlesource.com/c/go/+/753040
Reviewed-by: Quim Muntal <quimmuntal@gmail.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
Commit-Queue: Cherry Mui <cherryyz@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-on: https://go-review.googlesource.com/c/go/+/753480
Reviewed-by: Mark Freeman <markfreeman@google.com>
---
src/internal/poll/fd_windows.go | 94 +++++++++++++++++++++------------
1 file changed, 59 insertions(+), 35 deletions(-)
diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
index 2ba967f990982f..26319548e3c310 100644
--- a/src/internal/poll/fd_windows.go
+++ b/src/internal/poll/fd_windows.go
@@ -149,7 +149,7 @@ var wsaMsgPool = sync.Pool{
// newWSAMsg creates a new WSAMsg with the provided parameters.
// Use [freeWSAMsg] to free it.
-func newWSAMsg(p []byte, oob []byte, flags int, unconnected bool) *windows.WSAMsg {
+func newWSAMsg(p []byte, oob []byte, flags int, rsa *wsaRsa) *windows.WSAMsg {
// The returned object can't be allocated in the stack because it is accessed asynchronously
// by Windows in between several system calls. If the stack frame is moved while that happens,
// then Windows may access invalid memory.
@@ -166,34 +166,46 @@ func newWSAMsg(p []byte, oob []byte, flags int, unconnected bool) *windows.WSAMs
}
}
msg.Flags = uint32(flags)
- if unconnected {
- msg.Name = wsaRsaPool.Get().(*syscall.RawSockaddrAny)
- msg.Namelen = int32(unsafe.Sizeof(syscall.RawSockaddrAny{}))
+ if rsa != nil {
+ msg.Name = &rsa.name
+ msg.Namelen = rsa.namelen
}
return msg
}
func freeWSAMsg(msg *windows.WSAMsg) {
// Clear pointers to buffers so they can be released by garbage collector.
+ msg.Name = nil
+ msg.Namelen = 0
msg.Buffers.Len = 0
msg.Buffers.Buf = nil
msg.Control.Len = 0
msg.Control.Buf = nil
- if msg.Name != nil {
- *msg.Name = syscall.RawSockaddrAny{}
- wsaRsaPool.Put(msg.Name)
- msg.Name = nil
- msg.Namelen = 0
- }
wsaMsgPool.Put(msg)
}
+// wsaRsa bundles a [syscall.RawSockaddrAny] with its length for efficient caching.
+//
+// When used by WSARecvFrom, wsaRsa must be on the heap. See
+// https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsarecvfrom.
+type wsaRsa struct {
+ name syscall.RawSockaddrAny
+ namelen int32
+}
+
var wsaRsaPool = sync.Pool{
New: func() any {
- return new(syscall.RawSockaddrAny)
+ return new(wsaRsa)
},
}
+func newWSARsa() *wsaRsa {
+ rsa := wsaRsaPool.Get().(*wsaRsa)
+ rsa.name = syscall.RawSockaddrAny{}
+ rsa.namelen = int32(unsafe.Sizeof(syscall.RawSockaddrAny{}))
+ return rsa
+}
+
var operationPool = sync.Pool{
New: func() any {
return new(operation)
@@ -739,19 +751,18 @@ func (fd *FD) ReadFrom(buf []byte) (int, syscall.Sockaddr, error) {
fd.pin('r', &buf[0])
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
+ rsa := newWSARsa()
defer wsaRsaPool.Put(rsa)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
- rsan := int32(unsafe.Sizeof(*rsa))
var flags uint32
- err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, rsa, &rsan, &o.o, nil)
+ err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, &rsa.name, &rsa.namelen, &o.o, nil)
return qty, err
})
err = fd.eofError(n, err)
if err != nil {
return n, nil, err
}
- sa, _ := rsa.Sockaddr()
+ sa, _ := rsa.name.Sockaddr()
return n, sa, nil
}
@@ -770,19 +781,18 @@ func (fd *FD) ReadFromInet4(buf []byte, sa4 *syscall.SockaddrInet4) (int, error)
fd.pin('r', &buf[0])
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
+ rsa := newWSARsa()
defer wsaRsaPool.Put(rsa)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
- rsan := int32(unsafe.Sizeof(*rsa))
var flags uint32
- err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, rsa, &rsan, &o.o, nil)
+ err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, &rsa.name, &rsa.namelen, &o.o, nil)
return qty, err
})
err = fd.eofError(n, err)
if err != nil {
return n, err
}
- rawToSockaddrInet4(rsa, sa4)
+ rawToSockaddrInet4(&rsa.name, sa4)
return n, err
}
@@ -801,19 +811,18 @@ func (fd *FD) ReadFromInet6(buf []byte, sa6 *syscall.SockaddrInet6) (int, error)
fd.pin('r', &buf[0])
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
+ rsa := newWSARsa()
defer wsaRsaPool.Put(rsa)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
- rsan := int32(unsafe.Sizeof(*rsa))
var flags uint32
- err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, rsa, &rsan, &o.o, nil)
+ err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, &rsa.name, &rsa.namelen, &o.o, nil)
return qty, err
})
err = fd.eofError(n, err)
if err != nil {
return n, err
}
- rawToSockaddrInet6(rsa, sa6)
+ rawToSockaddrInet6(&rsa.name, sa6)
return n, err
}
@@ -1373,7 +1382,9 @@ func (fd *FD) ReadMsg(p []byte, oob []byte, flags int) (int, int, int, syscall.S
p = p[:maxRW]
}
- msg := newWSAMsg(p, oob, flags, true)
+ rsa := newWSARsa()
+ defer wsaRsaPool.Put(rsa)
+ msg := newWSAMsg(p, oob, flags, rsa)
defer freeWSAMsg(msg)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
@@ -1398,7 +1409,9 @@ func (fd *FD) ReadMsgInet4(p []byte, oob []byte, flags int, sa4 *syscall.Sockadd
p = p[:maxRW]
}
- msg := newWSAMsg(p, oob, flags, true)
+ rsa := newWSARsa()
+ defer wsaRsaPool.Put(rsa)
+ msg := newWSAMsg(p, oob, flags, rsa)
defer freeWSAMsg(msg)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
@@ -1422,7 +1435,9 @@ func (fd *FD) ReadMsgInet6(p []byte, oob []byte, flags int, sa6 *syscall.Sockadd
p = p[:maxRW]
}
- msg := newWSAMsg(p, oob, flags, true)
+ rsa := newWSARsa()
+ defer wsaRsaPool.Put(rsa)
+ msg := newWSAMsg(p, oob, flags, rsa)
defer freeWSAMsg(msg)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
@@ -1446,15 +1461,18 @@ func (fd *FD) WriteMsg(p []byte, oob []byte, sa syscall.Sockaddr) (int, int, err
}
defer fd.writeUnlock()
- msg := newWSAMsg(p, oob, 0, sa != nil)
- defer freeWSAMsg(msg)
+ var rsa *wsaRsa
if sa != nil {
+ rsa = newWSARsa()
+ defer wsaRsaPool.Put(rsa)
var err error
- msg.Namelen, err = sockaddrToRaw(msg.Name, sa)
+ rsa.namelen, err = sockaddrToRaw(&rsa.name, sa)
if err != nil {
return 0, 0, err
}
}
+ msg := newWSAMsg(p, oob, 0, rsa)
+ defer freeWSAMsg(msg)
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
return qty, err
@@ -1473,11 +1491,14 @@ func (fd *FD) WriteMsgInet4(p []byte, oob []byte, sa *syscall.SockaddrInet4) (in
}
defer fd.writeUnlock()
- msg := newWSAMsg(p, oob, 0, sa != nil)
- defer freeWSAMsg(msg)
+ var rsa *wsaRsa
if sa != nil {
- msg.Namelen = sockaddrInet4ToRaw(msg.Name, sa)
+ rsa = newWSARsa()
+ defer wsaRsaPool.Put(rsa)
+ rsa.namelen = sockaddrInet4ToRaw(&rsa.name, sa)
}
+ msg := newWSAMsg(p, oob, 0, rsa)
+ defer freeWSAMsg(msg)
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
return qty, err
@@ -1496,11 +1517,14 @@ func (fd *FD) WriteMsgInet6(p []byte, oob []byte, sa *syscall.SockaddrInet6) (in
}
defer fd.writeUnlock()
- msg := newWSAMsg(p, oob, 0, sa != nil)
- defer freeWSAMsg(msg)
+ var rsa *wsaRsa
if sa != nil {
- msg.Namelen = sockaddrInet6ToRaw(msg.Name, sa)
+ rsa = newWSARsa()
+ defer wsaRsaPool.Put(rsa)
+ rsa.namelen = sockaddrInet6ToRaw(&rsa.name, sa)
}
+ msg := newWSAMsg(p, oob, 0, rsa)
+ defer freeWSAMsg(msg)
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
return qty, err
-14
View File
@@ -186,20 +186,6 @@ jobs:
- name: Verify Go env
run: go env
# TODO: remove after issue77731 fixed, see: https://github.com/golang/go/issues/77731
- name: Fix issue77731 for Golang1.26
if: ${{ matrix.jobs.goversion == '' }}
run: |
cd $(go env GOROOT)
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/issue77731.patch
# TODO: remove after issue77975 fixed, see: https://github.com/golang/go/issues/77975
- name: Fix issue77975 for Golang1.26
if: ${{ matrix.jobs.goversion == '' }}
run: |
cd $(go env GOROOT)
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/issue77975.patch
# TODO: remove after issue77930 fixed, see: https://github.com/golang/go/issues/77930
- name: Fix issue77930 for Golang1.26
if: ${{ matrix.jobs.goversion == '' }}
-7
View File
@@ -57,13 +57,6 @@ jobs:
- name: Verify Go env
run: go env
# TODO: remove after issue77975 fixed, see: https://github.com/golang/go/issues/77975
- name: Fix issue77975 for Golang1.26
if: ${{ matrix.go-version == '1.26' }}
run: |
cd $(go env GOROOT)
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/issue77975.patch
- name: Remove inbound test for macOS
if: ${{ runner.os == 'macOS' }}
run: |
+2 -2
View File
@@ -82,7 +82,7 @@ type XHTTPOptions struct {
Headers map[string]string `proxy:"headers,omitempty"`
NoGRPCHeader bool `proxy:"no-grpc-header,omitempty"`
XPaddingBytes string `proxy:"x-padding-bytes,omitempty"`
ScMaxEachPostBytes int `proxy:"sc-max-each-post-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"`
}
@@ -102,7 +102,7 @@ type XHTTPDownloadSettings struct {
Headers *map[string]string `proxy:"headers,omitempty"`
NoGRPCHeader *bool `proxy:"no-grpc-header,omitempty"`
XPaddingBytes *string `proxy:"x-padding-bytes,omitempty"`
ScMaxEachPostBytes *int `proxy:"sc-max-each-post-bytes,omitempty"`
ScMaxEachPostBytes *string `proxy:"sc-max-each-post-bytes,omitempty"`
ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX
// proxy part
Server *string `proxy:"server,omitempty"`
+2 -2
View File
@@ -1218,8 +1218,8 @@ proxies: # socks5
# quic: true # 默认为false
# congestion-controller: bbr
### reuse options
# max-connections: 1 # Maximum connections. Conflict with max-streams.
# min-streams: 0 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams.
# 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.
# max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams.
# dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理
+2 -2
View File
@@ -21,7 +21,7 @@ require (
github.com/metacubex/edwards25519 v1.2.0
github.com/metacubex/fswatch v0.1.1
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
github.com/metacubex/http v0.1.0
github.com/metacubex/http v0.1.1
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604
github.com/metacubex/mhurl v0.1.0
github.com/metacubex/mlkem v0.1.0
@@ -39,7 +39,7 @@ require (
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443
github.com/metacubex/tls v0.1.4
github.com/metacubex/tls v0.1.5
github.com/metacubex/utls v1.8.4
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f
github.com/mroth/weightedrand/v2 v2.1.0
+4 -4
View File
@@ -103,8 +103,8 @@ github.com/metacubex/hkdf v0.1.0 h1:fPA6VzXK8cU1foc/TOmGCDmSa7pZbxlnqhl3RNsthaA=
github.com/metacubex/hkdf v0.1.0/go.mod h1:3seEfds3smgTAXqUGn+tgEJH3uXdsUjOiduG/2EtvZ4=
github.com/metacubex/hpke v0.1.0 h1:gu2jUNhraehWi0P/z5HX2md3d7L1FhPQE6/Q0E9r9xQ=
github.com/metacubex/hpke v0.1.0/go.mod h1:vfDm6gfgrwlXUxKDkWbcE44hXtmc1uxLDm2BcR11b3U=
github.com/metacubex/http v0.1.0 h1:Jcy0I9zKjYijSUaksZU34XEe2xNdoFkgUTB7z7K5q0o=
github.com/metacubex/http v0.1.0/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg=
github.com/metacubex/http v0.1.1 h1:zVea0zOoaZvxe51EvJMRWGNQv6MvWqJhkZSuoAjOjVw=
github.com/metacubex/http v0.1.1/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg=
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 h1:hJwCVlE3ojViC35MGHB+FBr8TuIf3BUFn2EQ1VIamsI=
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604/go.mod h1:lpmN3m269b3V5jFCWtffqBLS4U3QQoIid9ugtO+OhVc=
github.com/metacubex/mhurl v0.1.0 h1:ZdW4Zxe3j3uJ89gNytOazHu6kbHn5owutN/VfXOI8GE=
@@ -145,8 +145,8 @@ github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 h1:DK2l6m2Fc85H2Bhi
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141/go.mod h1:/yI4OiGOSn0SURhZdJF3CbtPg3nwK700bG8TZLMBvAg=
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 h1:H6TnfM12tOoTizYE/qBHH3nEuibIelmHI+BVSxVJr8o=
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/tls v0.1.4 h1:Gm5GrkyMUh52gYOMIAQ1kHIym4v4M3Qb87Wsmd8Kpdc=
github.com/metacubex/tls v0.1.4/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM=
github.com/metacubex/tls v0.1.5 h1:ECcB83dj+zadnhlKcLnUUf1Sq6+vU0f/zoyU0+9oPTc=
github.com/metacubex/tls v0.1.5/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM=
github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg=
github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f h1:FGBPRb1zUabhPhDrlKEjQ9lgIwQ6cHL4x8M9lrERhbk=
+1 -1
View File
@@ -36,7 +36,7 @@ type XHTTPConfig struct {
Mode string
NoSSEHeader bool
ScStreamUpServerSecs string
ScMaxEachPostBytes int
ScMaxEachPostBytes string
}
func (t VlessServer) String() string {
+1 -1
View File
@@ -37,7 +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"`
ScMaxEachPostBytes int `inbound:"sc-max-each-post-bytes,omitempty"`
ScMaxEachPostBytes string `inbound:"sc-max-each-post-bytes,omitempty"`
}
func (o XHTTPConfig) Build() LC.XHTTPConfig {
+4 -1
View File
@@ -154,7 +154,7 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition)
}
}
if config.XHTTPConfig.Path != "" || config.XHTTPConfig.Host != "" || config.XHTTPConfig.Mode != "" {
httpServer.Handler = xhttp.NewServerHandler(xhttp.ServerOption{
httpServer.Handler, err = xhttp.NewServerHandler(xhttp.ServerOption{
Config: xhttp.Config{
Host: config.XHTTPConfig.Host,
Path: config.XHTTPConfig.Path,
@@ -168,6 +168,9 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition)
},
HttpHandler: httpServer.Handler,
})
if err != nil {
return nil, err
}
if !slices.Contains(tlsConfig.NextProtos, "http/1.1") {
tlsConfig.NextProtos = append([]string{"http/1.1"}, tlsConfig.NextProtos...)
}
+14 -38
View File
@@ -168,52 +168,37 @@ func (c *Client) roundTrip(request *http.Request, conn *httpConn) {
}()
}
func (c *Client) Dial(ctx context.Context, host string) (net.Conn, error) {
func (c *Client) newConnectRequest(host, userAgent string) *http.Request {
request := &http.Request{
Method: http.MethodConnect,
URL: &url.URL{
Scheme: "https",
Host: host,
Host: c.server, // Use the proxy server authority so the pool keys reuse against the actual proxy endpoint.
},
Header: make(http.Header),
Host: host,
Host: host, // Send the actual CONNECT target as the Host header (:authority).
}
request.Header.Add("User-Agent", TCPUserAgent)
request.Header.Add("User-Agent", userAgent)
request.Header.Add("Proxy-Authorization", c.auth)
return request
}
func (c *Client) Dial(ctx context.Context, host string) (net.Conn, error) {
request := c.newConnectRequest(host, TCPUserAgent)
conn := &tcpConn{}
c.roundTrip(request, &conn.httpConn)
return conn, nil
}
func (c *Client) ListenPacket(ctx context.Context) (net.PacketConn, error) {
request := &http.Request{
Method: http.MethodConnect,
URL: &url.URL{
Scheme: "https",
Host: UDPMagicAddress,
},
Header: make(http.Header),
Host: UDPMagicAddress,
}
request.Header.Add("User-Agent", UDPUserAgent)
request.Header.Add("Proxy-Authorization", c.auth)
request := c.newConnectRequest(UDPMagicAddress, UDPUserAgent)
conn := &clientPacketConn{}
c.roundTrip(request, &conn.httpConn)
return conn, nil
}
func (c *Client) ListenICMP(ctx context.Context) (*IcmpConn, error) {
request := &http.Request{
Method: http.MethodConnect,
URL: &url.URL{
Scheme: "https",
Host: ICMPMagicAddress,
},
Header: make(http.Header),
Host: ICMPMagicAddress,
}
request.Header.Add("User-Agent", ICMPUserAgent)
request.Header.Add("Proxy-Authorization", c.auth)
request := c.newConnectRequest(ICMPMagicAddress, ICMPUserAgent)
conn := &IcmpConn{}
c.roundTrip(request, &conn.httpConn)
return conn, nil
@@ -234,17 +219,7 @@ func (c *Client) ResetConnections() {
func (c *Client) HealthCheck(ctx context.Context) error {
defer c.resetHealthCheckTimer()
request := &http.Request{
Method: http.MethodConnect,
URL: &url.URL{
Scheme: "https",
Host: HealthCheckMagicAddress,
},
Header: make(http.Header),
Host: HealthCheckMagicAddress,
}
request.Header.Add("User-Agent", HealthCheckUserAgent)
request.Header.Add("Proxy-Authorization", c.auth)
request := c.newConnectRequest(HealthCheckMagicAddress, HealthCheckUserAgent)
response, err := c.roundTripper.RoundTrip(request.WithContext(ctx))
if err != nil {
return err
@@ -271,7 +246,8 @@ func NewPoolClient(ctx context.Context, options ClientOptions) (*PoolClient, err
minStreams := options.MinStreams
maxStreams := options.MaxStreams
if maxConnections == 0 && minStreams == 0 && maxStreams == 0 {
maxConnections = 1
maxConnections = 8
minStreams = 5
}
client, err := NewClient(ctx, options) // reserve one client and verify the configuration
if err != nil {
+22 -14
View File
@@ -24,19 +24,20 @@ type WrapTLSFunc func(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn,
type TransportMaker func() http.RoundTripper
type PacketUpWriter struct {
ctx context.Context
cancel context.CancelFunc
cfg *Config
sessionID string
transport http.RoundTripper
writeMu sync.Mutex
seq uint64
ctx context.Context
cancel context.CancelFunc
cfg *Config
scMaxEachPostBytes Range
sessionID string
transport http.RoundTripper
writeMu sync.Mutex
seq uint64
}
func (c *PacketUpWriter) Write(b []byte) (int, error) {
c.writeMu.Lock()
defer c.writeMu.Unlock()
scMaxEachPostBytes := c.cfg.GetNormalizedScMaxEachPostBytes()
scMaxEachPostBytes := c.scMaxEachPostBytes.Rand()
if len(b) < scMaxEachPostBytes {
return c.write(b)
}
@@ -117,6 +118,7 @@ type Client struct {
cancel context.CancelFunc
mode string
cfg *Config
scMaxEachPostBytes Range
makeTransport TransportMaker
makeDownloadTransport TransportMaker
uploadManager *ReuseManager
@@ -130,11 +132,16 @@ func NewClient(cfg *Config, makeTransport TransportMaker, makeDownloadTransport
default:
return nil, fmt.Errorf("xhttp mode %s is not implemented yet", mode)
}
scMaxEachPostBytes, err := cfg.GetNormalizedScMaxEachPostBytes()
if err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(context.Background())
client := &Client{
mode: mode,
cfg: cfg,
scMaxEachPostBytes: scMaxEachPostBytes,
makeTransport: makeTransport,
makeDownloadTransport: makeDownloadTransport,
ctx: ctx,
@@ -403,12 +410,13 @@ func (c *Client) DialPacketUp() (net.Conn, error) {
writerCtx, writerCancel := context.WithCancel(c.ctx)
writer := &PacketUpWriter{
ctx: writerCtx,
cancel: writerCancel,
cfg: c.cfg,
sessionID: sessionID,
transport: uploadTransport,
seq: 0,
ctx: writerCtx,
cancel: writerCancel,
cfg: c.cfg,
scMaxEachPostBytes: c.scMaxEachPostBytes,
sessionID: sessionID,
transport: uploadTransport,
seq: 0,
}
conn := &Conn{writer: writer}
+57 -85
View File
@@ -20,7 +20,7 @@ type Config struct {
XPaddingBytes string
NoSSEHeader bool // server only
ScStreamUpServerSecs string // server only
ScMaxEachPostBytes int
ScMaxEachPostBytes string
ReuseConfig *ReuseConfig
DownloadConfig *Config
}
@@ -94,142 +94,114 @@ func (c *Config) RequestHeader() http.Header {
}
func (c *Config) RandomPadding() (string, error) {
paddingRange := c.XPaddingBytes
if paddingRange == "" {
paddingRange = "100-1000"
}
minVal, maxVal, err := parseRange(paddingRange)
r, err := ParseRange(c.XPaddingBytes, "100-1000")
if err != nil {
return "", err
return "", fmt.Errorf("invalid x-padding-bytes: %w", err)
}
if minVal < 0 || maxVal < minVal {
return "", fmt.Errorf("invalid x-padding-bytes range: %s", paddingRange)
}
if maxVal == 0 {
return "", nil
}
n := minVal
if maxVal > minVal {
n = minVal + rand.Intn(maxVal-minVal+1)
}
return strings.Repeat("X", n), nil
return strings.Repeat("X", r.Rand()), nil
}
func (c *Config) GetNormalizedScStreamUpServerSecs() (int, error) {
scStreamUpServerSecs := c.ScStreamUpServerSecs
if scStreamUpServerSecs == "" {
scStreamUpServerSecs = "20-80"
}
minVal, maxVal, err := parseRange(scStreamUpServerSecs)
func (c *Config) GetNormalizedScStreamUpServerSecs() (Range, error) {
r, err := ParseRange(c.ScStreamUpServerSecs, "20-80")
if err != nil {
return 0, err
return Range{}, fmt.Errorf("invalid sc-stream-up-server-secs: %w", err)
}
if minVal < 0 || maxVal < minVal {
return 0, fmt.Errorf("invalid sc-stream-up-server-secs range: %s", scStreamUpServerSecs)
}
if maxVal == 0 {
return 0, nil
}
n := minVal
if maxVal > minVal {
n = minVal + rand.Intn(maxVal-minVal+1)
}
return n, nil
return r, nil
}
func (c *Config) GetNormalizedScMaxEachPostBytes() int {
if c.ScMaxEachPostBytes == 0 {
return 1000000
func (c *Config) GetNormalizedScMaxEachPostBytes() (Range, error) {
r, err := ParseRange(c.ScStreamUpServerSecs, "1000000")
if err != nil {
return Range{}, fmt.Errorf("invalid sc-max-each-post-bytes: %w", err)
}
return c.ScMaxEachPostBytes
if r.Max == 0 {
return Range{}, fmt.Errorf("invalid sc-max-each-post-bytes: must be greater than zero")
}
return r, nil
}
func parseRange(s string) (int, int, error) {
type Range struct {
Min int
Max int
}
func (r Range) Rand() int {
if r.Min == r.Max {
return r.Min
}
return r.Min + rand.Intn(r.Max-r.Min+1)
}
func ParseRange(s string, fallback string) (Range, error) {
if strings.TrimSpace(s) == "" {
return parseRange(fallback)
}
return parseRange(s)
}
func parseRange(s string) (Range, error) {
parts := strings.Split(strings.TrimSpace(s), "-")
if len(parts) == 1 {
v, err := strconv.Atoi(parts[0])
if err != nil {
return 0, 0, err
return Range{}, err
}
return v, v, nil
return Range{v, v}, nil
}
if len(parts) != 2 {
return 0, 0, fmt.Errorf("invalid range: %s", s)
return Range{}, fmt.Errorf("invalid range: %s", s)
}
minVal, err := strconv.Atoi(strings.TrimSpace(parts[0]))
if err != nil {
return 0, 0, err
return Range{}, err
}
maxVal, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil {
return 0, 0, err
}
return minVal, maxVal, nil
}
func resolveRangeValue(s string, fallback int) (int, error) {
if strings.TrimSpace(s) == "" {
return fallback, nil
}
minVal, maxVal, err := parseRange(s)
if err != nil {
return 0, err
return Range{}, err
}
if minVal < 0 || maxVal < minVal {
return 0, fmt.Errorf("invalid range: %s", s)
return Range{}, fmt.Errorf("invalid range: %s", s)
}
if minVal == maxVal {
return minVal, nil
}
return minVal + rand.Intn(maxVal-minVal+1), nil
return Range{minVal, maxVal}, nil
}
func (c *ReuseConfig) ResolveManagerConfig() (int, int, error) {
func (c *ReuseConfig) ResolveManagerConfig() (Range, Range, error) {
if c == nil {
return 0, 0, nil
return Range{}, Range{}, nil
}
maxConnections, err := resolveRangeValue(c.MaxConnections, 0)
maxConnections, err := ParseRange(c.MaxConnections, "0")
if err != nil {
return 0, 0, fmt.Errorf("invalid max-connections: %w", err)
return Range{}, Range{}, fmt.Errorf("invalid max-connections: %w", err)
}
maxConcurrency, err := resolveRangeValue(c.MaxConcurrency, 0)
maxConcurrency, err := ParseRange(c.MaxConcurrency, "0")
if err != nil {
return 0, 0, fmt.Errorf("invalid max-concurrency: %w", err)
return Range{}, Range{}, fmt.Errorf("invalid max-concurrency: %w", err)
}
return maxConnections, maxConcurrency, nil
}
func (c *ReuseConfig) ResolveEntryConfig() (int, int, int, error) {
func (c *ReuseConfig) ResolveEntryConfig() (Range, Range, Range, error) {
if c == nil {
return 0, 0, 0, nil
return Range{}, Range{}, Range{}, nil
}
hMaxRequestTimes, err := resolveRangeValue(c.HMaxRequestTimes, 0)
hMaxRequestTimes, err := ParseRange(c.HMaxRequestTimes, "0")
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid h-max-request-times: %w", err)
return Range{}, Range{}, Range{}, fmt.Errorf("invalid h-max-request-times: %w", err)
}
hMaxReusableSecs, err := resolveRangeValue(c.HMaxReusableSecs, 0)
hMaxReusableSecs, err := ParseRange(c.HMaxReusableSecs, "0")
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid h-max-reusable-secs: %w", err)
return Range{}, Range{}, Range{}, fmt.Errorf("invalid h-max-reusable-secs: %w", err)
}
cMaxReuseTimes, err := resolveRangeValue(c.CMaxReuseTimes, 0)
cMaxReuseTimes, err := ParseRange(c.CMaxReuseTimes, "0")
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid c-max-reuse-times: %w", err)
return Range{}, Range{}, Range{}, fmt.Errorf("invalid c-max-reuse-times: %w", err)
}
return hMaxRequestTimes, hMaxReusableSecs, cMaxReuseTimes, nil
+22 -19
View File
@@ -55,12 +55,14 @@ func (rt *ReuseTransport) Close() error {
var _ http.RoundTripper = (*ReuseTransport)(nil)
type ReuseManager struct {
cfg *ReuseConfig
maxConnections int
maxConcurrency int
maker TransportMaker
mu sync.Mutex
entries []*reuseEntry
maxConnections int
maxConcurrency int
hMaxRequestTimes Range
hMaxReusableSecs Range
cMaxReuseTimes Range
maker TransportMaker
mu sync.Mutex
entries []*reuseEntry
}
func NewReuseManager(cfg *ReuseConfig, makeTransport TransportMaker) (*ReuseManager, error) {
@@ -71,16 +73,18 @@ func NewReuseManager(cfg *ReuseConfig, makeTransport TransportMaker) (*ReuseMana
if err != nil {
return nil, err
}
_, _, _, err = cfg.ResolveEntryConfig() // check if config is valid
hMaxRequestTimes, hMaxReusableSecs, cMaxReuseTimes, err := cfg.ResolveEntryConfig()
if err != nil {
return nil, err
}
return &ReuseManager{
cfg: cfg,
maxConnections: connections,
maxConcurrency: concurrency,
maker: makeTransport,
entries: make([]*reuseEntry, 0),
maxConnections: connections.Rand(),
maxConcurrency: concurrency.Rand(),
hMaxRequestTimes: hMaxRequestTimes,
hMaxReusableSecs: hMaxReusableSecs,
cMaxReuseTimes: cMaxReuseTimes,
maker: makeTransport,
entries: make([]*reuseEntry, 0),
}, nil
}
@@ -169,17 +173,16 @@ func (m *ReuseManager) canCreateLocked() bool {
func (m *ReuseManager) newEntryLocked(transport http.RoundTripper, now time.Time) *reuseEntry {
entry := &reuseEntry{transport: transport}
hMaxRequestTimes, hMaxReusableSecs, cMaxReuseTimes, _ := m.cfg.ResolveEntryConfig() // error already checked in [NewReuseManager]
if hMaxRequestTimes > 0 {
entry.leftRequests.Store(int32(hMaxRequestTimes))
if m.hMaxRequestTimes.Max > 0 {
entry.leftRequests.Store(int32(m.hMaxRequestTimes.Rand()))
} else {
entry.leftRequests.Store(1<<30 - 1)
}
if hMaxReusableSecs > 0 {
entry.unreusableAt = now.Add(time.Duration(hMaxReusableSecs) * time.Second)
if m.hMaxReusableSecs.Max > 0 {
entry.unreusableAt = now.Add(time.Duration(m.hMaxReusableSecs.Rand()) * time.Second)
}
if cMaxReuseTimes > 0 {
entry.maxReuseTimes = int32(cMaxReuseTimes)
if m.cMaxReuseTimes.Max > 0 {
entry.maxReuseTimes = int32(m.cMaxReuseTimes.Rand())
}
m.entries = append(m.entries, entry)
+27 -39
View File
@@ -98,21 +98,34 @@ type requestHandler struct {
connHandler func(net.Conn)
httpHandler http.Handler
scMaxEachPostBytes Range
scStreamUpServerSecs Range
mu sync.Mutex
sessions map[string]*httpSession
}
func NewServerHandler(opt ServerOption) http.Handler {
func NewServerHandler(opt ServerOption) (http.Handler, error) {
scMaxEachPostBytes, err := opt.Config.GetNormalizedScMaxEachPostBytes()
if err != nil {
return nil, err
}
scStreamUpServerSecs, err := opt.Config.GetNormalizedScStreamUpServerSecs()
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{
config: opt.Config,
connHandler: opt.ConnHandler,
httpHandler: opt.HttpHandler,
sessions: map[string]*httpSession{},
config: opt.Config,
connHandler: opt.ConnHandler,
httpHandler: opt.HttpHandler,
scMaxEachPostBytes: scMaxEachPostBytes,
scStreamUpServerSecs: scStreamUpServerSecs,
sessions: map[string]*httpSession{},
}, &http.Http2Server{
IdleTimeout: 30 * time.Second,
})
}), nil
}
func (h *requestHandler) getOrCreateSession(sessionID string) *httpSession {
@@ -209,12 +222,7 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
parts := splitNonEmpty(rest)
// stream-one: POST /path
if r.Method == http.MethodPost && len(parts) == 0 {
if !h.allowStreamOne() {
http.NotFound(w, r)
return
}
if r.Method == http.MethodPost && len(parts) == 0 && h.allowStreamOne() {
w.Header().Set("X-Accel-Buffering", "no")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusOK)
@@ -241,12 +249,7 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
// stream-up/packet-up download: GET /path/{session}
if r.Method == http.MethodGet && len(parts) == 1 {
if !h.allowSessionDownload() {
http.NotFound(w, r)
return
}
if r.Method == http.MethodGet && len(parts) == 1 && h.allowSessionDownload() {
sessionID := parts[0]
session := h.getOrCreateSession(sessionID)
session.markConnected()
@@ -288,12 +291,7 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
// stream-up upload: POST /path/{session}
if r.Method == http.MethodPost && len(parts) == 1 {
if !h.allowStreamUpUpload() {
http.NotFound(w, r)
return
}
if r.Method == http.MethodPost && len(parts) == 1 && h.allowStreamUpUpload() {
sessionID := parts[0]
session := h.getSession(sessionID)
if session == nil {
@@ -322,13 +320,9 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusOK)
referrer := r.Header.Get("Referer")
if referrer != "" {
if referrer != "" && h.scStreamUpServerSecs.Max > 0 {
go func() {
for {
scStreamUpServerSecs, _ := h.config.GetNormalizedScStreamUpServerSecs()
if scStreamUpServerSecs == 0 {
break
}
paddingValue, _ := h.config.RandomPadding()
if paddingValue == "" {
break
@@ -337,7 +331,7 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err != nil {
break
}
time.Sleep(time.Duration(scStreamUpServerSecs) * time.Second)
time.Sleep(time.Duration(h.scStreamUpServerSecs.Rand()) * time.Second)
}
}()
}
@@ -352,12 +346,7 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
// packet-up upload: POST /path/{session}/{seq}
if r.Method == http.MethodPost && len(parts) == 2 {
if !h.allowPacketUpUpload() {
http.NotFound(w, r)
return
}
if r.Method == http.MethodPost && len(parts) == 2 && h.allowPacketUpUpload() {
sessionID := parts[0]
seq, err := strconv.ParseUint(parts[1], 10, 64)
if err != nil {
@@ -371,13 +360,12 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
scMaxEachPostBytes := int64(h.config.GetNormalizedScMaxEachPostBytes())
if r.ContentLength > scMaxEachPostBytes {
if r.ContentLength > int64(h.scMaxEachPostBytes.Max) {
http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
return
}
body, err := io.ReadAll(io.LimitReader(r.Body, scMaxEachPostBytes+1))
body, err := io.ReadAll(io.LimitReader(r.Body, int64(h.scMaxEachPostBytes.Max)+1))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
+4 -1
View File
@@ -78,7 +78,7 @@ func TestServerHandlerModeRestrictions(t *testing.T) {
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
handler := NewServerHandler(ServerOption{
handler, err := NewServerHandler(ServerOption{
Config: Config{
Path: "/xhttp",
Mode: testCase.mode,
@@ -87,6 +87,9 @@ func TestServerHandlerModeRestrictions(t *testing.T) {
_ = conn.Close()
},
})
if err != nil {
panic(err)
}
req := httptest.NewRequest(testCase.method, testCase.target, io.NopCloser(http.NoBody))
recorder := httptest.NewRecorder()
@@ -34,7 +34,7 @@
"@tanstack/router-zod-adapter": "1.81.5",
"@tauri-apps/api": "2.10.1",
"@uidotdev/usehooks": "2.4.1",
"@uiw/react-color": "2.9.6",
"@uiw/react-color": "2.10.0",
"ahooks": "3.9.7",
"allotment": "1.20.5",
"class-variance-authority": "0.7.1",
@@ -44,7 +44,7 @@
"dayjs": "1.11.20",
"framer-motion": "12.38.0",
"i18next": "25.10.10",
"jotai": "2.19.0",
"jotai": "2.19.1",
"json-schema": "0.4.0",
"material-react-table": "3.2.1",
"monaco-editor": "0.55.1",
@@ -97,7 +97,7 @@
"clsx": "2.1.1",
"core-js": "3.49.0",
"filesize": "11.0.15",
"meta-json-schema": "1.19.22",
"meta-json-schema": "1.19.23",
"monaco-yaml": "5.4.1",
"nanoid": "5.1.7",
"sass-embedded": "1.99.0",
+2 -2
View File
@@ -2,7 +2,7 @@
"manifest_version": 1,
"latest": {
"mihomo": "v1.19.22",
"mihomo_alpha": "alpha-4730f67",
"mihomo_alpha": "alpha-5e8bd85",
"clash_rs": "v0.9.6",
"clash_premium": "2023-09-05-gdcc8d87",
"clash_rs_alpha": "0.9.6-alpha+sha.c414fb7"
@@ -69,5 +69,5 @@
"linux-armv7hf": "clash-rs-armv7-unknown-linux-gnueabihf"
}
},
"updated_at": "2026-04-06T22:23:54.897Z"
"updated_at": "2026-04-07T22:24:28.715Z"
}
+2 -2
View File
@@ -68,11 +68,11 @@
"cross-env": "10.1.0",
"dedent": "1.7.2",
"globals": "17.4.0",
"knip": "6.3.0",
"knip": "6.3.1",
"lint-staged": "16.4.0",
"npm-run-all2": "8.0.4",
"oxlint": "1.59.0",
"postcss": "8.5.8",
"postcss": "8.5.9",
"postcss-html": "1.8.1",
"postcss-import": "16.1.1",
"postcss-scss": "4.0.9",
+206 -206
View File
@@ -45,7 +45,7 @@ importers:
version: 24.11.0
autoprefixer:
specifier: 10.4.27
version: 10.4.27(postcss@8.5.8)
version: 10.4.27(postcss@8.5.9)
conventional-changelog-conventionalcommits:
specifier: 9.3.1
version: 9.3.1
@@ -59,8 +59,8 @@ importers:
specifier: 17.4.0
version: 17.4.0
knip:
specifier: 6.3.0
version: 6.3.0
specifier: 6.3.1
version: 6.3.1
lint-staged:
specifier: 16.4.0
version: 16.4.0
@@ -71,17 +71,17 @@ importers:
specifier: 1.59.0
version: 1.59.0
postcss:
specifier: 8.5.8
version: 8.5.8
specifier: 8.5.9
version: 8.5.9
postcss-html:
specifier: 1.8.1
version: 1.8.1
postcss-import:
specifier: 16.1.1
version: 16.1.1(postcss@8.5.8)
version: 16.1.1(postcss@8.5.9)
postcss-scss:
specifier: 4.0.9
version: 4.0.9(postcss@8.5.8)
version: 4.0.9(postcss@8.5.9)
prettier:
specifier: 3.8.1
version: 3.8.1
@@ -234,8 +234,8 @@ importers:
specifier: 2.4.1
version: 2.4.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color':
specifier: 2.9.6
version: 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
specifier: 2.10.0
version: 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
ahooks:
specifier: 3.9.7
version: 3.9.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -264,8 +264,8 @@ importers:
specifier: 25.10.10
version: 25.10.10(typescript@5.9.3)
jotai:
specifier: 2.19.0
version: 2.19.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4)
specifier: 2.19.1
version: 2.19.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4)
json-schema:
specifier: 0.4.0
version: 0.4.0
@@ -418,8 +418,8 @@ importers:
specifier: 11.0.15
version: 11.0.15
meta-json-schema:
specifier: 1.19.22
version: 1.19.22
specifier: 1.19.23
version: 1.19.23
monaco-yaml:
specifier: 5.4.1
version: 5.4.1(monaco-editor@0.55.1)
@@ -449,7 +449,7 @@ importers:
version: 3.2.2(vite@7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2))
vite-plugin-sass-dts:
specifier: 1.3.37
version: 1.3.37(postcss@8.5.8)(prettier@3.8.1)(sass-embedded@1.99.0)(vite@7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2))
version: 1.3.37(postcss@8.5.9)(prettier@3.8.1)(sass-embedded@1.99.0)(vite@7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2))
vite-plugin-svgr:
specifier: 4.5.0
version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2))
@@ -4457,151 +4457,151 @@ packages:
react: '>=18.0.0'
react-dom: '>=18.0.0'
'@uiw/color-convert@2.9.6':
resolution: {integrity: sha512-w8TpU3MRcquurQJxWR1daKcRygu/a0hLP/VGsLMA3ebb41sAZGxMQLHtS+zC/e3ciFNB7BbPrSPlzOcz6w6cRg==}
'@uiw/color-convert@2.10.0':
resolution: {integrity: sha512-4woviyrzPi3Xauo12lnKTgNlugCk0su2PhjtZiHZuakK0yQNOMeN/QZi9W8ASb/DuRS7bSr8jSyIHLRYml90+g==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
'@uiw/react-color-alpha@2.9.6':
resolution: {integrity: sha512-DNzEVHZ0Izp4NAwzKqTcl4rLdPjSFjyZCP6Q2vKJEglugZ/bdPsmZaos9IYOrgnd1kPDmTSKZ/p8nI7vBIATGw==}
'@uiw/react-color-alpha@2.10.0':
resolution: {integrity: sha512-2uGoCuDlqBm3CN/RI3XbOwxIF7GJKwyUUpN/krMEOLg7HhIJMYWxrhDZ4SCQecGPeR/b8o3XTxV7+PHIZMcgMw==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@uiw/react-color-block@2.9.6':
resolution: {integrity: sha512-Y2D/ejhxRZKjmmaKLfKhRKgFS68R7SmkgVdX1KDZ4lYtfdUbXIccYjoL0dsYvQkx6+8WitEZDPUnQ3qLAR7jhQ==}
'@uiw/react-color-block@2.10.0':
resolution: {integrity: sha512-uBwxbWwIdGqxbVbeXcDR7O4WT4wfLOfeDit+wogYxrwjLdd6txB1YX/x6ELtFiVx+BhSd9YyVvkPaJGabZhk0Q==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@uiw/react-color-chrome@2.9.6':
resolution: {integrity: sha512-ah9H6ZpCHyvHdDzS3qrK4UcvWhj0ZaSfbAE4WLSv/m70E0i/uivjtS242jrPG2Kg02wkgd+jyfmOvpgGkGF3FA==}
'@uiw/react-color-chrome@2.10.0':
resolution: {integrity: sha512-gfKr/pdFESqCx0PywmKHxYPd5NfyVnB0wgFhKsITH1EC7zgAkDxUB3gyA9ihNL53TpqN28O461G59CoaBVBPOg==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@uiw/react-color-circle@2.9.6':
resolution: {integrity: sha512-65CxtZyAxrsSetdi6c+qNaP3o5hg0H4r3gemLT6h7ruI2e3AVWfopHz3gLQrplpjEQobzx/T9om7k+0tDSlqKg==}
'@uiw/react-color-circle@2.10.0':
resolution: {integrity: sha512-1E61c0nOqHijAXN+IsWSOer/asZj41gU+/Wda3XkUHWu219ZIhu/koTpvRLEFJKs5akvXLJlO0Oq4BqmugEiMQ==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@uiw/react-color-colorful@2.9.6':
resolution: {integrity: sha512-h74zo+ve9Rpv7xwb1dRfoa23yN39b6eYScDIm7V2d5FzkXN6hR7jnnJ7ZUD9Joz/rdaCz1eFQD9ig+wp8+wSnQ==}
'@uiw/react-color-colorful@2.10.0':
resolution: {integrity: sha512-eQVojLvOz+LNjBwIValgu2tNJfXEOvnXZDLCr6jlKYjofwgBluQ+Z2lRrfpfmS4auJmsZsQh5dUXrF8kEwnI6g==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@uiw/react-color-compact@2.9.6':
resolution: {integrity: sha512-ikcphwfn+vMeldvIMJStnPsM+LGgv3QcI8ZUHRU0OyDGsWilfpkIW/XUFxepLp9L0ybIm9PK7nZc68wrFvL6/g==}
'@uiw/react-color-compact@2.10.0':
resolution: {integrity: sha512-AI8OPFlj4mBCUjBP4EMU4CL5NxSN29LGqutwX8aWqRtSp1T7cLO+KfRFivyJHCvh4af2Wa+z+5ALRFgvzgeGDg==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@uiw/react-color-editable-input-hsla@2.9.6':
resolution: {integrity: sha512-qZOUf5/2ilD9ToLnEctHsmvEvrjZhDMfzwI4Ii63q68rF/mD1fUbbKy3tCFn+JgGSKvMdEAMhAGjwtCsWkpFtg==}
'@uiw/react-color-editable-input-hsla@2.10.0':
resolution: {integrity: sha512-nSful4fAAWuVXSVAk2Qqi5RQzVFtHLgUS1FsQ1fsUOsLSNS5KPkybCabzhaprEIiSMBDTNTyFe4K1SM+poHeiQ==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@uiw/react-color-editable-input-rgba@2.9.6':
resolution: {integrity: sha512-vzC+uBl6ZaGESVOUbglPqfOACsmwIwMQcmvdELhVw9KiKWp168fE4LqSQf57a2BHaPM9Jl8Lh8GLXn284bWRyw==}
'@uiw/react-color-editable-input-rgba@2.10.0':
resolution: {integrity: sha512-e4pTP2CLnR4Ha6ZfP6q7GLFd3NkgsVRaO5lHEKljBPlGzPxJmMFlJ4VUHRxwHanISwQkJDmPaU8J3IBZyorPJA==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@uiw/react-color-editable-input@2.9.6':
resolution: {integrity: sha512-KrbXonGXxSPxGNTcZY1QZ6QCon9/ekNxdMDlAvBgapUjrrcwqZ+nkgsAsUe+BNa6Ods3KnA2OKcOs99v6nIJ8A==}
'@uiw/react-color-editable-input@2.10.0':
resolution: {integrity: sha512-hMNnuSdr0mtKSyfYwjPODKP0ZJrPKWmUAf0QtlzSwQKkFxdW5hxyVR7aXB2cc+1j44kmTcSyKqQjNS5bx6nY/g==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@uiw/react-color-github@2.9.6':
resolution: {integrity: sha512-Yk7Q4B+WVAA4wz15i4EPo02SJSr8oPFi4mWnB157+v/DnKakcXrIN/Y9z3fi9qlDRo3LsZUfmzJMz4e4W/Fhrg==}
'@uiw/react-color-github@2.10.0':
resolution: {integrity: sha512-dahUCuU63mHWFOwfRfEIOuzmV3h2O8vu8wRr4UXF62et6CCCfr+5ZF6Q/XxX1fjBA1TNbMOGnuI8knSEVAZhNg==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@uiw/react-color-hue@2.9.6':
resolution: {integrity: sha512-B99dW2/AHMD3py83BrXl94bhXeGCZR1FMpU/FNbIIbUrV9QTiIXDs2/SB/tMD9ltcSP59RD5Sc5m2vCb/8anjw==}
'@uiw/react-color-hue@2.10.0':
resolution: {integrity: sha512-C/l3UsHq/8Re7a5e4WI4lgMOeNQNNOMby10O/uOHUYbzWEd1zC1ZM9WhsN44ZXwR3FbaaQ01YenuXl9c19z77g==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@uiw/react-color-material@2.9.6':
resolution: {integrity: sha512-V/Tv6NT1vdC0A6G1qIBdaQWkPjv7Q2x1kZ+M9sa28juP5jJU3/DR9zXrFB5NXTK+j8jG2IPQjRYejTtRNTLEyQ==}
'@uiw/react-color-material@2.10.0':
resolution: {integrity: sha512-H0frUGmsx1qj/vtKRmPf6czu8KzJPnjL6x5fzOU4JhNkg73A5THPDEu+5IurSYII5+ChAYJ7LOwCUQGOd4paMQ==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@uiw/react-color-name@2.9.6':
resolution: {integrity: sha512-rAwE/JF/mhN3bDqu9XrHCovqVt4L68oDtm7IkamfMkqoydZ9HsjFmanhMEyBS9SU+hSK14u7OtebR48xFBQrDw==}
'@uiw/react-color-name@2.10.0':
resolution: {integrity: sha512-KdJhTC51Blb22dXI303e2K1q6MrdOy0WWNAb2U71KMQaBSh3hB8olnsI6eF5MyxrV3dL+3AwN+p7s954QzeA2A==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
'@uiw/react-color-saturation@2.9.6':
resolution: {integrity: sha512-R1tiKbTG2WiJXerkmuaKnBFfzgyZUn08q9OjQSvNH1f3ov2/YeUVlOwQY9MbQE7ytZv+9x+1h0Lpk4QG7AdulQ==}
'@uiw/react-color-saturation@2.10.0':
resolution: {integrity: sha512-GDEdktEWsr0V/5Pi6QtFGrX/TwJ7ZE90nG7xFeuzSqxnVUqlbu0izKwt2Tyg254fFHSlEPv53CZgbyWZ0vpwLQ==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@uiw/react-color-shade-slider@2.9.6':
resolution: {integrity: sha512-8WJQfqA1tenocgkHyO1AERDK7DLkvLhymNPxau42X0qaAGy5MgqvK9+sI6lbp+NpNrmC1g6ltta7PmVUMWBsgg==}
'@uiw/react-color-shade-slider@2.10.0':
resolution: {integrity: sha512-sdQ521RMl3u3U4oHuYPzoxuPayLCMjYHmDh6qsDqBzHLCF652MnLUmoHuSTQJQfPmKXB9AS5kI3YsI4Fqxdw0Q==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@uiw/react-color-sketch@2.9.6':
resolution: {integrity: sha512-yEB14Q2dYYldiHBfnKpotPSa9DaxI3QnTswA6zFBl7b+mfJxmGl77YOIjd6kl90wodl+EdrkjnyE8HquiuJC3w==}
'@uiw/react-color-sketch@2.10.0':
resolution: {integrity: sha512-keM9a95mX7HeHxtZ5rglsMdFfakNwFUSAeMkkR0fsV04lC4CgpO5Ou1UdaJx5pkJ/eKksJj0QgAYC1NY6urZMw==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@uiw/react-color-slider@2.9.6':
resolution: {integrity: sha512-Jc6/YqL5A09t6l0iMTcy/ZRY4Rutq0Zj87JGz0Vg8erEtlowgnLIKt/SUPd/Kpbt4AcNEHKnA9N6SZW1rLMvPw==}
'@uiw/react-color-slider@2.10.0':
resolution: {integrity: sha512-e/+/HHI+GLU1af3NBtSmskF3uyowI4PecDU4ysPtSjjYPFHkgb+aF9DhCysceQrcTaEZh+Pd7KcXCHWrW8nUXg==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@uiw/react-color-swatch@2.9.6':
resolution: {integrity: sha512-ekhCz55GdB+Al/na2tj3GxjaoMd6NRtqosYNFZA3axXxhnREG5TfaaOvR8r96yZswo/sKSSFuX2gpmro7BcBSA==}
'@uiw/react-color-swatch@2.10.0':
resolution: {integrity: sha512-2gyrqmkZrkV/ULXVzhyDv9AvORekeyxeMR+tbOTDQjoV5S/bozceHe0JvsJdisnml9Aq1viVg2x8JUE6LjJo9w==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@uiw/react-color-wheel@2.9.6':
resolution: {integrity: sha512-a2qw544xrdVBfl2TY1VlmhLM4PMAN/pl1FLdAlSSy8u1xl1f851n9C3lze/9myQwnPO500NfQ8gcXq40nPEqPw==}
'@uiw/react-color-wheel@2.10.0':
resolution: {integrity: sha512-1bdFdUC+tsoNaSH3obKx3NjWG+kClccoUGAmZ8oeVw8kALvtlJ8GjLMPfQ8AuyztaLdwlaTx4BBh+34Mprw4nA==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@uiw/react-color@2.9.6':
resolution: {integrity: sha512-+0k2m6H7M/KSQcUGY/GaR9Gi3NgW45refRSJGg2yWHsMUN+Z8NZhxuZv3J8d6mQaiMijnNStLTgIxikKvfA2xw==}
'@uiw/react-color@2.10.0':
resolution: {integrity: sha512-8YWAgEXen5FeFi6Qhd0SBYjEGntUZDt0vRExgkscjbkb3ACsY7nZrYNWDF8ED6jInpVp2+uYsOP4+MDubsUjsA==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@uiw/react-drag-event-interactive@2.9.6':
resolution: {integrity: sha512-jXzt3Xis/BIYap2Hj2++gB3aEUD0mZoVNGfckurrwjAwxasxNiwkmTGxV5er3due0ZgaVKdOAfTRoYKlgZukSg==}
'@uiw/react-drag-event-interactive@2.10.0':
resolution: {integrity: sha512-+TIgGJdvC87L6V5JEP5QJ01aVDHBoSQe1inc/fkkhdOQgnkXwWasqLaSrnpO71W0KYHgkMEfTtj8aZVLsUhtjA==}
peerDependencies:
'@babel/runtime': '>=7.19.0'
react: '>=16.9.0'
@@ -6002,8 +6002,8 @@ packages:
jju@1.4.0:
resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==}
jotai@2.19.0:
resolution: {integrity: sha512-r2wwxEXP1F2JteDLZEOPoIpAHhV89paKsN5GWVYndPNMMP/uVZDcC+fNj0A8NjKgaPWzdyO8Vp8YcYKe0uCEqQ==}
jotai@2.19.1:
resolution: {integrity: sha512-sqm9lVZiqBHZH8aSRk32DSiZDHY3yUIlulXYn9GQj7/LvoUdYXSMti7ZPJGo+6zjzKFt5a25k/I6iBCi43PJcw==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@babel/core': '>=7.0.0'
@@ -6089,8 +6089,8 @@ packages:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
knip@6.3.0:
resolution: {integrity: sha512-g6dVPoTw6iNm3cubC5IWxVkVsd0r5hXhTBTbAGIEQN53GdA2ZM/slMTPJ7n5l8pBebNQPHpxjmKxuR4xVQ2/hQ==}
knip@6.3.1:
resolution: {integrity: sha512-22kLJloVcOVOAudCxlFOC0ICAMme7dKsS7pVTEnrmyKGpswb8ieznvAiSKUeFVDJhb01ect6dkDc1Ha1g1sPpg==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
@@ -6331,8 +6331,8 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
meta-json-schema@1.19.22:
resolution: {integrity: sha512-j/s7HbG90iZdiL7YBIqmbr/DY0BwGDViCsQxLLKjsIqGryt/SjoV1TgZ1dRaRRa77/m3XQkRqJEWgIAs/yk8Ig==}
meta-json-schema@1.19.23:
resolution: {integrity: sha512-WIVB7Vg+eeiih/IOlSlvOmZoH+v5lMPrItg0hMzEQ6/udghhhrspZ4T1cIHDhbtAfQnJwcSt5IGfP8LfzOoKAQ==}
engines: {node: '>=18', pnpm: '>=9'}
micromark-core-commonmark@2.0.1:
@@ -6793,8 +6793,8 @@ packages:
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
postcss@8.5.8:
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
postcss@8.5.9:
resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==}
engines: {node: ^10 || ^12 || >=14}
prettier-plugin-ember-template-tag@2.1.3:
@@ -11574,7 +11574,7 @@ snapshots:
'@alloc/quick-lru': 5.2.0
'@tailwindcss/node': 4.2.2
'@tailwindcss/oxide': 4.2.2
postcss: 8.5.8
postcss: 8.5.9
tailwindcss: 4.2.2
'@tanstack/history@1.161.6': {}
@@ -12019,11 +12019,11 @@ snapshots:
'@types/postcss-modules-local-by-default@4.0.2':
dependencies:
postcss: 8.5.8
postcss: 8.5.9
'@types/postcss-modules-scope@3.0.4':
dependencies:
postcss: 8.5.8
postcss: 8.5.9
'@types/prop-types@15.7.15': {}
@@ -12061,201 +12061,201 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/color-convert@2.9.6(@babel/runtime@7.29.2)':
'@uiw/color-convert@2.10.0(@babel/runtime@7.29.2)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/react-color-alpha@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-color-alpha@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/color-convert': 2.9.6(@babel/runtime@7.29.2)
'@uiw/react-drag-event-interactive': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/color-convert': 2.10.0(@babel/runtime@7.29.2)
'@uiw/react-drag-event-interactive': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/react-color-block@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-color-block@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/color-convert': 2.9.6(@babel/runtime@7.29.2)
'@uiw/react-color-editable-input': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-swatch': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/color-convert': 2.10.0(@babel/runtime@7.29.2)
'@uiw/react-color-editable-input': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-swatch': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/react-color-chrome@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-color-chrome@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/color-convert': 2.9.6(@babel/runtime@7.29.2)
'@uiw/react-color-alpha': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input-hsla': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input-rgba': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-github': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-hue': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-saturation': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/color-convert': 2.10.0(@babel/runtime@7.29.2)
'@uiw/react-color-alpha': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input-hsla': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input-rgba': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-github': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-hue': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-saturation': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/react-color-circle@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-color-circle@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/color-convert': 2.9.6(@babel/runtime@7.29.2)
'@uiw/react-color-swatch': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/color-convert': 2.10.0(@babel/runtime@7.29.2)
'@uiw/react-color-swatch': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/react-color-colorful@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-color-colorful@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/color-convert': 2.9.6(@babel/runtime@7.29.2)
'@uiw/react-color-alpha': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-hue': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-saturation': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/color-convert': 2.10.0(@babel/runtime@7.29.2)
'@uiw/react-color-alpha': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-hue': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-saturation': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/react-color-compact@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-color-compact@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/color-convert': 2.9.6(@babel/runtime@7.29.2)
'@uiw/react-color-editable-input': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input-rgba': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-swatch': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/color-convert': 2.10.0(@babel/runtime@7.29.2)
'@uiw/react-color-editable-input': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input-rgba': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-swatch': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/react-color-editable-input-hsla@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-color-editable-input-hsla@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/color-convert': 2.9.6(@babel/runtime@7.29.2)
'@uiw/react-color-editable-input-rgba': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/color-convert': 2.10.0(@babel/runtime@7.29.2)
'@uiw/react-color-editable-input-rgba': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/react-color-editable-input-rgba@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-color-editable-input-rgba@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/color-convert': 2.9.6(@babel/runtime@7.29.2)
'@uiw/react-color-editable-input': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/color-convert': 2.10.0(@babel/runtime@7.29.2)
'@uiw/react-color-editable-input': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/react-color-editable-input@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-color-editable-input@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/react-color-github@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-color-github@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/color-convert': 2.9.6(@babel/runtime@7.29.2)
'@uiw/react-color-swatch': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/color-convert': 2.10.0(@babel/runtime@7.29.2)
'@uiw/react-color-swatch': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/react-color-hue@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-color-hue@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/color-convert': 2.9.6(@babel/runtime@7.29.2)
'@uiw/react-color-alpha': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/color-convert': 2.10.0(@babel/runtime@7.29.2)
'@uiw/react-color-alpha': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/react-color-material@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-color-material@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/color-convert': 2.9.6(@babel/runtime@7.29.2)
'@uiw/react-color-editable-input': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input-rgba': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/color-convert': 2.10.0(@babel/runtime@7.29.2)
'@uiw/react-color-editable-input': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input-rgba': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/react-color-name@2.9.6(@babel/runtime@7.29.2)':
'@uiw/react-color-name@2.10.0(@babel/runtime@7.29.2)':
dependencies:
'@babel/runtime': 7.29.2
colors-named: 1.0.4
colors-named-hex: 1.0.3
'@uiw/react-color-saturation@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-color-saturation@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/color-convert': 2.9.6(@babel/runtime@7.29.2)
'@uiw/react-drag-event-interactive': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/color-convert': 2.10.0(@babel/runtime@7.29.2)
'@uiw/react-drag-event-interactive': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/react-color-shade-slider@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-color-shade-slider@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/color-convert': 2.9.6(@babel/runtime@7.29.2)
'@uiw/react-color-alpha': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/color-convert': 2.10.0(@babel/runtime@7.29.2)
'@uiw/react-color-alpha': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/react-color-sketch@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-color-sketch@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/color-convert': 2.9.6(@babel/runtime@7.29.2)
'@uiw/react-color-alpha': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input-rgba': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-hue': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-saturation': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-swatch': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/color-convert': 2.10.0(@babel/runtime@7.29.2)
'@uiw/react-color-alpha': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input-rgba': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-hue': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-saturation': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-swatch': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/react-color-slider@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-color-slider@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/color-convert': 2.9.6(@babel/runtime@7.29.2)
'@uiw/react-color-alpha': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/color-convert': 2.10.0(@babel/runtime@7.29.2)
'@uiw/react-color-alpha': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/react-color-swatch@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-color-swatch@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/color-convert': 2.9.6(@babel/runtime@7.29.2)
'@uiw/color-convert': 2.10.0(@babel/runtime@7.29.2)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/react-color-wheel@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-color-wheel@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/color-convert': 2.9.6(@babel/runtime@7.29.2)
'@uiw/react-drag-event-interactive': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/color-convert': 2.10.0(@babel/runtime@7.29.2)
'@uiw/react-drag-event-interactive': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/react-color@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-color@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/color-convert': 2.9.6(@babel/runtime@7.29.2)
'@uiw/react-color-alpha': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-block': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-chrome': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-circle': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-colorful': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-compact': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input-hsla': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input-rgba': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-github': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-hue': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-material': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-name': 2.9.6(@babel/runtime@7.29.2)
'@uiw/react-color-saturation': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-shade-slider': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-sketch': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-slider': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-swatch': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-wheel': 2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/color-convert': 2.10.0(@babel/runtime@7.29.2)
'@uiw/react-color-alpha': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-block': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-chrome': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-circle': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-colorful': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-compact': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input-hsla': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-editable-input-rgba': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-github': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-hue': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-material': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-name': 2.10.0(@babel/runtime@7.29.2)
'@uiw/react-color-saturation': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-shade-slider': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-sketch': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-slider': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-swatch': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-color-wheel': 2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@uiw/react-drag-event-interactive@2.9.6(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-drag-event-interactive@2.10.0(@babel/runtime@7.29.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
react: 19.2.4
@@ -12459,13 +12459,13 @@ snapshots:
async@3.2.6: {}
autoprefixer@10.4.27(postcss@8.5.8):
autoprefixer@10.4.27(postcss@8.5.9):
dependencies:
browserslist: 4.28.1
caniuse-lite: 1.0.30001776
fraction.js: 5.3.4
picocolors: 1.1.1
postcss: 8.5.8
postcss: 8.5.9
postcss-value-parser: 4.2.0
babel-dead-code-elimination@1.0.12:
@@ -13530,9 +13530,9 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
icss-utils@5.1.0(postcss@8.5.8):
icss-utils@5.1.0(postcss@8.5.9):
dependencies:
postcss: 8.5.8
postcss: 8.5.9
ieee754@1.2.1: {}
@@ -13668,7 +13668,7 @@ snapshots:
jju@1.4.0: {}
jotai@2.19.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4):
jotai@2.19.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4):
optionalDependencies:
'@babel/core': 7.29.0
'@babel/template': 7.28.6
@@ -13722,7 +13722,7 @@ snapshots:
kind-of@6.0.3: {}
knip@6.3.0:
knip@6.3.1:
dependencies:
'@nodelib/fs.walk': 1.2.8
fast-glob: 3.3.3
@@ -14014,7 +14014,7 @@ snapshots:
merge2@1.4.1: {}
meta-json-schema@1.19.22: {}
meta-json-schema@1.19.23: {}
micromark-core-commonmark@2.0.1:
dependencies:
@@ -14557,59 +14557,59 @@ snapshots:
dependencies:
htmlparser2: 8.0.2
js-tokens: 9.0.1
postcss: 8.5.8
postcss-safe-parser: 6.0.0(postcss@8.5.8)
postcss: 8.5.9
postcss-safe-parser: 6.0.0(postcss@8.5.9)
postcss-import@16.1.1(postcss@8.5.8):
postcss-import@16.1.1(postcss@8.5.9):
dependencies:
postcss: 8.5.8
postcss: 8.5.9
postcss-value-parser: 4.2.0
read-cache: 1.0.0
resolve: 1.22.8
postcss-js@4.0.1(postcss@8.5.8):
postcss-js@4.0.1(postcss@8.5.9):
dependencies:
camelcase-css: 2.0.1
postcss: 8.5.8
postcss: 8.5.9
postcss-load-config@3.1.4(postcss@8.5.8):
postcss-load-config@3.1.4(postcss@8.5.9):
dependencies:
lilconfig: 2.1.0
yaml: 1.10.2
optionalDependencies:
postcss: 8.5.8
postcss: 8.5.9
postcss-media-query-parser@0.2.3: {}
postcss-modules-extract-imports@3.1.0(postcss@8.5.8):
postcss-modules-extract-imports@3.1.0(postcss@8.5.9):
dependencies:
postcss: 8.5.8
postcss: 8.5.9
postcss-modules-local-by-default@4.0.5(postcss@8.5.8):
postcss-modules-local-by-default@4.0.5(postcss@8.5.9):
dependencies:
icss-utils: 5.1.0(postcss@8.5.8)
postcss: 8.5.8
icss-utils: 5.1.0(postcss@8.5.9)
postcss: 8.5.9
postcss-selector-parser: 6.1.2
postcss-value-parser: 4.2.0
postcss-modules-scope@3.2.0(postcss@8.5.8):
postcss-modules-scope@3.2.0(postcss@8.5.9):
dependencies:
postcss: 8.5.8
postcss: 8.5.9
postcss-selector-parser: 6.1.2
postcss-resolve-nested-selector@0.1.6: {}
postcss-safe-parser@6.0.0(postcss@8.5.8):
postcss-safe-parser@6.0.0(postcss@8.5.9):
dependencies:
postcss: 8.5.8
postcss: 8.5.9
postcss-safe-parser@7.0.1(postcss@8.5.8):
postcss-safe-parser@7.0.1(postcss@8.5.9):
dependencies:
postcss: 8.5.8
postcss: 8.5.9
postcss-scss@4.0.9(postcss@8.5.8):
postcss-scss@4.0.9(postcss@8.5.9):
dependencies:
postcss: 8.5.8
postcss: 8.5.9
postcss-selector-parser@6.1.2:
dependencies:
@@ -14621,13 +14621,13 @@ snapshots:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss-sorting@10.0.0(postcss@8.5.8):
postcss-sorting@10.0.0(postcss@8.5.9):
dependencies:
postcss: 8.5.8
postcss: 8.5.9
postcss-value-parser@4.2.0: {}
postcss@8.5.8:
postcss@8.5.9:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
@@ -15360,8 +15360,8 @@ snapshots:
stylelint-order@8.1.1(stylelint@17.6.0(typescript@5.9.3)):
dependencies:
postcss: 8.5.8
postcss-sorting: 10.0.0(postcss@8.5.8)
postcss: 8.5.9
postcss-sorting: 10.0.0(postcss@8.5.9)
stylelint: 17.6.0(typescript@5.9.3)
stylelint-scss@7.0.0(stylelint@17.6.0(typescript@5.9.3)):
@@ -15405,8 +15405,8 @@ snapshots:
micromatch: 4.0.8
normalize-path: 3.0.0
picocolors: 1.1.1
postcss: 8.5.8
postcss-safe-parser: 7.0.1(postcss@8.5.8)
postcss: 8.5.9
postcss-safe-parser: 7.0.1(postcss@8.5.9)
postcss-selector-parser: 7.1.1
postcss-value-parser: 4.2.0
string-width: 8.2.0
@@ -15587,14 +15587,14 @@ snapshots:
'@types/postcss-modules-local-by-default': 4.0.2
'@types/postcss-modules-scope': 3.0.4
dotenv: 16.4.5
icss-utils: 5.1.0(postcss@8.5.8)
icss-utils: 5.1.0(postcss@8.5.9)
less: 4.2.0
lodash.camelcase: 4.3.0
postcss: 8.5.8
postcss-load-config: 3.1.4(postcss@8.5.8)
postcss-modules-extract-imports: 3.1.0(postcss@8.5.8)
postcss-modules-local-by-default: 4.0.5(postcss@8.5.8)
postcss-modules-scope: 3.2.0(postcss@8.5.8)
postcss: 8.5.9
postcss-load-config: 3.1.4(postcss@8.5.9)
postcss-modules-extract-imports: 3.1.0(postcss@8.5.9)
postcss-modules-local-by-default: 4.0.5(postcss@8.5.9)
postcss-modules-scope: 3.2.0(postcss@8.5.9)
reserved-words: 0.1.2
sass: 1.83.0
source-map-js: 1.2.1
@@ -15856,10 +15856,10 @@ snapshots:
pathe: 0.2.0
vite: 7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2)
vite-plugin-sass-dts@1.3.37(postcss@8.5.8)(prettier@3.8.1)(sass-embedded@1.99.0)(vite@7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2)):
vite-plugin-sass-dts@1.3.37(postcss@8.5.9)(prettier@3.8.1)(sass-embedded@1.99.0)(vite@7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2)):
dependencies:
postcss: 8.5.8
postcss-js: 4.0.1(postcss@8.5.8)
postcss: 8.5.9
postcss-js: 4.0.1(postcss@8.5.9)
prettier: 3.8.1
sass-embedded: 1.99.0
vite: 7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2)
@@ -15890,7 +15890,7 @@ snapshots:
esbuild: 0.27.2
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.8
postcss: 8.5.9
rollup: 4.46.2
tinyglobby: 0.2.15
optionalDependencies:
+2 -6
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=ksmbd
PKG_VERSION:=3.5.4
PKG_RELEASE:=$(AUTORELEASE)
PKG_RELEASE:=2
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/cifsd-team/cifsd/tar.gz/$(PKG_VERSION)?
@@ -59,13 +59,9 @@ PKG_EXTRA_KCONFIG:=CONFIG_SMB_INSECURE_SERVER=y
EXTRA_CFLAGS += -DCONFIG_SMB_INSECURE_SERVER=1
endif
ifdef CONFIG_LINUX_5_10
EXTRA_CFLAGS += -DKSMBD_VFS_RENAME_HAS_NODATA_IDMAP=1
endif
define Build/Compile
$(KERNEL_MAKE) M="$(PKG_BUILD_DIR)" \
EXTRA_CFLAGS="$(EXTRA_CFLAGS)" \
KCFLAGS+=" $(EXTRA_CFLAGS)" \
$(PKG_EXTRA_KCONFIG) \
CONFIG_SMB_SERVER=m \
modules
@@ -1,11 +1,13 @@
--- a/glob.h
+++ b/glob.h
@@ -9,6 +9,10 @@
@@ -9,6 +9,12 @@
#include <linux/ctype.h>
+#include <linux/lockdep.h>
+
+#ifndef lockdep_assert_not_held
+#define lockdep_assert_not_held(l) do { } while (0)
+#define lockdep_assert_not_held(l) do { (void)(l); } while (0)
+#endif
+
#include "unicode.h"
@@ -18,15 +20,24 @@
err = -ENOTEMPTY;
if (dst_dent != trap_dent && !d_really_is_positive(dst_dent)) {
-#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 12, 0)
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 12, 0) || defined(KSMBD_VFS_RENAME_HAS_NODATA_IDMAP)
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0)
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 3, 0)
struct renamedata rd = {
.old_mnt_idmap = src_idmap,
@@ -1715,6 +1715,15 @@
@@ -1715,7 +1715,7 @@
.new_dir = d_inode(dst_dent_parent),
.new_dentry = dst_dent,
};
+#elif defined(KSMBD_VFS_RENAME_HAS_NODATA_IDMAP)
-#else
+#elif LINUX_VERSION_CODE >= KERNEL_VERSION(5, 12, 0)
struct renamedata rd = {
.old_mnt_userns = src_user_ns,
.old_dir = d_inode(src_dent_parent),
@@ -1724,6 +1724,15 @@
.new_dir = d_inode(dst_dent_parent),
.new_dentry = dst_dent,
};
+#else
+ struct renamedata rd = {
+ .old_dir = d_inode(src_dent_parent),
+ .old_dentry = src_dent,
@@ -35,6 +46,6 @@
+ .delegated_inode = NULL,
+ .flags = 0,
+ };
#endif
err = vfs_rename(&rd);
#else
struct renamedata rd = {
.old_mnt_userns = src_user_ns,
+9 -10
View File
@@ -11,18 +11,16 @@ PKG_RELEASE:=1
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL=$(PROJECT_GIT)/project/iwinfo.git
PKG_SOURCE_DATE:=2025-02-06
PKG_SOURCE_VERSION:=9cec6b4dd2df80d4c02bad322a5db14203a92cba
PKG_MIRROR_HASH:=0541587de92b669bd8dbde0cb1ec70435cb8999eece901e2ea986135c875c396
PKG_SOURCE_DATE:=2026-01-14
PKG_SOURCE_VERSION:=f5dd57a84cc31a403a1383dd14944fa2e2b5824a
PKG_MIRROR_HASH:=a249f1c376c5e3be8fdd3f8414510000c7879b4acb6ae13e2f114d54734f6419
PKG_MAINTAINER:=Jo-Philipp Wich <jo@mein.io>
PKG_LICENSE:=GPL-2.0
PKG_BUILD_FLAGS:=no-lto
PKG_FLAGS := nonshared
PKG_CONFIG_DEPENDS := \
CONFIG_PACKAGE_kmod-mt7615d_dbdc \
CONFIG_PACKAGE_kmod-cfg80211
PKG_CONFIG_DEPENDS:=CONFIG_PACKAGE_kmod-mt7615d_dbdc
IWINFO_ABI_VERSION:=20230701
@@ -79,19 +77,20 @@ define Build/Configure
endef
IWINFO_BACKENDS := \
$(if $(CONFIG_PACKAGE_kmod-mt7615d_dbdc),ra) \
$(if $(CONFIG_PACKAGE_kmod-cfg80211),nl80211)
nl80211 \
$(if $(CONFIG_PACKAGE_kmod-mt7615d_dbdc),ra)
TARGET_CFLAGS += \
-I$(PKG_BUILD_DIR)/include \
-I$(STAGING_DIR)/usr/include/libnl-tiny \
-I$(STAGING_DIR)/usr/include \
-D_GNU_SOURCE
MAKE_FLAGS += \
FPIC="$(FPIC)" \
CFLAGS="$(TARGET_CFLAGS)" \
CFLAGS="$(TARGET_CFLAGS) $(TARGET_CPPFLAGS)" \
LDFLAGS="$(TARGET_LDFLAGS)" \
BACKENDS="$(IWINFO_BACKENDS)" \
BACKENDS="$(strip $(IWINFO_BACKENDS))" \
SOVERSION="$(IWINFO_ABI_VERSION)"
define Build/InstallDev
@@ -0,0 +1,16 @@
--- a/Makefile
+++ b/Makefile
@@ -55,10 +55,10 @@ $(IWINFO_LIB): $(IWINFO_LIB_OBJ)
$(CC) $(IWINFO_LDFLAGS) $(IWINFO_LIB_LDFLAGS) -o $(IWINFO_LIB).$(IWINFO_SOVERSION) $(IWINFO_LIB_OBJ) && \
ln -sf $(IWINFO_LIB).$(IWINFO_SOVERSION) $(IWINFO_LIB)
$(IWINFO_LUA): $(IWINFO_LUA_OBJ)
- $(CC) $(IWINFO_LDFLAGS) $(IWINFO_LUA_LDFLAGS) -o $(IWINFO_LUA) $(IWINFO_LUA_OBJ)
+ $(CC) $(IWINFO_LDFLAGS) -o $(IWINFO_LUA) $(IWINFO_LUA_OBJ) $(IWINFO_LUA_LDFLAGS)
$(IWINFO_CLI): $(IWINFO_CLI_OBJ)
- $(CC) $(IWINFO_LDFLAGS) $(IWINFO_CLI_LDFLAGS) -o $(IWINFO_CLI) $(IWINFO_CLI_OBJ)
+ $(CC) $(IWINFO_LDFLAGS) -o $(IWINFO_CLI) $(IWINFO_CLI_OBJ) $(IWINFO_CLI_LDFLAGS)
clean:
rm -f *.o $(IWINFO_LIB) $(IWINFO_LUA) $(IWINFO_CLI)
@@ -0,0 +1,54 @@
--- a/iwinfo_cli.c
+++ b/iwinfo_cli.c
@@ -343,16 +343,36 @@
return buf;
}
+static const uint16_t cli_ht_chan_width[] = {
+ 20,
+ 2040,
+};
+
+static const uint16_t cli_vht_chan_width[] = {
+ 40,
+ 80,
+ 160,
+ 8080,
+};
+
+static const uint16_t cli_eht_chan_width[] = {
+ 20,
+ 40,
+ 80,
+ 160,
+ 320,
+};
+
static const char* format_chan_width(bool vht, uint8_t width)
{
- if (!vht && width < ARRAY_SIZE(ht_chan_width))
- switch (ht_chan_width[width]) {
+ if (!vht && width < ARRAY_SIZE(cli_ht_chan_width))
+ switch (cli_ht_chan_width[width]) {
case 20: return "20 MHz";
case 2040: return "40 MHz or higher";
}
- if (vht && width < ARRAY_SIZE(vht_chan_width))
- switch (vht_chan_width[width]) {
+ if (vht && width < ARRAY_SIZE(cli_vht_chan_width))
+ switch (cli_vht_chan_width[width]) {
case 40: return "20 or 40 MHz";
case 80: return "80 MHz";
case 8080: return "80+80 MHz";
@@ -364,8 +384,8 @@
static const char* format_6ghz_chan_width(uint8_t width)
{
- if (width < ARRAY_SIZE(eht_chan_width))
- switch (eht_chan_width[width]) {
+ if (width < ARRAY_SIZE(cli_eht_chan_width))
+ switch (cli_eht_chan_width[width]) {
case 20: return "20 MHz";
case 40: return "40 MHz";
case 80: return "80 MHz";
-36
View File
@@ -1,36 +0,0 @@
From b8f897a9da7a82ad8584a22284ceac61262fcb7e Mon Sep 17 00:00:00 2001
From: Jorropo <jorropo.pgm@gmail.com>
Date: Sun, 22 Feb 2026 01:47:45 +0100
Subject: [PATCH] runtime: fix value of ENOSYS on mips from 38 to 89
Fixes #77731
Change-Id: Iaca444e2d5f9e19fd2de38414b357b41471a668c
---
diff --git a/src/runtime/defs_linux_mips64x.go b/src/runtime/defs_linux_mips64x.go
index 7449d2c..4d0f103 100644
--- a/src/runtime/defs_linux_mips64x.go
+++ b/src/runtime/defs_linux_mips64x.go
@@ -12,7 +12,7 @@
_EINTR = 0x4
_EAGAIN = 0xb
_ENOMEM = 0xc
- _ENOSYS = 0x26
+ _ENOSYS = 0x59
_PROT_NONE = 0x0
_PROT_READ = 0x1
diff --git a/src/runtime/defs_linux_mipsx.go b/src/runtime/defs_linux_mipsx.go
index 5a446e0..b8da4d0 100644
--- a/src/runtime/defs_linux_mipsx.go
+++ b/src/runtime/defs_linux_mipsx.go
@@ -12,7 +12,7 @@
_EINTR = 0x4
_EAGAIN = 0xb
_ENOMEM = 0xc
- _ENOSYS = 0x26
+ _ENOSYS = 0x59
_PROT_NONE = 0x0
_PROT_READ = 0x1
-265
View File
@@ -1,265 +0,0 @@
From 1a44be4cecdc742ac6cce9825f9ffc19857c99f3 Mon Sep 17 00:00:00 2001
From: database64128 <free122448@hotmail.com>
Date: Mon, 9 Mar 2026 16:25:16 +0800
Subject: [PATCH] [release-branch.go1.26] internal/poll: move rsan to heap on
windows
According to https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsarecvfrom,
the memory pointed to by lpFromlen must remain available during the
overlapped I/O, and therefore cannot be allocated on the stack.
CL 685417 moved the rsan field out of the operation struct and placed
it on stack, which violates the above requirement and causes stack
corruption.
Unfortunately, it is no longer possible to cleanly revert CL 685417.
Instead of attempting to revert it, this CL bundles rsan together
with rsa in the same sync.Pool. The new wsaRsa struct is still in the
same size class, so no additional overhead is introduced by this
change.
Fixes #78041.
Change-Id: I5ffbccb332515116ddc03fb7c40ffc9293cad2ab
Reviewed-on: https://go-review.googlesource.com/c/go/+/753040
Reviewed-by: Quim Muntal <quimmuntal@gmail.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
Commit-Queue: Cherry Mui <cherryyz@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-on: https://go-review.googlesource.com/c/go/+/753480
Reviewed-by: Mark Freeman <markfreeman@google.com>
---
src/internal/poll/fd_windows.go | 94 +++++++++++++++++++++------------
1 file changed, 59 insertions(+), 35 deletions(-)
diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
index 2ba967f990982f..26319548e3c310 100644
--- a/src/internal/poll/fd_windows.go
+++ b/src/internal/poll/fd_windows.go
@@ -149,7 +149,7 @@ var wsaMsgPool = sync.Pool{
// newWSAMsg creates a new WSAMsg with the provided parameters.
// Use [freeWSAMsg] to free it.
-func newWSAMsg(p []byte, oob []byte, flags int, unconnected bool) *windows.WSAMsg {
+func newWSAMsg(p []byte, oob []byte, flags int, rsa *wsaRsa) *windows.WSAMsg {
// The returned object can't be allocated in the stack because it is accessed asynchronously
// by Windows in between several system calls. If the stack frame is moved while that happens,
// then Windows may access invalid memory.
@@ -166,34 +166,46 @@ func newWSAMsg(p []byte, oob []byte, flags int, unconnected bool) *windows.WSAMs
}
}
msg.Flags = uint32(flags)
- if unconnected {
- msg.Name = wsaRsaPool.Get().(*syscall.RawSockaddrAny)
- msg.Namelen = int32(unsafe.Sizeof(syscall.RawSockaddrAny{}))
+ if rsa != nil {
+ msg.Name = &rsa.name
+ msg.Namelen = rsa.namelen
}
return msg
}
func freeWSAMsg(msg *windows.WSAMsg) {
// Clear pointers to buffers so they can be released by garbage collector.
+ msg.Name = nil
+ msg.Namelen = 0
msg.Buffers.Len = 0
msg.Buffers.Buf = nil
msg.Control.Len = 0
msg.Control.Buf = nil
- if msg.Name != nil {
- *msg.Name = syscall.RawSockaddrAny{}
- wsaRsaPool.Put(msg.Name)
- msg.Name = nil
- msg.Namelen = 0
- }
wsaMsgPool.Put(msg)
}
+// wsaRsa bundles a [syscall.RawSockaddrAny] with its length for efficient caching.
+//
+// When used by WSARecvFrom, wsaRsa must be on the heap. See
+// https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsarecvfrom.
+type wsaRsa struct {
+ name syscall.RawSockaddrAny
+ namelen int32
+}
+
var wsaRsaPool = sync.Pool{
New: func() any {
- return new(syscall.RawSockaddrAny)
+ return new(wsaRsa)
},
}
+func newWSARsa() *wsaRsa {
+ rsa := wsaRsaPool.Get().(*wsaRsa)
+ rsa.name = syscall.RawSockaddrAny{}
+ rsa.namelen = int32(unsafe.Sizeof(syscall.RawSockaddrAny{}))
+ return rsa
+}
+
var operationPool = sync.Pool{
New: func() any {
return new(operation)
@@ -739,19 +751,18 @@ func (fd *FD) ReadFrom(buf []byte) (int, syscall.Sockaddr, error) {
fd.pin('r', &buf[0])
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
+ rsa := newWSARsa()
defer wsaRsaPool.Put(rsa)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
- rsan := int32(unsafe.Sizeof(*rsa))
var flags uint32
- err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, rsa, &rsan, &o.o, nil)
+ err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, &rsa.name, &rsa.namelen, &o.o, nil)
return qty, err
})
err = fd.eofError(n, err)
if err != nil {
return n, nil, err
}
- sa, _ := rsa.Sockaddr()
+ sa, _ := rsa.name.Sockaddr()
return n, sa, nil
}
@@ -770,19 +781,18 @@ func (fd *FD) ReadFromInet4(buf []byte, sa4 *syscall.SockaddrInet4) (int, error)
fd.pin('r', &buf[0])
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
+ rsa := newWSARsa()
defer wsaRsaPool.Put(rsa)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
- rsan := int32(unsafe.Sizeof(*rsa))
var flags uint32
- err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, rsa, &rsan, &o.o, nil)
+ err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, &rsa.name, &rsa.namelen, &o.o, nil)
return qty, err
})
err = fd.eofError(n, err)
if err != nil {
return n, err
}
- rawToSockaddrInet4(rsa, sa4)
+ rawToSockaddrInet4(&rsa.name, sa4)
return n, err
}
@@ -801,19 +811,18 @@ func (fd *FD) ReadFromInet6(buf []byte, sa6 *syscall.SockaddrInet6) (int, error)
fd.pin('r', &buf[0])
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
+ rsa := newWSARsa()
defer wsaRsaPool.Put(rsa)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
- rsan := int32(unsafe.Sizeof(*rsa))
var flags uint32
- err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, rsa, &rsan, &o.o, nil)
+ err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, &rsa.name, &rsa.namelen, &o.o, nil)
return qty, err
})
err = fd.eofError(n, err)
if err != nil {
return n, err
}
- rawToSockaddrInet6(rsa, sa6)
+ rawToSockaddrInet6(&rsa.name, sa6)
return n, err
}
@@ -1373,7 +1382,9 @@ func (fd *FD) ReadMsg(p []byte, oob []byte, flags int) (int, int, int, syscall.S
p = p[:maxRW]
}
- msg := newWSAMsg(p, oob, flags, true)
+ rsa := newWSARsa()
+ defer wsaRsaPool.Put(rsa)
+ msg := newWSAMsg(p, oob, flags, rsa)
defer freeWSAMsg(msg)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
@@ -1398,7 +1409,9 @@ func (fd *FD) ReadMsgInet4(p []byte, oob []byte, flags int, sa4 *syscall.Sockadd
p = p[:maxRW]
}
- msg := newWSAMsg(p, oob, flags, true)
+ rsa := newWSARsa()
+ defer wsaRsaPool.Put(rsa)
+ msg := newWSAMsg(p, oob, flags, rsa)
defer freeWSAMsg(msg)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
@@ -1422,7 +1435,9 @@ func (fd *FD) ReadMsgInet6(p []byte, oob []byte, flags int, sa6 *syscall.Sockadd
p = p[:maxRW]
}
- msg := newWSAMsg(p, oob, flags, true)
+ rsa := newWSARsa()
+ defer wsaRsaPool.Put(rsa)
+ msg := newWSAMsg(p, oob, flags, rsa)
defer freeWSAMsg(msg)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
@@ -1446,15 +1461,18 @@ func (fd *FD) WriteMsg(p []byte, oob []byte, sa syscall.Sockaddr) (int, int, err
}
defer fd.writeUnlock()
- msg := newWSAMsg(p, oob, 0, sa != nil)
- defer freeWSAMsg(msg)
+ var rsa *wsaRsa
if sa != nil {
+ rsa = newWSARsa()
+ defer wsaRsaPool.Put(rsa)
var err error
- msg.Namelen, err = sockaddrToRaw(msg.Name, sa)
+ rsa.namelen, err = sockaddrToRaw(&rsa.name, sa)
if err != nil {
return 0, 0, err
}
}
+ msg := newWSAMsg(p, oob, 0, rsa)
+ defer freeWSAMsg(msg)
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
return qty, err
@@ -1473,11 +1491,14 @@ func (fd *FD) WriteMsgInet4(p []byte, oob []byte, sa *syscall.SockaddrInet4) (in
}
defer fd.writeUnlock()
- msg := newWSAMsg(p, oob, 0, sa != nil)
- defer freeWSAMsg(msg)
+ var rsa *wsaRsa
if sa != nil {
- msg.Namelen = sockaddrInet4ToRaw(msg.Name, sa)
+ rsa = newWSARsa()
+ defer wsaRsaPool.Put(rsa)
+ rsa.namelen = sockaddrInet4ToRaw(&rsa.name, sa)
}
+ msg := newWSAMsg(p, oob, 0, rsa)
+ defer freeWSAMsg(msg)
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
return qty, err
@@ -1496,11 +1517,14 @@ func (fd *FD) WriteMsgInet6(p []byte, oob []byte, sa *syscall.SockaddrInet6) (in
}
defer fd.writeUnlock()
- msg := newWSAMsg(p, oob, 0, sa != nil)
- defer freeWSAMsg(msg)
+ var rsa *wsaRsa
if sa != nil {
- msg.Namelen = sockaddrInet6ToRaw(msg.Name, sa)
+ rsa = newWSARsa()
+ defer wsaRsaPool.Put(rsa)
+ rsa.namelen = sockaddrInet6ToRaw(&rsa.name, sa)
}
+ msg := newWSAMsg(p, oob, 0, rsa)
+ defer freeWSAMsg(msg)
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
return qty, err
-14
View File
@@ -186,20 +186,6 @@ jobs:
- name: Verify Go env
run: go env
# TODO: remove after issue77731 fixed, see: https://github.com/golang/go/issues/77731
- name: Fix issue77731 for Golang1.26
if: ${{ matrix.jobs.goversion == '' }}
run: |
cd $(go env GOROOT)
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/issue77731.patch
# TODO: remove after issue77975 fixed, see: https://github.com/golang/go/issues/77975
- name: Fix issue77975 for Golang1.26
if: ${{ matrix.jobs.goversion == '' }}
run: |
cd $(go env GOROOT)
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/issue77975.patch
# TODO: remove after issue77930 fixed, see: https://github.com/golang/go/issues/77930
- name: Fix issue77930 for Golang1.26
if: ${{ matrix.jobs.goversion == '' }}
-7
View File
@@ -57,13 +57,6 @@ jobs:
- name: Verify Go env
run: go env
# TODO: remove after issue77975 fixed, see: https://github.com/golang/go/issues/77975
- name: Fix issue77975 for Golang1.26
if: ${{ matrix.go-version == '1.26' }}
run: |
cd $(go env GOROOT)
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/issue77975.patch
- name: Remove inbound test for macOS
if: ${{ runner.os == 'macOS' }}
run: |
+2 -2
View File
@@ -82,7 +82,7 @@ type XHTTPOptions struct {
Headers map[string]string `proxy:"headers,omitempty"`
NoGRPCHeader bool `proxy:"no-grpc-header,omitempty"`
XPaddingBytes string `proxy:"x-padding-bytes,omitempty"`
ScMaxEachPostBytes int `proxy:"sc-max-each-post-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"`
}
@@ -102,7 +102,7 @@ type XHTTPDownloadSettings struct {
Headers *map[string]string `proxy:"headers,omitempty"`
NoGRPCHeader *bool `proxy:"no-grpc-header,omitempty"`
XPaddingBytes *string `proxy:"x-padding-bytes,omitempty"`
ScMaxEachPostBytes *int `proxy:"sc-max-each-post-bytes,omitempty"`
ScMaxEachPostBytes *string `proxy:"sc-max-each-post-bytes,omitempty"`
ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX
// proxy part
Server *string `proxy:"server,omitempty"`
+2 -2
View File
@@ -1218,8 +1218,8 @@ proxies: # socks5
# quic: true # 默认为false
# congestion-controller: bbr
### reuse options
# max-connections: 1 # Maximum connections. Conflict with max-streams.
# min-streams: 0 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams.
# 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.
# max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams.
# dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理
+2 -2
View File
@@ -21,7 +21,7 @@ require (
github.com/metacubex/edwards25519 v1.2.0
github.com/metacubex/fswatch v0.1.1
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
github.com/metacubex/http v0.1.0
github.com/metacubex/http v0.1.1
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604
github.com/metacubex/mhurl v0.1.0
github.com/metacubex/mlkem v0.1.0
@@ -39,7 +39,7 @@ require (
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443
github.com/metacubex/tls v0.1.4
github.com/metacubex/tls v0.1.5
github.com/metacubex/utls v1.8.4
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f
github.com/mroth/weightedrand/v2 v2.1.0
+4 -4
View File
@@ -103,8 +103,8 @@ github.com/metacubex/hkdf v0.1.0 h1:fPA6VzXK8cU1foc/TOmGCDmSa7pZbxlnqhl3RNsthaA=
github.com/metacubex/hkdf v0.1.0/go.mod h1:3seEfds3smgTAXqUGn+tgEJH3uXdsUjOiduG/2EtvZ4=
github.com/metacubex/hpke v0.1.0 h1:gu2jUNhraehWi0P/z5HX2md3d7L1FhPQE6/Q0E9r9xQ=
github.com/metacubex/hpke v0.1.0/go.mod h1:vfDm6gfgrwlXUxKDkWbcE44hXtmc1uxLDm2BcR11b3U=
github.com/metacubex/http v0.1.0 h1:Jcy0I9zKjYijSUaksZU34XEe2xNdoFkgUTB7z7K5q0o=
github.com/metacubex/http v0.1.0/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg=
github.com/metacubex/http v0.1.1 h1:zVea0zOoaZvxe51EvJMRWGNQv6MvWqJhkZSuoAjOjVw=
github.com/metacubex/http v0.1.1/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg=
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 h1:hJwCVlE3ojViC35MGHB+FBr8TuIf3BUFn2EQ1VIamsI=
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604/go.mod h1:lpmN3m269b3V5jFCWtffqBLS4U3QQoIid9ugtO+OhVc=
github.com/metacubex/mhurl v0.1.0 h1:ZdW4Zxe3j3uJ89gNytOazHu6kbHn5owutN/VfXOI8GE=
@@ -145,8 +145,8 @@ github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 h1:DK2l6m2Fc85H2Bhi
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141/go.mod h1:/yI4OiGOSn0SURhZdJF3CbtPg3nwK700bG8TZLMBvAg=
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 h1:H6TnfM12tOoTizYE/qBHH3nEuibIelmHI+BVSxVJr8o=
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/tls v0.1.4 h1:Gm5GrkyMUh52gYOMIAQ1kHIym4v4M3Qb87Wsmd8Kpdc=
github.com/metacubex/tls v0.1.4/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM=
github.com/metacubex/tls v0.1.5 h1:ECcB83dj+zadnhlKcLnUUf1Sq6+vU0f/zoyU0+9oPTc=
github.com/metacubex/tls v0.1.5/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM=
github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg=
github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f h1:FGBPRb1zUabhPhDrlKEjQ9lgIwQ6cHL4x8M9lrERhbk=
+1 -1
View File
@@ -36,7 +36,7 @@ type XHTTPConfig struct {
Mode string
NoSSEHeader bool
ScStreamUpServerSecs string
ScMaxEachPostBytes int
ScMaxEachPostBytes string
}
func (t VlessServer) String() string {
+1 -1
View File
@@ -37,7 +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"`
ScMaxEachPostBytes int `inbound:"sc-max-each-post-bytes,omitempty"`
ScMaxEachPostBytes string `inbound:"sc-max-each-post-bytes,omitempty"`
}
func (o XHTTPConfig) Build() LC.XHTTPConfig {
+4 -1
View File
@@ -154,7 +154,7 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition)
}
}
if config.XHTTPConfig.Path != "" || config.XHTTPConfig.Host != "" || config.XHTTPConfig.Mode != "" {
httpServer.Handler = xhttp.NewServerHandler(xhttp.ServerOption{
httpServer.Handler, err = xhttp.NewServerHandler(xhttp.ServerOption{
Config: xhttp.Config{
Host: config.XHTTPConfig.Host,
Path: config.XHTTPConfig.Path,
@@ -168,6 +168,9 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition)
},
HttpHandler: httpServer.Handler,
})
if err != nil {
return nil, err
}
if !slices.Contains(tlsConfig.NextProtos, "http/1.1") {
tlsConfig.NextProtos = append([]string{"http/1.1"}, tlsConfig.NextProtos...)
}
+14 -38
View File
@@ -168,52 +168,37 @@ func (c *Client) roundTrip(request *http.Request, conn *httpConn) {
}()
}
func (c *Client) Dial(ctx context.Context, host string) (net.Conn, error) {
func (c *Client) newConnectRequest(host, userAgent string) *http.Request {
request := &http.Request{
Method: http.MethodConnect,
URL: &url.URL{
Scheme: "https",
Host: host,
Host: c.server, // Use the proxy server authority so the pool keys reuse against the actual proxy endpoint.
},
Header: make(http.Header),
Host: host,
Host: host, // Send the actual CONNECT target as the Host header (:authority).
}
request.Header.Add("User-Agent", TCPUserAgent)
request.Header.Add("User-Agent", userAgent)
request.Header.Add("Proxy-Authorization", c.auth)
return request
}
func (c *Client) Dial(ctx context.Context, host string) (net.Conn, error) {
request := c.newConnectRequest(host, TCPUserAgent)
conn := &tcpConn{}
c.roundTrip(request, &conn.httpConn)
return conn, nil
}
func (c *Client) ListenPacket(ctx context.Context) (net.PacketConn, error) {
request := &http.Request{
Method: http.MethodConnect,
URL: &url.URL{
Scheme: "https",
Host: UDPMagicAddress,
},
Header: make(http.Header),
Host: UDPMagicAddress,
}
request.Header.Add("User-Agent", UDPUserAgent)
request.Header.Add("Proxy-Authorization", c.auth)
request := c.newConnectRequest(UDPMagicAddress, UDPUserAgent)
conn := &clientPacketConn{}
c.roundTrip(request, &conn.httpConn)
return conn, nil
}
func (c *Client) ListenICMP(ctx context.Context) (*IcmpConn, error) {
request := &http.Request{
Method: http.MethodConnect,
URL: &url.URL{
Scheme: "https",
Host: ICMPMagicAddress,
},
Header: make(http.Header),
Host: ICMPMagicAddress,
}
request.Header.Add("User-Agent", ICMPUserAgent)
request.Header.Add("Proxy-Authorization", c.auth)
request := c.newConnectRequest(ICMPMagicAddress, ICMPUserAgent)
conn := &IcmpConn{}
c.roundTrip(request, &conn.httpConn)
return conn, nil
@@ -234,17 +219,7 @@ func (c *Client) ResetConnections() {
func (c *Client) HealthCheck(ctx context.Context) error {
defer c.resetHealthCheckTimer()
request := &http.Request{
Method: http.MethodConnect,
URL: &url.URL{
Scheme: "https",
Host: HealthCheckMagicAddress,
},
Header: make(http.Header),
Host: HealthCheckMagicAddress,
}
request.Header.Add("User-Agent", HealthCheckUserAgent)
request.Header.Add("Proxy-Authorization", c.auth)
request := c.newConnectRequest(HealthCheckMagicAddress, HealthCheckUserAgent)
response, err := c.roundTripper.RoundTrip(request.WithContext(ctx))
if err != nil {
return err
@@ -271,7 +246,8 @@ func NewPoolClient(ctx context.Context, options ClientOptions) (*PoolClient, err
minStreams := options.MinStreams
maxStreams := options.MaxStreams
if maxConnections == 0 && minStreams == 0 && maxStreams == 0 {
maxConnections = 1
maxConnections = 8
minStreams = 5
}
client, err := NewClient(ctx, options) // reserve one client and verify the configuration
if err != nil {
+22 -14
View File
@@ -24,19 +24,20 @@ type WrapTLSFunc func(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn,
type TransportMaker func() http.RoundTripper
type PacketUpWriter struct {
ctx context.Context
cancel context.CancelFunc
cfg *Config
sessionID string
transport http.RoundTripper
writeMu sync.Mutex
seq uint64
ctx context.Context
cancel context.CancelFunc
cfg *Config
scMaxEachPostBytes Range
sessionID string
transport http.RoundTripper
writeMu sync.Mutex
seq uint64
}
func (c *PacketUpWriter) Write(b []byte) (int, error) {
c.writeMu.Lock()
defer c.writeMu.Unlock()
scMaxEachPostBytes := c.cfg.GetNormalizedScMaxEachPostBytes()
scMaxEachPostBytes := c.scMaxEachPostBytes.Rand()
if len(b) < scMaxEachPostBytes {
return c.write(b)
}
@@ -117,6 +118,7 @@ type Client struct {
cancel context.CancelFunc
mode string
cfg *Config
scMaxEachPostBytes Range
makeTransport TransportMaker
makeDownloadTransport TransportMaker
uploadManager *ReuseManager
@@ -130,11 +132,16 @@ func NewClient(cfg *Config, makeTransport TransportMaker, makeDownloadTransport
default:
return nil, fmt.Errorf("xhttp mode %s is not implemented yet", mode)
}
scMaxEachPostBytes, err := cfg.GetNormalizedScMaxEachPostBytes()
if err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(context.Background())
client := &Client{
mode: mode,
cfg: cfg,
scMaxEachPostBytes: scMaxEachPostBytes,
makeTransport: makeTransport,
makeDownloadTransport: makeDownloadTransport,
ctx: ctx,
@@ -403,12 +410,13 @@ func (c *Client) DialPacketUp() (net.Conn, error) {
writerCtx, writerCancel := context.WithCancel(c.ctx)
writer := &PacketUpWriter{
ctx: writerCtx,
cancel: writerCancel,
cfg: c.cfg,
sessionID: sessionID,
transport: uploadTransport,
seq: 0,
ctx: writerCtx,
cancel: writerCancel,
cfg: c.cfg,
scMaxEachPostBytes: c.scMaxEachPostBytes,
sessionID: sessionID,
transport: uploadTransport,
seq: 0,
}
conn := &Conn{writer: writer}
+57 -85
View File
@@ -20,7 +20,7 @@ type Config struct {
XPaddingBytes string
NoSSEHeader bool // server only
ScStreamUpServerSecs string // server only
ScMaxEachPostBytes int
ScMaxEachPostBytes string
ReuseConfig *ReuseConfig
DownloadConfig *Config
}
@@ -94,142 +94,114 @@ func (c *Config) RequestHeader() http.Header {
}
func (c *Config) RandomPadding() (string, error) {
paddingRange := c.XPaddingBytes
if paddingRange == "" {
paddingRange = "100-1000"
}
minVal, maxVal, err := parseRange(paddingRange)
r, err := ParseRange(c.XPaddingBytes, "100-1000")
if err != nil {
return "", err
return "", fmt.Errorf("invalid x-padding-bytes: %w", err)
}
if minVal < 0 || maxVal < minVal {
return "", fmt.Errorf("invalid x-padding-bytes range: %s", paddingRange)
}
if maxVal == 0 {
return "", nil
}
n := minVal
if maxVal > minVal {
n = minVal + rand.Intn(maxVal-minVal+1)
}
return strings.Repeat("X", n), nil
return strings.Repeat("X", r.Rand()), nil
}
func (c *Config) GetNormalizedScStreamUpServerSecs() (int, error) {
scStreamUpServerSecs := c.ScStreamUpServerSecs
if scStreamUpServerSecs == "" {
scStreamUpServerSecs = "20-80"
}
minVal, maxVal, err := parseRange(scStreamUpServerSecs)
func (c *Config) GetNormalizedScStreamUpServerSecs() (Range, error) {
r, err := ParseRange(c.ScStreamUpServerSecs, "20-80")
if err != nil {
return 0, err
return Range{}, fmt.Errorf("invalid sc-stream-up-server-secs: %w", err)
}
if minVal < 0 || maxVal < minVal {
return 0, fmt.Errorf("invalid sc-stream-up-server-secs range: %s", scStreamUpServerSecs)
}
if maxVal == 0 {
return 0, nil
}
n := minVal
if maxVal > minVal {
n = minVal + rand.Intn(maxVal-minVal+1)
}
return n, nil
return r, nil
}
func (c *Config) GetNormalizedScMaxEachPostBytes() int {
if c.ScMaxEachPostBytes == 0 {
return 1000000
func (c *Config) GetNormalizedScMaxEachPostBytes() (Range, error) {
r, err := ParseRange(c.ScStreamUpServerSecs, "1000000")
if err != nil {
return Range{}, fmt.Errorf("invalid sc-max-each-post-bytes: %w", err)
}
return c.ScMaxEachPostBytes
if r.Max == 0 {
return Range{}, fmt.Errorf("invalid sc-max-each-post-bytes: must be greater than zero")
}
return r, nil
}
func parseRange(s string) (int, int, error) {
type Range struct {
Min int
Max int
}
func (r Range) Rand() int {
if r.Min == r.Max {
return r.Min
}
return r.Min + rand.Intn(r.Max-r.Min+1)
}
func ParseRange(s string, fallback string) (Range, error) {
if strings.TrimSpace(s) == "" {
return parseRange(fallback)
}
return parseRange(s)
}
func parseRange(s string) (Range, error) {
parts := strings.Split(strings.TrimSpace(s), "-")
if len(parts) == 1 {
v, err := strconv.Atoi(parts[0])
if err != nil {
return 0, 0, err
return Range{}, err
}
return v, v, nil
return Range{v, v}, nil
}
if len(parts) != 2 {
return 0, 0, fmt.Errorf("invalid range: %s", s)
return Range{}, fmt.Errorf("invalid range: %s", s)
}
minVal, err := strconv.Atoi(strings.TrimSpace(parts[0]))
if err != nil {
return 0, 0, err
return Range{}, err
}
maxVal, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil {
return 0, 0, err
}
return minVal, maxVal, nil
}
func resolveRangeValue(s string, fallback int) (int, error) {
if strings.TrimSpace(s) == "" {
return fallback, nil
}
minVal, maxVal, err := parseRange(s)
if err != nil {
return 0, err
return Range{}, err
}
if minVal < 0 || maxVal < minVal {
return 0, fmt.Errorf("invalid range: %s", s)
return Range{}, fmt.Errorf("invalid range: %s", s)
}
if minVal == maxVal {
return minVal, nil
}
return minVal + rand.Intn(maxVal-minVal+1), nil
return Range{minVal, maxVal}, nil
}
func (c *ReuseConfig) ResolveManagerConfig() (int, int, error) {
func (c *ReuseConfig) ResolveManagerConfig() (Range, Range, error) {
if c == nil {
return 0, 0, nil
return Range{}, Range{}, nil
}
maxConnections, err := resolveRangeValue(c.MaxConnections, 0)
maxConnections, err := ParseRange(c.MaxConnections, "0")
if err != nil {
return 0, 0, fmt.Errorf("invalid max-connections: %w", err)
return Range{}, Range{}, fmt.Errorf("invalid max-connections: %w", err)
}
maxConcurrency, err := resolveRangeValue(c.MaxConcurrency, 0)
maxConcurrency, err := ParseRange(c.MaxConcurrency, "0")
if err != nil {
return 0, 0, fmt.Errorf("invalid max-concurrency: %w", err)
return Range{}, Range{}, fmt.Errorf("invalid max-concurrency: %w", err)
}
return maxConnections, maxConcurrency, nil
}
func (c *ReuseConfig) ResolveEntryConfig() (int, int, int, error) {
func (c *ReuseConfig) ResolveEntryConfig() (Range, Range, Range, error) {
if c == nil {
return 0, 0, 0, nil
return Range{}, Range{}, Range{}, nil
}
hMaxRequestTimes, err := resolveRangeValue(c.HMaxRequestTimes, 0)
hMaxRequestTimes, err := ParseRange(c.HMaxRequestTimes, "0")
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid h-max-request-times: %w", err)
return Range{}, Range{}, Range{}, fmt.Errorf("invalid h-max-request-times: %w", err)
}
hMaxReusableSecs, err := resolveRangeValue(c.HMaxReusableSecs, 0)
hMaxReusableSecs, err := ParseRange(c.HMaxReusableSecs, "0")
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid h-max-reusable-secs: %w", err)
return Range{}, Range{}, Range{}, fmt.Errorf("invalid h-max-reusable-secs: %w", err)
}
cMaxReuseTimes, err := resolveRangeValue(c.CMaxReuseTimes, 0)
cMaxReuseTimes, err := ParseRange(c.CMaxReuseTimes, "0")
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid c-max-reuse-times: %w", err)
return Range{}, Range{}, Range{}, fmt.Errorf("invalid c-max-reuse-times: %w", err)
}
return hMaxRequestTimes, hMaxReusableSecs, cMaxReuseTimes, nil
+22 -19
View File
@@ -55,12 +55,14 @@ func (rt *ReuseTransport) Close() error {
var _ http.RoundTripper = (*ReuseTransport)(nil)
type ReuseManager struct {
cfg *ReuseConfig
maxConnections int
maxConcurrency int
maker TransportMaker
mu sync.Mutex
entries []*reuseEntry
maxConnections int
maxConcurrency int
hMaxRequestTimes Range
hMaxReusableSecs Range
cMaxReuseTimes Range
maker TransportMaker
mu sync.Mutex
entries []*reuseEntry
}
func NewReuseManager(cfg *ReuseConfig, makeTransport TransportMaker) (*ReuseManager, error) {
@@ -71,16 +73,18 @@ func NewReuseManager(cfg *ReuseConfig, makeTransport TransportMaker) (*ReuseMana
if err != nil {
return nil, err
}
_, _, _, err = cfg.ResolveEntryConfig() // check if config is valid
hMaxRequestTimes, hMaxReusableSecs, cMaxReuseTimes, err := cfg.ResolveEntryConfig()
if err != nil {
return nil, err
}
return &ReuseManager{
cfg: cfg,
maxConnections: connections,
maxConcurrency: concurrency,
maker: makeTransport,
entries: make([]*reuseEntry, 0),
maxConnections: connections.Rand(),
maxConcurrency: concurrency.Rand(),
hMaxRequestTimes: hMaxRequestTimes,
hMaxReusableSecs: hMaxReusableSecs,
cMaxReuseTimes: cMaxReuseTimes,
maker: makeTransport,
entries: make([]*reuseEntry, 0),
}, nil
}
@@ -169,17 +173,16 @@ func (m *ReuseManager) canCreateLocked() bool {
func (m *ReuseManager) newEntryLocked(transport http.RoundTripper, now time.Time) *reuseEntry {
entry := &reuseEntry{transport: transport}
hMaxRequestTimes, hMaxReusableSecs, cMaxReuseTimes, _ := m.cfg.ResolveEntryConfig() // error already checked in [NewReuseManager]
if hMaxRequestTimes > 0 {
entry.leftRequests.Store(int32(hMaxRequestTimes))
if m.hMaxRequestTimes.Max > 0 {
entry.leftRequests.Store(int32(m.hMaxRequestTimes.Rand()))
} else {
entry.leftRequests.Store(1<<30 - 1)
}
if hMaxReusableSecs > 0 {
entry.unreusableAt = now.Add(time.Duration(hMaxReusableSecs) * time.Second)
if m.hMaxReusableSecs.Max > 0 {
entry.unreusableAt = now.Add(time.Duration(m.hMaxReusableSecs.Rand()) * time.Second)
}
if cMaxReuseTimes > 0 {
entry.maxReuseTimes = int32(cMaxReuseTimes)
if m.cMaxReuseTimes.Max > 0 {
entry.maxReuseTimes = int32(m.cMaxReuseTimes.Rand())
}
m.entries = append(m.entries, entry)
+27 -39
View File
@@ -98,21 +98,34 @@ type requestHandler struct {
connHandler func(net.Conn)
httpHandler http.Handler
scMaxEachPostBytes Range
scStreamUpServerSecs Range
mu sync.Mutex
sessions map[string]*httpSession
}
func NewServerHandler(opt ServerOption) http.Handler {
func NewServerHandler(opt ServerOption) (http.Handler, error) {
scMaxEachPostBytes, err := opt.Config.GetNormalizedScMaxEachPostBytes()
if err != nil {
return nil, err
}
scStreamUpServerSecs, err := opt.Config.GetNormalizedScStreamUpServerSecs()
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{
config: opt.Config,
connHandler: opt.ConnHandler,
httpHandler: opt.HttpHandler,
sessions: map[string]*httpSession{},
config: opt.Config,
connHandler: opt.ConnHandler,
httpHandler: opt.HttpHandler,
scMaxEachPostBytes: scMaxEachPostBytes,
scStreamUpServerSecs: scStreamUpServerSecs,
sessions: map[string]*httpSession{},
}, &http.Http2Server{
IdleTimeout: 30 * time.Second,
})
}), nil
}
func (h *requestHandler) getOrCreateSession(sessionID string) *httpSession {
@@ -209,12 +222,7 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
parts := splitNonEmpty(rest)
// stream-one: POST /path
if r.Method == http.MethodPost && len(parts) == 0 {
if !h.allowStreamOne() {
http.NotFound(w, r)
return
}
if r.Method == http.MethodPost && len(parts) == 0 && h.allowStreamOne() {
w.Header().Set("X-Accel-Buffering", "no")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusOK)
@@ -241,12 +249,7 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
// stream-up/packet-up download: GET /path/{session}
if r.Method == http.MethodGet && len(parts) == 1 {
if !h.allowSessionDownload() {
http.NotFound(w, r)
return
}
if r.Method == http.MethodGet && len(parts) == 1 && h.allowSessionDownload() {
sessionID := parts[0]
session := h.getOrCreateSession(sessionID)
session.markConnected()
@@ -288,12 +291,7 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
// stream-up upload: POST /path/{session}
if r.Method == http.MethodPost && len(parts) == 1 {
if !h.allowStreamUpUpload() {
http.NotFound(w, r)
return
}
if r.Method == http.MethodPost && len(parts) == 1 && h.allowStreamUpUpload() {
sessionID := parts[0]
session := h.getSession(sessionID)
if session == nil {
@@ -322,13 +320,9 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusOK)
referrer := r.Header.Get("Referer")
if referrer != "" {
if referrer != "" && h.scStreamUpServerSecs.Max > 0 {
go func() {
for {
scStreamUpServerSecs, _ := h.config.GetNormalizedScStreamUpServerSecs()
if scStreamUpServerSecs == 0 {
break
}
paddingValue, _ := h.config.RandomPadding()
if paddingValue == "" {
break
@@ -337,7 +331,7 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err != nil {
break
}
time.Sleep(time.Duration(scStreamUpServerSecs) * time.Second)
time.Sleep(time.Duration(h.scStreamUpServerSecs.Rand()) * time.Second)
}
}()
}
@@ -352,12 +346,7 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
// packet-up upload: POST /path/{session}/{seq}
if r.Method == http.MethodPost && len(parts) == 2 {
if !h.allowPacketUpUpload() {
http.NotFound(w, r)
return
}
if r.Method == http.MethodPost && len(parts) == 2 && h.allowPacketUpUpload() {
sessionID := parts[0]
seq, err := strconv.ParseUint(parts[1], 10, 64)
if err != nil {
@@ -371,13 +360,12 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
scMaxEachPostBytes := int64(h.config.GetNormalizedScMaxEachPostBytes())
if r.ContentLength > scMaxEachPostBytes {
if r.ContentLength > int64(h.scMaxEachPostBytes.Max) {
http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
return
}
body, err := io.ReadAll(io.LimitReader(r.Body, scMaxEachPostBytes+1))
body, err := io.ReadAll(io.LimitReader(r.Body, int64(h.scMaxEachPostBytes.Max)+1))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
+4 -1
View File
@@ -78,7 +78,7 @@ func TestServerHandlerModeRestrictions(t *testing.T) {
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
handler := NewServerHandler(ServerOption{
handler, err := NewServerHandler(ServerOption{
Config: Config{
Path: "/xhttp",
Mode: testCase.mode,
@@ -87,6 +87,9 @@ func TestServerHandlerModeRestrictions(t *testing.T) {
_ = conn.Close()
},
})
if err != nil {
panic(err)
}
req := httptest.NewRequest(testCase.method, testCase.target, io.NopCloser(http.NoBody))
recorder := httptest.NewRecorder()
@@ -274,14 +274,15 @@ int NaiveConnection::DoConnectServer() {
return ERR_ADDRESS_INVALID;
}
LOG(INFO) << "Connection " << id_ << " to " << origin.ToString();
LOG(INFO) << "Connection " << id_ << " to " << origin.ToString() << " via "
<< proxy_info_.ToDebugString();
// Ignores socket limit set by socket pool for this type of socket.
return InitSocketHandleForHttpRequest(
std::move(endpoint), LOAD_IGNORE_LIMITS, MAXIMUM_PRIORITY, session_,
proxy_info_, {}, PRIVACY_MODE_DISABLED,
network_anonymization_key_, SecureDnsPolicy::kDisable, SocketTag(),
net_log_, server_socket_handle_.get(), io_callback_,
proxy_info_, {}, PRIVACY_MODE_DISABLED, network_anonymization_key_,
SecureDnsPolicy::kDisable, SocketTag(), net_log_,
server_socket_handle_.get(), io_callback_,
ClientSocketPool::ProxyAuthCallback());
}
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
sudo apt-get update
sudo apt-get -y install jq
- uses: actions/checkout@v4
- uses: actions/checkout@v6
# https://github.com/marketplace/actions/git-changesets
- id: changed_files

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