mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-06-20 09:52:09 +03:00
Compare commits
29 Commits
v1.12.17-e
...
v1.12.22-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d16078651 | ||
|
|
18b1101fbe | ||
|
|
4ebe870306 | ||
|
|
50c5e9df0d | ||
|
|
c8a993834e | ||
|
|
260bbbfb45 | ||
|
|
82337299b9 | ||
|
|
c229c79dcc | ||
|
|
f63091d14d | ||
|
|
1c4a01ee90 | ||
|
|
4d7f99310c | ||
|
|
6fc511f56e | ||
|
|
d18d2b352a | ||
|
|
534128bba9 | ||
|
|
736a7368c6 | ||
|
|
e7a9c90213 | ||
|
|
0f3774e501 | ||
|
|
2f8e656522 | ||
|
|
3ba30e3f00 | ||
|
|
f2639a5829 | ||
|
|
69bebbda82 | ||
|
|
00b2c042ee | ||
|
|
d9eb8f3ab6 | ||
|
|
58025a01f8 | ||
|
|
99cad72ea8 | ||
|
|
6e96d620fe | ||
|
|
596291567f | ||
|
|
51ce402dbb | ||
|
|
8b404b5a4c |
2
.github/setup_go_for_windows7.sh
vendored
2
.github/setup_go_for_windows7.sh
vendored
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
VERSION="1.25.5"
|
||||
VERSION="1.25.7"
|
||||
|
||||
mkdir -p $HOME/go
|
||||
cd $HOME/go
|
||||
|
||||
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.5
|
||||
go-version: ^1.25.7
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.5
|
||||
go-version: ^1.25.7
|
||||
- name: Setup Go 1.24
|
||||
if: matrix.legacy_go124
|
||||
uses: actions/setup-go@v5
|
||||
@@ -300,7 +300,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.5
|
||||
go-version: ^1.25.7
|
||||
- name: Setup Android NDK
|
||||
id: setup-ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
@@ -380,7 +380,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.5
|
||||
go-version: ^1.25.7
|
||||
- name: Setup Android NDK
|
||||
id: setup-ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
@@ -479,7 +479,7 @@ jobs:
|
||||
if: matrix.if
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.5
|
||||
go-version: ^1.25.7
|
||||
- name: Set tag
|
||||
if: matrix.if
|
||||
run: |-
|
||||
|
||||
4
.github/workflows/linux.yml
vendored
4
.github/workflows/linux.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.5
|
||||
go-version: ^1.25.7
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.5
|
||||
go-version: ^1.25.7
|
||||
- name: Setup Android NDK
|
||||
if: matrix.os == 'android'
|
||||
uses: nttld/setup-ndk@v1
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
|
||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||
LABEL maintainer="shtorm-7"
|
||||
COPY . /go/src/github.com/sagernet/sing-box
|
||||
WORKDIR /go/src/github.com/sagernet/sing-box
|
||||
ARG TARGETOS TARGETARCH
|
||||
@@ -18,7 +18,7 @@ RUN set -ex \
|
||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
||||
./cmd/sing-box
|
||||
FROM --platform=$TARGETPLATFORM alpine AS dist
|
||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||
LABEL maintainer="shtorm-7"
|
||||
RUN set -ex \
|
||||
&& apk add --no-cache --upgrade bash tzdata ca-certificates nftables
|
||||
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
|
||||
|
||||
Submodule clients/android updated: 8b3433e9ba...eb87216961
Submodule clients/apple updated: 532c140f05...97402ba8b6
@@ -20,6 +20,9 @@ func (c *Range) Build() *Range {
|
||||
}
|
||||
|
||||
func (c *Range) MarshalJSON() ([]byte, error) {
|
||||
if c.From == c.To {
|
||||
return json.Marshal(c.From)
|
||||
}
|
||||
return json.Marshal(fmt.Sprintf("%d-%d", c.From, c.To))
|
||||
}
|
||||
|
||||
@@ -50,9 +53,15 @@ func (c *Range) UnmarshalJSON(content []byte) error {
|
||||
rangeValue.From, rangeValue.To = int32(from), int32(to)
|
||||
}
|
||||
} else {
|
||||
err := json.Unmarshal(content, &rangeValue)
|
||||
if err != nil {
|
||||
return err
|
||||
var int32Value int32
|
||||
err := json.Unmarshal(content, &int32Value)
|
||||
if err == nil {
|
||||
rangeValue.From, rangeValue.To = int32Value, int32Value
|
||||
} else {
|
||||
err := json.Unmarshal(content, &rangeValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if rangeValue.From > rangeValue.To {
|
||||
|
||||
@@ -3,7 +3,6 @@ package pipe
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -136,11 +135,10 @@ func (p *pipe) writeMultiBufferInternal(mb buf.MultiBuffer) error {
|
||||
|
||||
if p.data == nil {
|
||||
p.data = mb
|
||||
return nil
|
||||
} else {
|
||||
p.data, _ = buf.MergeMulti(p.data, mb)
|
||||
}
|
||||
|
||||
p.data, _ = buf.MergeMulti(p.data, mb)
|
||||
return errSlowDown
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *pipe) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
@@ -155,30 +153,23 @@ func (p *pipe) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err == errSlowDown {
|
||||
p.readSignal.Signal()
|
||||
|
||||
// Yield current goroutine. Hopefully the reading counterpart can pick up the payload.
|
||||
runtime.Gosched()
|
||||
return nil
|
||||
if err == errBufferFull {
|
||||
if p.option.discardOverflow {
|
||||
buf.ReleaseMulti(mb)
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-p.writeSignal.Wait():
|
||||
continue
|
||||
case <-p.done.Wait():
|
||||
buf.ReleaseMulti(mb)
|
||||
return io.ErrClosedPipe
|
||||
}
|
||||
}
|
||||
|
||||
if err == errBufferFull && p.option.discardOverflow {
|
||||
buf.ReleaseMulti(mb)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != errBufferFull {
|
||||
buf.ReleaseMulti(mb)
|
||||
p.readSignal.Signal()
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-p.writeSignal.Wait():
|
||||
case <-p.done.Wait():
|
||||
return io.ErrClosedPipe
|
||||
}
|
||||
buf.ReleaseMulti(mb)
|
||||
p.readSignal.Signal()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
28
common/xray/utils/browser.go
Normal file
28
common/xray/utils/browser.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/cpuid/v2"
|
||||
)
|
||||
|
||||
func ChromeVersion() int {
|
||||
// Use only CPU info as seed for PRNG
|
||||
seed := int64(cpuid.CPU.Family + cpuid.CPU.Model + cpuid.CPU.PhysicalCores + cpuid.CPU.LogicalCores + cpuid.CPU.CacheLine)
|
||||
rng := rand.New(rand.NewSource(seed))
|
||||
// Start from Chrome 144 released on 2026.1.13
|
||||
releaseDate := time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC)
|
||||
version := 144
|
||||
now := time.Now()
|
||||
// Each version has random 25-45 day interval
|
||||
for releaseDate.Before(now) {
|
||||
releaseDate = releaseDate.AddDate(0, 0, rng.Intn(21)+25)
|
||||
version++
|
||||
}
|
||||
return version - 1
|
||||
}
|
||||
|
||||
// ChromeUA provides default browser User-Agent based on CPU-seeded PRNG.
|
||||
var ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + strconv.Itoa(ChromeVersion()) + ".0.0.0 Safari/537.36"
|
||||
24
common/xray/utils/padding.go
Normal file
24
common/xray/utils/padding.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
// 8 ÷ (397/62)
|
||||
h2packCorrectionFactor = 1.2493702770780857
|
||||
base62TotalCharsNum = 62
|
||||
base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
)
|
||||
|
||||
// H2Base62Pad generates a base62 padding string for HTTP/2 header
|
||||
// The total len will be slightly longer than the input to match the length after h2(h3 also) header huffman encoding
|
||||
func H2Base62Pad[T int32 | int64 | int](expectedLen T) string {
|
||||
actualLenFloat := float64(expectedLen) * h2packCorrectionFactor
|
||||
actualLen := int(actualLenFloat)
|
||||
result := make([]byte, actualLen)
|
||||
for i := range actualLen {
|
||||
result[i] = base62Chars[rand.N(base62TotalCharsNum)]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
@@ -85,10 +85,14 @@ func ParseString(str string) (UUID, error) {
|
||||
b := uuid.Bytes()
|
||||
|
||||
for _, byteGroup := range byteGroups {
|
||||
if text[0] == '-' {
|
||||
if len(text) > 0 && text[0] == '-' {
|
||||
text = text[1:]
|
||||
}
|
||||
|
||||
if len(text) < byteGroup {
|
||||
return uuid, E.New("invalid UUID: ", str)
|
||||
}
|
||||
|
||||
if _, err := hex.Decode(b[:byteGroup/2], text[:byteGroup]); err != nil {
|
||||
return uuid, err
|
||||
}
|
||||
|
||||
@@ -144,7 +144,11 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
if c.cache != nil {
|
||||
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
|
||||
if loaded {
|
||||
<-cond
|
||||
select {
|
||||
case <-cond:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
} else {
|
||||
defer func() {
|
||||
c.cacheLock.Delete(question)
|
||||
@@ -154,7 +158,11 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
} else if c.transportCache != nil {
|
||||
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
|
||||
if loaded {
|
||||
<-cond
|
||||
select {
|
||||
case <-cond:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
} else {
|
||||
defer func() {
|
||||
c.transportCacheLock.Delete(question)
|
||||
|
||||
@@ -378,9 +378,11 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
||||
case *R.RuleActionReject:
|
||||
return nil, &R.RejectedError{Cause: action.Error(ctx)}
|
||||
case *R.RuleActionPredefined:
|
||||
responseAddrs = nil
|
||||
if action.Rcode != mDNS.RcodeSuccess {
|
||||
err = RcodeError(action.Rcode)
|
||||
} else {
|
||||
err = nil
|
||||
for _, answer := range action.Answer {
|
||||
switch record := answer.(type) {
|
||||
case *mDNS.A:
|
||||
|
||||
@@ -2,6 +2,34 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.12.22
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.21
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.20
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.19
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.18
|
||||
|
||||
* Add fallback routing rule for `auto_redirect` **1**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default),
|
||||
ensuring traffic is routed to the sing-box table when no route is found in system tables.
|
||||
|
||||
The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768).
|
||||
|
||||
#### 1.12.17
|
||||
|
||||
* Update uTLS to v1.8.2 **1**
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.18"
|
||||
|
||||
:material-plus: [auto_redirect_iproute2_fallback_rule_index](#auto_redirect_iproute2_fallback_rule_index)
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-plus: [loopback_address](#loopback_address)
|
||||
@@ -63,6 +67,7 @@ icon: material/new-box
|
||||
"auto_redirect": true,
|
||||
"auto_redirect_input_mark": "0x2023",
|
||||
"auto_redirect_output_mark": "0x2024",
|
||||
"auto_redirect_iproute2_fallback_rule_index": 32768,
|
||||
"loopback_address": [
|
||||
"10.7.0.1"
|
||||
],
|
||||
@@ -278,6 +283,17 @@ Connection output mark used by `auto_redirect`.
|
||||
|
||||
`0x2024` is used by default.
|
||||
|
||||
#### auto_redirect_iproute2_fallback_rule_index
|
||||
|
||||
!!! question "Since sing-box 1.12.18"
|
||||
|
||||
Linux iproute2 fallback rule index generated by `auto_redirect`.
|
||||
|
||||
This rule is checked after system default rules (32766: main, 32767: default),
|
||||
routing traffic to the sing-box table only when no route is found in system tables.
|
||||
|
||||
`32768` is used by default.
|
||||
|
||||
#### loopback_address
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.12.18 中的更改"
|
||||
|
||||
:material-plus: [auto_redirect_iproute2_fallback_rule_index](#auto_redirect_iproute2_fallback_rule_index)
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-plus: [loopback_address](#loopback_address)
|
||||
@@ -63,6 +67,7 @@ icon: material/new-box
|
||||
"auto_redirect": true,
|
||||
"auto_redirect_input_mark": "0x2023",
|
||||
"auto_redirect_output_mark": "0x2024",
|
||||
"auto_redirect_iproute2_fallback_rule_index": 32768,
|
||||
"loopback_address": [
|
||||
"10.7.0.1"
|
||||
],
|
||||
@@ -277,6 +282,17 @@ tun 接口的 IPv6 前缀。
|
||||
|
||||
默认使用 `0x2024`。
|
||||
|
||||
#### auto_redirect_iproute2_fallback_rule_index
|
||||
|
||||
!!! question "自 sing-box 1.12.18 起"
|
||||
|
||||
`auto_redirect` 生成的 iproute2 回退规则索引。
|
||||
|
||||
此规则在系统默认规则(32766: main,32767: default)之后检查,
|
||||
仅当系统路由表中未找到路由时才将流量路由到 sing-box 路由表。
|
||||
|
||||
默认使用 `32768`。
|
||||
|
||||
#### loopback_address
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"server": "example.com",
|
||||
"server_port": 443,
|
||||
"uuid": "3179dce2-2ff9-413c-85b4-c1d53ed41668",
|
||||
"tls": {
|
||||
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#outbound
|
||||
"enabled": true,
|
||||
"server_name": "example.com",
|
||||
"alpn": "h2" // h3 for QUIC
|
||||
@@ -39,33 +39,65 @@
|
||||
"host": "example.com",
|
||||
"path": "/xhttp",
|
||||
"domain_strategy": "prefer_ipv4",
|
||||
"xmux": {
|
||||
"max_concurrency": "0-1",
|
||||
"max_connections": "0-1",
|
||||
"c_max_reuse_times": "0-1",
|
||||
"h_max_request_times": "0-1",
|
||||
"h_max_reusable_secs": "0-1",
|
||||
"h_keep_alive_period": 60
|
||||
"x_padding_bytes": "100-1000",
|
||||
"no_grpc_header": false, // stream-up/one, client only
|
||||
"sc_max_each_post_bytes": 1000000, // packet-up only
|
||||
"sc_min_posts_interval_ms": 30, // packet-up, client only
|
||||
"xmux": { // h2/h3 mainly, client only
|
||||
"max_concurrency": "16-32",
|
||||
"max_connections": 0,
|
||||
"c_max_reuse_times": 0,
|
||||
"h_max_request_times": "600-900",
|
||||
"h_max_reusable_secs": "1800-3000",
|
||||
"h_keep_alive_period": 0
|
||||
},
|
||||
"x_padding_obfs_mode": false,
|
||||
"x_padding_key": "",
|
||||
"x_padding_header": "",
|
||||
"x_padding_placement": "",
|
||||
"x_padding_method": "",
|
||||
"uplink_http_method": "",
|
||||
"session_placement": "",
|
||||
"session_key": "",
|
||||
"seq_placement": "",
|
||||
"seq_key": "",
|
||||
"uplink_data_placement": "",
|
||||
"uplink_data_key": "",
|
||||
"uplink_chunk_size": 0,
|
||||
"server": "example.com",
|
||||
"server_port": 443,
|
||||
"download": {
|
||||
"host": "example.com",
|
||||
"path": "/xhttp",
|
||||
"domain_strategy": "prefer_ipv4",
|
||||
"x_padding_bytes": "0-0",
|
||||
"sc_max_each_post_bytes": "0-0",
|
||||
"sc_min_posts_interval_ms": "0-0",
|
||||
"sc_stream_up_server_secs": "0-0",
|
||||
"xmux": {
|
||||
"max_concurrency": "0-1",
|
||||
"max_connections": "0-1",
|
||||
"c_max_reuse_times": "0-1",
|
||||
"h_max_request_times": "0-1",
|
||||
"h_max_reusable_secs": "0-1",
|
||||
"h_keep_alive_period": 60
|
||||
"x_padding_bytes": "100-1000",
|
||||
"no_grpc_header": false, // stream-up/one, client only
|
||||
"sc_max_each_post_bytes": 1000000, // packet-up only
|
||||
"sc_min_posts_interval_ms": 30, // packet-up, client only
|
||||
"xmux": { // h2/h3 mainly, client only
|
||||
"max_concurrency": "16-32",
|
||||
"max_connections": 0,
|
||||
"c_max_reuse_times": 0,
|
||||
"h_max_request_times": "600-900",
|
||||
"h_max_reusable_secs": "1800-3000",
|
||||
"h_keep_alive_period": 0
|
||||
},
|
||||
"x_padding_obfs_mode": false,
|
||||
"x_padding_key": "",
|
||||
"x_padding_header": "",
|
||||
"x_padding_placement": "",
|
||||
"x_padding_method": "",
|
||||
"uplink_http_method": "",
|
||||
"session_placement": "",
|
||||
"session_key": "",
|
||||
"seq_placement": "",
|
||||
"seq_key": "",
|
||||
"uplink_data_placement": "",
|
||||
"uplink_data_key": "",
|
||||
"uplink_chunk_size": 0,
|
||||
"server": "example.com",
|
||||
"server_port": 443,
|
||||
"tls": {
|
||||
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#outbound
|
||||
"enabled": true,
|
||||
"server_name": "example.com",
|
||||
"alpn": "h2" // h3 for QUIC
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"uuid": "3179dce2-2ff9-413c-85b4-c1d53ed41668"
|
||||
}
|
||||
],
|
||||
"tls": {
|
||||
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#inbound
|
||||
"enabled": true,
|
||||
"server_name": "example.com",
|
||||
"alpn": "h2", // h3 for QUIC
|
||||
@@ -33,6 +33,23 @@
|
||||
"type": "xhttp",
|
||||
"mode": "stream-up",
|
||||
"path": "/xhttp",
|
||||
"x_padding_bytes": "100-1000",
|
||||
"no_sse_header": false, // server only
|
||||
"sc_max_buffered_posts": 30, // packet-up, server only
|
||||
"sc_stream_up_server_secs": "20-80", // stream-up, server only
|
||||
"x_padding_obfs_mode": false,
|
||||
"x_padding_key": "",
|
||||
"x_padding_header": "",
|
||||
"x_padding_placement": "",
|
||||
"x_padding_method": "",
|
||||
"uplink_http_method": "",
|
||||
"session_placement": "",
|
||||
"session_key": "",
|
||||
"seq_placement": "",
|
||||
"seq_key": "",
|
||||
"uplink_data_placement": "",
|
||||
"uplink_data_key": "",
|
||||
"uplink_chunk_size": 0,
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
8
go.mod
8
go.mod
@@ -30,13 +30,13 @@ require (
|
||||
github.com/sagernet/gomobile v0.1.8
|
||||
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
|
||||
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3
|
||||
github.com/sagernet/sing v0.7.14
|
||||
github.com/sagernet/sing v0.7.18
|
||||
github.com/sagernet/sing-mux v0.3.4
|
||||
github.com/sagernet/sing-quic v0.5.2
|
||||
github.com/sagernet/sing-quic v0.5.3
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
|
||||
github.com/sagernet/sing-tun v0.7.3
|
||||
github.com/sagernet/sing-tun v0.7.11
|
||||
github.com/sagernet/sing-vmess v0.2.7
|
||||
github.com/sagernet/smux v1.5.50-sing-box-mod.1
|
||||
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2
|
||||
@@ -148,7 +148,7 @@ require (
|
||||
lukechampine.com/blake3 v1.4.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/sagernet/wireguard-go => github.com/shtorm-7/wireguard-go v0.0.1-beta.7-extended-1.1.0
|
||||
replace github.com/sagernet/wireguard-go => github.com/shtorm-7/wireguard-go v0.0.1-beta.7-extended-1.2.0
|
||||
|
||||
replace github.com/sagernet/tailscale => github.com/shtorm-7/tailscale v1.80.3-sing-box-1.12-mod.2-extended-1.0.0
|
||||
|
||||
|
||||
16
go.sum
16
go.sum
@@ -171,20 +171,20 @@ 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.52.0-sing-box-mod.3 h1:ySqffGm82rPqI1TUPqmtHIYd12pfEGScygnOxjTL56w=
|
||||
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
|
||||
github.com/sagernet/sing v0.7.14 h1:5QQRDCUvYNOMyVp3LuK/hYEBAIv0VsbD3x/l9zH467s=
|
||||
github.com/sagernet/sing v0.7.14/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing v0.7.18 h1:iZHkaru1/MoHugx3G+9S3WG4owMewKO/KvieE2Pzk4E=
|
||||
github.com/sagernet/sing v0.7.18/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
|
||||
github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
|
||||
github.com/sagernet/sing-quic v0.5.2 h1:I3vlfRImhr0uLwRS3b3ib70RMG9FcXtOKKUDz3eKRWc=
|
||||
github.com/sagernet/sing-quic v0.5.2/go.mod h1:evP1e++ZG8TJHVV5HudXV4vWeYzGfCdF4HwSJZcdqkI=
|
||||
github.com/sagernet/sing-quic v0.5.3 h1:K937DKJN98xqyztijRkLJqbBfyV4rEZcYxFyP3EBikU=
|
||||
github.com/sagernet/sing-quic v0.5.3/go.mod h1:evP1e++ZG8TJHVV5HudXV4vWeYzGfCdF4HwSJZcdqkI=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
|
||||
github.com/sagernet/sing-tun v0.7.3 h1:MFnAir+l24ElEyxdfwtY8mqvUUL9nPnL9TDYLkOmVes=
|
||||
github.com/sagernet/sing-tun v0.7.3/go.mod h1:pUEjh9YHQ2gJT6Lk0TYDklh3WJy7lz+848vleGM3JPM=
|
||||
github.com/sagernet/sing-tun v0.7.11 h1:qB7jy8JKqXg73fYBsDkBSy4ulRSbLrFut0e+y+QPhqU=
|
||||
github.com/sagernet/sing-tun v0.7.11/go.mod h1:pUEjh9YHQ2gJT6Lk0TYDklh3WJy7lz+848vleGM3JPM=
|
||||
github.com/sagernet/sing-vmess v0.2.7 h1:2ee+9kO0xW5P4mfe6TYVWf9VtY8k1JhNysBqsiYj0sk=
|
||||
github.com/sagernet/sing-vmess v0.2.7/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
|
||||
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
|
||||
@@ -195,8 +195,8 @@ github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0 h1:e5s7RKBd2rIPR0StbvZ2vTV
|
||||
github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI=
|
||||
github.com/shtorm-7/tailscale v1.80.3-sing-box-1.12-mod.2-extended-1.0.0 h1:Yp4dIRwiwLda9JXyGMHkfYRr2r01NarkzsNd/oi10dk=
|
||||
github.com/shtorm-7/tailscale v1.80.3-sing-box-1.12-mod.2-extended-1.0.0/go.mod h1:+znUAXWwgcgza5mb5do8j9RC95rpY9lbSc/TyEyCGa4=
|
||||
github.com/shtorm-7/wireguard-go v0.0.1-beta.7-extended-1.1.0 h1:bTmx3NiEeH7mdgsifyNUxIEAA0wokRMSm8iS/hln6n0=
|
||||
github.com/shtorm-7/wireguard-go v0.0.1-beta.7-extended-1.1.0/go.mod h1:DHxMTUaBGHP3tf8nJ/N8AkcoJDD0PHECLhTfLsw+ylQ=
|
||||
github.com/shtorm-7/wireguard-go v0.0.1-beta.7-extended-1.2.0 h1:o/AAMCZPDCrwat2m0rAicFJ+iHfuzBR4nNueORUiEtM=
|
||||
github.com/shtorm-7/wireguard-go v0.0.1-beta.7-extended-1.2.0/go.mod h1:3Ps4sTih9KeKik6xsMdIa+2TWDgTb+ysnq+ztxespk8=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
|
||||
@@ -11,33 +11,34 @@ import (
|
||||
)
|
||||
|
||||
type TunInboundOptions struct {
|
||||
InterfaceName string `json:"interface_name,omitempty"`
|
||||
MTU uint32 `json:"mtu,omitempty"`
|
||||
Address badoption.Listable[netip.Prefix] `json:"address,omitempty"`
|
||||
AutoRoute bool `json:"auto_route,omitempty"`
|
||||
IPRoute2TableIndex int `json:"iproute2_table_index,omitempty"`
|
||||
IPRoute2RuleIndex int `json:"iproute2_rule_index,omitempty"`
|
||||
AutoRedirect bool `json:"auto_redirect,omitempty"`
|
||||
AutoRedirectInputMark FwMark `json:"auto_redirect_input_mark,omitempty"`
|
||||
AutoRedirectOutputMark FwMark `json:"auto_redirect_output_mark,omitempty"`
|
||||
LoopbackAddress badoption.Listable[netip.Addr] `json:"loopback_address,omitempty"`
|
||||
StrictRoute bool `json:"strict_route,omitempty"`
|
||||
RouteAddress badoption.Listable[netip.Prefix] `json:"route_address,omitempty"`
|
||||
RouteAddressSet badoption.Listable[string] `json:"route_address_set,omitempty"`
|
||||
RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"route_exclude_address,omitempty"`
|
||||
RouteExcludeAddressSet badoption.Listable[string] `json:"route_exclude_address_set,omitempty"`
|
||||
IncludeInterface badoption.Listable[string] `json:"include_interface,omitempty"`
|
||||
ExcludeInterface badoption.Listable[string] `json:"exclude_interface,omitempty"`
|
||||
IncludeUID badoption.Listable[uint32] `json:"include_uid,omitempty"`
|
||||
IncludeUIDRange badoption.Listable[string] `json:"include_uid_range,omitempty"`
|
||||
ExcludeUID badoption.Listable[uint32] `json:"exclude_uid,omitempty"`
|
||||
ExcludeUIDRange badoption.Listable[string] `json:"exclude_uid_range,omitempty"`
|
||||
IncludeAndroidUser badoption.Listable[int] `json:"include_android_user,omitempty"`
|
||||
IncludePackage badoption.Listable[string] `json:"include_package,omitempty"`
|
||||
ExcludePackage badoption.Listable[string] `json:"exclude_package,omitempty"`
|
||||
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
|
||||
Stack string `json:"stack,omitempty"`
|
||||
Platform *TunPlatformOptions `json:"platform,omitempty"`
|
||||
InterfaceName string `json:"interface_name,omitempty"`
|
||||
MTU uint32 `json:"mtu,omitempty"`
|
||||
Address badoption.Listable[netip.Prefix] `json:"address,omitempty"`
|
||||
AutoRoute bool `json:"auto_route,omitempty"`
|
||||
IPRoute2TableIndex int `json:"iproute2_table_index,omitempty"`
|
||||
IPRoute2RuleIndex int `json:"iproute2_rule_index,omitempty"`
|
||||
AutoRedirect bool `json:"auto_redirect,omitempty"`
|
||||
AutoRedirectInputMark FwMark `json:"auto_redirect_input_mark,omitempty"`
|
||||
AutoRedirectOutputMark FwMark `json:"auto_redirect_output_mark,omitempty"`
|
||||
AutoRedirectIPRoute2FallbackRuleIndex int `json:"auto_redirect_iproute2_fallback_rule_index,omitempty"`
|
||||
LoopbackAddress badoption.Listable[netip.Addr] `json:"loopback_address,omitempty"`
|
||||
StrictRoute bool `json:"strict_route,omitempty"`
|
||||
RouteAddress badoption.Listable[netip.Prefix] `json:"route_address,omitempty"`
|
||||
RouteAddressSet badoption.Listable[string] `json:"route_address_set,omitempty"`
|
||||
RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"route_exclude_address,omitempty"`
|
||||
RouteExcludeAddressSet badoption.Listable[string] `json:"route_exclude_address_set,omitempty"`
|
||||
IncludeInterface badoption.Listable[string] `json:"include_interface,omitempty"`
|
||||
ExcludeInterface badoption.Listable[string] `json:"exclude_interface,omitempty"`
|
||||
IncludeUID badoption.Listable[uint32] `json:"include_uid,omitempty"`
|
||||
IncludeUIDRange badoption.Listable[string] `json:"include_uid_range,omitempty"`
|
||||
ExcludeUID badoption.Listable[uint32] `json:"exclude_uid,omitempty"`
|
||||
ExcludeUIDRange badoption.Listable[string] `json:"exclude_uid_range,omitempty"`
|
||||
IncludeAndroidUser badoption.Listable[int] `json:"include_android_user,omitempty"`
|
||||
IncludePackage badoption.Listable[string] `json:"include_package,omitempty"`
|
||||
ExcludePackage badoption.Listable[string] `json:"exclude_package,omitempty"`
|
||||
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
|
||||
Stack string `json:"stack,omitempty"`
|
||||
Platform *TunPlatformOptions `json:"platform,omitempty"`
|
||||
InboundOptions
|
||||
|
||||
// Deprecated: removed
|
||||
|
||||
@@ -2,10 +2,10 @@ package option
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
Xbadoption "github.com/sagernet/sing-box/common/xray/json/badoption"
|
||||
"github.com/sagernet/sing-box/common/xray/utils"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
@@ -122,14 +122,29 @@ type V2RayXHTTPBaseOptions struct {
|
||||
ScMaxBufferedPosts int64 `json:"sc_max_buffered_posts,omitempty"`
|
||||
ScStreamUpServerSecs Xbadoption.Range `json:"sc_stream_up_server_secs"`
|
||||
Xmux *V2RayXHTTPXmuxOptions `json:"xmux"`
|
||||
XPaddingObfsMode bool `json:"x_padding_obfs_mode,omitempty"`
|
||||
XPaddingKey string `json:"x_padding_key,omitempty"`
|
||||
XPaddingHeader string `json:"x_padding_header,omitempty"`
|
||||
XPaddingPlacement string `json:"x_padding_placement,omitempty"`
|
||||
XPaddingMethod string `json:"x_padding_method,omitempty"`
|
||||
UplinkHTTPMethod string `json:"uplink_http_method,omitempty"`
|
||||
SessionPlacement string `json:"session_placement,omitempty"`
|
||||
SessionKey string `json:"session_key,omitempty"`
|
||||
SeqPlacement string `json:"seq_placement,omitempty"`
|
||||
SeqKey string `json:"seq_key,omitempty"`
|
||||
UplinkDataPlacement string `json:"uplink_data_placement,omitempty"`
|
||||
UplinkDataKey string `json:"uplink_data_key,omitempty"`
|
||||
UplinkChunkSize uint32 `json:"uplink_chunk_size,omitempty"`
|
||||
}
|
||||
|
||||
type V2RayXHTTPOptions struct {
|
||||
type _V2RayXHTTPOptions struct {
|
||||
Mode string `json:"mode"`
|
||||
V2RayXHTTPBaseOptions
|
||||
Download *V2RayXHTTPDownloadOptions `json:"download"`
|
||||
}
|
||||
|
||||
type V2RayXHTTPOptions _V2RayXHTTPOptions
|
||||
|
||||
type V2RayXHTTPDownloadOptions struct {
|
||||
V2RayXHTTPBaseOptions
|
||||
ServerOptions
|
||||
@@ -137,6 +152,158 @@ type V2RayXHTTPDownloadOptions struct {
|
||||
Detour string `json:"detour,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
PlacementQueryInHeader = "queryInHeader"
|
||||
PlacementCookie = "cookie"
|
||||
PlacementHeader = "header"
|
||||
PlacementQuery = "query"
|
||||
PlacementPath = "path"
|
||||
PlacementBody = "body"
|
||||
)
|
||||
|
||||
func (c V2RayXHTTPOptions) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal((*_V2RayXHTTPOptions)(&c))
|
||||
}
|
||||
|
||||
func (c *V2RayXHTTPOptions) UnmarshalJSON(bytes []byte) error {
|
||||
err := json.Unmarshal(bytes, (*_V2RayXHTTPOptions)(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch c.Mode {
|
||||
case "":
|
||||
c.Mode = "auto"
|
||||
case "auto", "packet-up", "stream-up", "stream-one":
|
||||
default:
|
||||
return E.New("unsupported mode: " + c.Mode)
|
||||
}
|
||||
err = checkV2RayXHTTPBaseOptions(c.Mode, &c.V2RayXHTTPBaseOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Download != nil {
|
||||
err = checkV2RayXHTTPBaseOptions(c.Mode, &c.Download.V2RayXHTTPBaseOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkV2RayXHTTPBaseOptions(mode string, options *V2RayXHTTPBaseOptions) error {
|
||||
// Priority (client): host > serverName > address
|
||||
for k := range options.Headers {
|
||||
if strings.ToLower(k) == "host" {
|
||||
return E.New(`"headers" can't contain "host"`)
|
||||
}
|
||||
}
|
||||
if options.XPaddingBytes.From <= 0 || options.XPaddingBytes.To <= 0 {
|
||||
return E.New("xPaddingBytes cannot be disabled")
|
||||
}
|
||||
if options.XPaddingKey == "" {
|
||||
options.XPaddingKey = "x_padding"
|
||||
}
|
||||
if options.XPaddingHeader == "" {
|
||||
options.XPaddingHeader = "X-Padding"
|
||||
}
|
||||
switch options.XPaddingPlacement {
|
||||
case "":
|
||||
options.XPaddingPlacement = "queryInHeader"
|
||||
case "cookie", "header", "query", "queryInHeader":
|
||||
default:
|
||||
return E.New("unsupported padding placement: " + options.XPaddingPlacement)
|
||||
}
|
||||
switch options.XPaddingMethod {
|
||||
case "":
|
||||
options.XPaddingMethod = "repeat-x"
|
||||
case "repeat-x", "tokenish":
|
||||
default:
|
||||
return E.New("unsupported padding method: " + options.XPaddingMethod)
|
||||
}
|
||||
switch options.UplinkDataPlacement {
|
||||
case "":
|
||||
options.UplinkDataPlacement = "body"
|
||||
case "body":
|
||||
case "cookie", "header":
|
||||
if mode != "packet-up" {
|
||||
return E.New("UplinkDataPlacement can be " + options.UplinkDataPlacement + " only in packet-up mode")
|
||||
}
|
||||
default:
|
||||
return E.New("unsupported uplink data placement: " + options.UplinkDataPlacement)
|
||||
}
|
||||
if options.UplinkHTTPMethod == "" {
|
||||
options.UplinkHTTPMethod = "POST"
|
||||
}
|
||||
options.UplinkHTTPMethod = strings.ToUpper(options.UplinkHTTPMethod)
|
||||
if options.UplinkHTTPMethod == "GET" && mode != "packet-up" {
|
||||
return E.New("uplinkHTTPMethod can be GET only in packet-up mode")
|
||||
}
|
||||
switch options.SessionPlacement {
|
||||
case "":
|
||||
options.SessionPlacement = "path"
|
||||
case "path", "cookie", "header", "query":
|
||||
default:
|
||||
return E.New("unsupported session placement: " + options.SessionPlacement)
|
||||
}
|
||||
switch options.SeqPlacement {
|
||||
case "":
|
||||
options.SeqPlacement = "path"
|
||||
case "path":
|
||||
case "cookie", "header", "query":
|
||||
if options.SessionPlacement == "path" {
|
||||
return E.New("SeqPlacement must be path when SessionPlacement is path")
|
||||
}
|
||||
default:
|
||||
return E.New("unsupported seq placement: " + options.SeqPlacement)
|
||||
}
|
||||
if options.SessionPlacement != "path" && options.SessionKey == "" {
|
||||
switch options.SessionPlacement {
|
||||
case "cookie", "query":
|
||||
options.SessionKey = "x_session"
|
||||
case "header":
|
||||
options.SessionKey = "X-Session"
|
||||
}
|
||||
}
|
||||
if options.SeqPlacement != "path" && options.SeqKey == "" {
|
||||
switch options.SeqPlacement {
|
||||
case "cookie", "query":
|
||||
options.SeqKey = "x_seq"
|
||||
case "header":
|
||||
options.SeqKey = "X-Seq"
|
||||
}
|
||||
}
|
||||
if options.UplinkDataPlacement != "body" && options.UplinkDataKey == "" {
|
||||
switch options.UplinkDataPlacement {
|
||||
case "cookie":
|
||||
options.UplinkDataKey = "x_data"
|
||||
case "header":
|
||||
options.UplinkDataKey = "X-Data"
|
||||
}
|
||||
}
|
||||
if options.UplinkChunkSize == 0 {
|
||||
switch options.UplinkDataPlacement {
|
||||
case "cookie":
|
||||
options.UplinkChunkSize = 3 * 1024 // 3KB
|
||||
case "header":
|
||||
options.UplinkChunkSize = 4 * 1024 // 4KB
|
||||
}
|
||||
} else if options.UplinkChunkSize < 64 {
|
||||
options.UplinkChunkSize = 64
|
||||
}
|
||||
if options.Xmux == nil {
|
||||
options.Xmux = &V2RayXHTTPXmuxOptions{}
|
||||
options.Xmux.MaxConcurrency.From = 1
|
||||
options.Xmux.MaxConcurrency.To = 1
|
||||
options.Xmux.HMaxRequestTimes.From = 600
|
||||
options.Xmux.HMaxRequestTimes.To = 900
|
||||
options.Xmux.HMaxReusableSecs.From = 1800
|
||||
options.Xmux.HMaxReusableSecs.To = 3000
|
||||
} else if options.Xmux.MaxConnections.To > 0 && options.Xmux.MaxConcurrency.To > 0 {
|
||||
return E.New("maxConnections cannot be specified together with maxConcurrency")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *V2RayXHTTPBaseOptions) GetNormalizedPath() string {
|
||||
pathAndQuery := strings.SplitN(c.Path, "?", 2)
|
||||
path := pathAndQuery[0]
|
||||
@@ -158,19 +325,14 @@ func (c *V2RayXHTTPBaseOptions) GetNormalizedQuery() string {
|
||||
return query
|
||||
}
|
||||
|
||||
func (c *V2RayXHTTPBaseOptions) GetRequestHeader(rawURL string) http.Header {
|
||||
func (c *V2RayXHTTPBaseOptions) GetRequestHeader() http.Header {
|
||||
header := http.Header{}
|
||||
for k, v := range c.Headers {
|
||||
header.Add(k, v)
|
||||
}
|
||||
u, _ := url.Parse(rawURL)
|
||||
// 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' is 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.
|
||||
u.RawQuery = "x_padding=" + strings.Repeat("X", int(c.GetNormalizedXPaddingBytes().Rand()))
|
||||
header.Set("Referer", u.String())
|
||||
if header.Get("User-Agent") == "" {
|
||||
header.Set("User-Agent", utils.ChromeUA)
|
||||
}
|
||||
return header
|
||||
}
|
||||
|
||||
@@ -184,6 +346,13 @@ func (c *V2RayXHTTPBaseOptions) GetNormalizedXPaddingBytes() Xbadoption.Range {
|
||||
return c.XPaddingBytes
|
||||
}
|
||||
|
||||
func (c *V2RayXHTTPBaseOptions) GetNormalizedUplinkHTTPMethod() string {
|
||||
if c.UplinkHTTPMethod == "" {
|
||||
return "POST"
|
||||
}
|
||||
return c.UplinkHTTPMethod
|
||||
}
|
||||
|
||||
func (c *V2RayXHTTPBaseOptions) GetNormalizedScMaxEachPostBytes() Xbadoption.Range {
|
||||
if c.ScMaxEachPostBytes.To == 0 {
|
||||
return Xbadoption.Range{
|
||||
@@ -222,6 +391,55 @@ func (c *V2RayXHTTPBaseOptions) GetNormalizedScStreamUpServerSecs() Xbadoption.R
|
||||
return c.ScStreamUpServerSecs
|
||||
}
|
||||
|
||||
func (c *V2RayXHTTPBaseOptions) GetNormalizedSessionPlacement() string {
|
||||
if c.SessionPlacement == "" {
|
||||
return PlacementPath
|
||||
}
|
||||
return c.SessionPlacement
|
||||
}
|
||||
|
||||
func (c *V2RayXHTTPBaseOptions) GetNormalizedSeqPlacement() string {
|
||||
if c.SeqPlacement == "" {
|
||||
return PlacementPath
|
||||
}
|
||||
return c.SeqPlacement
|
||||
}
|
||||
|
||||
func (c *V2RayXHTTPBaseOptions) GetNormalizedUplinkDataPlacement() string {
|
||||
if c.UplinkDataPlacement == "" {
|
||||
return PlacementBody
|
||||
}
|
||||
return c.UplinkDataPlacement
|
||||
}
|
||||
|
||||
func (c *V2RayXHTTPBaseOptions) 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 *V2RayXHTTPBaseOptions) 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 V2RayXHTTPXmuxOptions struct {
|
||||
MaxConcurrency Xbadoption.Range `json:"max_concurrency"`
|
||||
MaxConnections Xbadoption.Range `json:"max_connections"`
|
||||
|
||||
@@ -95,6 +95,7 @@ func (p *paddingConn) writeWithPadding(writer io.Writer, data []byte) (n int, er
|
||||
binary.BigEndian.PutUint16(header, uint16(len(data)))
|
||||
header[2] = byte(paddingSize)
|
||||
common.Must1(buffer.Write(data))
|
||||
buffer.Extend(paddingSize)
|
||||
_, err = writer.Write(buffer.Bytes())
|
||||
if err == nil {
|
||||
n = len(data)
|
||||
|
||||
@@ -174,6 +174,10 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
if ruleIndex == 0 {
|
||||
ruleIndex = tun.DefaultIPRoute2RuleIndex
|
||||
}
|
||||
autoRedirectFallbackRuleIndex := options.AutoRedirectIPRoute2FallbackRuleIndex
|
||||
if autoRedirectFallbackRuleIndex == 0 {
|
||||
autoRedirectFallbackRuleIndex = tun.DefaultIPRoute2AutoRedirectFallbackRuleIndex
|
||||
}
|
||||
inputMark := uint32(options.AutoRedirectInputMark)
|
||||
if inputMark == 0 {
|
||||
inputMark = tun.DefaultAutoRedirectInputMark
|
||||
@@ -192,32 +196,33 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
logger: logger,
|
||||
inboundOptions: options.InboundOptions,
|
||||
tunOptions: tun.Options{
|
||||
Name: options.InterfaceName,
|
||||
MTU: tunMTU,
|
||||
GSO: enableGSO,
|
||||
Inet4Address: inet4Address,
|
||||
Inet6Address: inet6Address,
|
||||
AutoRoute: options.AutoRoute,
|
||||
IPRoute2TableIndex: tableIndex,
|
||||
IPRoute2RuleIndex: ruleIndex,
|
||||
AutoRedirectInputMark: inputMark,
|
||||
AutoRedirectOutputMark: outputMark,
|
||||
Inet4LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is4),
|
||||
Inet6LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is6),
|
||||
StrictRoute: options.StrictRoute,
|
||||
IncludeInterface: options.IncludeInterface,
|
||||
ExcludeInterface: options.ExcludeInterface,
|
||||
Inet4RouteAddress: inet4RouteAddress,
|
||||
Inet6RouteAddress: inet6RouteAddress,
|
||||
Inet4RouteExcludeAddress: inet4RouteExcludeAddress,
|
||||
Inet6RouteExcludeAddress: inet6RouteExcludeAddress,
|
||||
IncludeUID: includeUID,
|
||||
ExcludeUID: excludeUID,
|
||||
IncludeAndroidUser: options.IncludeAndroidUser,
|
||||
IncludePackage: options.IncludePackage,
|
||||
ExcludePackage: options.ExcludePackage,
|
||||
InterfaceMonitor: networkManager.InterfaceMonitor(),
|
||||
EXP_MultiPendingPackets: multiPendingPackets,
|
||||
Name: options.InterfaceName,
|
||||
MTU: tunMTU,
|
||||
GSO: enableGSO,
|
||||
Inet4Address: inet4Address,
|
||||
Inet6Address: inet6Address,
|
||||
AutoRoute: options.AutoRoute,
|
||||
IPRoute2TableIndex: tableIndex,
|
||||
IPRoute2RuleIndex: ruleIndex,
|
||||
IPRoute2AutoRedirectFallbackRuleIndex: autoRedirectFallbackRuleIndex,
|
||||
AutoRedirectInputMark: inputMark,
|
||||
AutoRedirectOutputMark: outputMark,
|
||||
Inet4LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is4),
|
||||
Inet6LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is6),
|
||||
StrictRoute: options.StrictRoute,
|
||||
IncludeInterface: options.IncludeInterface,
|
||||
ExcludeInterface: options.ExcludeInterface,
|
||||
Inet4RouteAddress: inet4RouteAddress,
|
||||
Inet6RouteAddress: inet6RouteAddress,
|
||||
Inet4RouteExcludeAddress: inet4RouteExcludeAddress,
|
||||
Inet6RouteExcludeAddress: inet6RouteExcludeAddress,
|
||||
IncludeUID: includeUID,
|
||||
ExcludeUID: excludeUID,
|
||||
IncludeAndroidUser: options.IncludeAndroidUser,
|
||||
IncludePackage: options.IncludePackage,
|
||||
ExcludePackage: options.ExcludePackage,
|
||||
InterfaceMonitor: networkManager.InterfaceMonitor(),
|
||||
EXP_MultiPendingPackets: multiPendingPackets,
|
||||
},
|
||||
udpTimeout: udpTimeout,
|
||||
stack: options.Stack,
|
||||
@@ -319,7 +324,6 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
|
||||
t.tunOptions.Name = tun.CalculateInterfaceName("")
|
||||
}
|
||||
if t.platformInterface == nil {
|
||||
t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet)
|
||||
for _, routeRuleSet := range t.routeRuleSet {
|
||||
ipSets := routeRuleSet.ExtractIPSet()
|
||||
if len(ipSets) == 0 {
|
||||
@@ -331,7 +335,6 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
|
||||
t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet))
|
||||
}
|
||||
}
|
||||
t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet)
|
||||
for _, routeExcludeRuleSet := range t.routeExcludeRuleSet {
|
||||
ipSets := routeExcludeRuleSet.ExtractIPSet()
|
||||
if len(ipSets) == 0 {
|
||||
|
||||
@@ -107,9 +107,7 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
|
||||
}
|
||||
|
||||
for _, item := range r.items {
|
||||
if _, isRuleSet := item.(*RuleSetItem); !isRuleSet {
|
||||
metadata.DidMatch = true
|
||||
}
|
||||
metadata.DidMatch = true
|
||||
if !item.Match(metadata) {
|
||||
return r.invert
|
||||
}
|
||||
|
||||
157
route/rule/rule_abstract_test.go
Normal file
157
route/rule/rule_abstract_test.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go4.org/netipx"
|
||||
)
|
||||
|
||||
type fakeRuleSet struct {
|
||||
matched bool
|
||||
}
|
||||
|
||||
func (f *fakeRuleSet) Name() string {
|
||||
return "fake-rule-set"
|
||||
}
|
||||
|
||||
func (f *fakeRuleSet) StartContext(context.Context, *adapter.HTTPStartContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeRuleSet) PostStart() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeRuleSet) Metadata() adapter.RuleSetMetadata {
|
||||
return adapter.RuleSetMetadata{}
|
||||
}
|
||||
|
||||
func (f *fakeRuleSet) ExtractIPSet() []*netipx.IPSet {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeRuleSet) IncRef() {}
|
||||
|
||||
func (f *fakeRuleSet) DecRef() {}
|
||||
|
||||
func (f *fakeRuleSet) Cleanup() {}
|
||||
|
||||
func (f *fakeRuleSet) RegisterCallback(adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeRuleSet) UnregisterCallback(*list.Element[adapter.RuleSetUpdateCallback]) {}
|
||||
|
||||
func (f *fakeRuleSet) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeRuleSet) Match(*adapter.InboundContext) bool {
|
||||
return f.matched
|
||||
}
|
||||
|
||||
func (f *fakeRuleSet) String() string {
|
||||
return "fake-rule-set"
|
||||
}
|
||||
|
||||
type fakeRuleItem struct {
|
||||
matched bool
|
||||
}
|
||||
|
||||
func (f *fakeRuleItem) Match(*adapter.InboundContext) bool {
|
||||
return f.matched
|
||||
}
|
||||
|
||||
func (f *fakeRuleItem) String() string {
|
||||
return "fake-rule-item"
|
||||
}
|
||||
|
||||
func newRuleSetOnlyRule(ruleSetMatched bool, invert bool) *DefaultRule {
|
||||
ruleSetItem := &RuleSetItem{
|
||||
setList: []adapter.RuleSet{&fakeRuleSet{matched: ruleSetMatched}},
|
||||
}
|
||||
return &DefaultRule{
|
||||
abstractDefaultRule: abstractDefaultRule{
|
||||
items: []RuleItem{ruleSetItem},
|
||||
allItems: []RuleItem{ruleSetItem},
|
||||
invert: invert,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newSingleItemRule(matched bool) *DefaultRule {
|
||||
item := &fakeRuleItem{matched: matched}
|
||||
return &DefaultRule{
|
||||
abstractDefaultRule: abstractDefaultRule{
|
||||
items: []RuleItem{item},
|
||||
allItems: []RuleItem{item},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestAbstractDefaultRule_RuleSetOnly_InvertFalse(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.True(t, newRuleSetOnlyRule(true, false).Match(&adapter.InboundContext{}))
|
||||
require.False(t, newRuleSetOnlyRule(false, false).Match(&adapter.InboundContext{}))
|
||||
}
|
||||
|
||||
func TestAbstractDefaultRule_RuleSetOnly_InvertTrue(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.False(t, newRuleSetOnlyRule(true, true).Match(&adapter.InboundContext{}))
|
||||
require.True(t, newRuleSetOnlyRule(false, true).Match(&adapter.InboundContext{}))
|
||||
}
|
||||
|
||||
func TestAbstractLogicalRule_And_WithRuleSetInvert(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
name string
|
||||
aMatched bool
|
||||
ruleSetBMatch bool
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "A true B true",
|
||||
aMatched: true,
|
||||
ruleSetBMatch: true,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "A true B false",
|
||||
aMatched: true,
|
||||
ruleSetBMatch: false,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "A false B true",
|
||||
aMatched: false,
|
||||
ruleSetBMatch: true,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "A false B false",
|
||||
aMatched: false,
|
||||
ruleSetBMatch: false,
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
logicalRule := &abstractLogicalRule{
|
||||
mode: C.LogicalTypeAnd,
|
||||
rules: []adapter.HeadlessRule{
|
||||
newSingleItemRule(testCase.aMatched),
|
||||
newRuleSetOnlyRule(testCase.ruleSetBMatch, true),
|
||||
},
|
||||
}
|
||||
require.Equal(t, testCase.expected, logicalRule.Match(&adapter.InboundContext{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -35,12 +35,12 @@ import (
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
ctx context.Context
|
||||
options *option.V2RayXHTTPOptions
|
||||
getRequestURL func(sessionId string) url.URL
|
||||
getRequestURL2 func(sessionId string) url.URL
|
||||
getHTTPClient func() (DialerClient, *XmuxClient)
|
||||
getHTTPClient2 func() (DialerClient, *XmuxClient)
|
||||
ctx context.Context
|
||||
options *option.V2RayXHTTPOptions
|
||||
baseRequestURL url.URL
|
||||
baseRequestURL2 url.URL
|
||||
getHTTPClient func() (DialerClient, *XmuxClient)
|
||||
getHTTPClient2 func() (DialerClient, *XmuxClient)
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayXHTTPOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) {
|
||||
@@ -48,17 +48,10 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
|
||||
return nil, E.New("mode is not set")
|
||||
}
|
||||
dest := serverAddr
|
||||
baseRequestURL, err := getBaseRequestURL(
|
||||
&options.V2RayXHTTPBaseOptions, dest, tlsConfig,
|
||||
)
|
||||
baseRequestURL, err := getBaseRequestURL(&options.V2RayXHTTPBaseOptions, dest, tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
getRequestURL := func(sessionId string) url.URL {
|
||||
requestURL := baseRequestURL
|
||||
requestURL.Path += sessionId
|
||||
return requestURL
|
||||
}
|
||||
var xmuxOptions option.V2RayXHTTPXmuxOptions
|
||||
if options.Xmux != nil {
|
||||
xmuxOptions = *options.Xmux
|
||||
@@ -70,7 +63,7 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
|
||||
xmuxClient := xmuxManager.GetXmuxClient(ctx)
|
||||
return xmuxClient.XmuxConn.(DialerClient), xmuxClient
|
||||
}
|
||||
getRequestURL2 := getRequestURL
|
||||
baseRequestURL2 := baseRequestURL
|
||||
getHTTPClient2 := getHTTPClient
|
||||
if options.Download != nil {
|
||||
options2 := options.Download
|
||||
@@ -90,15 +83,10 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
baseRequestURL2, err := getBaseRequestURL(&options2.V2RayXHTTPBaseOptions, dest2, tlsConfig2)
|
||||
baseRequestURL2, err = getBaseRequestURL(&options2.V2RayXHTTPBaseOptions, dest2, tlsConfig2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
getRequestURL2 = func(sessionId string) url.URL {
|
||||
requestURL2 := baseRequestURL2
|
||||
requestURL2.Path += sessionId
|
||||
return requestURL2
|
||||
}
|
||||
var xmuxOptions2 option.V2RayXHTTPXmuxOptions
|
||||
if options2.Xmux != nil {
|
||||
xmuxOptions2 = *options2.Xmux
|
||||
@@ -112,21 +100,25 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
|
||||
}
|
||||
}
|
||||
return &Client{
|
||||
ctx: ctx,
|
||||
options: &options,
|
||||
getHTTPClient: getHTTPClient,
|
||||
getHTTPClient2: getHTTPClient2,
|
||||
getRequestURL: getRequestURL,
|
||||
getRequestURL2: getRequestURL2,
|
||||
ctx: ctx,
|
||||
options: &options,
|
||||
getHTTPClient: getHTTPClient,
|
||||
getHTTPClient2: getHTTPClient2,
|
||||
baseRequestURL: baseRequestURL,
|
||||
baseRequestURL2: baseRequestURL2,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
|
||||
options := c.options
|
||||
mode := c.options.Mode
|
||||
sessionIdUuid := uuid.New()
|
||||
requestURL := c.getRequestURL(sessionIdUuid.String())
|
||||
requestURL2 := c.getRequestURL2(sessionIdUuid.String())
|
||||
sessionId := ""
|
||||
if c.options.Mode != "stream-one" {
|
||||
sessionIdUuid := uuid.New()
|
||||
sessionId = sessionIdUuid.String()
|
||||
}
|
||||
requestURL := c.baseRequestURL
|
||||
requestURL2 := c.baseRequestURL2
|
||||
httpClient, xmuxClient := c.getHTTPClient()
|
||||
httpClient2, xmuxClient2 := c.getHTTPClient2()
|
||||
if xmuxClient != nil {
|
||||
@@ -157,7 +149,7 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
|
||||
if xmuxClient != nil {
|
||||
xmuxClient.LeftRequests.Add(-1)
|
||||
}
|
||||
conn.reader, conn.remoteAddr, conn.localAddr, err = httpClient.OpenStream(ctx, requestURL.String(), reader, false)
|
||||
conn.reader, conn.remoteAddr, conn.localAddr, err = httpClient.OpenStream(ctx, requestURL.String(), sessionId, reader, false)
|
||||
if err != nil { // browser dialer only
|
||||
return nil, err
|
||||
}
|
||||
@@ -166,7 +158,7 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
|
||||
if xmuxClient2 != nil {
|
||||
xmuxClient2.LeftRequests.Add(-1)
|
||||
}
|
||||
conn.reader, conn.remoteAddr, conn.localAddr, err = httpClient2.OpenStream(ctx, requestURL2.String(), nil, false)
|
||||
conn.reader, conn.remoteAddr, conn.localAddr, err = httpClient2.OpenStream(ctx, requestURL2.String(), sessionId, nil, false)
|
||||
if err != nil { // browser dialer only
|
||||
return nil, err
|
||||
}
|
||||
@@ -175,7 +167,7 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
|
||||
if xmuxClient != nil {
|
||||
xmuxClient.LeftRequests.Add(-1)
|
||||
}
|
||||
_, _, _, err = httpClient.OpenStream(ctx, requestURL.String(), reader, true)
|
||||
_, _, _, err = httpClient.OpenStream(ctx, requestURL.String(), sessionId, reader, true)
|
||||
if err != nil { // browser dialer only
|
||||
return nil, err
|
||||
}
|
||||
@@ -209,7 +201,7 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
|
||||
// this intentionally makes a shallow-copy of the struct so we
|
||||
// can reassign Path (potentially concurrently)
|
||||
url := requestURL
|
||||
url.Path += "/" + strconv.FormatInt(seq, 10)
|
||||
seqStr := strconv.FormatInt(seq, 10)
|
||||
seq += 1
|
||||
if scMinPostsIntervalMs.From > 0 {
|
||||
time.Sleep(time.Duration(scMinPostsIntervalMs.Rand())*time.Millisecond - time.Since(lastWrite))
|
||||
@@ -230,6 +222,8 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
|
||||
err := httpClient.PostPacket(
|
||||
ctx,
|
||||
url.String(),
|
||||
sessionId,
|
||||
seqStr,
|
||||
&buf.MultiBufferContainer{MultiBuffer: chunk},
|
||||
int64(chunk.Len()),
|
||||
)
|
||||
|
||||
@@ -3,14 +3,16 @@ package xhttp
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/common/xray"
|
||||
common "github.com/sagernet/sing-box/common/xray"
|
||||
"github.com/sagernet/sing-box/common/xray/signal/done"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
@@ -19,11 +21,11 @@ import (
|
||||
type DialerClient interface {
|
||||
IsClosed() bool
|
||||
|
||||
// ctx, url, body, uploadOnly
|
||||
OpenStream(context.Context, string, io.Reader, bool) (io.ReadCloser, net.Addr, net.Addr, error)
|
||||
// ctx, url, sessionId, body, uploadOnly
|
||||
OpenStream(context.Context, string, string, io.Reader, bool) (io.ReadCloser, net.Addr, net.Addr, error)
|
||||
|
||||
// ctx, url, body, contentLength
|
||||
PostPacket(context.Context, string, io.Reader, int64) error
|
||||
// ctx, url, sessionId, seqStr, body, contentLength
|
||||
PostPacket(context.Context, string, string, string, io.Reader, int64) error
|
||||
}
|
||||
|
||||
// implements xhttp.DialerClient in terms of direct network connections
|
||||
@@ -41,7 +43,7 @@ func (c *DefaultDialerClient) IsClosed() bool {
|
||||
return c.closed
|
||||
}
|
||||
|
||||
func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body io.Reader, uploadOnly bool) (wrc io.ReadCloser, remoteAddr, localAddr net.Addr, err error) {
|
||||
func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessionId string, body io.Reader, uploadOnly bool) (wrc io.ReadCloser, remoteAddr, localAddr net.Addr, err error) {
|
||||
// this is done when the TCP/UDP connection to the server was established,
|
||||
// and we can unblock the Dial function and print correct net addresses in
|
||||
// logs
|
||||
@@ -55,11 +57,31 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body i
|
||||
})
|
||||
method := "GET" // stream-down
|
||||
if body != nil {
|
||||
method = "POST" // stream-up/one
|
||||
method = c.options.GetNormalizedUplinkHTTPMethod() // stream-up/one
|
||||
}
|
||||
req, _ := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body)
|
||||
req.Header = c.options.GetRequestHeader(url)
|
||||
if method == "POST" && !c.options.NoGRPCHeader {
|
||||
req.Header = c.options.GetRequestHeader()
|
||||
length := int(c.options.GetNormalizedXPaddingBytes().Rand())
|
||||
config := XPaddingConfig{Length: length}
|
||||
if c.options.XPaddingObfsMode {
|
||||
config.Placement = XPaddingPlacement{
|
||||
Placement: c.options.XPaddingPlacement,
|
||||
Key: c.options.XPaddingKey,
|
||||
Header: c.options.XPaddingHeader,
|
||||
RawURL: url,
|
||||
}
|
||||
config.Method = PaddingMethod(c.options.XPaddingMethod)
|
||||
} else {
|
||||
config.Placement = XPaddingPlacement{
|
||||
Placement: option.PlacementQueryInHeader,
|
||||
Key: "x_padding",
|
||||
Header: "Referer",
|
||||
RawURL: url,
|
||||
}
|
||||
}
|
||||
ApplyXPaddingToRequest(req, config)
|
||||
ApplyMetaToRequest(c.options, req, sessionId, "")
|
||||
if method == c.options.GetNormalizedUplinkHTTPMethod() && !c.options.NoGRPCHeader {
|
||||
req.Header.Set("Content-Type", "application/grpc")
|
||||
}
|
||||
wrc = &WaitReadCloser{Wait: make(chan struct{})}
|
||||
@@ -85,13 +107,76 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body i
|
||||
return
|
||||
}
|
||||
|
||||
func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, body io.Reader, contentLength int64) error {
|
||||
req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), "POST", url, body)
|
||||
func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, sessionId string, seqStr string, body io.Reader, contentLength int64) error {
|
||||
var encodedData string
|
||||
dataPlacement := c.options.GetNormalizedUplinkDataPlacement()
|
||||
if dataPlacement != option.PlacementBody {
|
||||
data, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encodedData = base64.RawURLEncoding.EncodeToString(data)
|
||||
body = nil
|
||||
contentLength = 0
|
||||
}
|
||||
method := c.options.GetNormalizedUplinkHTTPMethod()
|
||||
req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.ContentLength = contentLength
|
||||
req.Header = c.options.GetRequestHeader(url)
|
||||
req.Header = c.options.GetRequestHeader()
|
||||
if dataPlacement != option.PlacementBody {
|
||||
key := c.options.UplinkDataKey
|
||||
chunkSize := int(c.options.UplinkChunkSize)
|
||||
switch dataPlacement {
|
||||
case option.PlacementHeader:
|
||||
for i := 0; i < len(encodedData); i += chunkSize {
|
||||
end := i + chunkSize
|
||||
if end > len(encodedData) {
|
||||
end = len(encodedData)
|
||||
}
|
||||
chunk := encodedData[i:end]
|
||||
headerKey := fmt.Sprintf("%s-%d", key, i/chunkSize)
|
||||
req.Header.Set(headerKey, chunk)
|
||||
}
|
||||
|
||||
req.Header.Set(key+"-Length", fmt.Sprintf("%d", len(encodedData)))
|
||||
req.Header.Set(key+"-Upstream", "1")
|
||||
case option.PlacementCookie:
|
||||
for i := 0; i < len(encodedData); i += chunkSize {
|
||||
end := i + chunkSize
|
||||
if end > len(encodedData) {
|
||||
end = len(encodedData)
|
||||
}
|
||||
chunk := encodedData[i:end]
|
||||
cookieName := fmt.Sprintf("%s_%d", key, i/chunkSize)
|
||||
req.AddCookie(&http.Cookie{Name: cookieName, Value: chunk})
|
||||
}
|
||||
|
||||
req.AddCookie(&http.Cookie{Name: key + "_upstream", Value: "1"})
|
||||
}
|
||||
}
|
||||
length := int(c.options.GetNormalizedXPaddingBytes().Rand())
|
||||
config := XPaddingConfig{Length: length}
|
||||
if c.options.XPaddingObfsMode {
|
||||
config.Placement = XPaddingPlacement{
|
||||
Placement: c.options.XPaddingPlacement,
|
||||
Key: c.options.XPaddingKey,
|
||||
Header: c.options.XPaddingHeader,
|
||||
RawURL: url,
|
||||
}
|
||||
config.Method = PaddingMethod(c.options.XPaddingMethod)
|
||||
} else {
|
||||
config.Placement = XPaddingPlacement{
|
||||
Placement: option.PlacementQueryInHeader,
|
||||
Key: "x_padding",
|
||||
Header: "Referer",
|
||||
RawURL: url,
|
||||
}
|
||||
}
|
||||
ApplyXPaddingToRequest(req, config)
|
||||
ApplyMetaToRequest(c.options, req, sessionId, seqStr)
|
||||
if c.httpVersion != "1.1" {
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
@@ -150,7 +235,6 @@ func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, body i
|
||||
}
|
||||
c.uploadRawPool.Put(uploadConn)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -190,3 +274,45 @@ func (w *WaitReadCloser) Close() error {
|
||||
close(w.Wait)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ApplyMetaToRequest(options *option.V2RayXHTTPBaseOptions, req *http.Request, sessionId string, seqStr string) {
|
||||
sessionPlacement := options.GetNormalizedSessionPlacement()
|
||||
seqPlacement := options.GetNormalizedSeqPlacement()
|
||||
sessionKey := options.GetNormalizedSessionKey()
|
||||
seqKey := options.GetNormalizedSeqKey()
|
||||
if sessionId != "" {
|
||||
switch sessionPlacement {
|
||||
case option.PlacementPath:
|
||||
req.URL.Path = appendToPath(req.URL.Path, sessionId)
|
||||
case option.PlacementQuery:
|
||||
q := req.URL.Query()
|
||||
q.Set(sessionKey, sessionId)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
case option.PlacementHeader:
|
||||
req.Header.Set(sessionKey, sessionId)
|
||||
case option.PlacementCookie:
|
||||
req.AddCookie(&http.Cookie{Name: sessionKey, Value: sessionId})
|
||||
}
|
||||
}
|
||||
if seqStr != "" {
|
||||
switch seqPlacement {
|
||||
case option.PlacementPath:
|
||||
req.URL.Path = appendToPath(req.URL.Path, seqStr)
|
||||
case option.PlacementQuery:
|
||||
q := req.URL.Query()
|
||||
q.Set(seqKey, seqStr)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
case option.PlacementHeader:
|
||||
req.Header.Set(seqKey, seqStr)
|
||||
case option.PlacementCookie:
|
||||
req.AddCookie(&http.Cookie{Name: seqKey, Value: seqStr})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func appendToPath(path, value string) string {
|
||||
if strings.HasSuffix(path, "/") {
|
||||
return path + value
|
||||
}
|
||||
return path + "/" + value
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ package xhttp
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -17,13 +18,11 @@ import (
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/common/xray/signal/done"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
qtls "github.com/sagernet/sing-quic"
|
||||
|
||||
// qtls "github.com/sagernet/sing-quic"
|
||||
"github.com/sagernet/sing-box/common/xray/signal/done"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
@@ -99,29 +98,31 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
return
|
||||
}
|
||||
writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
writer.Header().Set("Access-Control-Allow-Methods", "GET, POST")
|
||||
writer.Header().Set("X-Padding", strings.Repeat("X", int(s.options.GetNormalizedXPaddingBytes().Rand())))
|
||||
validRange := s.options.GetNormalizedXPaddingBytes()
|
||||
paddingLength := 0
|
||||
referrer := request.Header.Get("Referer")
|
||||
if referrer != "" {
|
||||
if referrerURL, err := url.Parse(referrer); err == nil {
|
||||
// Browser dialer cannot control the host part of referrer header, so only check the query
|
||||
paddingLength = len(referrerURL.Query().Get("x_padding"))
|
||||
writer.Header().Set("Access-Control-Allow-Methods", "*")
|
||||
length := int(s.options.GetNormalizedXPaddingBytes().Rand())
|
||||
config := XPaddingConfig{Length: length}
|
||||
if s.options.XPaddingObfsMode {
|
||||
config.Placement = XPaddingPlacement{
|
||||
Placement: s.options.XPaddingPlacement,
|
||||
Key: s.options.XPaddingKey,
|
||||
Header: s.options.XPaddingHeader,
|
||||
}
|
||||
config.Method = PaddingMethod(s.options.XPaddingMethod)
|
||||
} else {
|
||||
paddingLength = len(request.URL.Query().Get("x_padding"))
|
||||
config.Placement = XPaddingPlacement{
|
||||
Placement: option.PlacementHeader,
|
||||
Header: "X-Padding",
|
||||
}
|
||||
}
|
||||
if int32(paddingLength) < validRange.From || int32(paddingLength) > validRange.To {
|
||||
s.logger.ErrorContext(request.Context(), "invalid x_padding length:", int32(paddingLength))
|
||||
ApplyXPaddingToHeader(writer.Header(), config)
|
||||
validRange := s.options.GetNormalizedXPaddingBytes()
|
||||
paddingValue, paddingPlacement := ExtractXPaddingFromRequest(&s.options.V2RayXHTTPBaseOptions, request, s.options.XPaddingObfsMode)
|
||||
if !IsPaddingValid(&s.options.V2RayXHTTPBaseOptions, paddingValue, validRange.From, validRange.To, PaddingMethod(s.options.XPaddingMethod)) {
|
||||
s.logger.ErrorContext(request.Context(), "invalid padding ("+paddingPlacement+") length:", int32(len(paddingValue)))
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sessionId := ""
|
||||
subpath := strings.Split(request.URL.Path[len(s.path):], "/")
|
||||
if len(subpath) > 0 {
|
||||
sessionId = subpath[0]
|
||||
}
|
||||
sessionId, seqStr := ExtractMetaFromRequest(s.options, request, s.path)
|
||||
if sessionId == "" && s.options.Mode != "" && s.options.Mode != "auto" && s.options.Mode != "stream-one" && s.options.Mode != "stream-up" {
|
||||
s.logger.ErrorContext(request.Context(), "stream-one mode is not allowed")
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
@@ -154,12 +155,25 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
currentSession = s.upsertSession(sessionId)
|
||||
}
|
||||
scMaxEachPostBytes := int(s.options.GetNormalizedScMaxEachPostBytes().To)
|
||||
if request.Method == "POST" && sessionId != "" { // stream-up, packet-up
|
||||
seq := ""
|
||||
if len(subpath) > 1 {
|
||||
seq = subpath[1]
|
||||
uplinkHTTPMethod := s.options.GetNormalizedUplinkHTTPMethod()
|
||||
isUplinkRequest := false
|
||||
if uplinkHTTPMethod != "GET" && request.Method == uplinkHTTPMethod {
|
||||
isUplinkRequest = true
|
||||
}
|
||||
uplinkDataPlacement := s.options.GetNormalizedUplinkDataPlacement()
|
||||
uplinkDataKey := s.options.UplinkDataKey
|
||||
switch uplinkDataPlacement {
|
||||
case option.PlacementHeader:
|
||||
if request.Header.Get(uplinkDataKey+"-Upstream") == "1" {
|
||||
isUplinkRequest = true
|
||||
}
|
||||
if seq == "" {
|
||||
case option.PlacementCookie:
|
||||
if c, _ := request.Cookie(uplinkDataKey + "_upstream"); c != nil && c.Value == "1" {
|
||||
isUplinkRequest = true
|
||||
}
|
||||
}
|
||||
if isUplinkRequest && sessionId != "" { // stream-up, packet-up
|
||||
if seqStr == "" {
|
||||
if s.options.Mode != "" && s.options.Mode != "auto" && s.options.Mode != "stream-up" {
|
||||
s.logger.ErrorContext(request.Context(), "stream-up mode is not allowed")
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
@@ -181,6 +195,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.Header().Set("Cache-Control", "no-store")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
scStreamUpServerSecs := s.options.GetNormalizedScStreamUpServerSecs()
|
||||
referrer := request.Header.Get("Referer")
|
||||
if referrer != "" && scStreamUpServerSecs.To > 0 {
|
||||
go func() {
|
||||
for {
|
||||
@@ -205,7 +220,55 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
payload, err := io.ReadAll(io.LimitReader(request.Body, int64(scMaxEachPostBytes)+1))
|
||||
var payload []byte
|
||||
if uplinkDataPlacement != option.PlacementBody {
|
||||
var encodedStr string
|
||||
switch uplinkDataPlacement {
|
||||
case option.PlacementHeader:
|
||||
dataLenStr := request.Header.Get(uplinkDataKey + "-Length")
|
||||
if dataLenStr != "" {
|
||||
dataLen, _ := strconv.Atoi(dataLenStr)
|
||||
var chunks []string
|
||||
i := 0
|
||||
for {
|
||||
chunk := request.Header.Get(fmt.Sprintf("%s-%d", uplinkDataKey, i))
|
||||
if chunk == "" {
|
||||
break
|
||||
}
|
||||
chunks = append(chunks, chunk)
|
||||
i++
|
||||
}
|
||||
encodedStr = strings.Join(chunks, "")
|
||||
if len(encodedStr) != dataLen {
|
||||
encodedStr = ""
|
||||
}
|
||||
}
|
||||
case option.PlacementCookie:
|
||||
var chunks []string
|
||||
i := 0
|
||||
for {
|
||||
cookieName := fmt.Sprintf("%s_%d", uplinkDataKey, i)
|
||||
if c, _ := request.Cookie(cookieName); c != nil {
|
||||
chunks = append(chunks, c.Value)
|
||||
i++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(chunks) > 0 {
|
||||
encodedStr = strings.Join(chunks, "")
|
||||
}
|
||||
}
|
||||
if encodedStr != "" {
|
||||
payload, err = base64.RawURLEncoding.DecodeString(encodedStr)
|
||||
} else {
|
||||
s.logger.ErrorContext(request.Context(), err, "failed to extract data from key "+uplinkDataKey+" placed in "+uplinkDataPlacement)
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
payload, err = io.ReadAll(io.LimitReader(request.Body, int64(scMaxEachPostBytes)+1))
|
||||
}
|
||||
if len(payload) > scMaxEachPostBytes {
|
||||
s.logger.ErrorContext(request.Context(), "Too large upload. scMaxEachPostBytes is set to ", scMaxEachPostBytes, "but request size exceed it. Adjust scMaxEachPostBytes on the server to be at least as large as client.")
|
||||
writer.WriteHeader(http.StatusRequestEntityTooLarge)
|
||||
@@ -216,7 +279,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
seqInt, err := strconv.ParseUint(seq, 10, 64)
|
||||
seq, err := strconv.ParseUint(seqStr, 10, 64)
|
||||
if err != nil {
|
||||
s.logger.InfoContext(request.Context(), err, "failed to upload (ParseUint)")
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
@@ -224,7 +287,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
err = currentSession.uploadQueue.Push(Packet{
|
||||
Payload: payload,
|
||||
Seq: seqInt,
|
||||
Seq: seq,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.InfoContext(request.Context(), err, "failed to upload (PushPayload)")
|
||||
@@ -352,3 +415,41 @@ func (s *Server) upsertSession(sessionId string) *httpSession {
|
||||
}()
|
||||
return session
|
||||
}
|
||||
|
||||
func ExtractMetaFromRequest(options *option.V2RayXHTTPOptions, req *http.Request, path string) (sessionId string, seqStr string) {
|
||||
sessionPlacement := options.GetNormalizedSessionPlacement()
|
||||
seqPlacement := options.GetNormalizedSeqPlacement()
|
||||
sessionKey := options.GetNormalizedSessionKey()
|
||||
seqKey := options.GetNormalizedSeqKey()
|
||||
if sessionPlacement == option.PlacementPath && seqPlacement == option.PlacementPath {
|
||||
subpath := strings.Split(req.URL.Path[len(path):], "/")
|
||||
if len(subpath) > 0 {
|
||||
sessionId = subpath[0]
|
||||
}
|
||||
if len(subpath) > 1 {
|
||||
seqStr = subpath[1]
|
||||
}
|
||||
return sessionId, seqStr
|
||||
}
|
||||
switch sessionPlacement {
|
||||
case option.PlacementQuery:
|
||||
sessionId = req.URL.Query().Get(sessionKey)
|
||||
case option.PlacementHeader:
|
||||
sessionId = req.Header.Get(sessionKey)
|
||||
case option.PlacementCookie:
|
||||
if cookie, e := req.Cookie(sessionKey); e == nil {
|
||||
sessionId = cookie.Value
|
||||
}
|
||||
}
|
||||
switch seqPlacement {
|
||||
case option.PlacementQuery:
|
||||
seqStr = req.URL.Query().Get(seqKey)
|
||||
case option.PlacementHeader:
|
||||
seqStr = req.Header.Get(seqKey)
|
||||
case option.PlacementCookie:
|
||||
if cookie, e := req.Cookie(seqKey); e == nil {
|
||||
seqStr = cookie.Value
|
||||
}
|
||||
}
|
||||
return sessionId, seqStr
|
||||
}
|
||||
|
||||
268
transport/v2rayxhttp/xpadding.go
Normal file
268
transport/v2rayxhttp/xpadding.go
Normal file
@@ -0,0 +1,268 @@
|
||||
package xhttp
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"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 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 ApplyXPaddingToHeader(h http.Header, config XPaddingConfig) {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
paddingValue := GeneratePadding(config.Method, config.Length)
|
||||
switch p := config.Placement; p.Placement {
|
||||
case option.PlacementHeader:
|
||||
h.Set(p.Header, paddingValue)
|
||||
case option.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 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 == option.PlacementHeader || placement == option.PlacementQueryInHeader {
|
||||
ApplyXPaddingToHeader(req.Header, config)
|
||||
return
|
||||
}
|
||||
paddingValue := GeneratePadding(config.Method, config.Length)
|
||||
switch placement {
|
||||
case option.PlacementCookie:
|
||||
ApplyPaddingToCookie(req, config.Placement.Key, paddingValue)
|
||||
case option.PlacementQuery:
|
||||
ApplyPaddingToQuery(req.URL, config.Placement.Key, paddingValue)
|
||||
}
|
||||
}
|
||||
|
||||
func ExtractXPaddingFromRequest(options *option.V2RayXHTTPBaseOptions, 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 := option.PlacementQueryInHeader + "=Referer, key=x_padding"
|
||||
return paddingValue, paddingPlacement
|
||||
}
|
||||
} else {
|
||||
paddingValue := req.URL.Query().Get("x_padding")
|
||||
return paddingValue, option.PlacementQuery + ", key=x_padding"
|
||||
}
|
||||
}
|
||||
key := options.XPaddingKey
|
||||
header := options.XPaddingHeader
|
||||
if cookie, err := req.Cookie(key); err == nil {
|
||||
if cookie != nil && cookie.Value != "" {
|
||||
paddingValue := cookie.Value
|
||||
paddingPlacement := option.PlacementCookie + ", key=" + key
|
||||
return paddingValue, paddingPlacement
|
||||
}
|
||||
}
|
||||
headerValue := req.Header.Get(header)
|
||||
if headerValue != "" {
|
||||
if options.XPaddingPlacement == option.PlacementHeader {
|
||||
paddingPlacement := option.PlacementHeader + "=" + header
|
||||
return headerValue, paddingPlacement
|
||||
}
|
||||
|
||||
if parsedURL, err := url.Parse(headerValue); err == nil {
|
||||
paddingPlacement := option.PlacementQueryInHeader + "=" + header + ", key=" + key
|
||||
|
||||
return parsedURL.Query().Get(key), paddingPlacement
|
||||
}
|
||||
}
|
||||
queryValue := req.URL.Query().Get(key)
|
||||
if queryValue != "" {
|
||||
paddingPlacement := option.PlacementQuery + ", key=" + key
|
||||
return queryValue, paddingPlacement
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func IsPaddingValid(options *option.V2RayXHTTPBaseOptions, paddingValue string, from, to int32, method PaddingMethod) bool {
|
||||
if paddingValue == "" {
|
||||
return false
|
||||
}
|
||||
if to <= 0 {
|
||||
r := options.GetNormalizedXPaddingBytes()
|
||||
from, to = r.From, r.To
|
||||
}
|
||||
switch method {
|
||||
case PaddingMethodRepeatX:
|
||||
n := int32(len(paddingValue))
|
||||
return n >= from && n <= to
|
||||
case PaddingMethodTokenish:
|
||||
const tolerance = int32(validationTolerance)
|
||||
n := int32(hpack.HuffmanEncodeLength(paddingValue))
|
||||
f := from - tolerance
|
||||
t := to + tolerance
|
||||
if f < 0 {
|
||||
f = 0
|
||||
}
|
||||
return n >= f && n <= t
|
||||
default:
|
||||
n := int32(len(paddingValue))
|
||||
return n >= from && n <= to
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ func (w *systemDevice) Start() error {
|
||||
w.options.Logger.Info("started at ", w.options.Name)
|
||||
w.device = tunInterface
|
||||
batchTUN, isBatchTUN := tunInterface.(tun.LinuxTUN)
|
||||
if isBatchTUN {
|
||||
if isBatchTUN && batchTUN.BatchSize() > 1 {
|
||||
w.batchDevice = batchTUN
|
||||
}
|
||||
w.events <- wgTun.EventUp
|
||||
|
||||
Reference in New Issue
Block a user