diff --git a/.github/update.log b/.github/update.log index 6ffe65cc83..3145bb0016 100644 --- a/.github/update.log +++ b/.github/update.log @@ -1333,3 +1333,4 @@ Update On Tue Apr 14 21:27:36 CEST 2026 Update On Wed Apr 15 21:30:45 CEST 2026 Update On Thu Apr 16 21:28:40 CEST 2026 Update On Fri Apr 17 21:15:01 CEST 2026 +Update On Sat Apr 18 21:05:03 CEST 2026 diff --git a/clash-meta-android/.github/workflows/update-dependencies.yaml b/clash-meta-android/.github/workflows/update-dependencies.yaml index fb9a3963bc..d634c2349a 100644 --- a/clash-meta-android/.github/workflows/update-dependencies.yaml +++ b/clash-meta-android/.github/workflows/update-dependencies.yaml @@ -57,11 +57,12 @@ jobs: update-go-mod-replace ${{ github.workspace }}/core/src/foss/golang/clash/go.mod $(pwd)/go.mod go mod tidy - - uses: tibdex/github-app-token@v2 + - uses: actions/create-github-app-token@v3 id: generate-token with: - app_id: ${{ secrets.MAINTAINER_APPID }} - private_key: ${{ secrets.MAINTAINER_APP_PRIVATE_KEY }} + client-id: ${{ secrets.MAINTAINER_APPID }} + private-key: ${{ secrets.MAINTAINER_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} - name: Create Pull Request id: cpr diff --git a/clash-meta/.github/workflows/trigger-cmfa-update.yml b/clash-meta/.github/workflows/trigger-cmfa-update.yml index ed12c2a11c..35fd276cf3 100644 --- a/clash-meta/.github/workflows/trigger-cmfa-update.yml +++ b/clash-meta/.github/workflows/trigger-cmfa-update.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/create-github-app-token@v3 id: generate-token with: - app-id: ${{ secrets.MAINTAINER_APPID }} + client-id: ${{ secrets.MAINTAINER_APPID }} private-key: ${{ secrets.MAINTAINER_APP_PRIVATE_KEY }} owner: ${{ github.repository_owner }} diff --git a/clash-meta/adapter/outbound/vless.go b/clash-meta/adapter/outbound/vless.go index dbd822bbf7..5c294043da 100644 --- a/clash-meta/adapter/outbound/vless.go +++ b/clash-meta/adapter/outbound/vless.go @@ -85,6 +85,19 @@ type XHTTPOptions struct { Headers map[string]string `proxy:"headers,omitempty"` NoGRPCHeader bool `proxy:"no-grpc-header,omitempty"` XPaddingBytes string `proxy:"x-padding-bytes,omitempty"` + XPaddingObfsMode bool `proxy:"x-padding-obfs-mode,omitempty"` + XPaddingKey string `proxy:"x-padding-key,omitempty"` + XPaddingHeader string `proxy:"x-padding-header,omitempty"` + XPaddingPlacement string `proxy:"x-padding-placement,omitempty"` + XPaddingMethod string `proxy:"x-padding-method,omitempty"` + UplinkHTTPMethod string `proxy:"uplink-http-method,omitempty"` + SessionPlacement string `proxy:"session-placement,omitempty"` + SessionKey string `proxy:"session-key,omitempty"` + SeqPlacement string `proxy:"seq-placement,omitempty"` + SeqKey string `proxy:"seq-key,omitempty"` + UplinkDataPlacement string `proxy:"uplink-data-placement,omitempty"` + UplinkDataKey string `proxy:"uplink-data-key,omitempty"` + UplinkChunkSize string `proxy:"uplink-chunk-size,omitempty"` ScMaxEachPostBytes string `proxy:"sc-max-each-post-bytes,omitempty"` ScMinPostsIntervalMs string `proxy:"sc-min-posts-interval-ms,omitempty"` ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX @@ -107,6 +120,19 @@ type XHTTPDownloadSettings struct { Headers *map[string]string `proxy:"headers,omitempty"` NoGRPCHeader *bool `proxy:"no-grpc-header,omitempty"` XPaddingBytes *string `proxy:"x-padding-bytes,omitempty"` + XPaddingObfsMode *bool `proxy:"x-padding-obfs-mode,omitempty"` + XPaddingKey *string `proxy:"x-padding-key,omitempty"` + XPaddingHeader *string `proxy:"x-padding-header,omitempty"` + XPaddingPlacement *string `proxy:"x-padding-placement,omitempty"` + XPaddingMethod *string `proxy:"x-padding-method,omitempty"` + UplinkHTTPMethod *string `proxy:"uplink-http-method,omitempty"` + SessionPlacement *string `proxy:"session-placement,omitempty"` + SessionKey *string `proxy:"session-key,omitempty"` + SeqPlacement *string `proxy:"seq-placement,omitempty"` + SeqKey *string `proxy:"seq-key,omitempty"` + UplinkDataPlacement *string `proxy:"uplink-data-placement,omitempty"` + UplinkDataKey *string `proxy:"uplink-data-key,omitempty"` + UplinkChunkSize *string `proxy:"uplink-chunk-size,omitempty"` ScMaxEachPostBytes *string `proxy:"sc-max-each-post-bytes,omitempty"` ScMinPostsIntervalMs *string `proxy:"sc-min-posts-interval-ms,omitempty"` ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX @@ -552,6 +578,19 @@ func NewVless(option VlessOption) (*Vless, error) { Headers: v.option.XHTTPOpts.Headers, NoGRPCHeader: v.option.XHTTPOpts.NoGRPCHeader, XPaddingBytes: v.option.XHTTPOpts.XPaddingBytes, + XPaddingObfsMode: v.option.XHTTPOpts.XPaddingObfsMode, + XPaddingKey: v.option.XHTTPOpts.XPaddingKey, + XPaddingHeader: v.option.XHTTPOpts.XPaddingHeader, + XPaddingPlacement: v.option.XHTTPOpts.XPaddingPlacement, + XPaddingMethod: v.option.XHTTPOpts.XPaddingMethod, + UplinkHTTPMethod: v.option.XHTTPOpts.UplinkHTTPMethod, + SessionPlacement: v.option.XHTTPOpts.SessionPlacement, + SessionKey: v.option.XHTTPOpts.SessionKey, + SeqPlacement: v.option.XHTTPOpts.SeqPlacement, + SeqKey: v.option.XHTTPOpts.SeqKey, + UplinkDataPlacement: v.option.XHTTPOpts.UplinkDataPlacement, + UplinkDataKey: v.option.XHTTPOpts.UplinkDataKey, + UplinkChunkSize: v.option.XHTTPOpts.UplinkChunkSize, ScMaxEachPostBytes: v.option.XHTTPOpts.ScMaxEachPostBytes, ScMinPostsIntervalMs: v.option.XHTTPOpts.ScMinPostsIntervalMs, ReuseConfig: reuseCfg, @@ -667,6 +706,19 @@ func NewVless(option VlessOption) (*Vless, error) { 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), + XPaddingObfsMode: lo.FromPtrOr(ds.XPaddingObfsMode, v.option.XHTTPOpts.XPaddingObfsMode), + XPaddingKey: lo.FromPtrOr(ds.XPaddingKey, v.option.XHTTPOpts.XPaddingKey), + XPaddingHeader: lo.FromPtrOr(ds.XPaddingHeader, v.option.XHTTPOpts.XPaddingHeader), + XPaddingPlacement: lo.FromPtrOr(ds.XPaddingPlacement, v.option.XHTTPOpts.XPaddingPlacement), + XPaddingMethod: lo.FromPtrOr(ds.XPaddingMethod, v.option.XHTTPOpts.XPaddingMethod), + UplinkHTTPMethod: lo.FromPtrOr(ds.UplinkHTTPMethod, v.option.XHTTPOpts.UplinkHTTPMethod), + SessionPlacement: lo.FromPtrOr(ds.SessionPlacement, v.option.XHTTPOpts.SessionPlacement), + SessionKey: lo.FromPtrOr(ds.SessionKey, v.option.XHTTPOpts.SessionKey), + SeqPlacement: lo.FromPtrOr(ds.SeqPlacement, v.option.XHTTPOpts.SeqPlacement), + SeqKey: lo.FromPtrOr(ds.SeqKey, v.option.XHTTPOpts.SeqKey), + UplinkDataPlacement: lo.FromPtrOr(ds.UplinkDataPlacement, v.option.XHTTPOpts.UplinkDataPlacement), + UplinkDataKey: lo.FromPtrOr(ds.UplinkDataKey, v.option.XHTTPOpts.UplinkDataKey), + UplinkChunkSize: lo.FromPtrOr(ds.UplinkChunkSize, v.option.XHTTPOpts.UplinkChunkSize), ScMaxEachPostBytes: lo.FromPtrOr(ds.ScMaxEachPostBytes, v.option.XHTTPOpts.ScMaxEachPostBytes), ScMinPostsIntervalMs: lo.FromPtrOr(ds.ScMinPostsIntervalMs, v.option.XHTTPOpts.ScMinPostsIntervalMs), ReuseConfig: downloadReuseCfg, diff --git a/clash-meta/docs/config.yaml b/clash-meta/docs/config.yaml index 42f8ef4e8f..44711c33ed 100644 --- a/clash-meta/docs/config.yaml +++ b/clash-meta/docs/config.yaml @@ -825,6 +825,19 @@ proxies: # socks5 # X-Forwarded-For: "" # no-grpc-header: false # x-padding-bytes: "100-1000" + # x-padding-obfs-mode: false + # x-padding-key: x_padding + # x-padding-header: Referer + # x-padding-placement: queryInHeader # Available: queryInHeader, cookie, header, query + # x-padding-method: repeat-x # Available: repeat-x, tokenish + # uplink-http-method: POST # Available: POST, PUT, PATCH, DELETE + # session-placement: path # Available: path, query, cookie, header + # session-key: "" + # seq-placement: path # Available: path, query, cookie, header + # seq-key: "" + # uplink-data-placement: body # Available: body, cookie, header + # uplink-data-key: "" + # uplink-chunk-size: 0 # only applicable when uplink-data-placement is not body # sc-max-each-post-bytes: 1000000 # sc-min-posts-interval-ms: 30 # reuse-settings: # aka XMUX @@ -842,6 +855,19 @@ proxies: # socks5 # X-Forwarded-For: "" # no-grpc-header: false # x-padding-bytes: "100-1000" + # x-padding-obfs-mode: false + # x-padding-key: x_padding + # x-padding-header: Referer + # x-padding-placement: queryInHeader # Available: queryInHeader, cookie, header, query + # x-padding-method: repeat-x # Available: repeat-x, tokenish + # uplink-http-method: POST # Available: POST, PUT, PATCH, DELETE + # session-placement: path # Available: path, query, cookie, header + # session-key: "" + # seq-placement: path # Available: path, query, cookie, header + # seq-key: "" + # uplink-data-placement: body # Available: body, cookie, header + # uplink-data-key: "" + # uplink-chunk-size: 0 # only applicable when uplink-data-placement is not body # sc-max-each-post-bytes: 1000000 # sc-min-posts-interval-ms: 30 # reuse-settings: # aka XMUX @@ -1687,6 +1713,20 @@ listeners: # host: "" # mode: auto # Available: "stream-one", "stream-up" or "packet-up" # no-sse-header: false + # x-padding-bytes: "100-1000" + # x-padding-obfs-mode: false + # x-padding-key: x_padding + # x-padding-header: Referer + # x-padding-placement: queryInHeader # Available: queryInHeader, cookie, header, query + # x-padding-method: repeat-x # Available: repeat-x, tokenish + # uplink-http-method: POST # Available: POST, PUT, PATCH, DELETE + # session-placement: path # Available: path, query, cookie, header + # session-key: "" + # seq-placement: path # Available: path, query, cookie, header + # seq-key: "" + # uplink-data-placement: body # Available: body, cookie, header + # uplink-data-key: "" + # uplink-chunk-size: 0 # only applicable when uplink-data-placement is not body # sc-max-buffered-posts: 30 # sc-stream-up-server-secs: "20-80" # sc-max-each-post-bytes: 1000000 @@ -1768,6 +1808,8 @@ listeners: username2: password2 # 一个 base64 字符串用于微调网络行为 # traffic-pattern: "" + # 如果开启,且客户端不发送用户提示,代理服务器将拒绝连接 + # user-hint-is-mandatory: false - name: sudoku-in-1 type: sudoku diff --git a/clash-meta/go.mod b/clash-meta/go.mod index 9f1f593ffb..0e4202bb0e 100644 --- a/clash-meta/go.mod +++ b/clash-meta/go.mod @@ -6,7 +6,7 @@ require ( github.com/bahlo/generic-list-go v0.2.0 github.com/coreos/go-iptables v0.8.0 github.com/dlclark/regexp2 v1.11.5 - github.com/enfein/mieru/v3 v3.30.1 + github.com/enfein/mieru/v3 v3.31.0 github.com/gobwas/ws v1.4.0 github.com/gofrs/uuid/v5 v5.4.0 github.com/golang/snappy v1.0.0 diff --git a/clash-meta/go.sum b/clash-meta/go.sum index aa667bd763..9d301a099a 100644 --- a/clash-meta/go.sum +++ b/clash-meta/go.sum @@ -20,8 +20,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0= github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= -github.com/enfein/mieru/v3 v3.30.1 h1:gHHXQfpQO/5d789o9kokVfej7jl795aJwPihUk3gTDU= -github.com/enfein/mieru/v3 v3.30.1/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM= +github.com/enfein/mieru/v3 v3.31.0 h1:Fl2ocRCRXJzMygzdRjBHgqI996ZuIDHUmyQyovSf9sA= +github.com/enfein/mieru/v3 v3.31.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= diff --git a/clash-meta/listener/config/vless.go b/clash-meta/listener/config/vless.go index 5e67e3f780..a4de7f57df 100644 --- a/clash-meta/listener/config/vless.go +++ b/clash-meta/listener/config/vless.go @@ -34,6 +34,20 @@ type XHTTPConfig struct { Path string Host string Mode string + XPaddingBytes string + XPaddingObfsMode bool + XPaddingKey string + XPaddingHeader string + XPaddingPlacement string + XPaddingMethod string + UplinkHTTPMethod string + SessionPlacement string + SessionKey string + SeqPlacement string + SeqKey string + UplinkDataPlacement string + UplinkDataKey string + UplinkChunkSize string NoSSEHeader bool ScStreamUpServerSecs string ScMaxBufferedPosts string diff --git a/clash-meta/listener/inbound/anytls_test.go b/clash-meta/listener/inbound/anytls_test.go index 444c943a46..5d9ac8ba5b 100644 --- a/clash-meta/listener/inbound/anytls_test.go +++ b/clash-meta/listener/inbound/anytls_test.go @@ -41,6 +41,7 @@ func testInboundAnyTLS(t *testing.T, inboundOptions inbound.AnyTLSOption, outbou outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) outboundOptions.Password = userUUID + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewAnyTLS(outboundOptions) if !assert.NoError(t, err) { diff --git a/clash-meta/listener/inbound/common_test.go b/clash-meta/listener/inbound/common_test.go index f4aa36cd19..74df75a2bd 100644 --- a/clash-meta/listener/inbound/common_test.go +++ b/clash-meta/listener/inbound/common_test.go @@ -58,6 +58,30 @@ func init() { realityPublickey = base64.RawURLEncoding.EncodeToString(privateKey.PublicKey().Bytes()) } +type TestDialer struct { + dialer C.Dialer + ctx context.Context +} + +func (t *TestDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { +start: + conn, err := t.dialer.DialContext(ctx, network, address) + if err != nil && ctx.Err() == nil && t.ctx.Err() == nil { + // We are conducting tests locally, and they shouldn't fail. + // However, a large number of requests in a short period during concurrent testing can exhaust system ports. + // This can lead to various errors such as WSAECONNREFUSED and WSAENOBUFS. + // So we just retry if the context is not canceled. + goto start + } + return conn, err +} + +func (t *TestDialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) { + return t.dialer.ListenPacket(ctx, network, address, rAddrPort) +} + +var _ C.Dialer = (*TestDialer)(nil) + type TestTunnel struct { HandleTCPConnFn func(conn net.Conn, metadata *C.Metadata) HandleUDPPacketFn func(packet C.UDPPacket, metadata *C.Metadata) @@ -65,6 +89,7 @@ type TestTunnel struct { CloseFn func() error DoSequentialTestFn func(t *testing.T, proxy C.ProxyAdapter) DoConcurrentTestFn func(t *testing.T, proxy C.ProxyAdapter) + NewDialerFn func() C.Dialer } func (tt *TestTunnel) HandleTCPConn(conn net.Conn, metadata *C.Metadata) { @@ -96,6 +121,10 @@ func (tt *TestTunnel) DoConcurrentTest(t *testing.T, proxy C.ProxyAdapter) { tt.DoConcurrentTestFn(t, proxy) } +func (tt *TestTunnel) NewDialer() C.Dialer { + return tt.NewDialerFn() +} + type TestTunnelListener struct { ch chan net.Conn ctx context.Context @@ -328,6 +357,7 @@ func NewHttpTestTunnel() *TestTunnel { CloseFn: ln.Close, DoSequentialTestFn: sequentialTestFn, DoConcurrentTestFn: concurrentTestFn, + NewDialerFn: func() C.Dialer { return &TestDialer{dialer: dialer.NewDialer(), ctx: ctx} }, } return tunnel } diff --git a/clash-meta/listener/inbound/hysteria2_test.go b/clash-meta/listener/inbound/hysteria2_test.go index 52fc07b7e1..b0132fa94a 100644 --- a/clash-meta/listener/inbound/hysteria2_test.go +++ b/clash-meta/listener/inbound/hysteria2_test.go @@ -41,6 +41,7 @@ func testInboundHysteria2(t *testing.T, inboundOptions inbound.Hysteria2Option, outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) outboundOptions.Password = userUUID + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewHysteria2(outboundOptions) if !assert.NoError(t, err) { diff --git a/clash-meta/listener/inbound/mieru.go b/clash-meta/listener/inbound/mieru.go index 5dfb690925..71882ecdba 100644 --- a/clash-meta/listener/inbound/mieru.go +++ b/clash-meta/listener/inbound/mieru.go @@ -27,9 +27,10 @@ type Mieru struct { type MieruOption struct { BaseOption - Transport string `inbound:"transport"` - Users map[string]string `inbound:"users"` - TrafficPattern string `inbound:"traffic-pattern,omitempty"` + Transport string `inbound:"transport"` + Users map[string]string `inbound:"users"` + TrafficPattern string `inbound:"traffic-pattern,omitempty"` + UserHintIsMandatory bool `inbound:"user-hint-is-mandatory,omitempty"` } type mieruListenerFactory struct{} @@ -158,11 +159,18 @@ func buildMieruServerConfig(option *MieruOption, ports utils.IntRanges[uint16]) } var trafficPattern *mierupb.TrafficPattern trafficPattern, _ = mierutp.Decode(option.TrafficPattern) + var advancedSettings *mierupb.ServerAdvancedSettings + if option.UserHintIsMandatory { + advancedSettings = &mierupb.ServerAdvancedSettings{ + UserHintIsMandatory: proto.Bool(true), + } + } return &mieruserver.ServerConfig{ Config: &mierupb.ServerConfig{ - PortBindings: portBindings, - Users: users, - TrafficPattern: trafficPattern, + PortBindings: portBindings, + Users: users, + TrafficPattern: trafficPattern, + AdvancedSettings: advancedSettings, }, StreamListenerFactory: mieruListenerFactory{}, PacketListenerFactory: mieruListenerFactory{}, diff --git a/clash-meta/listener/inbound/mieru_test.go b/clash-meta/listener/inbound/mieru_test.go index e3a5d23ce3..9774218620 100644 --- a/clash-meta/listener/inbound/mieru_test.go +++ b/clash-meta/listener/inbound/mieru_test.go @@ -206,8 +206,9 @@ func testInboundMieruTCP(t *testing.T, handshakeMode string) { Listen: "127.0.0.1", Port: strconv.Itoa(port), }, - Transport: "TCP", - Users: map[string]string{"test": "password"}, + Transport: "TCP", + Users: map[string]string{"test": "password"}, + UserHintIsMandatory: true, } in, err := inbound.NewMieru(&inboundOptions) if !assert.NoError(t, err) { @@ -236,6 +237,7 @@ func testInboundMieruTCP(t *testing.T, handshakeMode string) { Password: "password", HandshakeMode: handshakeMode, } + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewMieru(outboundOptions) if !assert.NoError(t, err) { return @@ -260,8 +262,9 @@ func testInboundMieruUDP(t *testing.T, handshakeMode string) { Listen: "127.0.0.1", Port: strconv.Itoa(port), }, - Transport: "UDP", - Users: map[string]string{"test": "password"}, + Transport: "UDP", + Users: map[string]string{"test": "password"}, + UserHintIsMandatory: true, } in, err := inbound.NewMieru(&inboundOptions) if !assert.NoError(t, err) { @@ -290,6 +293,7 @@ func testInboundMieruUDP(t *testing.T, handshakeMode string) { Password: "password", HandshakeMode: handshakeMode, } + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewMieru(outboundOptions) if !assert.NoError(t, err) { return diff --git a/clash-meta/listener/inbound/shadowsocks_test.go b/clash-meta/listener/inbound/shadowsocks_test.go index d29af71225..755db2cf87 100644 --- a/clash-meta/listener/inbound/shadowsocks_test.go +++ b/clash-meta/listener/inbound/shadowsocks_test.go @@ -85,6 +85,7 @@ func testInboundShadowSocks0(t *testing.T, inboundOptions inbound.ShadowSocksOpt outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) outboundOptions.Password = password + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewShadowSocks(outboundOptions) if !assert.NoError(t, err) { diff --git a/clash-meta/listener/inbound/sudoku_test.go b/clash-meta/listener/inbound/sudoku_test.go index 9ba814dafd..41348008f2 100644 --- a/clash-meta/listener/inbound/sudoku_test.go +++ b/clash-meta/listener/inbound/sudoku_test.go @@ -43,6 +43,7 @@ func testInboundSudoku(t *testing.T, inboundOptions inbound.SudokuOption, outbou outboundOptions.Name = "sudoku_outbound" outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewSudoku(outboundOptions) if !assert.NoError(t, err) { diff --git a/clash-meta/listener/inbound/trojan_test.go b/clash-meta/listener/inbound/trojan_test.go index 7b92b08560..14c36e8278 100644 --- a/clash-meta/listener/inbound/trojan_test.go +++ b/clash-meta/listener/inbound/trojan_test.go @@ -43,6 +43,7 @@ func testInboundTrojan(t *testing.T, inboundOptions inbound.TrojanOption, outbou outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) outboundOptions.Password = userUUID + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewTrojan(outboundOptions) if !assert.NoError(t, err) { diff --git a/clash-meta/listener/inbound/trusttunnel_test.go b/clash-meta/listener/inbound/trusttunnel_test.go index 5283d0880a..4c6c3cdd38 100644 --- a/clash-meta/listener/inbound/trusttunnel_test.go +++ b/clash-meta/listener/inbound/trusttunnel_test.go @@ -42,6 +42,7 @@ func testInboundTrustTunnel(t *testing.T, inboundOptions inbound.TrustTunnelOpti outboundOptions.Port = int(addrPort.Port()) outboundOptions.UserName = "test" outboundOptions.Password = userUUID + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewTrustTunnel(outboundOptions) if !assert.NoError(t, err) { diff --git a/clash-meta/listener/inbound/tuic_test.go b/clash-meta/listener/inbound/tuic_test.go index f887609de7..34bf8e4d75 100644 --- a/clash-meta/listener/inbound/tuic_test.go +++ b/clash-meta/listener/inbound/tuic_test.go @@ -69,6 +69,7 @@ func testInboundTuic0(t *testing.T, inboundOptions inbound.TuicOption, outboundO outboundOptions.Name = "tuic_outbound" outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewTuic(outboundOptions) if !assert.NoError(t, err) { diff --git a/clash-meta/listener/inbound/vless.go b/clash-meta/listener/inbound/vless.go index aad9e70ecf..02e3f74780 100644 --- a/clash-meta/listener/inbound/vless.go +++ b/clash-meta/listener/inbound/vless.go @@ -35,6 +35,20 @@ type XHTTPConfig struct { Path string `inbound:"path,omitempty"` Host string `inbound:"host,omitempty"` Mode string `inbound:"mode,omitempty"` + XPaddingBytes string `inbound:"xpadding-bytes,omitempty"` + XPaddingObfsMode bool `inbound:"xpadding-obfs-mode,omitempty"` + XPaddingKey string `inbound:"xpadding-key,omitempty"` + XPaddingHeader string `inbound:"xpadding-header,omitempty"` + XPaddingPlacement string `inbound:"xpadding-placement,omitempty"` + XPaddingMethod string `inbound:"xpadding-method,omitempty"` + UplinkHTTPMethod string `inbound:"uplink-http-method,omitempty"` + SessionPlacement string `inbound:"session-placement,omitempty"` + SessionKey string `inbound:"session-key,omitempty"` + SeqPlacement string `inbound:"seq-placement,omitempty"` + SeqKey string `inbound:"seq-key,omitempty"` + UplinkDataPlacement string `inbound:"uplink-data-placement,omitempty"` + UplinkDataKey string `inbound:"uplink-data-key,omitempty"` + UplinkChunkSize string `inbound:"uplink-chunk-size,omitempty"` NoSSEHeader bool `inbound:"no-sse-header,omitempty"` ScStreamUpServerSecs string `inbound:"sc-stream-up-server-secs,omitempty"` ScMaxBufferedPosts string `inbound:"sc-max-buffered-posts,omitempty"` @@ -47,6 +61,19 @@ func (o XHTTPConfig) Build() LC.XHTTPConfig { Host: o.Host, Mode: o.Mode, NoSSEHeader: o.NoSSEHeader, + XPaddingBytes: o.XPaddingBytes, + XPaddingObfsMode: o.XPaddingObfsMode, + XPaddingKey: o.XPaddingKey, + XPaddingHeader: o.XPaddingHeader, + XPaddingPlacement: o.XPaddingPlacement, + UplinkHTTPMethod: o.UplinkHTTPMethod, + SessionPlacement: o.SessionPlacement, + SessionKey: o.SessionKey, + SeqPlacement: o.SeqPlacement, + SeqKey: o.SeqKey, + UplinkDataPlacement: o.UplinkDataPlacement, + UplinkDataKey: o.UplinkDataKey, + UplinkChunkSize: o.UplinkChunkSize, ScStreamUpServerSecs: o.ScStreamUpServerSecs, ScMaxBufferedPosts: o.ScMaxBufferedPosts, ScMaxEachPostBytes: o.ScMaxEachPostBytes, diff --git a/clash-meta/listener/inbound/vless_test.go b/clash-meta/listener/inbound/vless_test.go index a3e707a5c6..a483c34113 100644 --- a/clash-meta/listener/inbound/vless_test.go +++ b/clash-meta/listener/inbound/vless_test.go @@ -44,6 +44,7 @@ func testInboundVless(t *testing.T, inboundOptions inbound.VlessOption, outbound outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) outboundOptions.UUID = userUUID + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewVless(outboundOptions) if !assert.NoError(t, err) { @@ -488,6 +489,77 @@ func TestInboundVless_XHTTP_Reality(t *testing.T) { } } +func TestInboundVless_XHTTP_Encryption(t *testing.T) { + privateKeyBase64, passwordBase64, _, err := encryption.GenX25519("") + if err != nil { + t.Fatal(err) + return + } + testCases := []struct { + mode string + }{ + {mode: "auto"}, + {mode: "stream-one"}, + {mode: "stream-up"}, + {mode: "packet-up"}, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.mode, func(t *testing.T) { + getConfig := func() (inbound.VlessOption, outbound.VlessOption) { + inboundOptions := inbound.VlessOption{ + Decryption: "mlkem768x25519plus.native.600s." + privateKeyBase64, + XHTTPConfig: inbound.XHTTPConfig{ + Path: "/vless-xhttp", + Host: "example.com", + Mode: testCase.mode, + }, + } + outboundOptions := outbound.VlessOption{ + Encryption: "mlkem768x25519plus.native.0rtt." + passwordBase64, + 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)) + }) + }) + }) + } +} + func TestInboundVless_XHTTP_PacketUp_H1(t *testing.T) { getConfig := func() (inbound.VlessOption, outbound.VlessOption) { inboundOptions := inbound.VlessOption{ @@ -524,6 +596,55 @@ func TestInboundVless_XHTTP_PacketUp_H1(t *testing.T) { }) } +func TestInboundVless_XHTTP_PacketUp_H1_Encryption(t *testing.T) { + privateKeyBase64, passwordBase64, _, err := encryption.GenX25519("") + if err != nil { + t.Fatal(err) + return + } + getConfig := func() (inbound.VlessOption, outbound.VlessOption) { + inboundOptions := inbound.VlessOption{ + Decryption: "mlkem768x25519plus.native.600s." + privateKeyBase64, + XHTTPConfig: inbound.XHTTPConfig{ + Path: "/vless-xhttp", + Host: "example.com", + Mode: "packet-up", + }, + } + outboundOptions := outbound.VlessOption{ + Encryption: "mlkem768x25519plus.native.0rtt." + passwordBase64, + Network: "xhttp", + ALPN: []string{"http/1.1"}, + XHTTPOpts: outbound.XHTTPOptions{ + Path: "/vless-xhttp", + Host: "example.com", + Mode: "packet-up", + }, + } + return inboundOptions, outboundOptions + } + + t.Run("default", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + testInboundVless(t, inboundOptions, outboundOptions) + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) + }) + + t.Run("reuse", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + testInboundVless(t, inboundOptions, outboundOptions) + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) + }) +} + func withXHTTPReuse(out outbound.VlessOption) outbound.VlessOption { out.XHTTPOpts.ReuseSettings = &outbound.XHTTPReuseSettings{ MaxConnections: "0", diff --git a/clash-meta/listener/inbound/vmess_test.go b/clash-meta/listener/inbound/vmess_test.go index de3a62d20e..175d98031b 100644 --- a/clash-meta/listener/inbound/vmess_test.go +++ b/clash-meta/listener/inbound/vmess_test.go @@ -45,6 +45,7 @@ func testInboundVMess(t *testing.T, inboundOptions inbound.VmessOption, outbound outboundOptions.UUID = userUUID outboundOptions.AlterID = 0 outboundOptions.Cipher = "auto" + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewVmess(outboundOptions) if !assert.NoError(t, err) { diff --git a/clash-meta/listener/sing_vless/server.go b/clash-meta/listener/sing_vless/server.go index d684056b8c..4e41f6bee4 100644 --- a/clash-meta/listener/sing_vless/server.go +++ b/clash-meta/listener/sing_vless/server.go @@ -159,6 +159,20 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) Host: config.XHTTPConfig.Host, Path: config.XHTTPConfig.Path, Mode: config.XHTTPConfig.Mode, + XPaddingBytes: config.XHTTPConfig.XPaddingBytes, + XPaddingObfsMode: config.XHTTPConfig.XPaddingObfsMode, + XPaddingKey: config.XHTTPConfig.XPaddingKey, + XPaddingHeader: config.XHTTPConfig.XPaddingHeader, + XPaddingPlacement: config.XHTTPConfig.XPaddingPlacement, + XPaddingMethod: config.XHTTPConfig.XPaddingMethod, + UplinkHTTPMethod: config.XHTTPConfig.UplinkHTTPMethod, + SessionPlacement: config.XHTTPConfig.SessionPlacement, + SessionKey: config.XHTTPConfig.SessionKey, + SeqPlacement: config.XHTTPConfig.SeqPlacement, + SeqKey: config.XHTTPConfig.SeqKey, + UplinkDataPlacement: config.XHTTPConfig.UplinkDataPlacement, + UplinkDataKey: config.XHTTPConfig.UplinkDataKey, + UplinkChunkSize: config.XHTTPConfig.UplinkChunkSize, NoSSEHeader: config.XHTTPConfig.NoSSEHeader, ScStreamUpServerSecs: config.XHTTPConfig.ScStreamUpServerSecs, ScMaxBufferedPosts: config.XHTTPConfig.ScMaxBufferedPosts, diff --git a/clash-meta/transport/xhttp/browser.go b/clash-meta/transport/xhttp/browser.go new file mode 100644 index 0000000000..9a8f7f107f --- /dev/null +++ b/clash-meta/transport/xhttp/browser.go @@ -0,0 +1,249 @@ +package xhttp + +import ( + "math" + "strconv" + "strings" + "time" + + "github.com/metacubex/http" + "github.com/metacubex/randv2" +) + +// The Chrome version generator will suffer from deviation of a normal distribution. + +func ChromeVersion() int { + // Start from Chrome 144, released on 2026.1.13. + var startVersion int = 144 + var timeStart int64 = time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC).Unix() / 86400 + var timeCurrent int64 = time.Now().Unix() / 86400 + var timeDiff int = int((timeCurrent - timeStart - 35)) - int(math.Floor(math.Pow(randv2.Float64(), 2)*105)) + return startVersion + (timeDiff / 35) // It's 31.15 currently. +} + +var safariMinorMap [25]int = [25]int{0, 0, 0, 1, 1, + 1, 2, 2, 2, 2, 3, 3, 3, 4, 4, + 4, 5, 5, 5, 5, 5, 6, 6, 6, 6} + +// The following version generators use deterministic generators, but with the distribution scaled by a curve. + +func CurlVersion() string { + // curl 8.0.0 was released on 20/03/2023. + var timeCurrent int64 = time.Now().Unix() / 86400 + var timeStart int64 = time.Date(2023, 3, 20, 0, 0, 0, 0, time.UTC).Unix() / 86400 + var timeDiff int = int((timeCurrent - timeStart - 60)) - int(math.Floor(math.Pow(randv2.Float64(), 2)*165)) + var minorValue int = int(timeDiff / 57) // The release cadence is actually 56.67 days. + return "8." + strconv.Itoa(minorValue) + ".0" +} +func FirefoxVersion() int { + // Firefox 128 ESR was released on 09/07/2023. + var timeCurrent int64 = time.Now().Unix() / 86400 + var timeStart int64 = time.Date(2024, 7, 29, 0, 0, 0, 0, time.UTC).Unix() / 86400 + var timeDiff = timeCurrent - timeStart - 25 - int64(math.Floor(math.Pow(randv2.Float64(), 2)*50)) + return int(timeDiff/30) + 128 +} +func SafariVersion() string { + var anchoredTime time.Time = time.Now() + var releaseYear int = anchoredTime.Year() + var splitPoint time.Time = time.Date(releaseYear, 9, 23, 0, 0, 0, 0, time.UTC) + var delayedDays = int(math.Floor(math.Pow(randv2.Float64(), 3) * 75)) + splitPoint = splitPoint.AddDate(0, 0, delayedDays) + if anchoredTime.Compare(splitPoint) < 0 { + releaseYear-- + splitPoint = time.Date(releaseYear, 9, 23, 0, 0, 0, 0, time.UTC) + splitPoint = splitPoint.AddDate(0, 0, delayedDays) + } + var minorVersion = safariMinorMap[(anchoredTime.Unix()-splitPoint.Unix())/1296000] + return strconv.Itoa(releaseYear-1999) + "." + strconv.Itoa(minorVersion) +} + +// The full Chromium brand GREASE implementation +var clientHintGreaseNA = []string{" ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_"} +var clientHintVersionNA = []string{"8", "99", "24"} +var clientHintShuffle3 = [][3]int{{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}} +var clientHintShuffle4 = [][4]int{ + {0, 1, 2, 3}, {0, 1, 3, 2}, {0, 2, 1, 3}, {0, 2, 3, 1}, {0, 3, 1, 2}, {0, 3, 2, 1}, + {1, 0, 2, 3}, {1, 0, 3, 2}, {1, 2, 0, 3}, {1, 2, 3, 0}, {1, 3, 0, 2}, {1, 3, 2, 0}, + {2, 0, 1, 3}, {2, 0, 3, 1}, {2, 1, 0, 3}, {2, 1, 3, 0}, {2, 3, 0, 1}, {2, 3, 1, 0}, + {3, 0, 1, 2}, {3, 0, 2, 1}, {3, 1, 0, 2}, {3, 1, 2, 0}, {3, 2, 0, 1}, {3, 2, 1, 0}} + +func getGreasedChInvalidBrand(seed int) string { + return "\"Not" + clientHintGreaseNA[seed%len(clientHintGreaseNA)] + "A" + clientHintGreaseNA[(seed+1)%len(clientHintGreaseNA)] + "Brand\";v=\"" + clientHintVersionNA[seed%len(clientHintVersionNA)] + "\"" +} +func getGreasedChOrder(brandLength int, seed int) []int { + switch brandLength { + case 1: + return []int{0} + case 2: + return []int{seed % brandLength, (seed + 1) % brandLength} + case 3: + return clientHintShuffle3[seed%len(clientHintShuffle3)][:] + default: + return clientHintShuffle4[seed%len(clientHintShuffle4)][:] + } + //return []int{} +} +func getUngreasedChUa(majorVersion int, forkName string) []string { + // Set the capacity to 4, the maximum allowed brand size, so Go will never allocate memory twice + baseChUa := make([]string, 0, 4) + baseChUa = append(baseChUa, getGreasedChInvalidBrand(majorVersion), + "\"Chromium\";v=\""+strconv.Itoa(majorVersion)+"\"") + switch forkName { + case "chrome": + baseChUa = append(baseChUa, "\"Google Chrome\";v=\""+strconv.Itoa(majorVersion)+"\"") + case "edge": + baseChUa = append(baseChUa, "\"Microsoft Edge\";v=\""+strconv.Itoa(majorVersion)+"\"") + } + return baseChUa +} +func getGreasedChUa(majorVersion int, forkName string) string { + ungreasedCh := getUngreasedChUa(majorVersion, forkName) + shuffleMap := getGreasedChOrder(len(ungreasedCh), majorVersion) + shuffledCh := make([]string, len(ungreasedCh)) + for i, e := range shuffleMap { + shuffledCh[e] = ungreasedCh[i] + } + return strings.Join(shuffledCh, ", ") +} + +// The code below provides a coherent default browser user agent string based on a CPU-seeded PRNG. + +var CurlUA = "curl/" + CurlVersion() +var AnchoredFirefoxVersion = strconv.Itoa(FirefoxVersion()) +var FirefoxUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:" + AnchoredFirefoxVersion + ".0) Gecko/20100101 Firefox/" + AnchoredFirefoxVersion + ".0" +var SafariUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/" + SafariVersion() + " Safari/605.1.15" + +// Chromium browsers. + +var AnchoredChromeVersion = ChromeVersion() +var ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0 Safari/537.36" +var ChromeUACH = getGreasedChUa(AnchoredChromeVersion, "chrome") +var MSEdgeUA = ChromeUA + "Edg/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0" +var MSEdgeUACH = getGreasedChUa(AnchoredChromeVersion, "edge") + +func applyMasqueradedHeaders(header http.Header, browser string, variant string) { + // Browser-specific. + switch browser { + case "chrome": + header["Sec-CH-UA"] = []string{ChromeUACH} + header["Sec-CH-UA-Mobile"] = []string{"?0"} + header["Sec-CH-UA-Platform"] = []string{"\"Windows\""} + header["DNT"] = []string{"1"} + header.Set("User-Agent", ChromeUA) + header.Set("Accept-Language", "en-US,en;q=0.9") + case "edge": + header["Sec-CH-UA"] = []string{MSEdgeUACH} + header["Sec-CH-UA-Mobile"] = []string{"?0"} + header["Sec-CH-UA-Platform"] = []string{"\"Windows\""} + header["DNT"] = []string{"1"} + header.Set("User-Agent", MSEdgeUA) + header.Set("Accept-Language", "en-US,en;q=0.9") + case "firefox": + header.Set("User-Agent", FirefoxUA) + header["DNT"] = []string{"1"} + header.Set("Accept-Language", "en-US,en;q=0.5") + case "safari": + header.Set("User-Agent", SafariUA) + header.Set("Accept-Language", "en-US,en;q=0.9") + case "golang": + // Expose the default net/http header. + header.Del("User-Agent") + return + case "curl": + header.Set("User-Agent", CurlUA) + return + } + // Context-specific. + switch variant { + case "nav": + if header.Get("Cache-Control") == "" { + switch browser { + case "chrome", "edge": + header.Set("Cache-Control", "max-age=0") + } + } + header.Set("Upgrade-Insecure-Requests", "1") + if header.Get("Accept") == "" { + switch browser { + case "chrome", "edge": + header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") + case "firefox", "safari": + header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + } + } + header.Set("Sec-Fetch-Site", "none") + header.Set("Sec-Fetch-Mode", "navigate") + switch browser { + case "safari": + default: + header.Set("Sec-Fetch-User", "?1") + } + header.Set("Sec-Fetch-Dest", "document") + header.Set("Priority", "u=0, i") + case "ws": + header.Set("Sec-Fetch-Mode", "websocket") + switch browser { + case "safari": + // Safari is NOT web-compliant here! + header.Set("Sec-Fetch-Dest", "websocket") + default: + header.Set("Sec-Fetch-Dest", "empty") + } + header.Set("Sec-Fetch-Site", "same-origin") + if header.Get("Cache-Control") == "" { + header.Set("Cache-Control", "no-cache") + } + if header.Get("Pragma") == "" { + header.Set("Pragma", "no-cache") + } + if header.Get("Accept") == "" { + header.Set("Accept", "*/*") + } + case "fetch": + header.Set("Sec-Fetch-Mode", "cors") + header.Set("Sec-Fetch-Dest", "empty") + header.Set("Sec-Fetch-Site", "same-origin") + if header.Get("Priority") == "" { + switch browser { + case "chrome", "edge": + header.Set("Priority", "u=1, i") + case "firefox": + header.Set("Priority", "u=4") + case "safari": + header.Set("Priority", "u=3, i") + } + } + if header.Get("Cache-Control") == "" { + header.Set("Cache-Control", "no-cache") + } + if header.Get("Pragma") == "" { + header.Set("Pragma", "no-cache") + } + if header.Get("Accept") == "" { + header.Set("Accept", "*/*") + } + } +} + +func TryDefaultHeadersWith(header http.Header, variant string) { + // The global UA special value handler for transports. Used to be called HandleTransportUASettings. + // Just a FYI to whoever needing to fix this piece of code after some spontaneous event, I tried to make the two methods separate to let the code be cleaner and more organized. + if len(header.Values("User-Agent")) < 1 { + applyMasqueradedHeaders(header, "chrome", variant) + } else { + switch header.Get("User-Agent") { + case "chrome": + applyMasqueradedHeaders(header, "chrome", variant) + case "firefox": + applyMasqueradedHeaders(header, "firefox", variant) + case "safari": + applyMasqueradedHeaders(header, "safari", variant) + case "edge": + applyMasqueradedHeaders(header, "edge", variant) + case "curl": + applyMasqueradedHeaders(header, "curl", variant) + case "golang": + applyMasqueradedHeaders(header, "golang", variant) + } + } +} diff --git a/clash-meta/transport/xhttp/client.go b/clash-meta/transport/xhttp/client.go index 3e7c726df4..fccfd60964 100644 --- a/clash-meta/transport/xhttp/client.go +++ b/clash-meta/transport/xhttp/client.go @@ -21,7 +21,6 @@ import ( "github.com/metacubex/quic-go" "github.com/metacubex/quic-go/http3" "github.com/metacubex/tls" - "golang.org/x/sync/semaphore" ) // ConnIdleTimeout defines the maximum time an idle TCP session can survive in the tunnel, @@ -115,7 +114,7 @@ func (c *PacketUpWriter) write(b []byte) (int, error) { Path: c.cfg.NormalizedPath(), } - req, err := http.NewRequestWithContext(c.ctx, http.MethodPost, u.String(), nil) + req, err := http.NewRequestWithContext(c.ctx, c.cfg.GetNormalizedUplinkHTTPMethod(), u.String(), nil) if err != nil { return 0, err } @@ -177,12 +176,7 @@ func NewTransport(dialRaw DialRawFunc, wrapTLS WrapTLSFunc, dialQUIC DialQUICFun } } if len(alpn) == 1 && alpn[0] == "http/1.1" { // `alpn: [http/1.1]` means using http/1.1 mode - w := semaphore.NewWeighted(20) // limit concurrent dialing to avoid WSAECONNREFUSED on Windows dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) { - if err := w.Acquire(ctx, 1); err != nil { - return nil, err - } - defer w.Release(1) raw, err := dialRaw(ctx) if err != nil { return nil, err @@ -359,7 +353,7 @@ func (c *Client) DialStreamOne() (net.Conn, error) { }, }) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), pr) + req, err := http.NewRequestWithContext(ctx, c.cfg.GetNormalizedUplinkHTTPMethod(), requestURL.String(), pr) if err != nil { _ = pr.Close() _ = pw.Close() @@ -470,7 +464,7 @@ func (c *Client) DialStreamUp() (net.Conn, error) { uploadReq, err := http.NewRequestWithContext( c.ctx, - http.MethodPost, + c.cfg.GetNormalizedUplinkHTTPMethod(), streamURL.String(), pr, ) diff --git a/clash-meta/transport/xhttp/config.go b/clash-meta/transport/xhttp/config.go index 0c6d2a784f..62208f2b43 100644 --- a/clash-meta/transport/xhttp/config.go +++ b/clash-meta/transport/xhttp/config.go @@ -2,6 +2,7 @@ package xhttp import ( "bytes" + "encoding/base64" "fmt" "io" "math/rand" @@ -11,6 +12,16 @@ import ( "github.com/metacubex/http" ) +const ( + PlacementQueryInHeader = "queryInHeader" + PlacementCookie = "cookie" + PlacementHeader = "header" + PlacementQuery = "query" + PlacementPath = "path" + PlacementBody = "body" + PlacementAuto = "auto" +) + type Config struct { Host string Path string @@ -18,6 +29,19 @@ type Config struct { Headers map[string]string NoGRPCHeader bool XPaddingBytes string + XPaddingObfsMode bool + XPaddingKey string + XPaddingHeader string + XPaddingPlacement string + XPaddingMethod string + UplinkHTTPMethod string + SessionPlacement string + SessionKey string + SeqPlacement string + SeqKey string + UplinkDataPlacement string + UplinkDataKey string + UplinkChunkSize string NoSSEHeader bool // server only ScStreamUpServerSecs string // server only ScMaxBufferedPosts string // server only @@ -70,37 +94,92 @@ func (c *Config) NormalizedPath() string { return path } -func (c *Config) RequestHeader() http.Header { +func (c *Config) GetRequestHeader() http.Header { h := http.Header{} for k, v := range c.Headers { h.Set(k, v) } - - if h.Get("User-Agent") == "" { - h.Set("User-Agent", "Mozilla/5.0") - } - if h.Get("Accept") == "" { - h.Set("Accept", "*/*") - } - if h.Get("Accept-Language") == "" { - h.Set("Accept-Language", "en-US,en;q=0.9") - } - if h.Get("Cache-Control") == "" { - h.Set("Cache-Control", "no-cache") - } - if h.Get("Pragma") == "" { - h.Set("Pragma", "no-cache") - } - + TryDefaultHeadersWith(h, "fetch") return h } -func (c *Config) RandomPadding() (string, error) { - r, err := ParseRange(c.XPaddingBytes, "100-1000") - if err != nil { - return "", fmt.Errorf("invalid x-padding-bytes: %w", err) +func (c *Config) GetRequestHeaderWithPayload(payload []byte, uplinkChunkSize Range) http.Header { + header := c.GetRequestHeader() + + key := c.UplinkDataKey + encodedData := base64.RawURLEncoding.EncodeToString(payload) + + for i := 0; len(encodedData) > 0; i++ { + chunkSize := uplinkChunkSize.Rand() + if len(encodedData) < chunkSize { + chunkSize = len(encodedData) + } + chunk := encodedData[:chunkSize] + encodedData = encodedData[chunkSize:] + headerKey := fmt.Sprintf("%s-%d", key, i) + header.Set(headerKey, chunk) } - return strings.Repeat("X", r.Rand()), nil + + return header +} + +func (c *Config) GetRequestCookiesWithPayload(payload []byte, uplinkChunkSize Range) []*http.Cookie { + cookies := []*http.Cookie{} + + key := c.UplinkDataKey + encodedData := base64.RawURLEncoding.EncodeToString(payload) + + for i := 0; len(encodedData) > 0; i++ { + chunkSize := uplinkChunkSize.Rand() + if len(encodedData) < chunkSize { + chunkSize = len(encodedData) + } + chunk := encodedData[:chunkSize] + encodedData = encodedData[chunkSize:] + cookieName := fmt.Sprintf("%s_%d", key, i) + cookies = append(cookies, &http.Cookie{Name: cookieName, Value: chunk}) + } + + return cookies +} + +func (c *Config) WriteResponseHeader(writer http.ResponseWriter, requestMethod string, requestHeader http.Header) { + if origin := requestHeader.Get("Origin"); origin == "" { + writer.Header().Set("Access-Control-Allow-Origin", "*") + } else { + // Chrome says: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. + writer.Header().Set("Access-Control-Allow-Origin", origin) + } + + if c.GetNormalizedSessionPlacement() == PlacementCookie || + c.GetNormalizedSeqPlacement() == PlacementCookie || + c.XPaddingPlacement == PlacementCookie || + c.GetNormalizedUplinkDataPlacement() == PlacementCookie { + writer.Header().Set("Access-Control-Allow-Credentials", "true") + } + + if requestMethod == "OPTIONS" { + requestedMethod := requestHeader.Get("Access-Control-Request-Method") + if requestedMethod != "" { + writer.Header().Set("Access-Control-Allow-Methods", requestedMethod) + } else { + writer.Header().Set("Access-Control-Allow-Methods", "*") + } + + requestedHeaders := requestHeader.Get("Access-Control-Request-Headers") + if requestedHeaders == "" { + writer.Header().Set("Access-Control-Allow-Headers", "*") + } else { + writer.Header().Set("Access-Control-Allow-Headers", requestedHeaders) + } + } +} + +func (c *Config) GetNormalizedUplinkHTTPMethod() string { + if c.UplinkHTTPMethod == "" { + return "POST" + } + return c.UplinkHTTPMethod } func (c *Config) GetNormalizedScStreamUpServerSecs() (Range, error) { @@ -144,6 +223,84 @@ func (c *Config) GetNormalizedScMinPostsIntervalMs() (Range, error) { return r, nil } +func (c *Config) GetNormalizedUplinkChunkSize() (Range, error) { + uplinkChunkSize, err := ParseRange(c.UplinkChunkSize, "") + if err != nil { + return Range{}, fmt.Errorf("invalid uplink-chunk-size: %w", err) + } + if uplinkChunkSize.Max == 0 { + switch c.GetNormalizedUplinkDataPlacement() { + case PlacementCookie: + return Range{ + Min: 2 * 1024, // 2 KiB + Max: 3 * 1024, // 3 KiB + }, nil + case PlacementHeader: + return Range{ + Min: 3 * 1024, // 3 KiB + Max: 4 * 1024, // 4 KiB + }, nil + default: + return c.GetNormalizedScMaxEachPostBytes() + } + } else if uplinkChunkSize.Min < 64 { + uplinkChunkSize.Min = 64 + if uplinkChunkSize.Max < 64 { + uplinkChunkSize.Max = 64 + } + } + return uplinkChunkSize, nil +} + +func (c *Config) GetNormalizedSessionPlacement() string { + if c.SessionPlacement == "" { + return PlacementPath + } + return c.SessionPlacement +} + +func (c *Config) GetNormalizedSeqPlacement() string { + if c.SeqPlacement == "" { + return PlacementPath + } + return c.SeqPlacement +} + +func (c *Config) GetNormalizedUplinkDataPlacement() string { + if c.UplinkDataPlacement == "" { + return PlacementBody + } + return c.UplinkDataPlacement +} + +func (c *Config) GetNormalizedSessionKey() string { + if c.SessionKey != "" { + return c.SessionKey + } + switch c.GetNormalizedSessionPlacement() { + case PlacementHeader: + return "X-Session" + case PlacementCookie, PlacementQuery: + return "x_session" + default: + return "" + } +} + +func (c *Config) GetNormalizedSeqKey() string { + if c.SeqKey != "" { + return c.SeqKey + } + switch c.GetNormalizedSeqPlacement() { + case PlacementHeader: + return "X-Seq" + case PlacementCookie, PlacementQuery: + return "x_seq" + default: + return "" + } +} + type Range struct { Min int Max int @@ -231,32 +388,6 @@ func (c *ReuseConfig) ResolveEntryConfig() (Range, Range, Range, error) { return cMaxReuseTimes, hMaxRequestTimes, hMaxReusableSecs, nil } -func (c *Config) FillStreamRequest(req *http.Request, sessionID string) error { - req.Header = c.RequestHeader() - - paddingValue, err := c.RandomPadding() - if err != nil { - return err - } - - if paddingValue != "" { - rawURL := req.URL.String() - sep := "?" - if strings.Contains(rawURL, "?") { - sep = "&" - } - req.Header.Set("Referer", rawURL+sep+"x_padding="+paddingValue) - } - - c.ApplyMetaToRequest(req, sessionID, "") - - if req.Body != nil && !c.NoGRPCHeader { - req.Header.Set("Content-Type", "application/grpc") - } - - return nil -} - func appendToPath(path, value string) string { if strings.HasSuffix(path, "/") { return path + value @@ -264,53 +395,185 @@ func appendToPath(path, value string) string { return path + "/" + value } -func (c *Config) ApplyMetaToRequest(req *http.Request, sessionID string, seqStr string) { - if sessionID != "" { - req.URL.Path = appendToPath(req.URL.Path, sessionID) +func (c *Config) ApplyMetaToRequest(req *http.Request, sessionId string, seqStr string) { + sessionPlacement := c.GetNormalizedSessionPlacement() + seqPlacement := c.GetNormalizedSeqPlacement() + sessionKey := c.GetNormalizedSessionKey() + seqKey := c.GetNormalizedSeqKey() + + if sessionId != "" { + switch sessionPlacement { + case PlacementPath: + req.URL.Path = appendToPath(req.URL.Path, sessionId) + case PlacementQuery: + q := req.URL.Query() + q.Set(sessionKey, sessionId) + req.URL.RawQuery = q.Encode() + case PlacementHeader: + req.Header.Set(sessionKey, sessionId) + case PlacementCookie: + req.AddCookie(&http.Cookie{Name: sessionKey, Value: sessionId}) + } } + if seqStr != "" { - req.URL.Path = appendToPath(req.URL.Path, seqStr) + switch seqPlacement { + case PlacementPath: + req.URL.Path = appendToPath(req.URL.Path, seqStr) + case PlacementQuery: + q := req.URL.Query() + q.Set(seqKey, seqStr) + req.URL.RawQuery = q.Encode() + case PlacementHeader: + req.Header.Set(seqKey, seqStr) + case PlacementCookie: + req.AddCookie(&http.Cookie{Name: seqKey, Value: seqStr}) + } } } -func (c *Config) FillPacketRequest(req *http.Request, sessionID string, seqStr string, payload []byte) error { - req.Header = c.RequestHeader() - req.Body = io.NopCloser(bytes.NewReader(payload)) - req.ContentLength = int64(len(payload)) +func (c *Config) ExtractMetaFromRequest(req *http.Request, path string) (sessionId string, seqStr string) { + sessionPlacement := c.GetNormalizedSessionPlacement() + seqPlacement := c.GetNormalizedSeqPlacement() + sessionKey := c.GetNormalizedSessionKey() + seqKey := c.GetNormalizedSeqKey() - paddingValue, err := c.RandomPadding() + var subpath []string + pathPart := 0 + if sessionPlacement == PlacementPath || seqPlacement == PlacementPath { + subpath = strings.Split(req.URL.Path[len(path):], "/") + } + + switch sessionPlacement { + case PlacementPath: + if len(subpath) > pathPart { + sessionId = subpath[pathPart] + pathPart += 1 + } + case PlacementQuery: + sessionId = req.URL.Query().Get(sessionKey) + case PlacementHeader: + sessionId = req.Header.Get(sessionKey) + case PlacementCookie: + if cookie, e := req.Cookie(sessionKey); e == nil { + sessionId = cookie.Value + } + } + + switch seqPlacement { + case PlacementPath: + if len(subpath) > pathPart { + seqStr = subpath[pathPart] + pathPart += 1 + } + case PlacementQuery: + seqStr = req.URL.Query().Get(seqKey) + case PlacementHeader: + seqStr = req.Header.Get(seqKey) + case PlacementCookie: + if cookie, e := req.Cookie(seqKey); e == nil { + seqStr = cookie.Value + } + } + + return sessionId, seqStr +} + +func (c *Config) FillStreamRequest(req *http.Request, sessionID string) error { + req.Header = c.GetRequestHeader() + xPaddingBytes, err := c.GetNormalizedXPaddingBytes() if err != nil { return err } - if paddingValue != "" { - rawURL := req.URL.String() - sep := "?" - if strings.Contains(rawURL, "?") { - sep = "&" + length := xPaddingBytes.Rand() + config := XPaddingConfig{Length: length} + + if c.XPaddingObfsMode { + config.Placement = XPaddingPlacement{ + Placement: c.XPaddingPlacement, + Key: c.XPaddingKey, + Header: c.XPaddingHeader, + RawURL: req.URL.String(), + } + config.Method = PaddingMethod(c.XPaddingMethod) + } else { + config.Placement = XPaddingPlacement{ + Placement: PlacementQueryInHeader, + Key: "x_padding", + Header: "Referer", + RawURL: req.URL.String(), } - req.Header.Set("Referer", rawURL+sep+"x_padding="+paddingValue) } - c.ApplyMetaToRequest(req, sessionID, seqStr) + c.ApplyXPaddingToRequest(req, config) + c.ApplyMetaToRequest(req, sessionID, "") + + if req.Body != nil && !c.NoGRPCHeader { // stream-up/one + req.Header.Set("Content-Type", "application/grpc") + } + return nil } func (c *Config) FillDownloadRequest(req *http.Request, sessionID string) error { - req.Header = c.RequestHeader() + return c.FillStreamRequest(req, sessionID) +} - paddingValue, err := c.RandomPadding() +func (c *Config) FillPacketRequest(request *http.Request, sessionId string, seqStr string, data []byte) error { + dataPlacement := c.GetNormalizedUplinkDataPlacement() + + if dataPlacement == PlacementBody || dataPlacement == PlacementAuto { + request.Header = c.GetRequestHeader() + request.Body = io.NopCloser(bytes.NewReader(data)) + request.ContentLength = int64(len(data)) + } else { + request.Body = nil + request.ContentLength = 0 + switch dataPlacement { + case PlacementHeader: + uplinkChunkSize, err := c.GetNormalizedUplinkChunkSize() + if err != nil { + return err + } + request.Header = c.GetRequestHeaderWithPayload(data, uplinkChunkSize) + case PlacementCookie: + request.Header = c.GetRequestHeader() + uplinkChunkSize, err := c.GetNormalizedUplinkChunkSize() + if err != nil { + return err + } + for _, cookie := range c.GetRequestCookiesWithPayload(data, uplinkChunkSize) { + request.AddCookie(cookie) + } + } + } + + xPaddingBytes, err := c.GetNormalizedXPaddingBytes() if err != nil { return err } - if paddingValue != "" { - rawURL := req.URL.String() - sep := "?" - if strings.Contains(rawURL, "?") { - sep = "&" + length := xPaddingBytes.Rand() + config := XPaddingConfig{Length: length} + + if c.XPaddingObfsMode { + config.Placement = XPaddingPlacement{ + Placement: c.XPaddingPlacement, + Key: c.XPaddingKey, + Header: c.XPaddingHeader, + RawURL: request.URL.String(), + } + config.Method = PaddingMethod(c.XPaddingMethod) + } else { + config.Placement = XPaddingPlacement{ + Placement: PlacementQueryInHeader, + Key: "x_padding", + Header: "Referer", + RawURL: request.URL.String(), } - req.Header.Set("Referer", rawURL+sep+"x_padding="+paddingValue) } - c.ApplyMetaToRequest(req, sessionID, "") + c.ApplyXPaddingToRequest(request, config) + c.ApplyMetaToRequest(request, sessionId, seqStr) + return nil } diff --git a/clash-meta/transport/xhttp/server.go b/clash-meta/transport/xhttp/server.go index c8b4fa84c0..4f41019ce1 100644 --- a/clash-meta/transport/xhttp/server.go +++ b/clash-meta/transport/xhttp/server.go @@ -1,6 +1,9 @@ package xhttp import ( + "bytes" + "encoding/base64" + "fmt" "io" "net" "strconv" @@ -98,6 +101,7 @@ type requestHandler struct { connHandler func(net.Conn) httpHandler http.Handler + xPaddingBytes Range scMaxEachPostBytes Range scStreamUpServerSecs Range scMaxBufferedPosts Range @@ -107,6 +111,10 @@ type requestHandler struct { } func NewServerHandler(opt ServerOption) (http.Handler, error) { + xPaddingBytes, err := opt.Config.GetNormalizedXPaddingBytes() + if err != nil { + return nil, err + } scMaxEachPostBytes, err := opt.Config.GetNormalizedScMaxEachPostBytes() if err != nil { return nil, err @@ -125,6 +133,7 @@ func NewServerHandler(opt ServerOption) (http.Handler, error) { config: opt.Config, connHandler: opt.ConnHandler, httpHandler: opt.HttpHandler, + xPaddingBytes: xPaddingBytes, scMaxEachPostBytes: scMaxEachPostBytes, scStreamUpServerSecs: scStreamUpServerSecs, scMaxBufferedPosts: scMaxBufferedPosts, @@ -134,7 +143,7 @@ func NewServerHandler(opt ServerOption) (http.Handler, error) { }), nil } -func (h *requestHandler) getOrCreateSession(sessionID string) *httpSession { +func (h *requestHandler) upsertSession(sessionID string) *httpSession { h.mu.Lock() defer h.mu.Unlock() @@ -161,8 +170,6 @@ func (h *requestHandler) getOrCreateSession(sessionID string) *httpSession { return s } - - func (h *requestHandler) deleteSession(sessionID string) { h.mu.Lock() defer h.mu.Unlock() @@ -239,11 +246,227 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - rest := strings.TrimPrefix(r.URL.Path, path) - parts := splitNonEmpty(rest) + h.config.WriteResponseHeader(w, r.Method, r.Header) + length := h.xPaddingBytes.Rand() + config := XPaddingConfig{Length: length} + + if h.config.XPaddingObfsMode { + config.Placement = XPaddingPlacement{ + Placement: h.config.XPaddingPlacement, + Key: h.config.XPaddingKey, + Header: h.config.XPaddingHeader, + } + config.Method = PaddingMethod(h.config.XPaddingMethod) + } else { + config.Placement = XPaddingPlacement{ + Placement: PlacementHeader, + Header: "X-Padding", + } + } + + h.config.ApplyXPaddingToResponse(w, config) + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + paddingValue, _ := h.config.ExtractXPaddingFromRequest(r, h.config.XPaddingObfsMode) + if !h.config.IsPaddingValid(paddingValue, h.xPaddingBytes.Min, h.xPaddingBytes.Max, PaddingMethod(h.config.XPaddingMethod)) { + http.Error(w, "invalid xpadding", http.StatusBadRequest) + return + } + sessionId, seqStr := h.config.ExtractMetaFromRequest(r, path) + + var currentSession *httpSession + if sessionId != "" { + currentSession = h.upsertSession(sessionId) + } + + // stream-up upload: POST /path/{session} + if r.Method != http.MethodGet && sessionId != "" && seqStr == "" && h.allowStreamUpUpload() { + httpSC := newHTTPServerConn(w, r.Body) + err := currentSession.uploadQueue.Push(Packet{ + Reader: httpSC, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusConflict) + 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 != "" && h.scStreamUpServerSecs.Max > 0 { + go func() { + for { + _, err := httpSC.Write(bytes.Repeat([]byte{'X'}, int(h.xPaddingBytes.Rand()))) + if err != nil { + break + } + time.Sleep(time.Duration(h.scStreamUpServerSecs.Rand()) * time.Second) + } + }() + } + + select { + case <-r.Context().Done(): + case <-httpSC.Wait(): + } + + _ = httpSC.Close() + return + } + + // packet-up upload: POST /path/{session}/{seq} + if r.Method != http.MethodGet && sessionId != "" && seqStr != "" && h.allowPacketUpUpload() { + scMaxEachPostBytes := h.scMaxEachPostBytes.Max + dataPlacement := h.config.GetNormalizedUplinkDataPlacement() + uplinkDataKey := h.config.UplinkDataKey + var headerPayload []byte + var err error + if dataPlacement == PlacementAuto || dataPlacement == PlacementHeader { + var headerPayloadChunks []string + for i := 0; true; i++ { + chunk := r.Header.Get(fmt.Sprintf("%s-%d", uplinkDataKey, i)) + if chunk == "" { + break + } + headerPayloadChunks = append(headerPayloadChunks, chunk) + } + headerPayloadEncoded := strings.Join(headerPayloadChunks, "") + headerPayload, err = base64.RawURLEncoding.DecodeString(headerPayloadEncoded) + if err != nil { + http.Error(w, "invalid base64 in header's payload", http.StatusBadRequest) + return + } + } + + var cookiePayload []byte + if dataPlacement == PlacementAuto || dataPlacement == PlacementCookie { + var cookiePayloadChunks []string + for i := 0; true; i++ { + cookieName := fmt.Sprintf("%s_%d", uplinkDataKey, i) + if c, _ := r.Cookie(cookieName); c != nil { + cookiePayloadChunks = append(cookiePayloadChunks, c.Value) + } else { + break + } + } + cookiePayloadEncoded := strings.Join(cookiePayloadChunks, "") + cookiePayload, err = base64.RawURLEncoding.DecodeString(cookiePayloadEncoded) + if err != nil { + http.Error(w, "invalid base64 in cookies' payload", http.StatusBadRequest) + return + } + } + + var bodyPayload []byte + if dataPlacement == PlacementAuto || dataPlacement == PlacementBody { + if r.ContentLength > int64(scMaxEachPostBytes) { + http.Error(w, "body too large", http.StatusRequestEntityTooLarge) + return + } + bodyPayload, err = io.ReadAll(io.LimitReader(r.Body, int64(scMaxEachPostBytes)+1)) + if err != nil { + http.Error(w, "failed to read body", http.StatusBadRequest) + return + } + } + + var payload []byte + switch dataPlacement { + case PlacementHeader: + payload = headerPayload + case PlacementCookie: + payload = cookiePayload + case PlacementBody: + payload = bodyPayload + case PlacementAuto: + payload = headerPayload + payload = append(payload, cookiePayload...) + payload = append(payload, bodyPayload...) + } + + if len(payload) > h.scMaxEachPostBytes.Max { + http.Error(w, "body too large", http.StatusRequestEntityTooLarge) + return + } + + seq, err := strconv.ParseUint(seqStr, 10, 64) + if err != nil { + http.Error(w, "invalid xhttp seq", http.StatusBadRequest) + return + } + + err = currentSession.uploadQueue.Push(Packet{ + Seq: seq, + Payload: payload, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if len(payload) == 0 { + // Methods without a body are usually cached by default. + w.Header().Set("Cache-Control", "no-store") + } + w.WriteHeader(http.StatusOK) + return + } + + // stream-up/packet-up download: GET /path/{session} + if r.Method == http.MethodGet && sessionId != "" && seqStr == "" && h.allowSessionDownload() { + currentSession.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() + } + + httpSC := newHTTPServerConn(w, r.Body) + conn := &Conn{ + writer: httpSC, + reader: currentSession.uploadQueue, + onClose: func() { + h.deleteSession(sessionId) + }, + } + httputils.SetAddrFromRequest(&conn.NetAddr, r) + + go h.connHandler(N.NewDeadlineConn(conn)) + + select { + case <-r.Context().Done(): + case <-httpSC.Wait(): + } + + _ = conn.Close() + return + } // stream-one: POST /path - if r.Method == http.MethodPost && len(parts) == 0 && h.allowStreamOne() { + if r.Method != http.MethodGet && sessionId == "" && seqStr == "" && h.allowStreamOne() { w.Header().Set("X-Accel-Buffering", "no") w.Header().Set("Cache-Control", "no-store") w.WriteHeader(http.StatusOK) @@ -269,137 +492,6 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - // stream-up/packet-up download: GET /path/{session} - if r.Method == http.MethodGet && len(parts) == 1 && h.allowSessionDownload() { - 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() - } - - httpSC := newHTTPServerConn(w, r.Body) - conn := &Conn{ - writer: httpSC, - reader: session.uploadQueue, - onClose: func() { - h.deleteSession(sessionID) - }, - } - httputils.SetAddrFromRequest(&conn.NetAddr, r) - - go h.connHandler(N.NewDeadlineConn(conn)) - - select { - case <-r.Context().Done(): - case <-httpSC.Wait(): - } - - _ = conn.Close() - return - } - - // stream-up upload: POST /path/{session} - if r.Method == http.MethodPost && len(parts) == 1 && h.allowStreamUpUpload() { - sessionID := parts[0] - session := h.getOrCreateSession(sessionID) - - 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 != "" && h.scStreamUpServerSecs.Max > 0 { - go func() { - for { - paddingValue, _ := h.config.RandomPadding() - if paddingValue == "" { - break - } - _, err = httpSC.Write([]byte(paddingValue)) - if err != nil { - break - } - time.Sleep(time.Duration(h.scStreamUpServerSecs.Rand()) * 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 && h.allowPacketUpUpload() { - sessionID := parts[0] - seq, err := strconv.ParseUint(parts[1], 10, 64) - if err != nil { - http.Error(w, "invalid xhttp seq", http.StatusBadRequest) - return - } - - session := h.getOrCreateSession(sessionID) - - if r.ContentLength > int64(h.scMaxEachPostBytes.Max) { - http.Error(w, "body too large", http.StatusRequestEntityTooLarge) - return - } - - body, err := io.ReadAll(io.LimitReader(r.Body, int64(h.scMaxEachPostBytes.Max)+1)) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - err = session.uploadQueue.Push(Packet{ - Seq: seq, - Payload: body, - }) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - if len(body) == 0 { - w.Header().Set("Cache-Control", "no-store") - } - w.WriteHeader(http.StatusOK) - return - } - http.NotFound(w, r) } diff --git a/clash-meta/transport/xhttp/server_test.go b/clash-meta/transport/xhttp/server_test.go index f7d951d8ff..b194186b80 100644 --- a/clash-meta/transport/xhttp/server_test.go +++ b/clash-meta/transport/xhttp/server_test.go @@ -78,22 +78,24 @@ func TestServerHandlerModeRestrictions(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { + config := Config{ + Path: "/xhttp", + Mode: testCase.mode, + } handler, err := NewServerHandler(ServerOption{ - Config: Config{ - Path: "/xhttp", - Mode: testCase.mode, - }, + Config: config, ConnHandler: func(conn net.Conn) { _ = conn.Close() }, }) - if err != nil { - panic(err) - } + assert.NoError(t, err) req := httptest.NewRequest(testCase.method, testCase.target, io.NopCloser(http.NoBody)) recorder := httptest.NewRecorder() + err = config.FillStreamRequest(req, "") + assert.NoError(t, err) + handler.ServeHTTP(recorder, req) assert.Equal(t, testCase.wantStatus, recorder.Result().StatusCode) diff --git a/clash-meta/transport/xhttp/xpadding.go b/clash-meta/transport/xhttp/xpadding.go new file mode 100644 index 0000000000..3b22a019e0 --- /dev/null +++ b/clash-meta/transport/xhttp/xpadding.go @@ -0,0 +1,333 @@ +package xhttp + +import ( + "crypto/rand" + "fmt" + "math" + "net/url" + "strings" + + "github.com/metacubex/http" + "golang.org/x/net/http2/hpack" +) + +type PaddingMethod string + +const ( + PaddingMethodRepeatX PaddingMethod = "repeat-x" + PaddingMethodTokenish PaddingMethod = "tokenish" +) + +const charsetBase62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + +// Huffman encoding gives ~20% size reduction for base62 sequences +const avgHuffmanBytesPerCharBase62 = 0.8 + +const validationTolerance = 2 + +type XPaddingPlacement struct { + Placement string + Key string + Header string + RawURL string +} + +type XPaddingConfig struct { + Length int + Placement XPaddingPlacement + Method PaddingMethod +} + +func randStringFromCharset(n int, charset string) (string, bool) { + if n <= 0 || len(charset) == 0 { + return "", false + } + + m := len(charset) + limit := byte(256 - (256 % m)) + + result := make([]byte, n) + i := 0 + + buf := make([]byte, 256) + for i < n { + if _, err := rand.Read(buf); err != nil { + return "", false + } + for _, rb := range buf { + if rb >= limit { + continue + } + result[i] = charset[int(rb)%m] + i++ + if i == n { + break + } + } + } + + return string(result), true +} + +func absInt(x int) int { + if x < 0 { + return -x + } + return x +} + +func GenerateTokenishPaddingBase62(targetHuffmanBytes int) string { + n := int(math.Ceil(float64(targetHuffmanBytes) / avgHuffmanBytesPerCharBase62)) + if n < 1 { + n = 1 + } + + randBase62Str, ok := randStringFromCharset(n, charsetBase62) + if !ok { + return "" + } + + const maxIter = 150 + adjustChar := byte('X') + + // Adjust until close enough + for iter := 0; iter < maxIter; iter++ { + currentLength := int(hpack.HuffmanEncodeLength(randBase62Str)) + diff := currentLength - targetHuffmanBytes + + if absInt(diff) <= validationTolerance { + return randBase62Str + } + + if diff < 0 { + // Too small -> append padding char(s) + randBase62Str += string(adjustChar) + + // Avoid a long run of identical chars + if adjustChar == 'X' { + adjustChar = 'Z' + } else { + adjustChar = 'X' + } + } else { + // Too big -> remove from the end + if len(randBase62Str) <= 1 { + return randBase62Str + } + randBase62Str = randBase62Str[:len(randBase62Str)-1] + } + } + + return randBase62Str +} + +func GeneratePadding(method PaddingMethod, length int) string { + if length <= 0 { + return "" + } + + // https://www.rfc-editor.org/rfc/rfc7541.html#appendix-B + // h2's HPACK Header Compression feature employs a huffman encoding using a static table. + // 'X' and 'Z' are assigned an 8 bit code, so HPACK compression won't change actual padding length on the wire. + // https://www.rfc-editor.org/rfc/rfc9204.html#section-4.1.2-2 + // h3's similar QPACK feature uses the same huffman table. + + switch method { + case PaddingMethodRepeatX: + return strings.Repeat("X", length) + case PaddingMethodTokenish: + paddingValue := GenerateTokenishPaddingBase62(length) + if paddingValue == "" { + return strings.Repeat("X", length) + } + return paddingValue + default: + return strings.Repeat("X", length) + } +} + +func ApplyPaddingToCookie(req *http.Request, name, value string) { + if req == nil || name == "" || value == "" { + return + } + req.AddCookie(&http.Cookie{ + Name: name, + Value: value, + Path: "/", + }) +} + +func ApplyPaddingToResponseCookie(writer http.ResponseWriter, name, value string) { + if name == "" || value == "" { + return + } + http.SetCookie(writer, &http.Cookie{ + Name: name, + Value: value, + Path: "/", + }) +} + +func ApplyPaddingToQuery(u *url.URL, key, value string) { + if u == nil || key == "" || value == "" { + return + } + q := u.Query() + q.Set(key, value) + u.RawQuery = q.Encode() +} + +func (c *Config) GetNormalizedXPaddingBytes() (Range, error) { + r, err := ParseRange(c.XPaddingBytes, "100-1000") + if err != nil { + return Range{}, fmt.Errorf("invalid x-padding-bytes: %w", err) + } + return r, nil +} + +func (c *Config) ApplyXPaddingToHeader(h http.Header, config XPaddingConfig) { + if h == nil { + return + } + + paddingValue := GeneratePadding(config.Method, config.Length) + + switch p := config.Placement; p.Placement { + case PlacementHeader: + h.Set(p.Header, paddingValue) + case PlacementQueryInHeader: + u, err := url.Parse(p.RawURL) + if err != nil || u == nil { + return + } + u.RawQuery = p.Key + "=" + paddingValue + h.Set(p.Header, u.String()) + } +} + +func (c *Config) ApplyXPaddingToRequest(req *http.Request, config XPaddingConfig) { + if req == nil { + return + } + if req.Header == nil { + req.Header = make(http.Header) + } + + placement := config.Placement.Placement + + if placement == PlacementHeader || placement == PlacementQueryInHeader { + c.ApplyXPaddingToHeader(req.Header, config) + return + } + + paddingValue := GeneratePadding(config.Method, config.Length) + + switch placement { + case PlacementCookie: + ApplyPaddingToCookie(req, config.Placement.Key, paddingValue) + case PlacementQuery: + ApplyPaddingToQuery(req.URL, config.Placement.Key, paddingValue) + } +} + +func (c *Config) ApplyXPaddingToResponse(writer http.ResponseWriter, config XPaddingConfig) { + placement := config.Placement.Placement + + if placement == PlacementHeader || placement == PlacementQueryInHeader { + c.ApplyXPaddingToHeader(writer.Header(), config) + return + } + + paddingValue := GeneratePadding(config.Method, config.Length) + + switch placement { + case PlacementCookie: + ApplyPaddingToResponseCookie(writer, config.Placement.Key, paddingValue) + } +} + +func (c *Config) ExtractXPaddingFromRequest(req *http.Request, obfsMode bool) (string, string) { + if req == nil { + return "", "" + } + + if !obfsMode { + referrer := req.Header.Get("Referer") + + if referrer != "" { + if referrerURL, err := url.Parse(referrer); err == nil { + paddingValue := referrerURL.Query().Get("x_padding") + paddingPlacement := PlacementQueryInHeader + "=Referer, key=x_padding" + return paddingValue, paddingPlacement + } + } else { + paddingValue := req.URL.Query().Get("x_padding") + return paddingValue, PlacementQuery + ", key=x_padding" + } + } + + key := c.XPaddingKey + header := c.XPaddingHeader + + if cookie, err := req.Cookie(key); err == nil { + if cookie != nil && cookie.Value != "" { + paddingValue := cookie.Value + paddingPlacement := PlacementCookie + ", key=" + key + return paddingValue, paddingPlacement + } + } + + headerValue := req.Header.Get(header) + + if headerValue != "" { + if c.XPaddingPlacement == PlacementHeader { + paddingPlacement := PlacementHeader + "=" + header + return headerValue, paddingPlacement + } + + if parsedURL, err := url.Parse(headerValue); err == nil { + paddingPlacement := PlacementQueryInHeader + "=" + header + ", key=" + key + + return parsedURL.Query().Get(key), paddingPlacement + } + } + + queryValue := req.URL.Query().Get(key) + + if queryValue != "" { + paddingPlacement := PlacementQuery + ", key=" + key + return queryValue, paddingPlacement + } + + return "", "" +} + +func (c *Config) IsPaddingValid(paddingValue string, from, to int, method PaddingMethod) bool { + if paddingValue == "" { + return false + } + if to <= 0 { + if r, err := c.GetNormalizedXPaddingBytes(); err == nil { + from, to = r.Min, r.Max + } + } + + switch method { + case PaddingMethodRepeatX: + n := len(paddingValue) + return n >= from && n <= to + case PaddingMethodTokenish: + const tolerance = validationTolerance + + n := int(hpack.HuffmanEncodeLength(paddingValue)) + f := from - tolerance + t := to + tolerance + if f < 0 { + f = 0 + } + return n >= f && n <= t + default: + n := len(paddingValue) + return n >= from && n <= to + } +} diff --git a/clash-nyanpasu/backend/Cargo.lock b/clash-nyanpasu/backend/Cargo.lock index c09038f9bc..44bc297fb6 100644 --- a/clash-nyanpasu/backend/Cargo.lock +++ b/clash-nyanpasu/backend/Cargo.lock @@ -10683,9 +10683,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.1", "js-sys", diff --git a/clash-nyanpasu/frontend/nyanpasu/package.json b/clash-nyanpasu/frontend/nyanpasu/package.json index d78eb6f9d9..2830c9d71d 100644 --- a/clash-nyanpasu/frontend/nyanpasu/package.json +++ b/clash-nyanpasu/frontend/nyanpasu/package.json @@ -26,11 +26,11 @@ "@mui/x-date-pickers": "8.27.2", "@nyanpasu/interface": "workspace:^", "@nyanpasu/ui": "workspace:^", - "@paper-design/shaders-react": "0.0.72", + "@paper-design/shaders-react": "0.0.76", "@radix-ui/react-use-controllable-state": "1.2.2", "@tailwindcss/postcss": "4.2.2", "@tanstack/react-table": "8.21.3", - "@tanstack/react-virtual": "3.13.23", + "@tanstack/react-virtual": "3.13.24", "@tanstack/router-zod-adapter": "1.81.5", "@tauri-apps/api": "2.10.1", "@uidotdev/usehooks": "2.4.1", @@ -73,9 +73,9 @@ "@iconify/json": "2.2.463", "@monaco-editor/react": "4.7.0", "@tanstack/react-query": "5.97.0", - "@tanstack/react-router": "1.168.13", - "@tanstack/react-router-devtools": "1.166.11", - "@tanstack/router-plugin": "1.167.12", + "@tanstack/react-router": "1.168.22", + "@tanstack/react-router-devtools": "1.166.13", + "@tanstack/router-plugin": "1.167.22", "@tauri-apps/plugin-clipboard-manager": "2.3.2", "@tauri-apps/plugin-dialog": "2.6.0", "@tauri-apps/plugin-fs": "2.4.5", diff --git a/clash-nyanpasu/frontend/nyanpasu/vite.config.ts b/clash-nyanpasu/frontend/nyanpasu/vite.config.ts index d5fd9d3e35..63e58204b2 100644 --- a/clash-nyanpasu/frontend/nyanpasu/vite.config.ts +++ b/clash-nyanpasu/frontend/nyanpasu/vite.config.ts @@ -68,7 +68,7 @@ export default defineConfig(({ command, mode }) => { tsconfigPaths(), legacy({ renderLegacyChunks: false, - modernTargets: ['edge>=109', 'safari>=13'], + modernTargets: ['edge>=109', 'safari>=15'], modernPolyfills: true, additionalModernPolyfills: [ 'core-js/modules/es.object.has-own.js', diff --git a/clash-nyanpasu/manifest/version.json b/clash-nyanpasu/manifest/version.json index dec15c3c06..e26634e0da 100644 --- a/clash-nyanpasu/manifest/version.json +++ b/clash-nyanpasu/manifest/version.json @@ -2,7 +2,7 @@ "manifest_version": 1, "latest": { "mihomo": "v1.19.23", - "mihomo_alpha": "alpha-7ab4eed", + "mihomo_alpha": "alpha-0e0265f", "clash_rs": "v0.9.7", "clash_premium": "2023-09-05-gdcc8d87", "clash_rs_alpha": "0.9.7-alpha+sha.dd693bf" @@ -69,5 +69,5 @@ "linux-armv7hf": "clash-rs-armv7-unknown-linux-gnueabihf" } }, - "updated_at": "2026-04-16T22:25:52.280Z" + "updated_at": "2026-04-17T22:24:36.473Z" } diff --git a/clash-nyanpasu/package.json b/clash-nyanpasu/package.json index 42dbee900f..3e246e4f85 100644 --- a/clash-nyanpasu/package.json +++ b/clash-nyanpasu/package.json @@ -51,7 +51,7 @@ "prepare:check": "deno run -A scripts/deno/check.ts" }, "dependencies": { - "@prettier/plugin-oxc": "0.1.3", + "@prettier/plugin-oxc": "0.1.4", "husky": "9.1.7", "lodash-es": "4.18.1" }, @@ -63,7 +63,7 @@ "@types/fs-extra": "11.0.4", "@types/lodash-es": "4.17.12", "@types/node": "24.11.0", - "autoprefixer": "10.4.27", + "autoprefixer": "10.5.0", "conventional-changelog-conventionalcommits": "9.3.1", "cross-env": "10.1.0", "dedent": "1.7.2", @@ -76,11 +76,11 @@ "postcss-html": "1.8.1", "postcss-import": "16.1.1", "postcss-scss": "4.0.9", - "prettier": "3.8.2", - "prettier-plugin-ember-template-tag": "2.1.4", + "prettier": "3.8.3", + "prettier-plugin-ember-template-tag": "2.1.5", "prettier-plugin-tailwindcss": "0.7.2", "prettier-plugin-toml": "2.0.6", - "stylelint": "17.6.0", + "stylelint": "17.8.0", "stylelint-config-html": "1.1.0", "stylelint-config-recess-order": "7.7.0", "stylelint-config-standard": "40.0.0", diff --git a/clash-nyanpasu/pnpm-lock.yaml b/clash-nyanpasu/pnpm-lock.yaml index 3c55c786dd..659913a402 100644 --- a/clash-nyanpasu/pnpm-lock.yaml +++ b/clash-nyanpasu/pnpm-lock.yaml @@ -13,8 +13,8 @@ importers: .: dependencies: '@prettier/plugin-oxc': - specifier: 0.1.3 - version: 0.1.3 + specifier: 0.1.4 + version: 0.1.4 husky: specifier: 9.1.7 version: 9.1.7 @@ -30,7 +30,7 @@ importers: version: 20.5.0 '@ianvs/prettier-plugin-sort-imports': specifier: 4.7.1 - version: 4.7.1(@prettier/plugin-oxc@0.1.3)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.4(prettier@3.8.2))(prettier@3.8.2) + version: 4.7.1(@prettier/plugin-oxc@0.1.4)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.5(prettier@3.8.3))(prettier@3.8.3) '@tauri-apps/cli': specifier: 2.10.1 version: 2.10.1 @@ -44,8 +44,8 @@ importers: specifier: 24.11.0 version: 24.11.0 autoprefixer: - specifier: 10.4.27 - version: 10.4.27(postcss@8.5.10) + specifier: 10.5.0 + version: 10.5.0(postcss@8.5.10) conventional-changelog-conventionalcommits: specifier: 9.3.1 version: 9.3.1 @@ -83,38 +83,38 @@ importers: specifier: 4.0.9 version: 4.0.9(postcss@8.5.10) prettier: - specifier: 3.8.2 - version: 3.8.2 + specifier: 3.8.3 + version: 3.8.3 prettier-plugin-ember-template-tag: - specifier: 2.1.4 - version: 2.1.4(prettier@3.8.2) + specifier: 2.1.5 + version: 2.1.5(prettier@3.8.3) prettier-plugin-tailwindcss: specifier: 0.7.2 - version: 0.7.2(@ianvs/prettier-plugin-sort-imports@4.7.1(@prettier/plugin-oxc@0.1.3)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.4(prettier@3.8.2))(prettier@3.8.2))(@prettier/plugin-oxc@0.1.3)(@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.8.2))(prettier@3.8.2) + version: 0.7.2(@ianvs/prettier-plugin-sort-imports@4.7.1(@prettier/plugin-oxc@0.1.4)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.5(prettier@3.8.3))(prettier@3.8.3))(@prettier/plugin-oxc@0.1.4)(@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.8.3))(prettier@3.8.3) prettier-plugin-toml: specifier: 2.0.6 - version: 2.0.6(prettier@3.8.2) + version: 2.0.6(prettier@3.8.3) stylelint: - specifier: 17.6.0 - version: 17.6.0(typescript@5.9.3) + specifier: 17.8.0 + version: 17.8.0(typescript@5.9.3) stylelint-config-html: specifier: 1.1.0 - version: 1.1.0(postcss-html@1.8.1)(stylelint@17.6.0(typescript@5.9.3)) + version: 1.1.0(postcss-html@1.8.1)(stylelint@17.8.0(typescript@5.9.3)) stylelint-config-recess-order: specifier: 7.7.0 - version: 7.7.0(stylelint-order@8.1.1(stylelint@17.6.0(typescript@5.9.3)))(stylelint@17.6.0(typescript@5.9.3)) + version: 7.7.0(stylelint-order@8.1.1(stylelint@17.8.0(typescript@5.9.3)))(stylelint@17.8.0(typescript@5.9.3)) stylelint-config-standard: specifier: 40.0.0 - version: 40.0.0(stylelint@17.6.0(typescript@5.9.3)) + version: 40.0.0(stylelint@17.8.0(typescript@5.9.3)) stylelint-declaration-block-no-ignored-properties: specifier: 3.0.0 - version: 3.0.0(stylelint@17.6.0(typescript@5.9.3)) + version: 3.0.0(stylelint@17.8.0(typescript@5.9.3)) stylelint-order: specifier: 8.1.1 - version: 8.1.1(stylelint@17.6.0(typescript@5.9.3)) + version: 8.1.1(stylelint@17.8.0(typescript@5.9.3)) stylelint-scss: specifier: 7.0.0 - version: 7.0.0(stylelint@17.6.0(typescript@5.9.3)) + version: 7.0.0(stylelint@17.8.0(typescript@5.9.3)) tailwindcss: specifier: 4.2.2 version: 4.2.2 @@ -210,8 +210,8 @@ importers: specifier: workspace:^ version: link:../ui '@paper-design/shaders-react': - specifier: 0.0.72 - version: 0.0.72(@types/react@19.2.14)(react@19.2.5) + specifier: 0.0.76 + version: 0.0.76(@types/react@19.2.14)(react@19.2.5) '@radix-ui/react-use-controllable-state': specifier: 1.2.2 version: 1.2.2(@types/react@19.2.14)(react@19.2.5) @@ -222,11 +222,11 @@ importers: specifier: 8.21.3 version: 8.21.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/react-virtual': - specifier: 3.13.23 - version: 3.13.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: 3.13.24 + version: 3.13.24(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/router-zod-adapter': specifier: 1.81.5 - version: 1.81.5(@tanstack/react-router@1.168.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(zod@4.3.6) + version: 1.81.5(@tanstack/react-router@1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(zod@4.3.6) '@tauri-apps/api': specifier: 2.10.1 version: 2.10.1 @@ -346,14 +346,14 @@ importers: specifier: 5.97.0 version: 5.97.0(react@19.2.5) '@tanstack/react-router': - specifier: 1.168.13 - version: 1.168.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: 1.168.22 + version: 1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/react-router-devtools': - specifier: 1.166.11 - version: 1.166.11(@tanstack/react-router@1.168.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.168.9)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: 1.166.13 + version: 1.166.13(@tanstack/react-router@1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.168.15)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/router-plugin': - specifier: 1.167.12 - version: 1.167.12(@tanstack/react-router@1.168.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(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)) + specifier: 1.167.22 + version: 1.167.22(@tanstack/react-router@1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(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)) '@tauri-apps/plugin-clipboard-manager': specifier: 2.3.2 version: 2.3.2 @@ -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.10)(prettier@3.8.2)(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.10)(prettier@3.8.3)(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)) @@ -1351,8 +1351,8 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.1': - resolution: {integrity: sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==} + '@csstools/css-syntax-patches-for-csstree@1.1.3': + resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} peerDependencies: css-tree: ^3.2.1 peerDependenciesMeta: @@ -1435,21 +1435,24 @@ packages: peerDependencies: react: '>=16.8.0' - '@emnapi/core@1.5.0': - resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} - '@emnapi/core@1.7.1': resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} - '@emnapi/runtime@1.5.0': - resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} '@emnapi/runtime@1.7.1': resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -2040,12 +2043,15 @@ packages: peerDependencies: react: ^17.0.0 || ^18.0.0 || ^19.0.0 - '@napi-rs/wasm-runtime@1.0.7': - resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} - '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2253,14 +2259,20 @@ packages: cpu: [arm] os: [android] + '@oxc-parser/binding-android-arm-eabi@0.125.0': + resolution: {integrity: sha512-YfHwPEH8c5XNOlffaAqhsChNOBgmJ7rEgVbxSwAr65KDR0wbpZUBkrSaCClYL4urf0LmwyULrahHMvFAyk/dwA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + '@oxc-parser/binding-android-arm64@0.121.0': resolution: {integrity: sha512-/Dd1xIXboYAicw+twT2utxPD7bL8qh7d3ej0qvaYIMj3/EgIrGR+tSnjCUkiCT6g6uTC0neSS4JY8LxhdSU/sA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxc-parser/binding-android-arm64@0.99.0': - resolution: {integrity: sha512-V4jhmKXgQQdRnm73F+r3ZY4pUEsijQeSraFeaCGng7abSNJGs76X6l82wHnmjLGFAeY00LWtjcELs7ZmbJ9+lA==} + '@oxc-parser/binding-android-arm64@0.125.0': + resolution: {integrity: sha512-rh72O8ackqp0HC+3W38oCTkCFmOpXrHRrbP+4xrX8O1UmCWcyb5pIbA/+0ATPGVVl9NcHt/CgqI8rBuw4Y9kMg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] @@ -2271,8 +2283,8 @@ packages: cpu: [arm64] os: [darwin] - '@oxc-parser/binding-darwin-arm64@0.99.0': - resolution: {integrity: sha512-Rp41nf9zD5FyLZciS9l1GfK8PhYqrD5kEGxyTOA2esTLeAy37rZxetG2E3xteEolAkeb2WDkVrlxPtibeAncMg==} + '@oxc-parser/binding-darwin-arm64@0.125.0': + resolution: {integrity: sha512-14Q74TMQA/eO0N5dz5Tel25qma9vVJEpmrmqXnx0R7jMXhqFxkSSy40NOtCQijWUfeD5ho5+NuXDl5WSxyifJQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] @@ -2283,8 +2295,8 @@ packages: cpu: [x64] os: [darwin] - '@oxc-parser/binding-darwin-x64@0.99.0': - resolution: {integrity: sha512-WVonp40fPPxo5Gs0POTI57iEFv485TvNKOHMwZRhigwZRhZY2accEAkYIhei9eswF4HN5B44Wybkz7Gd1Qr/5Q==} + '@oxc-parser/binding-darwin-x64@0.125.0': + resolution: {integrity: sha512-qWQDphAaIS6qXeuYcWm4jta8qFZpjjim2WxiPwZmHi77COS8i0Jct8tBcNIOZ/JaVh+hCL2it228m2Lr9GOL6A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] @@ -2295,8 +2307,8 @@ packages: cpu: [x64] os: [freebsd] - '@oxc-parser/binding-freebsd-x64@0.99.0': - resolution: {integrity: sha512-H30bjOOttPmG54gAqu6+HzbLEzuNOYO2jZYrIq4At+NtLJwvNhXz28Hf5iEAFZIH/4hMpLkM4VN7uc+5UlNW3Q==} + '@oxc-parser/binding-freebsd-x64@0.125.0': + resolution: {integrity: sha512-PTATC/j2MvDP8lejoCC7PFWNoYV2NsVzzM0WgBqZDFAkFdKsW0wfbQWochfY3fHNUN1QhZNetrd/K4Pdo6cIHQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] @@ -2307,8 +2319,8 @@ packages: cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm-gnueabihf@0.99.0': - resolution: {integrity: sha512-0Z/Th0SYqzSRDPs6tk5lQdW0i73UCupnim3dgq2oW0//UdLonV/5wIZCArfKGC7w9y4h8TxgXpgtIyD1kKzzlQ==} + '@oxc-parser/binding-linux-arm-gnueabihf@0.125.0': + resolution: {integrity: sha512-Colj5agHBAMKZrkyPcCEelfKuh8sNi1lWpJf1TiEeEmbREQ6I2ytG+ccfdDaiUV7Z0Vw5FyJbnqEPgHo8kF3RQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] @@ -2319,8 +2331,8 @@ packages: cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm-musleabihf@0.99.0': - resolution: {integrity: sha512-xo0wqNd5bpbzQVNpAIFbHk1xa+SaS/FGBABCd942SRTnrpxl6GeDj/s1BFaGcTl8MlwlKVMwOcyKrw/2Kdfquw==} + '@oxc-parser/binding-linux-arm-musleabihf@0.125.0': + resolution: {integrity: sha512-BxQ8o082+/qtjAFK6WUV+/bi0y3M0RPvPQNm8JSY7/7LfhbWq6NykgZiGayrtauO1nowpmGlnpJXXMp9q0oT1A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] @@ -2332,8 +2344,8 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-arm64-gnu@0.99.0': - resolution: {integrity: sha512-u26I6LKoLTPTd4Fcpr0aoAtjnGf5/ulMllo+QUiBhupgbVCAlaj4RyXH/mvcjcsl2bVBv9E/gYJZz2JjxQWXBA==} + '@oxc-parser/binding-linux-arm64-gnu@0.125.0': + resolution: {integrity: sha512-qR0dOth+4whygUwoNnfews8jMC78gjhIBfcy9AFzvxoh7PFGdferRp3KV/4kkeaVk2kOS/5grlAeJevpA+/Pfg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -2346,8 +2358,8 @@ packages: os: [linux] libc: [musl] - '@oxc-parser/binding-linux-arm64-musl@0.99.0': - resolution: {integrity: sha512-qhftDo2D37SqCEl3ZTa367NqWSZNb1Ddp34CTmShLKFrnKdNiUn55RdokLnHtf1AL5ssaQlYDwBECX7XiBWOhw==} + '@oxc-parser/binding-linux-arm64-musl@0.125.0': + resolution: {integrity: sha512-eIXyzpA12/+maKjMSsXdHfpzwQcoRfzokT+/ZhVEo6u/9RcXQrZZmZ70MmmJqwVcLez6U4ScjB/eiYlsEs7p0g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -2360,6 +2372,13 @@ packages: os: [linux] libc: [glibc] + '@oxc-parser/binding-linux-ppc64-gnu@0.125.0': + resolution: {integrity: sha512-w7ir5OuqSJUKLadmsSAWwTNso/ZGem2bPT/1LSU7l+ecmKPyegIvU+wzY0ADhZ/t/goaedqyp24SDRxyLxO9zg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@oxc-parser/binding-linux-riscv64-gnu@0.121.0': resolution: {integrity: sha512-+QiFoGxhAbaI/amqX567784cDyyuZIpinBrJNxUzb+/L2aBRX67mN6Jv40pqduHf15yYByI+K5gUEygCuv0z9w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2367,8 +2386,8 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-riscv64-gnu@0.99.0': - resolution: {integrity: sha512-zxn/xkf519f12FKkpL5XwJipsylfSSnm36h6c1zBDTz4fbIDMGyIhHfWfwM7uUmHo9Aqw1pLxFpY39Etv398+Q==} + '@oxc-parser/binding-linux-riscv64-gnu@0.125.0': + resolution: {integrity: sha512-2KPTfWorcW8RNE8aEMHKbPSjHDBjFVYqg8nSLRBp7pe7VBqHsmkO9jpK8YmaYA5d5GcUy+J++5O4EgxkrQBEtw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] @@ -2381,6 +2400,13 @@ packages: os: [linux] libc: [musl] + '@oxc-parser/binding-linux-riscv64-musl@0.125.0': + resolution: {integrity: sha512-Vsl8dmQdKtDsQiDPHP5VFjXOuVGcZQcziYMkU/yPnlaKHMqoX/q+bxt7K+BwResi9Cc8pnZ6oYGTgPcjAtt5QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + '@oxc-parser/binding-linux-s390x-gnu@0.121.0': resolution: {integrity: sha512-DB1EW5VHZdc1lIRjOI3bW/wV6R6y0xlfvdVrqj6kKi7Ayu2U3UqUBdq9KviVkcUGd5Oq+dROqvUEEFRXGAM7EQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2388,8 +2414,8 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-s390x-gnu@0.99.0': - resolution: {integrity: sha512-Y1eSDKDS5E4IVC7Oxw+NbYAKRmJPMJTIjW+9xOWwteDHkFqpocKe0USxog+Q1uhzalD9M0p9eXWEWdGQCMDBMQ==} + '@oxc-parser/binding-linux-s390x-gnu@0.125.0': + resolution: {integrity: sha512-HwY5kuM818r/kHdHG2TZqzqxyF7fz90prPg85R/2VmgRWk8cMyGZo+8BNZDQAMJ6aGSTRvn2sdGXv3sZ5bsUWw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] @@ -2402,8 +2428,8 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-x64-gnu@0.99.0': - resolution: {integrity: sha512-YVJMfk5cFWB8i2/nIrbk6n15bFkMHqWnMIWkVx7r2KwpTxHyFMfu2IpeVKo1ITDSmt5nBrGdLHD36QRlu2nDLg==} + '@oxc-parser/binding-linux-x64-gnu@0.125.0': + resolution: {integrity: sha512-o7k6+xAI2pIkjBsCqM0elI4q+qY/3TexH6cpIlGm+nJze1tvx7QEHCKdiy6wnRacFvUYmySEZ5hWFBc9MbxrIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -2416,8 +2442,8 @@ packages: os: [linux] libc: [musl] - '@oxc-parser/binding-linux-x64-musl@0.99.0': - resolution: {integrity: sha512-2+SDPrie5f90A1b9EirtVggOgsqtsYU5raZwkDYKyS1uvJzjqHCDhG/f4TwQxHmIc5YkczdQfwvN91lwmjsKYQ==} + '@oxc-parser/binding-linux-x64-musl@0.125.0': + resolution: {integrity: sha512-vksRynFD6vytE1sDZCaeIk6y6rCsq0a18T4kcXbfGHBq2q/qSyDogWLk3A3S3hl/ikNfse7yrEwAuQ8ldIJeAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -2429,13 +2455,19 @@ packages: cpu: [arm64] os: [openharmony] + '@oxc-parser/binding-openharmony-arm64@0.125.0': + resolution: {integrity: sha512-AAtg4pnKvrKsay2ldZZRY98ALFBOgbyy3Gyxo658z6aecM0Zr5mI9BOHRCchSVKUHqMqmjhCA4wIdZvz02VrAw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@oxc-parser/binding-wasm32-wasi@0.121.0': resolution: {integrity: sha512-5TFISkPTymKvsmIlKasPVTPuWxzCcrT8pM+p77+mtQbIZDd1UC8zww4CJcRI46kolmgrEX6QpKO8AvWMVZ+ifw==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@oxc-parser/binding-wasm32-wasi@0.99.0': - resolution: {integrity: sha512-DKA4j0QerUWSMADziLM5sAyM7V53Fj95CV9SjP77bPfEfT7MnvFKnneaRMqPK1cpzjAGiQF52OBUIKyk0dwOQA==} + '@oxc-parser/binding-wasm32-wasi@0.125.0': + resolution: {integrity: sha512-FkIQFrwlBXoFsazb9NQpQPP4YI9sWWXUOLkIPYlQb+hPwr+VY6d0B7l26yMBR2ktf2h3qyAMOW6Pd+mX9rtOJg==} engines: {node: '>=14.0.0'} cpu: [wasm32] @@ -2445,8 +2477,8 @@ packages: cpu: [arm64] os: [win32] - '@oxc-parser/binding-win32-arm64-msvc@0.99.0': - resolution: {integrity: sha512-EaB3AvsxqdNUhh9FOoAxRZ2L4PCRwDlDb//QXItwyOJrX7XS+uGK9B1KEUV4FZ/7rDhHsWieLt5e07wl2Ti5AQ==} + '@oxc-parser/binding-win32-arm64-msvc@0.125.0': + resolution: {integrity: sha512-bi4RY9oktNm3kQ3qRCJgBKtwqSg+mtnt5W9l33rdiTyiXlL8a1LQQy1x7aym/ArHDE+19kSWSr2YDd2ExxzbfQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] @@ -2457,14 +2489,20 @@ packages: cpu: [ia32] os: [win32] + '@oxc-parser/binding-win32-ia32-msvc@0.125.0': + resolution: {integrity: sha512-ZhvL2vK+9rzjk1US2d2u6NeI1/jtkzsm//ilFac+Kn3klTpJJlKNZwF23CUiAu+B3rdQUbPItm/BHlL6f/5uPA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + '@oxc-parser/binding-win32-x64-msvc@0.121.0': resolution: {integrity: sha512-BOp1KCzdboB1tPqoCPXgntgFs0jjeSyOXHzgxVFR7B/qfr3F8r4YDacHkTOUNXtDgM8YwKnkf3rE5gwALYX7NA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxc-parser/binding-win32-x64-msvc@0.99.0': - resolution: {integrity: sha512-sJN1Q8h7ggFOyDn0zsHaXbP/MklAVUvhrbq0LA46Qum686P3SZQHjbATqJn9yaVEvaSKXCshgl0vQ1gWkGgpcQ==} + '@oxc-parser/binding-win32-x64-msvc@0.125.0': + resolution: {integrity: sha512-P4ywUSCYIg44Y82wF3e0ns1BV1dNn+ZhfjNDwm0FTPtBKXedOCRPrvmjXn7Qb+IDGGHAA68lmDLCjGxuKUwXPw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2472,8 +2510,8 @@ packages: '@oxc-project/types@0.121.0': resolution: {integrity: sha512-CGtOARQb9tyv7ECgdAlFxi0Fv7lmzvmlm2rpD/RdijOO9rfk/JvB1CjT8EnoD+tjna/IYgKKw3IV7objRb+aYw==} - '@oxc-project/types@0.99.0': - resolution: {integrity: sha512-LLDEhXB7g1m5J+woRSgfKsFPS3LhR9xRhTeIoEBm5WrkwMxn6eZ0Ld0c0K5eHB57ChZX6I3uSmmLjZ8pcjlRcw==} + '@oxc-project/types@0.125.0': + resolution: {integrity: sha512-s9RKLJbRR+3kEFB3mmJVPWah3cZUAl0Jzmthx6Pb/QXnlNkRwTP75tK4uVahp/ifiiTmNYMXI1+NnGP1rNurXg==} '@oxc-resolver/binding-android-arm-eabi@11.19.1': resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} @@ -2705,8 +2743,8 @@ packages: cpu: [x64] os: [win32] - '@paper-design/shaders-react@0.0.72': - resolution: {integrity: sha512-q6KwquL93ZVNcuSM7pqzW0z/VLjnDVb/NSpYyGJBxf7MEHHCXx37E+zw/Px6RZLt3SGCUerIDDCGCMT385oW0w==} + '@paper-design/shaders-react@0.0.76': + resolution: {integrity: sha512-uPJWrYRf6cJdO2H+fuXlahaqz0QjYglNAyUTaRfIInpzCa/d6guxBIK003soAZQFuQ035yg9FhtfFzKNWm+a5A==} peerDependencies: '@types/react': ^18 || ^19 react: ^18 || ^19 @@ -2714,8 +2752,8 @@ packages: '@types/react': optional: true - '@paper-design/shaders@0.0.72': - resolution: {integrity: sha512-rk2BFuV5ood2DaivbxJC2jQMzaB434isDUzdUQ85Cy0OWnUMuxl8kyGMR74TDPyjo3EvcHIyreNLkJdRG+GfSA==} + '@paper-design/shaders@0.0.76': + resolution: {integrity: sha512-AcNDY4J66YQHUfQYFInkCP7M9VOje0od7wLpOR7LtCmc532opJy6ll+h1W9zBovz8tt9U7OADUmJ/qKEXyOX/A==} '@parcel/watcher-android-arm64@2.4.1': resolution: {integrity: sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==} @@ -2804,8 +2842,8 @@ packages: '@preact/signals-core@1.14.0': resolution: {integrity: sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ==} - '@prettier/plugin-oxc@0.1.3': - resolution: {integrity: sha512-aABz3zIRilpWMekbt1FL1JVBQrQLR8L4Td2SRctECrWSsXGTNn/G1BqNSKCdbvQS1LWstAXfqcXzDki7GAAJyg==} + '@prettier/plugin-oxc@0.1.4': + resolution: {integrity: sha512-P/KX37tuR1R7xMHMakgzdWsRDMeze7SkwUcGQKbqQVSsJLW0q5kxax2dxEJgK4E4zIoMy7pG6UUE7x4al8AQeg==} engines: {node: '>=14'} '@radix-ui/number@1.1.1': @@ -4002,20 +4040,20 @@ packages: peerDependencies: react: ^18 || ^19 - '@tanstack/react-router-devtools@1.166.11': - resolution: {integrity: sha512-WYR3q4Xui5yPT/5PXtQh8i03iUA7q8dONBjWpV3nsGdM8Cs1FxpfhLstW0wZO1dOvSyElscwTRCJ6nO5N8r3Lg==} + '@tanstack/react-router-devtools@1.166.13': + resolution: {integrity: sha512-6yKRFFJrEEOiGp5RAAuGCYsl81M4XAhJmLcu9PKj+HZle4A3dsP60lwHoqQYWHMK9nKKFkdXR+D8qxzxqtQbEA==} engines: {node: '>=20.19'} peerDependencies: - '@tanstack/react-router': ^1.168.2 - '@tanstack/router-core': ^1.168.2 + '@tanstack/react-router': ^1.168.15 + '@tanstack/router-core': ^1.168.11 react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' peerDependenciesMeta: '@tanstack/router-core': optional: true - '@tanstack/react-router@1.168.13': - resolution: {integrity: sha512-rvrL1bK/bOjjfDyAKKFvkVXeo8IZZmwfy9DwJaMlj7lCmpWl4Qban2/d+EirlHPcwHDNq5OkS5jhcBiZ+24RJw==} + '@tanstack/react-router@1.168.22': + resolution: {integrity: sha512-W2LyfkfJtDCf//jOjZeUBWwOVl8iDRVTECpGHa2M28MT3T5/VVnjgicYNHR/ax0Filk1iU67MRjcjHheTYvK1Q==} engines: {node: '>=20.19'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -4034,8 +4072,8 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-virtual@3.13.23': - resolution: {integrity: sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==} + '@tanstack/react-virtual@3.13.24': + resolution: {integrity: sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4046,34 +4084,34 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.168.9': - resolution: {integrity: sha512-18oeEwEDyXOIuO1VBP9ACaK7tYHZUjynGDCoUh/5c/BNhia9vCJCp9O0LfhZXOorDc/PmLSgvmweFhVmIxF10g==} + '@tanstack/router-core@1.168.15': + resolution: {integrity: sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA==} engines: {node: '>=20.19'} hasBin: true - '@tanstack/router-devtools-core@1.167.1': - resolution: {integrity: sha512-ECMM47J4KmifUvJguGituSiBpfN8SyCUEoxQks5RY09hpIBfR2eswCv2e6cJimjkKwBQXOVTPkTUk/yRvER+9w==} + '@tanstack/router-devtools-core@1.167.3': + resolution: {integrity: sha512-fJ1VMhyQgnoashTrP763c2HRc9kofgF61L7Jb3F6eTHAmCKtGVx8BRtiFt37sr3U0P0jmaaiiSPGP6nT5JtVNg==} engines: {node: '>=20.19'} peerDependencies: - '@tanstack/router-core': ^1.168.2 + '@tanstack/router-core': ^1.168.11 csstype: ^3.0.10 peerDependenciesMeta: csstype: optional: true - '@tanstack/router-generator@1.166.24': - resolution: {integrity: sha512-vdaGKwuH+r+DPe6R1mjk+TDDmDH6NTG7QqwxHqGEvOH4aGf9sPjhmRKNJZqQr8cPIbfp6u5lXyZ1TeDcSNMVEA==} + '@tanstack/router-generator@1.166.32': + resolution: {integrity: sha512-VuusKwEXcgKq+myq1JQfZogY8scTXIIeFls50dJ/UXgCXWp5n14iFreYNlg41wURcak2oA3M+t2TVfD0xUUD6g==} engines: {node: '>=20.19'} - '@tanstack/router-plugin@1.167.12': - resolution: {integrity: sha512-StEHcctCuFI5taSjO+lhR/yQ+EK63BdyYa+ne6FoNQPB3MMrOUrz2ZVnbqILRLkh2b+p2EfBKt65sgAKdKygPQ==} + '@tanstack/router-plugin@1.167.22': + resolution: {integrity: sha512-wYPzIvBK8bcmXVUpZfSgGBXOrfBAdF4odKevz6rejio5rEd947NtKDF5R7eYdwlAOmRqYpLJnJ1QHkc5t8bY4w==} engines: {node: '>=20.19'} hasBin: true peerDependencies: '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.168.10 - vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' - vite-plugin-solid: ^2.11.10 + '@tanstack/react-router': ^1.168.21 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0' + vite-plugin-solid: ^2.11.10 || ^3.0.0-0 webpack: '>=5.92.0' peerDependenciesMeta: '@rsbuild/core': @@ -4105,12 +4143,12 @@ packages: resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} - '@tanstack/virtual-core@3.13.23': - resolution: {integrity: sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==} - '@tanstack/virtual-core@3.13.9': resolution: {integrity: sha512-3jztt0jpaoJO5TARe2WIHC1UQC3VMLAFUW5mmMo0yrkwtDB2AQP0+sh10BVUpWrnvHjSLvzFizydtEGLCJKFoQ==} + '@tanstack/virtual-core@3.14.0': + resolution: {integrity: sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==} + '@tanstack/virtual-file-routes@1.161.7': resolution: {integrity: sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ==} engines: {node: '>=20.19'} @@ -4756,10 +4794,6 @@ packages: array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} - ast-types@0.16.1: - resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} - engines: {node: '>=4'} - astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} @@ -4770,8 +4804,8 @@ packages: async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - autoprefixer@10.4.27: - resolution: {integrity: sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==} + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -4808,6 +4842,11 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.20: + resolution: {integrity: sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==} + engines: {node: '>=6.0.0'} + hasBin: true + baseline-browser-mapping@2.9.9: resolution: {integrity: sha512-V8fbOCSeOFvlDj7LLChUcqbZrdKD9RU/VR260piF1790vT0mfLSwGc/Qzxv3IqiTukOpNtItePa0HBpMAj7MDg==} hasBin: true @@ -4859,6 +4898,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -4897,6 +4941,9 @@ packages: caniuse-lite@1.0.30001776: resolution: {integrity: sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==} + caniuse-lite@1.0.30001788: + resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -5069,8 +5116,8 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-es@2.0.0: - resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==} + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} copy-anything@2.0.6: resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==} @@ -5442,6 +5489,9 @@ packages: electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + electron-to-chromium@1.5.340: + resolution: {integrity: sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==} + emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -5715,8 +5765,8 @@ packages: resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} engines: {node: '>=18'} - globby@16.1.1: - resolution: {integrity: sha512-dW7vl+yiAJSp6aCekaVnVJxurRv7DCOLyXqEG3RYMYUg7AuJ2jCqPkZTA8ooqC2vtnkaMcV5WfFBMuEnTu1OQg==} + globby@16.2.0: + resolution: {integrity: sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==} engines: {node: '>=20'} globjoin@0.1.4: @@ -6545,6 +6595,9 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -6596,8 +6649,8 @@ packages: resolution: {integrity: sha512-ek9o58+SCv6AV7nchiAcUJy1DNE2CC5WRdBcO0mF+W4oRjNQfPO7b3pLjTHSFECpHkKGOZSQxx3hk8viIL5YCg==} engines: {node: ^20.19.0 || >=22.12.0} - oxc-parser@0.99.0: - resolution: {integrity: sha512-MpS1lbd2vR0NZn1v0drpgu7RUFu3x9Rd0kxExObZc2+F+DIrV0BOMval/RO3BYGwssIOerII6iS8EbbpCCZQpQ==} + oxc-parser@0.125.0: + resolution: {integrity: sha512-6M0gEDDVMGGy+Ckg/mlLh4PL87sfKRMlkQJTVTxdcEREwDa4usWjM9n4jC6Jxa5+nc3YlZTecUs4hHjoTVWKaw==} engines: {node: ^20.19.0 || >=22.12.0} oxc-resolver@11.19.1: @@ -6795,8 +6848,8 @@ packages: resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} engines: {node: ^10 || ^12 || >=14} - prettier-plugin-ember-template-tag@2.1.4: - resolution: {integrity: sha512-KMjDL4UFDuLpFD69SlhW2AB5YQ3AUS/1NNBjdY/rXko180eWo1VNeCvyZdqNR4Zamii/6Y/E3D/ey8BmhKLaJw==} + prettier-plugin-ember-template-tag@2.1.5: + resolution: {integrity: sha512-AF8Ld5DagbD5V3cw0tkw8youFD9fxsKAR4nowJ41WzgnaFZHXkyw+PnOAWP2ose1rw+Aff3Z7PXFPkn0/CmA5Q==} engines: {node: 18.* || >= 20} peerDependencies: prettier: '>= 3.0.0' @@ -6862,8 +6915,8 @@ packages: peerDependencies: prettier: ^3.0.3 - prettier@3.8.2: - resolution: {integrity: sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==} + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} engines: {node: '>=14'} hasBin: true @@ -7056,10 +7109,6 @@ packages: real-cancellable-promise@1.2.0: resolution: {integrity: sha512-FYhmx1FVSgoPRjneoTjh+EKZcNb8ijl/dyatTzase5eujYhVrLNDOiIY6AgQq7GU1kOoLgEd9jLVbhFg8k8dOQ==} - recast@0.23.11: - resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} - engines: {node: '>= 4'} - regenerate-unicode-properties@10.2.0: resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} engines: {node: '>=4'} @@ -7347,14 +7396,14 @@ packages: engines: {node: '>=10'} hasBin: true - seroval-plugins@1.4.2: - resolution: {integrity: sha512-X7p4MEDTi+60o2sXZ4bnDBhgsUYDSkQEvzYZuJyFqWg9jcoPsHts5nrg5O956py2wyt28lUrBxk0M0/wU8URpA==} + seroval-plugins@1.5.2: + resolution: {integrity: sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg==} engines: {node: '>=10'} peerDependencies: seroval: ^1.0 - seroval@1.4.2: - resolution: {integrity: sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ==} + seroval@1.5.2: + resolution: {integrity: sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==} engines: {node: '>=10'} set-harmonic-interval@1.0.1: @@ -7563,8 +7612,8 @@ packages: peerDependencies: stylelint: ^16.8.2 || ^17.0.0 - stylelint@17.6.0: - resolution: {integrity: sha512-tokrsMIVAR9vAQ/q3UVEr7S0dGXCi7zkCezPRnS2kqPUulvUh5Vgfwngrk4EoAoW7wnrThqTdnTFN5Ra7CaxIg==} + stylelint@17.8.0: + resolution: {integrity: sha512-oHkld9T60LDSaUQ4CSVc+tlt9eUoDlxhaGWShsUCKyIL14boZfmK5bSphZqx64aiC5tCqX+BsQMTMoSz8D1zIg==} engines: {node: '>=20.19.0'} hasBin: true @@ -7650,9 +7699,6 @@ packages: resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} engines: {node: '>=10'} - tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -9152,7 +9198,7 @@ snapshots: dependencies: '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.1(css-tree@3.2.1)': + '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': optionalDependencies: css-tree: 3.2.1 @@ -9244,20 +9290,15 @@ snapshots: react: 19.2.5 tslib: 2.7.0 - '@emnapi/core@1.5.0': - dependencies: - '@emnapi/wasi-threads': 1.1.0 - tslib: 2.8.1 - optional: true - '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.5.0': + '@emnapi/core@1.9.2': dependencies: + '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true @@ -9266,11 +9307,21 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.1.0': dependencies: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.25.9 @@ -9472,18 +9523,18 @@ snapshots: '@standard-schema/utils': 0.3.0 react-hook-form: 7.72.1(react@19.2.5) - '@ianvs/prettier-plugin-sort-imports@4.7.1(@prettier/plugin-oxc@0.1.3)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.4(prettier@3.8.2))(prettier@3.8.2)': + '@ianvs/prettier-plugin-sort-imports@4.7.1(@prettier/plugin-oxc@0.1.4)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.5(prettier@3.8.3))(prettier@3.8.3)': dependencies: '@babel/generator': 7.29.0 '@babel/parser': 7.29.0 '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 - prettier: 3.8.2 + prettier: 3.8.3 semver: 7.7.3 optionalDependencies: - '@prettier/plugin-oxc': 0.1.3 + '@prettier/plugin-oxc': 0.1.4 content-tag: 4.0.0 - prettier-plugin-ember-template-tag: 2.1.4(prettier@3.8.2) + prettier-plugin-ember-template-tag: 2.1.5(prettier@3.8.3) transitivePeerDependencies: - supports-color @@ -9841,13 +9892,6 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@napi-rs/wasm-runtime@1.0.7': - dependencies: - '@emnapi/core': 1.5.0 - '@emnapi/runtime': 1.5.0 - '@tybys/wasm-util': 0.10.1 - optional: true - '@napi-rs/wasm-runtime@1.1.1': dependencies: '@emnapi/core': 1.7.1 @@ -9855,6 +9899,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -10129,115 +10180,132 @@ snapshots: '@oxc-parser/binding-android-arm-eabi@0.121.0': optional: true + '@oxc-parser/binding-android-arm-eabi@0.125.0': + optional: true + '@oxc-parser/binding-android-arm64@0.121.0': optional: true - '@oxc-parser/binding-android-arm64@0.99.0': + '@oxc-parser/binding-android-arm64@0.125.0': optional: true '@oxc-parser/binding-darwin-arm64@0.121.0': optional: true - '@oxc-parser/binding-darwin-arm64@0.99.0': + '@oxc-parser/binding-darwin-arm64@0.125.0': optional: true '@oxc-parser/binding-darwin-x64@0.121.0': optional: true - '@oxc-parser/binding-darwin-x64@0.99.0': + '@oxc-parser/binding-darwin-x64@0.125.0': optional: true '@oxc-parser/binding-freebsd-x64@0.121.0': optional: true - '@oxc-parser/binding-freebsd-x64@0.99.0': + '@oxc-parser/binding-freebsd-x64@0.125.0': optional: true '@oxc-parser/binding-linux-arm-gnueabihf@0.121.0': optional: true - '@oxc-parser/binding-linux-arm-gnueabihf@0.99.0': + '@oxc-parser/binding-linux-arm-gnueabihf@0.125.0': optional: true '@oxc-parser/binding-linux-arm-musleabihf@0.121.0': optional: true - '@oxc-parser/binding-linux-arm-musleabihf@0.99.0': + '@oxc-parser/binding-linux-arm-musleabihf@0.125.0': optional: true '@oxc-parser/binding-linux-arm64-gnu@0.121.0': optional: true - '@oxc-parser/binding-linux-arm64-gnu@0.99.0': + '@oxc-parser/binding-linux-arm64-gnu@0.125.0': optional: true '@oxc-parser/binding-linux-arm64-musl@0.121.0': optional: true - '@oxc-parser/binding-linux-arm64-musl@0.99.0': + '@oxc-parser/binding-linux-arm64-musl@0.125.0': optional: true '@oxc-parser/binding-linux-ppc64-gnu@0.121.0': optional: true + '@oxc-parser/binding-linux-ppc64-gnu@0.125.0': + optional: true + '@oxc-parser/binding-linux-riscv64-gnu@0.121.0': optional: true - '@oxc-parser/binding-linux-riscv64-gnu@0.99.0': + '@oxc-parser/binding-linux-riscv64-gnu@0.125.0': optional: true '@oxc-parser/binding-linux-riscv64-musl@0.121.0': optional: true + '@oxc-parser/binding-linux-riscv64-musl@0.125.0': + optional: true + '@oxc-parser/binding-linux-s390x-gnu@0.121.0': optional: true - '@oxc-parser/binding-linux-s390x-gnu@0.99.0': + '@oxc-parser/binding-linux-s390x-gnu@0.125.0': optional: true '@oxc-parser/binding-linux-x64-gnu@0.121.0': optional: true - '@oxc-parser/binding-linux-x64-gnu@0.99.0': + '@oxc-parser/binding-linux-x64-gnu@0.125.0': optional: true '@oxc-parser/binding-linux-x64-musl@0.121.0': optional: true - '@oxc-parser/binding-linux-x64-musl@0.99.0': + '@oxc-parser/binding-linux-x64-musl@0.125.0': optional: true '@oxc-parser/binding-openharmony-arm64@0.121.0': optional: true + '@oxc-parser/binding-openharmony-arm64@0.125.0': + optional: true + '@oxc-parser/binding-wasm32-wasi@0.121.0': dependencies: '@napi-rs/wasm-runtime': 1.1.1 optional: true - '@oxc-parser/binding-wasm32-wasi@0.99.0': + '@oxc-parser/binding-wasm32-wasi@0.125.0': dependencies: - '@napi-rs/wasm-runtime': 1.0.7 + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) optional: true '@oxc-parser/binding-win32-arm64-msvc@0.121.0': optional: true - '@oxc-parser/binding-win32-arm64-msvc@0.99.0': + '@oxc-parser/binding-win32-arm64-msvc@0.125.0': optional: true '@oxc-parser/binding-win32-ia32-msvc@0.121.0': optional: true + '@oxc-parser/binding-win32-ia32-msvc@0.125.0': + optional: true + '@oxc-parser/binding-win32-x64-msvc@0.121.0': optional: true - '@oxc-parser/binding-win32-x64-msvc@0.99.0': + '@oxc-parser/binding-win32-x64-msvc@0.125.0': optional: true '@oxc-project/types@0.121.0': {} - '@oxc-project/types@0.99.0': {} + '@oxc-project/types@0.125.0': {} '@oxc-resolver/binding-android-arm-eabi@11.19.1': optional: true @@ -10358,14 +10426,14 @@ snapshots: '@oxlint/binding-win32-x64-msvc@1.59.0': optional: true - '@paper-design/shaders-react@0.0.72(@types/react@19.2.14)(react@19.2.5)': + '@paper-design/shaders-react@0.0.76(@types/react@19.2.14)(react@19.2.5)': dependencies: - '@paper-design/shaders': 0.0.72 + '@paper-design/shaders': 0.0.76 react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@paper-design/shaders@0.0.72': {} + '@paper-design/shaders@0.0.76': {} '@parcel/watcher-android-arm64@2.4.1': optional: true @@ -10428,9 +10496,9 @@ snapshots: '@preact/signals-core@1.14.0': {} - '@prettier/plugin-oxc@0.1.3': + '@prettier/plugin-oxc@0.1.4': dependencies: - oxc-parser: 0.99.0 + oxc-parser: 0.125.0 '@radix-ui/number@1.1.1': {} @@ -11582,22 +11650,22 @@ snapshots: '@tanstack/query-core': 5.97.0 react: 19.2.5 - '@tanstack/react-router-devtools@1.166.11(@tanstack/react-router@1.168.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.168.9)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@tanstack/react-router-devtools@1.166.13(@tanstack/react-router@1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.168.15)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@tanstack/react-router': 1.168.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@tanstack/router-devtools-core': 1.167.1(@tanstack/router-core@1.168.9)(csstype@3.2.3) + '@tanstack/react-router': 1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tanstack/router-devtools-core': 1.167.3(@tanstack/router-core@1.168.15)(csstype@3.2.3) react: 19.2.5 react-dom: 19.2.5(react@19.2.5) optionalDependencies: - '@tanstack/router-core': 1.168.9 + '@tanstack/router-core': 1.168.15 transitivePeerDependencies: - csstype - '@tanstack/react-router@1.168.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@tanstack/react-router@1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@tanstack/history': 1.161.6 '@tanstack/react-store': 0.9.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@tanstack/router-core': 1.168.9 + '@tanstack/router-core': 1.168.15 isbot: 5.1.28 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -11615,9 +11683,9 @@ snapshots: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - '@tanstack/react-virtual@3.13.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@tanstack/react-virtual@3.13.24(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@tanstack/virtual-core': 3.13.23 + '@tanstack/virtual-core': 3.14.0 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -11627,35 +11695,35 @@ snapshots: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - '@tanstack/router-core@1.168.9': + '@tanstack/router-core@1.168.15': dependencies: '@tanstack/history': 1.161.6 - cookie-es: 2.0.0 - seroval: 1.4.2 - seroval-plugins: 1.4.2(seroval@1.4.2) + cookie-es: 3.1.1 + seroval: 1.5.2 + seroval-plugins: 1.5.2(seroval@1.5.2) - '@tanstack/router-devtools-core@1.167.1(@tanstack/router-core@1.168.9)(csstype@3.2.3)': + '@tanstack/router-devtools-core@1.167.3(@tanstack/router-core@1.168.15)(csstype@3.2.3)': dependencies: - '@tanstack/router-core': 1.168.9 + '@tanstack/router-core': 1.168.15 clsx: 2.1.1 goober: 2.1.16(csstype@3.2.3) optionalDependencies: csstype: 3.2.3 - '@tanstack/router-generator@1.166.24': + '@tanstack/router-generator@1.166.32': dependencies: - '@tanstack/router-core': 1.168.9 + '@babel/types': 7.29.0 + '@tanstack/router-core': 1.168.15 '@tanstack/router-utils': 1.161.6 '@tanstack/virtual-file-routes': 1.161.7 - prettier: 3.8.2 - recast: 0.23.11 - source-map: 0.7.4 + magic-string: 0.30.21 + prettier: 3.8.3 tsx: 4.21.0 zod: 3.25.76 transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.167.12(@tanstack/react-router@1.168.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(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))': + '@tanstack/router-plugin@1.167.22(@tanstack/react-router@1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(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: '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.0) @@ -11663,15 +11731,15 @@ snapshots: '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 - '@tanstack/router-core': 1.168.9 - '@tanstack/router-generator': 1.166.24 + '@tanstack/router-core': 1.168.15 + '@tanstack/router-generator': 1.166.32 '@tanstack/router-utils': 1.161.6 '@tanstack/virtual-file-routes': 1.161.7 chokidar: 3.6.0 unplugin: 2.3.11 zod: 3.25.76 optionalDependencies: - '@tanstack/react-router': 1.168.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tanstack/react-router': 1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5) 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) transitivePeerDependencies: - supports-color @@ -11690,19 +11758,19 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-zod-adapter@1.81.5(@tanstack/react-router@1.168.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(zod@4.3.6)': + '@tanstack/router-zod-adapter@1.81.5(@tanstack/react-router@1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(zod@4.3.6)': dependencies: - '@tanstack/react-router': 1.168.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tanstack/react-router': 1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5) zod: 4.3.6 '@tanstack/store@0.9.3': {} '@tanstack/table-core@8.21.3': {} - '@tanstack/virtual-core@3.13.23': {} - '@tanstack/virtual-core@3.13.9': {} + '@tanstack/virtual-core@3.14.0': {} + '@tanstack/virtual-file-routes@1.161.7': {} '@taplo/core@0.2.0': {} @@ -11792,7 +11860,7 @@ snapshots: dependencies: '@tauri-apps/api': 2.10.1 - '@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.8.2)': + '@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.8.3)': dependencies: '@babel/generator': 7.17.7 '@babel/parser': 7.29.0 @@ -11800,7 +11868,7 @@ snapshots: '@babel/types': 7.17.0 javascript-natural-sort: 0.7.1 lodash: 4.17.21 - prettier: 3.8.2 + prettier: 3.8.3 transitivePeerDependencies: - supports-color optional: true @@ -12439,10 +12507,6 @@ snapshots: array-timsort@1.0.3: {} - ast-types@0.16.1: - dependencies: - tslib: 2.8.1 - astral-regex@2.0.0: {} async-mutex@0.3.2: @@ -12451,10 +12515,10 @@ snapshots: async@3.2.6: {} - autoprefixer@10.4.27(postcss@8.5.10): + autoprefixer@10.5.0(postcss@8.5.10): dependencies: - browserslist: 4.28.1 - caniuse-lite: 1.0.30001776 + browserslist: 4.28.2 + caniuse-lite: 1.0.30001788 fraction.js: 5.3.4 picocolors: 1.1.1 postcss: 8.5.10 @@ -12505,6 +12569,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.10.20: {} + baseline-browser-mapping@2.9.9: {} before-after-hook@2.2.3: {} @@ -12552,6 +12618,14 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.20 + caniuse-lite: 1.0.30001788 + electron-to-chromium: 1.5.340 + node-releases: 2.0.37 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + buffer-from@1.1.2: {} buffer@6.0.3: @@ -12588,6 +12662,8 @@ snapshots: caniuse-lite@1.0.30001776: {} + caniuse-lite@1.0.30001788: {} + ccount@2.0.1: {} chalk@4.1.2: @@ -12734,7 +12810,7 @@ snapshots: convert-source-map@2.0.0: {} - cookie-es@2.0.0: {} + cookie-es@3.1.1: {} copy-anything@2.0.6: dependencies: @@ -13111,6 +13187,8 @@ snapshots: electron-to-chromium@1.5.267: {} + electron-to-chromium@1.5.340: {} + emoji-regex@10.4.0: {} emoji-regex@8.0.0: {} @@ -13393,7 +13471,7 @@ snapshots: globals@17.4.0: {} - globby@16.1.1: + globby@16.2.0: dependencies: '@sindresorhus/merge-streams': 4.0.0 fast-glob: 3.3.3 @@ -14221,7 +14299,7 @@ snapshots: monaco-types: 0.1.0 monaco-worker-manager: 2.0.1(monaco-editor@0.55.1) path-browserify: 1.0.1 - prettier: 3.8.2 + prettier: 3.8.3 vscode-languageserver-textdocument: 1.0.12 vscode-languageserver-types: 3.17.5 vscode-uri: 3.0.8 @@ -14300,6 +14378,8 @@ snapshots: node-releases@2.0.27: {} + node-releases@2.0.37: {} + normalize-path@3.0.0: {} npm-normalize-package-bin@4.0.0: {} @@ -14390,25 +14470,30 @@ snapshots: '@oxc-parser/binding-win32-ia32-msvc': 0.121.0 '@oxc-parser/binding-win32-x64-msvc': 0.121.0 - oxc-parser@0.99.0: + oxc-parser@0.125.0: dependencies: - '@oxc-project/types': 0.99.0 + '@oxc-project/types': 0.125.0 optionalDependencies: - '@oxc-parser/binding-android-arm64': 0.99.0 - '@oxc-parser/binding-darwin-arm64': 0.99.0 - '@oxc-parser/binding-darwin-x64': 0.99.0 - '@oxc-parser/binding-freebsd-x64': 0.99.0 - '@oxc-parser/binding-linux-arm-gnueabihf': 0.99.0 - '@oxc-parser/binding-linux-arm-musleabihf': 0.99.0 - '@oxc-parser/binding-linux-arm64-gnu': 0.99.0 - '@oxc-parser/binding-linux-arm64-musl': 0.99.0 - '@oxc-parser/binding-linux-riscv64-gnu': 0.99.0 - '@oxc-parser/binding-linux-s390x-gnu': 0.99.0 - '@oxc-parser/binding-linux-x64-gnu': 0.99.0 - '@oxc-parser/binding-linux-x64-musl': 0.99.0 - '@oxc-parser/binding-wasm32-wasi': 0.99.0 - '@oxc-parser/binding-win32-arm64-msvc': 0.99.0 - '@oxc-parser/binding-win32-x64-msvc': 0.99.0 + '@oxc-parser/binding-android-arm-eabi': 0.125.0 + '@oxc-parser/binding-android-arm64': 0.125.0 + '@oxc-parser/binding-darwin-arm64': 0.125.0 + '@oxc-parser/binding-darwin-x64': 0.125.0 + '@oxc-parser/binding-freebsd-x64': 0.125.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.125.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.125.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.125.0 + '@oxc-parser/binding-linux-arm64-musl': 0.125.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.125.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.125.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.125.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.125.0 + '@oxc-parser/binding-linux-x64-gnu': 0.125.0 + '@oxc-parser/binding-linux-x64-musl': 0.125.0 + '@oxc-parser/binding-openharmony-arm64': 0.125.0 + '@oxc-parser/binding-wasm32-wasi': 0.125.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.125.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.125.0 + '@oxc-parser/binding-win32-x64-msvc': 0.125.0 oxc-resolver@11.19.1: optionalDependencies: @@ -14625,28 +14710,28 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prettier-plugin-ember-template-tag@2.1.4(prettier@3.8.2): + prettier-plugin-ember-template-tag@2.1.5(prettier@3.8.3): dependencies: '@babel/traverse': 7.29.0 content-tag: 4.0.0 - prettier: 3.8.2 + prettier: 3.8.3 transitivePeerDependencies: - supports-color - prettier-plugin-tailwindcss@0.7.2(@ianvs/prettier-plugin-sort-imports@4.7.1(@prettier/plugin-oxc@0.1.3)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.4(prettier@3.8.2))(prettier@3.8.2))(@prettier/plugin-oxc@0.1.3)(@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.8.2))(prettier@3.8.2): + prettier-plugin-tailwindcss@0.7.2(@ianvs/prettier-plugin-sort-imports@4.7.1(@prettier/plugin-oxc@0.1.4)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.5(prettier@3.8.3))(prettier@3.8.3))(@prettier/plugin-oxc@0.1.4)(@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.8.3))(prettier@3.8.3): dependencies: - prettier: 3.8.2 + prettier: 3.8.3 optionalDependencies: - '@ianvs/prettier-plugin-sort-imports': 4.7.1(@prettier/plugin-oxc@0.1.3)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.4(prettier@3.8.2))(prettier@3.8.2) - '@prettier/plugin-oxc': 0.1.3 - '@trivago/prettier-plugin-sort-imports': 4.3.0(prettier@3.8.2) + '@ianvs/prettier-plugin-sort-imports': 4.7.1(@prettier/plugin-oxc@0.1.4)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.5(prettier@3.8.3))(prettier@3.8.3) + '@prettier/plugin-oxc': 0.1.4 + '@trivago/prettier-plugin-sort-imports': 4.3.0(prettier@3.8.3) - prettier-plugin-toml@2.0.6(prettier@3.8.2): + prettier-plugin-toml@2.0.6(prettier@3.8.3): dependencies: '@taplo/lib': 0.5.0 - prettier: 3.8.2 + prettier: 3.8.3 - prettier@3.8.2: {} + prettier@3.8.3: {} prop-types@15.8.1: dependencies: @@ -14885,14 +14970,6 @@ snapshots: real-cancellable-promise@1.2.0: {} - recast@0.23.11: - dependencies: - ast-types: 0.16.1 - esprima: 4.0.1 - source-map: 0.6.1 - tiny-invariant: 1.3.3 - tslib: 2.8.1 - regenerate-unicode-properties@10.2.0: dependencies: regenerate: 1.4.2 @@ -15163,11 +15240,11 @@ snapshots: semver@7.7.4: {} - seroval-plugins@1.4.2(seroval@1.4.2): + seroval-plugins@1.5.2(seroval@1.5.2): dependencies: - seroval: 1.4.2 + seroval: 1.5.2 - seroval@1.4.2: {} + seroval@1.5.2: {} set-harmonic-interval@1.0.1: {} @@ -15224,8 +15301,8 @@ snapshots: solid-js@1.9.5: dependencies: csstype: 3.2.3 - seroval: 1.4.2 - seroval-plugins: 1.4.2(seroval@1.4.2) + seroval: 1.5.2 + seroval-plugins: 1.5.2(seroval@1.5.2) optional: true source-map-js@1.2.1: {} @@ -15327,36 +15404,36 @@ snapshots: dependencies: inline-style-parser: 0.2.3 - stylelint-config-html@1.1.0(postcss-html@1.8.1)(stylelint@17.6.0(typescript@5.9.3)): + stylelint-config-html@1.1.0(postcss-html@1.8.1)(stylelint@17.8.0(typescript@5.9.3)): dependencies: postcss-html: 1.8.1 - stylelint: 17.6.0(typescript@5.9.3) + stylelint: 17.8.0(typescript@5.9.3) - stylelint-config-recess-order@7.7.0(stylelint-order@8.1.1(stylelint@17.6.0(typescript@5.9.3)))(stylelint@17.6.0(typescript@5.9.3)): + stylelint-config-recess-order@7.7.0(stylelint-order@8.1.1(stylelint@17.8.0(typescript@5.9.3)))(stylelint@17.8.0(typescript@5.9.3)): dependencies: - stylelint: 17.6.0(typescript@5.9.3) - stylelint-order: 8.1.1(stylelint@17.6.0(typescript@5.9.3)) + stylelint: 17.8.0(typescript@5.9.3) + stylelint-order: 8.1.1(stylelint@17.8.0(typescript@5.9.3)) - stylelint-config-recommended@18.0.0(stylelint@17.6.0(typescript@5.9.3)): + stylelint-config-recommended@18.0.0(stylelint@17.8.0(typescript@5.9.3)): dependencies: - stylelint: 17.6.0(typescript@5.9.3) + stylelint: 17.8.0(typescript@5.9.3) - stylelint-config-standard@40.0.0(stylelint@17.6.0(typescript@5.9.3)): + stylelint-config-standard@40.0.0(stylelint@17.8.0(typescript@5.9.3)): dependencies: - stylelint: 17.6.0(typescript@5.9.3) - stylelint-config-recommended: 18.0.0(stylelint@17.6.0(typescript@5.9.3)) + stylelint: 17.8.0(typescript@5.9.3) + stylelint-config-recommended: 18.0.0(stylelint@17.8.0(typescript@5.9.3)) - stylelint-declaration-block-no-ignored-properties@3.0.0(stylelint@17.6.0(typescript@5.9.3)): + stylelint-declaration-block-no-ignored-properties@3.0.0(stylelint@17.8.0(typescript@5.9.3)): dependencies: - stylelint: 17.6.0(typescript@5.9.3) + stylelint: 17.8.0(typescript@5.9.3) - stylelint-order@8.1.1(stylelint@17.6.0(typescript@5.9.3)): + stylelint-order@8.1.1(stylelint@17.8.0(typescript@5.9.3)): dependencies: postcss: 8.5.10 postcss-sorting: 10.0.0(postcss@8.5.10) - stylelint: 17.6.0(typescript@5.9.3) + stylelint: 17.8.0(typescript@5.9.3) - stylelint-scss@7.0.0(stylelint@17.6.0(typescript@5.9.3)): + stylelint-scss@7.0.0(stylelint@17.8.0(typescript@5.9.3)): dependencies: css-tree: 3.1.0 is-plain-object: 5.0.0 @@ -15366,13 +15443,13 @@ snapshots: postcss-resolve-nested-selector: 0.1.6 postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - stylelint: 17.6.0(typescript@5.9.3) + stylelint: 17.8.0(typescript@5.9.3) - stylelint@17.6.0(typescript@5.9.3): + stylelint@17.8.0(typescript@5.9.3): dependencies: '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-syntax-patches-for-csstree': 1.1.1(css-tree@3.2.1) + '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) '@csstools/css-tokenizer': 4.0.0 '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1) @@ -15386,7 +15463,7 @@ snapshots: fastest-levenshtein: 1.0.16 file-entry-cache: 11.1.2 global-modules: 2.0.0 - globby: 16.1.1 + globby: 16.2.0 globjoin: 0.1.4 html-tags: 5.1.0 ignore: 7.0.5 @@ -15513,8 +15590,6 @@ snapshots: throttle-debounce@3.0.1: {} - tiny-invariant@1.3.3: {} - tinyexec@1.0.2: {} tinyexec@1.0.4: {} @@ -15733,6 +15808,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -15848,11 +15929,11 @@ 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.10)(prettier@3.8.2)(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.10)(prettier@3.8.3)(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.10 postcss-js: 4.0.1(postcss@8.5.10) - prettier: 3.8.2 + prettier: 3.8.3 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) diff --git a/filebrowser/.github/workflows/docs.yml b/filebrowser/.github/workflows/docs.yml index 4e7c4da4e7..fcc612c94a 100644 --- a/filebrowser/.github/workflows/docs.yml +++ b/filebrowser/.github/workflows/docs.yml @@ -45,7 +45,7 @@ jobs: - name: Build site run: task docs - name: Upload static files as artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@v5 with: path: www/public - name: Deploy to GitHub Pages diff --git a/filebrowser/compose.yaml b/filebrowser/compose.yaml index 33bd8dc6ce..0865c60f51 100644 --- a/filebrowser/compose.yaml +++ b/filebrowser/compose.yaml @@ -9,7 +9,7 @@ services: volumes: - filebrowser:/flux/vault environment: - - REDIS_CACHE_URL=redis://default:filebrowser@redis:6379 # Use rediss:// for ssl + - FB_REDIS_CACHE_URL=redis://default:filebrowser@redis:6379 # Use rediss:// for ssl redis: container_name: redis diff --git a/filebrowser/frontend/pnpm-lock.yaml b/filebrowser/frontend/pnpm-lock.yaml index e027a36c07..a1c78cf9b5 100644 --- a/filebrowser/frontend/pnpm-lock.yaml +++ b/filebrowser/frontend/pnpm-lock.yaml @@ -28,7 +28,7 @@ importers: version: 1.11.20 dompurify: specifier: ^3.2.6 - version: 3.3.3 + version: 3.4.0 epubjs: specifier: ^0.3.93 version: 0.3.93 @@ -101,7 +101,7 @@ importers: devDependencies: '@intlify/unplugin-vue-i18n': specifier: ^11.0.1 - version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.0)(rollup@4.57.1)(typescript@5.9.3)(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) + version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1)(rollup@4.57.1)(typescript@5.9.3)(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) '@tsconfig/node24': specifier: ^24.0.2 version: 24.0.4 @@ -113,43 +113,43 @@ importers: version: 24.12.2 '@typescript-eslint/eslint-plugin': specifier: ^8.37.0 - version: 8.58.1(@typescript-eslint/parser@8.56.0(eslint@10.2.0)(typescript@5.9.3))(eslint@10.2.0)(typescript@5.9.3) + version: 8.58.2(@typescript-eslint/parser@8.56.0(eslint@10.2.1)(typescript@5.9.3))(eslint@10.2.1)(typescript@5.9.3) '@vitejs/plugin-legacy': specifier: ^8.0.0 version: 8.0.1(terser@5.46.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.3)(terser@5.46.1)(yaml@2.8.3)) '@vitejs/plugin-vue': specifier: ^6.0.1 - version: 6.0.5(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.3)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)) + version: 6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.3)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)) '@vue/eslint-config-prettier': specifier: ^10.2.0 - version: 10.2.0(eslint@10.2.0)(prettier@3.8.2) + version: 10.2.0(eslint@10.2.1)(prettier@3.8.3) '@vue/eslint-config-typescript': specifier: ^14.6.0 - version: 14.7.0(eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.56.0(eslint@10.2.0)(typescript@5.9.3))(eslint@10.2.0)(vue-eslint-parser@10.4.0(eslint@10.2.0)))(eslint@10.2.0)(typescript@5.9.3) + version: 14.7.0(eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.56.0(eslint@10.2.1)(typescript@5.9.3))(eslint@10.2.1)(vue-eslint-parser@10.4.0(eslint@10.2.1)))(eslint@10.2.1)(typescript@5.9.3) '@vue/tsconfig': specifier: ^0.9.0 version: 0.9.1(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)) autoprefixer: specifier: ^10.4.21 - version: 10.4.27(postcss@8.5.9) + version: 10.5.0(postcss@8.5.10) eslint: specifier: ^10.0.0 - version: 10.2.0 + version: 10.2.1 eslint-config-prettier: specifier: ^10.1.5 - version: 10.1.8(eslint@10.2.0) + version: 10.1.8(eslint@10.2.1) eslint-plugin-prettier: specifier: ^5.5.1 - version: 5.5.5(eslint-config-prettier@10.1.8(eslint@10.2.0))(eslint@10.2.0)(prettier@3.8.2) + version: 5.5.5(eslint-config-prettier@10.1.8(eslint@10.2.1))(eslint@10.2.1)(prettier@3.8.3) eslint-plugin-vue: specifier: ^10.5.1 - version: 10.8.0(@typescript-eslint/parser@8.56.0(eslint@10.2.0)(typescript@5.9.3))(eslint@10.2.0)(vue-eslint-parser@10.4.0(eslint@10.2.0)) + version: 10.8.0(@typescript-eslint/parser@8.56.0(eslint@10.2.1)(typescript@5.9.3))(eslint@10.2.1)(vue-eslint-parser@10.4.0(eslint@10.2.1)) postcss: specifier: ^8.5.6 - version: 8.5.9 + version: 8.5.10 prettier: specifier: ^3.6.2 - version: 3.8.2 + version: 3.8.3 terser: specifier: ^5.43.1 version: 5.46.1 @@ -1022,12 +1022,16 @@ packages: resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': @@ -1240,12 +1244,12 @@ packages: cpu: [x64] os: [win32] + '@rolldown/pluginutils@1.0.0-rc.13': + resolution: {integrity: sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==} + '@rolldown/pluginutils@1.0.0-rc.15': resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} - '@rolldown/pluginutils@1.0.0-rc.2': - resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} - '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -1444,11 +1448,11 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/eslint-plugin@8.58.1': - resolution: {integrity: sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==} + '@typescript-eslint/eslint-plugin@8.58.2': + resolution: {integrity: sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.58.1 + '@typescript-eslint/parser': ^8.58.2 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' @@ -1465,8 +1469,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.58.1': - resolution: {integrity: sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==} + '@typescript-eslint/project-service@8.58.2': + resolution: {integrity: sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' @@ -1475,8 +1479,8 @@ packages: resolution: {integrity: sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.58.1': - resolution: {integrity: sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==} + '@typescript-eslint/scope-manager@8.58.2': + resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/tsconfig-utils@8.56.0': @@ -1485,8 +1489,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/tsconfig-utils@8.58.1': - resolution: {integrity: sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==} + '@typescript-eslint/tsconfig-utils@8.58.2': + resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' @@ -1498,8 +1502,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.58.1': - resolution: {integrity: sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==} + '@typescript-eslint/type-utils@8.58.2': + resolution: {integrity: sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -1509,8 +1513,8 @@ packages: resolution: {integrity: sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.58.1': - resolution: {integrity: sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==} + '@typescript-eslint/types@8.58.2': + resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/typescript-estree@8.56.0': @@ -1519,8 +1523,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/typescript-estree@8.58.1': - resolution: {integrity: sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==} + '@typescript-eslint/typescript-estree@8.58.2': + resolution: {integrity: sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' @@ -1532,8 +1536,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.58.1': - resolution: {integrity: sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==} + '@typescript-eslint/utils@8.58.2': + resolution: {integrity: sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -1543,8 +1547,8 @@ packages: resolution: {integrity: sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.58.1': - resolution: {integrity: sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==} + '@typescript-eslint/visitor-keys@8.58.2': + resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@videojs/http-streaming@3.17.4': @@ -1567,8 +1571,8 @@ packages: terser: ^5.16.0 vite: ^8.0.0 - '@vitejs/plugin-vue@6.0.5': - resolution: {integrity: sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==} + '@vitejs/plugin-vue@6.0.6': + resolution: {integrity: sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1801,8 +1805,8 @@ packages: resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==} engines: {node: '>=20.19.0'} - autoprefixer@10.4.27: - resolution: {integrity: sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==} + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -1830,6 +1834,11 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + baseline-browser-mapping@2.10.19: + resolution: {integrity: sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==} + engines: {node: '>=6.0.0'} + hasBin: true + baseline-browser-mapping@2.9.19: resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true @@ -1840,8 +1849,8 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@2.0.3: - resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} brace-expansion@5.0.5: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} @@ -1863,11 +1872,16 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - caniuse-lite@1.0.30001774: - resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==} + caniuse-lite@1.0.30001788: + resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==} chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} @@ -1956,12 +1970,15 @@ packages: dom-walk@0.1.2: resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} - dompurify@3.3.3: - resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==} + dompurify@3.4.0: + resolution: {integrity: sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==} electron-to-chromium@1.5.286: resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} + electron-to-chromium@1.5.340: + resolution: {integrity: sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==} + entities@7.0.1: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} @@ -2052,8 +2069,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.2.0: - resolution: {integrity: sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==} + eslint@10.2.1: + resolution: {integrity: sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -2514,6 +2531,9 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + normalize.css@8.0.1: resolution: {integrity: sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==} @@ -2601,8 +2621,8 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.9: - resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} + postcss@8.5.10: + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -2613,8 +2633,8 @@ packages: resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} - prettier@3.8.2: - resolution: {integrity: sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==} + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} engines: {node: '>=14'} hasBin: true @@ -3939,9 +3959,9 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.2.0)': + '@eslint-community/eslint-utils@4.9.1(eslint@10.2.1)': dependencies: - eslint: 10.2.0 + eslint: 10.2.1 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -3969,13 +3989,18 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.7': + '@humanfs/core@0.19.2': dependencies: - '@humanfs/core': 0.19.1 + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 '@humanwhocodes/retry': 0.4.3 + '@humanfs/types@0.15.0': {} + '@humanwhocodes/module-importer@1.0.1': {} '@humanwhocodes/retry@0.4.3': {} @@ -4014,9 +4039,9 @@ snapshots: '@intlify/shared@11.3.2': {} - '@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.0)(rollup@4.57.1)(typescript@5.9.3)(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))': + '@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1)(rollup@4.57.1)(typescript@5.9.3)(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1) '@intlify/bundle-utils': 11.0.7(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3))) '@intlify/shared': 11.2.8 '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.2.8)(@vue/compiler-dom@3.5.32)(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) @@ -4143,9 +4168,9 @@ snapshots: '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': optional: true - '@rolldown/pluginutils@1.0.0-rc.15': {} + '@rolldown/pluginutils@1.0.0-rc.13': {} - '@rolldown/pluginutils@1.0.0-rc.2': {} + '@rolldown/pluginutils@1.0.0-rc.15': {} '@rollup/pluginutils@5.3.0(rollup@4.57.1)': dependencies: @@ -4271,15 +4296,15 @@ snapshots: '@types/web-bluetooth@0.0.21': {} - '@typescript-eslint/eslint-plugin@8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.2.0)(typescript@5.9.3))(eslint@10.2.0)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.2.1)(typescript@5.9.3))(eslint@10.2.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.56.0(eslint@10.2.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.0(eslint@10.2.1)(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.56.0 - '@typescript-eslint/type-utils': 8.56.0(eslint@10.2.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.0(eslint@10.2.0)(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.56.0(eslint@10.2.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.0(eslint@10.2.1)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.56.0 - eslint: 10.2.0 + eslint: 10.2.1 ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@5.9.3) @@ -4287,15 +4312,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.56.0(eslint@10.2.0)(typescript@5.9.3))(eslint@10.2.0)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.56.0(eslint@10.2.1)(typescript@5.9.3))(eslint@10.2.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.56.0(eslint@10.2.0)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.58.1 - '@typescript-eslint/type-utils': 8.58.1(eslint@10.2.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.58.1(eslint@10.2.0)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.58.1 - eslint: 10.2.0 + '@typescript-eslint/parser': 8.56.0(eslint@10.2.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.58.2 + '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.2(eslint@10.2.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.2 + eslint: 10.2.1 ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@5.9.3) @@ -4303,14 +4328,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.56.0(eslint@10.2.0)(typescript@5.9.3)': + '@typescript-eslint/parser@8.56.0(eslint@10.2.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.56.0 '@typescript-eslint/types': 8.56.0 '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.56.0 debug: 4.4.3 - eslint: 10.2.0 + eslint: 10.2.1 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -4324,10 +4349,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.58.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.58.2(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.9.3) - '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@5.9.3) + '@typescript-eslint/types': 8.58.2 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: @@ -4338,38 +4363,38 @@ snapshots: '@typescript-eslint/types': 8.56.0 '@typescript-eslint/visitor-keys': 8.56.0 - '@typescript-eslint/scope-manager@8.58.1': + '@typescript-eslint/scope-manager@8.58.2': dependencies: - '@typescript-eslint/types': 8.58.1 - '@typescript-eslint/visitor-keys': 8.58.1 + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/visitor-keys': 8.58.2 '@typescript-eslint/tsconfig-utils@8.56.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.58.1(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.58.2(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.56.0(eslint@10.2.0)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.56.0(eslint@10.2.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.56.0 '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.0(eslint@10.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.0(eslint@10.2.1)(typescript@5.9.3) debug: 4.4.3 - eslint: 10.2.0 + eslint: 10.2.1 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.58.1(eslint@10.2.0)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.58.2(eslint@10.2.1)(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.58.1 - '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.58.1(eslint@10.2.0)(typescript@5.9.3) + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.2(eslint@10.2.1)(typescript@5.9.3) debug: 4.4.3 - eslint: 10.2.0 + eslint: 10.2.1 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -4377,7 +4402,7 @@ snapshots: '@typescript-eslint/types@8.56.0': {} - '@typescript-eslint/types@8.58.1': {} + '@typescript-eslint/types@8.58.2': {} '@typescript-eslint/typescript-estree@8.56.0(typescript@5.9.3)': dependencies: @@ -4394,12 +4419,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.58.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.58.2(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.58.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.9.3) - '@typescript-eslint/types': 8.58.1 - '@typescript-eslint/visitor-keys': 8.58.1 + '@typescript-eslint/project-service': 8.58.2(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@5.9.3) + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/visitor-keys': 8.58.2 debug: 4.4.3 minimatch: 10.2.5 semver: 7.7.4 @@ -4409,24 +4434,24 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.56.0(eslint@10.2.0)(typescript@5.9.3)': + '@typescript-eslint/utils@8.56.0(eslint@10.2.1)(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1) '@typescript-eslint/scope-manager': 8.56.0 '@typescript-eslint/types': 8.56.0 '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.3) - eslint: 10.2.0 + eslint: 10.2.1 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.58.1(eslint@10.2.0)(typescript@5.9.3)': + '@typescript-eslint/utils@8.58.2(eslint@10.2.1)(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) - '@typescript-eslint/scope-manager': 8.58.1 - '@typescript-eslint/types': 8.58.1 - '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3) - eslint: 10.2.0 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1) + '@typescript-eslint/scope-manager': 8.58.2 + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3) + eslint: 10.2.1 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -4436,9 +4461,9 @@ snapshots: '@typescript-eslint/types': 8.56.0 eslint-visitor-keys: 5.0.1 - '@typescript-eslint/visitor-keys@8.58.1': + '@typescript-eslint/visitor-keys@8.58.2': dependencies: - '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/types': 8.58.2 eslint-visitor-keys: 5.0.1 '@videojs/http-streaming@3.17.4(video.js@8.23.7)': @@ -4482,9 +4507,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@6.0.5(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.3)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.3)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))': dependencies: - '@rolldown/pluginutils': 1.0.0-rc.2 + '@rolldown/pluginutils': 1.0.0-rc.13 vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.3)(terser@5.46.1)(yaml@2.8.3) vue: 3.5.32(typescript@5.9.3) @@ -4573,7 +4598,7 @@ snapshots: '@vue/shared': 3.5.32 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.9 + postcss: 8.5.10 source-map-js: 1.2.1 '@vue/compiler-ssr@3.5.32': @@ -4614,23 +4639,23 @@ snapshots: '@vue/devtools-shared@8.1.1': {} - '@vue/eslint-config-prettier@10.2.0(eslint@10.2.0)(prettier@3.8.2)': + '@vue/eslint-config-prettier@10.2.0(eslint@10.2.1)(prettier@3.8.3)': dependencies: - eslint: 10.2.0 - eslint-config-prettier: 10.1.8(eslint@10.2.0) - eslint-plugin-prettier: 5.5.5(eslint-config-prettier@10.1.8(eslint@10.2.0))(eslint@10.2.0)(prettier@3.8.2) - prettier: 3.8.2 + eslint: 10.2.1 + eslint-config-prettier: 10.1.8(eslint@10.2.1) + eslint-plugin-prettier: 5.5.5(eslint-config-prettier@10.1.8(eslint@10.2.1))(eslint@10.2.1)(prettier@3.8.3) + prettier: 3.8.3 transitivePeerDependencies: - '@types/eslint' - '@vue/eslint-config-typescript@14.7.0(eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.56.0(eslint@10.2.0)(typescript@5.9.3))(eslint@10.2.0)(vue-eslint-parser@10.4.0(eslint@10.2.0)))(eslint@10.2.0)(typescript@5.9.3)': + '@vue/eslint-config-typescript@14.7.0(eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.56.0(eslint@10.2.1)(typescript@5.9.3))(eslint@10.2.1)(vue-eslint-parser@10.4.0(eslint@10.2.1)))(eslint@10.2.1)(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.56.0(eslint@10.2.0)(typescript@5.9.3) - eslint: 10.2.0 - eslint-plugin-vue: 10.8.0(@typescript-eslint/parser@8.56.0(eslint@10.2.0)(typescript@5.9.3))(eslint@10.2.0)(vue-eslint-parser@10.4.0(eslint@10.2.0)) + '@typescript-eslint/utils': 8.56.0(eslint@10.2.1)(typescript@5.9.3) + eslint: 10.2.1 + eslint-plugin-vue: 10.8.0(@typescript-eslint/parser@8.56.0(eslint@10.2.1)(typescript@5.9.3))(eslint@10.2.1)(vue-eslint-parser@10.4.0(eslint@10.2.1)) fast-glob: 3.3.3 - typescript-eslint: 8.56.0(eslint@10.2.0)(typescript@5.9.3) - vue-eslint-parser: 10.4.0(eslint@10.2.0) + typescript-eslint: 8.56.0(eslint@10.2.1)(typescript@5.9.3) + vue-eslint-parser: 10.4.0(eslint@10.2.1) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -4737,13 +4762,13 @@ snapshots: '@babel/parser': 7.29.2 ast-kit: 2.2.0 - autoprefixer@10.4.27(postcss@8.5.9): + autoprefixer@10.5.0(postcss@8.5.10): dependencies: - browserslist: 4.28.1 - caniuse-lite: 1.0.30001774 + browserslist: 4.28.2 + caniuse-lite: 1.0.30001788 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.9 + postcss: 8.5.10 postcss-value-parser: 4.2.0 babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.0): @@ -4774,13 +4799,15 @@ snapshots: balanced-match@4.0.4: {} + baseline-browser-mapping@2.10.19: {} + baseline-browser-mapping@2.9.19: {} birpc@2.9.0: {} boolbase@1.0.0: {} - brace-expansion@2.0.3: + brace-expansion@2.1.0: dependencies: balanced-match: 1.0.2 @@ -4800,14 +4827,22 @@ snapshots: browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001774 + caniuse-lite: 1.0.30001788 electron-to-chromium: 1.5.286 node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.19 + caniuse-lite: 1.0.30001788 + electron-to-chromium: 1.5.340 + node-releases: 2.0.37 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + buffer-from@1.1.2: {} - caniuse-lite@1.0.30001774: {} + caniuse-lite@1.0.30001788: {} chai@6.2.2: {} @@ -4875,12 +4910,14 @@ snapshots: dom-walk@0.1.2: {} - dompurify@3.3.3: + dompurify@3.4.0: optionalDependencies: '@types/trusted-types': 2.0.7 electron-to-chromium@1.5.286: {} + electron-to-chromium@1.5.340: {} + entities@7.0.1: {} epubjs@0.3.93: @@ -4986,31 +5023,31 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-prettier@10.1.8(eslint@10.2.0): + eslint-config-prettier@10.1.8(eslint@10.2.1): dependencies: - eslint: 10.2.0 + eslint: 10.2.1 - eslint-plugin-prettier@5.5.5(eslint-config-prettier@10.1.8(eslint@10.2.0))(eslint@10.2.0)(prettier@3.8.2): + eslint-plugin-prettier@5.5.5(eslint-config-prettier@10.1.8(eslint@10.2.1))(eslint@10.2.1)(prettier@3.8.3): dependencies: - eslint: 10.2.0 - prettier: 3.8.2 + eslint: 10.2.1 + prettier: 3.8.3 prettier-linter-helpers: 1.0.1 synckit: 0.11.12 optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@10.2.0) + eslint-config-prettier: 10.1.8(eslint@10.2.1) - eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.56.0(eslint@10.2.0)(typescript@5.9.3))(eslint@10.2.0)(vue-eslint-parser@10.4.0(eslint@10.2.0)): + eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.56.0(eslint@10.2.1)(typescript@5.9.3))(eslint@10.2.1)(vue-eslint-parser@10.4.0(eslint@10.2.1)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) - eslint: 10.2.0 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1) + eslint: 10.2.1 natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 7.1.1 semver: 7.7.4 - vue-eslint-parser: 10.4.0(eslint@10.2.0) + vue-eslint-parser: 10.4.0(eslint@10.2.1) xml-name-validator: 4.0.0 optionalDependencies: - '@typescript-eslint/parser': 8.56.0(eslint@10.2.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.0(eslint@10.2.1)(typescript@5.9.3) eslint-scope@9.1.2: dependencies: @@ -5023,15 +5060,15 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.2.0: + eslint@10.2.1: dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.5.5 '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.1 - '@humanfs/node': 0.16.7 + '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 @@ -5417,7 +5454,7 @@ snapshots: minimatch@9.0.9: dependencies: - brace-expansion: 2.0.3 + brace-expansion: 2.1.0 mitt@3.0.1: {} @@ -5452,6 +5489,8 @@ snapshots: node-releases@2.0.27: {} + node-releases@2.0.37: {} + normalize.css@8.0.1: {} nth-check@2.1.1: @@ -5531,7 +5570,7 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.5.9: + postcss@8.5.10: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -5543,7 +5582,7 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier@3.8.2: {} + prettier@3.8.3: {} pretty-bytes@7.1.0: {} @@ -5781,13 +5820,13 @@ snapshots: type@2.7.3: {} - typescript-eslint@8.56.0(eslint@10.2.0)(typescript@5.9.3): + typescript-eslint@8.56.0(eslint@10.2.1)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.2.0)(typescript@5.9.3))(eslint@10.2.0)(typescript@5.9.3) - '@typescript-eslint/parser': 8.56.0(eslint@10.2.0)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.2.1)(typescript@5.9.3))(eslint@10.2.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.0(eslint@10.2.1)(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.0(eslint@10.2.0)(typescript@5.9.3) - eslint: 10.2.0 + '@typescript-eslint/utils': 8.56.0(eslint@10.2.1)(typescript@5.9.3) + eslint: 10.2.1 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5833,6 +5872,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -5892,7 +5937,7 @@ snapshots: dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.9 + postcss: 8.5.10 rolldown: 1.0.0-rc.15 tinyglobby: 0.2.16 optionalDependencies: @@ -5931,10 +5976,10 @@ snapshots: vscode-uri@3.1.0: {} - vue-eslint-parser@10.4.0(eslint@10.2.0): + vue-eslint-parser@10.4.0(eslint@10.2.1): dependencies: debug: 4.4.3 - eslint: 10.2.0 + eslint: 10.2.1 eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 espree: 11.2.0 diff --git a/filebrowser/frontend/src/i18n/fr.json b/filebrowser/frontend/src/i18n/fr.json index ed40886b43..f5b9b5529f 100644 --- a/filebrowser/frontend/src/i18n/fr.json +++ b/filebrowser/frontend/src/i18n/fr.json @@ -119,7 +119,7 @@ "signup": "S'inscrire", "submit": "Se connecter", "username": "Utilisateur", - "usernameTaken": "Le nom d'utilisateur·ice est déjà pris", + "usernameTaken": "Le nom d'utilisateur est déjà pris", "wrongCredentials": "Identifiants incorrects !", "passwordTooShort": "Le mot de passe doit contenir au moins {min} caractères", "logout_reasons": { diff --git a/filebrowser/frontend/src/views/settings/Global.vue b/filebrowser/frontend/src/views/settings/Global.vue index 3d3912065e..b638711e33 100644 --- a/filebrowser/frontend/src/views/settings/Global.vue +++ b/filebrowser/frontend/src/views/settings/Global.vue @@ -70,7 +70,7 @@ {{ t("settings.documentation") }} @@ -209,7 +209,7 @@ {{ t("settings.documentation") }} diff --git a/hysteria/.github/workflows/test.yml b/hysteria/.github/workflows/test.yml index 1226250cad..b81ca55515 100644 --- a/hysteria/.github/workflows/test.yml +++ b/hysteria/.github/workflows/test.yml @@ -30,6 +30,12 @@ jobs: - name: Install Python dependencies run: pip install -r requirements.txt + - name: Install gofumpt + run: go install mvdan.cc/gofumpt@latest + + - name: Check format + run: python hyperbole.py format-check + - name: Test core working-directory: core run: go test -v -count=1 -skip 'Stress' ./... diff --git a/hysteria/extras/sniff/internal/quic/payload.go b/hysteria/extras/sniff/internal/quic/payload.go index 453b714a4c..14e80c3c6e 100644 --- a/hysteria/extras/sniff/internal/quic/payload.go +++ b/hysteria/extras/sniff/internal/quic/payload.go @@ -6,12 +6,18 @@ import ( "errors" "fmt" "io" + "math" "sort" "github.com/apernet/quic-go/quicvarint" "golang.org/x/crypto/hkdf" ) +const ( + maxCryptoFrameDataLen = 256 * 1024 // 256 KiB + maxCryptoPayloadLen = 256 * 1024 // 256 KiB +) + func ReadCryptoPayload(packet []byte) ([]byte, error) { hdr, offset, err := ParseInitialHeader(packet) if err != nil { @@ -82,11 +88,20 @@ func extractCryptoFrames(r *bytes.Reader) ([]cryptoFrame, error) { if err != nil { return nil, err } + if offset > uint64(math.MaxInt64) { + return nil, errors.New("invalid crypto frame offset") + } frame.Offset = int64(offset) dataLen, err := quicvarint.Read(r) if err != nil { return nil, err } + if dataLen > maxCryptoFrameDataLen { + return nil, errors.New("crypto frame data too large") + } + if dataLen > uint64(r.Len()) { + return nil, io.ErrUnexpectedEOF + } frame.Data = make([]byte, dataLen) if _, err := io.ReadFull(r, frame.Data); err != nil { return nil, err @@ -114,7 +129,18 @@ func assembleCryptoFrames(frames []cryptoFrame) []byte { } } // concatenate the frames - data := make([]byte, frames[len(frames)-1].Offset+int64(len(frames[len(frames)-1].Data))) + last := frames[len(frames)-1] + if last.Offset < 0 { + return nil + } + if last.Offset > maxCryptoPayloadLen { + return nil + } + end := last.Offset + int64(len(last.Data)) + if end < 0 || end > maxCryptoPayloadLen { + return nil + } + data := make([]byte, end) for _, frame := range frames { copy(data[frame.Offset:], frame.Data) } diff --git a/hysteria/hyperbole.py b/hysteria/hyperbole.py index 13dcd0a738..1755788fa9 100755 --- a/hysteria/hyperbole.py +++ b/hysteria/hyperbole.py @@ -356,6 +356,25 @@ def cmd_format(): print("Failed to format code") +def cmd_format_check(): + if not check_command(["gofumpt", "-version"]): + print("gofumpt is not installed. Please install gofumpt and try again.") + sys.exit(1) + + try: + output = ( + subprocess.check_output(["gofumpt", "-l", "-extra", "."]).decode().strip() + ) + except Exception: + print("Failed to check code format") + sys.exit(1) + + if output: + print("The following files are not properly formatted:") + print(output) + sys.exit(1) + + def cmd_mockgen(): if not check_command(["mockery", "--version"]): print("mockery is not installed. Please install mockery and try again.") @@ -500,6 +519,9 @@ def main(): # Format p_cmd.add_parser("format", help="Format the code") + # Format check + p_cmd.add_parser("format-check", help="Check code format") + # Mockgen p_cmd.add_parser("mockgen", help="Generate mock interfaces") @@ -533,6 +555,8 @@ def main(): cmd_build(args.pprof, args.release, args.race) elif args.command == "format": cmd_format() + elif args.command == "format-check": + cmd_format_check() elif args.command == "mockgen": cmd_mockgen() elif args.command == "protogen": diff --git a/lede/tools/cmake/Makefile b/lede/tools/cmake/Makefile index 152d295cb3..2294427cc3 100644 --- a/lede/tools/cmake/Makefile +++ b/lede/tools/cmake/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=cmake -PKG_VERSION:=3.30.5 +PKG_VERSION:=3.31.5 PKG_VERSION_MAJOR:=$(word 1,$(subst ., ,$(PKG_VERSION))).$(word 2,$(subst ., ,$(PKG_VERSION))) PKG_RELEASE:=1 PKG_CPE_ID:=cpe:/a:kitware:cmake @@ -15,7 +15,7 @@ PKG_CPE_ID:=cpe:/a:kitware:cmake PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://github.com/Kitware/CMake/releases/download/v$(PKG_VERSION)/ \ https://cmake.org/files/v$(PKG_VERSION_MAJOR)/ -PKG_HASH:=9f55e1a40508f2f29b7e065fa08c29f82c402fa0402da839fffe64a25755a86d +PKG_HASH:=66fb53a145648be56b46fa9e8ccade3a4d0dfc92e401e52ce76bdad1fea43d27 HOST_BUILD_PARALLEL:=1 HOST_CONFIGURE_PARALLEL:=1 @@ -30,13 +30,8 @@ HOST_CONFIGURE_VARS += \ MAKE="$(STAGING_DIR_HOST)/bin/ninja" HOST_CONFIGURE_ARGS := \ - --no-debugger \ $(if $(MAKE_JOBSERVER),--parallel="$(MAKE_JOBSERVER)") \ --prefix="$(STAGING_DIR_HOST)" \ - --system-expat \ - --system-liblzma \ - --system-zlib \ - --system-zstd \ --generator=Ninja define Host/Compile/Default diff --git a/lede/tools/cmake/patches/100-no-testing.patch b/lede/tools/cmake/patches/100-no-testing.patch index 736a365e87..44452ce39e 100644 --- a/lede/tools/cmake/patches/100-no-testing.patch +++ b/lede/tools/cmake/patches/100-no-testing.patch @@ -11,15 +11,15 @@ # like vs9 or vs10 --- a/Modules/Dart.cmake +++ b/Modules/Dart.cmake -@@ -47,7 +47,7 @@ if(cmp0145 STREQUAL "") - message(AUTHOR_WARNING "${_cmp0145_warning}") - endif() +@@ -33,7 +33,7 @@ whether testing support should be enable + # + # -option(BUILD_TESTING "Build the testing tree." ON) +option(BUILD_TESTING "Build the testing tree." OFF) if(BUILD_TESTING) - # We only get here if a project already ran include(Dart), + find_package(Dart QUIET) --- a/Tests/Contracts/VTK/Dashboard.cmake.in +++ b/Tests/Contracts/VTK/Dashboard.cmake.in @@ -25,7 +25,7 @@ ctest_empty_binary_directory(${CTEST_BIN diff --git a/lede/tools/cmake/patches/110-liblzma.patch b/lede/tools/cmake/patches/110-liblzma.patch deleted file mode 100644 index 4b8ec0d33f..0000000000 --- a/lede/tools/cmake/patches/110-liblzma.patch +++ /dev/null @@ -1,17 +0,0 @@ ---- a/Modules/FindLibLZMA.cmake -+++ b/Modules/FindLibLZMA.cmake -@@ -61,7 +61,13 @@ The following variables are provided for - cmake_policy(PUSH) - cmake_policy(SET CMP0159 NEW) # file(STRINGS) with REGEX updates CMAKE_MATCH_ - --find_path(LIBLZMA_INCLUDE_DIR lzma.h ) -+if(UNIX) -+ find_package(PkgConfig QUIET) -+ pkg_search_module(PC_liblzma liblzma) -+endif() -+ -+find_path(LIBLZMA_INCLUDE_DIR lzma.h HINTS ${PC_liblzma_INCLUDEDIR} ${PC_liblzma_INCLUDE_DIRS}) -+find_library(LIBLZMA_LIBRARY NAMES lzma HINTS ${PC_liblzma_LIBDIR} ${PC_liblzma_LIBRARY_DIRS}) - if(NOT LIBLZMA_LIBRARY) - find_library(LIBLZMA_LIBRARY_RELEASE NAMES lzma liblzma NAMES_PER_DIR PATH_SUFFIXES lib) - find_library(LIBLZMA_LIBRARY_DEBUG NAMES lzmad liblzmad NAMES_PER_DIR PATH_SUFFIXES lib) diff --git a/lede/tools/cmake/patches/120-curl-fix-libressl-linking.patch b/lede/tools/cmake/patches/120-curl-fix-libressl-linking.patch deleted file mode 100644 index 19217343d1..0000000000 --- a/lede/tools/cmake/patches/120-curl-fix-libressl-linking.patch +++ /dev/null @@ -1,37 +0,0 @@ -From: Jo-Philipp Wich -Date: Wed, 11 Jan 2017 03:36:04 +0100 -Subject: [PATCH] cmcurl: link librt - -When cmake is linked against LibreSSL, there might be an indirect -dependency on librt on certain systems if LibreSSL's libcrypto uses -clock_gettime() from librt: - - [ 28%] Linking C executable LIBCURL - .../lib/libcrypto.a(getentropy_linux.o): In function `getentropy_fallback': - getentropy_linux.c:(.text+0x16d): undefined reference to `clock_gettime' - getentropy_linux.c:(.text+0x412): undefined reference to `clock_gettime' - collect2: error: ld returned 1 exit status - make[5]: *** [Utilities/cmcurl/LIBCURL] Error 1 - -Modify the cmcurl CMakeLists.txt to check for clock_gettime() in librt -and unconditionally link the rt library when the symbol is found. - -Signed-off-by: Jo-Philipp Wich ---- ---- a/Utilities/cmcurl/CMakeLists.txt -+++ b/Utilities/cmcurl/CMakeLists.txt -@@ -648,6 +648,14 @@ if(CURL_USE_OPENSSL) - endif() - set(SSL_ENABLED ON) - set(USE_OPENSSL ON) -+ check_library_exists("rt" clock_gettime "" HAVE_LIBRT) -+ if(HAVE_LIBRT) -+ list(APPEND OPENSSL_LIBRARIES rt) -+ endif() -+ check_library_exists("pthread" pthread_once "" HAVE_PTHREAD) -+ if(HAVE_PTHREAD) -+ list(APPEND OPENSSL_LIBRARIES pthread) -+ endif() - list(APPEND CURL_LIBS ${OPENSSL_LIBRARIES}) - include_directories(${OPENSSL_INCLUDE_DIR}) - diff --git a/lede/tools/cmake/patches/130-bootstrap_parallel_make_flag.patch b/lede/tools/cmake/patches/130-bootstrap_parallel_make_flag.patch index a2a873fcea..fb047747de 100644 --- a/lede/tools/cmake/patches/130-bootstrap_parallel_make_flag.patch +++ b/lede/tools/cmake/patches/130-bootstrap_parallel_make_flag.patch @@ -1,6 +1,6 @@ --- a/bootstrap +++ b/bootstrap -@@ -1509,7 +1509,10 @@ int main(){ printf("1%c", (char)0x0a); r +@@ -1421,7 +1421,10 @@ int main(){ printf("1%c", (char)0x0a); r ' > "test.c" cmake_original_make_flags="${cmake_make_flags}" if test "x${cmake_parallel_make}" != "x"; then diff --git a/lede/tools/cmake/patches/140-zlib.patch b/lede/tools/cmake/patches/140-zlib.patch index 17334a66e0..df03e1d888 100644 --- a/lede/tools/cmake/patches/140-zlib.patch +++ b/lede/tools/cmake/patches/140-zlib.patch @@ -1,6 +1,6 @@ --- a/Modules/FindZLIB.cmake +++ b/Modules/FindZLIB.cmake -@@ -120,10 +120,13 @@ else() +@@ -160,10 +160,13 @@ else() set(ZLIB_NAMES_DEBUG zd zlibd zdlld zlibd1 zlib1d zlibstaticd zlibwapid zlibvcd zlibstatd) endif() diff --git a/lede/tools/cmake/patches/150-zstd-libarchive.patch b/lede/tools/cmake/patches/150-zstd-libarchive.patch index a6f8e35e6b..6b73a3be23 100644 --- a/lede/tools/cmake/patches/150-zstd-libarchive.patch +++ b/lede/tools/cmake/patches/150-zstd-libarchive.patch @@ -1,6 +1,6 @@ --- a/Utilities/cmlibarchive/CMakeLists.txt +++ b/Utilities/cmlibarchive/CMakeLists.txt -@@ -656,7 +656,7 @@ IF(ENABLE_ZSTD) +@@ -669,7 +669,7 @@ IF(ENABLE_ZSTD) SET(ZSTD_FIND_QUIETLY TRUE) ENDIF (ZSTD_INCLUDE_DIR) diff --git a/lede/tools/cmake/patches/160-disable_xcode_generator.patch b/lede/tools/cmake/patches/160-disable_xcode_generator.patch index 0e29dedd9b..d4fc92560e 100644 --- a/lede/tools/cmake/patches/160-disable_xcode_generator.patch +++ b/lede/tools/cmake/patches/160-disable_xcode_generator.patch @@ -1,6 +1,6 @@ --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt -@@ -858,7 +858,7 @@ if(CMake_USE_XCOFF_PARSER) +@@ -903,7 +903,7 @@ if(CMake_USE_XCOFF_PARSER) endif() # Xcode only works on Apple @@ -11,8 +11,8 @@ PRIVATE --- a/Source/cmake.cxx +++ b/Source/cmake.cxx -@@ -134,7 +134,7 @@ - # include "cmGlobalGhsMultiGenerator.h" +@@ -143,7 +143,7 @@ + # endif #endif -#if defined(__APPLE__) diff --git a/mieru/Makefile b/mieru/Makefile index ab282357bf..9674df96e5 100644 --- a/mieru/Makefile +++ b/mieru/Makefile @@ -32,7 +32,7 @@ PROJECT_NAME=$(shell basename "${ROOT}") # - pkg/version/current.go # # Use `tools/bump_version.sh` script to change all those files at one shot. -VERSION="3.30.1" +VERSION="3.31.0" # With .ONESHELL, each recipe is executed in a single shell instance. # This allows `cd` to affect subsequent commands in the same recipe. diff --git a/mieru/build/package/mieru/amd64/debian/DEBIAN/control b/mieru/build/package/mieru/amd64/debian/DEBIAN/control index 10f349e959..aa92559f93 100755 --- a/mieru/build/package/mieru/amd64/debian/DEBIAN/control +++ b/mieru/build/package/mieru/amd64/debian/DEBIAN/control @@ -1,5 +1,5 @@ Package: mieru -Version: 3.30.1 +Version: 3.31.0 Section: net Priority: optional Architecture: amd64 diff --git a/mieru/build/package/mieru/amd64/rpm/mieru.spec b/mieru/build/package/mieru/amd64/rpm/mieru.spec index 4936c100b5..2cc4d1f0ff 100644 --- a/mieru/build/package/mieru/amd64/rpm/mieru.spec +++ b/mieru/build/package/mieru/amd64/rpm/mieru.spec @@ -1,5 +1,5 @@ Name: mieru -Version: 3.30.1 +Version: 3.31.0 Release: 1%{?dist} Summary: Mieru proxy client License: GPLv3+ diff --git a/mieru/build/package/mieru/arm64/debian/DEBIAN/control b/mieru/build/package/mieru/arm64/debian/DEBIAN/control index 5be2fea938..3f9bee66a0 100755 --- a/mieru/build/package/mieru/arm64/debian/DEBIAN/control +++ b/mieru/build/package/mieru/arm64/debian/DEBIAN/control @@ -1,5 +1,5 @@ Package: mieru -Version: 3.30.1 +Version: 3.31.0 Section: net Priority: optional Architecture: arm64 diff --git a/mieru/build/package/mieru/arm64/rpm/mieru.spec b/mieru/build/package/mieru/arm64/rpm/mieru.spec index 4936c100b5..2cc4d1f0ff 100644 --- a/mieru/build/package/mieru/arm64/rpm/mieru.spec +++ b/mieru/build/package/mieru/arm64/rpm/mieru.spec @@ -1,5 +1,5 @@ Name: mieru -Version: 3.30.1 +Version: 3.31.0 Release: 1%{?dist} Summary: Mieru proxy client License: GPLv3+ diff --git a/mieru/build/package/mita/amd64/debian/DEBIAN/control b/mieru/build/package/mita/amd64/debian/DEBIAN/control index 933f923b52..8c65c687b4 100755 --- a/mieru/build/package/mita/amd64/debian/DEBIAN/control +++ b/mieru/build/package/mita/amd64/debian/DEBIAN/control @@ -1,5 +1,5 @@ Package: mita -Version: 3.30.1 +Version: 3.31.0 Section: net Priority: optional Architecture: amd64 diff --git a/mieru/build/package/mita/amd64/rpm/mita.spec b/mieru/build/package/mita/amd64/rpm/mita.spec index 4743facb5d..5acb760186 100644 --- a/mieru/build/package/mita/amd64/rpm/mita.spec +++ b/mieru/build/package/mita/amd64/rpm/mita.spec @@ -1,5 +1,5 @@ Name: mita -Version: 3.30.1 +Version: 3.31.0 Release: 1%{?dist} Summary: Mieru proxy server License: GPLv3+ diff --git a/mieru/build/package/mita/arm64/debian/DEBIAN/control b/mieru/build/package/mita/arm64/debian/DEBIAN/control index 34365f4df5..c58aaf231b 100755 --- a/mieru/build/package/mita/arm64/debian/DEBIAN/control +++ b/mieru/build/package/mita/arm64/debian/DEBIAN/control @@ -1,5 +1,5 @@ Package: mita -Version: 3.30.1 +Version: 3.31.0 Section: net Priority: optional Architecture: arm64 diff --git a/mieru/build/package/mita/arm64/rpm/mita.spec b/mieru/build/package/mita/arm64/rpm/mita.spec index b043494516..1b62288f58 100644 --- a/mieru/build/package/mita/arm64/rpm/mita.spec +++ b/mieru/build/package/mita/arm64/rpm/mita.spec @@ -1,5 +1,5 @@ Name: mita -Version: 3.30.1 +Version: 3.31.0 Release: 1%{?dist} Summary: Mieru proxy server License: GPLv3+ diff --git a/mieru/docs/protocol.md b/mieru/docs/protocol.md index bcf3ebc2e6..645928de0d 100644 --- a/mieru/docs/protocol.md +++ b/mieru/docs/protocol.md @@ -20,6 +20,8 @@ Since the key depends on the system time, the time difference between the client The mieru protocol allows the use of any [AEAD](https://en.wikipedia.org/wiki/Authenticated_encryption) algorithm for encryption. The nonce length of the AEAD algorithm must be 24 bytes. The current version of mieru only implements the XChaCha20-Poly1305 algorithm. +To accelerate user lookup, the last 4 bytes of the nonce is replace by the first 4 bytes of a SHA-256 output. The input of SHA-256 is user name concatenate by the first 16 bytes of the nonce. + ## Segment Format When mieru receives a network access request from a user, it divides the original data stream into small fragments and sends them to the Internet after encryption and encapsulation. The fields and their lengths in each segment are as shown in the following table: diff --git a/mieru/docs/protocol.zh_CN.md b/mieru/docs/protocol.zh_CN.md index e94df73c67..c01e44b024 100644 --- a/mieru/docs/protocol.zh_CN.md +++ b/mieru/docs/protocol.zh_CN.md @@ -20,6 +20,8 @@ TCP 和 UDP 协议共用同一套密钥生成方法。 mieru 协议允许使用任何 [AEAD](https://en.wikipedia.org/wiki/Authenticated_encryption) 算法进行加密。算法的 nonce 长度必须为 24 字节。当前 mieru 版本只实现了 XChaCha20-Poly1305 算法。 +为了加快用户查找,nonce 的最后 4 个字节被替换为 SHA-256 输出的前 4 个字节,其中 SHA-256 的输入是用户名再接上 nonce 的前 16 个字节。 + ## 数据段的格式 mieru 收到用户的网络访问请求后,会将原始数据流量切分成小段(fragment),经过加密封装发送到互联网上。每个数据段(segment)中的数据项(field)及其长度如下表所示。 diff --git a/mieru/docs/server-install.md b/mieru/docs/server-install.md index 78037d727b..29513d7608 100644 --- a/mieru/docs/server-install.md +++ b/mieru/docs/server-install.md @@ -18,32 +18,32 @@ Or you can manually install and configure proxy server using the steps below. ```sh # Debian / Ubuntu - X86_64 -curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.1/mita_3.30.1_amd64.deb +curl -LSO https://github.com/enfein/mieru/releases/download/v3.31.0/mita_3.31.0_amd64.deb # Debian / Ubuntu - ARM 64 -curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.1/mita_3.30.1_arm64.deb +curl -LSO https://github.com/enfein/mieru/releases/download/v3.31.0/mita_3.31.0_arm64.deb # RedHat / CentOS / Rocky Linux - X86_64 -curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.1/mita-3.30.1-1.x86_64.rpm +curl -LSO https://github.com/enfein/mieru/releases/download/v3.31.0/mita-3.31.0-1.x86_64.rpm # RedHat / CentOS / Rocky Linux - ARM 64 -curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.1/mita-3.30.1-1.aarch64.rpm +curl -LSO https://github.com/enfein/mieru/releases/download/v3.31.0/mita-3.31.0-1.aarch64.rpm ``` ## Install mita package ```sh # Debian / Ubuntu - X86_64 -sudo dpkg -i mita_3.30.1_amd64.deb +sudo dpkg -i mita_3.31.0_amd64.deb # Debian / Ubuntu - ARM 64 -sudo dpkg -i mita_3.30.1_arm64.deb +sudo dpkg -i mita_3.31.0_arm64.deb # RedHat / CentOS / Rocky Linux - X86_64 -sudo rpm -Uvh --force mita-3.30.1-1.x86_64.rpm +sudo rpm -Uvh --force mita-3.31.0-1.x86_64.rpm # RedHat / CentOS / Rocky Linux - ARM 64 -sudo rpm -Uvh --force mita-3.30.1-1.aarch64.rpm +sudo rpm -Uvh --force mita-3.31.0-1.aarch64.rpm ``` Those instructions can also be used to upgrade the version of mita software package. @@ -316,6 +316,20 @@ We can use the `users` -> `quotas` property to limit the amount of traffic a use } ``` +### User Hint + +Starting from v3.31.0 release, mieru client send user hint to accelerate the decryption of network packets in mita server. This is especially helpful when the number of proxy users is large. + +Because mita server will have higher CPU consumption when the user hint is not available, you can apply the following configuration to block old mieru clients. + +``` +{ + "advancedSettings": { + "userHintIsMandatory": true + } +} +``` + ## [Optional] Install NTP network time synchronization service The client and proxy server software calculate the key based on the user name, password and system time. The server can decrypt and respond to the client's request only if the client and server have the same key. This requires that the system time of the client and the server must be in sync. diff --git a/mieru/docs/server-install.zh_CN.md b/mieru/docs/server-install.zh_CN.md index e76f694a98..1369029a46 100644 --- a/mieru/docs/server-install.zh_CN.md +++ b/mieru/docs/server-install.zh_CN.md @@ -18,32 +18,32 @@ sudo python3 setup.py --lang=zh ```sh # Debian / Ubuntu - X86_64 -curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.1/mita_3.30.1_amd64.deb +curl -LSO https://github.com/enfein/mieru/releases/download/v3.31.0/mita_3.31.0_amd64.deb # Debian / Ubuntu - ARM 64 -curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.1/mita_3.30.1_arm64.deb +curl -LSO https://github.com/enfein/mieru/releases/download/v3.31.0/mita_3.31.0_arm64.deb # RedHat / CentOS / Rocky Linux - X86_64 -curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.1/mita-3.30.1-1.x86_64.rpm +curl -LSO https://github.com/enfein/mieru/releases/download/v3.31.0/mita-3.31.0-1.x86_64.rpm # RedHat / CentOS / Rocky Linux - ARM 64 -curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.1/mita-3.30.1-1.aarch64.rpm +curl -LSO https://github.com/enfein/mieru/releases/download/v3.31.0/mita-3.31.0-1.aarch64.rpm ``` ## 安装 mita 软件包 ```sh # Debian / Ubuntu - X86_64 -sudo dpkg -i mita_3.30.1_amd64.deb +sudo dpkg -i mita_3.31.0_amd64.deb # Debian / Ubuntu - ARM 64 -sudo dpkg -i mita_3.30.1_arm64.deb +sudo dpkg -i mita_3.31.0_arm64.deb # RedHat / CentOS / Rocky Linux - X86_64 -sudo rpm -Uvh --force mita-3.30.1-1.x86_64.rpm +sudo rpm -Uvh --force mita-3.31.0-1.x86_64.rpm # RedHat / CentOS / Rocky Linux - ARM 64 -sudo rpm -Uvh --force mita-3.30.1-1.aarch64.rpm +sudo rpm -Uvh --force mita-3.31.0-1.aarch64.rpm ``` 上述指令也可以用来升级 mita 软件包的版本。 @@ -316,6 +316,20 @@ Tor 浏览器 -> mieru 客户端 -> GFW -> mita 服务器 -> Tor 网络 -> 目 } ``` +### 用户提示 + +从 v3.31.0 版本开始,mieru 客户端会发送用户提示,以加速 mita 服务器对网络数据包的解密。代理用户数量较多时,这尤其有帮助。 + +由于在没有用户提示的情况下 mita 服务器的 CPU 消耗会更高,你可以应用以下配置来屏蔽旧版本的 mieru 客户端。 + +``` +{ + "advancedSettings": { + "userHintIsMandatory": true + } +} +``` + ## 【可选】安装 NTP 网络时间同步服务 客户端和代理服务器软件会根据用户名、密码和系统时间,分别计算密钥。只有当客户端和服务器的密钥相同时,服务器才能解密和响应客户端的请求。这要求客户端和服务器的系统时间不能有很大的差别。 diff --git a/mieru/pkg/replay/replay_test.go b/mieru/pkg/replay/replay_test.go index a76c46cf59..969fe305f3 100644 --- a/mieru/pkg/replay/replay_test.go +++ b/mieru/pkg/replay/replay_test.go @@ -153,7 +153,7 @@ func TestCapacity(t *testing.T) { } func TestExpireInterval(t *testing.T) { - cache := replay.NewCache(10, 50*time.Millisecond) + cache := replay.NewCache(10, 100*time.Millisecond) a := make([]byte, 256) if _, err := crand.Read(a); err != nil { t.Fatalf("rand.Read() failed: %v", err) @@ -170,7 +170,7 @@ func TestExpireInterval(t *testing.T) { t.Errorf("cache sizes are %d %d, want 1 0.", curr, prev) } - time.Sleep(75 * time.Millisecond) + time.Sleep(150 * time.Millisecond) if res := cache.IsDuplicate(a, replay.EmptyTag); res == false { t.Errorf("IsDuplicate() = false, want true") @@ -179,7 +179,7 @@ func TestExpireInterval(t *testing.T) { t.Errorf("cache sizes are %d %d, want 1 1.", curr, prev) } - time.Sleep(150 * time.Millisecond) + time.Sleep(200 * time.Millisecond) if res := cache.IsDuplicate(b, replay.EmptyTag); res == true { t.Errorf("IsDuplicate() = true, want false") diff --git a/mieru/pkg/version/current.go b/mieru/pkg/version/current.go index 940ef508a7..130dde1ba2 100644 --- a/mieru/pkg/version/current.go +++ b/mieru/pkg/version/current.go @@ -16,5 +16,5 @@ package version const ( - AppVersion = "3.30.1" + AppVersion = "3.31.0" ) diff --git a/mieru/test/deploy/singbox/singbox-server-config.json b/mieru/test/deploy/singbox/singbox-server-config.json index 1640d3a03f..8be47c23b1 100644 --- a/mieru/test/deploy/singbox/singbox-server-config.json +++ b/mieru/test/deploy/singbox/singbox-server-config.json @@ -11,7 +11,8 @@ "name": "baozi", "password": "manlianpenfen" } - ] + ], + "user_hint_is_mandatory": true }, { "type": "mieru", @@ -24,7 +25,8 @@ "name": "baozi", "password": "manlianpenfen" } - ] + ], + "user_hint_is_mandatory": true } ], "outbounds": [], diff --git a/mihomo/.github/workflows/trigger-cmfa-update.yml b/mihomo/.github/workflows/trigger-cmfa-update.yml index ed12c2a11c..35fd276cf3 100644 --- a/mihomo/.github/workflows/trigger-cmfa-update.yml +++ b/mihomo/.github/workflows/trigger-cmfa-update.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/create-github-app-token@v3 id: generate-token with: - app-id: ${{ secrets.MAINTAINER_APPID }} + client-id: ${{ secrets.MAINTAINER_APPID }} private-key: ${{ secrets.MAINTAINER_APP_PRIVATE_KEY }} owner: ${{ github.repository_owner }} diff --git a/mihomo/adapter/outbound/vless.go b/mihomo/adapter/outbound/vless.go index dbd822bbf7..5c294043da 100644 --- a/mihomo/adapter/outbound/vless.go +++ b/mihomo/adapter/outbound/vless.go @@ -85,6 +85,19 @@ type XHTTPOptions struct { Headers map[string]string `proxy:"headers,omitempty"` NoGRPCHeader bool `proxy:"no-grpc-header,omitempty"` XPaddingBytes string `proxy:"x-padding-bytes,omitempty"` + XPaddingObfsMode bool `proxy:"x-padding-obfs-mode,omitempty"` + XPaddingKey string `proxy:"x-padding-key,omitempty"` + XPaddingHeader string `proxy:"x-padding-header,omitempty"` + XPaddingPlacement string `proxy:"x-padding-placement,omitempty"` + XPaddingMethod string `proxy:"x-padding-method,omitempty"` + UplinkHTTPMethod string `proxy:"uplink-http-method,omitempty"` + SessionPlacement string `proxy:"session-placement,omitempty"` + SessionKey string `proxy:"session-key,omitempty"` + SeqPlacement string `proxy:"seq-placement,omitempty"` + SeqKey string `proxy:"seq-key,omitempty"` + UplinkDataPlacement string `proxy:"uplink-data-placement,omitempty"` + UplinkDataKey string `proxy:"uplink-data-key,omitempty"` + UplinkChunkSize string `proxy:"uplink-chunk-size,omitempty"` ScMaxEachPostBytes string `proxy:"sc-max-each-post-bytes,omitempty"` ScMinPostsIntervalMs string `proxy:"sc-min-posts-interval-ms,omitempty"` ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX @@ -107,6 +120,19 @@ type XHTTPDownloadSettings struct { Headers *map[string]string `proxy:"headers,omitempty"` NoGRPCHeader *bool `proxy:"no-grpc-header,omitempty"` XPaddingBytes *string `proxy:"x-padding-bytes,omitempty"` + XPaddingObfsMode *bool `proxy:"x-padding-obfs-mode,omitempty"` + XPaddingKey *string `proxy:"x-padding-key,omitempty"` + XPaddingHeader *string `proxy:"x-padding-header,omitempty"` + XPaddingPlacement *string `proxy:"x-padding-placement,omitempty"` + XPaddingMethod *string `proxy:"x-padding-method,omitempty"` + UplinkHTTPMethod *string `proxy:"uplink-http-method,omitempty"` + SessionPlacement *string `proxy:"session-placement,omitempty"` + SessionKey *string `proxy:"session-key,omitempty"` + SeqPlacement *string `proxy:"seq-placement,omitempty"` + SeqKey *string `proxy:"seq-key,omitempty"` + UplinkDataPlacement *string `proxy:"uplink-data-placement,omitempty"` + UplinkDataKey *string `proxy:"uplink-data-key,omitempty"` + UplinkChunkSize *string `proxy:"uplink-chunk-size,omitempty"` ScMaxEachPostBytes *string `proxy:"sc-max-each-post-bytes,omitempty"` ScMinPostsIntervalMs *string `proxy:"sc-min-posts-interval-ms,omitempty"` ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX @@ -552,6 +578,19 @@ func NewVless(option VlessOption) (*Vless, error) { Headers: v.option.XHTTPOpts.Headers, NoGRPCHeader: v.option.XHTTPOpts.NoGRPCHeader, XPaddingBytes: v.option.XHTTPOpts.XPaddingBytes, + XPaddingObfsMode: v.option.XHTTPOpts.XPaddingObfsMode, + XPaddingKey: v.option.XHTTPOpts.XPaddingKey, + XPaddingHeader: v.option.XHTTPOpts.XPaddingHeader, + XPaddingPlacement: v.option.XHTTPOpts.XPaddingPlacement, + XPaddingMethod: v.option.XHTTPOpts.XPaddingMethod, + UplinkHTTPMethod: v.option.XHTTPOpts.UplinkHTTPMethod, + SessionPlacement: v.option.XHTTPOpts.SessionPlacement, + SessionKey: v.option.XHTTPOpts.SessionKey, + SeqPlacement: v.option.XHTTPOpts.SeqPlacement, + SeqKey: v.option.XHTTPOpts.SeqKey, + UplinkDataPlacement: v.option.XHTTPOpts.UplinkDataPlacement, + UplinkDataKey: v.option.XHTTPOpts.UplinkDataKey, + UplinkChunkSize: v.option.XHTTPOpts.UplinkChunkSize, ScMaxEachPostBytes: v.option.XHTTPOpts.ScMaxEachPostBytes, ScMinPostsIntervalMs: v.option.XHTTPOpts.ScMinPostsIntervalMs, ReuseConfig: reuseCfg, @@ -667,6 +706,19 @@ func NewVless(option VlessOption) (*Vless, error) { 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), + XPaddingObfsMode: lo.FromPtrOr(ds.XPaddingObfsMode, v.option.XHTTPOpts.XPaddingObfsMode), + XPaddingKey: lo.FromPtrOr(ds.XPaddingKey, v.option.XHTTPOpts.XPaddingKey), + XPaddingHeader: lo.FromPtrOr(ds.XPaddingHeader, v.option.XHTTPOpts.XPaddingHeader), + XPaddingPlacement: lo.FromPtrOr(ds.XPaddingPlacement, v.option.XHTTPOpts.XPaddingPlacement), + XPaddingMethod: lo.FromPtrOr(ds.XPaddingMethod, v.option.XHTTPOpts.XPaddingMethod), + UplinkHTTPMethod: lo.FromPtrOr(ds.UplinkHTTPMethod, v.option.XHTTPOpts.UplinkHTTPMethod), + SessionPlacement: lo.FromPtrOr(ds.SessionPlacement, v.option.XHTTPOpts.SessionPlacement), + SessionKey: lo.FromPtrOr(ds.SessionKey, v.option.XHTTPOpts.SessionKey), + SeqPlacement: lo.FromPtrOr(ds.SeqPlacement, v.option.XHTTPOpts.SeqPlacement), + SeqKey: lo.FromPtrOr(ds.SeqKey, v.option.XHTTPOpts.SeqKey), + UplinkDataPlacement: lo.FromPtrOr(ds.UplinkDataPlacement, v.option.XHTTPOpts.UplinkDataPlacement), + UplinkDataKey: lo.FromPtrOr(ds.UplinkDataKey, v.option.XHTTPOpts.UplinkDataKey), + UplinkChunkSize: lo.FromPtrOr(ds.UplinkChunkSize, v.option.XHTTPOpts.UplinkChunkSize), ScMaxEachPostBytes: lo.FromPtrOr(ds.ScMaxEachPostBytes, v.option.XHTTPOpts.ScMaxEachPostBytes), ScMinPostsIntervalMs: lo.FromPtrOr(ds.ScMinPostsIntervalMs, v.option.XHTTPOpts.ScMinPostsIntervalMs), ReuseConfig: downloadReuseCfg, diff --git a/mihomo/docs/config.yaml b/mihomo/docs/config.yaml index 42f8ef4e8f..44711c33ed 100644 --- a/mihomo/docs/config.yaml +++ b/mihomo/docs/config.yaml @@ -825,6 +825,19 @@ proxies: # socks5 # X-Forwarded-For: "" # no-grpc-header: false # x-padding-bytes: "100-1000" + # x-padding-obfs-mode: false + # x-padding-key: x_padding + # x-padding-header: Referer + # x-padding-placement: queryInHeader # Available: queryInHeader, cookie, header, query + # x-padding-method: repeat-x # Available: repeat-x, tokenish + # uplink-http-method: POST # Available: POST, PUT, PATCH, DELETE + # session-placement: path # Available: path, query, cookie, header + # session-key: "" + # seq-placement: path # Available: path, query, cookie, header + # seq-key: "" + # uplink-data-placement: body # Available: body, cookie, header + # uplink-data-key: "" + # uplink-chunk-size: 0 # only applicable when uplink-data-placement is not body # sc-max-each-post-bytes: 1000000 # sc-min-posts-interval-ms: 30 # reuse-settings: # aka XMUX @@ -842,6 +855,19 @@ proxies: # socks5 # X-Forwarded-For: "" # no-grpc-header: false # x-padding-bytes: "100-1000" + # x-padding-obfs-mode: false + # x-padding-key: x_padding + # x-padding-header: Referer + # x-padding-placement: queryInHeader # Available: queryInHeader, cookie, header, query + # x-padding-method: repeat-x # Available: repeat-x, tokenish + # uplink-http-method: POST # Available: POST, PUT, PATCH, DELETE + # session-placement: path # Available: path, query, cookie, header + # session-key: "" + # seq-placement: path # Available: path, query, cookie, header + # seq-key: "" + # uplink-data-placement: body # Available: body, cookie, header + # uplink-data-key: "" + # uplink-chunk-size: 0 # only applicable when uplink-data-placement is not body # sc-max-each-post-bytes: 1000000 # sc-min-posts-interval-ms: 30 # reuse-settings: # aka XMUX @@ -1687,6 +1713,20 @@ listeners: # host: "" # mode: auto # Available: "stream-one", "stream-up" or "packet-up" # no-sse-header: false + # x-padding-bytes: "100-1000" + # x-padding-obfs-mode: false + # x-padding-key: x_padding + # x-padding-header: Referer + # x-padding-placement: queryInHeader # Available: queryInHeader, cookie, header, query + # x-padding-method: repeat-x # Available: repeat-x, tokenish + # uplink-http-method: POST # Available: POST, PUT, PATCH, DELETE + # session-placement: path # Available: path, query, cookie, header + # session-key: "" + # seq-placement: path # Available: path, query, cookie, header + # seq-key: "" + # uplink-data-placement: body # Available: body, cookie, header + # uplink-data-key: "" + # uplink-chunk-size: 0 # only applicable when uplink-data-placement is not body # sc-max-buffered-posts: 30 # sc-stream-up-server-secs: "20-80" # sc-max-each-post-bytes: 1000000 @@ -1768,6 +1808,8 @@ listeners: username2: password2 # 一个 base64 字符串用于微调网络行为 # traffic-pattern: "" + # 如果开启,且客户端不发送用户提示,代理服务器将拒绝连接 + # user-hint-is-mandatory: false - name: sudoku-in-1 type: sudoku diff --git a/mihomo/go.mod b/mihomo/go.mod index 9f1f593ffb..0e4202bb0e 100644 --- a/mihomo/go.mod +++ b/mihomo/go.mod @@ -6,7 +6,7 @@ require ( github.com/bahlo/generic-list-go v0.2.0 github.com/coreos/go-iptables v0.8.0 github.com/dlclark/regexp2 v1.11.5 - github.com/enfein/mieru/v3 v3.30.1 + github.com/enfein/mieru/v3 v3.31.0 github.com/gobwas/ws v1.4.0 github.com/gofrs/uuid/v5 v5.4.0 github.com/golang/snappy v1.0.0 diff --git a/mihomo/go.sum b/mihomo/go.sum index aa667bd763..9d301a099a 100644 --- a/mihomo/go.sum +++ b/mihomo/go.sum @@ -20,8 +20,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0= github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= -github.com/enfein/mieru/v3 v3.30.1 h1:gHHXQfpQO/5d789o9kokVfej7jl795aJwPihUk3gTDU= -github.com/enfein/mieru/v3 v3.30.1/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM= +github.com/enfein/mieru/v3 v3.31.0 h1:Fl2ocRCRXJzMygzdRjBHgqI996ZuIDHUmyQyovSf9sA= +github.com/enfein/mieru/v3 v3.31.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= diff --git a/mihomo/listener/config/vless.go b/mihomo/listener/config/vless.go index 5e67e3f780..a4de7f57df 100644 --- a/mihomo/listener/config/vless.go +++ b/mihomo/listener/config/vless.go @@ -34,6 +34,20 @@ type XHTTPConfig struct { Path string Host string Mode string + XPaddingBytes string + XPaddingObfsMode bool + XPaddingKey string + XPaddingHeader string + XPaddingPlacement string + XPaddingMethod string + UplinkHTTPMethod string + SessionPlacement string + SessionKey string + SeqPlacement string + SeqKey string + UplinkDataPlacement string + UplinkDataKey string + UplinkChunkSize string NoSSEHeader bool ScStreamUpServerSecs string ScMaxBufferedPosts string diff --git a/mihomo/listener/inbound/anytls_test.go b/mihomo/listener/inbound/anytls_test.go index 444c943a46..5d9ac8ba5b 100644 --- a/mihomo/listener/inbound/anytls_test.go +++ b/mihomo/listener/inbound/anytls_test.go @@ -41,6 +41,7 @@ func testInboundAnyTLS(t *testing.T, inboundOptions inbound.AnyTLSOption, outbou outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) outboundOptions.Password = userUUID + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewAnyTLS(outboundOptions) if !assert.NoError(t, err) { diff --git a/mihomo/listener/inbound/common_test.go b/mihomo/listener/inbound/common_test.go index f4aa36cd19..74df75a2bd 100644 --- a/mihomo/listener/inbound/common_test.go +++ b/mihomo/listener/inbound/common_test.go @@ -58,6 +58,30 @@ func init() { realityPublickey = base64.RawURLEncoding.EncodeToString(privateKey.PublicKey().Bytes()) } +type TestDialer struct { + dialer C.Dialer + ctx context.Context +} + +func (t *TestDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { +start: + conn, err := t.dialer.DialContext(ctx, network, address) + if err != nil && ctx.Err() == nil && t.ctx.Err() == nil { + // We are conducting tests locally, and they shouldn't fail. + // However, a large number of requests in a short period during concurrent testing can exhaust system ports. + // This can lead to various errors such as WSAECONNREFUSED and WSAENOBUFS. + // So we just retry if the context is not canceled. + goto start + } + return conn, err +} + +func (t *TestDialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) { + return t.dialer.ListenPacket(ctx, network, address, rAddrPort) +} + +var _ C.Dialer = (*TestDialer)(nil) + type TestTunnel struct { HandleTCPConnFn func(conn net.Conn, metadata *C.Metadata) HandleUDPPacketFn func(packet C.UDPPacket, metadata *C.Metadata) @@ -65,6 +89,7 @@ type TestTunnel struct { CloseFn func() error DoSequentialTestFn func(t *testing.T, proxy C.ProxyAdapter) DoConcurrentTestFn func(t *testing.T, proxy C.ProxyAdapter) + NewDialerFn func() C.Dialer } func (tt *TestTunnel) HandleTCPConn(conn net.Conn, metadata *C.Metadata) { @@ -96,6 +121,10 @@ func (tt *TestTunnel) DoConcurrentTest(t *testing.T, proxy C.ProxyAdapter) { tt.DoConcurrentTestFn(t, proxy) } +func (tt *TestTunnel) NewDialer() C.Dialer { + return tt.NewDialerFn() +} + type TestTunnelListener struct { ch chan net.Conn ctx context.Context @@ -328,6 +357,7 @@ func NewHttpTestTunnel() *TestTunnel { CloseFn: ln.Close, DoSequentialTestFn: sequentialTestFn, DoConcurrentTestFn: concurrentTestFn, + NewDialerFn: func() C.Dialer { return &TestDialer{dialer: dialer.NewDialer(), ctx: ctx} }, } return tunnel } diff --git a/mihomo/listener/inbound/hysteria2_test.go b/mihomo/listener/inbound/hysteria2_test.go index 52fc07b7e1..b0132fa94a 100644 --- a/mihomo/listener/inbound/hysteria2_test.go +++ b/mihomo/listener/inbound/hysteria2_test.go @@ -41,6 +41,7 @@ func testInboundHysteria2(t *testing.T, inboundOptions inbound.Hysteria2Option, outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) outboundOptions.Password = userUUID + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewHysteria2(outboundOptions) if !assert.NoError(t, err) { diff --git a/mihomo/listener/inbound/mieru.go b/mihomo/listener/inbound/mieru.go index 5dfb690925..71882ecdba 100644 --- a/mihomo/listener/inbound/mieru.go +++ b/mihomo/listener/inbound/mieru.go @@ -27,9 +27,10 @@ type Mieru struct { type MieruOption struct { BaseOption - Transport string `inbound:"transport"` - Users map[string]string `inbound:"users"` - TrafficPattern string `inbound:"traffic-pattern,omitempty"` + Transport string `inbound:"transport"` + Users map[string]string `inbound:"users"` + TrafficPattern string `inbound:"traffic-pattern,omitempty"` + UserHintIsMandatory bool `inbound:"user-hint-is-mandatory,omitempty"` } type mieruListenerFactory struct{} @@ -158,11 +159,18 @@ func buildMieruServerConfig(option *MieruOption, ports utils.IntRanges[uint16]) } var trafficPattern *mierupb.TrafficPattern trafficPattern, _ = mierutp.Decode(option.TrafficPattern) + var advancedSettings *mierupb.ServerAdvancedSettings + if option.UserHintIsMandatory { + advancedSettings = &mierupb.ServerAdvancedSettings{ + UserHintIsMandatory: proto.Bool(true), + } + } return &mieruserver.ServerConfig{ Config: &mierupb.ServerConfig{ - PortBindings: portBindings, - Users: users, - TrafficPattern: trafficPattern, + PortBindings: portBindings, + Users: users, + TrafficPattern: trafficPattern, + AdvancedSettings: advancedSettings, }, StreamListenerFactory: mieruListenerFactory{}, PacketListenerFactory: mieruListenerFactory{}, diff --git a/mihomo/listener/inbound/mieru_test.go b/mihomo/listener/inbound/mieru_test.go index e3a5d23ce3..9774218620 100644 --- a/mihomo/listener/inbound/mieru_test.go +++ b/mihomo/listener/inbound/mieru_test.go @@ -206,8 +206,9 @@ func testInboundMieruTCP(t *testing.T, handshakeMode string) { Listen: "127.0.0.1", Port: strconv.Itoa(port), }, - Transport: "TCP", - Users: map[string]string{"test": "password"}, + Transport: "TCP", + Users: map[string]string{"test": "password"}, + UserHintIsMandatory: true, } in, err := inbound.NewMieru(&inboundOptions) if !assert.NoError(t, err) { @@ -236,6 +237,7 @@ func testInboundMieruTCP(t *testing.T, handshakeMode string) { Password: "password", HandshakeMode: handshakeMode, } + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewMieru(outboundOptions) if !assert.NoError(t, err) { return @@ -260,8 +262,9 @@ func testInboundMieruUDP(t *testing.T, handshakeMode string) { Listen: "127.0.0.1", Port: strconv.Itoa(port), }, - Transport: "UDP", - Users: map[string]string{"test": "password"}, + Transport: "UDP", + Users: map[string]string{"test": "password"}, + UserHintIsMandatory: true, } in, err := inbound.NewMieru(&inboundOptions) if !assert.NoError(t, err) { @@ -290,6 +293,7 @@ func testInboundMieruUDP(t *testing.T, handshakeMode string) { Password: "password", HandshakeMode: handshakeMode, } + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewMieru(outboundOptions) if !assert.NoError(t, err) { return diff --git a/mihomo/listener/inbound/shadowsocks_test.go b/mihomo/listener/inbound/shadowsocks_test.go index d29af71225..755db2cf87 100644 --- a/mihomo/listener/inbound/shadowsocks_test.go +++ b/mihomo/listener/inbound/shadowsocks_test.go @@ -85,6 +85,7 @@ func testInboundShadowSocks0(t *testing.T, inboundOptions inbound.ShadowSocksOpt outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) outboundOptions.Password = password + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewShadowSocks(outboundOptions) if !assert.NoError(t, err) { diff --git a/mihomo/listener/inbound/sudoku_test.go b/mihomo/listener/inbound/sudoku_test.go index 9ba814dafd..41348008f2 100644 --- a/mihomo/listener/inbound/sudoku_test.go +++ b/mihomo/listener/inbound/sudoku_test.go @@ -43,6 +43,7 @@ func testInboundSudoku(t *testing.T, inboundOptions inbound.SudokuOption, outbou outboundOptions.Name = "sudoku_outbound" outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewSudoku(outboundOptions) if !assert.NoError(t, err) { diff --git a/mihomo/listener/inbound/trojan_test.go b/mihomo/listener/inbound/trojan_test.go index 7b92b08560..14c36e8278 100644 --- a/mihomo/listener/inbound/trojan_test.go +++ b/mihomo/listener/inbound/trojan_test.go @@ -43,6 +43,7 @@ func testInboundTrojan(t *testing.T, inboundOptions inbound.TrojanOption, outbou outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) outboundOptions.Password = userUUID + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewTrojan(outboundOptions) if !assert.NoError(t, err) { diff --git a/mihomo/listener/inbound/trusttunnel_test.go b/mihomo/listener/inbound/trusttunnel_test.go index 5283d0880a..4c6c3cdd38 100644 --- a/mihomo/listener/inbound/trusttunnel_test.go +++ b/mihomo/listener/inbound/trusttunnel_test.go @@ -42,6 +42,7 @@ func testInboundTrustTunnel(t *testing.T, inboundOptions inbound.TrustTunnelOpti outboundOptions.Port = int(addrPort.Port()) outboundOptions.UserName = "test" outboundOptions.Password = userUUID + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewTrustTunnel(outboundOptions) if !assert.NoError(t, err) { diff --git a/mihomo/listener/inbound/tuic_test.go b/mihomo/listener/inbound/tuic_test.go index f887609de7..34bf8e4d75 100644 --- a/mihomo/listener/inbound/tuic_test.go +++ b/mihomo/listener/inbound/tuic_test.go @@ -69,6 +69,7 @@ func testInboundTuic0(t *testing.T, inboundOptions inbound.TuicOption, outboundO outboundOptions.Name = "tuic_outbound" outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewTuic(outboundOptions) if !assert.NoError(t, err) { diff --git a/mihomo/listener/inbound/vless.go b/mihomo/listener/inbound/vless.go index aad9e70ecf..02e3f74780 100644 --- a/mihomo/listener/inbound/vless.go +++ b/mihomo/listener/inbound/vless.go @@ -35,6 +35,20 @@ type XHTTPConfig struct { Path string `inbound:"path,omitempty"` Host string `inbound:"host,omitempty"` Mode string `inbound:"mode,omitempty"` + XPaddingBytes string `inbound:"xpadding-bytes,omitempty"` + XPaddingObfsMode bool `inbound:"xpadding-obfs-mode,omitempty"` + XPaddingKey string `inbound:"xpadding-key,omitempty"` + XPaddingHeader string `inbound:"xpadding-header,omitempty"` + XPaddingPlacement string `inbound:"xpadding-placement,omitempty"` + XPaddingMethod string `inbound:"xpadding-method,omitempty"` + UplinkHTTPMethod string `inbound:"uplink-http-method,omitempty"` + SessionPlacement string `inbound:"session-placement,omitempty"` + SessionKey string `inbound:"session-key,omitempty"` + SeqPlacement string `inbound:"seq-placement,omitempty"` + SeqKey string `inbound:"seq-key,omitempty"` + UplinkDataPlacement string `inbound:"uplink-data-placement,omitempty"` + UplinkDataKey string `inbound:"uplink-data-key,omitempty"` + UplinkChunkSize string `inbound:"uplink-chunk-size,omitempty"` NoSSEHeader bool `inbound:"no-sse-header,omitempty"` ScStreamUpServerSecs string `inbound:"sc-stream-up-server-secs,omitempty"` ScMaxBufferedPosts string `inbound:"sc-max-buffered-posts,omitempty"` @@ -47,6 +61,19 @@ func (o XHTTPConfig) Build() LC.XHTTPConfig { Host: o.Host, Mode: o.Mode, NoSSEHeader: o.NoSSEHeader, + XPaddingBytes: o.XPaddingBytes, + XPaddingObfsMode: o.XPaddingObfsMode, + XPaddingKey: o.XPaddingKey, + XPaddingHeader: o.XPaddingHeader, + XPaddingPlacement: o.XPaddingPlacement, + UplinkHTTPMethod: o.UplinkHTTPMethod, + SessionPlacement: o.SessionPlacement, + SessionKey: o.SessionKey, + SeqPlacement: o.SeqPlacement, + SeqKey: o.SeqKey, + UplinkDataPlacement: o.UplinkDataPlacement, + UplinkDataKey: o.UplinkDataKey, + UplinkChunkSize: o.UplinkChunkSize, ScStreamUpServerSecs: o.ScStreamUpServerSecs, ScMaxBufferedPosts: o.ScMaxBufferedPosts, ScMaxEachPostBytes: o.ScMaxEachPostBytes, diff --git a/mihomo/listener/inbound/vless_test.go b/mihomo/listener/inbound/vless_test.go index a3e707a5c6..a483c34113 100644 --- a/mihomo/listener/inbound/vless_test.go +++ b/mihomo/listener/inbound/vless_test.go @@ -44,6 +44,7 @@ func testInboundVless(t *testing.T, inboundOptions inbound.VlessOption, outbound outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) outboundOptions.UUID = userUUID + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewVless(outboundOptions) if !assert.NoError(t, err) { @@ -488,6 +489,77 @@ func TestInboundVless_XHTTP_Reality(t *testing.T) { } } +func TestInboundVless_XHTTP_Encryption(t *testing.T) { + privateKeyBase64, passwordBase64, _, err := encryption.GenX25519("") + if err != nil { + t.Fatal(err) + return + } + testCases := []struct { + mode string + }{ + {mode: "auto"}, + {mode: "stream-one"}, + {mode: "stream-up"}, + {mode: "packet-up"}, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.mode, func(t *testing.T) { + getConfig := func() (inbound.VlessOption, outbound.VlessOption) { + inboundOptions := inbound.VlessOption{ + Decryption: "mlkem768x25519plus.native.600s." + privateKeyBase64, + XHTTPConfig: inbound.XHTTPConfig{ + Path: "/vless-xhttp", + Host: "example.com", + Mode: testCase.mode, + }, + } + outboundOptions := outbound.VlessOption{ + Encryption: "mlkem768x25519plus.native.0rtt." + passwordBase64, + 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)) + }) + }) + }) + } +} + func TestInboundVless_XHTTP_PacketUp_H1(t *testing.T) { getConfig := func() (inbound.VlessOption, outbound.VlessOption) { inboundOptions := inbound.VlessOption{ @@ -524,6 +596,55 @@ func TestInboundVless_XHTTP_PacketUp_H1(t *testing.T) { }) } +func TestInboundVless_XHTTP_PacketUp_H1_Encryption(t *testing.T) { + privateKeyBase64, passwordBase64, _, err := encryption.GenX25519("") + if err != nil { + t.Fatal(err) + return + } + getConfig := func() (inbound.VlessOption, outbound.VlessOption) { + inboundOptions := inbound.VlessOption{ + Decryption: "mlkem768x25519plus.native.600s." + privateKeyBase64, + XHTTPConfig: inbound.XHTTPConfig{ + Path: "/vless-xhttp", + Host: "example.com", + Mode: "packet-up", + }, + } + outboundOptions := outbound.VlessOption{ + Encryption: "mlkem768x25519plus.native.0rtt." + passwordBase64, + Network: "xhttp", + ALPN: []string{"http/1.1"}, + XHTTPOpts: outbound.XHTTPOptions{ + Path: "/vless-xhttp", + Host: "example.com", + Mode: "packet-up", + }, + } + return inboundOptions, outboundOptions + } + + t.Run("default", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + testInboundVless(t, inboundOptions, outboundOptions) + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) + }) + + t.Run("reuse", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + testInboundVless(t, inboundOptions, outboundOptions) + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) + }) +} + func withXHTTPReuse(out outbound.VlessOption) outbound.VlessOption { out.XHTTPOpts.ReuseSettings = &outbound.XHTTPReuseSettings{ MaxConnections: "0", diff --git a/mihomo/listener/inbound/vmess_test.go b/mihomo/listener/inbound/vmess_test.go index de3a62d20e..175d98031b 100644 --- a/mihomo/listener/inbound/vmess_test.go +++ b/mihomo/listener/inbound/vmess_test.go @@ -45,6 +45,7 @@ func testInboundVMess(t *testing.T, inboundOptions inbound.VmessOption, outbound outboundOptions.UUID = userUUID outboundOptions.AlterID = 0 outboundOptions.Cipher = "auto" + outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewVmess(outboundOptions) if !assert.NoError(t, err) { diff --git a/mihomo/listener/sing_vless/server.go b/mihomo/listener/sing_vless/server.go index d684056b8c..4e41f6bee4 100644 --- a/mihomo/listener/sing_vless/server.go +++ b/mihomo/listener/sing_vless/server.go @@ -159,6 +159,20 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) Host: config.XHTTPConfig.Host, Path: config.XHTTPConfig.Path, Mode: config.XHTTPConfig.Mode, + XPaddingBytes: config.XHTTPConfig.XPaddingBytes, + XPaddingObfsMode: config.XHTTPConfig.XPaddingObfsMode, + XPaddingKey: config.XHTTPConfig.XPaddingKey, + XPaddingHeader: config.XHTTPConfig.XPaddingHeader, + XPaddingPlacement: config.XHTTPConfig.XPaddingPlacement, + XPaddingMethod: config.XHTTPConfig.XPaddingMethod, + UplinkHTTPMethod: config.XHTTPConfig.UplinkHTTPMethod, + SessionPlacement: config.XHTTPConfig.SessionPlacement, + SessionKey: config.XHTTPConfig.SessionKey, + SeqPlacement: config.XHTTPConfig.SeqPlacement, + SeqKey: config.XHTTPConfig.SeqKey, + UplinkDataPlacement: config.XHTTPConfig.UplinkDataPlacement, + UplinkDataKey: config.XHTTPConfig.UplinkDataKey, + UplinkChunkSize: config.XHTTPConfig.UplinkChunkSize, NoSSEHeader: config.XHTTPConfig.NoSSEHeader, ScStreamUpServerSecs: config.XHTTPConfig.ScStreamUpServerSecs, ScMaxBufferedPosts: config.XHTTPConfig.ScMaxBufferedPosts, diff --git a/mihomo/transport/xhttp/browser.go b/mihomo/transport/xhttp/browser.go new file mode 100644 index 0000000000..9a8f7f107f --- /dev/null +++ b/mihomo/transport/xhttp/browser.go @@ -0,0 +1,249 @@ +package xhttp + +import ( + "math" + "strconv" + "strings" + "time" + + "github.com/metacubex/http" + "github.com/metacubex/randv2" +) + +// The Chrome version generator will suffer from deviation of a normal distribution. + +func ChromeVersion() int { + // Start from Chrome 144, released on 2026.1.13. + var startVersion int = 144 + var timeStart int64 = time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC).Unix() / 86400 + var timeCurrent int64 = time.Now().Unix() / 86400 + var timeDiff int = int((timeCurrent - timeStart - 35)) - int(math.Floor(math.Pow(randv2.Float64(), 2)*105)) + return startVersion + (timeDiff / 35) // It's 31.15 currently. +} + +var safariMinorMap [25]int = [25]int{0, 0, 0, 1, 1, + 1, 2, 2, 2, 2, 3, 3, 3, 4, 4, + 4, 5, 5, 5, 5, 5, 6, 6, 6, 6} + +// The following version generators use deterministic generators, but with the distribution scaled by a curve. + +func CurlVersion() string { + // curl 8.0.0 was released on 20/03/2023. + var timeCurrent int64 = time.Now().Unix() / 86400 + var timeStart int64 = time.Date(2023, 3, 20, 0, 0, 0, 0, time.UTC).Unix() / 86400 + var timeDiff int = int((timeCurrent - timeStart - 60)) - int(math.Floor(math.Pow(randv2.Float64(), 2)*165)) + var minorValue int = int(timeDiff / 57) // The release cadence is actually 56.67 days. + return "8." + strconv.Itoa(minorValue) + ".0" +} +func FirefoxVersion() int { + // Firefox 128 ESR was released on 09/07/2023. + var timeCurrent int64 = time.Now().Unix() / 86400 + var timeStart int64 = time.Date(2024, 7, 29, 0, 0, 0, 0, time.UTC).Unix() / 86400 + var timeDiff = timeCurrent - timeStart - 25 - int64(math.Floor(math.Pow(randv2.Float64(), 2)*50)) + return int(timeDiff/30) + 128 +} +func SafariVersion() string { + var anchoredTime time.Time = time.Now() + var releaseYear int = anchoredTime.Year() + var splitPoint time.Time = time.Date(releaseYear, 9, 23, 0, 0, 0, 0, time.UTC) + var delayedDays = int(math.Floor(math.Pow(randv2.Float64(), 3) * 75)) + splitPoint = splitPoint.AddDate(0, 0, delayedDays) + if anchoredTime.Compare(splitPoint) < 0 { + releaseYear-- + splitPoint = time.Date(releaseYear, 9, 23, 0, 0, 0, 0, time.UTC) + splitPoint = splitPoint.AddDate(0, 0, delayedDays) + } + var minorVersion = safariMinorMap[(anchoredTime.Unix()-splitPoint.Unix())/1296000] + return strconv.Itoa(releaseYear-1999) + "." + strconv.Itoa(minorVersion) +} + +// The full Chromium brand GREASE implementation +var clientHintGreaseNA = []string{" ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_"} +var clientHintVersionNA = []string{"8", "99", "24"} +var clientHintShuffle3 = [][3]int{{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}} +var clientHintShuffle4 = [][4]int{ + {0, 1, 2, 3}, {0, 1, 3, 2}, {0, 2, 1, 3}, {0, 2, 3, 1}, {0, 3, 1, 2}, {0, 3, 2, 1}, + {1, 0, 2, 3}, {1, 0, 3, 2}, {1, 2, 0, 3}, {1, 2, 3, 0}, {1, 3, 0, 2}, {1, 3, 2, 0}, + {2, 0, 1, 3}, {2, 0, 3, 1}, {2, 1, 0, 3}, {2, 1, 3, 0}, {2, 3, 0, 1}, {2, 3, 1, 0}, + {3, 0, 1, 2}, {3, 0, 2, 1}, {3, 1, 0, 2}, {3, 1, 2, 0}, {3, 2, 0, 1}, {3, 2, 1, 0}} + +func getGreasedChInvalidBrand(seed int) string { + return "\"Not" + clientHintGreaseNA[seed%len(clientHintGreaseNA)] + "A" + clientHintGreaseNA[(seed+1)%len(clientHintGreaseNA)] + "Brand\";v=\"" + clientHintVersionNA[seed%len(clientHintVersionNA)] + "\"" +} +func getGreasedChOrder(brandLength int, seed int) []int { + switch brandLength { + case 1: + return []int{0} + case 2: + return []int{seed % brandLength, (seed + 1) % brandLength} + case 3: + return clientHintShuffle3[seed%len(clientHintShuffle3)][:] + default: + return clientHintShuffle4[seed%len(clientHintShuffle4)][:] + } + //return []int{} +} +func getUngreasedChUa(majorVersion int, forkName string) []string { + // Set the capacity to 4, the maximum allowed brand size, so Go will never allocate memory twice + baseChUa := make([]string, 0, 4) + baseChUa = append(baseChUa, getGreasedChInvalidBrand(majorVersion), + "\"Chromium\";v=\""+strconv.Itoa(majorVersion)+"\"") + switch forkName { + case "chrome": + baseChUa = append(baseChUa, "\"Google Chrome\";v=\""+strconv.Itoa(majorVersion)+"\"") + case "edge": + baseChUa = append(baseChUa, "\"Microsoft Edge\";v=\""+strconv.Itoa(majorVersion)+"\"") + } + return baseChUa +} +func getGreasedChUa(majorVersion int, forkName string) string { + ungreasedCh := getUngreasedChUa(majorVersion, forkName) + shuffleMap := getGreasedChOrder(len(ungreasedCh), majorVersion) + shuffledCh := make([]string, len(ungreasedCh)) + for i, e := range shuffleMap { + shuffledCh[e] = ungreasedCh[i] + } + return strings.Join(shuffledCh, ", ") +} + +// The code below provides a coherent default browser user agent string based on a CPU-seeded PRNG. + +var CurlUA = "curl/" + CurlVersion() +var AnchoredFirefoxVersion = strconv.Itoa(FirefoxVersion()) +var FirefoxUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:" + AnchoredFirefoxVersion + ".0) Gecko/20100101 Firefox/" + AnchoredFirefoxVersion + ".0" +var SafariUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/" + SafariVersion() + " Safari/605.1.15" + +// Chromium browsers. + +var AnchoredChromeVersion = ChromeVersion() +var ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0 Safari/537.36" +var ChromeUACH = getGreasedChUa(AnchoredChromeVersion, "chrome") +var MSEdgeUA = ChromeUA + "Edg/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0" +var MSEdgeUACH = getGreasedChUa(AnchoredChromeVersion, "edge") + +func applyMasqueradedHeaders(header http.Header, browser string, variant string) { + // Browser-specific. + switch browser { + case "chrome": + header["Sec-CH-UA"] = []string{ChromeUACH} + header["Sec-CH-UA-Mobile"] = []string{"?0"} + header["Sec-CH-UA-Platform"] = []string{"\"Windows\""} + header["DNT"] = []string{"1"} + header.Set("User-Agent", ChromeUA) + header.Set("Accept-Language", "en-US,en;q=0.9") + case "edge": + header["Sec-CH-UA"] = []string{MSEdgeUACH} + header["Sec-CH-UA-Mobile"] = []string{"?0"} + header["Sec-CH-UA-Platform"] = []string{"\"Windows\""} + header["DNT"] = []string{"1"} + header.Set("User-Agent", MSEdgeUA) + header.Set("Accept-Language", "en-US,en;q=0.9") + case "firefox": + header.Set("User-Agent", FirefoxUA) + header["DNT"] = []string{"1"} + header.Set("Accept-Language", "en-US,en;q=0.5") + case "safari": + header.Set("User-Agent", SafariUA) + header.Set("Accept-Language", "en-US,en;q=0.9") + case "golang": + // Expose the default net/http header. + header.Del("User-Agent") + return + case "curl": + header.Set("User-Agent", CurlUA) + return + } + // Context-specific. + switch variant { + case "nav": + if header.Get("Cache-Control") == "" { + switch browser { + case "chrome", "edge": + header.Set("Cache-Control", "max-age=0") + } + } + header.Set("Upgrade-Insecure-Requests", "1") + if header.Get("Accept") == "" { + switch browser { + case "chrome", "edge": + header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") + case "firefox", "safari": + header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + } + } + header.Set("Sec-Fetch-Site", "none") + header.Set("Sec-Fetch-Mode", "navigate") + switch browser { + case "safari": + default: + header.Set("Sec-Fetch-User", "?1") + } + header.Set("Sec-Fetch-Dest", "document") + header.Set("Priority", "u=0, i") + case "ws": + header.Set("Sec-Fetch-Mode", "websocket") + switch browser { + case "safari": + // Safari is NOT web-compliant here! + header.Set("Sec-Fetch-Dest", "websocket") + default: + header.Set("Sec-Fetch-Dest", "empty") + } + header.Set("Sec-Fetch-Site", "same-origin") + if header.Get("Cache-Control") == "" { + header.Set("Cache-Control", "no-cache") + } + if header.Get("Pragma") == "" { + header.Set("Pragma", "no-cache") + } + if header.Get("Accept") == "" { + header.Set("Accept", "*/*") + } + case "fetch": + header.Set("Sec-Fetch-Mode", "cors") + header.Set("Sec-Fetch-Dest", "empty") + header.Set("Sec-Fetch-Site", "same-origin") + if header.Get("Priority") == "" { + switch browser { + case "chrome", "edge": + header.Set("Priority", "u=1, i") + case "firefox": + header.Set("Priority", "u=4") + case "safari": + header.Set("Priority", "u=3, i") + } + } + if header.Get("Cache-Control") == "" { + header.Set("Cache-Control", "no-cache") + } + if header.Get("Pragma") == "" { + header.Set("Pragma", "no-cache") + } + if header.Get("Accept") == "" { + header.Set("Accept", "*/*") + } + } +} + +func TryDefaultHeadersWith(header http.Header, variant string) { + // The global UA special value handler for transports. Used to be called HandleTransportUASettings. + // Just a FYI to whoever needing to fix this piece of code after some spontaneous event, I tried to make the two methods separate to let the code be cleaner and more organized. + if len(header.Values("User-Agent")) < 1 { + applyMasqueradedHeaders(header, "chrome", variant) + } else { + switch header.Get("User-Agent") { + case "chrome": + applyMasqueradedHeaders(header, "chrome", variant) + case "firefox": + applyMasqueradedHeaders(header, "firefox", variant) + case "safari": + applyMasqueradedHeaders(header, "safari", variant) + case "edge": + applyMasqueradedHeaders(header, "edge", variant) + case "curl": + applyMasqueradedHeaders(header, "curl", variant) + case "golang": + applyMasqueradedHeaders(header, "golang", variant) + } + } +} diff --git a/mihomo/transport/xhttp/client.go b/mihomo/transport/xhttp/client.go index 3e7c726df4..fccfd60964 100644 --- a/mihomo/transport/xhttp/client.go +++ b/mihomo/transport/xhttp/client.go @@ -21,7 +21,6 @@ import ( "github.com/metacubex/quic-go" "github.com/metacubex/quic-go/http3" "github.com/metacubex/tls" - "golang.org/x/sync/semaphore" ) // ConnIdleTimeout defines the maximum time an idle TCP session can survive in the tunnel, @@ -115,7 +114,7 @@ func (c *PacketUpWriter) write(b []byte) (int, error) { Path: c.cfg.NormalizedPath(), } - req, err := http.NewRequestWithContext(c.ctx, http.MethodPost, u.String(), nil) + req, err := http.NewRequestWithContext(c.ctx, c.cfg.GetNormalizedUplinkHTTPMethod(), u.String(), nil) if err != nil { return 0, err } @@ -177,12 +176,7 @@ func NewTransport(dialRaw DialRawFunc, wrapTLS WrapTLSFunc, dialQUIC DialQUICFun } } if len(alpn) == 1 && alpn[0] == "http/1.1" { // `alpn: [http/1.1]` means using http/1.1 mode - w := semaphore.NewWeighted(20) // limit concurrent dialing to avoid WSAECONNREFUSED on Windows dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) { - if err := w.Acquire(ctx, 1); err != nil { - return nil, err - } - defer w.Release(1) raw, err := dialRaw(ctx) if err != nil { return nil, err @@ -359,7 +353,7 @@ func (c *Client) DialStreamOne() (net.Conn, error) { }, }) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), pr) + req, err := http.NewRequestWithContext(ctx, c.cfg.GetNormalizedUplinkHTTPMethod(), requestURL.String(), pr) if err != nil { _ = pr.Close() _ = pw.Close() @@ -470,7 +464,7 @@ func (c *Client) DialStreamUp() (net.Conn, error) { uploadReq, err := http.NewRequestWithContext( c.ctx, - http.MethodPost, + c.cfg.GetNormalizedUplinkHTTPMethod(), streamURL.String(), pr, ) diff --git a/mihomo/transport/xhttp/config.go b/mihomo/transport/xhttp/config.go index 0c6d2a784f..62208f2b43 100644 --- a/mihomo/transport/xhttp/config.go +++ b/mihomo/transport/xhttp/config.go @@ -2,6 +2,7 @@ package xhttp import ( "bytes" + "encoding/base64" "fmt" "io" "math/rand" @@ -11,6 +12,16 @@ import ( "github.com/metacubex/http" ) +const ( + PlacementQueryInHeader = "queryInHeader" + PlacementCookie = "cookie" + PlacementHeader = "header" + PlacementQuery = "query" + PlacementPath = "path" + PlacementBody = "body" + PlacementAuto = "auto" +) + type Config struct { Host string Path string @@ -18,6 +29,19 @@ type Config struct { Headers map[string]string NoGRPCHeader bool XPaddingBytes string + XPaddingObfsMode bool + XPaddingKey string + XPaddingHeader string + XPaddingPlacement string + XPaddingMethod string + UplinkHTTPMethod string + SessionPlacement string + SessionKey string + SeqPlacement string + SeqKey string + UplinkDataPlacement string + UplinkDataKey string + UplinkChunkSize string NoSSEHeader bool // server only ScStreamUpServerSecs string // server only ScMaxBufferedPosts string // server only @@ -70,37 +94,92 @@ func (c *Config) NormalizedPath() string { return path } -func (c *Config) RequestHeader() http.Header { +func (c *Config) GetRequestHeader() http.Header { h := http.Header{} for k, v := range c.Headers { h.Set(k, v) } - - if h.Get("User-Agent") == "" { - h.Set("User-Agent", "Mozilla/5.0") - } - if h.Get("Accept") == "" { - h.Set("Accept", "*/*") - } - if h.Get("Accept-Language") == "" { - h.Set("Accept-Language", "en-US,en;q=0.9") - } - if h.Get("Cache-Control") == "" { - h.Set("Cache-Control", "no-cache") - } - if h.Get("Pragma") == "" { - h.Set("Pragma", "no-cache") - } - + TryDefaultHeadersWith(h, "fetch") return h } -func (c *Config) RandomPadding() (string, error) { - r, err := ParseRange(c.XPaddingBytes, "100-1000") - if err != nil { - return "", fmt.Errorf("invalid x-padding-bytes: %w", err) +func (c *Config) GetRequestHeaderWithPayload(payload []byte, uplinkChunkSize Range) http.Header { + header := c.GetRequestHeader() + + key := c.UplinkDataKey + encodedData := base64.RawURLEncoding.EncodeToString(payload) + + for i := 0; len(encodedData) > 0; i++ { + chunkSize := uplinkChunkSize.Rand() + if len(encodedData) < chunkSize { + chunkSize = len(encodedData) + } + chunk := encodedData[:chunkSize] + encodedData = encodedData[chunkSize:] + headerKey := fmt.Sprintf("%s-%d", key, i) + header.Set(headerKey, chunk) } - return strings.Repeat("X", r.Rand()), nil + + return header +} + +func (c *Config) GetRequestCookiesWithPayload(payload []byte, uplinkChunkSize Range) []*http.Cookie { + cookies := []*http.Cookie{} + + key := c.UplinkDataKey + encodedData := base64.RawURLEncoding.EncodeToString(payload) + + for i := 0; len(encodedData) > 0; i++ { + chunkSize := uplinkChunkSize.Rand() + if len(encodedData) < chunkSize { + chunkSize = len(encodedData) + } + chunk := encodedData[:chunkSize] + encodedData = encodedData[chunkSize:] + cookieName := fmt.Sprintf("%s_%d", key, i) + cookies = append(cookies, &http.Cookie{Name: cookieName, Value: chunk}) + } + + return cookies +} + +func (c *Config) WriteResponseHeader(writer http.ResponseWriter, requestMethod string, requestHeader http.Header) { + if origin := requestHeader.Get("Origin"); origin == "" { + writer.Header().Set("Access-Control-Allow-Origin", "*") + } else { + // Chrome says: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. + writer.Header().Set("Access-Control-Allow-Origin", origin) + } + + if c.GetNormalizedSessionPlacement() == PlacementCookie || + c.GetNormalizedSeqPlacement() == PlacementCookie || + c.XPaddingPlacement == PlacementCookie || + c.GetNormalizedUplinkDataPlacement() == PlacementCookie { + writer.Header().Set("Access-Control-Allow-Credentials", "true") + } + + if requestMethod == "OPTIONS" { + requestedMethod := requestHeader.Get("Access-Control-Request-Method") + if requestedMethod != "" { + writer.Header().Set("Access-Control-Allow-Methods", requestedMethod) + } else { + writer.Header().Set("Access-Control-Allow-Methods", "*") + } + + requestedHeaders := requestHeader.Get("Access-Control-Request-Headers") + if requestedHeaders == "" { + writer.Header().Set("Access-Control-Allow-Headers", "*") + } else { + writer.Header().Set("Access-Control-Allow-Headers", requestedHeaders) + } + } +} + +func (c *Config) GetNormalizedUplinkHTTPMethod() string { + if c.UplinkHTTPMethod == "" { + return "POST" + } + return c.UplinkHTTPMethod } func (c *Config) GetNormalizedScStreamUpServerSecs() (Range, error) { @@ -144,6 +223,84 @@ func (c *Config) GetNormalizedScMinPostsIntervalMs() (Range, error) { return r, nil } +func (c *Config) GetNormalizedUplinkChunkSize() (Range, error) { + uplinkChunkSize, err := ParseRange(c.UplinkChunkSize, "") + if err != nil { + return Range{}, fmt.Errorf("invalid uplink-chunk-size: %w", err) + } + if uplinkChunkSize.Max == 0 { + switch c.GetNormalizedUplinkDataPlacement() { + case PlacementCookie: + return Range{ + Min: 2 * 1024, // 2 KiB + Max: 3 * 1024, // 3 KiB + }, nil + case PlacementHeader: + return Range{ + Min: 3 * 1024, // 3 KiB + Max: 4 * 1024, // 4 KiB + }, nil + default: + return c.GetNormalizedScMaxEachPostBytes() + } + } else if uplinkChunkSize.Min < 64 { + uplinkChunkSize.Min = 64 + if uplinkChunkSize.Max < 64 { + uplinkChunkSize.Max = 64 + } + } + return uplinkChunkSize, nil +} + +func (c *Config) GetNormalizedSessionPlacement() string { + if c.SessionPlacement == "" { + return PlacementPath + } + return c.SessionPlacement +} + +func (c *Config) GetNormalizedSeqPlacement() string { + if c.SeqPlacement == "" { + return PlacementPath + } + return c.SeqPlacement +} + +func (c *Config) GetNormalizedUplinkDataPlacement() string { + if c.UplinkDataPlacement == "" { + return PlacementBody + } + return c.UplinkDataPlacement +} + +func (c *Config) GetNormalizedSessionKey() string { + if c.SessionKey != "" { + return c.SessionKey + } + switch c.GetNormalizedSessionPlacement() { + case PlacementHeader: + return "X-Session" + case PlacementCookie, PlacementQuery: + return "x_session" + default: + return "" + } +} + +func (c *Config) GetNormalizedSeqKey() string { + if c.SeqKey != "" { + return c.SeqKey + } + switch c.GetNormalizedSeqPlacement() { + case PlacementHeader: + return "X-Seq" + case PlacementCookie, PlacementQuery: + return "x_seq" + default: + return "" + } +} + type Range struct { Min int Max int @@ -231,32 +388,6 @@ func (c *ReuseConfig) ResolveEntryConfig() (Range, Range, Range, error) { return cMaxReuseTimes, hMaxRequestTimes, hMaxReusableSecs, nil } -func (c *Config) FillStreamRequest(req *http.Request, sessionID string) error { - req.Header = c.RequestHeader() - - paddingValue, err := c.RandomPadding() - if err != nil { - return err - } - - if paddingValue != "" { - rawURL := req.URL.String() - sep := "?" - if strings.Contains(rawURL, "?") { - sep = "&" - } - req.Header.Set("Referer", rawURL+sep+"x_padding="+paddingValue) - } - - c.ApplyMetaToRequest(req, sessionID, "") - - if req.Body != nil && !c.NoGRPCHeader { - req.Header.Set("Content-Type", "application/grpc") - } - - return nil -} - func appendToPath(path, value string) string { if strings.HasSuffix(path, "/") { return path + value @@ -264,53 +395,185 @@ func appendToPath(path, value string) string { return path + "/" + value } -func (c *Config) ApplyMetaToRequest(req *http.Request, sessionID string, seqStr string) { - if sessionID != "" { - req.URL.Path = appendToPath(req.URL.Path, sessionID) +func (c *Config) ApplyMetaToRequest(req *http.Request, sessionId string, seqStr string) { + sessionPlacement := c.GetNormalizedSessionPlacement() + seqPlacement := c.GetNormalizedSeqPlacement() + sessionKey := c.GetNormalizedSessionKey() + seqKey := c.GetNormalizedSeqKey() + + if sessionId != "" { + switch sessionPlacement { + case PlacementPath: + req.URL.Path = appendToPath(req.URL.Path, sessionId) + case PlacementQuery: + q := req.URL.Query() + q.Set(sessionKey, sessionId) + req.URL.RawQuery = q.Encode() + case PlacementHeader: + req.Header.Set(sessionKey, sessionId) + case PlacementCookie: + req.AddCookie(&http.Cookie{Name: sessionKey, Value: sessionId}) + } } + if seqStr != "" { - req.URL.Path = appendToPath(req.URL.Path, seqStr) + switch seqPlacement { + case PlacementPath: + req.URL.Path = appendToPath(req.URL.Path, seqStr) + case PlacementQuery: + q := req.URL.Query() + q.Set(seqKey, seqStr) + req.URL.RawQuery = q.Encode() + case PlacementHeader: + req.Header.Set(seqKey, seqStr) + case PlacementCookie: + req.AddCookie(&http.Cookie{Name: seqKey, Value: seqStr}) + } } } -func (c *Config) FillPacketRequest(req *http.Request, sessionID string, seqStr string, payload []byte) error { - req.Header = c.RequestHeader() - req.Body = io.NopCloser(bytes.NewReader(payload)) - req.ContentLength = int64(len(payload)) +func (c *Config) ExtractMetaFromRequest(req *http.Request, path string) (sessionId string, seqStr string) { + sessionPlacement := c.GetNormalizedSessionPlacement() + seqPlacement := c.GetNormalizedSeqPlacement() + sessionKey := c.GetNormalizedSessionKey() + seqKey := c.GetNormalizedSeqKey() - paddingValue, err := c.RandomPadding() + var subpath []string + pathPart := 0 + if sessionPlacement == PlacementPath || seqPlacement == PlacementPath { + subpath = strings.Split(req.URL.Path[len(path):], "/") + } + + switch sessionPlacement { + case PlacementPath: + if len(subpath) > pathPart { + sessionId = subpath[pathPart] + pathPart += 1 + } + case PlacementQuery: + sessionId = req.URL.Query().Get(sessionKey) + case PlacementHeader: + sessionId = req.Header.Get(sessionKey) + case PlacementCookie: + if cookie, e := req.Cookie(sessionKey); e == nil { + sessionId = cookie.Value + } + } + + switch seqPlacement { + case PlacementPath: + if len(subpath) > pathPart { + seqStr = subpath[pathPart] + pathPart += 1 + } + case PlacementQuery: + seqStr = req.URL.Query().Get(seqKey) + case PlacementHeader: + seqStr = req.Header.Get(seqKey) + case PlacementCookie: + if cookie, e := req.Cookie(seqKey); e == nil { + seqStr = cookie.Value + } + } + + return sessionId, seqStr +} + +func (c *Config) FillStreamRequest(req *http.Request, sessionID string) error { + req.Header = c.GetRequestHeader() + xPaddingBytes, err := c.GetNormalizedXPaddingBytes() if err != nil { return err } - if paddingValue != "" { - rawURL := req.URL.String() - sep := "?" - if strings.Contains(rawURL, "?") { - sep = "&" + length := xPaddingBytes.Rand() + config := XPaddingConfig{Length: length} + + if c.XPaddingObfsMode { + config.Placement = XPaddingPlacement{ + Placement: c.XPaddingPlacement, + Key: c.XPaddingKey, + Header: c.XPaddingHeader, + RawURL: req.URL.String(), + } + config.Method = PaddingMethod(c.XPaddingMethod) + } else { + config.Placement = XPaddingPlacement{ + Placement: PlacementQueryInHeader, + Key: "x_padding", + Header: "Referer", + RawURL: req.URL.String(), } - req.Header.Set("Referer", rawURL+sep+"x_padding="+paddingValue) } - c.ApplyMetaToRequest(req, sessionID, seqStr) + c.ApplyXPaddingToRequest(req, config) + c.ApplyMetaToRequest(req, sessionID, "") + + if req.Body != nil && !c.NoGRPCHeader { // stream-up/one + req.Header.Set("Content-Type", "application/grpc") + } + return nil } func (c *Config) FillDownloadRequest(req *http.Request, sessionID string) error { - req.Header = c.RequestHeader() + return c.FillStreamRequest(req, sessionID) +} - paddingValue, err := c.RandomPadding() +func (c *Config) FillPacketRequest(request *http.Request, sessionId string, seqStr string, data []byte) error { + dataPlacement := c.GetNormalizedUplinkDataPlacement() + + if dataPlacement == PlacementBody || dataPlacement == PlacementAuto { + request.Header = c.GetRequestHeader() + request.Body = io.NopCloser(bytes.NewReader(data)) + request.ContentLength = int64(len(data)) + } else { + request.Body = nil + request.ContentLength = 0 + switch dataPlacement { + case PlacementHeader: + uplinkChunkSize, err := c.GetNormalizedUplinkChunkSize() + if err != nil { + return err + } + request.Header = c.GetRequestHeaderWithPayload(data, uplinkChunkSize) + case PlacementCookie: + request.Header = c.GetRequestHeader() + uplinkChunkSize, err := c.GetNormalizedUplinkChunkSize() + if err != nil { + return err + } + for _, cookie := range c.GetRequestCookiesWithPayload(data, uplinkChunkSize) { + request.AddCookie(cookie) + } + } + } + + xPaddingBytes, err := c.GetNormalizedXPaddingBytes() if err != nil { return err } - if paddingValue != "" { - rawURL := req.URL.String() - sep := "?" - if strings.Contains(rawURL, "?") { - sep = "&" + length := xPaddingBytes.Rand() + config := XPaddingConfig{Length: length} + + if c.XPaddingObfsMode { + config.Placement = XPaddingPlacement{ + Placement: c.XPaddingPlacement, + Key: c.XPaddingKey, + Header: c.XPaddingHeader, + RawURL: request.URL.String(), + } + config.Method = PaddingMethod(c.XPaddingMethod) + } else { + config.Placement = XPaddingPlacement{ + Placement: PlacementQueryInHeader, + Key: "x_padding", + Header: "Referer", + RawURL: request.URL.String(), } - req.Header.Set("Referer", rawURL+sep+"x_padding="+paddingValue) } - c.ApplyMetaToRequest(req, sessionID, "") + c.ApplyXPaddingToRequest(request, config) + c.ApplyMetaToRequest(request, sessionId, seqStr) + return nil } diff --git a/mihomo/transport/xhttp/server.go b/mihomo/transport/xhttp/server.go index c8b4fa84c0..4f41019ce1 100644 --- a/mihomo/transport/xhttp/server.go +++ b/mihomo/transport/xhttp/server.go @@ -1,6 +1,9 @@ package xhttp import ( + "bytes" + "encoding/base64" + "fmt" "io" "net" "strconv" @@ -98,6 +101,7 @@ type requestHandler struct { connHandler func(net.Conn) httpHandler http.Handler + xPaddingBytes Range scMaxEachPostBytes Range scStreamUpServerSecs Range scMaxBufferedPosts Range @@ -107,6 +111,10 @@ type requestHandler struct { } func NewServerHandler(opt ServerOption) (http.Handler, error) { + xPaddingBytes, err := opt.Config.GetNormalizedXPaddingBytes() + if err != nil { + return nil, err + } scMaxEachPostBytes, err := opt.Config.GetNormalizedScMaxEachPostBytes() if err != nil { return nil, err @@ -125,6 +133,7 @@ func NewServerHandler(opt ServerOption) (http.Handler, error) { config: opt.Config, connHandler: opt.ConnHandler, httpHandler: opt.HttpHandler, + xPaddingBytes: xPaddingBytes, scMaxEachPostBytes: scMaxEachPostBytes, scStreamUpServerSecs: scStreamUpServerSecs, scMaxBufferedPosts: scMaxBufferedPosts, @@ -134,7 +143,7 @@ func NewServerHandler(opt ServerOption) (http.Handler, error) { }), nil } -func (h *requestHandler) getOrCreateSession(sessionID string) *httpSession { +func (h *requestHandler) upsertSession(sessionID string) *httpSession { h.mu.Lock() defer h.mu.Unlock() @@ -161,8 +170,6 @@ func (h *requestHandler) getOrCreateSession(sessionID string) *httpSession { return s } - - func (h *requestHandler) deleteSession(sessionID string) { h.mu.Lock() defer h.mu.Unlock() @@ -239,11 +246,227 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - rest := strings.TrimPrefix(r.URL.Path, path) - parts := splitNonEmpty(rest) + h.config.WriteResponseHeader(w, r.Method, r.Header) + length := h.xPaddingBytes.Rand() + config := XPaddingConfig{Length: length} + + if h.config.XPaddingObfsMode { + config.Placement = XPaddingPlacement{ + Placement: h.config.XPaddingPlacement, + Key: h.config.XPaddingKey, + Header: h.config.XPaddingHeader, + } + config.Method = PaddingMethod(h.config.XPaddingMethod) + } else { + config.Placement = XPaddingPlacement{ + Placement: PlacementHeader, + Header: "X-Padding", + } + } + + h.config.ApplyXPaddingToResponse(w, config) + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + paddingValue, _ := h.config.ExtractXPaddingFromRequest(r, h.config.XPaddingObfsMode) + if !h.config.IsPaddingValid(paddingValue, h.xPaddingBytes.Min, h.xPaddingBytes.Max, PaddingMethod(h.config.XPaddingMethod)) { + http.Error(w, "invalid xpadding", http.StatusBadRequest) + return + } + sessionId, seqStr := h.config.ExtractMetaFromRequest(r, path) + + var currentSession *httpSession + if sessionId != "" { + currentSession = h.upsertSession(sessionId) + } + + // stream-up upload: POST /path/{session} + if r.Method != http.MethodGet && sessionId != "" && seqStr == "" && h.allowStreamUpUpload() { + httpSC := newHTTPServerConn(w, r.Body) + err := currentSession.uploadQueue.Push(Packet{ + Reader: httpSC, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusConflict) + 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 != "" && h.scStreamUpServerSecs.Max > 0 { + go func() { + for { + _, err := httpSC.Write(bytes.Repeat([]byte{'X'}, int(h.xPaddingBytes.Rand()))) + if err != nil { + break + } + time.Sleep(time.Duration(h.scStreamUpServerSecs.Rand()) * time.Second) + } + }() + } + + select { + case <-r.Context().Done(): + case <-httpSC.Wait(): + } + + _ = httpSC.Close() + return + } + + // packet-up upload: POST /path/{session}/{seq} + if r.Method != http.MethodGet && sessionId != "" && seqStr != "" && h.allowPacketUpUpload() { + scMaxEachPostBytes := h.scMaxEachPostBytes.Max + dataPlacement := h.config.GetNormalizedUplinkDataPlacement() + uplinkDataKey := h.config.UplinkDataKey + var headerPayload []byte + var err error + if dataPlacement == PlacementAuto || dataPlacement == PlacementHeader { + var headerPayloadChunks []string + for i := 0; true; i++ { + chunk := r.Header.Get(fmt.Sprintf("%s-%d", uplinkDataKey, i)) + if chunk == "" { + break + } + headerPayloadChunks = append(headerPayloadChunks, chunk) + } + headerPayloadEncoded := strings.Join(headerPayloadChunks, "") + headerPayload, err = base64.RawURLEncoding.DecodeString(headerPayloadEncoded) + if err != nil { + http.Error(w, "invalid base64 in header's payload", http.StatusBadRequest) + return + } + } + + var cookiePayload []byte + if dataPlacement == PlacementAuto || dataPlacement == PlacementCookie { + var cookiePayloadChunks []string + for i := 0; true; i++ { + cookieName := fmt.Sprintf("%s_%d", uplinkDataKey, i) + if c, _ := r.Cookie(cookieName); c != nil { + cookiePayloadChunks = append(cookiePayloadChunks, c.Value) + } else { + break + } + } + cookiePayloadEncoded := strings.Join(cookiePayloadChunks, "") + cookiePayload, err = base64.RawURLEncoding.DecodeString(cookiePayloadEncoded) + if err != nil { + http.Error(w, "invalid base64 in cookies' payload", http.StatusBadRequest) + return + } + } + + var bodyPayload []byte + if dataPlacement == PlacementAuto || dataPlacement == PlacementBody { + if r.ContentLength > int64(scMaxEachPostBytes) { + http.Error(w, "body too large", http.StatusRequestEntityTooLarge) + return + } + bodyPayload, err = io.ReadAll(io.LimitReader(r.Body, int64(scMaxEachPostBytes)+1)) + if err != nil { + http.Error(w, "failed to read body", http.StatusBadRequest) + return + } + } + + var payload []byte + switch dataPlacement { + case PlacementHeader: + payload = headerPayload + case PlacementCookie: + payload = cookiePayload + case PlacementBody: + payload = bodyPayload + case PlacementAuto: + payload = headerPayload + payload = append(payload, cookiePayload...) + payload = append(payload, bodyPayload...) + } + + if len(payload) > h.scMaxEachPostBytes.Max { + http.Error(w, "body too large", http.StatusRequestEntityTooLarge) + return + } + + seq, err := strconv.ParseUint(seqStr, 10, 64) + if err != nil { + http.Error(w, "invalid xhttp seq", http.StatusBadRequest) + return + } + + err = currentSession.uploadQueue.Push(Packet{ + Seq: seq, + Payload: payload, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if len(payload) == 0 { + // Methods without a body are usually cached by default. + w.Header().Set("Cache-Control", "no-store") + } + w.WriteHeader(http.StatusOK) + return + } + + // stream-up/packet-up download: GET /path/{session} + if r.Method == http.MethodGet && sessionId != "" && seqStr == "" && h.allowSessionDownload() { + currentSession.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() + } + + httpSC := newHTTPServerConn(w, r.Body) + conn := &Conn{ + writer: httpSC, + reader: currentSession.uploadQueue, + onClose: func() { + h.deleteSession(sessionId) + }, + } + httputils.SetAddrFromRequest(&conn.NetAddr, r) + + go h.connHandler(N.NewDeadlineConn(conn)) + + select { + case <-r.Context().Done(): + case <-httpSC.Wait(): + } + + _ = conn.Close() + return + } // stream-one: POST /path - if r.Method == http.MethodPost && len(parts) == 0 && h.allowStreamOne() { + if r.Method != http.MethodGet && sessionId == "" && seqStr == "" && h.allowStreamOne() { w.Header().Set("X-Accel-Buffering", "no") w.Header().Set("Cache-Control", "no-store") w.WriteHeader(http.StatusOK) @@ -269,137 +492,6 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - // stream-up/packet-up download: GET /path/{session} - if r.Method == http.MethodGet && len(parts) == 1 && h.allowSessionDownload() { - 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() - } - - httpSC := newHTTPServerConn(w, r.Body) - conn := &Conn{ - writer: httpSC, - reader: session.uploadQueue, - onClose: func() { - h.deleteSession(sessionID) - }, - } - httputils.SetAddrFromRequest(&conn.NetAddr, r) - - go h.connHandler(N.NewDeadlineConn(conn)) - - select { - case <-r.Context().Done(): - case <-httpSC.Wait(): - } - - _ = conn.Close() - return - } - - // stream-up upload: POST /path/{session} - if r.Method == http.MethodPost && len(parts) == 1 && h.allowStreamUpUpload() { - sessionID := parts[0] - session := h.getOrCreateSession(sessionID) - - 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 != "" && h.scStreamUpServerSecs.Max > 0 { - go func() { - for { - paddingValue, _ := h.config.RandomPadding() - if paddingValue == "" { - break - } - _, err = httpSC.Write([]byte(paddingValue)) - if err != nil { - break - } - time.Sleep(time.Duration(h.scStreamUpServerSecs.Rand()) * 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 && h.allowPacketUpUpload() { - sessionID := parts[0] - seq, err := strconv.ParseUint(parts[1], 10, 64) - if err != nil { - http.Error(w, "invalid xhttp seq", http.StatusBadRequest) - return - } - - session := h.getOrCreateSession(sessionID) - - if r.ContentLength > int64(h.scMaxEachPostBytes.Max) { - http.Error(w, "body too large", http.StatusRequestEntityTooLarge) - return - } - - body, err := io.ReadAll(io.LimitReader(r.Body, int64(h.scMaxEachPostBytes.Max)+1)) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - err = session.uploadQueue.Push(Packet{ - Seq: seq, - Payload: body, - }) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - if len(body) == 0 { - w.Header().Set("Cache-Control", "no-store") - } - w.WriteHeader(http.StatusOK) - return - } - http.NotFound(w, r) } diff --git a/mihomo/transport/xhttp/server_test.go b/mihomo/transport/xhttp/server_test.go index f7d951d8ff..b194186b80 100644 --- a/mihomo/transport/xhttp/server_test.go +++ b/mihomo/transport/xhttp/server_test.go @@ -78,22 +78,24 @@ func TestServerHandlerModeRestrictions(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { + config := Config{ + Path: "/xhttp", + Mode: testCase.mode, + } handler, err := NewServerHandler(ServerOption{ - Config: Config{ - Path: "/xhttp", - Mode: testCase.mode, - }, + Config: config, ConnHandler: func(conn net.Conn) { _ = conn.Close() }, }) - if err != nil { - panic(err) - } + assert.NoError(t, err) req := httptest.NewRequest(testCase.method, testCase.target, io.NopCloser(http.NoBody)) recorder := httptest.NewRecorder() + err = config.FillStreamRequest(req, "") + assert.NoError(t, err) + handler.ServeHTTP(recorder, req) assert.Equal(t, testCase.wantStatus, recorder.Result().StatusCode) diff --git a/mihomo/transport/xhttp/xpadding.go b/mihomo/transport/xhttp/xpadding.go new file mode 100644 index 0000000000..3b22a019e0 --- /dev/null +++ b/mihomo/transport/xhttp/xpadding.go @@ -0,0 +1,333 @@ +package xhttp + +import ( + "crypto/rand" + "fmt" + "math" + "net/url" + "strings" + + "github.com/metacubex/http" + "golang.org/x/net/http2/hpack" +) + +type PaddingMethod string + +const ( + PaddingMethodRepeatX PaddingMethod = "repeat-x" + PaddingMethodTokenish PaddingMethod = "tokenish" +) + +const charsetBase62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + +// Huffman encoding gives ~20% size reduction for base62 sequences +const avgHuffmanBytesPerCharBase62 = 0.8 + +const validationTolerance = 2 + +type XPaddingPlacement struct { + Placement string + Key string + Header string + RawURL string +} + +type XPaddingConfig struct { + Length int + Placement XPaddingPlacement + Method PaddingMethod +} + +func randStringFromCharset(n int, charset string) (string, bool) { + if n <= 0 || len(charset) == 0 { + return "", false + } + + m := len(charset) + limit := byte(256 - (256 % m)) + + result := make([]byte, n) + i := 0 + + buf := make([]byte, 256) + for i < n { + if _, err := rand.Read(buf); err != nil { + return "", false + } + for _, rb := range buf { + if rb >= limit { + continue + } + result[i] = charset[int(rb)%m] + i++ + if i == n { + break + } + } + } + + return string(result), true +} + +func absInt(x int) int { + if x < 0 { + return -x + } + return x +} + +func GenerateTokenishPaddingBase62(targetHuffmanBytes int) string { + n := int(math.Ceil(float64(targetHuffmanBytes) / avgHuffmanBytesPerCharBase62)) + if n < 1 { + n = 1 + } + + randBase62Str, ok := randStringFromCharset(n, charsetBase62) + if !ok { + return "" + } + + const maxIter = 150 + adjustChar := byte('X') + + // Adjust until close enough + for iter := 0; iter < maxIter; iter++ { + currentLength := int(hpack.HuffmanEncodeLength(randBase62Str)) + diff := currentLength - targetHuffmanBytes + + if absInt(diff) <= validationTolerance { + return randBase62Str + } + + if diff < 0 { + // Too small -> append padding char(s) + randBase62Str += string(adjustChar) + + // Avoid a long run of identical chars + if adjustChar == 'X' { + adjustChar = 'Z' + } else { + adjustChar = 'X' + } + } else { + // Too big -> remove from the end + if len(randBase62Str) <= 1 { + return randBase62Str + } + randBase62Str = randBase62Str[:len(randBase62Str)-1] + } + } + + return randBase62Str +} + +func GeneratePadding(method PaddingMethod, length int) string { + if length <= 0 { + return "" + } + + // https://www.rfc-editor.org/rfc/rfc7541.html#appendix-B + // h2's HPACK Header Compression feature employs a huffman encoding using a static table. + // 'X' and 'Z' are assigned an 8 bit code, so HPACK compression won't change actual padding length on the wire. + // https://www.rfc-editor.org/rfc/rfc9204.html#section-4.1.2-2 + // h3's similar QPACK feature uses the same huffman table. + + switch method { + case PaddingMethodRepeatX: + return strings.Repeat("X", length) + case PaddingMethodTokenish: + paddingValue := GenerateTokenishPaddingBase62(length) + if paddingValue == "" { + return strings.Repeat("X", length) + } + return paddingValue + default: + return strings.Repeat("X", length) + } +} + +func ApplyPaddingToCookie(req *http.Request, name, value string) { + if req == nil || name == "" || value == "" { + return + } + req.AddCookie(&http.Cookie{ + Name: name, + Value: value, + Path: "/", + }) +} + +func ApplyPaddingToResponseCookie(writer http.ResponseWriter, name, value string) { + if name == "" || value == "" { + return + } + http.SetCookie(writer, &http.Cookie{ + Name: name, + Value: value, + Path: "/", + }) +} + +func ApplyPaddingToQuery(u *url.URL, key, value string) { + if u == nil || key == "" || value == "" { + return + } + q := u.Query() + q.Set(key, value) + u.RawQuery = q.Encode() +} + +func (c *Config) GetNormalizedXPaddingBytes() (Range, error) { + r, err := ParseRange(c.XPaddingBytes, "100-1000") + if err != nil { + return Range{}, fmt.Errorf("invalid x-padding-bytes: %w", err) + } + return r, nil +} + +func (c *Config) ApplyXPaddingToHeader(h http.Header, config XPaddingConfig) { + if h == nil { + return + } + + paddingValue := GeneratePadding(config.Method, config.Length) + + switch p := config.Placement; p.Placement { + case PlacementHeader: + h.Set(p.Header, paddingValue) + case PlacementQueryInHeader: + u, err := url.Parse(p.RawURL) + if err != nil || u == nil { + return + } + u.RawQuery = p.Key + "=" + paddingValue + h.Set(p.Header, u.String()) + } +} + +func (c *Config) ApplyXPaddingToRequest(req *http.Request, config XPaddingConfig) { + if req == nil { + return + } + if req.Header == nil { + req.Header = make(http.Header) + } + + placement := config.Placement.Placement + + if placement == PlacementHeader || placement == PlacementQueryInHeader { + c.ApplyXPaddingToHeader(req.Header, config) + return + } + + paddingValue := GeneratePadding(config.Method, config.Length) + + switch placement { + case PlacementCookie: + ApplyPaddingToCookie(req, config.Placement.Key, paddingValue) + case PlacementQuery: + ApplyPaddingToQuery(req.URL, config.Placement.Key, paddingValue) + } +} + +func (c *Config) ApplyXPaddingToResponse(writer http.ResponseWriter, config XPaddingConfig) { + placement := config.Placement.Placement + + if placement == PlacementHeader || placement == PlacementQueryInHeader { + c.ApplyXPaddingToHeader(writer.Header(), config) + return + } + + paddingValue := GeneratePadding(config.Method, config.Length) + + switch placement { + case PlacementCookie: + ApplyPaddingToResponseCookie(writer, config.Placement.Key, paddingValue) + } +} + +func (c *Config) ExtractXPaddingFromRequest(req *http.Request, obfsMode bool) (string, string) { + if req == nil { + return "", "" + } + + if !obfsMode { + referrer := req.Header.Get("Referer") + + if referrer != "" { + if referrerURL, err := url.Parse(referrer); err == nil { + paddingValue := referrerURL.Query().Get("x_padding") + paddingPlacement := PlacementQueryInHeader + "=Referer, key=x_padding" + return paddingValue, paddingPlacement + } + } else { + paddingValue := req.URL.Query().Get("x_padding") + return paddingValue, PlacementQuery + ", key=x_padding" + } + } + + key := c.XPaddingKey + header := c.XPaddingHeader + + if cookie, err := req.Cookie(key); err == nil { + if cookie != nil && cookie.Value != "" { + paddingValue := cookie.Value + paddingPlacement := PlacementCookie + ", key=" + key + return paddingValue, paddingPlacement + } + } + + headerValue := req.Header.Get(header) + + if headerValue != "" { + if c.XPaddingPlacement == PlacementHeader { + paddingPlacement := PlacementHeader + "=" + header + return headerValue, paddingPlacement + } + + if parsedURL, err := url.Parse(headerValue); err == nil { + paddingPlacement := PlacementQueryInHeader + "=" + header + ", key=" + key + + return parsedURL.Query().Get(key), paddingPlacement + } + } + + queryValue := req.URL.Query().Get(key) + + if queryValue != "" { + paddingPlacement := PlacementQuery + ", key=" + key + return queryValue, paddingPlacement + } + + return "", "" +} + +func (c *Config) IsPaddingValid(paddingValue string, from, to int, method PaddingMethod) bool { + if paddingValue == "" { + return false + } + if to <= 0 { + if r, err := c.GetNormalizedXPaddingBytes(); err == nil { + from, to = r.Min, r.Max + } + } + + switch method { + case PaddingMethodRepeatX: + n := len(paddingValue) + return n >= from && n <= to + case PaddingMethodTokenish: + const tolerance = validationTolerance + + n := int(hpack.HuffmanEncodeLength(paddingValue)) + f := from - tolerance + t := to + tolerance + if f < 0 { + f = 0 + } + return n >= f && n <= t + default: + n := len(paddingValue) + return n >= from && n <= to + } +} diff --git a/openwrt-packages/ddns-go/Makefile b/openwrt-packages/ddns-go/Makefile index 77f73a3f05..b7477bdff3 100644 --- a/openwrt-packages/ddns-go/Makefile +++ b/openwrt-packages/ddns-go/Makefile @@ -8,12 +8,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=ddns-go -PKG_VERSION:=6.16.8 +PKG_VERSION:=6.16.9 PKG_RELEASE:=1 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/jeessy2/ddns-go/tar.gz/v$(PKG_VERSION)? -PKG_HASH:=1fe5a923fc2ebaf73f46dabf905c307e8149bb33cda5b5d81a962f4cc47bef9c +PKG_HASH:=1a961050870e5a706c124de498ab84510f19ea49d9284b890a64860e1570ead2 PKG_LICENSE:=MIT PKG_LICENSE_FILES:=LICENSE diff --git a/openwrt-packages/luci-app-amlogic/Makefile b/openwrt-packages/luci-app-amlogic/Makefile index d4c8fbbadf..5675fc81d4 100644 --- a/openwrt-packages/luci-app-amlogic/Makefile +++ b/openwrt-packages/luci-app-amlogic/Makefile @@ -16,7 +16,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-amlogic -PKG_VERSION:=3.1.288 +PKG_VERSION:=3.1.289 PKG_RELEASE:=1 PKG_LICENSE:=GPL-2.0 License diff --git a/openwrt-packages/luci-app-amlogic/root/etc/config/amlogic b/openwrt-packages/luci-app-amlogic/root/etc/config/amlogic index d7c13aa709..a152eb8ff8 100644 --- a/openwrt-packages/luci-app-amlogic/root/etc/config/amlogic +++ b/openwrt-packages/luci-app-amlogic/root/etc/config/amlogic @@ -4,7 +4,7 @@ config amlogic 'config' option amlogic_firmware_repo 'https://github.com/breakingbadboy/OpenWrt' option amlogic_firmware_tag 'ARMv8' option amlogic_firmware_suffix '.img.gz' - option amlogic_kernel_path 'https://github.com/breakingbadboy/OpenWrt' + option amlogic_kernel_path 'https://github.com/ophub/kernel' option amlogic_kernel_tags '' option amlogic_kernel_branch '' option amlogic_firmware_config '1' diff --git a/openwrt-packages/luci-app-amlogic/root/usr/share/amlogic/amlogic_check_kernel.sh b/openwrt-packages/luci-app-amlogic/root/usr/share/amlogic/amlogic_check_kernel.sh index fec3989dce..6cd8a0f80f 100755 --- a/openwrt-packages/luci-app-amlogic/root/usr/share/amlogic/amlogic_check_kernel.sh +++ b/openwrt-packages/luci-app-amlogic/root/usr/share/amlogic/amlogic_check_kernel.sh @@ -133,6 +133,8 @@ else kernel_tag="rk3588" elif [[ "${kernel_uname}" =~ -rk35xx ]]; then kernel_tag="rk35xx" + elif [[ "${kernel_uname}" =~ -flippy ]]; then + kernel_tag="flippy" elif [[ "${kernel_uname}" =~ -h6|-zicai ]]; then kernel_tag="h6" else diff --git a/openwrt-packages/luci-app-netwizard/Makefile b/openwrt-packages/luci-app-netwizard/Makefile new file mode 100644 index 0000000000..138b8dac47 --- /dev/null +++ b/openwrt-packages/luci-app-netwizard/Makefile @@ -0,0 +1,38 @@ +# +# Copyright (C) 2017-2019 Chen Minqiang +# Copyright 2022-2025 sirpdboy +# This is free software, licensed under the GNU General Public License v3. +# See /LICENSE for more information. +# + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI Support for wizard +LUCI_DEPENDS:= +LUCI_PKGARCH:=all +PKG_NAME:=luci-app-netwizard +PKG_VERSION:=2.1.5 +PKG_RELEASE:=20260312 + +PKG_LICENSE:=GPLv3 +PKG_LICENSE_FILES:=LICENSE +PKG_MAINTAINER:=sirpdboy + + +define Package/luci-app-netwizard + CATEGORY:=X + SUBMENU:=Configuration netWizard Support + TITLE:=LuCI Support for netwizard + PKGARCH:=all +endef + +define Package/luci-app-netwizard/description + LuCI Support for netwizard. +endef +define Package/luci-app-netwizard/conffiles +/etc/config/netwizard +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature \ No newline at end of file diff --git a/openwrt-packages/luci-app-netwizard/htdocs/luci-static/resources/view/netwizard/netwizard.js b/openwrt-packages/luci-app-netwizard/htdocs/luci-static/resources/view/netwizard/netwizard.js new file mode 100644 index 0000000000..daba7ecbe5 --- /dev/null +++ b/openwrt-packages/luci-app-netwizard/htdocs/luci-static/resources/view/netwizard/netwizard.js @@ -0,0 +1,1228 @@ +// Copyright 2023-2026 sirpdboy +'use strict'; +'require view'; +'require form'; +'require network'; +'require uci'; +'require validation'; +'require rpc'; +'require fs'; +'require dom'; +'require poll'; +'require tools.widgets as widgets'; + +var callExecRPC = rpc.declare({ + object: 'file', + method: 'exec', + params: ['command', 'params'], + expect: { '': {} } +}); + + +return view.extend({ + load: function() { + return Promise.all([ + network.getDevices(), + uci.changes(), + L.resolveDefault(uci.load('wireless'), null), + uci.load('network'), + uci.load('netwizard') + ]); + }, + + render: function(data) { + var devices = data[0] || []; + var has_wifi = false; + var m, o, s; + + try { + var wirelessSections = uci.sections('wireless', 'wifi-device'); + if (wirelessSections && wirelessSections.length > 0) { + has_wifi = true; + } else { + var wifiIfaces = uci.sections('wireless', 'wifi-iface'); + if (wifiIfaces && wifiIfaces.length > 0) { + has_wifi = true; + } + } + } catch (e) { + has_wifi = false; + } + + var physicalIfaces = 0; + var physicalInterfaces = []; + + for (var i = 0; i < devices.length; i++) { + var iface = devices[i].getName(); + if (!iface.match(/_ifb$/) && !iface.match(/^ifb/) && + !iface.match(/^veth/) && !iface.match(/^tun/) && + !iface.match(/^tap/) && !iface.match(/^gre/) && + !iface.match(/^gretap/) && !iface.match(/^lo$/) && + !iface.match(/^br-/) && + (iface.match(/^(eth|en|usb)/) || iface.match(/^wlan|^wl/))) { + + physicalIfaces++; + physicalInterfaces.push(iface); + } + } + + var lan_ip = uci.get('netwizard', 'default', 'lan_ipaddr'); + var lan_mask = uci.get('netwizard', 'default', 'lan_netmask'); + var wan_face = uci.get('netwizard', 'default', 'wan_interface'); + var wanproto = uci.get('netwizard', 'default', 'wan_proto'); + var LanHTTPS = uci.get('netwizard', 'default', 'https') || '0'; + + if (!lan_ip) { + lan_ip = uci.get('network', 'lan', 'ipaddr') || '192.168.10.1/24' ; + lan_ip = (lan_ip + ''); + if (lan_ip.indexOf('/') > -1) { + lan_ip = lan_ip.split('/')[0]; + } + } + + if (!lan_mask) { + lan_mask = uci.get('network', 'lan', 'netmask') || '255.255.255.0' ; + } + + if (!wan_face) { + wan_face = uci.get('network', 'wan', 'device') || 'eth1'; + } + + if (!wanproto) { + wanproto = uci.get('network', 'wan', 'proto') || 'siderouter'; + } + + this.devices = devices; + this.has_wifi = has_wifi; + this.physicalIfaces = physicalIfaces; + this.physicalInterfaces = physicalInterfaces; + this.lan_mask = lan_mask; + this.lan_ip = lan_ip; + this.LanHTTPS = LanHTTPS; + this.wan_face = wan_face; + this.wanproto = wanproto; + + this.addStyles(); + + var params = new URLSearchParams(window.location.search); + var selectedMode = params.get('selectedMode'); + + if (selectedMode) { + return this.renderConfigForm(selectedMode); + } else { + return this.renderModeSelection(); + } + }, + + addStylesnobnt: function() { + if (document.getElementById('netwizard-mode-styles-nobnt')) { + return; + } + + var stylen = E('style', { 'id': 'netwizard-mode-styles-nobnt' }, ` + #view .cbi-page-actions { + display: none; + } + `); + + document.head.appendChild(stylen); + }, + + addStyles: function() { + if (document.getElementById('netwizard-mode-styles')) { + return; + } + + var style = E('style', { 'id': 'netwizard-mode-styles' }, ` + .mode-selection-container { + margin-top: 5rem; + padding: 1rem; + } + + .mode-grid { + display: flex; + flex-wrap: wrap; + gap: 20px; + margin: 30px 0; + justify-content: center; + } + + .mode-card { + border-radius: 8px; + padding: 4rem 1rem; + cursor: pointer; + transition: all 0.3s; + text-align: center; + flex: 1; + min-width: 180px; + max-width: 180px; + box-shadow: 0 0.3rem 0.5rem var(--input-boxcolor); + display: flex; + flex-direction: column; + align-items: center; + border: 2px solid transparent; + } + + .mode-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0,0,0,0.1); + } + + .mode-card[data-mode="pppoe"] { + background: rgba(255,107,107,0.7); + border-color: rgba(255,107,107,0.7); + color: white; + } + + .mode-card[data-mode="pppoe"]:hover { + border-color: #ff3838; + box-shadow: 0 4px 12px rgba(255, 71, 87, 0.3); + } + + .mode-card[data-mode="dhcp"] { + background: rgba(51,154,240,0.7); + border-color: rgba(51,154,240,0.7); + color: white; + } + + .mode-card[data-mode="dhcp"]:hover { + border-color: #01b7ff; + box-shadow: 0 4px 12px rgba(34, 139, 230, 0.3); + } + + .mode-card[data-mode="siderouter"] { + background: rgba(81,207,102,0.7); + border-color: rgba(81,207,102,0.7); + color: white; + } + + .mode-card[data-mode="siderouter"]:hover { + border-color: #27f94d; + box-shadow: 0 4px 12px rgba(64, 192, 87, 0.3); + } + + .mode-icon-container { + width: 64px; + height: 64px; + margin-bottom: 15px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255,1); + border-radius: 10%; + padding: 10px; + box-shadow: 0 0.3rem 0.5rem rgba(0,0,0,0.22); + } + + .mode-icon { + width: 48px; + height: 48px; + object-fit: contain; + } + + .mode-title { + font-size: 16px; + font-weight: 600; + margin-top: 10px; + text-align: center; + } + + .mode-description { + font-size: 13px; + line-height: 1.4; + margin-bottom: 15px; + min-height: 60px; + text-align: center; + opacity: 0.9; + } + + .quick-nav-buttons { + display: flex; + justify-content: center; + gap: 10px; + margin: 20px; + flex-wrap: wrap; + } + + .quick-nav-btn { + color: white; + border: none; + border-radius: 4px; + font-size: 14px; + line-height: 1rem; + cursor: pointer; + transition: background 0.3s; + text-decoration: none; + display: inline-block; + } + + .mode-info-header { + border-radius: 8px; + padding: 1rem; + margin: 0 2% 2% 2%; + display: flex; + align-items: center; + gap: 15px; + } + + .mode-info-content { + flex: 1; + } + + .mode-info-header[data-mode="pppoe"] { + background: rgba(255,107,107,0.7); + } + + .mode-info-header[data-mode="dhcp"] { + background: rgba(51,154,240,0.7); + } + + .mode-info-header[data-mode="siderouter"] { + background: rgba(81,207,102,0.7); + } + + @media (max-width: 768px) { + .mode-selection-container { + margin-top: 0; + padding: 0; + } + .mode-grid { + flex-direction: column; + align-items: center; + } + + .mode-card { + min-width: 90%; + max-width: 90%; + } + + .quick-nav-buttons { + flex-direction: column; + } + + .quick-nav-btn { + width: 90%; + text-align: center; + } + + .mode-info-header { + flex-direction: column; + text-align: center; + } + } + `); + + document.head.appendChild(style); + }, + + getModeIcon: function(mode) { + var svgCode; + var color = this.getModeColor(mode); + + switch(mode) { + case 'pppoe': + svgCode = ``; + break; + case 'dhcp': + svgCode = ` + +`; + break; + case 'siderouter': + svgCode = ` + +`; + break; + } + + var svgUrl = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgCode))); + return '' + mode + ' icon'; + }, + + getModeTitle: function(mode) { + switch(mode) { + case 'pppoe': return _('PPPoE Dial-up'); + case 'dhcp': return _('DHCP Client'); + case 'siderouter': return _('Side Router'); + default: return _('WAN Settings'); + } + }, + + getModeDescription: function(mode) { + switch(mode) { + case 'pppoe': return _('Fiber broadband that requires username/password'); + case 'dhcp': return _('Connect to router as a subordinate router to internet'); + case 'siderouter': return _('Configure as side router in same network as main router'); + default: return _('Network connection mode'); + } + }, + + getModeColor: function(mode) { + switch(mode) { + case 'pppoe': return '#ff6b6b'; + case 'dhcp': return '#339af0'; + case 'siderouter': return '#51cf66'; + default: return '#36c'; + } + }, + + renderModeSelection: function() { + var container = E('div', { 'class': 'mode-selection-container' }, [ + E('h2', { 'style': 'margin-top: 4%;margin-bottom: 15px;text-align: center;padding: 1rem;font-size: 1.8rem;font-weight: 600;' }, + _('Select Network Connection Mode')), + E('p', { 'style': 'margin-bottom: 1rem;text-align: center;font-size: 1.1rem;' }, + _('Choose the connection mode that matches your network environment')) + ]); + + var modeGrid = E('div', { 'class': 'mode-grid' }); + + var modes = [ + { id: 'pppoe' }, + { id: 'dhcp' }, + { id: 'siderouter' } + ]; + + var self = this; + modes.forEach(function(mode) { + var iconDiv = E('div', { + 'class': 'mode-icon-container' + }); + + iconDiv.innerHTML = self.getModeIcon(mode.id); + + var card = E('div', { + 'class': 'mode-card', + 'data-mode': mode.id + }, [ + iconDiv, + E('div', { 'class': 'mode-title' }, self.getModeTitle(mode.id)), + ]); + + card.addEventListener('click', function() { + self.selectMode(mode.id); + }); + self.addStylesnobnt(); + modeGrid.appendChild(card); + }); + + container.appendChild(modeGrid); + return container; + }, + + selectMode: function(mode) { + uci.set('netwizard', 'default', 'wan_proto', mode); + uci.save(); + var currentUrl = window.location.pathname; + var newUrl = currentUrl + '?selectedMode=' + mode + '&tab=wansetup'; + window.location.href = newUrl; + }, + + renderConfigForm: function(selectedMode) { + var wanproto = selectedMode || this.wanproto; + + var m = new form.Map('netwizard', _('Quick Network Setup Wizard'), + _('Quick network setup wizard. If you need more settings, please enter network - interface to set.')); + + var s = m.section(form.NamedSection, 'default'); + s.addremove = false; + s.anonymous = true; + + s.tab('modesetup', _('Network Mode')); + s.tab('wansetup', _('WAN Settings')); + if (this.has_wifi) { + s.tab('wifisetup', _('Wireless Settings'), _('Set the router\'s wireless name and password. For more advanced settings, please go to the Network-Wireless page.')); + } + s.tab('othersetup', _('Other Settings')); + + var modeTitle = this.getModeTitle(wanproto); + var modeIcon = this.getModeIcon(wanproto); + var modeDescription = this.getModeDescription(wanproto); + var modeColor = this.getModeColor(wanproto); + + var o = s.taboption('modesetup', form.DummyValue, 'current_mode', _('Current Network Mode')); + o.rawhtml = true; + o.default = '
' + + '
' + modeIcon + '
' + + '

' + modeTitle + '

' + + '

' + modeDescription + '

' + + '
' + + '' + + '' + + '↻ ' + _('Change Mode') + '' + + '
' + + '
'; + + var modeInfoHeader = s.taboption('wansetup', form.DummyValue, 'mode_info_header', ''); + modeInfoHeader.rawhtml = true; + modeInfoHeader.default = '
' + + '
' + modeIcon + '
' + + '
' + + '

' + modeTitle + '

' + + '

' + modeDescription + '

' + + '' + + '
' + + '
'; + + o = s.taboption('modesetup', form.ListValue, 'wan_proto', _('Protocol'), + _('Three different ways to access the Internet, please choose according to your own situation.')); + o.default = wanproto; + o.value('dhcp', _('DHCP Client')); + o.value('pppoe', _('PPPoE Dial-up')); + o.value('siderouter', _('Side Router')); + o.rmempty = false; + o.readonly = true; + + o = s.taboption('wansetup', form.Flag, 'setlan', _('Add LAN port configuration')); + o.depends('wan_proto', 'pppoe'); + o.depends('wan_proto', 'dhcp'); + o.default = 0; + o.rmempty = false; + + o = s.taboption('wansetup', form.ListValue, 'lan_proto', _('LAN IP Address Mode'), + _('Warning: Setting up automatic IP address retrieval requires checking the IP address on the higher-level router')); + o.default = 'static'; + o.value('static', _('Static IP address (Specify non conflicting IP addresses)')); + o.value('dhcp', _('DHCP client (Main router assigns IP)')); + o.depends('wan_proto', 'siderouter'); + o.rmempty = false; + + o = s.taboption('wansetup', form.Value, 'lan_ipaddr', _('LAN IPv4 Address'), + _('You must specify the IP address of this machine, which is the IP address of the web access route')); + o.default = this.lan_ip; + o.datatype = 'ip4addr'; + o.rmempty = false; + o.depends({'wan_proto':'pppoe','setlan': '1'}); + o.depends({'wan_proto': 'dhcp' ,'setlan': '1'}); + o.depends({'wan_proto': 'siderouter', 'lan_proto': 'static' }); + + o = s.taboption('wansetup', form.Value, 'lan_netmask', _('LAN IPv4 Netmask')); + o.datatype = 'ip4addr'; + o.value('255.255.255.0'); + o.value('255.255.0.0'); + o.value('255.0.0.0'); + o.default = this.lan_mask; + o.depends({'wan_proto': 'siderouter', 'lan_proto': 'static'}); + o.depends({'wan_proto': 'pppoe','setlan': '1'}); + o.depends({'wan_proto': 'dhcp','setlan': '1'}); + o.rmempty = false; + + o = s.taboption('wansetup', form.Value, 'lan_gateway', _('LAN IPv4 Gateway'), + _('Please enter the main routing IP address. The bypass gateway is not the same as the login IP of this bypass WEB and is in the same network segment')); + o.depends({'wan_proto': 'siderouter', 'lan_proto': 'static'}); + o.datatype = 'ip4addr'; + o.rmempty = false; + + o = s.taboption('wansetup', form.ListValue, 'dhcp_proto', _('WAN interface IP address mode'), + _('Choose how to get IP address for WAN interface')); + o.default = 'dhcp'; + o.value('static', _('Static IP address (Specify non conflicting IP addresses)')); + o.value('dhcp', _('DHCP client (existing router assigns IP)')); + o.depends('wan_proto', 'dhcp'); + o.rmempty = false; + + o = s.taboption('wansetup', form.DynamicList, 'lan_dns', _('Use Custom SideRouter DNS')); + o.value('223.5.5.5', _('Ali DNS: 223.5.5.5')); + o.value('180.76.76.76', _('Baidu DNS: 180.76.76.76')); + o.value('114.114.114.114', _('114 DNS: 114.114.114.114')); + o.value('8.8.8.8', _('Google DNS: 8.8.8.8')); + o.value('1.1.1.1', _('Cloudflare DNS: 1.1.1.1')); + o.depends({'wan_proto': 'siderouter'}); + o.datatype = 'ip4addr'; + o.default = '223.5.5.5'; + o.rmempty = false; + + o = s.taboption('wansetup', widgets.DeviceSelect, 'wan_interface', + _('Device'), + _('Allocate the physical interface of WAN port')); + o.depends({'wan_proto': 'pppoe','setlan': '1'}); + o.depends({'wan_proto': 'dhcp','setlan': '1'}); + o.default = this.wan_face; + o.ucioption = 'wan_interface'; + o.nobridges = false; + o.rmempty = false; + + o = s.taboption('wansetup', form.Value, 'wan_pppoe_user', _('PAP/CHAP Username')); + o.depends('wan_proto', 'pppoe'); + o.rmempty = false; + + o = s.taboption('wansetup', form.Value, 'wan_pppoe_pass', _('PAP/CHAP Password')); + o.depends('wan_proto', 'pppoe'); + o.password = true; + o.rmempty = false; + + o = s.taboption('wansetup', form.Value, 'wan_ipaddr', _('WAN IPv4 Address')); + o.depends({'wan_proto': 'dhcp', 'dhcp_proto': 'static'}); + o.datatype = 'ip4addr'; + o.rmempty = false; + + o = s.taboption('wansetup', form.Value, 'wan_netmask', _('WAN IPv4 Netmask')); + o.depends({'wan_proto': 'dhcp', 'dhcp_proto': 'static'}); + o.datatype = 'ip4addr'; + o.value('255.255.255.0'); + o.value('255.255.0.0'); + o.value('255.0.0.0'); + o.default = '255.255.255.0'; + o.rmempty = false; + + o = s.taboption('wansetup', form.Value, 'wan_gateway', _('WAN IPv4 Gateway')); + o.depends({'wan_proto': 'dhcp', 'dhcp_proto': 'static'}); + o.datatype = 'ip4addr'; + o.rmempty = false; + + o = s.taboption('wansetup', form.DynamicList, 'wan_dns', _('Use custom DNS servers')); + o.value('', _('Auto-fetch')); + o.value('223.5.5.5', _('Ali DNS: 223.5.5.5')); + o.value('180.76.76.76', _('Baidu DNS: 180.76.76.76')); + o.value('114.114.114.114', _('114 DNS: 114.114.114.114')); + o.value('8.8.8.8', _('Google DNS: 8.8.8.8')); + o.value('1.1.1.1', _('Cloudflare DNS: 1.1.1.1')); + o.depends({'wan_proto': 'dhcp'}); + o.depends('wan_proto', 'pppoe'); + o.datatype = 'ip4addr'; + + o = s.taboption('wansetup', form.Flag, 'ipv6', _('Enable IPv6')); + o.default = '0'; + o.rmempty = false; + + o = s.taboption('wansetup', form.Flag, 'lan_dhcp', _('Disable DHCP Server'), + _('Selecting means that the DHCP server is not enabled. In a network, only one DHCP server is needed to allocate and manage client IPs. If it is a siderouter route, it is recommended to turn off the primary routing DHCP server.')); + o.default = '0'; + o.rmempty = false; + + o = s.taboption('wansetup', form.Flag, 'dnsset', _('Enable DNS Notifications (IPv4/IPv6)'), + _('Forcefully specify the DNS server for this router')); + o.depends('lan_dhcp', '0'); + o.default = '0'; + o.rmempty = false; + + o = s.taboption('wansetup', form.ListValue, 'dns_tables', _('Use custom DNS servers')); + o.value('1', _('Use local IP for DNS (default)')); + o.value('223.5.5.5', _('Ali DNS: 223.5.5.5')); + o.value('180.76.76.76', _('Baidu DNS: 180.76.76.76')); + o.value('114.114.114.114', _('114 DNS: 114.114.114.114')); + o.value('8.8.8.8', _('Google DNS: 8.8.8.8')); + o.value('1.1.1.1', _('Cloudflare DNS: 1.1.1.1')); + o.depends('dnsset', '1'); + o.rmempty = false; + + o = s.taboption('wansetup', form.Flag, 'https', _('Redirect to HTTPS'), + _('Enable automatic redirection of HTTP requests to HTTPS port.')); + o.default = '0'; + o.rmempty = false; + + if (this.has_wifi) { + var wifi_ssid = s.taboption('wifisetup', form.Value, 'wifi_ssid', _('ESSID')); + wifi_ssid.datatype = 'maxlength(32)'; + + var wifi_key = s.taboption('wifisetup', form.Value, 'wifi_key', _('Key')); + wifi_key.datatype = 'wpakey'; + wifi_key.password = true; + } + + o = s.taboption('othersetup', form.Flag, 'synflood', _('Enable SYN-flood Defense'), + _('Enable Firewall SYN-flood defense [Suggest opening]')); + o.default = '1'; + o.rmempty = false; + + o = s.taboption('othersetup', form.Flag, 'updatacheck', _('Enable detection update prompts')); + o.default = '0'; + o.rmempty = false; + + var originalSave = m.save; + var currentLanIP = this.lan_ip; + var currentHTTPS = this.LanHTTPS; + var self = this; + + function getNewLanIP() { + var selectors = [ + 'input[id="widget.cbid.netwizard.default.lan_ipaddr"]', + 'input[name="widget.cbid.netwizard.default.lan_ipaddr"]', + 'input[data-option="lan_ipaddr"]', + 'input[placeholder*="IP"]', + '.cbi-input-text[type="text"]' + ]; + + for (var i = 0; i < selectors.length; i++) { + var inputs = document.querySelectorAll(selectors[i]); + for (var j = 0; j < inputs.length; j++) { + var input = inputs[j]; + if (input && input.value) { + var ipMatch = input.value.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/); + if (ipMatch) { + var valid = true; + for (var k = 1; k <= 4; k++) { + var part = parseInt(ipMatch[k]); + if (part < 0 || part > 255) { + valid = false; + break; + } + } + if (valid) { + return input.value; + } + } + } + } + } + + return null; + } + + function getLanproto() { + return new Promise(function(resolve, reject) { + try { + var selectors = [ + 'select[id="widget.cbid.netwizard.default.lan_proto"]', + 'select[name="widget.cbid.netwizard.default.lan_proto"]' + ]; + + for (var i = 0; i < selectors.length; i++) { + var selects = document.querySelectorAll(selectors[i]); + for (var j = 0; j < selects.length; j++) { + var select = selects[j]; + if (select && select.value) { + resolve(select.value === 'dhcp'); + return; + } + } + } + + var lanProtoConfig = uci.get('netwizard', 'default', 'lan_proto'); + if (lanProtoConfig) { + resolve(lanProtoConfig === 'dhcp'); + return; + } + + resolve(false); + + } catch (error) { + resolve(false); + } + }); + } + + function getNewhttps() { + var selectors = [ + 'input[data-widget-id="widget.cbid.netwizard.default.https"]' + ]; + + for (var i = 0; i < selectors.length; i++) { + var inputs = document.querySelectorAll(selectors[i]); + for (var j = 0; j < inputs.length; j++) { + var input = inputs[j]; + if (input.type === 'checkbox') { + return input.checked ? '1' : '0'; + } else if (input.type === 'hidden') { + return input.value === '1' ? '1' : '0'; + } + } + } + return '0'; + } + + function showDHCPWarningMessage() { + var overlay = document.createElement('div'); + overlay.id = 'netwizard-dhcp-warning-overlay'; + overlay.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.85); + z-index: 9999; + display: flex; + justify-content: center; + align-items: center; + font-family: Arial, sans-serif; + `; + + var messageBox = document.createElement('div'); + messageBox.style.cssText = ` + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 2%; + border-radius: 15px; + text-align: center; + min-width: 300px; + max-width: 600px; + box-shadow: rgba(255, 255, 255, 0.2) 0px 20px 40px; + `; + + var warningIcon = document.createElement('div'); + warningIcon.innerHTML = '⚠️'; + warningIcon.style.cssText = ` + font-size: 60px; + margin-bottom: 10px; + animation: pulse 2s infinite; + `; + + var style = document.createElement('style'); + style.textContent = ` + @keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); } + } + `; + document.head.appendChild(style); + + var title = document.createElement('h2'); + title.textContent = _('Set LAN to DHCP mode'); + title.style.cssText = ` + margin: 0 0 20px 0; + color: #FFD700; + font-size: 28px; + `; + + var message = document.createElement('div'); + message.innerHTML = _('The router is now configured to obtain IP address via DHCP.') + + '
' + + '' + _('Important Note:') + '
' + + '1. ' + _('The current router IP address will be assigned by the DHCP server of the superior router') + '
' + + '2. ' + _('Please login to the superior router to view the DHCP client list') + '
' + + '3. ' + _('Or access using the original IP address on the current router') + '
' + + '4. ' + _('Unable to automatically redirect to the new IP address') + + '
' + + '
' + + _('Configuration has been saved successfully. You can manually access the router management interface.') + + '
'; + + message.style.cssText = ` + color: rgba(255,255,255,0.9); + line-height: 1.5rem; + font-size: 0.875rem; + `; + + var buttonContainer = document.createElement('div'); + buttonContainer.style.cssText = ` + display: flex; + justify-content: center; + margin-top: 1rem; + flex-wrap: wrap; + `; + + var closeButton = document.createElement('button'); + closeButton.textContent = _('Close'); + closeButton.style.cssText = ` + background: #4CAF50; + color: white; + border: none; + border-radius: 50px; + font-size: 16px; + font-weight: bold; + cursor: pointer; + padding: 0 30px; + transition: all 0.3s ease; + box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4); + `; + + closeButton.onmouseover = function() { + this.style.transform = 'translateY(-2px)'; + this.style.boxShadow = '0 8px 20px rgba(76, 175, 80, 0.6)'; + }; + + closeButton.onmouseout = function() { + this.style.transform = 'translateY(0)'; + this.style.boxShadow = '0 5px 15px rgba(76, 175, 80, 0.4)'; + }; + + closeButton.onclick = function() { + document.body.removeChild(overlay); + }; + + messageBox.appendChild(warningIcon); + messageBox.appendChild(title); + messageBox.appendChild(message); + buttonContainer.appendChild(closeButton); + messageBox.appendChild(buttonContainer); + overlay.appendChild(messageBox); + + document.body.appendChild(overlay); + } + + function showRedirectMessage(newIP, useHTTPS, isDHCP) { + if (isDHCP) { + showDHCPWarningMessage(); + return; + } + + var overlay = document.createElement('div'); + overlay.id = 'netwizard-redirect-overlay'; + overlay.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.85); + z-index: 9999; + display: flex; + justify-content: center; + align-items: center; + font-family: Arial, sans-serif; + `; + + var messageBox = document.createElement('div'); + messageBox.style.cssText = ` + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 1rem; + border-radius: 15px; + text-align: center; + min-width: 350px; + box-shadow: 0 20px 40px rgba(0,0,0,0.3); + color: white; + `; + + var icon = document.createElement('div'); + icon.innerHTML = '✓'; + icon.style.cssText = ` + font-size: 60px; + color: #4CAF50; + background: white; + width: 100px; + height: 100px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 20px; + font-weight: bold; + box-shadow: 0 10px 20px rgba(0,0,0,0.2); + `; + + var title = document.createElement('h2'); + title.textContent = _('Configuration Applied Successfully!'); + title.style.cssText = ` + margin: 0 0 20px 0; + color: white; + `; + + var protocolText = useHTTPS === '1' ? 'HTTPS' : 'HTTP'; + var message = document.createElement('div'); + + message.innerHTML = _('The network configuration has been saved and applied.

') + + '
' + + _('Redirecting to') + ' ' + + ''+ newIP + '
' + + _('Access Protocol:') + ' ' + + '' +' ' + protocolText + '' + + '

' + + _('The page will automatically redirect in') + ' ' + + '10' + ' ' + + _('seconds...'); + + message.style.cssText = ` + color: rgba(255,255,255,0.9); + line-height: 1.8; + margin: 20px 0; + font-size: 16px; + `; + + var buttonContainer = document.createElement('div'); + buttonContainer.style.cssText = ` + display: flex; + justify-content: center; + gap: 15px; + margin-bottom: 2rem; + flex-wrap: wrap; + `; + + var redirectButton = document.createElement('button'); + redirectButton.textContent = _('Redirect Now'); + redirectButton.style.cssText = ` + background: #4CAF50; + color: white; + border: none; + padding: 0 30px; + border-radius: 50px; + font-size: 16px; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4); + `; + + redirectButton.onmouseover = function() { + this.style.transform = 'translateY(-2px)'; + this.style.boxShadow = '0 8px 20px rgba(76, 175, 80, 0.6)'; + }; + + redirectButton.onmouseout = function() { + this.style.transform = 'translateY(0)'; + this.style.boxShadow = '0 5px 15px rgba(76, 175, 80, 0.4)'; + }; + + redirectButton.onclick = function() { + redirectToNewIP(newIP, useHTTPS); + }; + + messageBox.appendChild(icon); + messageBox.appendChild(title); + messageBox.appendChild(message); + buttonContainer.appendChild(redirectButton); + messageBox.appendChild(buttonContainer); + overlay.appendChild(messageBox); + + document.body.appendChild(overlay); + + var countdown = 10; + var countdownElement = document.getElementById('netwizard-countdown'); + + var countdownInterval = setInterval(function() { + countdown--; + if (countdownElement) { + countdownElement.textContent = countdown; + + if (countdown <= 3) { + countdownElement.style.color = (countdown % 2 === 0) ? '#FF6B6B' : '#FFD700'; + } + } + + if (countdown <= 0) { + clearInterval(countdownInterval); + redirectToNewIP(newIP, useHTTPS); + } + }, 1000); + + overlay._countdownInterval = countdownInterval; + } + + function hideRedirectMessage() { + var overlay = document.getElementById('netwizard-redirect-overlay'); + if (overlay) { + if (overlay._countdownInterval) { + clearInterval(overlay._countdownInterval); + } + document.body.removeChild(overlay); + } + } + + function redirectToNewIP(newIP, useHTTPS) { + hideRedirectMessage(); + + var protocol = useHTTPS === '1' ? 'https:' : 'http:'; + var currentPort = window.location.port ? ':' + window.location.port : ''; + var newURL = protocol + '//' + newIP + currentPort + '/'; + + var jumpMsg = document.createElement('div'); + jumpMsg.id = 'netwizard-jump-msg'; + jumpMsg.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: #4CAF50; + color: white; + padding: 15px 25px; + border-radius: 10px; + z-index: 10000; + font-weight: bold; + box-shadow: 0 5px 15px rgba(0,0,0,0.3); + animation: slideIn 0.5s ease; + `; + + var style = document.createElement('style'); + style.textContent = ` + @keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } + } + `; + document.head.appendChild(style); + + jumpMsg.textContent = _('Redirecting to') + ' ' + (useHTTPS === '1' ? 'HTTPS://' : 'HTTP://') + newIP + '...'; + document.body.appendChild(jumpMsg); + + setTimeout(function() { + try { + window.location.href = newURL; + } catch (e) { + alert(_('Failed to redirect to') + ' ' + newIP + + _('\nPlease manually access:\n') + newURL); + + var jumpMsg = document.getElementById('netwizard-jump-msg'); + if (jumpMsg) { + document.body.removeChild(jumpMsg); + } + } + }, 1000); + } + + m.save = function() { + var newLanIP = getNewLanIP(); + var useHTTPS = getNewhttps(); + var self = this; + + var savingMsg = document.createElement('div'); + savingMsg.id = 'netwizard-saving-msg'; + savingMsg.style.cssText = ` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0,0,0,0.9); + color: white; + padding: 20px 40px; + border-radius: 10px; + z-index: 9998; + font-size: 16px; + `; + savingMsg.textContent = _('Saving configuration...'); + document.body.appendChild(savingMsg); + + var startTime = Date.now(); + + function cleanup() { + if (savingMsg && savingMsg.parentNode) { + document.body.removeChild(savingMsg); + } + } + + function applywait(wtime) { + return new Promise(function(resolve) { + setTimeout(resolve, wtime); + }); + } + + return getLanproto() + .then(async function(isDHCP) { + savingMsg.textContent = _('Saving configuration...'); + var result = await originalSave.call(m); + + savingMsg.textContent = _('Applying configuration...'); + await applywait(2000); + + var totalTime = Date.now() - startTime; + // console.log('Save time:', totalTime + 'ms'); + cleanup(); + + return { + result: result, + newLanIP: newLanIP || currentLanIP, + isDHCP: isDHCP, + useHTTPS: useHTTPS + }; + }) + .then(function(data) { + var result = data.result; + var actualNewLanIP = data.newLanIP; + var isDHCP = data.isDHCP; + var useHTTPS = data.useHTTPS; + + var ipChanged = actualNewLanIP && currentLanIP !== actualNewLanIP; + var HTTPSChanged = currentHTTPS !== useHTTPS; + var isHTTP = useHTTPS === '0'; + var needRedirect = true; + + if (isHTTP && !ipChanged && !HTTPSChanged && !isDHCP) { + needRedirect = false; + } + + if (!needRedirect) { + var successMsg = document.createElement('div'); + successMsg.id = 'netwizard-success-msg'; + successMsg.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: #4CAF50; + color: white; + padding: 15px 25px; + border-radius: 10px; + z-index: 9999; + font-weight: bold; + animation: slideIn 0.5s ease; + `; + successMsg.textContent = _('Configuration saved successfully!'); + document.body.appendChild(successMsg); + + setTimeout(function() { + if (successMsg && successMsg.parentNode) { + document.body.removeChild(successMsg); + } + }, 3000); + + return result; + } + + showRedirectMessage(actualNewLanIP, useHTTPS, isDHCP); + + return result; + }) + .catch(function(err) { + var msg = document.getElementById('netwizard-saving-msg'); + if (msg && msg.parentNode) { + document.body.removeChild(msg); + } + + var errorMsg = document.createElement('div'); + errorMsg.id = 'netwizard-error-msg'; + errorMsg.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: #f44336; + color: white; + padding: 15px 25px; + border-radius: 10px; + z-index: 9999; + font-weight: bold; + animation: slideIn 0.5s ease; + `; + errorMsg.textContent = _('Failed to save configuration'); + document.body.appendChild(errorMsg); + + setTimeout(function() { + var msg = document.getElementById('netwizard-error-msg'); + if (msg && msg.parentNode) { + document.body.removeChild(msg); + } + }, 5000); + + throw err; + }); + }; + + var script = document.createElement('script'); + script.textContent = ` + function switchToTab(tabName) { + var tabs = document.querySelectorAll('.cbi-tabmenu a'); + for (var i = 0; i < tabs.length; i++) { + var tab = tabs[i]; + var tabText = tab.textContent || tab.innerText; + if ((tabName === 'wansetup' && (tabText.trim() === 'WAN Settings' || tabText.includes('WAN') || tabText.includes('网络设置'))) || + (tabName === 'modesetup' && (tabText.trim() === 'Network Mode' || tabText.includes('Mode') || tabText.includes('网络模式'))) || + (tabName === 'wifisetup' && (tabText.trim() === 'Wireless Settings' || tabText.includes('Wireless') || tabText.includes('无线设置'))) || + (tabName === 'othersetup' && (tabText.trim() === 'Other Settings' || tabText.includes('Other') || tabText.includes('其他设置')))) { + tab.click(); + var tabItems = document.querySelectorAll('.cbi-tabmenu li'); + tabItems.forEach(function(item) { + item.classList.remove('cbi-tab-active'); + }); + tab.parentNode.classList.add('cbi-tab-active'); + break; + } + } + } + + if (window.location.search.includes('selectedMode')) { + setTimeout(function() { + switchToTab('wansetup'); + }, 200); + + document.addEventListener('DOMContentLoaded', function() { + setTimeout(function() { + switchToTab('wansetup'); + }, 100); + }); + } + `; + + document.head.appendChild(script); + + return m.render(); + } +}); \ No newline at end of file diff --git a/openwrt-packages/luci-app-netwizard/htdocs/luci-static/resources/view/status/include/10_ota.js b/openwrt-packages/luci-app-netwizard/htdocs/luci-static/resources/view/status/include/10_ota.js new file mode 100644 index 0000000000..83d819a489 --- /dev/null +++ b/openwrt-packages/luci-app-netwizard/htdocs/luci-static/resources/view/status/include/10_ota.js @@ -0,0 +1,93 @@ +// Copyright 2023-2026 sirpdboy +'use strict'; +'require baseclass'; +'require rpc'; + +var callOTACheck = rpc.declare({ + object: 'ota', + method: 'check' +}); + +const callUciGet = rpc.declare({ + object: 'uci', + method: 'get', + params: ['config', 'section', 'option'] +}); + +return baseclass.extend({ + title: _('Firmware Update'), + + load: function() { + return Promise.resolve({ code: -1 }); + }, + + render: function() { + callUciGet('netwizard', 'default', 'updatacheck') + .then((res) => { + const updatacheck = res?.value ?? '0'; + console.log('Update check setting:', updatacheck); + + // 只有当配置为1时才检测更新 + if (updatacheck == 1 || updatacheck == '1') { + setTimeout(() => { + this.checkOTAUpdate(); + }, 1000); + } + }) + .catch((err) => { + const updatacheck = '0'; + }); + + return null; + }, + + checkOTAUpdate: function() { + if (window.otaCheckStarted) return; + window.otaCheckStarted = true; + + callOTACheck() + .then(data => { + if (data && data.code === 0) { + this.addUpdateButton(); + } + }) + .catch(() => { + }); + }, + + addUpdateButton: function() { + if (document.getElementById('ota-notice')) { + return; + } + + var flashindicators = document.querySelector('#indicators'); + if (!flashindicators) return; + + var notice = document.createElement('div'); + notice.id = 'ota-notice'; + notice.innerHTML = [ + '' + ].join(''); + + flashindicators.parentNode.insertBefore(notice, flashindicators); + this.addResponsiveStyle(); + }, + + addResponsiveStyle: function() { + if (document.getElementById('ota-responsive-style')) return; + + var style = document.createElement('style'); + style.id = 'ota-responsive-style'; + style.textContent = '@media (max-width: 480px) { header>.fill>.container>.flex1>.brand { display: none; } }'; + document.head.appendChild(style); + } +}); diff --git a/openwrt-packages/luci-app-netwizard/po/zh-cn b/openwrt-packages/luci-app-netwizard/po/zh-cn new file mode 120000 index 0000000000..8d69574ddd --- /dev/null +++ b/openwrt-packages/luci-app-netwizard/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/openwrt-packages/luci-app-netwizard/po/zh_Hans/netwizard.po b/openwrt-packages/luci-app-netwizard/po/zh_Hans/netwizard.po new file mode 100644 index 0000000000..c824461087 --- /dev/null +++ b/openwrt-packages/luci-app-netwizard/po/zh_Hans/netwizard.po @@ -0,0 +1,280 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "Netwizard" +msgstr "设置向导" + +msgid "Quick Network Setup Wizard" +msgstr "快速网络设置向导" + +msgid "Quick network setup wizard. If you need more settings, please enter network - interface to set." +msgstr "快速网络设置向导。如需更多设置,请进入网络-接口 页面进行设置。" + +msgid "Network Mode" +msgstr "网络模式" + +msgid "WAN Settings" +msgstr "网络设置" + +msgid "Wireless Settings" +msgstr "无线设置" + +msgid "Set the router's wireless name and password. For more advanced settings, please go to the Network-Wireless page." +msgstr "设置路由器的无线名称和密码。如需更多高级设置,请前往网络-无线页面。" + +msgid "Other Settings" +msgstr "其他设置" + +msgid "Select Network Connection Mode" +msgstr "选择网络连接模式" + +msgid "Choose the connection mode that matches your network environment" +msgstr "选择与您的网络环境匹配的连接模式" + +msgid "PPPoE Dial-up" +msgstr "宽带拨号连接" + +msgid "Fiber broadband that requires username/password" +msgstr "适用于需要用户名/密码的光纤宽带" + +msgid "DHCP Client" +msgstr "连接现有路由器" + +msgid "Connect to router as a subordinate router to internet" +msgstr "连接到路由器做为下级路由上网" + +msgid "Side Router" +msgstr "配置为旁路由" + +msgid "Configure as side router in same network as main router" +msgstr "配置为与主路由器在同一网络中的旁路路由" + +msgid "Network connection mode" +msgstr "网络连接协议模式" + +msgid "Current Network Mode" +msgstr "当前网络协议模式" + +msgid "Go to WAN Settings" +msgstr "去设置网络参数" + +msgid "Change Mode" +msgstr "更改协议模式" + +msgid "Network protocol mode selection" +msgstr "网络协议模式选择" + +msgid "Add LAN port configuration" +msgstr "增加LAN口参数配置" + +msgid "Three different ways to access the Internet, please choose according to your own situation." +msgstr "三种不同的上网方式,请根据自身情况选择。" + +msgid "LAN IP Address Mode" +msgstr "LAN IP 地址模式" + +msgid "Warning: Setting up automatic IP address retrieval requires checking the IP address on the higher-level router" +msgstr "警告:设置自动获取IP地址须到上级路由上查看IP" + +msgid "Static IP address (Specify non conflicting IP addresses)" +msgstr "静态 IP 地址(指定非冲突的 IP 地址)" + +msgid "DHCP client (existing router assigns IP)" +msgstr "DHCP 客户端(现有路由器分配IP)" + +msgid "DHCP client (Main router assigns IP)" +msgstr "DHCP 客户端(主路由器分配 IP)" + +msgid "WAN interface IP address mode" +msgstr "WAN口IP地址模式" + +msgid "Choose how to get IP address for WAN interface" +msgstr "选择WAN接口获取IP地址的方式" + +msgid "LAN IPv4 Address" +msgstr "LAN口IPv4地址" + +msgid "You must specify the IP address of this machine, which is the IP address of the web access route" +msgstr "您必须指定本机的 IP 地址,即Web访问路由的IP地址" + +msgid "LAN IPv4 Netmask" +msgstr "LAN口IPv4子网掩码" + +msgid "LAN IPv4 Gateway" +msgstr "LAN口IPv4网关" + +msgid "Please enter the main routing IP address. The bypass gateway is not the same as the login IP of this bypass WEB and is in the same network segment" +msgstr "请输入主路由IP地址。旁路网关并非本旁路WEB登录IP,且处于同一网段" + +msgid "Use Custom SideRouter DNS" +msgstr "使用自定义旁路由DNS" + +msgid "Ali DNS: 223.5.5.5" +msgstr "阿里 DNS:223.5.5.5" + +msgid "Baidu DNS: 180.76.76.76" +msgstr "百度 DNS:180.76.76.76" + +msgid "114 DNS: 114.114.114.114" +msgstr "114 DNS:114.114.114.114" + +msgid "Google DNS: 8.8.8.8" +msgstr "谷歌 DNS:8.8.8.8" + +msgid "Cloudflare DNS: 1.1.1.1" +msgstr "Cloudflare DNS:1.1.1.1" + +msgid "Device" +msgstr "设备" + +msgid "Allocate the physical interface of WAN port" +msgstr "分配 WAN 口的物理接口" + +msgid "PAP/CHAP Username" +msgstr "PAP/CHAP 用户名" + +msgid "PAP/CHAP Password" +msgstr "PAP/CHAP 密码" + +msgid "WAN IPv4 Address" +msgstr "WAN IPv4 地址" + +msgid "WAN IPv4 Netmask" +msgstr "WAN IPv4 子网掩码" + +msgid "WAN IPv4 Gateway" +msgstr "WAN IPv4 网关" + +msgid "Use Custom WAN DNS" +msgstr "使用自定义 WAN DNS" + +msgid "Enable IPv6" +msgstr "启用 IPv6" + +msgid "Disable DHCP Server" +msgstr "禁用DHCP服务器" + +msgid "Selecting means that the DHCP server is not enabled. In a network, only one DHCP server is needed to allocate and manage client IPs. If it is a siderouter route, it is recommended to turn off the primary routing DHCP server." +msgstr "选择意味着不启用 DHCP 服务器。在一个网络中,只需要一个 DHCP 服务器来分配和管理客户端 IP。如果是旁路由,建议关闭主路由的 DHCP 服务器。" + +msgid "Enable DNS Notifications (IPv4/IPv6)" +msgstr "启用 DNS 通知(IPv4/IPv6)" + +msgid "Forcefully specify the DNS server for this router" +msgstr "强制指定此路由器的 DNS 服务器" + +msgid "DNS" +msgstr "DNS服务器" + +msgid "Use local IP for DNS (default)" +msgstr "使用本地 IP 作为 DNS(默认)" + +msgid "Forcefully Forwarding" +msgstr "强制转发" + +msgid "Forcefully add LAN to WAN forwarding" +msgstr "强制添加 LAN 到 WAN 的转发" + +msgid "Redirect to HTTPS" +msgstr "重定向到 HTTPS" + +msgid "Enable automatic redirection of HTTP requests to HTTPS port." +msgstr "启用自动将 HTTP 请求重定向到 HTTPS 端口。" + +msgid "ESSID" +msgstr "ESSID" + +msgid "Key" +msgstr "密钥" + +msgid "Enable SYN-flood Defense" +msgstr "启用 SYN 洪水防御" + +msgid "Enable Firewall SYN-flood defense [Suggest opening]" +msgstr "启用防火墙 SYN 洪水防御 [建议开启]" + +msgid "Configuration Applied Successfully!" +msgstr "配置应用成功!" + +msgid "The network configuration has been saved and applied.

" +msgstr "网络配置已保存并应用。

" + +msgid "The page will automatically redirect in" +msgstr "页面自动转向还有" + +msgid "seconds..." +msgstr "秒..." + +msgid "Redirect Now" +msgstr "立即转向" + +msgid "Redirecting to" +msgstr "转向地址" + +msgid "Failed to redirect to" +msgstr "无效重定向" + +msgid "\nPlease manually access:\n" +msgstr "\n请手动访问:\n" + +msgid "Applying network configuration..." +msgstr "正在应用网络配置..." + +msgid "Saving configuration..." +msgstr "正在保存配置..." + +msgid "Applying configuration..." +msgstr "正在应用配置..." + +msgid "Preparing network settings..." +msgstr "正在更新网络设置..." + +msgid "Configuration saved successfully!" +msgstr "配置保存成功!" + +msgid "Access Protocol:" +msgstr "访问协议:" + +msgid "Failed to save configuration" +msgstr "保存配置失败" + +msgid "Network configuration has been successfully applied!" +msgstr "网络配置已成功应用!" + +msgid "The router is now configured to obtain IP address via DHCP." +msgstr "路由器现已配置为通过DHCP获取IP地址。" + +msgid "Please check your main router DHCP client list or try to access the router using its original IP address." +msgstr "请检查您主路由器的DHCP客户端列表,或尝试使用其原始IP地址访问路由器。" + +msgid "Set LAN to DHCP mode" +msgstr "LAN设置为DHCP模式" + +msgid "Important Note:" +msgstr "重要提示:" + +msgid "The current router IP address will be assigned by the DHCP server of the superior router" +msgstr "当前路由器IP地址将由上级路由器的DHCP服务器分配" + +msgid "Please login to the superior router to view the DHCP client list" +msgstr "请登录上级路由器查看DHCP客户端列表" + +msgid "Or access using the original IP address on the current router" +msgstr "或在当前路由器使用原始IP地址访问" + +msgid "Unable to automatically redirect to the new IP address" +msgstr "无法自动重定向到新IP地址" + +msgid "Configuration has been saved successfully. You can manually access the router management interface." +msgstr "配置已保存成功,可以手动访问路由器管理界面。" + +msgid "Enable detection update prompts" +msgstr "启用检测更新提示" + +msgid "Auto-fetch" +msgstr "自动获取" + +msgid "" +msgstr "" \ No newline at end of file diff --git a/openwrt-packages/luci-app-netwizard/root/etc/config/netwizard b/openwrt-packages/luci-app-netwizard/root/etc/config/netwizard new file mode 100644 index 0000000000..68bf57363a --- /dev/null +++ b/openwrt-packages/luci-app-netwizard/root/etc/config/netwizard @@ -0,0 +1,2 @@ +config netwizard 'default' + option lan_netmask '255.255.255.0' \ No newline at end of file diff --git a/openwrt-packages/luci-app-netwizard/root/etc/init.d/netwizard b/openwrt-packages/luci-app-netwizard/root/etc/init.d/netwizard new file mode 100644 index 0000000000..5ff4ed7d43 --- /dev/null +++ b/openwrt-packages/luci-app-netwizard/root/etc/init.d/netwizard @@ -0,0 +1,720 @@ +#!/bin/sh /etc/rc.common + +START=99 + +UCISET='uci -q set' +UCIDEL='uci -q delete' +UCIADD='uci -q add' +UCIADDLIST='uci -q add_list' +LOCK=/var/lock/netwizard-boot.lock +LOCK_TIMEOUT=300 + +log() { + echo "netwizard:$1" +} + +validate_ip() { + echo "$1" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$' && return 0 || return 1 +} + +validate_cidr() { + echo "$1" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$' && return 0 || return 1 +} + +validate_interface() { + [ -n "$1" ] && [ -d "/sys/class/net/$1" ] && return 0 || return 1 +} + +# 检查锁文件 +check_lock() { + if [ -f "$LOCK" ]; then + local lock_age=$(($(date +%s) - $(stat -c %Y "$LOCK" 2>/dev/null || echo 0))) + if [ $lock_age -gt $LOCK_TIMEOUT ]; then + log "Removing stale lock file (age: ${lock_age}s)" + rm -f "$LOCK" + return 1 + fi + log "Another instance is running, exiting" + return 0 + fi + return 1 +} + +create_lock() { + touch "$LOCK" 2>/dev/null || { + log_error "Failed to create lock file" + return 1 + } + return 0 +} + +remove_lock() { + rm -f "$LOCK" 2>/dev/null +} + +check_dependencies() { + local missing="" + + for cmd in $REQUIRED_COMMANDS; do + if ! command -v "$cmd" >/dev/null 2>&1; then + missing="$missing $cmd" + fi + done + + if [ -n "$missing" ]; then + log_error "Required commands not found:$missing" + return 1 + fi + + return 0 +} +setipv6() { + case $1 in + 0) + bINDEX=$(uci show network | grep -E "@device\[[0-9]+\].name='br-lan'" | sed -n 's/.*@device\[\([0-9]*\)\].name.*/\1/p') + if [ -n "$bINDEX" ]; then + $UCISET network.@device[$bINDEX].ipv6='0' + fi + nINDEX=$(uci show network | grep -E "@device\[[0-9]+\].name=\'$wan_interface\'" | sed -n 's/.*@device\[\([0-9]*\)\].name.*/\1/p') + [ -n "$nINDEX" ] && $UCIDEL network.@device[$nINDEX] + uci -q batch </dev/null 2>&1; do + local src=$(uci -q get firewall.@forwarding[$idx].src) + local dest=$(uci -q get firewall.@forwarding[$idx].dest) + if [ "$src" = "lan" ] && [ "$dest" = "wan" ]; then + $UCIDEL firewall.@forwarding[$idx] + break + fi + idx=$((idx + 1)) + done + $UCIADD firewall forwarding + $UCISET firewall.@forwarding[-1].src='lan' + $UCISET firewall.@forwarding[-1].dest='wan' +} + +# 设置HTTPS +sethttps() { + local cfg=$1 + local https + + config_get https "$cfg" https '0' + + if [ -n "$(command -v nginx)" ]; then + if [ -z "$(uci -q get nginx._redirect2ssl)" ]; then + $UCISET nginx._redirect2ssl=server + $UCISET nginx._redirect2ssl.server_name='_redirect2ssl' + $UCISET nginx._redirect2ssl.return='302 https://$host$request_uri' + $UCISET nginx._redirect2ssl.access_log='off; # logd openwrt' + fi + + if [ "x${https}" = "x1" ]; then + $UCIDEL nginx.default_server.listen + $UCIADDLIST nginx.default_server.listen='80' + $UCIADDLIST nginx.default_server.listen='[::]:80' + $UCIDEL nginx._redirect2ssl.listen + $UCIADDLIST nginx._redirect2ssl.listen='80 default_server' + $UCIADDLIST nginx._redirect2ssl.listen='[::]:80 default_server' + else + $UCIDEL nginx._redirect2ssl.listen + $UCIADDLIST nginx._redirect2ssl.listen='80' + $UCIADDLIST nginx._redirect2ssl.listen='[::]:80' + $UCIDEL nginx.default_server.listen + $UCIADDLIST nginx.default_server.listen='80 default_server' + $UCIADDLIST nginx.default_server.listen='[::]:80 default_server' + fi + uci commit nginx + /etc/init.d/nginx reload + else + /etc/init.d/uhttpd stop 2>/dev/null + sed -i "/listen_https/d" /etc/config/uhttpd 2>/dev/null + $UCISET uhttpd.main.redirect_https='0' + + if [ "x${https}" = "x1" ]; then + certscrt='/etc/ssl/ezopwrt.crt' + certskey='/etc/ssl/ezopwrt.key' + + # 确保证书目录存在 + mkdir -p /etc/ssl/ + chmod 755 /etc/ssl/ + + rm -f $certskey $certscrt 2>/dev/null + $UCIADDLIST uhttpd.main.listen_https='0.0.0.0:443' + $UCIADDLIST uhttpd.main.listen_https='[::]:443' + $UCISET uhttpd.main.redirect_https='1' + hostname=$(uci -q get system.@system[0].hostname | awk '{print tolower($0)}' || echo 'openwrt') + $UCISET network.lan.hostname=${hostname} + $UCISET dhcp.@dnsmasq[0].domain="${hostname}.lan" + + # 生成证书 + openssl req -new -newkey rsa:2048 -days 3650 -sha256 -nodes -x509 \ + -keyout $certskey \ + -out $certscrt \ + -subj "/C=CN/CN=$hostname.lan" 2>/dev/null + + if [ $? -eq 0 ] && [ -f "$certskey" ] && [ -f "$certscrt" ]; then + chmod 600 $certskey + chmod 644 $certscrt + $UCISET uhttpd.main.cert="$certscrt" + $UCISET uhttpd.main.key="$certskey" + else + log "Failed to generate SSL certificate" + $UCIDEL uhttpd.main.cert + $UCIDEL uhttpd.main.key + fi + fi + fi +} + +# 备份配置 +backup_configs() { + local backup_dir="/tmp/netwizard_backup_$(date +%s)" + mkdir -p "$backup_dir" + for config in network firewall dhcp wireless system uhttpd nginx; do + if [ -f "/etc/config/$config" ]; then + cp "/etc/config/$config" "$backup_dir/" 2>/dev/null + fi + done + echo "$backup_dir" +} + +# 恢复配置 +restore_configs() { + local backup_dir="$1" + if [ -d "$backup_dir" ]; then + log "Restoring configurations from backup" + for config in "$backup_dir"/*; do + if [ -f "$config" ]; then + cp "$config" "/etc/config/" 2>/dev/null + fi + done + rm -rf "$backup_dir" + fi +} + +configure_network() { + local cfg=$1 + local wan_proto wan_ipaddr wan_netmask wan_gateway wan_dns wan_pppoe_user wan_pppoe_pass + local ipv6 wifi_ssid wifi_key old_wifi_ssid old_wifi_key showhide + local lan_ipaddr lan_netmask lan_gateway lan_dns lan_dhcp wan_interface lan_proto + local dns_tables synflood https + local ifname lannet netname netsum i + + # 获取配置 + config_get showhide "$cfg" showhide + config_get wan_proto "$cfg" wan_proto + config_get ipv6 "$cfg" ipv6 '0' + config_get dnsset "$cfg" dnsset '0' + config_get wan_interface "$cfg" wan_interface + config_get lan_dhcp "$cfg" lan_dhcp '0' + config_get synflood "$cfg" synflood '0' + config_get lan_ipaddr "$cfg" lan_ipaddr + config_get lan_netmask "$cfg" lan_netmask '255.255.255.0' + config_get lan_proto "$cfg" lan_proto 'static' + config_get lan_gateway "$cfg" lan_gateway + config_get lan_dns "$cfg" lan_dns + config_get dns_tables "$cfg" dns_tables + config_get https "$cfg" https '0' + config_get wan_ipaddr "$cfg" wan_ipaddr + config_get wan_netmask "$cfg" wan_netmask + config_get wan_gateway "$cfg" wan_gateway + config_get wan_dns "$cfg" wan_dns + config_get wan_pppoe_user "$cfg" wan_pppoe_user + config_get wan_pppoe_pass "$cfg" wan_pppoe_pass + config_get dhcp_proto "$cfg" dhcp_proto 'dhcp' + config_get wifi_ssid "$cfg" wifi_ssid + config_get wifi_key "$cfg" wifi_key + config_get old_wifi_ssid "$cfg" old_wifi_ssid + config_get old_wifi_key "$cfg" old_wifi_key + + # 备份当前配置 + local backup_dir=$(backup_configs) + + if [ "x$showhide" = "x1" ]; then + touch /etc/netwizard_hide 2>/dev/null + $UCISET advancedplus.@basic[0].wizard="1" + exit 0 + else + rm -rf /etc/netwizard_hide 2>/dev/null + $UCISET advancedplus.@basic[0].wizard="0" + fi + + if [ -z "$wan_proto" ]; then + wan_proto=$(uci -q get network.wan.proto) + [ -z "$wan_proto" ] && wan_proto="siderouter" + fi + + + ifname=$(uci -q get network.lan.ifname) + [ "x$ifname" = "x" ] && ifname="device" || ifname="ifname" + + [ -n "$wan_interface" ] || wan_interface=$(uci -q get network.wan.$ifname) + + $UCIDEL network.lan.gateway + $UCIDEL network.lan.dns + $UCIDEL firewall.@zone[0].masq + + + [ -n "$(uci -q get network.wan)" ] && { + $UCIDEL network.wan + WAN_INDEX=$(uci show firewall | grep -E "@zone\[[0-9]+\].name='wan'" | sed -n 's/.*@zone\[\([0-9]*\)\].name.*/\1/p') + [ -n "$WAN_INDEX" ] && $UCIDEL firewall.@zone[$WAN_INDEX] + } + [ -n "$(uci -q get network.wan6)" ] && $UCIDEL network.wan6 + [ -n "$(uci -q get network.lan6)" ] && $UCIDEL network.lan6 + + netname=$(ls /sys/class/net/ 2>/dev/null | grep -E '^(eth[0-9]+|en[op][0-9]+s[0-9]+|usb[0-9]+|wlan[0-9]+|wl[0-9]+)' | sort) + netsum=$(echo "$netname" | wc -l 2>/dev/null || echo 0) + + if [ "$netsum" -eq 0 ]; then + restore_configs "$backup_dir" + log "No network interfaces found" + return 1 + elif [ "$netsum" -eq 1 ]; then + lannet=$(echo "$netname") + wan_interface=$(echo "$netname") + elif [ "$netsum" -gt 1 ]; then + [ -z "$wan_interface" ] && wan_interface=$(echo "$netname" | grep '^eth' | tail -n 1) + [ -z "$wan_interface" ] && wan_interface=$(echo "$netname" | grep -v '^wl' | grep -v '^wlan' | head -n 1) + [ -z "$wan_interface" ] && wan_interface=$(echo "$netname" | grep -E '^en[opx][0-9]+' | tail -n 1) + lannet="" + for eth_interface in $netname; do + if [ "$eth_interface" != "$wan_interface" ]; then + lannet=$lannet" "${eth_interface##*/} + fi + done + lannet=$(echo "$lannet" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') + fi + + if [ -n "$wan_interface" ] && [ "$wan_proto" != "siderouter" ]; then + $UCISET network.wan=interface + $UCISET network.wan6=interface + $UCISET network.wan6.proto='dhcpv6' + $UCISET network.wan6.delegate='1' + $UCIDEL dhcp.wan.master + $UCIDEL dhcp.lan.master + $UCISET network.wan.metric='1' + $UCISET network.wan.$ifname="${wan_interface}" + [ "$wan_proto" == "pppoe" ] && $UCISET network.wan6.$ifname="@wan" || $UCISET network.wan6.$ifname="${wan_interface}" + # firewall ipv6 + $UCIDEL dhcp.wan.ra_flags + + $UCIADD firewall zone + $UCISET firewall.@zone[-1].name='wan' + $UCISET firewall.@zone[-1].input='REJECT' + $UCISET firewall.@zone[-1].output='ACCEPT' + $UCISET firewall.@zone[-1].forward='REJECT' + $UCIADDLIST firewall.@zone[-1].network='wan6' + $UCIADDLIST firewall.@zone[-1].network='wan' + $UCISET firewall.@zone[-1].masq='1' + $UCISET firewall.@zone[-1].masq6='1' + $UCISET firewall.@zone[0].masq6='0' + fi + + $UCISET firewall.@defaults[0].drop_invalid='1' + + case "${wan_proto}" in + pppoe) + $UCISET network.wan.proto='pppoe' + [ -n "${wan_pppoe_user}" ] && $UCISET network.wan.username="${wan_pppoe_user}" + [ -n "${wan_pppoe_pass}" ] && $UCISET network.wan.password="${wan_pppoe_pass}" + setforwarding + [ "x$ipv6" = "x1" ] && setipv6 1 || setipv6 0 + + if [ -n "${wan_dns}" ]; then + $UCISET network.wan.peerdns='0' + for dns in ${wan_dns}; do + $UCIADDLIST network.wan.dns="$dns" + done + else + $UCIDEL network.wan.peerdns + $UCIDEL network.wan.dns + fi + ;; + + dhcp) + case "${dhcp_proto}" in + static) + $UCISET network.wan.proto='static' + if [ -n "$wan_ipaddr" ]; then + $UCISET network.wan.ipaddr="${wan_ipaddr}" + fi + if [ -n "$wan_netmask" ]; then + $UCISET network.wan.netmask="${wan_netmask}" + fi + if [ -n "$wan_gateway" ]; then + $UCISET network.wan.gateway="${wan_gateway}" + fi + if [ -n "${wan_dns}" ]; then + $UCISET network.wan.defaultroute='1' + for dns in ${wan_dns}; do + $UCIADD network.wan.dns="$dns" + done + else + $UCIDEL network.wan.dns + fi + ;; + + dhcp) + $UCISET network.wan.proto='dhcp' + $UCISET network.wan.delegate='0' + if [ -n "${wan_dns}" ]; then + $UCISET network.wan.peerdns='0' + for dns in ${wan_dns}; do + $UCIADDLIST network.wan.dns="$dns" + done + else + $UCIDEL network.wan.peerdns + $UCIDEL network.wan.dns + fi + ;; + esac + setforwarding + [ "x$ipv6" = "x1" ] && setipv6 3 || setipv6 0 + ;; + + siderouter) + $UCIDEL firewall.@zone[0].network + $UCIADDLIST firewall.@zone[0].network='lan' + $UCIDEL dhcp.lan.ra_slaac + + case "${lan_proto}" in + static) + $UCISET network.lan.proto='static' + if [ -n "$lan_ipaddr" ]; then + $UCISET network.lan.ipaddr="${lan_ipaddr}" + fi + if [ -n "$lan_netmask" ]; then + $UCISET network.lan.netmask="${lan_netmask}" + fi + if [ -n "$lan_gateway" ]; then + $UCISET network.lan.gateway="${lan_gateway}" + fi + if [ -n "$lan_dns" ]; then + for dns in ${lan_dns}; do + $UCIADDLIST network.lan.dns="$dns" + done + else + $UCIDEL network.lan.dns + fi + ;; + + dhcp) + $UCISET network.lan.proto='dhcp' + $UCIDEL network.lan.ipaddr + $UCIDEL network.lan.netmask + if [ -n "$lan_dns" ]; then + $UCISET network.lan.peerdns='0' + for dns in ${lan_dns}; do + $UCIADDLIST network.lan.dns="$dns" + done + else + $UCIDEL network.lan.peerdns + $UCIDEL network.lan.dns + fi + ;; + esac + + if [ $netsum -gt 1 ]; then + [ -n "$wan_interface" ] && lannet=$lannet" "${wan_interface##*/} + fi + + $UCISET firewall.@zone[0].masq='1' + $UCIDEL firewall.@zone[0].network + $UCIADDLIST firewall.@zone[0].network='lan' + + [ $(uci show network 2>/dev/null | grep utun | wc -l) -gt 1 ] && $UCIADDLIST firewall.@zone[0].network='utun' + + if [ "x$ipv6" = "x1" ]; then + $UCISET network.lan6=interface + $UCISET network.lan6.proto='dhcpv6' + $UCISET network.lan6.delegate='1' + $UCISET network.lan6.$ifname="@lan" + setipv6 2 + $UCIADDLIST firewall.@zone[0].network='lan6' + else + setipv6 0 + fi + ;; + esac + + [ "x$ifname" = "xdevice" ] && $UCISET network.@$ifname[0].ports="${lannet}" || $UCISET network.lan.$ifname="${lannet}" + if [ -f /etc/config/dockerd ]; then + DOCKERINDEX=$(uci show firewall | grep -E "@zone\[[0-9]+\].name='docker'" | sed -n 's/.*@zone\[\([0-9]*\)\].name.*/\1/p') + [ -n "$DOCKERINDEX" ] && $UCIDEL firewall.@zone[$DOCKERINDEX] + uci -q batch </dev/null + sed -i "/list dns/d" /etc/config/dhcp 2>/dev/null + if [ "x$lan_dhcp" = "x1" ]; then + $UCIDEL dhcp.lan.force + $UCISET dhcp.lan.ignore='1' + $UCISET dhcp.lan.dynamicdhcp='0' + $UCISET dhcp.lan.ra_slaac="1" + $UCIDEL dhcp.@dnsmasq[0].authoritative + else + $UCIDEL dhcp.lan.ignore + $UCIDEL dhcp.lan.dynamicdhcp + $UCISET dhcp.lan.force='1' + $UCISET dhcp.@dnsmasq[0].authoritative='1' + + if [ "x$dnsset" = "x1" ]; then + if [ "${dns_tables}" = "1" ]; then + if [ -n "$lan_ipaddr" ]; then + $UCIADDLIST dhcp.lan.dhcp_option="6,${lan_ipaddr}" + fi + elif [ -n "${dns_tables}" ]; then + $UCIADDLIST dhcp.lan.dhcp_option="6,${dns_tables}" + fi + fi + fi + + if [ -n "${wifi_ssid}" ] && [ -n "${wifi_key}" ]; then + idx=0 + max_wifi_ifaces=10 + while [ $idx -lt $max_wifi_ifaces ] && uci -q get wireless.@wifi-iface[$idx] >/dev/null; do + if [ "$(uci -q get wireless.@wifi-iface[$idx].mode)" = "ap" ]; then + $UCISET wireless.@wifi-iface[$idx].ssid="${wifi_ssid}" + $UCISET wireless.@wifi-iface[$idx].key="${wifi_key}" + $UCISET wireless.@wifi-iface[$idx].encryption='psk2' + fi + idx=$((idx + 1)) + done + + for radio in radio0 radio1 radio2 radio3; do + if uci -q get wireless.${radio} >/dev/null 2>&1; then + if [ "$(uci -q get wireless.${radio}.band)" = "5g" ]; then + $UCISET wireless.default_${radio}.ssid="${wifi_ssid}_5G" + else + $UCISET wireless.default_${radio}.ssid="${wifi_ssid}_2.4G" + fi + $UCISET wireless.default_${radio}.device="${radio}" + $UCISET wireless.default_${radio}.encryption='psk2' + $UCISET wireless.default_${radio}.key="${wifi_key}" + fi + done + + $UCISET netwizard.default.old_wifi_ssid="${wifi_ssid}" + $UCISET netwizard.default.old_wifi_key="${wifi_key}" + uci commit wireless + fi + + # 提交所有配置 + uci -q batch <<'COMMIT_EOF' >/dev/null + commit advancedplus + commit uhttpd + commit system + commit netwizard + commit dhcp + commit firewall + commit network +COMMIT_EOF + + if [ $? -eq 0 ]; then + { + + /etc/init.d/network restart 2>/dev/null + /etc/init.d/system reload 2>/dev/null + /etc/init.d/rpcd reload 2>/dev/null + /etc/init.d/uhttpd reload 2>/dev/null + sleep 1 + /etc/init.d/dnsmasq reload 2>/dev/null + /etc/init.d/firewall reload 2>/dev/null + } >/dev/null 2>&1 & + rm -rf "$backup_dir" 2>/dev/null + log "Configuration applied successfully" + else + log "Failed to commit configuration, restoring backup" + restore_configs "$backup_dir" + return 1 + fi +} + +boot() { + XBOOT=1 + start +} + +start() { + check_lock && exit 0 + # 启动时跳过 + [ "x$XBOOT" = "x1" ] && exit 0 + + # 检查依赖 + for dep in uci awk grep sed; do + if ! command -v $dep >/dev/null 2>&1; then + log "ERROR: Required command '$dep' not found" + return 1 + fi + done + + create_lock || return 1 + config_load netwizard + config_foreach configure_network netwizard + remove_lock +} + +stop() { + remove_lock +} + +restart() { + stop + sleep 1 + start +} \ No newline at end of file diff --git a/openwrt-packages/luci-app-netwizard/root/etc/uci-defaults/40-luci-netwizard b/openwrt-packages/luci-app-netwizard/root/etc/uci-defaults/40-luci-netwizard new file mode 100644 index 0000000000..54a6ffc77b --- /dev/null +++ b/openwrt-packages/luci-app-netwizard/root/etc/uci-defaults/40-luci-netwizard @@ -0,0 +1,21 @@ +#!/bin/sh +[ ! -f "/usr/share/ucitrack/luci-app-netwizard.json" ] && { + cat > /usr/share/ucitrack/luci-app-netwizard.json << EEOF +{ + "config": "netwizard", + "init": "netwizard" +} +EEOF +} + +uci -q batch <<-EOF >/dev/null + delete ucitrack.@netwizard[-1] + add ucitrack netwizard + set ucitrack.@netwizard[-1].init=netwizard + commit ucitrack +EOF + +chmod +x /etc/init.d/netwizard 2>/dev/null +rm -f /tmp/luci-indexcache + +exit 0 diff --git a/openwrt-packages/luci-app-netwizard/root/usr/libexec/rpcd/luci.netwizard b/openwrt-packages/luci-app-netwizard/root/usr/libexec/rpcd/luci.netwizard new file mode 100644 index 0000000000..e92d397e2b --- /dev/null +++ b/openwrt-packages/luci-app-netwizard/root/usr/libexec/rpcd/luci.netwizard @@ -0,0 +1,58 @@ +#!/bin/sh + +# author github@sirpdboy + +# Copyright (C) 2019-2026 The Sirpdboy Team +. /usr/share/libubox/jshn.sh + +get_lan_ip() { + # 方法1:使用ip命令 + lan_ip=$(ip -4 addr show br-lan 2>/dev/null | grep inet | awk '{print $2}' | cut -d/ -f1) + + # 如果为空,尝试ubus方法 + if [ -z "$lan_ip" ]; then + lan_ip=$(ubus call network.interface.lan status 2>/dev/null | jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null) + fi + + echo "$lan_ip" +} + +# 网络向导RPC服务 +case "$1" in +"list") + + json_init + json_add_object "get_ip" + json_close_object + json_dump + json_cleanup + ;; + +"call") + case "$2" in + "get_ip") + + read -r input + + lan_ip=$(ip -4 addr show br-lan 2>/dev/null | grep inet | awk '{print $2}' | cut -d/ -f1) + if [ -z "$lan_ip" ]; then + lan_ip=$(ubus call network.interface.lan status 2>/dev/null | jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null) + fi + + + json_init + json_add_string "ip" "$lan_ip" + json_add_boolean "success" 1 + json_dump + json_cleanup + ;; + *) + echo '{"error": "Method not found"}' + ;; + esac + ;; + + *) + echo '{"error": "Invalid action"}' + ;; +esac \ No newline at end of file diff --git a/openwrt-packages/luci-app-netwizard/root/usr/share/luci/menu.d/luci-app-netwizard.json b/openwrt-packages/luci-app-netwizard/root/usr/share/luci/menu.d/luci-app-netwizard.json new file mode 100644 index 0000000000..f39a7234da --- /dev/null +++ b/openwrt-packages/luci-app-netwizard/root/usr/share/luci/menu.d/luci-app-netwizard.json @@ -0,0 +1,13 @@ +{ + "admin/netwizard": { + "title": "Netwizard", + "order": 11, + "action": { + "type": "view", + "path": "netwizard/netwizard" + }, + "depends": { + "acl": [ "luci-app-netwizard" ] + } + } +} diff --git a/openwrt-packages/luci-app-netwizard/root/usr/share/rpcd/acl.d/luci-app-netwizard.json b/openwrt-packages/luci-app-netwizard/root/usr/share/rpcd/acl.d/luci-app-netwizard.json new file mode 100644 index 0000000000..bc97c544f9 --- /dev/null +++ b/openwrt-packages/luci-app-netwizard/root/usr/share/rpcd/acl.d/luci-app-netwizard.json @@ -0,0 +1,19 @@ +{ + "luci-app-netwizard": { + "description": "Grant UCI access for luci-app-netwizard", + "read": { + "ubus": { + "luci.netwizard": ["*"], + "file": ["exec"] + }, + "uci": [ "netwizard", "wireless" ] + }, + "write": { + "ubus": { + "luci.netwizard": ["*"], + "file": ["exec"] + }, + "uci": [ "netwizard" ] + } + } +} diff --git a/openwrt-packages/luci-app-netwizard/root/usr/share/ucitrack/luci-app-netwizard.json b/openwrt-packages/luci-app-netwizard/root/usr/share/ucitrack/luci-app-netwizard.json new file mode 100644 index 0000000000..4749abc42d --- /dev/null +++ b/openwrt-packages/luci-app-netwizard/root/usr/share/ucitrack/luci-app-netwizard.json @@ -0,0 +1,4 @@ +{ + "config": "netwizard", + "init": "netwizard" +} diff --git a/sing-box/common/tls/apple_client_platform.go b/sing-box/common/tls/apple_client_platform.go index 38cef976f3..9e7d6e73a2 100644 --- a/sing-box/common/tls/apple_client_platform.go +++ b/sing-box/common/tls/apple_client_platform.go @@ -102,7 +102,20 @@ func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) return nil, err } - connectionState, rawCerts, err := copyAppleTLSConnectionState(client) + var state C.box_apple_tls_state_t + stateOK := C.box_apple_tls_client_copy_state(client, &state, &errorPtr) + if !bool(stateOK) { + C.box_apple_tls_client_cancel(client) + C.box_apple_tls_client_free(client) + if errorPtr != nil { + defer C.free(unsafe.Pointer(errorPtr)) + return nil, E.New(C.GoString(errorPtr)) + } + return nil, E.New("apple TLS: read metadata") + } + defer C.box_apple_tls_state_free(&state) + + connectionState, rawCerts, err := parseAppleTLSState(&state) if err != nil { C.box_apple_tls_client_cancel(client) C.box_apple_tls_client_free(client) @@ -127,23 +140,6 @@ func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) const appleTLSHandshakePollInterval = 100 * time.Millisecond -func copyAppleTLSConnectionState(client *C.box_apple_tls_client_t) (tls.ConnectionState, [][]byte, error) { - var ( - state C.box_apple_tls_state_t - errorPtr *C.char - ) - stateOK := C.box_apple_tls_client_copy_state(client, &state, &errorPtr) - if !bool(stateOK) { - if errorPtr != nil { - defer C.free(unsafe.Pointer(errorPtr)) - return tls.ConnectionState{}, nil, E.New(C.GoString(errorPtr)) - } - return tls.ConnectionState{}, nil, E.New("apple TLS: read metadata") - } - defer C.box_apple_tls_state_free(&state) - return parseAppleTLSState(&state) -} - func waitAppleTLSClientReady(ctx context.Context, client *C.box_apple_tls_client_t) error { for { if err := ctx.Err(); err != nil { diff --git a/sing-box/common/tls/apple_client_platform_darwin.m b/sing-box/common/tls/apple_client_platform_darwin.m index 7ce49158c4..d03f9fff93 100644 --- a/sing-box/common/tls/apple_client_platform_darwin.m +++ b/sing-box/common/tls/apple_client_platform_darwin.m @@ -113,83 +113,44 @@ static void box_set_error_from_nw_error(char **error_out, nw_error_t error) { CFRelease(cfError); } -static bool box_apple_tls_metadata_load_owned_string( - sec_protocol_metadata_t metadata, - const char *copy_symbol, - const char *get_symbol, - box_sec_protocol_metadata_string_accessor_f *copy_fn, - box_sec_protocol_metadata_string_accessor_f *get_fn, - dispatch_once_t *once_token, - char **value_out, - char **error_out -) { - dispatch_once(once_token, ^{ - *copy_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, copy_symbol); - *get_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, get_symbol); +static char *box_apple_tls_metadata_copy_negotiated_protocol(sec_protocol_metadata_t metadata) { + static box_sec_protocol_metadata_string_accessor_f copy_fn; + static box_sec_protocol_metadata_string_accessor_f get_fn; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + copy_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_copy_negotiated_protocol"); + get_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_get_negotiated_protocol"); }); - if (*copy_fn != NULL) { - const char *value = (*copy_fn)(metadata); - if (value == NULL) { - *value_out = NULL; - return true; - } - char *owned = strdup(value); - free((void *)value); - if (owned == NULL) { - box_set_error_message(error_out, "apple TLS: out of memory"); - return false; - } - *value_out = owned; - return true; + if (copy_fn != NULL) { + return (char *)copy_fn(metadata); } - if (*get_fn != NULL) { - const char *value = (*get_fn)(metadata); - if (value == NULL) { - *value_out = NULL; - return true; + if (get_fn != NULL) { + const char *protocol = get_fn(metadata); + if (protocol != NULL) { + return strdup(protocol); } - char *owned = strdup(value); - if (owned == NULL) { - box_set_error_message(error_out, "apple TLS: out of memory"); - return false; - } - *value_out = owned; - return true; } - *value_out = NULL; - return true; + return NULL; } -static bool box_apple_tls_metadata_load_negotiated_protocol(sec_protocol_metadata_t metadata, char **value_out, char **error_out) { +static char *box_apple_tls_metadata_copy_server_name(sec_protocol_metadata_t metadata) { static box_sec_protocol_metadata_string_accessor_f copy_fn; static box_sec_protocol_metadata_string_accessor_f get_fn; static dispatch_once_t onceToken; - return box_apple_tls_metadata_load_owned_string( - metadata, - "sec_protocol_metadata_copy_negotiated_protocol", - "sec_protocol_metadata_get_negotiated_protocol", - ©_fn, - &get_fn, - &onceToken, - value_out, - error_out - ); -} - -static bool box_apple_tls_metadata_load_server_name(sec_protocol_metadata_t metadata, char **value_out, char **error_out) { - static box_sec_protocol_metadata_string_accessor_f copy_fn; - static box_sec_protocol_metadata_string_accessor_f get_fn; - static dispatch_once_t onceToken; - return box_apple_tls_metadata_load_owned_string( - metadata, - "sec_protocol_metadata_copy_server_name", - "sec_protocol_metadata_get_server_name", - ©_fn, - &get_fn, - &onceToken, - value_out, - error_out - ); + dispatch_once(&onceToken, ^{ + copy_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_copy_server_name"); + get_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_get_server_name"); + }); + if (copy_fn != NULL) { + return (char *)copy_fn(metadata); + } + if (get_fn != NULL) { + const char *server_name = get_fn(metadata); + if (server_name != NULL) { + return strdup(server_name); + } + } + return NULL; } static NSArray *box_split_lines(const char *content, size_t content_len) { @@ -293,64 +254,6 @@ static nw_connection_t box_apple_tls_create_connection(int connected_socket, nw_ return create_fn(connected_socket, parameters); } -static bool box_apple_tls_state_load_sec_metadata(sec_protocol_metadata_t sec_metadata, box_apple_tls_state_t *state, char **error_out) { - box_apple_tls_state_reset(state); - if (sec_metadata == NULL) { - box_set_error_message(error_out, "apple TLS: metadata unavailable"); - return false; - } - state->version = (uint16_t)sec_protocol_metadata_get_negotiated_tls_protocol_version(sec_metadata); - state->cipher_suite = (uint16_t)sec_protocol_metadata_get_negotiated_tls_ciphersuite(sec_metadata); - if (!box_apple_tls_metadata_load_negotiated_protocol(sec_metadata, &state->alpn, error_out)) { - box_apple_tls_state_reset(state); - return false; - } - if (!box_apple_tls_metadata_load_server_name(sec_metadata, &state->server_name, error_out)) { - box_apple_tls_state_reset(state); - return false; - } - - NSMutableData *chain_data = [NSMutableData data]; - sec_protocol_metadata_access_peer_certificate_chain(sec_metadata, ^(sec_certificate_t certificate) { - SecCertificateRef certificate_ref = sec_certificate_copy_ref(certificate); - if (certificate_ref == NULL) { - return; - } - CFDataRef certificate_data = SecCertificateCopyData(certificate_ref); - CFRelease(certificate_ref); - if (certificate_data == NULL) { - return; - } - uint32_t certificate_len = (uint32_t)CFDataGetLength(certificate_data); - uint32_t network_len = htonl(certificate_len); - [chain_data appendBytes:&network_len length:sizeof(network_len)]; - [chain_data appendBytes:CFDataGetBytePtr(certificate_data) length:certificate_len]; - CFRelease(certificate_data); - }); - if (chain_data.length > 0) { - state->peer_cert_chain = malloc(chain_data.length); - if (state->peer_cert_chain == NULL) { - box_set_error_message(error_out, "apple TLS: out of memory"); - box_apple_tls_state_reset(state); - return false; - } - memcpy(state->peer_cert_chain, chain_data.bytes, chain_data.length); - state->peer_cert_chain_len = chain_data.length; - } - return true; -} - -static bool box_apple_tls_client_capture_state(box_apple_tls_client_t *client, sec_protocol_metadata_t metadata, char **error_out) { - box_apple_tls_state_t loaded_state = {0}; - if (!box_apple_tls_state_load_sec_metadata(metadata, &loaded_state, error_out)) { - return false; - } - box_apple_tls_state_reset(&client->state); - client->state = loaded_state; - memset(&loaded_state, 0, sizeof(box_apple_tls_state_t)); - return true; -} - static bool box_apple_tls_state_copy(const box_apple_tls_state_t *source, box_apple_tls_state_t *destination) { memset(destination, 0, sizeof(box_apple_tls_state_t)); destination->version = source->version; @@ -382,6 +285,41 @@ oom: return false; } +// Captures TLS negotiation results from the verify block. The sec_metadata +// exposed here is live for the duration of the handshake; the one retrieved +// after nw_connection_state_ready may return stale ALPN/server_name buffers. +static void box_apple_tls_state_load(sec_protocol_metadata_t sec_metadata, box_apple_tls_state_t *state) { + state->version = (uint16_t)sec_protocol_metadata_get_negotiated_tls_protocol_version(sec_metadata); + state->cipher_suite = (uint16_t)sec_protocol_metadata_get_negotiated_tls_ciphersuite(sec_metadata); + state->alpn = box_apple_tls_metadata_copy_negotiated_protocol(sec_metadata); + state->server_name = box_apple_tls_metadata_copy_server_name(sec_metadata); + + NSMutableData *chain_data = [NSMutableData data]; + sec_protocol_metadata_access_peer_certificate_chain(sec_metadata, ^(sec_certificate_t certificate) { + SecCertificateRef certificate_ref = sec_certificate_copy_ref(certificate); + if (certificate_ref == NULL) { + return; + } + CFDataRef certificate_data = SecCertificateCopyData(certificate_ref); + CFRelease(certificate_ref); + if (certificate_data == NULL) { + return; + } + uint32_t certificate_len = (uint32_t)CFDataGetLength(certificate_data); + uint32_t network_len = htonl(certificate_len); + [chain_data appendBytes:&network_len length:sizeof(network_len)]; + [chain_data appendBytes:CFDataGetBytePtr(certificate_data) length:certificate_len]; + CFRelease(certificate_data); + }); + if (chain_data.length > 0) { + state->peer_cert_chain = malloc(chain_data.length); + if (state->peer_cert_chain != NULL) { + memcpy(state->peer_cert_chain, chain_data.bytes, chain_data.length); + state->peer_cert_chain_len = chain_data.length; + } + } +} + box_apple_tls_client_t *box_apple_tls_client_create( int connected_socket, const char *server_name, @@ -431,17 +369,10 @@ box_apple_tls_client_t *box_apple_tls_client_create( } sec_protocol_options_set_peer_authentication_required(sec_options, !insecure); sec_protocol_options_set_verify_block(sec_options, ^(sec_protocol_metadata_t metadata, sec_trust_t trust, sec_protocol_verify_complete_t complete) { - if (client->ready_error == NULL) { - char *local_error = NULL; - if (!box_apple_tls_client_capture_state(client, metadata, &local_error)) { - client->ready_error = local_error; - } + if (client->state.version == 0) { + box_apple_tls_state_load(metadata, &client->state); } - if (insecure) { - complete(true); - return; - } - complete(box_evaluate_trust(trust, anchors, anchor_only, verifyDate)); + complete(insecure || box_evaluate_trust(trust, anchors, anchor_only, verifyDate)); }, box_apple_tls_client_queue(client)); }, NW_PARAMETERS_DEFAULT_CONFIGURATION); @@ -466,11 +397,11 @@ box_apple_tls_client_t *box_apple_tls_client_create( switch (state) { case nw_connection_state_ready: if (!atomic_load(&client->ready_done)) { - bool state_loaded = client->state.version != 0; - if (!state_loaded && client->ready_error == NULL) { + if (client->state.version == 0) { box_set_error_message(&client->ready_error, "apple TLS: metadata unavailable"); + } else { + atomic_store(&client->ready, true); } - atomic_store(&client->ready, state_loaded); atomic_store(&client->ready_done, true); dispatch_semaphore_signal(box_apple_tls_ready_semaphore(client)); } diff --git a/sing-box/common/tls/client.go b/sing-box/common/tls/client.go index 22215176e0..b5b975bf23 100644 --- a/sing-box/common/tls/client.go +++ b/sing-box/common/tls/client.go @@ -48,16 +48,7 @@ func applyTLSSpoof(conn net.Conn, spoof string, method tlsspoof.Method) (net.Con if spoof == "" { return conn, nil } - spoofer, err := tlsspoof.NewSpoofer(conn, method) - if err != nil { - return nil, err - } - spoofConn, err := tlsspoof.NewConn(conn, spoofer, spoof) - if err != nil { - spoofer.Close() - return nil, err - } - return spoofConn, nil + return tlsspoof.NewConn(conn, method, spoof) } func NewDialerFromOptions(ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) { diff --git a/sing-box/common/tlsspoof/conn_test.go b/sing-box/common/tlsspoof/conn_test.go index d27c53a989..b41cf54753 100644 --- a/sing-box/common/tlsspoof/conn_test.go +++ b/sing-box/common/tlsspoof/conn_test.go @@ -58,7 +58,7 @@ func TestConn_Write_InjectsThenForwards(t *testing.T) { client, server := net.Pipe() spoofer := &fakeSpoofer{} - wrapped, err := NewConn(client, spoofer, "letsencrypt.org") + wrapped, err := newConn(client, spoofer, "letsencrypt.org") require.NoError(t, err) serverRead := make(chan []byte, 1) @@ -87,7 +87,7 @@ func TestConn_Write_SecondWriteDoesNotInject(t *testing.T) { client, server := net.Pipe() spoofer := &fakeSpoofer{} - wrapped, err := NewConn(client, spoofer, "letsencrypt.org") + wrapped, err := newConn(client, spoofer, "letsencrypt.org") require.NoError(t, err) serverRead := make(chan []byte, 1) @@ -115,7 +115,7 @@ func TestConn_Write_SurfacesCloseError(t *testing.T) { defer client.Close() defer server.Close() spoofer := &fakeSpoofer{closeErr: errSpoofClose} - wrapped, err := NewConn(client, spoofer, "letsencrypt.org") + wrapped, err := newConn(client, spoofer, "letsencrypt.org") require.NoError(t, err) go func() { _, _ = io.ReadAll(server) }() @@ -130,7 +130,7 @@ func TestConn_NewConn_RejectsEmptySNI(t *testing.T) { client, server := net.Pipe() defer client.Close() defer server.Close() - _, err := NewConn(client, &fakeSpoofer{}, "") + _, err := newConn(client, &fakeSpoofer{}, "") require.Error(t, err, "empty SNI must fail at construction") } @@ -195,7 +195,7 @@ func TestConn_StackedWithRecordFragment(t *testing.T) { fragConn := tf.NewConn(client, context.Background(), false, true, time.Millisecond) spoofer := &fakeSpoofer{} - wrapped, err := NewConn(fragConn, spoofer, "letsencrypt.org") + wrapped, err := newConn(fragConn, spoofer, "letsencrypt.org") require.NoError(t, err) serverRead := make(chan []byte, 1) @@ -238,7 +238,7 @@ func TestConn_StackedWithPacketFragment(t *testing.T) { rc := &recordingConn{Conn: client} fragConn := tf.NewConn(rc, context.Background(), true, false, time.Millisecond) spoofer := &fakeSpoofer{} - wrapped, err := NewConn(fragConn, spoofer, "letsencrypt.org") + wrapped, err := newConn(fragConn, spoofer, "letsencrypt.org") require.NoError(t, err) serverRead := make(chan []byte, 1) @@ -273,7 +273,7 @@ func TestConn_StackedWithBothFragment(t *testing.T) { rc := &recordingConn{Conn: client} fragConn := tf.NewConn(rc, context.Background(), true, true, time.Millisecond) spoofer := &fakeSpoofer{} - wrapped, err := NewConn(fragConn, spoofer, "letsencrypt.org") + wrapped, err := newConn(fragConn, spoofer, "letsencrypt.org") require.NoError(t, err) serverRead := make(chan []byte, 1) @@ -330,7 +330,7 @@ func TestConn_StackedInjectionOrder(t *testing.T) { rc := &recordingConn{Conn: client, timeline: &timeline} fragConn := tf.NewConn(rc, context.Background(), true, true, time.Millisecond) spoofer := &trackingSpoofer{timeline: &timeline} - wrapped, err := NewConn(fragConn, spoofer, "letsencrypt.org") + wrapped, err := newConn(fragConn, spoofer, "letsencrypt.org") require.NoError(t, err) serverRead := make(chan []byte, 1) diff --git a/sing-box/common/tlsspoof/integration_tls_test.go b/sing-box/common/tlsspoof/integration_tls_test.go index 669df7eb2f..d179c3841c 100644 --- a/sing-box/common/tlsspoof/integration_tls_test.go +++ b/sing-box/common/tlsspoof/integration_tls_test.go @@ -86,9 +86,7 @@ func TestIntegrationConn_RealTLSHandshake(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { raw.Close() }) - spoofer, err := NewSpoofer(raw, MethodWrongSequence) - require.NoError(t, err) - wrapped, err := NewConn(raw, spoofer, fakeSNI) + wrapped, err := NewConn(raw, MethodWrongSequence, fakeSNI) require.NoError(t, err) clientConfig := &tls.Config{ diff --git a/sing-box/common/tlsspoof/integration_unix_test.go b/sing-box/common/tlsspoof/integration_unix_test.go index c5d26128a5..0f4585fd82 100644 --- a/sing-box/common/tlsspoof/integration_unix_test.go +++ b/sing-box/common/tlsspoof/integration_unix_test.go @@ -15,7 +15,7 @@ import ( func TestIntegrationSpoofer_WrongChecksum(t *testing.T) { requireRoot(t) client, serverPort := dialLocalEchoServer(t) - spoofer, err := NewSpoofer(client, MethodWrongChecksum) + spoofer, err := newRawSpoofer(client, MethodWrongChecksum) require.NoError(t, err) defer spoofer.Close() @@ -31,7 +31,7 @@ func TestIntegrationSpoofer_WrongChecksum(t *testing.T) { func TestIntegrationSpoofer_WrongSequence(t *testing.T) { requireRoot(t) client, serverPort := dialLocalEchoServer(t) - spoofer, err := NewSpoofer(client, MethodWrongSequence) + spoofer, err := newRawSpoofer(client, MethodWrongSequence) require.NoError(t, err) defer spoofer.Close() @@ -47,7 +47,7 @@ func TestIntegrationSpoofer_WrongSequence(t *testing.T) { func TestIntegrationSpoofer_IPv6_WrongChecksum(t *testing.T) { requireRoot(t) client, serverPort := dialLocalEchoServerIPv6(t) - spoofer, err := NewSpoofer(client, MethodWrongChecksum) + spoofer, err := newRawSpoofer(client, MethodWrongChecksum) require.NoError(t, err) defer spoofer.Close() @@ -63,7 +63,7 @@ func TestIntegrationSpoofer_IPv6_WrongChecksum(t *testing.T) { func TestIntegrationSpoofer_IPv6_WrongSequence(t *testing.T) { requireRoot(t) client, serverPort := dialLocalEchoServerIPv6(t) - spoofer, err := NewSpoofer(client, MethodWrongSequence) + spoofer, err := newRawSpoofer(client, MethodWrongSequence) require.NoError(t, err) defer spoofer.Close() @@ -130,9 +130,7 @@ func runFakeAndRealHaveDistinctSNIs(t *testing.T, network, address, fakeSNI stri listener.Close() }) - spoofer, err := NewSpoofer(client, MethodWrongSequence) - require.NoError(t, err) - wrapped, err := NewConn(client, spoofer, fakeSNI) + wrapped, err := NewConn(client, MethodWrongSequence, fakeSNI) require.NoError(t, err) payload, err := hex.DecodeString(realClientHello) @@ -185,9 +183,7 @@ func runInjectsThenForwardsRealCH(t *testing.T, network, address string) { listener.Close() }) - spoofer, err := NewSpoofer(client, MethodWrongSequence) - require.NoError(t, err) - wrapped, err := NewConn(client, spoofer, "letsencrypt.org") + wrapped, err := NewConn(client, MethodWrongSequence, "letsencrypt.org") require.NoError(t, err) payload, err := hex.DecodeString(realClientHello) diff --git a/sing-box/common/tlsspoof/integration_windows_test.go b/sing-box/common/tlsspoof/integration_windows_test.go index 3b8e3c549e..b0461a31b2 100644 --- a/sing-box/common/tlsspoof/integration_windows_test.go +++ b/sing-box/common/tlsspoof/integration_windows_test.go @@ -12,11 +12,11 @@ import ( "github.com/stretchr/testify/require" ) -func newSpoofer(t *testing.T, conn net.Conn, method Method) Spoofer { +func newSpoofer(t *testing.T, conn net.Conn, method Method) rawSpoofer { t.Helper() - spoofer, err := NewSpoofer(conn, method) + s, err := newRawSpoofer(conn, method) require.NoError(t, err) - return spoofer + return s } // Basic lifecycle: opening a spoofer against a live TCP conn installs @@ -71,8 +71,7 @@ func TestIntegrationConnInjectsThenForwardsRealCH(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { client.Close() }) - spoofer := newSpoofer(t, client, MethodWrongSequence) - wrapped, err := NewConn(client, spoofer, "letsencrypt.org") + wrapped, err := NewConn(client, MethodWrongSequence, "letsencrypt.org") require.NoError(t, err) payload, err := hex.DecodeString(realClientHello) @@ -94,7 +93,7 @@ func TestIntegrationConnInjectsThenForwardsRealCH(t *testing.T) { // Inject before any kernel payload: stages the fake, then Write flushes // the real CH. Same terminal expectation as the Conn variant but via the -// Spoofer primitive directly. +// raw spoofer primitive directly. func TestIntegrationSpooferInjectThenWrite(t *testing.T) { listener, err := net.Listen("tcp4", "127.0.0.1:0") require.NoError(t, err) diff --git a/sing-box/common/tlsspoof/raw_darwin.go b/sing-box/common/tlsspoof/raw_darwin.go index 73b11dd37a..ab31687692 100644 --- a/sing-box/common/tlsspoof/raw_darwin.go +++ b/sing-box/common/tlsspoof/raw_darwin.go @@ -67,7 +67,7 @@ type darwinSpoofer struct { receiveNext uint32 } -func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) { +func newRawSpoofer(conn net.Conn, method Method) (rawSpoofer, error) { _, src, dst, err := tcpEndpoints(conn) if err != nil { return nil, err diff --git a/sing-box/common/tlsspoof/raw_linux.go b/sing-box/common/tlsspoof/raw_linux.go index 70774c297c..f82fbc9efb 100644 --- a/sing-box/common/tlsspoof/raw_linux.go +++ b/sing-box/common/tlsspoof/raw_linux.go @@ -29,7 +29,7 @@ type linuxSpoofer struct { receiveNext uint32 } -func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) { +func newRawSpoofer(conn net.Conn, method Method) (rawSpoofer, error) { tcpConn, src, dst, err := tcpEndpoints(conn) if err != nil { return nil, err diff --git a/sing-box/common/tlsspoof/raw_stub.go b/sing-box/common/tlsspoof/raw_stub.go index a2da87d6b3..7edf2441a6 100644 --- a/sing-box/common/tlsspoof/raw_stub.go +++ b/sing-box/common/tlsspoof/raw_stub.go @@ -10,6 +10,6 @@ import ( const PlatformSupported = false -func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) { +func newRawSpoofer(conn net.Conn, method Method) (rawSpoofer, error) { return nil, E.New("tls_spoof: unsupported platform") } diff --git a/sing-box/common/tlsspoof/raw_windows.go b/sing-box/common/tlsspoof/raw_windows.go index 4c6d130fbf..9f6553f1b8 100644 --- a/sing-box/common/tlsspoof/raw_windows.go +++ b/sing-box/common/tlsspoof/raw_windows.go @@ -41,7 +41,7 @@ type windowsSpoofer struct { runErr atomic.Pointer[error] } -func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) { +func newRawSpoofer(conn net.Conn, method Method) (rawSpoofer, error) { _, src, dst, err := tcpEndpoints(conn) if err != nil { return nil, err diff --git a/sing-box/common/tlsspoof/spoof.go b/sing-box/common/tlsspoof/spoof.go index 249af08f6c..1bca5693fe 100644 --- a/sing-box/common/tlsspoof/spoof.go +++ b/sing-box/common/tlsspoof/spoof.go @@ -40,23 +40,32 @@ func (m Method) String() string { } } -type Spoofer interface { +type rawSpoofer interface { Inject(payload []byte) error Close() error } -func NewSpoofer(conn net.Conn, method Method) (Spoofer, error) { - return newRawSpoofer(conn, method) -} - type Conn struct { net.Conn - spoofer Spoofer + spoofer rawSpoofer fakeHello []byte injected bool } -func NewConn(conn net.Conn, spoofer Spoofer, fakeSNI string) (*Conn, error) { +func NewConn(conn net.Conn, method Method, fakeSNI string) (*Conn, error) { + spoofer, err := newRawSpoofer(conn, method) + if err != nil { + return nil, err + } + result, err := newConn(conn, spoofer, fakeSNI) + if err != nil { + spoofer.Close() + return nil, err + } + return result, nil +} + +func newConn(conn net.Conn, spoofer rawSpoofer, fakeSNI string) (*Conn, error) { fakeHello, err := buildFakeClientHello(fakeSNI) if err != nil { return nil, E.Cause(err, "tls_spoof: build fake ClientHello") @@ -88,7 +97,7 @@ func (c *Conn) Write(b []byte) (n int, err error) { func (c *Conn) Close() error { return E.Append(c.Conn.Close(), c.spoofer.Close(), func(e error) error { - return E.Cause(e, "close spoofer") + return E.Cause(e, "tls_spoof: close spoofer") }) } diff --git a/sing-box/docs/changelog.md b/sing-box/docs/changelog.md index 590d7bf1c8..1f88e21181 100644 --- a/sing-box/docs/changelog.md +++ b/sing-box/docs/changelog.md @@ -2,6 +2,10 @@ icon: material/alert-decagram --- +#### 1.14.0-alpha.14 + +* Fixes and improvements + #### 1.14.0-alpha.13 * Unify HTTP client **1** diff --git a/sing-box/go.mod b/sing-box/go.mod index d0f4f0b9ad..6fda1a8eef 100644 --- a/sing-box/go.mod +++ b/sing-box/go.mod @@ -37,7 +37,7 @@ require ( github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 - github.com/sagernet/sing v0.8.7-0.20260416084641-60846c117dd8 + github.com/sagernet/sing v0.8.7-0.20260417135631-0d22698ed386 github.com/sagernet/sing-cloudflared v0.0.0-20260416083718-efa6ab16dba9 github.com/sagernet/sing-mux v0.3.4 github.com/sagernet/sing-quic v0.6.2-0.20260412143638-8f65b6be7cd6 diff --git a/sing-box/go.sum b/sing-box/go.sum index 36c14d0bdd..77a3ad21ab 100644 --- a/sing-box/go.sum +++ b/sing-box/go.sum @@ -242,8 +242,8 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= -github.com/sagernet/sing v0.8.7-0.20260416084641-60846c117dd8 h1:gBTi6DvU/F/ZKcq9HvEWpT/DhpNU68FTGHK2c+m80vo= -github.com/sagernet/sing v0.8.7-0.20260416084641-60846c117dd8/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.8.7-0.20260417135631-0d22698ed386 h1:gNonzIaoKi4MiEmK/5iJI+RoGof1PtYqzSaNE/lmWLw= +github.com/sagernet/sing v0.8.7-0.20260417135631-0d22698ed386/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-cloudflared v0.0.0-20260416083718-efa6ab16dba9 h1:wWJexq4V7ope1c4XYSqT/hydqd9omdtbDRvSZIOhjfY= github.com/sagernet/sing-cloudflared v0.0.0-20260416083718-efa6ab16dba9/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= diff --git a/small/v2ray-geodata/Makefile b/small/v2ray-geodata/Makefile index 1b9d47e3aa..67f4197a0c 100644 --- a/small/v2ray-geodata/Makefile +++ b/small/v2ray-geodata/Makefile @@ -21,13 +21,13 @@ define Download/geoip HASH:=16dbd19ff8dddb69960f313a3b0c0623cae82dc9725687110c28740226d3b285 endef -GEOSITE_VER:=20260417103656 +GEOSITE_VER:=20260418094517 GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER) define Download/geosite URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/ URL_FILE:=dlc.dat FILE:=$(GEOSITE_FILE) - HASH:=e99ce48793a85e3ae66d0234414d20c7c469c5adfc123d5d7becd027c40f4dfa + HASH:=5ff843a4c8ddcc8a7864301c6a84c50ac190ec10a99c0b66c865861d68fb4c30 endef GEOSITE_IRAN_VER:=202604130112 diff --git a/v2rayn/v2rayN/ServiceLib/Global.cs b/v2rayn/v2rayN/ServiceLib/Global.cs index 267ec60a2f..7c0f98d806 100644 --- a/v2rayn/v2rayN/ServiceLib/Global.cs +++ b/v2rayn/v2rayN/ServiceLib/Global.cs @@ -94,7 +94,7 @@ public class Global public const string SingboxHostsDNSTag = "hosts_dns"; public const string SingboxFakeDNSTag = "fake_dns"; - public const int Hysteria2DefaultHopInt = 10; + public const int Hysteria2DefaultHopInt = 30; public const string PolicyGroupExcludeKeywords = @"剩余|过期|到期|重置|[Rr]emaining|[Ee]xpir|[Rr]eset"; diff --git a/v2rayn/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayn/v2rayN/ServiceLib/Handler/ConfigHandler.cs index eab544dabc..da7258a7d2 100644 --- a/v2rayn/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayn/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -77,10 +77,11 @@ public static class ConfigHandler Tti = 50, UplinkCapacity = 12, DownlinkCapacity = 100, - ReadBufferSize = 2, - WriteBufferSize = 2, - Congestion = false + CwndMultiplier = 1, + MaxSendingWindow = 2 * 1024 * 1024, }; + config.KcpItem.CwndMultiplier = config.KcpItem.CwndMultiplier <= 0 ? 1 : config.KcpItem.CwndMultiplier; + config.KcpItem.MaxSendingWindow = config.KcpItem.MaxSendingWindow <= 0 ? (2 * 1024 * 1024) : config.KcpItem.MaxSendingWindow; config.GrpcItem ??= new GrpcItem { IdleTimeout = 60, diff --git a/v2rayn/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayn/v2rayN/ServiceLib/Models/ConfigItems.cs index fa766f0d65..1988d3b713 100644 --- a/v2rayn/v2rayN/ServiceLib/Models/ConfigItems.cs +++ b/v2rayn/v2rayN/ServiceLib/Models/ConfigItems.cs @@ -49,11 +49,9 @@ public class KcpItem public int DownlinkCapacity { get; set; } - public bool Congestion { get; set; } + public int CwndMultiplier { get; set; } - public int ReadBufferSize { get; set; } - - public int WriteBufferSize { get; set; } + public int MaxSendingWindow { get; set; } } [Serializable] @@ -197,7 +195,7 @@ public class HysteriaItem { public int UpMbps { get; set; } public int DownMbps { get; set; } - public int HopInterval { get; set; } = 30; + public int HopInterval { get; set; } = Global.Hysteria2DefaultHopInt; } [Serializable] diff --git a/v2rayn/v2rayN/ServiceLib/Models/V2rayConfig.cs b/v2rayn/v2rayN/ServiceLib/Models/V2rayConfig.cs index a5ac2e24de..909a37fb1d 100644 --- a/v2rayn/v2rayN/ServiceLib/Models/V2rayConfig.cs +++ b/v2rayn/v2rayN/ServiceLib/Models/V2rayConfig.cs @@ -409,11 +409,9 @@ public class KcpSettings4Ray public int downlinkCapacity { get; set; } - public bool congestion { get; set; } + public int cwndMultiplier { get; set; } - public int readBufferSize { get; set; } - - public int writeBufferSize { get; set; } + public int maxSendingWindow { get; set; } } public class WsSettings4Ray diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 234cdef3cd..0743b6c7ff 100644 --- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -4681,7 +4681,16 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 XHTTP Extra raw JSON, format: { XHTTP Object } 的本地化字符串。 + /// 查找类似 XHTTP Extra 的本地化字符串。 + /// + public static string TransportExtra { + get { + return ResourceManager.GetString("TransportExtra", resourceCulture); + } + } + + /// + /// 查找类似 Raw JSON, format: { XHTTP Object } 的本地化字符串。 /// public static string TransportExtraTip { get { diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 08aaf89314..84f9722e2d 100644 --- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1321,7 +1321,7 @@ حالت xhttp - جیسون خام XHTTP Extra, فرمت: { XHTTPObject } + Raw JSON, format: { XHTTP Object } هنگام بستن پنجره در سینی پنهان شوید @@ -1704,4 +1704,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Host + + XHTTP Extra + \ No newline at end of file diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.fr.resx index 8540727fe7..84033130c2 100644 --- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.fr.resx +++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.fr.resx @@ -1327,7 +1327,7 @@ Mode XHTTP - JSON brut XHTTP Extra, format : { XHTTPObject } + Raw JSON, format: { XHTTP Object } Masquer dans la barre d’état à la fermeture de la fenêtre @@ -1707,4 +1707,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Host - + + XHTTP Extra + + \ No newline at end of file diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.hu.resx index f761cca9dd..e58ae6f06c 100644 --- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1321,7 +1321,7 @@ xhttp mód - XHTTP Extra nyers JSON, formátum: { XHTTP Objektum } + Raw JSON, format: { XHTTP Object } Ablak bezárásakor a tálcára rejtés @@ -1704,4 +1704,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Host + + XHTTP Extra + \ No newline at end of file diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.resx index d30c3f3183..b4120d3c71 100644 --- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1330,7 +1330,7 @@ xhttp mode - XHTTP Extra raw JSON, format: { XHTTP Object } + Raw JSON, format: { XHTTP Object } Hide to tray when closing the window @@ -1710,4 +1710,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Host + + XHTTP Extra + \ No newline at end of file diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 1521e67ce6..4d30949c4b 100644 --- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1321,7 +1321,7 @@ XHTTP-режим - Дополнительный сырой JSON для XHTTP, формат: { XHTTP Object } + Raw JSON, format: { XHTTP Object } Сворачивать в трей при закрытии окна @@ -1704,4 +1704,7 @@ Host + + XHTTP Extra + \ No newline at end of file diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index 379fb4bad6..b47b832e1d 100644 --- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1327,7 +1327,7 @@ XHTTP 模式 - XHTTP Extra 原始 JSON,格式: { XHTTPObject } + 原始 JSON,格式: { XHTTPObject } 关闭窗口时隐藏至托盘 @@ -1707,4 +1707,7 @@ Host + + XHTTP Extra + \ No newline at end of file diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index a903d16874..7ba89b3381 100644 --- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1318,7 +1318,7 @@ xhttp 模式 - XHTTP Extra 原始 JSON,格式: { XHTTPObject } + 原始 JSON,格式: { XHTTPObject } 關閉視窗時隱藏至托盤 @@ -1701,4 +1701,7 @@ Host + + XHTTP Extra + \ No newline at end of file diff --git a/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs b/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs index 6e563f0fc9..e6bc48cb87 100644 --- a/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs +++ b/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs @@ -5,12 +5,18 @@ public partial class CoreConfigV2rayService private string ApplyFullConfigTemplate() { var fullConfigTemplate = context.FullConfigTemplate; - if (fullConfigTemplate == null || !fullConfigTemplate.Enabled || fullConfigTemplate.Config.IsNullOrEmpty()) + if (fullConfigTemplate is not { Enabled: true }) { return JsonUtils.Serialize(_coreConfig); } - var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplate.Config); + var fullConfigTemplateItem = context.IsTunEnabled ? fullConfigTemplate.TunConfig : fullConfigTemplate.Config; + if (fullConfigTemplateItem.IsNullOrEmpty()) + { + return JsonUtils.Serialize(_coreConfig); + } + + var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplateItem); if (fullConfigTemplateNode == null) { return JsonUtils.Serialize(_coreConfig); diff --git a/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs b/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs index 6b0d335daa..7faae5b1b0 100644 --- a/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs +++ b/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs @@ -370,11 +370,11 @@ public partial class CoreConfigV2rayService try { var item = context.RawDnsItem; - var normalDNS = item?.NormalDNS; + var customDNS = context.IsTunEnabled ? item?.TunDNS : item?.NormalDNS; var domainStrategy4Freedom = item?.DomainStrategy4Freedom; - if (normalDNS.IsNullOrEmpty()) + if (customDNS.IsNullOrEmpty()) { - normalDNS = EmbedUtils.GetEmbedText(Global.DNSV2rayNormalFileName); + customDNS = EmbedUtils.GetEmbedText(Global.DNSV2rayNormalFileName); } //Outbound Freedom domainStrategy @@ -389,11 +389,11 @@ public partial class CoreConfigV2rayService } } - var obj = JsonUtils.ParseJson(normalDNS); + var obj = JsonUtils.ParseJson(customDNS); if (obj is null) { List servers = []; - var arrDNS = normalDNS.Split(','); + var arrDNS = customDNS.Split(','); foreach (var str in arrDNS) { servers.Add(str); diff --git a/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs index a0d4099fa8..d40ed8a377 100644 --- a/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs +++ b/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs @@ -477,9 +477,8 @@ public partial class CoreConfigV2rayService kcpSettings.uplinkCapacity = _config.KcpItem.UplinkCapacity; kcpSettings.downlinkCapacity = _config.KcpItem.DownlinkCapacity; - kcpSettings.congestion = _config.KcpItem.Congestion; - kcpSettings.readBufferSize = _config.KcpItem.ReadBufferSize; - kcpSettings.writeBufferSize = _config.KcpItem.WriteBufferSize; + kcpSettings.cwndMultiplier = _config.KcpItem.CwndMultiplier; + kcpSettings.maxSendingWindow = _config.KcpItem.MaxSendingWindow; var kcpFinalmask = new Finalmask4Ray(); if (Global.KcpHeaderMaskMap.TryGetValue(headerType, out var header)) { diff --git a/v2rayn/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs b/v2rayn/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs index 9fbb612578..0264bc1fbd 100644 --- a/v2rayn/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs +++ b/v2rayn/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs @@ -292,7 +292,7 @@ public class AddServerViewModel : MyReactiveObject SalamanderPass = protocolExtra?.SalamanderPass ?? string.Empty; UpMbps = protocolExtra?.UpMbps; DownMbps = protocolExtra?.DownMbps; - HopInterval = protocolExtra?.HopInterval.IsNullOrEmpty() ?? true ? Global.Hysteria2DefaultHopInt.ToString() : protocolExtra.HopInterval; + HopInterval = protocolExtra?.HopInterval ?? string.Empty; VmessSecurity = protocolExtra?.VmessSecurity?.IsNullOrEmpty() == false ? protocolExtra.VmessSecurity : Global.DefaultSecurity; VlessEncryption = protocolExtra?.VlessEncryption.IsNullOrEmpty() == false ? protocolExtra.VlessEncryption : Global.None; SsMethod = protocolExtra?.SsMethod ?? string.Empty; diff --git a/v2rayn/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs b/v2rayn/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs index 00178d1797..25fbe4b92b 100644 --- a/v2rayn/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs +++ b/v2rayn/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs @@ -20,6 +20,7 @@ public class DNSSettingViewModel : MyReactiveObject [Reactive] public string DomainStrategy4FreedomCompatible { get; set; } [Reactive] public string DomainDNSAddressCompatible { get; set; } [Reactive] public string NormalDNSCompatible { get; set; } + [Reactive] public string TunDNSCompatible { get; set; } [Reactive] public string DomainStrategy4Freedom2Compatible { get; set; } [Reactive] public string DomainDNSAddress2Compatible { get; set; } @@ -43,6 +44,7 @@ public class DNSSettingViewModel : MyReactiveObject ImportDefConfig4V2rayCompatibleCmd = ReactiveCommand.CreateFromTask(async () => { NormalDNSCompatible = EmbedUtils.GetEmbedText(Global.DNSV2rayNormalFileName); + TunDNSCompatible = EmbedUtils.GetEmbedText(Global.DNSV2rayNormalFileName); await Task.CompletedTask; }); @@ -84,6 +86,7 @@ public class DNSSettingViewModel : MyReactiveObject DomainStrategy4FreedomCompatible = item1?.DomainStrategy4Freedom ?? string.Empty; DomainDNSAddressCompatible = item1?.DomainDNSAddress ?? string.Empty; NormalDNSCompatible = item1?.NormalDNS ?? string.Empty; + TunDNSCompatible = item1?.TunDNS ?? string.Empty; var item2 = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); SBCustomDNSEnableCompatible = item2.Enabled; @@ -124,6 +127,21 @@ public class DNSSettingViewModel : MyReactiveObject } } } + if (TunDNSCompatible.IsNotEmpty()) + { + var obj = JsonUtils.ParseJson(TunDNSCompatible); + if (obj != null && obj["servers"] != null) + { + } + else + { + if (TunDNSCompatible.Contains('{') || TunDNSCompatible.Contains('}')) + { + NoticeManager.Instance.Enqueue(ResUI.FillCorrectDNSText); + return; + } + } + } if (NormalDNS2Compatible.IsNotEmpty()) { var obj2 = JsonUtils.Deserialize(NormalDNS2Compatible); @@ -149,6 +167,7 @@ public class DNSSettingViewModel : MyReactiveObject item1.DomainDNSAddress = DomainDNSAddressCompatible; item1.UseSystemHosts = UseSystemHostsCompatible; item1.NormalDNS = NormalDNSCompatible; + item1.TunDNS = TunDNSCompatible; await ConfigHandler.SaveDNSItems(_config, item1); var item2 = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); diff --git a/v2rayn/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs b/v2rayn/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs index 3a50b52ede..78cd10a260 100644 --- a/v2rayn/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs +++ b/v2rayn/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs @@ -13,6 +13,9 @@ public class FullConfigTemplateViewModel : MyReactiveObject [Reactive] public string FullConfigTemplate4Ray { get; set; } + [Reactive] + public string FullTunConfigTemplate4Ray { get; set; } + [Reactive] public string FullConfigTemplate4Singbox { get; set; } @@ -50,10 +53,15 @@ public class FullConfigTemplateViewModel : MyReactiveObject private async Task Init() { var item = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray); - EnableFullConfigTemplate4Ray = item?.Enabled ?? false; - FullConfigTemplate4Ray = item?.Config ?? string.Empty; - AddProxyOnly4Ray = item?.AddProxyOnly ?? false; - ProxyDetour4Ray = item?.ProxyDetour ?? string.Empty; + if (item == null) + { + return; + } + EnableFullConfigTemplate4Ray = item.Enabled; + FullConfigTemplate4Ray = item.Config ?? string.Empty; + FullTunConfigTemplate4Ray = item.TunConfig ?? string.Empty; + AddProxyOnly4Ray = item.AddProxyOnly ?? false; + ProxyDetour4Ray = item.ProxyDetour ?? string.Empty; var item2 = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.sing_box); EnableFullConfigTemplate4Singbox = item2?.Enabled ?? false; @@ -82,10 +90,13 @@ public class FullConfigTemplateViewModel : MyReactiveObject private async Task SaveXrayConfigAsync() { var item = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray); + if (item == null) + { + return false; + } item.Enabled = EnableFullConfigTemplate4Ray; - item.Config = null; - item.Config = FullConfigTemplate4Ray; + item.TunConfig = FullTunConfigTemplate4Ray; item.AddProxyOnly = AddProxyOnly4Ray; item.ProxyDetour = ProxyDetour4Ray; @@ -97,10 +108,11 @@ public class FullConfigTemplateViewModel : MyReactiveObject private async Task SaveSingboxConfigAsync() { var item = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.sing_box); + if (item == null) + { + return false; + } item.Enabled = EnableFullConfigTemplate4Singbox; - item.Config = null; - item.TunConfig = null; - item.Config = FullConfigTemplate4Singbox; item.TunConfig = FullTunConfigTemplate4Singbox; diff --git a/v2rayn/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml b/v2rayn/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml index 1d2bb899fe..56a579dbc2 100644 --- a/v2rayn/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml +++ b/v2rayn/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml @@ -814,17 +814,17 @@ Grid.Column="0" Margin="{StaticResource Margin4}" VerticalAlignment="Top" - Text="{x:Static resx:ResUI.TransportExtraTip}" + Text="{x:Static resx:ResUI.TransportExtra}" TextWrapping="Wrap" /> - + IsExpanded="True"> + + + + + - - - + + + + + + + + + + + + diff --git a/v2rayn/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs b/v2rayn/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs index dd39e22978..e2ea1201b0 100644 --- a/v2rayn/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs +++ b/v2rayn/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs @@ -52,6 +52,7 @@ public partial class DNSSettingWindow : WindowBase this.Bind(ViewModel, vm => vm.DomainStrategy4FreedomCompatible, v => v.cmbdomainStrategy4FreedomCompatible.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.DomainDNSAddressCompatible, v => v.cmbdomainDNSAddressCompatible.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.NormalDNSCompatible, v => v.txtnormalDNSCompatible.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.TunDNSCompatible, v => v.txttunDNSCompatible.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.DomainStrategy4Freedom2Compatible, v => v.cmbdomainStrategy4OutCompatible.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.DomainDNSAddress2Compatible, v => v.cmbdomainDNSAddress2Compatible.Text).DisposeWith(disposables); diff --git a/v2rayn/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml b/v2rayn/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml index 1a2b482c0d..7ff499374f 100644 --- a/v2rayn/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml +++ b/v2rayn/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml @@ -90,13 +90,26 @@ - - - + + + + + + + + + + + + diff --git a/v2rayn/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml.cs b/v2rayn/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml.cs index bfe0c2c580..6ea6e994ae 100644 --- a/v2rayn/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml.cs +++ b/v2rayn/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml.cs @@ -12,13 +12,14 @@ public partial class FullConfigTemplateWindow : WindowBase Close(); + btnCancel.Click += (_, _) => Close(); ViewModel = new FullConfigTemplateViewModel(UpdateViewHandler); this.WhenActivated(disposables => { this.Bind(ViewModel, vm => vm.EnableFullConfigTemplate4Ray, v => v.rayFullConfigTemplateEnable.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.FullConfigTemplate4Ray, v => v.rayFullConfigTemplate.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.FullTunConfigTemplate4Ray, v => v.rayFullTunConfigTemplate.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AddProxyOnly4Ray, v => v.togAddProxyProtocolOutboundOnly4Ray.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.ProxyDetour4Ray, v => v.txtProxyDetour4Ray.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.EnableFullConfigTemplate4Singbox, v => v.sbFullConfigTemplateEnable.IsChecked).DisposeWith(disposables); diff --git a/v2rayn/v2rayN/v2rayN/Views/AddServerWindow.xaml b/v2rayn/v2rayN/v2rayN/Views/AddServerWindow.xaml index 25a1234b8d..ec86e90490 100644 --- a/v2rayn/v2rayN/v2rayN/Views/AddServerWindow.xaml +++ b/v2rayn/v2rayN/v2rayN/Views/AddServerWindow.xaml @@ -1071,7 +1071,7 @@ Margin="{StaticResource Margin4}" VerticalAlignment="Top" Style="{StaticResource ToolbarTextBlock}" - Text="{x:Static resx:ResUI.TransportExtraTip}" + Text="{x:Static resx:ResUI.TransportExtra}" TextWrapping="Wrap" /> diff --git a/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml b/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml index 8788e97d80..d27188a4a8 100644 --- a/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml +++ b/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml @@ -453,16 +453,37 @@ - + + + + + + + + + + + + + diff --git a/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs b/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs index 2710b102b8..a7f1bd12d4 100644 --- a/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs +++ b/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs @@ -50,6 +50,7 @@ public partial class DNSSettingWindow this.Bind(ViewModel, vm => vm.DomainStrategy4FreedomCompatible, v => v.cmbdomainStrategy4FreedomCompatible.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.DomainDNSAddressCompatible, v => v.cmbdomainDNSAddressCompatible.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.NormalDNSCompatible, v => v.txtnormalDNSCompatible.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.TunDNSCompatible, v => v.txttunDNSCompatible.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.DomainStrategy4Freedom2Compatible, v => v.cmbdomainStrategy4OutCompatible.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.DomainDNSAddress2Compatible, v => v.cmbdomainDNSAddress2Compatible.Text).DisposeWith(disposables); diff --git a/v2rayn/v2rayN/v2rayN/Views/FullConfigTemplateWindow.xaml b/v2rayn/v2rayN/v2rayN/Views/FullConfigTemplateWindow.xaml index d3ca8af7bc..4b9953d7c0 100644 --- a/v2rayn/v2rayN/v2rayN/Views/FullConfigTemplateWindow.xaml +++ b/v2rayn/v2rayN/v2rayN/Views/FullConfigTemplateWindow.xaml @@ -107,16 +107,37 @@ - + + + + + + + + + + + + + diff --git a/v2rayn/v2rayN/v2rayN/Views/FullConfigTemplateWindow.xaml.cs b/v2rayn/v2rayN/v2rayN/Views/FullConfigTemplateWindow.xaml.cs index a9f95a5359..031dc40abb 100644 --- a/v2rayn/v2rayN/v2rayN/Views/FullConfigTemplateWindow.xaml.cs +++ b/v2rayn/v2rayN/v2rayN/Views/FullConfigTemplateWindow.xaml.cs @@ -17,6 +17,7 @@ public partial class FullConfigTemplateWindow { this.Bind(ViewModel, vm => vm.EnableFullConfigTemplate4Ray, v => v.rayFullConfigTemplateEnable.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.FullConfigTemplate4Ray, v => v.rayFullConfigTemplate.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.FullTunConfigTemplate4Ray, v => v.rayFullTunConfigTemplate.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AddProxyOnly4Ray, v => v.togAddProxyProtocolOutboundOnly4Ray.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.ProxyDetour4Ray, v => v.txtProxyDetour4Ray.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.EnableFullConfigTemplate4Singbox, v => v.sbFullConfigTemplateEnable.IsChecked).DisposeWith(disposables); diff --git a/v2rayng/AndroidLibXrayLite/go.mod b/v2rayng/AndroidLibXrayLite/go.mod index c30c8d27bb..4ae625cdc4 100644 --- a/v2rayng/AndroidLibXrayLite/go.mod +++ b/v2rayng/AndroidLibXrayLite/go.mod @@ -3,7 +3,7 @@ module github.com/2dust/AndroidLibXrayLite go 1.26 require ( - github.com/xtls/xray-core v1.260327.1-0.20260415235634-c5edc122b70e + github.com/xtls/xray-core v1.260327.1-0.20260417230405-b4650360d6a0 golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b ) @@ -19,7 +19,7 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/miekg/dns v1.1.72 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pires/go-proxyproto v0.11.0 // indirect + github.com/pires/go-proxyproto v0.12.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect diff --git a/v2rayng/AndroidLibXrayLite/go.sum b/v2rayng/AndroidLibXrayLite/go.sum index daf965fd7e..c63e1906d9 100644 --- a/v2rayng/AndroidLibXrayLite/go.sum +++ b/v2rayng/AndroidLibXrayLite/go.sum @@ -40,8 +40,8 @@ github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= -github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= +github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM= +github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= @@ -62,8 +62,8 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8= github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI= -github.com/xtls/xray-core v1.260327.1-0.20260415235634-c5edc122b70e h1:Ndk6i7q2rExWGsXzp1KsUvqkY4Id+NgpGr73Sg/x4lM= -github.com/xtls/xray-core v1.260327.1-0.20260415235634-c5edc122b70e/go.mod h1:nFFTSnhpW0qTKUOeABKy3XAxdZ9WRuyhP4dEtPy1Qps= +github.com/xtls/xray-core v1.260327.1-0.20260417230405-b4650360d6a0 h1:9cf2NebR+d4bytVztd5CM3PAfJyfJT4WHucjCYrLv+g= +github.com/xtls/xray-core v1.260327.1-0.20260417230405-b4650360d6a0/go.mod h1:DowU2gGzHZ9NB9kG2ykRx8SQ09KpSyNjLrdc8XPHVm8= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= diff --git a/v2rayng/V2rayNG/app/build.gradle.kts b/v2rayng/V2rayNG/app/build.gradle.kts index 61caf566b6..9679362c01 100644 --- a/v2rayng/V2rayNG/app/build.gradle.kts +++ b/v2rayng/V2rayNG/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "com.v2ray.ang" minSdk = 24 targetSdk = 36 - versionCode = 720 - versionName = "2.1.0" + versionCode = 721 + versionName = "2.1.1" multiDexEnabled = true val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';') diff --git a/v2rayng/V2rayNG/app/src/main/AndroidManifest.xml b/v2rayng/V2rayNG/app/src/main/AndroidManifest.xml index 0382514337..9d2dd6e833 100644 --- a/v2rayng/V2rayNG/app/src/main/AndroidManifest.xml +++ b/v2rayng/V2rayNG/app/src/main/AndroidManifest.xml @@ -88,6 +88,9 @@ + diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AppPickerActivity.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AppPickerActivity.kt new file mode 100644 index 0000000000..c3234b31ff --- /dev/null +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AppPickerActivity.kt @@ -0,0 +1,179 @@ +package com.v2ray.ang.ui + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.widget.SearchView +import androidx.lifecycle.lifecycleScope +import com.v2ray.ang.R +import com.v2ray.ang.databinding.ActivityAppPickerBinding +import com.v2ray.ang.dto.AppInfo +import com.v2ray.ang.util.AppManagerUtil +import com.v2ray.ang.util.LogUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.text.Collator + +class AppPickerActivity : BaseActivity() { + companion object { + private const val EXTRA_SELECTED_PACKAGES = "selected_packages" + private const val EXTRA_PICKER_TITLE = "picker_title" + + fun createIntent( + context: Context, + selectedPackages: Collection = emptyList(), + title: String? = null + ): Intent = Intent(context, AppPickerActivity::class.java).apply { + putStringArrayListExtra(EXTRA_SELECTED_PACKAGES, ArrayList(selectedPackages)) + title?.let { putExtra(EXTRA_PICKER_TITLE, it) } + } + + fun getSelectedPackages(intent: Intent?): List { + return intent?.getStringArrayListExtra(EXTRA_SELECTED_PACKAGES).orEmpty() + } + } + + private val binding by lazy { ActivityAppPickerBinding.inflate(layoutInflater) } + private val initialSelectedPackages by lazy { + intent.getStringArrayListExtra(EXTRA_SELECTED_PACKAGES).orEmpty() + } + private val selectedPackages = LinkedHashSet() + private var appsAll: List = emptyList() + private val adapter = AppSelectorAdapter(selectedPackages) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = resolveScreenTitle()) + + selectedPackages.addAll(initialSelectedPackages) + setupRecyclerView() + loadApps() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_app_picker, menu) + + val searchItem = menu.findItem(R.id.search_view) + if (searchItem != null) { + val searchView = searchItem.actionView as SearchView + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean = false + + override fun onQueryTextChange(newText: String?): Boolean { + filterApps(newText.orEmpty()) + return false + } + }) + } + + return true + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.select_all -> { + selectAllVisible() + true + } + + R.id.invert_selection -> { + invertVisibleSelection() + true + } + + else -> super.onOptionsItemSelected(item) + } + + override fun finish() { + setResult( + RESULT_OK, + Intent().apply { + putStringArrayListExtra(EXTRA_SELECTED_PACKAGES, getSelectedPackages()) + } + ) + super.finish() + } + + private fun setupRecyclerView() { + binding.recyclerView.adapter = adapter + addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider) + } + + private fun loadApps() { + showLoading() + + lifecycleScope.launch { + try { + val apps = withContext(Dispatchers.IO) { + val appsList = AppManagerUtil.loadNetworkAppList(this@AppPickerActivity) + sortApps(appsList) + } + + appsAll = apps + updateDisplayedApps(apps) + } catch (e: Exception) { + LogUtil.e("AppPickerActivity", "Failed to load app list", e) + } finally { + hideLoading() + } + } + } + + private fun filterApps(content: String) { + val key = content.uppercase() + val filteredApps = appsAll.filter { app -> + key.isEmpty() || matchesSearch(app, key) + } + updateDisplayedApps(filteredApps) + } + + private fun sortApps(apps: List): List { + val collator = Collator.getInstance() + return apps.sortedWith { p1, p2 -> + val p1Selected = selectedPackages.contains(p1.packageName) + val p2Selected = selectedPackages.contains(p2.packageName) + when { + p1Selected && !p2Selected -> -1 + !p1Selected && p2Selected -> 1 + p1.isSystemApp && !p2.isSystemApp -> 1 + !p1.isSystemApp && p2.isSystemApp -> -1 + else -> collator.compare(p1.appName, p2.appName) + } + } + } + + private fun matchesSearch(app: AppInfo, keyword: String): Boolean { + return app.appName.uppercase().contains(keyword) || app.packageName.uppercase().contains(keyword) + } + + private fun updateDisplayedApps(apps: List) { + adapter.submitList(apps) + } + + private fun selectAllVisible() { + adapter.apps.forEach { app -> selectedPackages.add(app.packageName) } + adapter.refreshSelection() + } + + private fun invertVisibleSelection() { + adapter.apps.forEach { app -> + if (selectedPackages.contains(app.packageName)) { + selectedPackages.remove(app.packageName) + } else { + selectedPackages.add(app.packageName) + } + } + adapter.refreshSelection() + } + + private fun getSelectedPackages(): ArrayList { + return ArrayList(selectedPackages.sorted()) + } + + private fun resolveScreenTitle(): String { + return intent.getStringExtra(EXTRA_PICKER_TITLE) ?: getString(R.string.per_app_proxy_settings) + } +} + diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AppSelectorAdapter.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AppSelectorAdapter.kt new file mode 100644 index 0000000000..719664a4c2 --- /dev/null +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AppSelectorAdapter.kt @@ -0,0 +1,64 @@ +package com.v2ray.ang.ui + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding +import com.v2ray.ang.dto.AppInfo + +class AppSelectorAdapter( + private val selectedPackages: MutableSet +) : RecyclerView.Adapter() { + + var apps: List = emptyList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder { + val binding = ItemRecyclerBypassListBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return AppViewHolder(binding) + } + + override fun getItemCount(): Int = apps.size + + override fun onBindViewHolder(holder: AppViewHolder, position: Int) { + holder.bind(apps[position]) + } + + fun submitList(newApps: List) { + apps = newApps + notifyDataSetChanged() + } + + fun refreshSelection() { + notifyDataSetChanged() + } + + inner class AppViewHolder(private val binding: ItemRecyclerBypassListBinding) : RecyclerView.ViewHolder(binding.root), + View.OnClickListener { + private lateinit var appInfo: AppInfo + + fun bind(item: AppInfo) { + appInfo = item + binding.icon.setImageDrawable(item.appIcon) + binding.name.text = if (item.isSystemApp) { + String.format("** %s", item.appName) + } else { + item.appName + } + binding.packageName.text = item.packageName + binding.checkBox.isChecked = selectedPackages.contains(item.packageName) + itemView.setOnClickListener(this) + } + + override fun onClick(v: View?) { + val packageName = appInfo.packageName + if (selectedPackages.contains(packageName)) { + selectedPackages.remove(packageName) + } else { + selectedPackages.add(packageName) + } + binding.checkBox.isChecked = selectedPackages.contains(packageName) + } + } +} + diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt index f20ce50482..27a6646a39 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.widget.ArrayAdapter +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.lifecycle.lifecycleScope import com.v2ray.ang.AppConfig.BUILTIN_OUTBOUND_TAGS @@ -23,12 +24,19 @@ import kotlinx.coroutines.launch class RoutingEditActivity : BaseActivity() { private val binding by lazy { ActivityRoutingEditBinding.inflate(layoutInflater) } private val position by lazy { intent.getIntExtra("position", -1) } + private val processPickerLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + val selectedPackages = AppPickerActivity.getSelectedPackages(result.data) + binding.etProcess.text = Utils.getEditable(selectedPackages.joinToString(",")) + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.routing_settings_rule_title)) setupOutboundTagInput() + setupProcessPicker() val rulesetItem = SettingsManager.getRoutingRuleset(position) if (rulesetItem != null) { @@ -39,9 +47,31 @@ class RoutingEditActivity : BaseActivity() { SettingsManager.canUseProcessRouting().let { canUse -> binding.etProcess.isEnabled = canUse + binding.btnProcessPicker.isEnabled = canUse } } + private fun setupProcessPicker() { + binding.btnProcessPicker.setOnClickListener { + processPickerLauncher.launch( + AppPickerActivity.createIntent( + context = this, + selectedPackages = getSelectedProcessPackages(), + title = getString(R.string.routing_settings_process) + ) + ) + } + } + + private fun getSelectedProcessPackages(): List { + return binding.etProcess.text + .toString() + .split(",") + .map { it.trim() } + .filter { it.isNotEmpty() } + .distinct() + } + /** * Sets up the AutoCompleteTextView for outbound tag: * suggestions = built-in tags (proxy/direct/block) + all existing profile remarks. diff --git a/v2rayng/V2rayNG/app/src/main/res/layout/activity_app_picker.xml b/v2rayng/V2rayNG/app/src/main/res/layout/activity_app_picker.xml new file mode 100644 index 0000000000..5544419970 --- /dev/null +++ b/v2rayng/V2rayNG/app/src/main/res/layout/activity_app_picker.xml @@ -0,0 +1,10 @@ + + + + diff --git a/v2rayng/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml b/v2rayng/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml index d5ad605291..9db3ec8e1c 100644 --- a/v2rayng/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml +++ b/v2rayng/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml @@ -114,13 +114,34 @@ android:layout_height="wrap_content" android:text="@string/routing_settings_process" /> - + android:layout_marginTop="@dimen/padding_spacing_dp8" + android:layout_marginBottom="@dimen/padding_spacing_dp8"> + + + + + + + + + + + + + + + + diff --git a/v2rayng/V2rayNG/app/src/main/res/values-ru/strings.xml b/v2rayng/V2rayNG/app/src/main/res/values-ru/strings.xml index be726045e4..55cba3dfc9 100644 --- a/v2rayng/V2rayNG/app/src/main/res/values-ru/strings.xml +++ b/v2rayng/V2rayNG/app/src/main/res/values-ru/strings.xml @@ -189,7 +189,7 @@ Добавить IPv6-адрес и маршруты в VPN-интерфейс Предпочитать IPv6 - Предпочитать IPv6-адреса при разрешении доменных имён + Предпочитать IPv6-адреса при определении доменных имён Удалённая DNS (UDP/TCP/HTTPS/QUIC) (необязательно) DNS @@ -198,8 +198,10 @@ VPN обходит LAN Адрес интерфейса VPN + VPN MTU (по умолчанию 1500) + Внутренняя DNS (необязательно) DNS @@ -221,7 +223,7 @@ Порт локального прокси Порт локального прокси - Динамически менять порт + Динамически менять порт локального прокси Использовать случайный порт локального прокси при каждом создании соединения Пользователь локального прокси (необязательно) Имя пользователя @@ -245,6 +247,7 @@ Общая вкладка групп Показывать дополнительную вкладку со всеми профилями групп + Обратная связь Предложить улучшение или сообщить об ошибке на GitHub @@ -306,7 +309,7 @@ Импортировано профилей: %d Экспортировано профилей: %d Обновлено профилей: %d - Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$d) + Обновлено профилей: %1$d (успешно: %2$d, ошибки: %3$d, пропущено: %4$d) Нет подписок Выбранный профиль не найден в текущей группе Фрагмент недоступен @@ -318,7 +321,7 @@ Доменная стратегия Маршрутизация - Разделяйте запятой (,). Укажите только одно: domain, ip или process + Через запятую (,)\nТолько что-то одно: домен, IP или процесс Сохранить Очистить Настройка правил маршрутизации @@ -331,6 +334,7 @@ Постоянное (сохранится при импорте правил) Домен IP + Процесс (название пакета — поддерживается только при использовании Xray TUN, включённой функции routeOnly и ОС Android 10+) Порт Протокол [http,tls,bittorrent] @@ -353,8 +357,8 @@ Невозможно импортировать подписку Настройки фрагментирования Фрагментирование пакетов - Длина фрагмента (от - до) - Интервал фрагментов (от - до) + Длина фрагмента (от — до) + Интервал фрагментов (от — до) Использовать фрагментирование Проверить обновление @@ -429,6 +433,7 @@ Определять и добавлять к узлам DNS Определять и заменять домен + Наименьшая задержка Наименьшая нагрузка diff --git a/xray-core/app/dns/dns.go b/xray-core/app/dns/dns.go index 2f2729897b..ec350e8486 100644 --- a/xray-core/app/dns/dns.go +++ b/xray-core/app/dns/dns.go @@ -271,11 +271,11 @@ func (s *DNS) sortClients(domain string) []*Client { // Priority domain matching hasMatch := false - MatchSlice := s.domainMatcher.Match(strings.ToLower(domain)) - sort.Slice(MatchSlice, func(i, j int) bool { - return MatchSlice[i] < MatchSlice[j] + matchSlice := s.domainMatcher.Match(strings.ToLower(domain)) + sort.Slice(matchSlice, func(i, j int) bool { + return matchSlice[i] < matchSlice[j] }) - for _, match := range MatchSlice { + for _, match := range matchSlice { info := s.matcherInfos[match] client := s.clients[info.clientIdx] domainRule := info.domainRule diff --git a/xray-core/app/dns/dns_test.go b/xray-core/app/dns/dns_test.go index c065056e39..067ff44566 100644 --- a/xray-core/app/dns/dns_test.go +++ b/xray-core/app/dns/dns_test.go @@ -548,15 +548,8 @@ func TestIPMatch(t *testing.T) { Port: uint32(port), }, ExpectedIp: []*geodata.IPRule{ - { - Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - // inner ip, will not match - Ip: []byte{192, 168, 11, 1}, - Prefix: 32, - }, - }, - }, + // inner ip, will not match + {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{192, 168, 11, 1}, Prefix: 32}}}}, }, }, // second dns, match ip @@ -571,22 +564,8 @@ func TestIPMatch(t *testing.T) { Port: uint32(port), }, ExpectedIp: []*geodata.IPRule{ - { - Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: []byte{8, 8, 8, 8}, - Prefix: 32, - }, - }, - }, - { - Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: []byte{8, 8, 8, 4}, - Prefix: 32, - }, - }, - }, + {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}}}}, + {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 4}, Prefix: 32}}}}, }, }, }, @@ -676,9 +655,9 @@ func TestLocalDomain(t *testing.T) { }, ExpectedIp: []*geodata.IPRule{ // Will match localhost, localhost-a and localhost-b, - {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 2}, Prefix: 32}}}, - {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 3}, Prefix: 32}}}, - {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 4}, Prefix: 32}}}, + {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 2}, Prefix: 32}}}}, + {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 3}, Prefix: 32}}}}, + {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 4}, Prefix: 32}}}}, }, }, { @@ -901,22 +880,8 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) { }, ExpectedIp: []*geodata.IPRule{ // Will only match 8.8.8.8 and 8.8.4.4 - { - Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: []byte{8, 8, 8, 8}, - Prefix: 32, - }, - }, - }, - { - Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: []byte{8, 8, 4, 4}, - Prefix: 32, - }, - }, - }, + {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}}}}, + {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 4, 4}, Prefix: 32}}}}, }, }, { @@ -936,14 +901,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) { }, ExpectedIp: []*geodata.IPRule{ // Will match 8.8.8.8 and 8.8.8.7, etc - { - Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: []byte{8, 8, 8, 7}, - Prefix: 24, - }, - }, - }, + {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 7}, Prefix: 24}}}}, }, }, { @@ -963,14 +921,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) { }, ExpectedIp: []*geodata.IPRule{ // Will only match 8.8.7.7 (api.google.com) - { - Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: []byte{8, 8, 7, 7}, - Prefix: 32, - }, - }, - }, + {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 7, 7}, Prefix: 32}}}}, }, }, { @@ -990,14 +941,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) { }, ExpectedIp: []*geodata.IPRule{ // Will only match 8.8.7.8 (v2.api.google.com) - { - Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: []byte{8, 8, 7, 8}, - Prefix: 32, - }, - }, - }, + {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 7, 8}, Prefix: 32}}}}, }, }, }, diff --git a/xray-core/app/router/command/command_test.go b/xray-core/app/router/command/command_test.go index e6706df83d..adfe07dc61 100644 --- a/xray-core/app/router/command/command_test.go +++ b/xray-core/app/router/command/command_test.go @@ -308,7 +308,7 @@ func TestServiceTestRoute(t *testing.T) { TargetTag: &router.RoutingRule_Tag{Tag: "out"}, }, { - SourceIp: []*geodata.IPRule{{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8}}}}, + SourceIp: []*geodata.IPRule{{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8}}}}}, TargetTag: &router.RoutingRule_Tag{Tag: "out"}, }, { diff --git a/xray-core/app/router/condition_test.go b/xray-core/app/router/condition_test.go index 9e57aa914f..1b94bb8efe 100644 --- a/xray-core/app/router/condition_test.go +++ b/xray-core/app/router/condition_test.go @@ -92,25 +92,22 @@ func TestRoutingRule(t *testing.T) { Ip: []*geodata.IPRule{ { Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: []byte{8, 8, 8, 8}, - Prefix: 32, + Custom: &geodata.CIDRRule{ + Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}, }, }, }, { Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: []byte{8, 8, 8, 8}, - Prefix: 32, + Custom: &geodata.CIDRRule{ + Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}, }, }, }, { Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(), - Prefix: 128, + Custom: &geodata.CIDRRule{ + Cidr: &geodata.CIDR{Ip: net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(), Prefix: 128}, }, }, }, @@ -140,9 +137,8 @@ func TestRoutingRule(t *testing.T) { SourceIp: []*geodata.IPRule{ { Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: []byte{192, 168, 0, 0}, - Prefix: 16, + Custom: &geodata.CIDRRule{ + Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16}, }, }, }, diff --git a/xray-core/app/router/router_test.go b/xray-core/app/router/router_test.go index f038937fa1..323e9a6299 100644 --- a/xray-core/app/router/router_test.go +++ b/xray-core/app/router/router_test.go @@ -159,9 +159,8 @@ func TestIPOnDemand(t *testing.T) { Ip: []*geodata.IPRule{ { Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: []byte{192, 168, 0, 0}, - Prefix: 16, + Custom: &geodata.CIDRRule{ + Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16}, }, }, }, @@ -204,9 +203,8 @@ func TestIPIfNonMatchDomain(t *testing.T) { Ip: []*geodata.IPRule{ { Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: []byte{192, 168, 0, 0}, - Prefix: 16, + Custom: &geodata.CIDRRule{ + Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16}, }, }, }, @@ -249,9 +247,8 @@ func TestIPIfNonMatchIP(t *testing.T) { Ip: []*geodata.IPRule{ { Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: []byte{127, 0, 0, 0}, - Prefix: 8, + Custom: &geodata.CIDRRule{ + Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8}, }, }, }, diff --git a/xray-core/common/geodata/domain_matcher.go b/xray-core/common/geodata/domain_matcher.go index a349061051..e5e854f9eb 100644 --- a/xray-core/common/geodata/domain_matcher.go +++ b/xray-core/common/geodata/domain_matcher.go @@ -11,7 +11,11 @@ import ( ) type DomainMatcher interface { + // Match returns the indices of all rules that match the input domain. + // The returned slice is owned by the caller and may be safely modified. + // Note: the slice may contain duplicates and the order is unspecified. Match(input string) []uint32 + MatchAny(input string) bool } diff --git a/xray-core/common/geodata/domain_matcher_test.go b/xray-core/common/geodata/domain_matcher_test.go index 775b33719c..0506df094b 100644 --- a/xray-core/common/geodata/domain_matcher_test.go +++ b/xray-core/common/geodata/domain_matcher_test.go @@ -48,3 +48,25 @@ func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) { t.Fatalf("Match() = %v, want %v", got, want) } } + +func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) { + matcher, err := (&MphDomainMatcherFactory{}).BuildMatcher([]*DomainRule{ + {Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}}, + {Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}}, + }) + if err != nil { + t.Fatalf("BuildMatcher() failed: %v", err) + } + + got := matcher.Match("example.com") + if !reflect.DeepEqual(got, []uint32{0, 1}) { + t.Fatalf("Match() = %v, want %v", got, []uint32{0, 1}) + } + + got[0] = 1 + + gotAgain := matcher.Match("example.com") + if !reflect.DeepEqual(gotAgain, []uint32{0, 1}) { + t.Fatalf("Match() after caller mutation = %v, want %v", gotAgain, []uint32{0, 1}) + } +} diff --git a/xray-core/common/geodata/geodat.pb.go b/xray-core/common/geodata/geodat.pb.go index b5cc279738..369839c523 100644 --- a/xray-core/common/geodata/geodat.pb.go +++ b/xray-core/common/geodata/geodat.pb.go @@ -433,6 +433,58 @@ func (x *CIDR) GetPrefix() uint32 { return 0 } +type CIDRRule struct { + state protoimpl.MessageState `protogen:"open.v1"` + Cidr *CIDR `protobuf:"bytes,1,opt,name=cidr,proto3" json:"cidr,omitempty"` + ReverseMatch bool `protobuf:"varint,2,opt,name=reverse_match,json=reverseMatch,proto3" json:"reverse_match,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CIDRRule) Reset() { + *x = CIDRRule{} + mi := &file_common_geodata_geodat_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CIDRRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CIDRRule) ProtoMessage() {} + +func (x *CIDRRule) ProtoReflect() protoreflect.Message { + mi := &file_common_geodata_geodat_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CIDRRule.ProtoReflect.Descriptor instead. +func (*CIDRRule) Descriptor() ([]byte, []int) { + return file_common_geodata_geodat_proto_rawDescGZIP(), []int{6} +} + +func (x *CIDRRule) GetCidr() *CIDR { + if x != nil { + return x.Cidr + } + return nil +} + +func (x *CIDRRule) GetReverseMatch() bool { + if x != nil { + return x.ReverseMatch + } + return false +} + type GeoIP struct { state protoimpl.MessageState `protogen:"open.v1"` Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"` @@ -444,7 +496,7 @@ type GeoIP struct { func (x *GeoIP) Reset() { *x = GeoIP{} - mi := &file_common_geodata_geodat_proto_msgTypes[6] + mi := &file_common_geodata_geodat_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -456,7 +508,7 @@ func (x *GeoIP) String() string { func (*GeoIP) ProtoMessage() {} func (x *GeoIP) ProtoReflect() protoreflect.Message { - mi := &file_common_geodata_geodat_proto_msgTypes[6] + mi := &file_common_geodata_geodat_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -469,7 +521,7 @@ func (x *GeoIP) ProtoReflect() protoreflect.Message { // Deprecated: Use GeoIP.ProtoReflect.Descriptor instead. func (*GeoIP) Descriptor() ([]byte, []int) { - return file_common_geodata_geodat_proto_rawDescGZIP(), []int{6} + return file_common_geodata_geodat_proto_rawDescGZIP(), []int{7} } func (x *GeoIP) GetCode() string { @@ -502,7 +554,7 @@ type GeoIPList struct { func (x *GeoIPList) Reset() { *x = GeoIPList{} - mi := &file_common_geodata_geodat_proto_msgTypes[7] + mi := &file_common_geodata_geodat_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -514,7 +566,7 @@ func (x *GeoIPList) String() string { func (*GeoIPList) ProtoMessage() {} func (x *GeoIPList) ProtoReflect() protoreflect.Message { - mi := &file_common_geodata_geodat_proto_msgTypes[7] + mi := &file_common_geodata_geodat_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -527,7 +579,7 @@ func (x *GeoIPList) ProtoReflect() protoreflect.Message { // Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead. func (*GeoIPList) Descriptor() ([]byte, []int) { - return file_common_geodata_geodat_proto_rawDescGZIP(), []int{7} + return file_common_geodata_geodat_proto_rawDescGZIP(), []int{8} } func (x *GeoIPList) GetEntry() []*GeoIP { @@ -548,7 +600,7 @@ type GeoIPRule struct { func (x *GeoIPRule) Reset() { *x = GeoIPRule{} - mi := &file_common_geodata_geodat_proto_msgTypes[8] + mi := &file_common_geodata_geodat_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -560,7 +612,7 @@ func (x *GeoIPRule) String() string { func (*GeoIPRule) ProtoMessage() {} func (x *GeoIPRule) ProtoReflect() protoreflect.Message { - mi := &file_common_geodata_geodat_proto_msgTypes[8] + mi := &file_common_geodata_geodat_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -573,7 +625,7 @@ func (x *GeoIPRule) ProtoReflect() protoreflect.Message { // Deprecated: Use GeoIPRule.ProtoReflect.Descriptor instead. func (*GeoIPRule) Descriptor() ([]byte, []int) { - return file_common_geodata_geodat_proto_rawDescGZIP(), []int{8} + return file_common_geodata_geodat_proto_rawDescGZIP(), []int{9} } func (x *GeoIPRule) GetFile() string { @@ -610,7 +662,7 @@ type IPRule struct { func (x *IPRule) Reset() { *x = IPRule{} - mi := &file_common_geodata_geodat_proto_msgTypes[9] + mi := &file_common_geodata_geodat_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -622,7 +674,7 @@ func (x *IPRule) String() string { func (*IPRule) ProtoMessage() {} func (x *IPRule) ProtoReflect() protoreflect.Message { - mi := &file_common_geodata_geodat_proto_msgTypes[9] + mi := &file_common_geodata_geodat_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -635,7 +687,7 @@ func (x *IPRule) ProtoReflect() protoreflect.Message { // Deprecated: Use IPRule.ProtoReflect.Descriptor instead. func (*IPRule) Descriptor() ([]byte, []int) { - return file_common_geodata_geodat_proto_rawDescGZIP(), []int{9} + return file_common_geodata_geodat_proto_rawDescGZIP(), []int{10} } func (x *IPRule) GetValue() isIPRule_Value { @@ -654,7 +706,7 @@ func (x *IPRule) GetGeoip() *GeoIPRule { return nil } -func (x *IPRule) GetCustom() *CIDR { +func (x *IPRule) GetCustom() *CIDRRule { if x != nil { if x, ok := x.Value.(*IPRule_Custom); ok { return x.Custom @@ -672,7 +724,7 @@ type IPRule_Geoip struct { } type IPRule_Custom struct { - Custom *CIDR `protobuf:"bytes,2,opt,name=custom,proto3,oneof"` + Custom *CIDRRule `protobuf:"bytes,2,opt,name=custom,proto3,oneof"` } func (*IPRule_Geoip) isIPRule_Value() {} @@ -693,7 +745,7 @@ type Domain_Attribute struct { func (x *Domain_Attribute) Reset() { *x = Domain_Attribute{} - mi := &file_common_geodata_geodat_proto_msgTypes[10] + mi := &file_common_geodata_geodat_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -705,7 +757,7 @@ func (x *Domain_Attribute) String() string { func (*Domain_Attribute) ProtoMessage() {} func (x *Domain_Attribute) ProtoReflect() protoreflect.Message { - mi := &file_common_geodata_geodat_proto_msgTypes[10] + mi := &file_common_geodata_geodat_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -807,7 +859,10 @@ const file_common_geodata_geodat_proto_rawDesc = "" + "\x05value\".\n" + "\x04CIDR\x12\x0e\n" + "\x02ip\x18\x01 \x01(\fR\x02ip\x12\x16\n" + - "\x06prefix\x18\x02 \x01(\rR\x06prefix\"o\n" + + "\x06prefix\x18\x02 \x01(\rR\x06prefix\"^\n" + + "\bCIDRRule\x12-\n" + + "\x04cidr\x18\x01 \x01(\v2\x19.xray.common.geodata.CIDRR\x04cidr\x12#\n" + + "\rreverse_match\x18\x02 \x01(\bR\freverseMatch\"o\n" + "\x05GeoIP\x12\x12\n" + "\x04code\x18\x01 \x01(\tR\x04code\x12-\n" + "\x04cidr\x18\x02 \x03(\v2\x19.xray.common.geodata.CIDRR\x04cidr\x12#\n" + @@ -817,10 +872,10 @@ const file_common_geodata_geodat_proto_rawDesc = "" + "\tGeoIPRule\x12\x12\n" + "\x04file\x18\x01 \x01(\tR\x04file\x12\x12\n" + "\x04code\x18\x02 \x01(\tR\x04code\x12#\n" + - "\rreverse_match\x18\x03 \x01(\bR\freverseMatch\"~\n" + + "\rreverse_match\x18\x03 \x01(\bR\freverseMatch\"\x82\x01\n" + "\x06IPRule\x126\n" + - "\x05geoip\x18\x01 \x01(\v2\x1e.xray.common.geodata.GeoIPRuleH\x00R\x05geoip\x123\n" + - "\x06custom\x18\x02 \x01(\v2\x19.xray.common.geodata.CIDRH\x00R\x06customB\a\n" + + "\x05geoip\x18\x01 \x01(\v2\x1e.xray.common.geodata.GeoIPRuleH\x00R\x05geoip\x127\n" + + "\x06custom\x18\x02 \x01(\v2\x1d.xray.common.geodata.CIDRRuleH\x00R\x06customB\a\n" + "\x05valueB[\n" + "\x17com.xray.common.geodataP\x01Z(github.com/xtls/xray-core/common/geodata\xaa\x02\x13Xray.Common.Geodatab\x06proto3" @@ -837,7 +892,7 @@ func file_common_geodata_geodat_proto_rawDescGZIP() []byte { } var file_common_geodata_geodat_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_common_geodata_geodat_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_common_geodata_geodat_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_common_geodata_geodat_proto_goTypes = []any{ (Domain_Type)(0), // 0: xray.common.geodata.Domain.Type (*Domain)(nil), // 1: xray.common.geodata.Domain @@ -846,28 +901,30 @@ var file_common_geodata_geodat_proto_goTypes = []any{ (*GeoSiteRule)(nil), // 4: xray.common.geodata.GeoSiteRule (*DomainRule)(nil), // 5: xray.common.geodata.DomainRule (*CIDR)(nil), // 6: xray.common.geodata.CIDR - (*GeoIP)(nil), // 7: xray.common.geodata.GeoIP - (*GeoIPList)(nil), // 8: xray.common.geodata.GeoIPList - (*GeoIPRule)(nil), // 9: xray.common.geodata.GeoIPRule - (*IPRule)(nil), // 10: xray.common.geodata.IPRule - (*Domain_Attribute)(nil), // 11: xray.common.geodata.Domain.Attribute + (*CIDRRule)(nil), // 7: xray.common.geodata.CIDRRule + (*GeoIP)(nil), // 8: xray.common.geodata.GeoIP + (*GeoIPList)(nil), // 9: xray.common.geodata.GeoIPList + (*GeoIPRule)(nil), // 10: xray.common.geodata.GeoIPRule + (*IPRule)(nil), // 11: xray.common.geodata.IPRule + (*Domain_Attribute)(nil), // 12: xray.common.geodata.Domain.Attribute } var file_common_geodata_geodat_proto_depIdxs = []int32{ 0, // 0: xray.common.geodata.Domain.type:type_name -> xray.common.geodata.Domain.Type - 11, // 1: xray.common.geodata.Domain.attribute:type_name -> xray.common.geodata.Domain.Attribute + 12, // 1: xray.common.geodata.Domain.attribute:type_name -> xray.common.geodata.Domain.Attribute 1, // 2: xray.common.geodata.GeoSite.domain:type_name -> xray.common.geodata.Domain 2, // 3: xray.common.geodata.GeoSiteList.entry:type_name -> xray.common.geodata.GeoSite 4, // 4: xray.common.geodata.DomainRule.geosite:type_name -> xray.common.geodata.GeoSiteRule 1, // 5: xray.common.geodata.DomainRule.custom:type_name -> xray.common.geodata.Domain - 6, // 6: xray.common.geodata.GeoIP.cidr:type_name -> xray.common.geodata.CIDR - 7, // 7: xray.common.geodata.GeoIPList.entry:type_name -> xray.common.geodata.GeoIP - 9, // 8: xray.common.geodata.IPRule.geoip:type_name -> xray.common.geodata.GeoIPRule - 6, // 9: xray.common.geodata.IPRule.custom:type_name -> xray.common.geodata.CIDR - 10, // [10:10] is the sub-list for method output_type - 10, // [10:10] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 6, // 6: xray.common.geodata.CIDRRule.cidr:type_name -> xray.common.geodata.CIDR + 6, // 7: xray.common.geodata.GeoIP.cidr:type_name -> xray.common.geodata.CIDR + 8, // 8: xray.common.geodata.GeoIPList.entry:type_name -> xray.common.geodata.GeoIP + 10, // 9: xray.common.geodata.IPRule.geoip:type_name -> xray.common.geodata.GeoIPRule + 7, // 10: xray.common.geodata.IPRule.custom:type_name -> xray.common.geodata.CIDRRule + 11, // [11:11] is the sub-list for method output_type + 11, // [11:11] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name } func init() { file_common_geodata_geodat_proto_init() } @@ -879,11 +936,11 @@ func file_common_geodata_geodat_proto_init() { (*DomainRule_Geosite)(nil), (*DomainRule_Custom)(nil), } - file_common_geodata_geodat_proto_msgTypes[9].OneofWrappers = []any{ + file_common_geodata_geodat_proto_msgTypes[10].OneofWrappers = []any{ (*IPRule_Geoip)(nil), (*IPRule_Custom)(nil), } - file_common_geodata_geodat_proto_msgTypes[10].OneofWrappers = []any{ + file_common_geodata_geodat_proto_msgTypes[11].OneofWrappers = []any{ (*Domain_Attribute_BoolValue)(nil), (*Domain_Attribute_IntValue)(nil), } @@ -893,7 +950,7 @@ func file_common_geodata_geodat_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_geodata_geodat_proto_rawDesc), len(file_common_geodata_geodat_proto_rawDesc)), NumEnums: 1, - NumMessages: 11, + NumMessages: 12, NumExtensions: 0, NumServices: 0, }, diff --git a/xray-core/common/geodata/geodat.proto b/xray-core/common/geodata/geodat.proto index 1828b91761..be91853e4e 100644 --- a/xray-core/common/geodata/geodat.proto +++ b/xray-core/common/geodata/geodat.proto @@ -66,6 +66,11 @@ message CIDR { uint32 prefix = 2; } +message CIDRRule { + CIDR cidr = 1; + bool reverse_match = 2; +} + message GeoIP { string code = 1; repeated CIDR cidr = 2; @@ -85,6 +90,6 @@ message GeoIPRule { message IPRule { oneof value { GeoIPRule geoip = 1; - CIDR custom = 2; + CIDRRule custom = 2; } } diff --git a/xray-core/common/geodata/ip_matcher.go b/xray-core/common/geodata/ip_matcher.go index 565b38eab0..315ce040cf 100644 --- a/xray-core/common/geodata/ip_matcher.go +++ b/xray-core/common/geodata/ip_matcher.go @@ -940,45 +940,58 @@ func (f *IPSetFactory) createFrom(yield func(func(*CIDR)) error) (*IPSet, error) func buildOptimizedIPMatcher(f *IPSetFactory, rules []*IPRule) (IPMatcher, error) { n := len(rules) - custom := make([]*CIDR, 0, n) - pos := make([]*GeoIPRule, 0, n) - neg := make([]*GeoIPRule, 0, n) + posCustom := make([]*CIDR, 0, n) + negCustom := make([]*CIDR, 0, n) + posGeoip := make([]*GeoIPRule, 0, n) + negGeoip := make([]*GeoIPRule, 0, n) for _, r := range rules { switch v := r.Value.(type) { case *IPRule_Custom: - custom = append(custom, v.Custom) + if !v.Custom.ReverseMatch { + posCustom = append(posCustom, v.Custom.Cidr) + } else { + negCustom = append(negCustom, v.Custom.Cidr) + } case *IPRule_Geoip: if !v.Geoip.ReverseMatch { - pos = append(pos, v.Geoip) + posGeoip = append(posGeoip, v.Geoip) } else { - neg = append(neg, v.Geoip) + negGeoip = append(negGeoip, v.Geoip) } default: panic("unknown ip rule type") } } - subs := make([]*HeuristicIPMatcher, 0, 3) + subs := make([]*HeuristicIPMatcher, 0, 4) - if len(custom) > 0 { - ipset, err := f.CreateFromCIDRs(custom) + if len(posCustom) > 0 { + ipset, err := f.CreateFromCIDRs(posCustom) if err != nil { return nil, err } subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false}) } - if len(pos) > 0 { - ipset, err := f.GetOrCreateFromGeoIPRules(pos) + if len(negCustom) > 0 { + ipset, err := f.CreateFromCIDRs(negCustom) + if err != nil { + return nil, err + } + subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: true}) + } + + if len(posGeoip) > 0 { + ipset, err := f.GetOrCreateFromGeoIPRules(posGeoip) if err != nil { return nil, err } subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false}) } - if len(neg) > 0 { - ipset, err := f.GetOrCreateFromGeoIPRules(neg) + if len(negGeoip) > 0 { + ipset, err := f.GetOrCreateFromGeoIPRules(negGeoip) if err != nil { return nil, err } diff --git a/xray-core/common/geodata/ip_matcher_test.go b/xray-core/common/geodata/ip_matcher_test.go index ac627506e2..d1dc2de228 100644 --- a/xray-core/common/geodata/ip_matcher_test.go +++ b/xray-core/common/geodata/ip_matcher_test.go @@ -189,6 +189,34 @@ func TestIPReverseMatcher2(t *testing.T) { } } +func TestIPCustomReverseMatcher(t *testing.T) { + matcher := buildIPMatcher("!8.8.8.8/32") + + testCases := []struct { + Input string + Output bool + }{ + { + Input: "8.8.8.8", + Output: false, + }, + { + Input: "1.1.1.1", + Output: true, + }, + { + Input: "2001:cdba::3257:9652", + Output: false, + }, + } + + for _, test := range testCases { + if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output { + t.Error("unexpected output: ", v, " for test case ", test) + } + } +} + func TestIPMatcherAnyMatchAndMatches(t *testing.T) { matcher := buildIPMatcher( "8.8.8.8/32", diff --git a/xray-core/common/geodata/rule_parser.go b/xray-core/common/geodata/rule_parser.go index 1184f553d9..c05fed31e6 100644 --- a/xray-core/common/geodata/rule_parser.go +++ b/xray-core/common/geodata/rule_parser.go @@ -17,6 +17,8 @@ func ParseIPRules(rules []string) ([]*IPRule, error) { var ipRules []*IPRule for i, r := range rules { + r, reverse := cutReversePrefix(r) + if strings.HasPrefix(r, "geoip:") { r = "ext:" + DefaultGeoIPDat + ":" + r[len("geoip:"):] } @@ -32,9 +34,9 @@ func ParseIPRules(rules []string) ([]*IPRule, error) { var rule isIPRule_Value var err error if prefix > 0 { - rule, err = parseGeoIPRule(r[prefix:]) + rule, err = parseGeoIPRule(r[prefix:], reverse) } else { - rule, err = parseCustomIPRule(r) + rule, err = parseCustomIPRule(r, reverse) } if err != nil { return nil, errors.New("illegal ip rule: ", rules[i]).Base(err) @@ -45,7 +47,16 @@ func ParseIPRules(rules []string) ([]*IPRule, error) { return ipRules, nil } -func parseGeoIPRule(rule string) (*IPRule_Geoip, error) { +func cutReversePrefix(s string) (string, bool) { + reverse := false + for strings.HasPrefix(s, "!") { + s = s[1:] + reverse = !reverse + } + return s, reverse +} + +func parseGeoIPRule(rule string, reverse bool) (*IPRule_Geoip, error) { file, code, ok := strings.Cut(rule, ":") if !ok { return nil, errors.New("syntax error") @@ -55,11 +66,8 @@ func parseGeoIPRule(rule string) (*IPRule_Geoip, error) { return nil, errors.New("empty file") } - reverse := false - if strings.HasPrefix(code, "!") { - code = code[1:] - reverse = true - } + code, codeReverse := cutReversePrefix(code) + reverse = reverse != codeReverse if code == "" { return nil, errors.New("empty code") } @@ -78,13 +86,16 @@ func parseGeoIPRule(rule string) (*IPRule_Geoip, error) { }, nil } -func parseCustomIPRule(rule string) (*IPRule_Custom, error) { +func parseCustomIPRule(rule string, reverse bool) (*IPRule_Custom, error) { cidr, err := parseCIDR(rule) if err != nil { return nil, err } return &IPRule_Custom{ - Custom: cidr, + Custom: &CIDRRule{ + Cidr: cidr, + ReverseMatch: reverse, + }, }, nil } diff --git a/xray-core/common/geodata/rule_parser_test.go b/xray-core/common/geodata/rule_parser_test.go index 87dbaaab92..7aaf44c5cf 100644 --- a/xray-core/common/geodata/rule_parser_test.go +++ b/xray-core/common/geodata/rule_parser_test.go @@ -13,12 +13,20 @@ func TestParseIPRules(t *testing.T) { rules := []string{ "geoip:us", "geoip:cn", + "!geoip:cn", + "!!geoip:cn", "geoip:!cn", + "geoip:!!cn", + "!geoip:!cn", "ext:geoip.dat:!cn", + "ext:geoip.dat:!!cn", "ext:geoip.dat:ca", "ext-ip:geoip.dat:!cn", "ext-ip:geoip.dat:!ca", "192.168.0.0/24", + "!192.168.0.0/24", + "!!192.168.0.0/24", + "!!!192.168.0.0/24", "192.168.0.1", "fe80::/64", "fe80::", @@ -30,6 +38,53 @@ func TestParseIPRules(t *testing.T) { } } +func TestParseIPRuleReverse(t *testing.T) { + t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) + + for _, tt := range []struct { + rule string + reverse bool + }{ + {rule: "!192.168.0.0/24", reverse: true}, + {rule: "!!192.168.0.0/24", reverse: false}, + {rule: "!!!192.168.0.0/24", reverse: true}, + {rule: "!!!!192.168.0.0/24", reverse: false}, + {rule: "geoip:cn", reverse: false}, + {rule: "!geoip:cn", reverse: true}, + {rule: "!!geoip:cn", reverse: false}, + {rule: "geoip:!cn", reverse: true}, + {rule: "geoip:!!cn", reverse: false}, + {rule: "!geoip:!cn", reverse: false}, + {rule: "!!geoip:!cn", reverse: true}, + {rule: "!geoip:!!cn", reverse: true}, + {rule: "ext:geoip.dat:!!!cn", reverse: true}, + } { + t.Run(tt.rule, func(t *testing.T) { + rules, err := geodata.ParseIPRules([]string{tt.rule}) + if err != nil { + t.Fatalf("Failed to parse ip rules, got %s", err) + } + + if len(rules) != 1 { + t.Fatalf("Expected 1 rule, got %d", len(rules)) + } + + switch rule := rules[0]; { + case rule.GetGeoip() != nil: + if rule.GetGeoip().GetReverseMatch() != tt.reverse { + t.Fatalf("Expected geoip reverse match to be %t", tt.reverse) + } + case rule.GetCustom() != nil: + if rule.GetCustom().GetReverseMatch() != tt.reverse { + t.Fatalf("Expected custom reverse match to be %t", tt.reverse) + } + default: + t.Fatal("Expected ip rule") + } + }) + } +} + func TestParseDomainRules(t *testing.T) { t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) diff --git a/xray-core/common/geodata/strmatcher/matchers.go b/xray-core/common/geodata/strmatcher/matchers.go index 7e073764c8..fa28880461 100644 --- a/xray-core/common/geodata/strmatcher/matchers.go +++ b/xray-core/common/geodata/strmatcher/matchers.go @@ -3,6 +3,7 @@ package strmatcher import ( "errors" "regexp" + "slices" "strings" "unicode/utf8" @@ -253,13 +254,12 @@ func AddMatcherToGroup(g MatcherGroup, matcher Matcher, value uint32) error { } // CompositeMatches flattens the matches slice to produce a single matched indices slice. -// It is designed to avoid new memory allocation as possible. func CompositeMatches(matches [][]uint32) []uint32 { switch len(matches) { case 0: return nil case 1: - return matches[0] + return slices.Clone(matches[0]) default: result := make([]uint32, 0, 5) for i := 0; i < len(matches); i++ { diff --git a/xray-core/common/geodata/strmatcher/strmatcher.go b/xray-core/common/geodata/strmatcher/strmatcher.go index e4187f6330..2cc6f252fc 100644 --- a/xray-core/common/geodata/strmatcher/strmatcher.go +++ b/xray-core/common/geodata/strmatcher/strmatcher.go @@ -62,6 +62,7 @@ type IndexMatcher interface { // Match returns the indices of all matchers that matches the input. // * Empty array is returned if no such matcher exists. // * The order of returned matchers should follow priority specification. + // * The returned slice is owned by the caller and may be safely modified. // Priority specification: // 1. Priority between matcher types: full > domain > substr > regex. // 2. Priority of same-priority matchers matching at same position: the early added takes precedence. @@ -89,6 +90,7 @@ type ValueMatcher interface { // * Empty array is returned if no such matcher exists. // * The order of returned values should follow priority specification. // * Same value may appear multiple times if multiple matched matchers were added with that value. + // * The returned slice is owned by the caller and may be safely modified. // Priority specification: // 1. Priority between matcher types: full > domain > substr > regex. // 2. Priority of same-priority matchers matching at same position: the early added takes precedence. diff --git a/xray-core/core/core.go b/xray-core/core/core.go index ccd1437d7d..2ba9b9cb96 100644 --- a/xray-core/core/core.go +++ b/xray-core/core/core.go @@ -20,7 +20,7 @@ import ( var ( Version_x byte = 26 Version_y byte = 4 - Version_z byte = 15 + Version_z byte = 17 ) var ( diff --git a/xray-core/go.mod b/xray-core/go.mod index 7ea49526f2..b079fba3c1 100644 --- a/xray-core/go.mod +++ b/xray-core/go.mod @@ -12,7 +12,7 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 github.com/miekg/dns v1.1.72 github.com/pelletier/go-toml v1.9.5 - github.com/pires/go-proxyproto v0.11.0 + github.com/pires/go-proxyproto v0.12.0 github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-shadowsocks v0.2.7 diff --git a/xray-core/go.sum b/xray-core/go.sum index 4ae1f959a6..f448783c82 100644 --- a/xray-core/go.sum +++ b/xray-core/go.sum @@ -45,8 +45,8 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= -github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= -github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= +github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM= +github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= diff --git a/xray-core/infra/conf/router_test.go b/xray-core/infra/conf/router_test.go index 26ebff1c96..130cf4f785 100644 --- a/xray-core/infra/conf/router_test.go +++ b/xray-core/infra/conf/router_test.go @@ -135,17 +135,15 @@ func TestRouterConfig(t *testing.T) { Ip: []*geodata.IPRule{ { Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: []byte{10, 0, 0, 0}, - Prefix: 8, + Custom: &geodata.CIDRRule{ + Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8}, }, }, }, { Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, - Prefix: 128, + Custom: &geodata.CIDRRule{ + Cidr: &geodata.CIDR{Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Prefix: 128}, }, }, }, @@ -216,17 +214,15 @@ func TestRouterConfig(t *testing.T) { Ip: []*geodata.IPRule{ { Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: []byte{10, 0, 0, 0}, - Prefix: 8, + Custom: &geodata.CIDRRule{ + Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8}, }, }, }, { Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, - Prefix: 128, + Custom: &geodata.CIDRRule{ + Cidr: &geodata.CIDR{Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Prefix: 128}, }, }, }, diff --git a/xray-core/infra/conf/xray_test.go b/xray-core/infra/conf/xray_test.go index d415f937a1..d4ff7126ae 100644 --- a/xray-core/infra/conf/xray_test.go +++ b/xray-core/infra/conf/xray_test.go @@ -99,9 +99,8 @@ func TestXrayConfig(t *testing.T) { Ip: []*geodata.IPRule{ { Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: []byte{10, 0, 0, 0}, - Prefix: 8, + Custom: &geodata.CIDRRule{ + Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8}, }, }, }, @@ -216,8 +215,12 @@ func TestSniffingConfig_Build(t *testing.T) { if rule == nil { t.Fatalf("SniffingConfig.Build() produced a non-custom ip rule at index %d", i) } - if !reflect.DeepEqual(rule.Ip, tc.ip) || rule.Prefix != tc.prefix { - t.Fatalf("SniffingConfig.Build() produced wrong ip rule at index %d: got (%v, %d), want (%v, %d)", i, rule.Ip, rule.Prefix, tc.ip, tc.prefix) + cidr := rule.GetCidr() + if cidr == nil { + t.Fatalf("SniffingConfig.Build() produced a custom ip rule without cidr at index %d", i) + } + if !reflect.DeepEqual(cidr.Ip, tc.ip) || cidr.Prefix != tc.prefix { + t.Fatalf("SniffingConfig.Build() produced wrong ip rule at index %d: got (%v, %d), want (%v, %d)", i, cidr.Ip, cidr.Prefix, tc.ip, tc.prefix) } } } diff --git a/xray-core/proxy/freedom/freedom.go b/xray-core/proxy/freedom/freedom.go index 008f700fe7..bc05d04a40 100644 --- a/xray-core/proxy/freedom/freedom.go +++ b/xray-core/proxy/freedom/freedom.go @@ -290,7 +290,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte if destination.Network == net.Network_TCP { reader = buf.NewReader(conn) } else { - reader = NewPacketReader(conn, UDPOverride, destination) + reader = NewPacketReader(conn, UDPOverride, destination, blockedIPMatcher) } if err := buf.Copy(reader, output, buf.UpdateActivity(timer)); err != nil { return errors.New("failed to process response").Base(err) @@ -309,7 +309,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte return nil } -func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.Destination) buf.Reader { +func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.Destination, blockedIPMatcher geodata.IPMatcher) buf.Reader { iConn := conn statConn, ok := iConn.(*stat.CounterConnection) if ok { @@ -328,6 +328,7 @@ func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.De return &PacketReader{ PacketConnWrapper: c, Counter: counter, + BlockedIPMatcher: blockedIPMatcher, IsOverridden: isOverridden, InitUnchangedAddr: DialDest.Address, InitChangedAddr: net.DestinationFromAddr(conn.RemoteAddr()).Address, @@ -339,6 +340,7 @@ func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.De type PacketReader struct { *internet.PacketConnWrapper stats.Counter + BlockedIPMatcher geodata.IPMatcher IsOverridden bool InitUnchangedAddr net.Address InitChangedAddr net.Address @@ -346,30 +348,38 @@ type PacketReader struct { func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) { b := buf.New() - b.Resize(0, buf.Size) - n, d, err := r.PacketConnWrapper.ReadFrom(b.Bytes()) - if err != nil { - b.Release() - return nil, err - } - b.Resize(0, int32(n)) - // if udp dest addr is changed, we are unable to get the correct src addr - // so we don't attach src info to udp packet, break cone behavior, assuming the dial dest is the expected scr addr - if !r.IsOverridden { - address := net.IPAddress(d.(*net.UDPAddr).IP) - if r.InitChangedAddr == address { - address = r.InitUnchangedAddr + for { + b.Resize(0, buf.Size) + n, d, err := r.PacketConnWrapper.ReadFrom(b.Bytes()) + if err != nil { + b.Release() + return nil, err } - b.UDP = &net.Destination{ - Address: address, - Port: net.Port(d.(*net.UDPAddr).Port), - Network: net.Network_UDP, + b.Resize(0, int32(n)) + + udpAddr := d.(*net.UDPAddr) + sourceAddr := net.IPAddress(udpAddr.IP) + if isBlockedAddress(r.BlockedIPMatcher, sourceAddr) { + continue } + + // if udp dest addr is changed, we are unable to get the correct src addr + // so we don't attach src info to udp packet, break cone behavior, assuming the dial dest is the expected scr addr + if !r.IsOverridden { + if r.InitChangedAddr == sourceAddr { + sourceAddr = r.InitUnchangedAddr + } + b.UDP = &net.Destination{ + Address: sourceAddr, + Port: net.Port(udpAddr.Port), + Network: net.Network_UDP, + } + } + if r.Counter != nil { + r.Counter.Add(int64(n)) + } + return buf.MultiBuffer{b}, nil } - if r.Counter != nil { - r.Counter.Add(int64(n)) - } - return buf.MultiBuffer{b}, nil } // DialDest means the dial target used in the dialer when creating conn @@ -468,10 +478,8 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { } } if isBlockedAddress(w.BlockedIPMatcher, b.UDP.Address) { - blockedAddr := b.UDP.Address b.Release() - buf.ReleaseMulti(mb) - return errors.New("blocked target IP: ", blockedAddr).AtDebug() + continue } destAddr := b.UDP.RawNetAddr() if destAddr == nil { diff --git a/xray-core/proxy/loopback/loopback.go b/xray-core/proxy/loopback/loopback.go index ab3360ae49..2f54fe7476 100644 --- a/xray-core/proxy/loopback/loopback.go +++ b/xray-core/proxy/loopback/loopback.go @@ -45,9 +45,11 @@ func (l *Loopback) Process(ctx context.Context, link *transport.Link, _ internet ctx = session.ContextWithContent(ctx, content) - inbound := session.InboundFromContext(ctx) - if inbound == nil { - inbound = &session.Inbound{} + inbound := &session.Inbound{} + originInbound := session.InboundFromContext(ctx) + if originInbound != nil { + // get a shallow copy to avoid modifying the inbound tag in upstream context + *inbound = *originInbound } inbound.Tag = l.config.InboundTag diff --git a/xray-core/testing/scenarios/dns_test.go b/xray-core/testing/scenarios/dns_test.go index 4f7fad5d94..9be19a7d8c 100644 --- a/xray-core/testing/scenarios/dns_test.go +++ b/xray-core/testing/scenarios/dns_test.go @@ -47,9 +47,8 @@ func TestResolveIP(t *testing.T) { Ip: []*geodata.IPRule{ { Value: &geodata.IPRule_Custom{ - Custom: &geodata.CIDR{ - Ip: []byte{127, 0, 0, 0}, - Prefix: 8, + Custom: &geodata.CIDRRule{ + Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8}, }, }, }, diff --git a/xray-core/transport/internet/finalmask/header/custom/evaluator.go b/xray-core/transport/internet/finalmask/header/custom/evaluator.go index 4665570060..728be54384 100644 --- a/xray-core/transport/internet/finalmask/header/custom/evaluator.go +++ b/xray-core/transport/internet/finalmask/header/custom/evaluator.go @@ -9,8 +9,9 @@ import ( ) type evalValue struct { - bytes []byte - u64 *uint64 + bytes []byte + u64 *uint64 + isBytes bool } type evalContext struct { @@ -175,7 +176,7 @@ func evaluateExpr(expr *Expr, ctx *evalContext) (evalValue, error) { } out = append(out, bytesValue...) } - return evalValue{bytes: out}, nil + return evalValue{bytes: out, isBytes: true}, nil case "slice": if len(expr.GetArgs()) != 3 { return evalValue{}, errors.New("slice expects 3 args") @@ -208,52 +209,236 @@ func evaluateExpr(expr *Expr, ctx *evalContext) (evalValue, error) { if end > uint64(len(sourceBytes)) { return evalValue{}, errors.New("slice out of bounds") } - return evalValue{bytes: append([]byte(nil), sourceBytes[offsetU64:end]...)}, nil + return evalValue{bytes: append([]byte(nil), sourceBytes[offsetU64:end]...), isBytes: true}, nil case "xor16": return evaluateXor(expr.GetArgs(), 0xFFFF, 2, ctx) case "xor32": return evaluateXor(expr.GetArgs(), 0xFFFFFFFF, 4, ctx) case "be16": - if len(expr.GetArgs()) != 1 { - return evalValue{}, errors.New("be16 expects 1 arg") - } - value, err := evaluateExprArg(expr.GetArgs()[0], ctx) - if err != nil { - return evalValue{}, err - } - u64Value, err := value.asU64() - if err != nil { - return evalValue{}, err - } - if u64Value > 0xFFFF { - return evalValue{}, errors.New("be16 overflow") - } - out := make([]byte, 2) - binary.BigEndian.PutUint16(out, uint16(u64Value)) - return evalValue{bytes: out}, nil + return evaluatePack(expr.GetArgs(), "be16", 2, binary.BigEndian, ctx) case "be32": - if len(expr.GetArgs()) != 1 { - return evalValue{}, errors.New("be32 expects 1 arg") - } - value, err := evaluateExprArg(expr.GetArgs()[0], ctx) - if err != nil { - return evalValue{}, err - } - u64Value, err := value.asU64() - if err != nil { - return evalValue{}, err - } - if u64Value > 0xFFFFFFFF { - return evalValue{}, errors.New("be32 overflow") - } - out := make([]byte, 4) - binary.BigEndian.PutUint32(out, uint32(u64Value)) - return evalValue{bytes: out}, nil + return evaluatePack(expr.GetArgs(), "be32", 4, binary.BigEndian, ctx) + case "le16": + return evaluatePack(expr.GetArgs(), "le16", 2, binary.LittleEndian, ctx) + case "le32": + return evaluatePack(expr.GetArgs(), "le32", 4, binary.LittleEndian, ctx) + case "le64": + return evaluatePack(expr.GetArgs(), "le64", 8, binary.LittleEndian, ctx) + case "pad": + return evaluatePad(expr.GetArgs(), ctx) + case "truncate": + return evaluateTruncate(expr.GetArgs(), ctx) + case "add": + return evaluateBinaryU64Op(expr.GetArgs(), "add", ctx, func(left, right uint64) (uint64, error) { + if left > ^uint64(0)-right { + return 0, errors.New("add overflow") + } + return left + right, nil + }) + case "sub": + return evaluateBinaryU64Op(expr.GetArgs(), "sub", ctx, func(left, right uint64) (uint64, error) { + if left < right { + return 0, errors.New("sub underflow") + } + return left - right, nil + }) + case "and": + return evaluateBinaryU64Op(expr.GetArgs(), "and", ctx, func(left, right uint64) (uint64, error) { + return left & right, nil + }) + case "or": + return evaluateBinaryU64Op(expr.GetArgs(), "or", ctx, func(left, right uint64) (uint64, error) { + return left | right, nil + }) + case "shl": + return evaluateShift(expr.GetArgs(), "shl", ctx, func(value uint64, shift uint) (uint64, error) { + if shift >= 64 { + return 0, errors.New("shift out of range") + } + if value > (^uint64(0) >> shift) { + return 0, errors.New("shl overflow") + } + return value << shift, nil + }) + case "shr": + return evaluateShift(expr.GetArgs(), "shr", ctx, func(value uint64, shift uint) (uint64, error) { + if shift >= 64 { + return 0, errors.New("shift out of range") + } + return value >> shift, nil + }) default: return evalValue{}, errors.New("unsupported expr op: ", expr.GetOp()) } } +func evaluatePack(args []*ExprArg, name string, width int, order binary.ByteOrder, ctx *evalContext) (evalValue, error) { + if len(args) != 1 { + return evalValue{}, errors.New(name, " expects 1 arg") + } + value, err := evaluateExprArg(args[0], ctx) + if err != nil { + return evalValue{}, err + } + u64Value, err := value.asU64() + if err != nil { + return evalValue{}, err + } + + switch width { + case 2: + if u64Value > 0xFFFF { + return evalValue{}, errors.New(name, " overflow") + } + out := make([]byte, 2) + order.PutUint16(out, uint16(u64Value)) + return evalValue{bytes: out, isBytes: true}, nil + case 4: + if u64Value > 0xFFFFFFFF { + return evalValue{}, errors.New(name, " overflow") + } + out := make([]byte, 4) + order.PutUint32(out, uint32(u64Value)) + return evalValue{bytes: out, isBytes: true}, nil + case 8: + out := make([]byte, 8) + order.PutUint64(out, u64Value) + return evalValue{bytes: out, isBytes: true}, nil + default: + return evalValue{}, errors.New("unsupported pack width") + } +} + +func evaluatePad(args []*ExprArg, ctx *evalContext) (evalValue, error) { + if len(args) != 3 { + return evalValue{}, errors.New("pad expects 3 args") + } + source, err := evaluateExprArg(args[0], ctx) + if err != nil { + return evalValue{}, err + } + target, err := evaluateExprArg(args[1], ctx) + if err != nil { + return evalValue{}, err + } + fill, err := evaluateExprArg(args[2], ctx) + if err != nil { + return evalValue{}, err + } + sourceBytes, err := source.asBytes() + if err != nil { + return evalValue{}, err + } + targetU64, err := target.asU64() + if err != nil { + return evalValue{}, err + } + fillBytes, err := fill.asBytes() + if err != nil { + return evalValue{}, err + } + if len(fillBytes) == 0 { + return evalValue{}, errors.New("pad fill must not be empty") + } + if targetU64 < uint64(len(sourceBytes)) { + return evalValue{}, errors.New("pad target shorter than source") + } + + out := append([]byte(nil), sourceBytes...) + for uint64(len(out)) < targetU64 { + remaining := int(targetU64) - len(out) + if remaining >= len(fillBytes) { + out = append(out, fillBytes...) + continue + } + out = append(out, fillBytes[:remaining]...) + } + return evalValue{bytes: out, isBytes: true}, nil +} + +func evaluateTruncate(args []*ExprArg, ctx *evalContext) (evalValue, error) { + if len(args) != 2 { + return evalValue{}, errors.New("truncate expects 2 args") + } + source, err := evaluateExprArg(args[0], ctx) + if err != nil { + return evalValue{}, err + } + length, err := evaluateExprArg(args[1], ctx) + if err != nil { + return evalValue{}, err + } + sourceBytes, err := source.asBytes() + if err != nil { + return evalValue{}, err + } + lengthU64, err := length.asU64() + if err != nil { + return evalValue{}, err + } + if lengthU64 > uint64(len(sourceBytes)) { + return evalValue{}, errors.New("truncate out of bounds") + } + return evalValue{bytes: append([]byte(nil), sourceBytes[:lengthU64]...), isBytes: true}, nil +} + +func evaluateBinaryU64Op(args []*ExprArg, name string, ctx *evalContext, op func(left, right uint64) (uint64, error)) (evalValue, error) { + if len(args) != 2 { + return evalValue{}, errors.New(name, " expects 2 args") + } + left, err := evaluateExprArg(args[0], ctx) + if err != nil { + return evalValue{}, err + } + right, err := evaluateExprArg(args[1], ctx) + if err != nil { + return evalValue{}, err + } + leftU64, err := left.asU64() + if err != nil { + return evalValue{}, err + } + rightU64, err := right.asU64() + if err != nil { + return evalValue{}, err + } + result, err := op(leftU64, rightU64) + if err != nil { + return evalValue{}, err + } + return evalValue{u64: &result}, nil +} + +func evaluateShift(args []*ExprArg, name string, ctx *evalContext, op func(value uint64, shift uint) (uint64, error)) (evalValue, error) { + if len(args) != 2 { + return evalValue{}, errors.New(name, " expects 2 args") + } + value, err := evaluateExprArg(args[0], ctx) + if err != nil { + return evalValue{}, err + } + shift, err := evaluateExprArg(args[1], ctx) + if err != nil { + return evalValue{}, err + } + valueU64, err := value.asU64() + if err != nil { + return evalValue{}, err + } + shiftU64, err := shift.asU64() + if err != nil { + return evalValue{}, err + } + if shiftU64 >= 64 { + return evalValue{}, errors.New("shift out of range") + } + result, err := op(valueU64, uint(shiftU64)) + if err != nil { + return evalValue{}, err + } + return evalValue{u64: &result}, nil +} + func evaluateXor(args []*ExprArg, mask uint64, width int, ctx *evalContext) (evalValue, error) { if len(args) != 2 { return evalValue{}, errors.New("xor expects 2 args") @@ -309,6 +494,30 @@ func measureExpr(expr *Expr, sizeCtx map[string]int) (int, error) { return 2, nil case "be32": return 4, nil + case "le16": + return 2, nil + case "le32": + return 4, nil + case "le64": + return 8, nil + case "pad": + if len(expr.GetArgs()) != 3 { + return 0, errors.New("pad expects 3 args") + } + lengthArg := expr.GetArgs()[1] + if value, ok := lengthArg.GetValue().(*ExprArg_U64); ok { + return int(value.U64), nil + } + return 0, errors.New("pad length must be u64") + case "truncate": + if len(expr.GetArgs()) != 2 { + return 0, errors.New("truncate expects 2 args") + } + lengthArg := expr.GetArgs()[1] + if value, ok := lengthArg.GetValue().(*ExprArg_U64); ok { + return int(value.U64), nil + } + return 0, errors.New("truncate length must be u64") default: return 0, errors.New("expr size is not bytes for op: ", expr.GetOp()) } @@ -317,7 +526,7 @@ func measureExpr(expr *Expr, sizeCtx map[string]int) (int, error) { func evaluateExprArg(arg *ExprArg, ctx *evalContext) (evalValue, error) { switch value := arg.GetValue().(type) { case *ExprArg_Bytes: - return evalValue{bytes: append([]byte(nil), value.Bytes...)}, nil + return evalValue{bytes: append([]byte(nil), value.Bytes...), isBytes: true}, nil case *ExprArg_U64: return evalValue{u64: &value.U64}, nil case *ExprArg_Var: @@ -325,7 +534,7 @@ func evaluateExprArg(arg *ExprArg, ctx *evalContext) (evalValue, error) { if !ok { return evalValue{}, errors.New("unknown variable: ", value.Var) } - return evalValue{bytes: append([]byte(nil), saved...)}, nil + return evalValue{bytes: append([]byte(nil), saved...), isBytes: true}, nil case *ExprArg_Metadata: metadata, ok := ctx.metadata[value.Metadata] if !ok { @@ -361,7 +570,7 @@ func measureExprArg(arg *ExprArg, sizeCtx map[string]int) (int, error) { } func (v evalValue) asBytes() ([]byte, error) { - if v.bytes != nil { + if v.isBytes { return append([]byte(nil), v.bytes...), nil } return nil, errors.New("expr value is not bytes") diff --git a/xray-core/transport/internet/finalmask/header/custom/evaluator_test.go b/xray-core/transport/internet/finalmask/header/custom/evaluator_test.go index 6bd342641e..51a9d2771e 100644 --- a/xray-core/transport/internet/finalmask/header/custom/evaluator_test.go +++ b/xray-core/transport/internet/finalmask/header/custom/evaluator_test.go @@ -128,3 +128,364 @@ func TestEvaluatorRejectsInvalidArgType(t *testing.T) { t.Fatal("expected evaluator error") } } + +func TestEvaluatorLittleEndianProducesExpectedBytes(t *testing.T) { + items := []*UDPItem{ + { + Expr: &Expr{ + Op: "concat", + Args: []*ExprArg{ + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "le16", + Args: []*ExprArg{ + {Value: &ExprArg_U64{U64: 0x1234}}, + }, + }, + }, + }, + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "le32", + Args: []*ExprArg{ + {Value: &ExprArg_U64{U64: 0xA1B2C3D4}}, + }, + }, + }, + }, + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "le64", + Args: []*ExprArg{ + {Value: &ExprArg_U64{U64: 0x0102030405060708}}, + }, + }, + }, + }, + }, + }, + }, + } + + got, err := evaluateUDPItems(items) + if err != nil { + t.Fatal(err) + } + + want := []byte{ + 0x34, 0x12, + 0xD4, 0xC3, 0xB2, 0xA1, + 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, + } + if !bytes.Equal(got, want) { + t.Fatalf("unexpected output: %x", got) + } +} + +func TestEvaluatorPadAndTruncateShapeBytes(t *testing.T) { + items := []*UDPItem{ + { + Expr: &Expr{ + Op: "concat", + Args: []*ExprArg{ + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "pad", + Args: []*ExprArg{ + {Value: &ExprArg_Bytes{Bytes: []byte{0xAA, 0xBB}}}, + {Value: &ExprArg_U64{U64: 5}}, + {Value: &ExprArg_Bytes{Bytes: []byte{0xCC, 0xDD}}}, + }, + }, + }, + }, + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "truncate", + Args: []*ExprArg{ + {Value: &ExprArg_Bytes{Bytes: []byte{1, 2, 3, 4}}}, + {Value: &ExprArg_U64{U64: 2}}, + }, + }, + }, + }, + }, + }, + }, + } + + got, err := evaluateUDPItems(items) + if err != nil { + t.Fatal(err) + } + + want := []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xCC, 0x01, 0x02} + if !bytes.Equal(got, want) { + t.Fatalf("unexpected output: %x", got) + } +} + +func TestMeasureUDPItemsSupportsPadAndTruncate(t *testing.T) { + items := []*UDPItem{ + { + Expr: &Expr{ + Op: "pad", + Args: []*ExprArg{ + {Value: &ExprArg_Bytes{Bytes: []byte{0xAA}}}, + {Value: &ExprArg_U64{U64: 4}}, + {Value: &ExprArg_Bytes{Bytes: []byte{0x00}}}, + }, + }, + }, + { + Expr: &Expr{ + Op: "truncate", + Args: []*ExprArg{ + {Value: &ExprArg_Bytes{Bytes: []byte{1, 2, 3, 4}}}, + {Value: &ExprArg_U64{U64: 3}}, + }, + }, + }, + } + + got, err := measureUDPItems(items) + if err != nil { + t.Fatal(err) + } + + if got != 7 { + t.Fatalf("unexpected size: %d", got) + } +} + +func TestEvaluatorArithmeticAndBitwiseProduceExpectedBytes(t *testing.T) { + items := []*UDPItem{ + { + Expr: &Expr{ + Op: "concat", + Args: []*ExprArg{ + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "be16", + Args: []*ExprArg{ + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "add", + Args: []*ExprArg{ + {Value: &ExprArg_U64{U64: 1}}, + {Value: &ExprArg_U64{U64: 2}}, + }, + }, + }, + }, + }, + }, + }, + }, + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "be16", + Args: []*ExprArg{ + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "sub", + Args: []*ExprArg{ + {Value: &ExprArg_U64{U64: 10}}, + {Value: &ExprArg_U64{U64: 3}}, + }, + }, + }, + }, + }, + }, + }, + }, + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "be16", + Args: []*ExprArg{ + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "and", + Args: []*ExprArg{ + {Value: &ExprArg_U64{U64: 0xF0F0}}, + {Value: &ExprArg_U64{U64: 0x0FF0}}, + }, + }, + }, + }, + }, + }, + }, + }, + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "be16", + Args: []*ExprArg{ + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "or", + Args: []*ExprArg{ + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "shl", + Args: []*ExprArg{ + {Value: &ExprArg_U64{U64: 1}}, + {Value: &ExprArg_U64{U64: 8}}, + }, + }, + }, + }, + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "shr", + Args: []*ExprArg{ + {Value: &ExprArg_U64{U64: 0x80}}, + {Value: &ExprArg_U64{U64: 7}}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + got, err := evaluateUDPItems(items) + if err != nil { + t.Fatal(err) + } + + want := []byte{ + 0x00, 0x03, + 0x00, 0x07, + 0x00, 0xF0, + 0x01, 0x01, + } + if !bytes.Equal(got, want) { + t.Fatalf("unexpected output: %x", got) + } +} + +func TestEvaluatorRejectsInvalidShapingAndArithmetic(t *testing.T) { + tests := []struct { + name string + items []*UDPItem + match string + }{ + { + name: "pad with empty fill", + items: []*UDPItem{ + { + Expr: &Expr{ + Op: "pad", + Args: []*ExprArg{ + {Value: &ExprArg_Bytes{Bytes: []byte{0xAA}}}, + {Value: &ExprArg_U64{U64: 4}}, + {Value: &ExprArg_Bytes{Bytes: []byte{}}}, + }, + }, + }, + }, + match: "pad fill", + }, + { + name: "truncate beyond source", + items: []*UDPItem{ + { + Expr: &Expr{ + Op: "truncate", + Args: []*ExprArg{ + {Value: &ExprArg_Bytes{Bytes: []byte{1, 2}}}, + {Value: &ExprArg_U64{U64: 3}}, + }, + }, + }, + }, + match: "truncate", + }, + { + name: "sub underflow", + items: []*UDPItem{ + { + Expr: &Expr{ + Op: "be16", + Args: []*ExprArg{ + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "sub", + Args: []*ExprArg{ + {Value: &ExprArg_U64{U64: 1}}, + {Value: &ExprArg_U64{U64: 2}}, + }, + }, + }, + }, + }, + }, + }, + }, + match: "underflow", + }, + { + name: "shift too large", + items: []*UDPItem{ + { + Expr: &Expr{ + Op: "be16", + Args: []*ExprArg{ + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "shl", + Args: []*ExprArg{ + {Value: &ExprArg_U64{U64: 1}}, + {Value: &ExprArg_U64{U64: 64}}, + }, + }, + }, + }, + }, + }, + }, + }, + match: "shift", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := evaluateUDPItems(tt.items) + if err == nil { + t.Fatal("expected evaluator error") + } + if !bytes.Contains([]byte(err.Error()), []byte(tt.match)) { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} diff --git a/xray-core/transport/internet/finalmask/header/custom/udp_runtime_test.go b/xray-core/transport/internet/finalmask/header/custom/udp_runtime_test.go index c66d4c41de..e0a091f4df 100644 --- a/xray-core/transport/internet/finalmask/header/custom/udp_runtime_test.go +++ b/xray-core/transport/internet/finalmask/header/custom/udp_runtime_test.go @@ -1,6 +1,10 @@ package custom -import "testing" +import ( + "bytes" + "net" + "testing" +) func TestDSLUDPClientSizeTracksEvaluatedItems(t *testing.T) { conn, err := NewConnClientUDP(&UDPConfig{ @@ -81,3 +85,104 @@ func TestDSLUDPServerRejectsMalformedVarReference(t *testing.T) { t.Fatal("expected packet mismatch") } } + +func TestDSLUDPClientWriteSupportsExtendedExprOps(t *testing.T) { + conn, err := NewConnClientUDP(&UDPConfig{ + Client: []*UDPItem{ + { + Expr: &Expr{ + Op: "le16", + Args: []*ExprArg{ + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "add", + Args: []*ExprArg{ + {Value: &ExprArg_U64{U64: 1}}, + {Value: &ExprArg_U64{U64: 2}}, + }, + }, + }, + }, + }, + }, + }, + { + Expr: &Expr{ + Op: "pad", + Args: []*ExprArg{ + {Value: &ExprArg_Bytes{Bytes: []byte{0xAA}}}, + {Value: &ExprArg_U64{U64: 3}}, + {Value: &ExprArg_Bytes{Bytes: []byte{0xBB}}}, + }, + }, + }, + { + Expr: &Expr{ + Op: "truncate", + Args: []*ExprArg{ + {Value: &ExprArg_Bytes{Bytes: []byte{1, 2, 3, 4}}}, + {Value: &ExprArg_U64{U64: 2}}, + }, + }, + }, + { + Expr: &Expr{ + Op: "be16", + Args: []*ExprArg{ + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "or", + Args: []*ExprArg{ + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "shl", + Args: []*ExprArg{ + {Value: &ExprArg_U64{U64: 1}}, + {Value: &ExprArg_U64{U64: 8}}, + }, + }, + }, + }, + { + Value: &ExprArg_Expr{ + Expr: &Expr{ + Op: "shr", + Args: []*ExprArg{ + {Value: &ExprArg_U64{U64: 0x80}}, + {Value: &ExprArg_U64{U64: 7}}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, nil) + if err != nil { + t.Fatal(err) + } + + client := conn.(*udpCustomClientConn) + buf := make([]byte, client.Size()) + if _, err := client.WriteTo(buf, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 53}); err != nil { + t.Fatal(err) + } + + want := []byte{ + 0x03, 0x00, + 0xAA, 0xBB, 0xBB, + 0x01, 0x02, + 0x01, 0x01, + } + if !bytes.Equal(buf, want) { + t.Fatalf("unexpected encoded header: %x", buf) + } +}