Compare commits

...

125 Commits

Author SHA1 Message Date
世界
0be9d7a208 documentation: Bump version 2024-08-20 21:14:11 +08:00
世界
6576d27c2a Introduce SSH sniffer 2024-08-20 21:10:16 +08:00
世界
30bea9f93c Temporarily set -checklinkname=0 to fix tfo-go not not being allowed to build with go1.23 2024-08-20 21:10:16 +08:00
世界
bab8856609 Write close error to log 2024-08-20 21:10:16 +08:00
世界
c3203e0d86 Add AdGuard DNS filter support 2024-08-20 21:10:16 +08:00
世界
e7ab92267e Improve QUIC sniffer 2024-08-20 21:10:05 +08:00
世界
0b5b6ac4cb Add inline rule-set & Add reload for local rule-set 2024-08-20 21:10:05 +08:00
世界
7e16e91477 Unique rule-set names 2024-08-20 21:10:05 +08:00
世界
933d6087bc Add accept empty DNS rule option 2024-08-20 21:10:05 +08:00
世界
0064506cdf Add custom options for TUN auto-route and auto-redirect 2024-08-20 21:10:05 +08:00
世界
df630b9a25 Improve base DNS transports & Minor fixes 2024-08-20 21:10:04 +08:00
世界
bd159d40fa Add auto-redirect & Improve auto-route 2024-08-20 21:10:04 +08:00
世界
0dc816cc40 Add rule-set decompile command 2024-08-20 21:10:04 +08:00
世界
2415ad0043 Add IP address support for rule-set match match 2024-08-20 21:10:03 +08:00
世界
5fe644717f Improve usages of json.Unmarshal 2024-08-20 21:10:03 +08:00
世界
4dbd42f4ec Bump rule-set version 2024-08-20 21:10:03 +08:00
世界
3c2af5a489 Implement read deadline for QUIC based UDP inbounds 2024-08-20 21:10:03 +08:00
世界
4d7b1f48fd WTF is this 2024-08-20 21:10:02 +08:00
世界
d9f3d5d46a platform: Fix clash server reload on android 2024-08-20 21:10:02 +08:00
世界
42ba5184c0 platform: Add log update interval 2024-08-20 21:10:02 +08:00
世界
813eb57a68 platform: Prepare connections list 2024-08-20 21:10:01 +08:00
世界
cd8588f70c Drop support for go1.18 and go1.19 2024-08-20 21:10:01 +08:00
世界
b0881ce873 Introduce DTLS sniffer 2024-08-20 21:10:01 +08:00
iosmanthus
668b3e3373 Introduce bittorrent related protocol sniffers
* Introduce bittorrent related protocol sniffers

including, sniffers of
1. BitTorrent Protocol (TCP)
2. uTorrent Transport Protocol (UDP)

Signed-off-by: iosmanthus <myosmanthustree@gmail.com>
Co-authored-by: 世界 <i@sekai.icu>
2024-08-20 21:10:01 +08:00
世界
064fb9b873 Fix direct dialer not resolving domain 2024-08-20 21:02:52 +08:00
世界
f6a1e123fc Make linter happy 2024-08-19 18:42:02 +08:00
世界
3066dfe3b3 Bump version 2024-08-19 10:43:09 +08:00
Liu Bingyan
1128fdd8c7 documentation: Update injectable description 2024-08-19 07:40:31 +08:00
世界
cfd9879b17 documentation: Remove unused 2024-08-19 06:39:33 +08:00
世界
9ceb660c57 documentation: Announce that our apps on Apple platforms are no longer available 2024-08-19 06:39:33 +08:00
世界
7d00d7df28 Update quic-go to v0.46.0 2024-08-19 06:25:15 +08:00
世界
21b1ac26b9 Fix UDP conn stuck on sniff
This change only avoids permanent hangs. We need to implement read deadlines for UDP conns in 1.10 for server inbounds.
2024-08-18 11:27:12 +08:00
世界
7fec8d842e Update golangci-lint configuration 2024-08-18 11:17:01 +08:00
世界
07c678fb85 Fix crash on Android when using process rules 2024-08-18 10:23:28 +08:00
世界
baecfc7778 Update quic-go to v0.45.2 2024-08-18 10:23:17 +08:00
世界
07de36ecdb Fix tests 2024-08-03 12:46:38 +08:00
世界
2c8a8303cd dns: Minor fixes 2024-07-27 11:04:57 +08:00
ruokeqx
e5991cae0b Use unix.SysctlRaw for macOS 2024-07-22 12:42:22 +08:00
世界
1349acfd5a Fix panic caused by possible generation of duplicate keys for domain_suffix 2024-07-17 16:02:17 +08:00
世界
98ff897f35 Fix reset outbounds 2024-07-13 17:46:22 +08:00
ayooh
6144c8e340 Fix default end value of the rangeItem 2024-07-13 17:45:32 +08:00
HystericalDragon
c8caac9f67 Fix v2rayquic default NextProtos (#1934) 2024-07-11 23:34:09 +08:00
printfer
81e9eda357 documentation: Fix typo 2024-07-07 16:09:41 +08:00
世界
7cba3da108 shadowsocks: Fix packet process for AEAD multi-user server 2024-07-05 17:09:09 +08:00
世界
82d06b43e7 vless: Fix missing deadline interfaces 2024-07-05 17:08:10 +08:00
世界
a7ac91f573 Move legacy vless implemetation to library 2024-07-03 20:17:32 +08:00
世界
0540a95a43 Fix v2ray-plugin 2024-07-02 14:08:17 +08:00
世界
94707dfcdd Add name to errors from v2ray HTTP transports 2024-07-02 14:08:17 +08:00
世界
8a17043502 Fix command output for rule-set match 2024-06-26 12:26:35 +08:00
世界
b0aaa86806 Minor fixes 2024-06-25 13:14:45 +08:00
世界
8a2d3fbb28 Update quic-go to v0.45.1 & Filter HTTPS ipv4/6hint 2024-06-24 11:07:32 +08:00
renovate[bot]
4652019608 [dependencies] Update docker/build-push-action action to v6 2024-06-24 10:24:13 +08:00
世界
06fa5abf63 Fix non-IP queries accepted by address filter rules 2024-06-24 10:13:16 +08:00
世界
996fbbf0c3 docs: Switch to venv 2024-06-24 10:10:49 +08:00
renovate[bot]
142ff1b455 [dependencies] Update actions/checkout digest to 692973e 2024-06-17 13:05:54 +08:00
世界
74d662f7a3 Fix always create package manager 2024-06-11 21:16:57 +08:00
世界
085f603377 Bump version 2024-06-09 13:20:56 +08:00
世界
460fae83dc Fix quic-go is caching dialErr since v0.43.0 2024-06-09 13:15:40 +08:00
世界
bb9bd9bff6 Fix package manager start order 2024-06-09 07:21:50 +08:00
世界
c2354ebf25 Bump version 2024-06-08 22:00:46 +08:00
世界
c1f4755c4e Fix parse UDP DNS server with addr:port address 2024-06-08 22:00:46 +08:00
世界
0ca5909b06 Stop passing device sleep events on Android and Apple platforms 2024-06-08 22:00:46 +08:00
世界
e77a8114c5 Fix rule-set start order 2024-06-08 22:00:46 +08:00
renovate[bot]
f1393235ff [dependencies] Update actions/checkout digest to a5ac7e5 2024-06-08 22:00:46 +08:00
renovate[bot]
bdba2365de [dependencies] Update goreleaser/goreleaser-action action to v6 2024-06-08 18:58:22 +08:00
世界
ce0da5b557 Fix typo 2024-06-08 18:58:14 +08:00
世界
3853201412 Bump version 2024-06-07 19:07:46 +08:00
世界
7003ef40a3 Fix wireguard start 2024-06-07 19:07:46 +08:00
世界
59ec92228c Fix repeatedly no route logs 2024-06-07 15:03:48 +08:00
世界
0eeb2da323 Fix reset HTTP3 DNS transport 2024-06-06 22:28:43 +08:00
世界
977b0fac02 Fix get source address from X-Forwarded-For 2024-06-06 22:21:37 +08:00
世界
51964801ff Fix enforced power listener on windows 2024-06-06 22:20:23 +08:00
世界
e08c052fc9 Fix wireguard client bind 2024-06-06 22:20:21 +08:00
世界
53927d8bbd Remove logs in router initialize 2024-06-06 22:20:19 +08:00
世界
968b9bc217 Fix crash on *bsd 2024-06-06 22:20:17 +08:00
lgjint
69dc87aa6d Fix set KDE6 system proxy 2024-06-06 22:20:14 +08:00
Fei1Yang
4193df375f build: Remove vendor in RPM packages 2024-05-27 19:28:15 +08:00
世界
5ff7006326 Bump version 2024-05-25 11:30:43 +08:00
世界
a89107ea9d documentation: Bump version 2024-05-23 14:57:45 +08:00
世界
9ffdbba2ed documentation: Add manuel for mitigating tunnelvision attacks 2024-05-23 14:57:27 +08:00
世界
65c71049ea documentation: Update DNS manual 2024-05-23 14:57:26 +08:00
世界
7d4e6a7f4e dialer: Allow nil router 2024-05-23 14:57:26 +08:00
世界
d612620c5d Add rule-set match command 2024-05-23 14:57:26 +08:00
世界
8a9a77a438 Add bypass_domain and search_domain platform HTTP proxy options 2024-05-23 14:57:26 +08:00
世界
a2098c18e1 Handle includeAllNetworks 2024-05-23 14:57:26 +08:00
世界
cf2181dd3a Update gVisor to 20240422.0 2024-05-23 14:57:15 +08:00
世界
5899e95ff1 Update quic-go to v0.43.1 2024-05-21 15:12:05 +08:00
世界
d7160c19cf Fixed order for Clash modes 2024-05-21 15:12:05 +08:00
世界
da9e22b4e6 Add custom prefix support in EDNS0 client subnet options 2024-05-21 15:12:05 +08:00
气息
0e120f8a44 Fix DNS exchange index
Signed-off-by: 气息 <qdshizh@gmail.com>
2024-05-21 15:12:05 +08:00
dyhkwong
d918863ac5 Always disable cache for fake-ip servers 2024-05-21 15:12:04 +08:00
PuerNya
2ae192305c Always disable cache for fake-ip DNS transport if independent_cache disabled 2024-05-21 15:12:03 +08:00
世界
71d1879bd6 Fix missing rule_set_ipcidr_match_source item in DNS rules 2024-05-21 15:12:03 +08:00
世界
917514e09f Improve DNS truncate behavior 2024-05-21 15:12:03 +08:00
世界
5327aeaea4 Fix DNS fallthrough incorrectly 2024-05-21 15:12:03 +08:00
世界
93ae3f7a1e Add rejected DNS response cache support 2024-05-21 15:12:03 +08:00
世界
f24a2aed7d Add support for client-subnet DNS options 2024-05-21 15:12:03 +08:00
世界
0517ceef76 Add address filter support for DNS rules 2024-05-21 15:12:02 +08:00
世界
830ea46932 Fix timezone for Android and iOS 2024-05-21 15:11:52 +08:00
世界
cd0fcd5ddc Improve loopback detector 2024-05-21 15:11:52 +08:00
世界
003176f069 Remove unused fakeip packet conn 2024-05-21 15:11:52 +08:00
世界
71d92518c1 Set the default TCP keep alive period 2024-05-21 15:11:52 +08:00
世界
b5dcd6bf59 Migrate ntp service to library 2024-05-21 15:11:52 +08:00
世界
11c7b4a866 Handle Windows power events 2024-05-21 15:11:52 +08:00
世界
ee14135298 Improve domain suffix match behavior
For historical reasons, sing-box's `domain_suffix` rule matches literal prefixes instead of the same as other projects.

This change modifies the behavior of `domain_suffix`: If the rule value is prefixed with `.`,
the behavior is unchanged, otherwise it matches `(domain|.+\.domain)` instead.
2024-05-21 15:11:41 +08:00
世界
cbcf005f37 Remove PROCESS_NAME_NATIVE dwFlag in process query output
The `process_path` rule of sing-box is inherited from Clash,
the original code uses the local system's path format (e.g. `\Device\HarddiskVolume1\folder\program.exe`),
but when the device has multiple disks, the HarddiskVolume serial number is not stable.

This change make QueryFullProcessImageNameW output a Win32 path (such as `C:\folder\program.exe`),
which will disrupt the existing `process_path` use cases in Windows.
2024-05-18 17:22:14 +08:00
世界
daee0b154e badtls: Support uTLS and TLS ECH for read waiter 2024-05-18 17:22:14 +08:00
世界
d530c724c0 Bump version 2024-05-18 16:53:05 +08:00
世界
7f698c1104 Fix hysteria2 panic 2024-05-18 16:20:32 +08:00
renovate[bot]
7a4a44c6d2 [dependencies] Update golangci/golangci-lint-action action to v6 2024-05-10 19:49:20 +08:00
世界
44277e5dd2 Fix urltest logic 2024-05-10 17:41:20 +08:00
世界
1f470c69c4 Update docker workflow 2024-05-03 19:33:20 +08:00
世界
742adacce7 Bump version 2024-05-03 17:38:26 +08:00
世界
32e1d5a5e2 documentation: Update package status 2024-05-03 17:38:26 +08:00
世界
cb9f4ce597 Minor fixes 2024-05-03 15:34:47 +08:00
世界
4b1a6185ba Fix DNF repo 2024-04-29 23:13:04 +08:00
世界
8d85c92356 Fix multiplex client dialer context 2024-04-29 11:58:20 +08:00
世界
c6164c9eca Fix usage of github actions 2024-04-29 11:58:20 +08:00
dyhkwong
3c85b8bc48 Fix fake-ip mapping 2024-04-29 11:58:20 +08:00
HystericalDragon
8b8fb4344c Remove unused encoder 2024-04-29 11:58:20 +08:00
renovate[bot]
e85a38e059 [dependencies] Update golangci/golangci-lint-action action to v5 2024-04-29 11:58:20 +08:00
renovate[bot]
f3ac91673a [dependencies] Update actions/checkout digest to 0ad4b8f 2024-04-29 11:58:20 +08:00
世界
0f1e58b917 documentation: Update TestFlight 2024-04-29 11:58:20 +08:00
世界
c4cfe24aef documentation: Fix strict_route description 2024-04-29 11:58:20 +08:00
世界
3d73b159ba Fix linux workflow 2024-04-29 11:58:20 +08:00
257 changed files with 7909 additions and 5215 deletions

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
with:
fetch-depth: 0
- name: Setup Go
@@ -33,32 +33,12 @@ jobs:
- name: Run Test
run: |
go test -v ./...
build_go118:
name: Debug build (Go 1.18)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.18
- name: Cache go module
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
key: go118-${{ hashFiles('**/go.sum') }}
- name: Run Test
run: make ci_build_go118
build_go120:
name: Debug build (Go 1.20)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
with:
fetch-depth: 0
- name: Setup Go
@@ -78,7 +58,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
with:
fetch-depth: 0
- name: Setup Go
@@ -208,7 +188,7 @@ jobs:
TAGS: with_clash_api,with_quic
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
with:
fetch-depth: 0
- name: Setup Go

View File

@@ -4,13 +4,35 @@ on:
release:
types:
- released
workflow_dispatch:
inputs:
tag:
description: "The tag version you want to build"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Get commit to build
id: ref
run: |-
if [[ -z "${{ github.event.inputs.tag }}" ]]; then
ref="${{ github.ref_name }}"
else
ref="${{ github.event.inputs.tag }}"
fi
echo "ref=$ref"
echo "ref=$ref" >> $GITHUB_OUTPUT
if [[ $ref == *"-"* ]]; then
latest=latest-beta
else
latest=latest
fi
echo "latest=$latest"
echo "latest=$latest" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
with:
ref: ${{ steps.ref.outputs.ref }}
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Setup QEMU for Docker Buildx
@@ -27,13 +49,14 @@ jobs:
with:
images: ghcr.io/sagernet/sing-box
- name: Build and release Docker images
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
platforms: linux/386,linux/amd64,linux/arm64,linux/s390x
context: .
target: dist
build-args: |
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
tags: |
ghcr.io/sagernet/sing-box:latest
ghcr.io/sagernet/sing-box:${{ github.ref_name }}
ghcr.io/sagernet/sing-box:${{ steps.ref.outputs.latest }}
ghcr.io/sagernet/sing-box:${{ steps.ref.outputs.ref }}
push: true

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
with:
fetch-depth: 0
- name: Setup Go
@@ -30,7 +30,7 @@ jobs:
with:
go-version: ^1.22
- name: golangci-lint
uses: golangci/golangci-lint-action@v4
uses: golangci/golangci-lint-action@v6
with:
version: latest
args: --timeout=30m

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
with:
fetch-depth: 0
- name: Setup Go
@@ -22,9 +22,11 @@ jobs:
mkdir -p $HOME/.gnupg
cat > $HOME/.gnupg/sagernet.key <<EOF
${{ secrets.GPG_KEY }}
echo "HOME=$HOME" >> "$GITHUB_ENV"
EOF
echo "HOME=$HOME" >> "$GITHUB_ENV"
- name: Publish release
uses: goreleaser/goreleaser-action@v5
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
version: latest
@@ -33,5 +35,5 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
NFPM_KEY_PATH: ${{ env.Home }}/.gnupg/sagernet.key
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}

2
.gitignore vendored
View File

@@ -14,3 +14,5 @@
/*.xcframework/
.DS_Store
/config.d/
/venv/

View File

@@ -6,14 +6,7 @@ linters:
- gci
- staticcheck
- paralleltest
run:
skip-dirs:
- transport/simple-obfs
- transport/clashssr
- transport/cloudflaretls
- transport/shadowtls/tls
- transport/shadowtls/tls_go119
- ineffassign
linters-settings:
gci:
@@ -23,4 +16,13 @@ linters-settings:
- prefix(github.com/sagernet/)
- default
staticcheck:
go: '1.20'
checks:
- all
- -SA1003
run:
go: "1.23"
issues:
exclude-dirs:
- transport/simple-obfs

View File

@@ -36,7 +36,6 @@ nfpms:
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
builds:
- main
vendor: sagernet
homepage: https://sing-box.sagernet.org/
maintainer: nekohasekai <contact-git@sekai.icu>
description: The universal proxy platform.

View File

@@ -7,7 +7,10 @@ builds:
- -v
- -trimpath
ldflags:
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
- -s
- -buildid=
- -checklinkname=0
tags:
- with_gvisor
- with_quic
@@ -46,6 +49,10 @@ builds:
- with_reality_server
- with_acme
- with_clash_api
ldflags:
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
- -s
- -buildid=
env:
- CGO_ENABLED=0
- GOROOT={{ .Env.GOPATH }}/go1.20.14
@@ -113,7 +120,6 @@ nfpms:
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
builds:
- main
vendor: sagernet
homepage: https://sing-box.sagernet.org/
maintainer: nekohasekai <contact-git@sekai.icu>
description: The universal proxy platform.

View File

@@ -1,7 +1,6 @@
NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD)
TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api
TAGS_GO120 = with_quic,with_utls
TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls
TAGS_GO121 = with_ech
TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121)
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
@@ -20,13 +19,9 @@ PREFIX ?= $(shell go env GOPATH)
build:
go build $(MAIN_PARAMS) $(MAIN)
ci_build_go118:
go build $(PARAMS) $(MAIN)
go build $(PARAMS) -tags "$(TAGS_GO118)" $(MAIN)
ci_build_go120:
go build $(PARAMS) $(MAIN)
go build $(PARAMS) -tags "$(TAGS_GO118),$(TAGS_GO120)" $(MAIN)
go build $(PARAMS) -tags "$(TAGS_GO120)" $(MAIN)
ci_build:
go build $(PARAMS) $(MAIN)
@@ -197,13 +192,15 @@ lib_install:
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.3
docs:
mkdocs serve
venv/bin/mkdocs serve
publish_docs:
mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
venv/bin/mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
docs_install:
pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*"
python -m venv venv
source ./venv/bin/activate && pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*"
clean:
rm -rf bin dist sing-box
rm -f $(shell go env GOPATH)/sing-box

View File

@@ -4,10 +4,6 @@ The universal proxy platform.
[![Packaging status](https://repology.org/badge/vertical-allrepos/sing-box.svg)](https://repology.org/project/sing-box/versions)
## Documentation
https://sing-box.sagernet.org
## Support
https://community.sagernet.org/c/sing-box/

View File

@@ -4,13 +4,13 @@ import (
"bytes"
"context"
"encoding/binary"
"io"
"net"
"time"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-dns"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/varbin"
)
type ClashServer interface {
@@ -30,6 +30,9 @@ type CacheFile interface {
StoreFakeIP() bool
FakeIPStorage
StoreRDRC() bool
dns.RDRCStore
LoadMode() string
StoreMode(mode string) error
LoadSelected(group string) string
@@ -52,16 +55,15 @@ func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
if err != nil {
return nil, err
}
err = rw.WriteUVariant(&buffer, uint64(len(s.Content)))
err = varbin.Write(&buffer, binary.BigEndian, s.Content)
if err != nil {
return nil, err
}
buffer.Write(s.Content)
err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix())
if err != nil {
return nil, err
}
err = rw.WriteVString(&buffer, s.LastEtag)
err = varbin.Write(&buffer, binary.BigEndian, s.LastEtag)
if err != nil {
return nil, err
}
@@ -75,12 +77,7 @@ func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
if err != nil {
return err
}
contentLen, err := rw.ReadUVariant(reader)
if err != nil {
return err
}
s.Content = make([]byte, contentLen)
_, err = io.ReadFull(reader, s.Content)
err = varbin.Read(reader, binary.BigEndian, &s.Content)
if err != nil {
return err
}
@@ -90,7 +87,7 @@ func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
return err
}
s.LastUpdated = time.Unix(lastUpdated, 0)
s.LastEtag, err = rw.ReadVString(reader)
err = varbin.Read(reader, binary.BigEndian, &s.LastEtag)
if err != nil {
return err
}

View File

@@ -31,11 +31,16 @@ type InboundContext struct {
Network string
Source M.Socksaddr
Destination M.Socksaddr
Domain string
Protocol string
User string
Outbound string
// sniffer
Protocol string
Domain string
Client string
SniffContext any
// cache
InboundDetour string
@@ -51,19 +56,25 @@ type InboundContext struct {
// rule cache
IPCIDRMatchSource bool
SourceAddressMatch bool
SourcePortMatch bool
DestinationAddressMatch bool
DestinationPortMatch bool
IPCIDRMatchSource bool
IPCIDRAcceptEmpty bool
SourceAddressMatch bool
SourcePortMatch bool
DestinationAddressMatch bool
DestinationPortMatch bool
DidMatch bool
IgnoreDestinationIPCIDRMatch bool
}
func (c *InboundContext) ResetRuleCache() {
c.IPCIDRMatchSource = false
c.IPCIDRAcceptEmpty = false
c.SourceAddressMatch = false
c.SourcePortMatch = false
c.DestinationAddressMatch = false
c.DestinationPortMatch = false
c.DidMatch = false
}
type inboundContextKey struct{}
@@ -96,3 +107,12 @@ func ExtendContext(ctx context.Context) (context.Context, *InboundContext) {
}
return WithContext(ctx, &newMetadata), &newMetadata
}
func OverrideContext(ctx context.Context) context.Context {
if metadata := ContextFrom(ctx); metadata != nil {
var newMetadata InboundContext
newMetadata = *metadata
return WithContext(ctx, &newMetadata)
}
return ctx
}

View File

@@ -10,15 +10,18 @@ import (
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service"
mdns "github.com/miekg/dns"
"go4.org/netipx"
)
type Router interface {
Service
PreStarter
PostStarter
Cleanup() error
Outbounds() []Outbound
Outbound(tag string) (Outbound, bool)
@@ -45,7 +48,9 @@ type Router interface {
DefaultInterface() string
AutoDetectInterface() bool
AutoDetectInterfaceFunc() control.Func
DefaultMark() int
DefaultMark() uint32
RegisterAutoRedirectOutputMark(mark uint32) error
AutoRedirectOutputMark() uint32
NetworkMonitor() tun.NetworkUpdateMonitor
InterfaceMonitor() tun.DefaultInterfaceMonitor
PackageManager() tun.PackageManager
@@ -71,6 +76,7 @@ func RouterFromContext(ctx context.Context) Router {
type HeadlessRule interface {
Match(metadata *InboundContext) bool
String() string
}
type Rule interface {
@@ -79,26 +85,38 @@ type Rule interface {
Type() string
UpdateGeosite() error
Outbound() string
String() string
}
type DNSRule interface {
Rule
DisableCache() bool
RewriteTTL() *uint32
ClientSubnet() *netip.Prefix
WithAddressLimit() bool
MatchAddressLimit(metadata *InboundContext) bool
}
type RuleSet interface {
Name() string
StartContext(ctx context.Context, startContext RuleSetStartContext) error
PostStart() error
Metadata() RuleSetMetadata
ExtractIPSet() []*netipx.IPSet
IncRef()
DecRef()
Cleanup()
RegisterCallback(callback RuleSetUpdateCallback) *list.Element[RuleSetUpdateCallback]
UnregisterCallback(element *list.Element[RuleSetUpdateCallback])
Close() error
HeadlessRule
}
type RuleSetUpdateCallback func(it RuleSet)
type RuleSetMetadata struct {
ContainsProcessRule bool
ContainsWIFIRule bool
ContainsIPCIDRRule bool
}
type RuleSetStartContext interface {

View File

@@ -22,4 +22,5 @@ type V2RayServerTransportHandler interface {
type V2RayClientTransport interface {
DialContext(ctx context.Context) (net.Conn, error)
Close() error
}

35
box.go
View File

@@ -111,6 +111,7 @@ func New(options Options) (*Box, error) {
ctx,
router,
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
tag,
inboundOptions,
options.PlatformInterface,
)
@@ -203,7 +204,7 @@ func (s *Box) PreStart() error {
defer func() {
v := recover()
if v != nil {
log.Error(E.Cause(err, "origin error"))
println(err.Error())
debug.PrintStack()
panic("panic on early close: " + fmt.Sprint(v))
}
@@ -222,9 +223,9 @@ func (s *Box) Start() error {
defer func() {
v := recover()
if v != nil {
log.Error(E.Cause(err, "origin error"))
println(err.Error())
debug.PrintStack()
panic("panic on early close: " + fmt.Sprint(v))
println("panic on early start: " + fmt.Sprint(v))
}
}()
s.Close()
@@ -302,7 +303,11 @@ func (s *Box) start() error {
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
}
}
return s.postStart()
err = s.postStart()
if err != nil {
return err
}
return s.router.Cleanup()
}
func (s *Box) postStart() error {
@@ -312,16 +317,28 @@ func (s *Box) postStart() error {
return E.Cause(err, "start ", serviceName)
}
}
for _, outbound := range s.outbounds {
if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound {
// TODO: reorganize ALL start order
for _, out := range s.outbounds {
if lateOutbound, isLateOutbound := out.(adapter.PostStarter); isLateOutbound {
err := lateOutbound.PostStart()
if err != nil {
return E.Cause(err, "post-start outbound/", outbound.Tag())
return E.Cause(err, "post-start outbound/", out.Tag())
}
}
}
return s.router.PostStart()
err := s.router.PostStart()
if err != nil {
return err
}
for _, in := range s.inbounds {
if lateInbound, isLateInbound := in.(adapter.PostStarter); isLateInbound {
err = lateInbound.PostStart()
if err != nil {
return E.Cause(err, "post-start inbound/", in.Tag())
}
}
}
return nil
}
func (s *Box) Close() error {

View File

@@ -45,7 +45,9 @@ func (s *Box) startOutbounds() error {
}
started[outboundTag] = true
canContinue = true
if starter, isStarter := outboundToStart.(common.Starter); isStarter {
if starter, isStarter := outboundToStart.(interface {
Start() error
}); isStarter {
monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
err := starter.Start()
monitor.Finish()

View File

@@ -93,7 +93,7 @@ func buildAndroid() {
const name = "libbox.aar"
copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs")
if rw.FileExists(copyPath) {
if rw.IsDir(copyPath) {
copyPath, _ = filepath.Abs(copyPath)
err = rw.CopyFile(name, filepath.Join(copyPath, name))
if err != nil {
@@ -134,7 +134,7 @@ func buildiOS() {
}
copyPath := filepath.Join("..", "sing-box-for-apple")
if rw.FileExists(copyPath) {
if rw.IsDir(copyPath) {
targetDir := filepath.Join(copyPath, "Libbox.xcframework")
targetDir, _ = filepath.Abs(targetDir)
os.RemoveAll(targetDir)

View File

@@ -30,7 +30,7 @@ func FindSDK() {
}
for _, path := range searchPath {
path = os.ExpandEnv(path)
if rw.FileExists(filepath.Join(path, "licenses", "android-sdk-license")) {
if rw.IsFile(filepath.Join(path, "licenses", "android-sdk-license")) {
androidSDKPath = path
break
}
@@ -60,7 +60,7 @@ func FindSDK() {
func findNDK() bool {
const fixedVersion = "26.2.11394342"
const versionFile = "source.properties"
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.FileExists(filepath.Join(fixedPath, versionFile)) {
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
androidNDKPath = fixedPath
return true
}
@@ -86,7 +86,7 @@ func findNDK() bool {
})
for _, versionName := range versionNames {
currentNDKPath := filepath.Join(androidSDKPath, "ndk", versionName)
if rw.FileExists(filepath.Join(androidSDKPath, versionFile)) {
if rw.IsFile(filepath.Join(androidSDKPath, versionFile)) {
androidNDKPath = currentNDKPath
log.Warn("reproducibility warning: using NDK version " + versionName + " instead of " + fixedVersion)
return true
@@ -100,11 +100,11 @@ var GoBinPath string
func FindMobile() {
goBin := filepath.Join(build.Default.GOPATH, "bin")
if runtime.GOOS == "windows" {
if !rw.FileExists(filepath.Join(goBin, "gobind.exe")) {
if !rw.IsFile(filepath.Join(goBin, "gobind.exe")) {
log.Fatal("missing gomobile installation")
}
} else {
if !rw.FileExists(filepath.Join(goBin, "gobind")) {
if !rw.IsFile(filepath.Join(goBin, "gobind")) {
log.Fatal("missing gomobile installation")
}
}

View File

@@ -87,7 +87,7 @@ func geoipExport(countryCode string) error {
headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String())
}
var plainRuleSet option.PlainRuleSetCompat
plainRuleSet.Version = C.RuleSetVersion1
plainRuleSet.Version = C.RuleSetVersion2
plainRuleSet.Options.Rules = []option.HeadlessRule{
{
Type: C.RuleTypeDefault,

View File

@@ -70,7 +70,7 @@ func geositeExport(category string) error {
headlessRule.DomainKeyword = defaultRule.DomainKeyword
headlessRule.DomainRegex = defaultRule.DomainRegex
var plainRuleSet option.PlainRuleSetCompat
plainRuleSet.Version = C.RuleSetVersion1
plainRuleSet.Version = C.RuleSetVersion2
plainRuleSet.Options.Rules = []option.HeadlessRule{
{
Type: C.RuleTypeDefault,

View File

@@ -54,7 +54,11 @@ func merge(outputPath string) error {
return nil
}
}
err = rw.WriteFile(outputPath, buffer.Bytes())
err = rw.MkdirParent(outputPath)
if err != nil {
return err
}
err = os.WriteFile(outputPath, buffer.Bytes(), 0o644)
if err != nil {
return err
}

View File

@@ -6,7 +6,7 @@ import (
var commandRuleSet = &cobra.Command{
Use: "rule-set",
Short: "Manage rule sets",
Short: "Manage rule-sets",
}
func init() {

View File

@@ -6,6 +6,7 @@ import (
"strings"
"github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json"
@@ -55,10 +56,10 @@ func compileRuleSet(sourcePath string) error {
if err != nil {
return err
}
ruleSet, err := plainRuleSet.Upgrade()
if err != nil {
return err
}
ruleSet := plainRuleSet.Upgrade()
var outputPath string
if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".json") {
@@ -73,7 +74,7 @@ func compileRuleSet(sourcePath string) error {
if err != nil {
return err
}
err = srs.Write(outputFile, ruleSet)
err = srs.Write(outputFile, ruleSet, plainRuleSet.Version == C.RuleSetVersion2)
if err != nil {
outputFile.Close()
os.Remove(outputPath)

View File

@@ -0,0 +1,88 @@
package main
import (
"io"
"os"
"strings"
"github.com/sagernet/sing-box/cmd/sing-box/internal/convertor/adguard"
"github.com/sagernet/sing-box/common/srs"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/spf13/cobra"
)
var (
flagRuleSetConvertType string
flagRuleSetConvertOutput string
)
var commandRuleSetConvert = &cobra.Command{
Use: "convert [source-path]",
Short: "Convert adguard DNS filter to rule-set",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := convertRuleSet(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandRuleSet.AddCommand(commandRuleSetConvert)
commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertType, "type", "t", "", "Source type, available: adguard")
commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file")
}
func convertRuleSet(sourcePath string) error {
var (
reader io.Reader
err error
)
if sourcePath == "stdin" {
reader = os.Stdin
} else {
reader, err = os.Open(sourcePath)
if err != nil {
return err
}
}
var rules []option.HeadlessRule
switch flagRuleSetConvertType {
case "adguard":
rules, err = adguard.Convert(reader)
case "":
return E.New("source type is required")
default:
return E.New("unsupported source type: ", flagRuleSetConvertType)
}
if err != nil {
return err
}
var outputPath string
if flagRuleSetConvertOutput == flagRuleSetCompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".txt") {
outputPath = sourcePath[:len(sourcePath)-4] + ".srs"
} else {
outputPath = sourcePath + ".srs"
}
} else {
outputPath = flagRuleSetConvertOutput
}
outputFile, err := os.Create(outputPath)
if err != nil {
return err
}
defer outputFile.Close()
err = srs.Write(outputFile, option.PlainRuleSet{Rules: rules}, true)
if err != nil {
outputFile.Close()
os.Remove(outputPath)
return err
}
outputFile.Close()
return nil
}

View File

@@ -0,0 +1,83 @@
package main
import (
"io"
"os"
"strings"
"github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json"
"github.com/spf13/cobra"
)
var flagRuleSetDecompileOutput string
const flagRuleSetDecompileDefaultOutput = "<file_name>.json"
var commandRuleSetDecompile = &cobra.Command{
Use: "decompile [binary-path]",
Short: "Decompile rule-set binary to json",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := decompileRuleSet(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandRuleSet.AddCommand(commandRuleSetDecompile)
commandRuleSetDecompile.Flags().StringVarP(&flagRuleSetDecompileOutput, "output", "o", flagRuleSetDecompileDefaultOutput, "Output file")
}
func decompileRuleSet(sourcePath string) error {
var (
reader io.Reader
err error
)
if sourcePath == "stdin" {
reader = os.Stdin
} else {
reader, err = os.Open(sourcePath)
if err != nil {
return err
}
}
plainRuleSet, err := srs.Read(reader, true)
if err != nil {
return err
}
ruleSet := option.PlainRuleSetCompat{
Version: C.RuleSetVersion1,
Options: plainRuleSet,
}
var outputPath string
if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".srs") {
outputPath = sourcePath[:len(sourcePath)-4] + ".json"
} else {
outputPath = sourcePath + ".json"
}
} else {
outputPath = flagRuleSetDecompileOutput
}
outputFile, err := os.Create(outputPath)
if err != nil {
return err
}
encoder := json.NewEncoder(outputFile)
encoder.SetIndent("", " ")
err = encoder.Encode(ruleSet)
if err != nil {
outputFile.Close()
os.Remove(outputPath)
return err
}
outputFile.Close()
return nil
}

View File

@@ -0,0 +1,96 @@
package main
import (
"bytes"
"io"
"os"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/route"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json"
M "github.com/sagernet/sing/common/metadata"
"github.com/spf13/cobra"
)
var flagRuleSetMatchFormat string
var commandRuleSetMatch = &cobra.Command{
Use: "match <rule-set path> <IP address/domain>",
Short: "Check if an IP address or a domain matches the rule-set",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
err := ruleSetMatch(args[0], args[1])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandRuleSetMatch.Flags().StringVarP(&flagRuleSetMatchFormat, "format", "f", "source", "rule-set format")
commandRuleSet.AddCommand(commandRuleSetMatch)
}
func ruleSetMatch(sourcePath string, domain string) error {
var (
reader io.Reader
err error
)
if sourcePath == "stdin" {
reader = os.Stdin
} else {
reader, err = os.Open(sourcePath)
if err != nil {
return E.Cause(err, "read rule-set")
}
}
content, err := io.ReadAll(reader)
if err != nil {
return E.Cause(err, "read rule-set")
}
var plainRuleSet option.PlainRuleSet
switch flagRuleSetMatchFormat {
case C.RuleSetFormatSource:
var compat option.PlainRuleSetCompat
compat, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content)
if err != nil {
return err
}
plainRuleSet, err = compat.Upgrade()
if err != nil {
return err
}
case C.RuleSetFormatBinary:
plainRuleSet, err = srs.Read(bytes.NewReader(content), false)
if err != nil {
return err
}
default:
return E.New("unknown rule-set format: ", flagRuleSetMatchFormat)
}
ipAddress := M.ParseAddr(domain)
var metadata adapter.InboundContext
if ipAddress.IsValid() {
metadata.Destination = M.SocksaddrFrom(ipAddress, 0)
} else {
metadata.Domain = domain
}
for i, ruleOptions := range plainRuleSet.Rules {
var currentRule adapter.HeadlessRule
currentRule, err = route.NewHeadlessRule(nil, ruleOptions)
if err != nil {
return E.Cause(err, "parse rule_set.rules.[", i, "]")
}
if currentRule.Match(&metadata) {
println(F.ToString("match rules.[", i, "]: ", currentRule))
}
}
return nil
}

View File

@@ -0,0 +1,94 @@
package main
import (
"bytes"
"io"
"os"
"path/filepath"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/spf13/cobra"
)
var commandRuleSetUpgradeFlagWrite bool
var commandRuleSetUpgrade = &cobra.Command{
Use: "upgrade <source-path>",
Short: "Upgrade rule-set json",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := upgradeRuleSet(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandRuleSetUpgrade.Flags().BoolVarP(&commandRuleSetUpgradeFlagWrite, "write", "w", false, "write result to (source) file instead of stdout")
commandRuleSet.AddCommand(commandRuleSetUpgrade)
}
func upgradeRuleSet(sourcePath string) error {
var (
reader io.Reader
err error
)
if sourcePath == "stdin" {
reader = os.Stdin
} else {
reader, err = os.Open(sourcePath)
if err != nil {
return err
}
}
content, err := io.ReadAll(reader)
if err != nil {
return err
}
plainRuleSetCompat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content)
if err != nil {
return err
}
switch plainRuleSetCompat.Version {
case C.RuleSetVersion1:
default:
log.Info("already up-to-date")
return nil
}
plainRuleSet, err := plainRuleSetCompat.Upgrade()
if err != nil {
return err
}
buffer := new(bytes.Buffer)
encoder := json.NewEncoder(buffer)
encoder.SetIndent("", " ")
err = encoder.Encode(plainRuleSet)
if err != nil {
return E.Cause(err, "encode config")
}
outputPath, _ := filepath.Abs(sourcePath)
if !commandRuleSetUpgradeFlagWrite || sourcePath == "stdin" {
os.Stdout.WriteString(buffer.String() + "\n")
return nil
}
if bytes.Equal(content, buffer.Bytes()) {
return nil
}
output, err := os.Create(sourcePath)
if err != nil {
return E.Cause(err, "open output")
}
_, err = output.Write(buffer.Bytes())
output.Close()
if err != nil {
return E.Cause(err, "write output")
}
os.Stderr.WriteString(outputPath + "\n")
return nil
}

View File

@@ -109,7 +109,7 @@ func readConfigAndMerge() (option.Options, error) {
}
var mergedMessage json.RawMessage
for _, options := range optionsList {
mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage)
mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage, false)
if err != nil {
return option.Options{}, E.Cause(err, "merge config at ", options.path)
}
@@ -188,9 +188,12 @@ func run() error {
cancel()
closeCtx, closed := context.WithCancel(context.Background())
go closeMonitor(closeCtx)
instance.Close()
err = instance.Close()
closed()
if osSignal != syscall.SIGHUP {
if err != nil {
log.Error(E.Cause(err, "sing-box did not closed properly"))
}
return nil
}
break

View File

@@ -9,8 +9,10 @@ import (
"net/url"
"os"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"github.com/spf13/cobra"
@@ -32,7 +34,10 @@ func init() {
commandTools.AddCommand(commandFetch)
}
var httpClient *http.Client
var (
httpClient *http.Client
http3Client *http.Client
)
func fetch(args []string) error {
instance, err := createPreStartedClient()
@@ -53,8 +58,16 @@ func fetch(args []string) error {
},
}
defer httpClient.CloseIdleConnections()
if C.WithQUIC {
err = initializeHTTP3Client(instance)
if err != nil {
return err
}
defer http3Client.CloseIdleConnections()
}
for _, urlString := range args {
parsedURL, err := url.Parse(urlString)
var parsedURL *url.URL
parsedURL, err = url.Parse(urlString)
if err != nil {
return err
}
@@ -63,16 +76,27 @@ func fetch(args []string) error {
parsedURL.Scheme = "http"
fallthrough
case "http", "https":
err = fetchHTTP(parsedURL)
err = fetchHTTP(httpClient, parsedURL)
if err != nil {
return err
}
case "http3":
if !C.WithQUIC {
return C.ErrQUICNotIncluded
}
parsedURL.Scheme = "https"
err = fetchHTTP(http3Client, parsedURL)
if err != nil {
return err
}
default:
return E.New("unsupported scheme: ", parsedURL.Scheme)
}
}
return nil
}
func fetchHTTP(parsedURL *url.URL) error {
func fetchHTTP(httpClient *http.Client, parsedURL *url.URL) error {
request, err := http.NewRequest("GET", parsedURL.String(), nil)
if err != nil {
return err

View File

@@ -0,0 +1,36 @@
//go:build with_quic
package main
import (
"context"
"crypto/tls"
"net/http"
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3"
box "github.com/sagernet/sing-box"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func initializeHTTP3Client(instance *box.Box) error {
dialer, err := createDialer(instance, N.NetworkUDP, commandToolsFlagOutbound)
if err != nil {
return err
}
http3Client = &http.Client{
Transport: &http3.RoundTripper{
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
destination := M.ParseSocksaddr(addr)
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
if dErr != nil {
return nil, dErr
}
return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsCfg, cfg)
},
},
}
return nil
}

View File

@@ -0,0 +1,18 @@
//go:build !with_quic
package main
import (
"net/url"
"os"
box "github.com/sagernet/sing-box"
)
func initializeHTTP3Client(instance *box.Box) error {
return os.ErrInvalid
}
func fetchHTTP3(parsedURL *url.URL) error {
return os.ErrInvalid
}

View File

@@ -0,0 +1,346 @@
package adguard
import (
"bufio"
"io"
"net/netip"
"os"
"strconv"
"strings"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
)
type agdguardRuleLine struct {
ruleLine string
isRawDomain bool
isExclude bool
isSuffix bool
hasStart bool
hasEnd bool
isRegexp bool
isImportant bool
}
func Convert(reader io.Reader) ([]option.HeadlessRule, error) {
scanner := bufio.NewScanner(reader)
var (
ruleLines []agdguardRuleLine
ignoredLines int
)
parseLine:
for scanner.Scan() {
ruleLine := scanner.Text()
if ruleLine == "" || ruleLine[0] == '!' || ruleLine[0] == '#' {
continue
}
originRuleLine := ruleLine
if M.IsDomainName(ruleLine) {
ruleLines = append(ruleLines, agdguardRuleLine{
ruleLine: ruleLine,
isRawDomain: true,
})
continue
}
hostLine, err := parseAdGuardHostLine(ruleLine)
if err == nil {
if hostLine != "" {
ruleLines = append(ruleLines, agdguardRuleLine{
ruleLine: hostLine,
isRawDomain: true,
hasStart: true,
hasEnd: true,
})
}
continue
}
if strings.HasSuffix(ruleLine, "|") {
ruleLine = ruleLine[:len(ruleLine)-1]
}
var (
isExclude bool
isSuffix bool
hasStart bool
hasEnd bool
isRegexp bool
isImportant bool
)
if !strings.HasPrefix(ruleLine, "/") && strings.Contains(ruleLine, "$") {
params := common.SubstringAfter(ruleLine, "$")
for _, param := range strings.Split(params, ",") {
paramParts := strings.Split(param, "=")
var ignored bool
if len(paramParts) > 0 && len(paramParts) <= 2 {
switch paramParts[0] {
case "app", "network":
// maybe support by package_name/process_name
case "dnstype":
// maybe support by query_type
case "important":
ignored = true
isImportant = true
case "dnsrewrite":
if len(paramParts) == 2 && M.ParseAddr(paramParts[1]).IsUnspecified() {
ignored = true
}
}
}
if !ignored {
ignoredLines++
log.Debug("ignored unsupported rule with modifier: ", paramParts[0], ": ", ruleLine)
continue parseLine
}
}
ruleLine = common.SubstringBefore(ruleLine, "$")
}
if strings.HasPrefix(ruleLine, "@@") {
ruleLine = ruleLine[2:]
isExclude = true
}
if strings.HasSuffix(ruleLine, "|") {
ruleLine = ruleLine[:len(ruleLine)-1]
}
if strings.HasPrefix(ruleLine, "||") {
ruleLine = ruleLine[2:]
isSuffix = true
} else if strings.HasPrefix(ruleLine, "|") {
ruleLine = ruleLine[1:]
hasStart = true
}
if strings.HasSuffix(ruleLine, "^") {
ruleLine = ruleLine[:len(ruleLine)-1]
hasEnd = true
}
if strings.HasPrefix(ruleLine, "/") && strings.HasSuffix(ruleLine, "/") {
ruleLine = ruleLine[1 : len(ruleLine)-1]
if ignoreIPCIDRRegexp(ruleLine) {
ignoredLines++
log.Debug("ignored unsupported rule with IPCIDR regexp: ", ruleLine)
continue
}
isRegexp = true
} else {
if strings.Contains(ruleLine, "://") {
ruleLine = common.SubstringAfter(ruleLine, "://")
}
if strings.Contains(ruleLine, "/") {
ignoredLines++
log.Debug("ignored unsupported rule with path: ", ruleLine)
continue
}
if strings.Contains(ruleLine, "##") {
ignoredLines++
log.Debug("ignored unsupported rule with element hiding: ", ruleLine)
continue
}
if strings.Contains(ruleLine, "#$#") {
ignoredLines++
log.Debug("ignored unsupported rule with element hiding: ", ruleLine)
continue
}
var domainCheck string
if strings.HasPrefix(ruleLine, ".") || strings.HasPrefix(ruleLine, "-") {
domainCheck = "r" + ruleLine
} else {
domainCheck = ruleLine
}
if ruleLine == "" {
ignoredLines++
log.Debug("ignored unsupported rule with empty domain", originRuleLine)
continue
} else {
domainCheck = strings.ReplaceAll(domainCheck, "*", "x")
if !M.IsDomainName(domainCheck) {
_, ipErr := parseADGuardIPCIDRLine(ruleLine)
if ipErr == nil {
ignoredLines++
log.Debug("ignored unsupported rule with IPCIDR: ", ruleLine)
continue
}
if M.ParseSocksaddr(domainCheck).Port != 0 {
log.Debug("ignored unsupported rule with port: ", ruleLine)
} else {
log.Debug("ignored unsupported rule with invalid domain: ", ruleLine)
}
ignoredLines++
continue
}
}
}
ruleLines = append(ruleLines, agdguardRuleLine{
ruleLine: ruleLine,
isExclude: isExclude,
isSuffix: isSuffix,
hasStart: hasStart,
hasEnd: hasEnd,
isRegexp: isRegexp,
isImportant: isImportant,
})
}
if len(ruleLines) == 0 {
return nil, E.New("AdGuard rule-set is empty or all rules are unsupported")
}
if common.All(ruleLines, func(it agdguardRuleLine) bool {
return it.isRawDomain
}) {
return []option.HeadlessRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultHeadlessRule{
Domain: common.Map(ruleLines, func(it agdguardRuleLine) string {
return it.ruleLine
}),
},
},
}, nil
}
mapDomain := func(it agdguardRuleLine) string {
ruleLine := it.ruleLine
if it.isSuffix {
ruleLine = "||" + ruleLine
} else if it.hasStart {
ruleLine = "|" + ruleLine
}
if it.hasEnd {
ruleLine += "^"
}
return ruleLine
}
importantDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && !it.isRegexp && !it.isExclude }), mapDomain)
importantDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && it.isRegexp && !it.isExclude }), mapDomain)
importantExcludeDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && !it.isRegexp && it.isExclude }), mapDomain)
importantExcludeDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && it.isRegexp && it.isExclude }), mapDomain)
domain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && !it.isRegexp && !it.isExclude }), mapDomain)
domainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && it.isRegexp && !it.isExclude }), mapDomain)
excludeDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && !it.isRegexp && it.isExclude }), mapDomain)
excludeDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && it.isRegexp && it.isExclude }), mapDomain)
currentRule := option.HeadlessRule{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultHeadlessRule{
AdGuardDomain: domain,
DomainRegex: domainRegex,
},
}
if len(excludeDomain) > 0 || len(excludeDomainRegex) > 0 {
currentRule = option.HeadlessRule{
Type: C.RuleTypeLogical,
LogicalOptions: option.LogicalHeadlessRule{
Mode: C.LogicalTypeAnd,
Rules: []option.HeadlessRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultHeadlessRule{
AdGuardDomain: excludeDomain,
DomainRegex: excludeDomainRegex,
Invert: true,
},
},
currentRule,
},
},
}
}
if len(importantDomain) > 0 || len(importantDomainRegex) > 0 {
currentRule = option.HeadlessRule{
Type: C.RuleTypeLogical,
LogicalOptions: option.LogicalHeadlessRule{
Mode: C.LogicalTypeOr,
Rules: []option.HeadlessRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultHeadlessRule{
AdGuardDomain: importantDomain,
DomainRegex: importantDomainRegex,
},
},
currentRule,
},
},
}
}
if len(importantExcludeDomain) > 0 || len(importantExcludeDomainRegex) > 0 {
currentRule = option.HeadlessRule{
Type: C.RuleTypeLogical,
LogicalOptions: option.LogicalHeadlessRule{
Mode: C.LogicalTypeAnd,
Rules: []option.HeadlessRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultHeadlessRule{
AdGuardDomain: importantExcludeDomain,
DomainRegex: importantExcludeDomainRegex,
Invert: true,
},
},
currentRule,
},
},
}
}
log.Info("parsed rules: ", len(ruleLines), "/", len(ruleLines)+ignoredLines)
return []option.HeadlessRule{currentRule}, nil
}
func ignoreIPCIDRRegexp(ruleLine string) bool {
if strings.HasPrefix(ruleLine, "(http?:\\/\\/)") {
ruleLine = ruleLine[12:]
} else if strings.HasPrefix(ruleLine, "(https?:\\/\\/)") {
ruleLine = ruleLine[13:]
} else if strings.HasPrefix(ruleLine, "^") {
ruleLine = ruleLine[1:]
} else {
return false
}
_, parseErr := strconv.ParseUint(common.SubstringBefore(ruleLine, "\\."), 10, 8)
return parseErr == nil
}
func parseAdGuardHostLine(ruleLine string) (string, error) {
idx := strings.Index(ruleLine, " ")
if idx == -1 {
return "", os.ErrInvalid
}
address, err := netip.ParseAddr(ruleLine[:idx])
if err != nil {
return "", err
}
if !address.IsUnspecified() {
return "", nil
}
domain := ruleLine[idx+1:]
if !M.IsDomainName(domain) {
return "", E.New("invalid domain name: ", domain)
}
return domain, nil
}
func parseADGuardIPCIDRLine(ruleLine string) (netip.Prefix, error) {
var isPrefix bool
if strings.HasSuffix(ruleLine, ".") {
isPrefix = true
ruleLine = ruleLine[:len(ruleLine)-1]
}
ruleStringParts := strings.Split(ruleLine, ".")
if len(ruleStringParts) > 4 || len(ruleStringParts) < 4 && !isPrefix {
return netip.Prefix{}, os.ErrInvalid
}
ruleParts := make([]uint8, 0, len(ruleStringParts))
for _, part := range ruleStringParts {
rulePart, err := strconv.ParseUint(part, 10, 8)
if err != nil {
return netip.Prefix{}, err
}
ruleParts = append(ruleParts, uint8(rulePart))
}
bitLen := len(ruleParts) * 8
for len(ruleParts) < 4 {
ruleParts = append(ruleParts, 0)
}
return netip.PrefixFrom(netip.AddrFrom4(*(*[4]byte)(ruleParts)), bitLen), nil
}

View File

@@ -0,0 +1,140 @@
package adguard
import (
"strings"
"testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/route"
"github.com/stretchr/testify/require"
)
func TestConverter(t *testing.T) {
t.Parallel()
rules, err := Convert(strings.NewReader(`
||example.org^
|example.com^
example.net^
||example.edu
||example.edu.tw^
|example.gov
example.arpa
@@|sagernet.example.org|
||sagernet.org^$important
@@|sing-box.sagernet.org^$important
`))
require.NoError(t, err)
require.Len(t, rules, 1)
rule, err := route.NewHeadlessRule(nil, rules[0])
require.NoError(t, err)
matchDomain := []string{
"example.org",
"www.example.org",
"example.com",
"example.net",
"isexample.net",
"www.example.net",
"example.edu",
"example.edu.cn",
"example.edu.tw",
"www.example.edu",
"www.example.edu.cn",
"example.gov",
"example.gov.cn",
"example.arpa",
"www.example.arpa",
"isexample.arpa",
"example.arpa.cn",
"www.example.arpa.cn",
"isexample.arpa.cn",
"sagernet.org",
"www.sagernet.org",
}
notMatchDomain := []string{
"example.org.cn",
"notexample.org",
"example.com.cn",
"www.example.com.cn",
"example.net.cn",
"notexample.edu",
"notexample.edu.cn",
"www.example.gov",
"notexample.gov",
"sagernet.example.org",
"sing-box.sagernet.org",
}
for _, domain := range matchDomain {
require.True(t, rule.Match(&adapter.InboundContext{
Domain: domain,
}), domain)
}
for _, domain := range notMatchDomain {
require.False(t, rule.Match(&adapter.InboundContext{
Domain: domain,
}), domain)
}
}
func TestHosts(t *testing.T) {
t.Parallel()
rules, err := Convert(strings.NewReader(`
127.0.0.1 localhost
::1 localhost #[IPv6]
0.0.0.0 google.com
`))
require.NoError(t, err)
require.Len(t, rules, 1)
rule, err := route.NewHeadlessRule(nil, rules[0])
require.NoError(t, err)
matchDomain := []string{
"google.com",
}
notMatchDomain := []string{
"www.google.com",
"notgoogle.com",
"localhost",
}
for _, domain := range matchDomain {
require.True(t, rule.Match(&adapter.InboundContext{
Domain: domain,
}), domain)
}
for _, domain := range notMatchDomain {
require.False(t, rule.Match(&adapter.InboundContext{
Domain: domain,
}), domain)
}
}
func TestSimpleHosts(t *testing.T) {
t.Parallel()
rules, err := Convert(strings.NewReader(`
example.com
www.example.org
`))
require.NoError(t, err)
require.Len(t, rules, 1)
rule, err := route.NewHeadlessRule(nil, rules[0])
require.NoError(t, err)
matchDomain := []string{
"example.com",
"www.example.org",
}
notMatchDomain := []string{
"example.com.cn",
"www.example.com",
"notexample.com",
"example.org",
}
for _, domain := range matchDomain {
require.True(t, rule.Match(&adapter.InboundContext{
Domain: domain,
}), domain)
}
for _, domain := range notMatchDomain {
require.False(t, rule.Match(&adapter.InboundContext{
Domain: domain,
}), domain)
}
}

View File

@@ -4,6 +4,8 @@ package badtls
import (
"bytes"
"context"
"net"
"os"
"reflect"
"sync"
@@ -18,20 +20,32 @@ import (
var _ N.ReadWaiter = (*ReadWaitConn)(nil)
type ReadWaitConn struct {
*tls.STDConn
halfAccess *sync.Mutex
rawInput *bytes.Buffer
input *bytes.Reader
hand *bytes.Buffer
readWaitOptions N.ReadWaitOptions
tls.Conn
halfAccess *sync.Mutex
rawInput *bytes.Buffer
input *bytes.Reader
hand *bytes.Buffer
readWaitOptions N.ReadWaitOptions
tlsReadRecord func() error
tlsHandlePostHandshakeMessage func() error
}
func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) {
stdConn, isSTDConn := conn.(*tls.STDConn)
if !isSTDConn {
var (
loaded bool
tlsReadRecord func() error
tlsHandlePostHandshakeMessage func() error
)
for _, tlsCreator := range tlsRegistry {
loaded, tlsReadRecord, tlsHandlePostHandshakeMessage = tlsCreator(conn)
if loaded {
break
}
}
if !loaded {
return nil, os.ErrInvalid
}
rawConn := reflect.Indirect(reflect.ValueOf(stdConn))
rawConn := reflect.Indirect(reflect.ValueOf(conn))
rawHalfConn := rawConn.FieldByName("in")
if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct {
return nil, E.New("badtls: invalid half conn")
@@ -57,11 +71,13 @@ func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) {
}
hand := (*bytes.Buffer)(unsafe.Pointer(rawHand.UnsafeAddr()))
return &ReadWaitConn{
STDConn: stdConn,
halfAccess: halfAccess,
rawInput: rawInput,
input: input,
hand: hand,
Conn: conn,
halfAccess: halfAccess,
rawInput: rawInput,
input: input,
hand: hand,
tlsReadRecord: tlsReadRecord,
tlsHandlePostHandshakeMessage: tlsHandlePostHandshakeMessage,
}, nil
}
@@ -71,19 +87,19 @@ func (c *ReadWaitConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy
}
func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
err = c.Handshake()
err = c.HandshakeContext(context.Background())
if err != nil {
return
}
c.halfAccess.Lock()
defer c.halfAccess.Unlock()
for c.input.Len() == 0 {
err = tlsReadRecord(c.STDConn)
err = c.tlsReadRecord()
if err != nil {
return
}
for c.hand.Len() > 0 {
err = tlsHandlePostHandshakeMessage(c.STDConn)
err = c.tlsHandlePostHandshakeMessage()
if err != nil {
return
}
@@ -100,7 +116,7 @@ func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
if n != 0 && c.input.Len() == 0 && c.rawInput.Len() > 0 &&
// recordType(c.rawInput.Bytes()[0]) == recordTypeAlert {
c.rawInput.Bytes()[0] == 21 {
_ = tlsReadRecord(c.STDConn)
_ = c.tlsReadRecord()
// return n, err // will be io.EOF on closeNotify
}
@@ -109,11 +125,27 @@ func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
}
func (c *ReadWaitConn) Upstream() any {
return c.STDConn
return c.Conn
}
//go:linkname tlsReadRecord crypto/tls.(*Conn).readRecord
func tlsReadRecord(c *tls.STDConn) error
var tlsRegistry []func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error)
//go:linkname tlsHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage
func tlsHandlePostHandshakeMessage(c *tls.STDConn) error
func init() {
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
tlsConn, loaded := conn.(*tls.STDConn)
if !loaded {
return
}
return true, func() error {
return stdTLSReadRecord(tlsConn)
}, func() error {
return stdTLSHandlePostHandshakeMessage(tlsConn)
}
})
}
//go:linkname stdTLSReadRecord crypto/tls.(*Conn).readRecord
func stdTLSReadRecord(c *tls.STDConn) error
//go:linkname stdTLSHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage
func stdTLSHandlePostHandshakeMessage(c *tls.STDConn) error

View File

@@ -0,0 +1,31 @@
//go:build go1.21 && !without_badtls && with_ech
package badtls
import (
"net"
_ "unsafe"
"github.com/sagernet/cloudflare-tls"
"github.com/sagernet/sing/common"
)
func init() {
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
tlsConn, loaded := common.Cast[*tls.Conn](conn)
if !loaded {
return
}
return true, func() error {
return echReadRecord(tlsConn)
}, func() error {
return echHandlePostHandshakeMessage(tlsConn)
}
})
}
//go:linkname echReadRecord github.com/sagernet/cloudflare-tls.(*Conn).readRecord
func echReadRecord(c *tls.Conn) error
//go:linkname echHandlePostHandshakeMessage github.com/sagernet/cloudflare-tls.(*Conn).handlePostHandshakeMessage
func echHandlePostHandshakeMessage(c *tls.Conn) error

View File

@@ -0,0 +1,31 @@
//go:build go1.21 && !without_badtls && with_utls
package badtls
import (
"net"
_ "unsafe"
"github.com/sagernet/sing/common"
"github.com/sagernet/utls"
)
func init() {
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
tlsConn, loaded := common.Cast[*tls.UConn](conn)
if !loaded {
return
}
return true, func() error {
return utlsReadRecord(tlsConn.Conn)
}, func() error {
return utlsHandlePostHandshakeMessage(tlsConn.Conn)
}
})
}
//go:linkname utlsReadRecord github.com/sagernet/utls.(*Conn).readRecord
func utlsReadRecord(c *tls.Conn) error
//go:linkname utlsHandlePostHandshakeMessage github.com/sagernet/utls.(*Conn).handlePostHandshakeMessage
func utlsHandlePostHandshakeMessage(c *tls.Conn) error

View File

@@ -32,24 +32,44 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
var dialer net.Dialer
var listener net.ListenConfig
if options.BindInterface != "" {
bindFunc := control.BindToInterface(router.InterfaceFinder(), options.BindInterface, -1)
var interfaceFinder control.InterfaceFinder
if router != nil {
interfaceFinder = router.InterfaceFinder()
} else {
interfaceFinder = control.NewDefaultInterfaceFinder()
}
bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1)
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
} else if router.AutoDetectInterface() {
} else if router != nil && router.AutoDetectInterface() {
bindFunc := router.AutoDetectInterfaceFunc()
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
} else if router.DefaultInterface() != "" {
} else if router != nil && router.DefaultInterface() != "" {
bindFunc := control.BindToInterface(router.InterfaceFinder(), router.DefaultInterface(), -1)
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
}
if options.RoutingMark != 0 {
var autoRedirectOutputMark uint32
if router != nil {
autoRedirectOutputMark = router.AutoRedirectOutputMark()
}
if autoRedirectOutputMark > 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark))
}
if options.RoutingMark > 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark))
} else if router.DefaultMark() != 0 {
if autoRedirectOutputMark > 0 {
return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `routing_mark`")
}
} else if router != nil && router.DefaultMark() > 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(router.DefaultMark()))
listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark()))
if autoRedirectOutputMark > 0 {
return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `default_mark`")
}
}
if options.ReuseAddr {
listener.Control = control.Append(listener.Control, control.ReuseAddr())
@@ -63,6 +83,9 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
} else {
dialer.Timeout = C.TCPTimeout
}
// TODO: Add an option to customize the keep alive period
dialer.KeepAlive = C.TCPKeepAliveInitial
dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(C.TCPKeepAliveInitial, C.TCPKeepAliveInterval))
var udpFragment bool
if options.UDPFragment != nil {
udpFragment = *options.UDPFragment

View File

@@ -13,6 +13,9 @@ func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error)
if options.IsWireGuardListener {
return NewDefault(router, options)
}
if router == nil {
return NewDefault(nil, options)
}
var (
dialer N.Dialer
err error
@@ -25,13 +28,12 @@ func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error)
} else {
dialer = NewDetour(router, options.Detour)
}
domainStrategy := dns.DomainStrategy(options.DomainStrategy)
if domainStrategy != dns.DomainStrategyAsIS || options.Detour == "" {
if options.Detour == "" {
dialer = NewResolveDialer(
router,
dialer,
options.Detour == "" && !options.TCPFastOpen,
domainStrategy,
dns.DomainStrategy(options.DomainStrategy),
time.Duration(options.FallbackDelay))
}
return dialer, nil

View File

@@ -1,17 +1,24 @@
package geosite
import (
"bufio"
"encoding/binary"
"io"
"os"
"sync"
"sync/atomic"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/varbin"
)
type Reader struct {
reader io.ReadSeeker
domainIndex map[string]int
domainLength map[string]int
access sync.Mutex
reader io.ReadSeeker
bufferedReader *bufio.Reader
metadataIndex int64
domainIndex map[string]int
domainLength map[string]int
}
func Open(path string) (*Reader, []string, error) {
@@ -34,15 +41,23 @@ func Open(path string) (*Reader, []string, error) {
return reader, codes, nil
}
type geositeMetadata struct {
Code string
Index uint64
Length uint64
}
func (r *Reader) readMetadata() error {
version, err := rw.ReadByte(r.reader)
counter := &readCounter{Reader: r.reader}
reader := bufio.NewReader(counter)
version, err := reader.ReadByte()
if err != nil {
return err
}
if version != 0 {
return E.New("unknown version")
}
entryLength, err := rw.ReadUVariant(r.reader)
entryLength, err := binary.ReadUvarint(reader)
if err != nil {
return err
}
@@ -55,16 +70,16 @@ func (r *Reader) readMetadata() error {
codeIndex uint64
codeLength uint64
)
code, err = rw.ReadVString(r.reader)
code, err = varbin.ReadValue[string](reader, binary.BigEndian)
if err != nil {
return err
}
keys[i] = code
codeIndex, err = rw.ReadUVariant(r.reader)
codeIndex, err = binary.ReadUvarint(reader)
if err != nil {
return err
}
codeLength, err = rw.ReadUVariant(r.reader)
codeLength, err = binary.ReadUvarint(reader)
if err != nil {
return err
}
@@ -73,6 +88,8 @@ func (r *Reader) readMetadata() error {
}
r.domainIndex = domainIndex
r.domainLength = domainLength
r.metadataIndex = counter.count - int64(reader.Buffered())
r.bufferedReader = reader
return nil
}
@@ -81,31 +98,32 @@ func (r *Reader) Read(code string) ([]Item, error) {
if !exists {
return nil, E.New("code ", code, " not exists!")
}
_, err := r.reader.Seek(int64(index), io.SeekCurrent)
_, err := r.reader.Seek(r.metadataIndex+int64(index), io.SeekStart)
if err != nil {
return nil, err
}
counter := &rw.ReadCounter{Reader: r.reader}
domain := make([]Item, r.domainLength[code])
for i := range domain {
var (
item Item
err error
)
item.Type, err = rw.ReadByte(counter)
if err != nil {
return nil, err
}
item.Value, err = rw.ReadVString(counter)
if err != nil {
return nil, err
}
domain[i] = item
r.bufferedReader.Reset(r.reader)
itemList := make([]Item, r.domainLength[code])
err = varbin.Read(r.bufferedReader, binary.BigEndian, &itemList)
if err != nil {
return nil, err
}
_, err = r.reader.Seek(int64(-index)-counter.Count(), io.SeekCurrent)
return domain, err
return itemList, nil
}
func (r *Reader) Upstream() any {
return r.reader
}
type readCounter struct {
io.Reader
count int64
}
func (r *readCounter) Read(p []byte) (n int, err error) {
n, err = r.Reader.Read(p)
if n > 0 {
atomic.AddInt64(&r.count, int64(n))
}
return
}

View File

@@ -2,13 +2,13 @@ package geosite
import (
"bytes"
"io"
"encoding/binary"
"sort"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/varbin"
)
func Write(writer io.Writer, domains map[string][]Item) error {
func Write(writer varbin.Writer, domains map[string][]Item) error {
keys := make([]string, 0, len(domains))
for code := range domains {
keys = append(keys, code)
@@ -19,35 +19,32 @@ func Write(writer io.Writer, domains map[string][]Item) error {
index := make(map[string]int)
for _, code := range keys {
index[code] = content.Len()
for _, domain := range domains[code] {
content.WriteByte(domain.Type)
err := rw.WriteVString(content, domain.Value)
if err != nil {
return err
}
err := varbin.Write(content, binary.BigEndian, domains[code])
if err != nil {
return err
}
}
err := rw.WriteByte(writer, 0)
err := writer.WriteByte(0)
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(keys)))
_, err = varbin.WriteUvarint(writer, uint64(len(keys)))
if err != nil {
return err
}
for _, code := range keys {
err = rw.WriteVString(writer, code)
err = varbin.Write(writer, binary.BigEndian, code)
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(index[code]))
_, err = varbin.WriteUvarint(writer, uint64(index[code]))
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(domains[code])))
_, err = varbin.WriteUvarint(writer, uint64(len(domains[code])))
if err != nil {
return err
}

29
common/ja3/LICENSE Normal file
View File

@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2018, Open Systems AG
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

3
common/ja3/README.md Normal file
View File

@@ -0,0 +1,3 @@
# JA3
mod from: https://github.com/open-ch/ja3

31
common/ja3/error.go Normal file
View File

@@ -0,0 +1,31 @@
// Copyright (c) 2018, Open Systems AG. All rights reserved.
//
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file in the root of the source
// tree.
package ja3
import "fmt"
// Error types
const (
LengthErr string = "length check %v failed"
ContentTypeErr string = "content type not matching"
VersionErr string = "version check %v failed"
HandshakeTypeErr string = "handshake type not matching"
SNITypeErr string = "SNI type not supported"
)
// ParseError can be encountered while parsing a segment
type ParseError struct {
errType string
check int
}
func (e *ParseError) Error() string {
if e.errType == LengthErr || e.errType == VersionErr {
return fmt.Sprintf(e.errType, e.check)
}
return fmt.Sprint(e.errType)
}

83
common/ja3/ja3.go Normal file
View File

@@ -0,0 +1,83 @@
// Copyright (c) 2018, Open Systems AG. All rights reserved.
//
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file in the root of the source
// tree.
package ja3
import (
"crypto/md5"
"encoding/hex"
"golang.org/x/exp/slices"
)
type ClientHello struct {
Version uint16
CipherSuites []uint16
Extensions []uint16
EllipticCurves []uint16
EllipticCurvePF []uint8
Versions []uint16
SignatureAlgorithms []uint16
ServerName string
ja3ByteString []byte
ja3Hash string
}
func (j *ClientHello) Equals(another *ClientHello, ignoreExtensionsSequence bool) bool {
if j.Version != another.Version {
return false
}
if !slices.Equal(j.CipherSuites, another.CipherSuites) {
return false
}
if !ignoreExtensionsSequence && !slices.Equal(j.Extensions, another.Extensions) {
return false
}
if ignoreExtensionsSequence && !slices.Equal(j.Extensions, another.sortedExtensions()) {
return false
}
if !slices.Equal(j.EllipticCurves, another.EllipticCurves) {
return false
}
if !slices.Equal(j.EllipticCurvePF, another.EllipticCurvePF) {
return false
}
if !slices.Equal(j.SignatureAlgorithms, another.SignatureAlgorithms) {
return false
}
return true
}
func (j *ClientHello) sortedExtensions() []uint16 {
extensions := make([]uint16, len(j.Extensions))
copy(extensions, j.Extensions)
slices.Sort(extensions)
return extensions
}
func Compute(payload []byte) (*ClientHello, error) {
ja3 := ClientHello{}
err := ja3.parseSegment(payload)
return &ja3, err
}
func (j *ClientHello) String() string {
if j.ja3ByteString == nil {
j.marshalJA3()
}
return string(j.ja3ByteString)
}
func (j *ClientHello) Hash() string {
if j.ja3ByteString == nil {
j.marshalJA3()
}
if j.ja3Hash == "" {
h := md5.Sum(j.ja3ByteString)
j.ja3Hash = hex.EncodeToString(h[:])
}
return j.ja3Hash
}

357
common/ja3/parser.go Normal file
View File

@@ -0,0 +1,357 @@
// Copyright (c) 2018, Open Systems AG. All rights reserved.
//
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file in the root of the source
// tree.
package ja3
import (
"encoding/binary"
"strconv"
)
const (
// Constants used for parsing
recordLayerHeaderLen int = 5
handshakeHeaderLen int = 6
randomDataLen int = 32
sessionIDHeaderLen int = 1
cipherSuiteHeaderLen int = 2
compressMethodHeaderLen int = 1
extensionsHeaderLen int = 2
extensionHeaderLen int = 4
sniExtensionHeaderLen int = 5
ecExtensionHeaderLen int = 2
ecpfExtensionHeaderLen int = 1
versionExtensionHeaderLen int = 1
signatureAlgorithmsExtensionHeaderLen int = 2
contentType uint8 = 22
handshakeType uint8 = 1
sniExtensionType uint16 = 0
sniNameDNSHostnameType uint8 = 0
ecExtensionType uint16 = 10
ecpfExtensionType uint16 = 11
versionExtensionType uint16 = 43
signatureAlgorithmsExtensionType uint16 = 13
// Versions
// The bitmask covers the versions SSL3.0 to TLS1.2
tlsVersionBitmask uint16 = 0xFFFC
tls13 uint16 = 0x0304
// GREASE values
// The bitmask covers all GREASE values
GreaseBitmask uint16 = 0x0F0F
// Constants used for marshalling
dashByte = byte(45)
commaByte = byte(44)
)
// parseSegment to populate the corresponding ClientHello object or return an error
func (j *ClientHello) parseSegment(segment []byte) error {
// Check if we can decode the next fields
if len(segment) < recordLayerHeaderLen {
return &ParseError{LengthErr, 1}
}
// Check if we have "Content Type: Handshake (22)"
contType := uint8(segment[0])
if contType != contentType {
return &ParseError{errType: ContentTypeErr}
}
// Check if TLS record layer version is supported
tlsRecordVersion := uint16(segment[1])<<8 | uint16(segment[2])
if tlsRecordVersion&tlsVersionBitmask != 0x0300 && tlsRecordVersion != tls13 {
return &ParseError{VersionErr, 1}
}
// Check that the Handshake is as long as expected from the length field
segmentLen := uint16(segment[3])<<8 | uint16(segment[4])
if len(segment[recordLayerHeaderLen:]) < int(segmentLen) {
return &ParseError{LengthErr, 2}
}
// Keep the Handshake messege, ignore any additional following record types
hs := segment[recordLayerHeaderLen : recordLayerHeaderLen+int(segmentLen)]
err := j.parseHandshake(hs)
return err
}
// parseHandshake body
func (j *ClientHello) parseHandshake(hs []byte) error {
// Check if we can decode the next fields
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen {
return &ParseError{LengthErr, 3}
}
// Check if we have "Handshake Type: Client Hello (1)"
handshType := uint8(hs[0])
if handshType != handshakeType {
return &ParseError{errType: HandshakeTypeErr}
}
// Check if actual length of handshake matches (this is a great exclusion criterion for false positives,
// as these fields have to match the actual length of the rest of the segment)
handshakeLen := uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3])
if len(hs[4:]) != int(handshakeLen) {
return &ParseError{LengthErr, 4}
}
// Check if Client Hello version is supported
tlsVersion := uint16(hs[4])<<8 | uint16(hs[5])
if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 {
return &ParseError{VersionErr, 2}
}
j.Version = tlsVersion
// Check if we can decode the next fields
sessionIDLen := uint8(hs[38])
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen) {
return &ParseError{LengthErr, 5}
}
// Cipher Suites
cs := hs[handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen):]
// Check if we can decode the next fields
if len(cs) < cipherSuiteHeaderLen {
return &ParseError{LengthErr, 6}
}
csLen := uint16(cs[0])<<8 | uint16(cs[1])
numCiphers := int(csLen / 2)
cipherSuites := make([]uint16, 0, numCiphers)
// Check if we can decode the next fields
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen {
return &ParseError{LengthErr, 7}
}
for i := 0; i < numCiphers; i++ {
cipherSuite := uint16(cs[2+i<<1])<<8 | uint16(cs[3+i<<1])
cipherSuites = append(cipherSuites, cipherSuite)
}
j.CipherSuites = cipherSuites
// Check if we can decode the next fields
compressMethodLen := uint16(cs[cipherSuiteHeaderLen+int(csLen)])
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen) {
return &ParseError{LengthErr, 8}
}
// Extensions
exs := cs[cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen):]
err := j.parseExtensions(exs)
return err
}
// parseExtensions of the handshake
func (j *ClientHello) parseExtensions(exs []byte) error {
// Check for no extensions, this fields header is nonexistent if no body is used
if len(exs) == 0 {
return nil
}
// Check if we can decode the next fields
if len(exs) < extensionsHeaderLen {
return &ParseError{LengthErr, 9}
}
exsLen := uint16(exs[0])<<8 | uint16(exs[1])
exs = exs[extensionsHeaderLen:]
// Check if we can decode the next fields
if len(exs) < int(exsLen) {
return &ParseError{LengthErr, 10}
}
var sni []byte
var extensions, ellipticCurves []uint16
var ellipticCurvePF []uint8
var versions []uint16
var signatureAlgorithms []uint16
for len(exs) > 0 {
// Check if we can decode the next fields
if len(exs) < extensionHeaderLen {
return &ParseError{LengthErr, 11}
}
exType := uint16(exs[0])<<8 | uint16(exs[1])
exLen := uint16(exs[2])<<8 | uint16(exs[3])
// Ignore any GREASE extensions
extensions = append(extensions, exType)
// Check if we can decode the next fields
if len(exs) < extensionHeaderLen+int(exLen) {
return &ParseError{LengthErr, 12}
}
sex := exs[extensionHeaderLen : extensionHeaderLen+int(exLen)]
switch exType {
case sniExtensionType: // Extensions: server_name
// Check if we can decode the next fields
if len(sex) < sniExtensionHeaderLen {
return &ParseError{LengthErr, 13}
}
sniType := uint8(sex[2])
sniLen := uint16(sex[3])<<8 | uint16(sex[4])
sex = sex[sniExtensionHeaderLen:]
// Check if we can decode the next fields
if len(sex) != int(sniLen) {
return &ParseError{LengthErr, 14}
}
switch sniType {
case sniNameDNSHostnameType:
sni = sex
default:
return &ParseError{errType: SNITypeErr}
}
case ecExtensionType: // Extensions: supported_groups
// Check if we can decode the next fields
if len(sex) < ecExtensionHeaderLen {
return &ParseError{LengthErr, 15}
}
ecsLen := uint16(sex[0])<<8 | uint16(sex[1])
numCurves := int(ecsLen / 2)
ellipticCurves = make([]uint16, 0, numCurves)
sex = sex[ecExtensionHeaderLen:]
// Check if we can decode the next fields
if len(sex) != int(ecsLen) {
return &ParseError{LengthErr, 16}
}
for i := 0; i < numCurves; i++ {
ecType := uint16(sex[i*2])<<8 | uint16(sex[1+i*2])
ellipticCurves = append(ellipticCurves, ecType)
}
case ecpfExtensionType: // Extensions: ec_point_formats
// Check if we can decode the next fields
if len(sex) < ecpfExtensionHeaderLen {
return &ParseError{LengthErr, 17}
}
ecpfsLen := uint8(sex[0])
numPF := int(ecpfsLen)
ellipticCurvePF = make([]uint8, numPF)
sex = sex[ecpfExtensionHeaderLen:]
// Check if we can decode the next fields
if len(sex) != numPF {
return &ParseError{LengthErr, 18}
}
for i := 0; i < numPF; i++ {
ellipticCurvePF[i] = uint8(sex[i])
}
case versionExtensionType:
if len(sex) < versionExtensionHeaderLen {
return &ParseError{LengthErr, 19}
}
versionsLen := int(sex[0])
for i := 0; i < versionsLen; i += 2 {
versions = append(versions, binary.BigEndian.Uint16(sex[1:][i:]))
}
case signatureAlgorithmsExtensionType:
if len(sex) < signatureAlgorithmsExtensionHeaderLen {
return &ParseError{LengthErr, 20}
}
ssaLen := binary.BigEndian.Uint16(sex)
for i := 0; i < int(ssaLen); i += 2 {
signatureAlgorithms = append(signatureAlgorithms, binary.BigEndian.Uint16(sex[2:][i:]))
}
}
exs = exs[4+exLen:]
}
j.ServerName = string(sni)
j.Extensions = extensions
j.EllipticCurves = ellipticCurves
j.EllipticCurvePF = ellipticCurvePF
j.Versions = versions
j.SignatureAlgorithms = signatureAlgorithms
return nil
}
// marshalJA3 into a byte string
func (j *ClientHello) marshalJA3() {
// An uint16 can contain numbers with up to 5 digits and an uint8 can contain numbers with up to 3 digits, but we
// also need a byte for each separating character, except at the end.
byteStringLen := 6*(1+len(j.CipherSuites)+len(j.Extensions)+len(j.EllipticCurves)) + 4*len(j.EllipticCurvePF) - 1
byteString := make([]byte, 0, byteStringLen)
// Version
byteString = strconv.AppendUint(byteString, uint64(j.Version), 10)
byteString = append(byteString, commaByte)
// Cipher Suites
if len(j.CipherSuites) != 0 {
for _, val := range j.CipherSuites {
if val&GreaseBitmask != 0x0A0A {
continue
}
byteString = strconv.AppendUint(byteString, uint64(val), 10)
byteString = append(byteString, dashByte)
}
// Replace last dash with a comma
byteString[len(byteString)-1] = commaByte
} else {
byteString = append(byteString, commaByte)
}
// Extensions
if len(j.Extensions) != 0 {
for _, val := range j.Extensions {
if val&GreaseBitmask != 0x0A0A {
continue
}
byteString = strconv.AppendUint(byteString, uint64(val), 10)
byteString = append(byteString, dashByte)
}
// Replace last dash with a comma
byteString[len(byteString)-1] = commaByte
} else {
byteString = append(byteString, commaByte)
}
// Elliptic curves
if len(j.EllipticCurves) != 0 {
for _, val := range j.EllipticCurves {
if val&GreaseBitmask != 0x0A0A {
continue
}
byteString = strconv.AppendUint(byteString, uint64(val), 10)
byteString = append(byteString, dashByte)
}
// Replace last dash with a comma
byteString[len(byteString)-1] = commaByte
} else {
byteString = append(byteString, commaByte)
}
// ECPF
if len(j.EllipticCurvePF) != 0 {
for _, val := range j.EllipticCurvePF {
byteString = strconv.AppendUint(byteString, uint64(val), 10)
byteString = append(byteString, dashByte)
}
// Remove last dash
byteString = byteString[:len(byteString)-1]
}
j.ja3ByteString = byteString
}

View File

@@ -1,11 +1,16 @@
package mux
import (
"context"
"net"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-mux"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
@@ -30,7 +35,7 @@ func NewClientWithOptions(dialer N.Dialer, logger logger.Logger, options option.
}
}
return mux.NewClient(mux.Options{
Dialer: dialer,
Dialer: &clientDialer{dialer},
Logger: logger,
Protocol: options.Protocol,
MaxConnections: options.MaxConnections,
@@ -40,3 +45,15 @@ func NewClientWithOptions(dialer N.Dialer, logger logger.Logger, options option.
Brutal: brutalOptions,
})
}
type clientDialer struct {
N.Dialer
}
func (d *clientDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
return d.Dialer.DialContext(adapter.OverrideContext(ctx), network, destination)
}
func (d *clientDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return d.Dialer.ListenPacket(adapter.OverrideContext(ctx), destination)
}

View File

@@ -60,12 +60,12 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
isIPv4 := ip.Is4()
value, err := syscall.Sysctl(spath)
value, err := unix.SysctlRaw(spath)
if err != nil {
return "", err
}
buf := []byte(value)
buf := value
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
// size/offset are round up (aligned) to 8 bytes in darwin

View File

@@ -223,7 +223,7 @@ func getExecPathFromPID(pid uint32) (string, error) {
r1, _, err := syscall.SyscallN(
procQueryFullProcessImageNameW.Addr(),
uintptr(h),
uintptr(1),
uintptr(0),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)),
)

View File

@@ -16,30 +16,40 @@ import (
)
type LinuxSystemProxy struct {
hasGSettings bool
hasKWriteConfig5 bool
sudoUser string
serverAddr M.Socksaddr
supportSOCKS bool
isEnabled bool
hasGSettings bool
kWriteConfigCmd string
sudoUser string
serverAddr M.Socksaddr
supportSOCKS bool
isEnabled bool
}
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*LinuxSystemProxy, error) {
hasGSettings := common.Error(exec.LookPath("gsettings")) == nil
hasKWriteConfig5 := common.Error(exec.LookPath("kwriteconfig5")) == nil
kWriteConfigCmds := []string{
"kwriteconfig5",
"kwriteconfig6",
}
var kWriteConfigCmd string
for _, cmd := range kWriteConfigCmds {
if common.Error(exec.LookPath(cmd)) == nil {
kWriteConfigCmd = cmd
break
}
}
var sudoUser string
if os.Getuid() == 0 {
sudoUser = os.Getenv("SUDO_USER")
}
if !hasGSettings && !hasKWriteConfig5 {
if !hasGSettings && kWriteConfigCmd == "" {
return nil, E.New("unsupported desktop environment")
}
return &LinuxSystemProxy{
hasGSettings: hasGSettings,
hasKWriteConfig5: hasKWriteConfig5,
sudoUser: sudoUser,
serverAddr: serverAddr,
supportSOCKS: supportSOCKS,
hasGSettings: hasGSettings,
kWriteConfigCmd: kWriteConfigCmd,
sudoUser: sudoUser,
serverAddr: serverAddr,
supportSOCKS: supportSOCKS,
}, nil
}
@@ -70,8 +80,8 @@ func (p *LinuxSystemProxy) Enable() error {
return err
}
}
if p.hasKWriteConfig5 {
err := p.runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "1")
if p.kWriteConfigCmd != "" {
err := p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "1")
if err != nil {
return err
}
@@ -83,7 +93,7 @@ func (p *LinuxSystemProxy) Enable() error {
if err != nil {
return err
}
err = p.runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "Authmode", "0")
err = p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "Authmode", "0")
if err != nil {
return err
}
@@ -103,8 +113,8 @@ func (p *LinuxSystemProxy) Disable() error {
return err
}
}
if p.hasKWriteConfig5 {
err := p.runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "0")
if p.kWriteConfigCmd != "" {
err := p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "0")
if err != nil {
return err
}
@@ -150,7 +160,7 @@ func (p *LinuxSystemProxy) setKDEProxy(proxyTypes ...string) error {
proxyUrl = "http://" + p.serverAddr.String()
}
err := p.runAsUser(
"kwriteconfig5",
p.kWriteConfigCmd,
"--file",
"kioslaverc",
"--group",

View File

@@ -0,0 +1,99 @@
package sniff
import (
"bytes"
"context"
"encoding/binary"
"io"
"os"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
)
const (
trackerConnectFlag = 0
trackerProtocolID = 0x41727101980
trackerConnectMinSize = 16
)
// BitTorrent detects if the stream is a BitTorrent connection.
// For the BitTorrent protocol specification, see https://www.bittorrent.org/beps/bep_0003.html
func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
var first byte
err := binary.Read(reader, binary.BigEndian, &first)
if err != nil {
return err
}
if first != 19 {
return os.ErrInvalid
}
var protocol [19]byte
_, err = reader.Read(protocol[:])
if err != nil {
return err
}
if string(protocol[:]) != "BitTorrent protocol" {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolBitTorrent
return nil
}
// UTP detects if the packet is a uTP connection packet.
// For the uTP protocol specification, see
// 1. https://www.bittorrent.org/beps/bep_0029.html
// 2. https://github.com/bittorrent/libutp/blob/2b364cbb0650bdab64a5de2abb4518f9f228ec44/utp_internal.cpp#L112
func UTP(_ context.Context, metadata *adapter.InboundContext, packet []byte) error {
// A valid uTP packet must be at least 20 bytes long.
if len(packet) < 20 {
return os.ErrInvalid
}
version := packet[0] & 0x0F
ty := packet[0] >> 4
if version != 1 || ty > 4 {
return os.ErrInvalid
}
// Validate the extensions
extension := packet[1]
reader := bytes.NewReader(packet[20:])
for extension != 0 {
err := binary.Read(reader, binary.BigEndian, &extension)
if err != nil {
return err
}
var length byte
err = binary.Read(reader, binary.BigEndian, &length)
if err != nil {
return err
}
_, err = reader.Seek(int64(length), io.SeekCurrent)
if err != nil {
return err
}
}
metadata.Protocol = C.ProtocolBitTorrent
return nil
}
// UDPTracker detects if the packet is a UDP Tracker Protocol packet.
// For the UDP Tracker Protocol specification, see https://www.bittorrent.org/beps/bep_0015.html
func UDPTracker(_ context.Context, metadata *adapter.InboundContext, packet []byte) error {
if len(packet) < trackerConnectMinSize {
return os.ErrInvalid
}
if binary.BigEndian.Uint64(packet[:8]) != trackerProtocolID {
return os.ErrInvalid
}
if binary.BigEndian.Uint32(packet[8:12]) != trackerConnectFlag {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolBitTorrent
return nil
}

View File

@@ -0,0 +1,73 @@
package sniff_test
import (
"bytes"
"context"
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require"
)
func TestSniffBittorrent(t *testing.T) {
t.Parallel()
packets := []string{
"13426974546f7272656e742070726f746f636f6c0000000000100000e21ea9569b69bab33c97851d0298bdfa89bc90922d5554313631302dea812fcd6a3563e3be40c1d1",
"13426974546f7272656e742070726f746f636f6c00000000001000052aa4f5a7e209e54b32803d43670971c4c8caaa052d5452333030302d653369733079647675763638",
"13426974546f7272656e742070726f746f636f6c00000000001000052aa4f5a7e209e54b32803d43670971c4c8caaa052d5452343035302d6f7a316c6e79377931716130",
}
for _, pkt := range packets {
pkt, err := hex.DecodeString(pkt)
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt))
require.NoError(t, err)
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
}
}
func TestSniffUTP(t *testing.T) {
t.Parallel()
packets := []string{
"010041a282d7ee7b583afb160004000006d8318da776968f92d666f7963f32dae23ba0d2c810d8b8209cc4939f54fde9eeaa521c2c20c9ba7f43f4fb0375f28de06643b5e3ca4685ab7ac76adca99783be72ef05ed59ef4234f5712b75b4c7c0d7bee8fe2ca20ad626ba5bb0ffcc16bf06790896f888048cf72716419a07db1a3dca4550fbcea75b53e97235168a221cf3e553dfbb723961bd719fab038d86e0ecb74747f5a2cd669de1c4b9ad375f3a492d09d98cdfad745435625401315bbba98d35d32086299801377b93495a63a9efddb8d05f5b37a5c5b1c0a25e917f12007bb5e05013ada8aff544fab8cadf61d80ddb0b60f12741e44515a109d144fd53ef845acb4b5ccf0d6fc302d7003d76df3fc3423bb0237301c9e88f900c2d392a8e0fdb36d143cf7527a93fd0a2638b746e72f6699fffcd4fd15348fce780d4caa04382fd9faf1ca0ae377ca805da7536662b84f5ee18dd3ae38fcb095a7543e55f9069ae92c8cf54ae44e97b558d35e2545c66601ed2149cbc32bd6df199a2be7cf0da8b2ff137e0d23e776bc87248425013876d3a3cc31a83b424b752bd0346437f24b532978005d8f5b1b0be1a37a2489c32a18a9ad3118e3f9d30eb299bffae18e1f0677c2a5c185e62519093fe6bc2b7339299ea50a587989f726ca6443a75dd5bb936f6367c6355d80fae53ff529d740b2e5576e3eefdf1fdbfc69c3c8d8ac750512635de63e054bee1d3b689bc1b2bc3d2601e42a00b5c89066d173d4ae7ffedfd2274e5cf6d868fbe640aedb69b8246142f00b32d459974287537ddd5373460dcbc92f5cfdd7a3ed6020822ae922d947893752ca1983d0d32977374c384ac8f5ab566859019b7351526b9f13e932037a55bb052d9deb3b3c23317e0784fdc51a64f2159bfea3b069cf5caf02ee2c3c1a6b6b427bb16165713e8802d95b5c8ed77953690e994bd38c9ae113fedaf6ee7fc2b96c032ceafc2a530ad0422e84546b9c6ad8ef6ea02fa508abddd1805c38a7b42e9b7c971b1b636865ebec06ed754bb404cd6b4e6cc8cb77bd4a0c43410d5cd5ef8fe853a66d49b3b9e06cb141236cdbfdd5761601dc54d1250b86c660e0f898fe62526fdd9acf0eab60a3bbbb2151970461f28f10b31689594bea646c4b03ee197d63bdef4e5a7c22716b3bb9494a83b78ecd81b338b80ac6c09c43485b1b09ba41c74343832c78f0520c1d659ac9eb1502094141e82fb9e5e620970ebc0655514c43c294a7714cbf9a499d277daf089f556398a01589a77494bec8bfb60a108f3813b55368672b88c1af40f6b3c8b513f7c70c3e0efce85228b8b9ec67ba0393f9f7305024d8e2da6a26cf85613d14f249170ce1000089df4c9c260df7f8292aa2ecb5d5bac97656d59aa248caedea2d198e51ce87baece338716d114b458de02d65c9ff808ca5b5b73723b4d1e962d9ac2d98176544dc9984cf8554d07820ef3dd0861cfe57b478328046380de589adad94ee44743ffac73bb7361feca5d56f07cf8ce75080e261282ae30350d7882679b15cab9e7e53ddf93310b33f7390ae5d318bb53f387e6af5d0ef4f947fc9cb8e7e38b52c7f8d772ece6156b38d88796ea19df02c53723b44df7c76315a0de9462f27287e682d2b4cda1a68fe00d7e48c51ee981be44e1ca940fb5190c12655edb4a83c3a4f33e48a015692df4f0b3d61656e362aca657b5ae8c12db5a0db3db1e45135ee918b66918f40e53c4f83e9da0cddfe63f736ae751ab3837a30ae3220d8e8e311487093a7b90c7e7e40dd54ca750e19452f9193aa892aa6a6229ab493dadae988b1724f7898ee69c36d3eb7364c4adbeca811cfe2065873e78c2b6dfdf1595f7a7831c07e03cda82e4f86f76438dfb2b07c13638ce7b509cfa71b88b5102b39a203b423202088e1c2103319cb32c13c1e546ff8612fa194c95a7808ab767c265a1bd5fa0efed5c8ec1701876a00ec8",
"01001ecb68176f215d04326300100000dbcf30292d14b54e9ee2d115ee5b8ebc7fad3e882d4fcdd0c14c6b917c11cb4c6a9f410b52a33ae97c2ac77c7a2b122b8955e09af3c5c595f1b2e79ca57cfe44c44e069610773b9bc9ba223d7f6b383e3adddd03fb88a8476028e30979c2ef321ffc97c5c132bcf9ac5b410bbb5ec6cefca3c7209202a14c5ae922b6b157b0a80249d13ffe5b996af0bc8e54ba576d148372494303e7ead0602b05b9c8fc97d48508a028a04d63a1fd28b0edfcd5c51715f63188b53eefede98a76912dca98518551a8856567307a56a702cbfcc115ea0c755b418bc2c7b57721239b82f09fb24328a4b0ce0f109bcb2a64e04b8aadb1f8487585425acdf8fc4ec8ea93cfcec5ac098bb29d42ddef6e46b03f34a5de28316726699b7cb5195c33e5c48abe87d591d63f9991c84c30819d186d6e0e95fd83c8dff07aa669c4430989bcaccfeacb9bcadbdb4d8f1964dbeb9687745656edd30b21c66cc0a1d742a78717d134a19a7f02d285a4973b1a198c00cfdff4676608dc4f3e817e3463c3b4e2c80d3e8d4fbac541a58a2fb7ad6939f607f8144eff6c8b0adc28ee5609ea158987519892fb",
"21001ecb6817f2805d044fd700100000dbd03029",
"410277ef0b1fb1f60000000000040000c233000000080000000000000000",
}
for _, pkt := range packets {
pkt, err := hex.DecodeString(pkt)
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.UTP(context.TODO(), &metadata, pkt)
require.NoError(t, err)
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
}
}
func TestSniffUDPTracker(t *testing.T) {
t.Parallel()
connectPackets := []string{
"00000417271019800000000078e90560",
"00000417271019800000000022c5d64d",
"000004172710198000000000b3863541",
}
for _, pkt := range connectPackets {
pkt, err := hex.DecodeString(pkt)
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.UDPTracker(context.TODO(), &metadata, pkt)
require.NoError(t, err)
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
}
}

View File

@@ -17,18 +17,17 @@ import (
mDNS "github.com/miekg/dns"
)
func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.InboundContext, error) {
func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
var length uint16
err := binary.Read(reader, binary.BigEndian, &length)
if err != nil {
return nil, err
return os.ErrInvalid
}
if length == 0 {
return nil, os.ErrInvalid
return os.ErrInvalid
}
buffer := buf.NewSize(int(length))
defer buffer.Release()
readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100)
var readTask task.Group
readTask.Append0(func(ctx context.Context) error {
@@ -37,19 +36,20 @@ func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.
err = readTask.Run(readCtx)
cancel()
if err != nil {
return nil, err
return err
}
return DomainNameQuery(readCtx, buffer.Bytes())
return DomainNameQuery(readCtx, metadata, buffer.Bytes())
}
func DomainNameQuery(ctx context.Context, packet []byte) (*adapter.InboundContext, error) {
func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
var msg mDNS.Msg
err := msg.Unpack(packet)
if err != nil {
return nil, err
return err
}
if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) {
return nil, os.ErrInvalid
return os.ErrInvalid
}
return &adapter.InboundContext{Protocol: C.ProtocolDNS}, nil
metadata.Protocol = C.ProtocolDNS
return nil
}

32
common/sniff/dtls.go Normal file
View File

@@ -0,0 +1,32 @@
package sniff
import (
"context"
"os"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
)
func DTLSRecord(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
const fixedHeaderSize = 13
if len(packet) < fixedHeaderSize {
return os.ErrInvalid
}
contentType := packet[0]
switch contentType {
case 20, 21, 22, 23, 25:
default:
return os.ErrInvalid
}
versionMajor := packet[1]
if versionMajor != 0xfe {
return os.ErrInvalid
}
versionMinor := packet[2]
if versionMinor != 0xff && versionMinor != 0xfd {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolDTLS
return nil
}

33
common/sniff/dtls_test.go Normal file
View File

@@ -0,0 +1,33 @@
package sniff_test
import (
"context"
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require"
)
func TestSniffDTLSClientHello(t *testing.T) {
t.Parallel()
packet, err := hex.DecodeString("16fefd0000000000000000007e010000720000000000000072fefd668a43523798e064bd806d0c87660de9c611a59bbdfc3892c4e072d94f2cafc40000000cc02bc02fc00ac014c02cc0300100003c000d0010000e0403050306030401050106010807ff01000100000a00080006001d00170018000b00020100000e000900060008000700010000170000")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.DTLSRecord(context.Background(), &metadata, packet)
require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolDTLS)
}
func TestSniffDTLSClientApplicationData(t *testing.T) {
t.Parallel()
packet, err := hex.DecodeString("17fefd000100000000000100440001000000000001a4f682b77ecadd10f3f3a2f78d90566212366ff8209fd77314f5a49352f9bb9bd12f4daba0b4736ae29e46b9714d3b424b3e6d0234736619b5aa0d3f")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.DTLSRecord(context.Background(), &metadata, packet)
require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolDTLS)
}

View File

@@ -11,10 +11,12 @@ import (
"github.com/sagernet/sing/protocol/http"
)
func HTTPHost(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error) {
func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
request, err := http.ReadRequest(std_bufio.NewReader(reader))
if err != nil {
return nil, err
return err
}
return &adapter.InboundContext{Protocol: C.ProtocolHTTP, Domain: M.ParseSocksaddr(request.Host).AddrString()}, nil
metadata.Protocol = C.ProtocolHTTP
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
return nil
}

View File

@@ -5,6 +5,7 @@ import (
"strings"
"testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff"
"github.com/stretchr/testify/require"
@@ -13,7 +14,8 @@ import (
func TestSniffHTTP1(t *testing.T) {
t.Parallel()
pkt := "GET / HTTP/1.1\r\nHost: www.google.com\r\nAccept: */*\r\n\r\n"
metadata, err := sniff.HTTPHost(context.Background(), strings.NewReader(pkt))
var metadata adapter.InboundContext
err := sniff.HTTPHost(context.Background(), &metadata, strings.NewReader(pkt))
require.NoError(t, err)
require.Equal(t, metadata.Domain, "www.google.com")
}
@@ -21,7 +23,8 @@ func TestSniffHTTP1(t *testing.T) {
func TestSniffHTTP1WithPort(t *testing.T) {
t.Parallel()
pkt := "GET / HTTP/1.1\r\nHost: www.gov.cn:8080\r\nAccept: */*\r\n\r\n"
metadata, err := sniff.HTTPHost(context.Background(), strings.NewReader(pkt))
var metadata adapter.InboundContext
err := sniff.HTTPHost(context.Background(), &metadata, strings.NewReader(pkt))
require.NoError(t, err)
require.Equal(t, metadata.Domain, "www.gov.cn")
}

View File

@@ -5,95 +5,99 @@ import (
"context"
"crypto"
"crypto/aes"
"crypto/tls"
"encoding/binary"
"io"
"os"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/ja3"
"github.com/sagernet/sing-box/common/sniff/internal/qtls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/crypto/hkdf"
)
func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContext, error) {
reader := bytes.NewReader(packet)
var ErrClientHelloFragmented = E.New("need more packet for chromium QUIC connection")
func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
reader := bytes.NewReader(packet)
typeByte, err := reader.ReadByte()
if err != nil {
return nil, err
return err
}
if typeByte&0x40 == 0 {
return nil, E.New("bad type byte")
return E.New("bad type byte")
}
var versionNumber uint32
err = binary.Read(reader, binary.BigEndian, &versionNumber)
if err != nil {
return nil, err
return err
}
if versionNumber != qtls.VersionDraft29 && versionNumber != qtls.Version1 && versionNumber != qtls.Version2 {
return nil, E.New("bad version")
return E.New("bad version")
}
packetType := (typeByte & 0x30) >> 4
if packetType == 0 && versionNumber == qtls.Version2 || packetType == 2 && versionNumber != qtls.Version2 || packetType > 2 {
return nil, E.New("bad packet type")
return E.New("bad packet type")
}
destConnIDLen, err := reader.ReadByte()
if err != nil {
return nil, err
return err
}
if destConnIDLen == 0 || destConnIDLen > 20 {
return nil, E.New("bad destination connection id length")
return E.New("bad destination connection id length")
}
destConnID := make([]byte, destConnIDLen)
_, err = io.ReadFull(reader, destConnID)
if err != nil {
return nil, err
return err
}
srcConnIDLen, err := reader.ReadByte()
if err != nil {
return nil, err
return err
}
_, err = io.CopyN(io.Discard, reader, int64(srcConnIDLen))
if err != nil {
return nil, err
return err
}
tokenLen, err := qtls.ReadUvarint(reader)
if err != nil {
return nil, err
return err
}
_, err = io.CopyN(io.Discard, reader, int64(tokenLen))
if err != nil {
return nil, err
return err
}
packetLen, err := qtls.ReadUvarint(reader)
if err != nil {
return nil, err
return err
}
hdrLen := int(reader.Size()) - reader.Len()
if hdrLen+int(packetLen) > len(packet) {
return nil, os.ErrInvalid
return os.ErrInvalid
}
_, err = io.CopyN(io.Discard, reader, 4)
if err != nil {
return nil, err
return err
}
pnBytes := make([]byte, aes.BlockSize)
_, err = io.ReadFull(reader, pnBytes)
if err != nil {
return nil, err
return err
}
var salt []byte
@@ -117,7 +121,7 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
hpKey := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, hkdfHeaderProtectionLabel, 16)
block, err := aes.NewCipher(hpKey)
if err != nil {
return nil, err
return err
}
mask := make([]byte, aes.BlockSize)
block.Encrypt(mask, pnBytes)
@@ -129,7 +133,7 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
}
packetNumberLength := newPacket[0]&0x3 + 1
if hdrLen+int(packetNumberLength) > int(packetLen)+hdrLen {
return nil, os.ErrInvalid
return os.ErrInvalid
}
var packetNumber uint32
switch packetNumberLength {
@@ -142,7 +146,7 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
case 4:
packetNumber = binary.BigEndian.Uint32(newPacket[hdrLen:])
default:
return nil, E.New("bad packet number length")
return E.New("bad packet number length")
}
extHdrLen := hdrLen + int(packetNumberLength)
copy(newPacket[extHdrLen:hdrLen+4], packet[extHdrLen:])
@@ -166,138 +170,208 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(packetNumber))
decrypted, err := cipher.Open(newPacket[extHdrLen:extHdrLen], nonce, data, newPacket[:extHdrLen])
if err != nil {
return nil, err
return err
}
var frameType byte
var frameLen uint64
var fragments []struct {
offset uint64
length uint64
payload []byte
}
var fragments []qCryptoFragment
decryptedReader := bytes.NewReader(decrypted)
const (
frameTypePadding = 0x00
frameTypePing = 0x01
frameTypeAck = 0x02
frameTypeAck2 = 0x03
frameTypeCrypto = 0x06
frameTypeConnectionClose = 0x1c
)
var frameTypeList []uint8
for {
frameType, err = decryptedReader.ReadByte()
if err == io.EOF {
break
}
frameTypeList = append(frameTypeList, frameType)
switch frameType {
case 0x00: // PADDING
case frameTypePadding:
continue
case 0x01: // PING
case frameTypePing:
continue
case 0x02, 0x03: // ACK
case frameTypeAck, frameTypeAck2:
_, err = qtls.ReadUvarint(decryptedReader) // Largest Acknowledged
if err != nil {
return nil, err
return err
}
_, err = qtls.ReadUvarint(decryptedReader) // ACK Delay
if err != nil {
return nil, err
return err
}
ackRangeCount, err := qtls.ReadUvarint(decryptedReader) // ACK Range Count
if err != nil {
return nil, err
return err
}
_, err = qtls.ReadUvarint(decryptedReader) // First ACK Range
if err != nil {
return nil, err
return err
}
for i := 0; i < int(ackRangeCount); i++ {
_, err = qtls.ReadUvarint(decryptedReader) // Gap
if err != nil {
return nil, err
return err
}
_, err = qtls.ReadUvarint(decryptedReader) // ACK Range Length
if err != nil {
return nil, err
return err
}
}
if frameType == 0x03 {
_, err = qtls.ReadUvarint(decryptedReader) // ECT0 Count
if err != nil {
return nil, err
return err
}
_, err = qtls.ReadUvarint(decryptedReader) // ECT1 Count
if err != nil {
return nil, err
return err
}
_, err = qtls.ReadUvarint(decryptedReader) // ECN-CE Count
if err != nil {
return nil, err
return err
}
}
case 0x06: // CRYPTO
case frameTypeCrypto:
var offset uint64
offset, err = qtls.ReadUvarint(decryptedReader)
if err != nil {
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err
return err
}
var length uint64
length, err = qtls.ReadUvarint(decryptedReader)
if err != nil {
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err
return err
}
index := len(decrypted) - decryptedReader.Len()
fragments = append(fragments, struct {
offset uint64
length uint64
payload []byte
}{offset, length, decrypted[index : index+int(length)]})
frameLen += length
fragments = append(fragments, qCryptoFragment{offset, length, decrypted[index : index+int(length)]})
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent)
if err != nil {
return nil, err
return err
}
case 0x1c: // CONNECTION_CLOSE
case frameTypeConnectionClose:
_, err = qtls.ReadUvarint(decryptedReader) // Error Code
if err != nil {
return nil, err
return err
}
_, err = qtls.ReadUvarint(decryptedReader) // Frame Type
if err != nil {
return nil, err
return err
}
var length uint64
length, err = qtls.ReadUvarint(decryptedReader) // Reason Phrase Length
if err != nil {
return nil, err
return err
}
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent) // Reason Phrase
if err != nil {
return nil, err
return err
}
default:
return nil, os.ErrInvalid
return os.ErrInvalid
}
}
tlsHdr := make([]byte, 5)
tlsHdr[0] = 0x16
binary.BigEndian.PutUint16(tlsHdr[1:], uint16(0x0303))
binary.BigEndian.PutUint16(tlsHdr[3:], uint16(frameLen))
if metadata.SniffContext != nil {
fragments = append(fragments, metadata.SniffContext.([]qCryptoFragment)...)
metadata.SniffContext = nil
}
var frameLen uint64
for _, fragment := range fragments {
frameLen += fragment.length
}
buffer := buf.NewSize(5 + int(frameLen))
defer buffer.Release()
buffer.WriteByte(0x16)
binary.Write(buffer, binary.BigEndian, uint16(0x0303))
binary.Write(buffer, binary.BigEndian, uint16(frameLen))
var index uint64
var length int
var readers []io.Reader
readers = append(readers, bytes.NewReader(tlsHdr))
find:
for {
for _, fragment := range fragments {
if fragment.offset == index {
readers = append(readers, bytes.NewReader(fragment.payload))
buffer.Write(fragment.payload)
index = fragment.offset + fragment.length
length++
continue find
}
}
if length == len(fragments) {
break
}
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, E.New("bad fragments")
}
metadata, err := TLSClientHello(ctx, io.MultiReader(readers...))
if err != nil {
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err
break
}
metadata.Protocol = C.ProtocolQUIC
return metadata, nil
fingerprint, err := ja3.Compute(buffer.Bytes())
if err != nil {
metadata.Protocol = C.ProtocolQUIC
metadata.Client = C.ClientChromium
metadata.SniffContext = fragments
return ErrClientHelloFragmented
}
metadata.Domain = fingerprint.ServerName
for metadata.Client == "" {
if len(frameTypeList) == 1 {
metadata.Client = C.ClientFirefox
break
}
if frameTypeList[0] == frameTypeCrypto && isZero(frameTypeList[1:]) {
if len(fingerprint.Versions) == 2 && fingerprint.Versions[0]&ja3.GreaseBitmask == 0x0A0A &&
len(fingerprint.EllipticCurves) == 5 && fingerprint.EllipticCurves[0]&ja3.GreaseBitmask == 0x0A0A {
metadata.Client = C.ClientSafari
break
}
if len(fingerprint.CipherSuites) == 1 && fingerprint.CipherSuites[0] == tls.TLS_AES_256_GCM_SHA384 &&
len(fingerprint.EllipticCurves) == 1 && fingerprint.EllipticCurves[0] == uint16(tls.X25519) &&
len(fingerprint.SignatureAlgorithms) == 1 && fingerprint.SignatureAlgorithms[0] == uint16(tls.ECDSAWithP256AndSHA256) {
metadata.Client = C.ClientSafari
break
}
}
if frameTypeList[len(frameTypeList)-1] == frameTypeCrypto && isZero(frameTypeList[:len(frameTypeList)-1]) {
metadata.Client = C.ClientQUICGo
break
}
if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 {
if maybeUQUIC(fingerprint) {
metadata.Client = C.ClientQUICGo
} else {
metadata.Client = C.ClientChromium
}
break
}
metadata.Client = C.ClientUnknown
//nolint:staticcheck
break
}
return nil
}
func isZero(slices []uint8) bool {
for _, slice := range slices {
if slice != 0 {
return false
}
}
return true
}
func count(slices []uint8, value uint8) int {
var times int
for _, slice := range slices {
if slice == value {
times++
}
}
return times
}
type qCryptoFragment struct {
offset uint64
length uint64
payload []byte
}

View File

@@ -0,0 +1,24 @@
package sniff
import (
"crypto/tls"
"github.com/sagernet/sing-box/common/ja3"
)
// Chromium sends separate client hello packets, but UQUIC has not yet implemented this behavior
// The cronet without this behavior does not have version 115
var uQUICChrome115 = &ja3.ClientHello{
Version: tls.VersionTLS12,
CipherSuites: []uint16{4865, 4866, 4867},
Extensions: []uint16{0, 10, 13, 16, 27, 43, 45, 51, 57, 17513},
EllipticCurves: []uint16{29, 23, 24},
SignatureAlgorithms: []uint16{1027, 2052, 1025, 1283, 2053, 1281, 2054, 1537, 513},
}
func maybeUQUIC(fingerprint *ja3.ClientHello) bool {
if uQUICChrome115.Equals(fingerprint, true) {
return true
}
return false
}

View File

@@ -5,31 +5,69 @@ import (
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require"
)
func TestSniffQUICv1(t *testing.T) {
func TestSniffQUICChromium(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("cc0000000108d2dc7bad02241f5003796e71004215a71bfcb05159416c724be418537389acdd9a4047306283dcb4d7a9cad5cc06322042d204da67a8dbaa328ab476bb428b48fd001501863afd203f8d4ef085629d664f1a734a65969a47e4a63d4e01a21f18c1d90db0c027180906dc135f9ae421bb8617314c8d54c175fef3d3383d310d0916ebcbd6eed9329befbbb109d8fd4af1d2cf9d6adce8e6c1260a7f8256e273e326da0aa7cc148d76e7a08489dc9d52ade89c027cbc3491ada46417c2c04e2ca768e9a7dd6aa00c594e48b678927325da796817693499bb727050cb3baf3d3291a397c3a8d868e8ec7b8f7295e347455c9dadbe2252ae917ac793d958c7fb8a3d2cdb34e3891eb4286f18617556ff7216dd60256aa5b1d11ff4753459fc5f9dedf11d483a26a0835dc6cd50e1c1f54f86e8f1e502821183cd874f6447a74e818bf3445c7795acf4559d1c1fac474911d2ead5c8d23e4aa4f67afb66efe305a30a0b5d825679b31ddc186cbea936535795c7e8c378c87b8c5adc065154d15bae8f85ac8fec2da40c3aa623b682a065440831555011d7647cde44446a0fb4cf5892f2c088ae1920643094be72e3c499fe8d265caf939e8ab607a5b9317917d2a32a812e8a0e6a2f84721bbb5984ffd242838f705d13f4cfb249bc6a5c80d58ac2595edf56648ec3fe21d787573c253a79805252d6d81e26d367d4ff29ef66b5fe8992086af7bada8cad10b82a7c0dc406c5b6d0c5ec3c583e767f759ce08cad6c3c8f91e5a8")
pkt, err := hex.DecodeString("c30000000108f40d654cc09b27f5000044d08a94548e57e43cc5483f129986187c432d58d46674830442988f869566a6e31e2ae37c9f7acbf61cc81621594fab0b3dfdc1635460b32389563dc8e74006315661cd22694114612973c1c45910621713a48b375854f095e8a77ccf3afa64e972f0f7f7002f50e0b014b1b146ea47c07fb20b73ad5587872b51a0b3fafdf1c4cf4fe6f8b112142392efa25d993abe2f42582be145148bdfe12edcd96c3655b65a4781b093e5594ba8e3ae5320f12e8314fc3ca374128cc43381046c322b964681ed4395c813b28534505118201459665a44b8f0abead877de322e9040631d20b05f15b81fa7ff785d4041aecc37c7e2ccdc5d1532787ce566517e8985fd5c200dbfd1e67bc255efaba94cfc07bb52fea4a90887413b134f2715b5643542aa897c6116486f428d82da64d2a2c1e1bdd40bd592558901a554b003d6966ac5a7b8b9413eddbf6ef21f28386c74981e3ce1d724c341e95494907626659692720c81114ca4acea35a14c402cfa3dc2228446e78dc1b81fa4325cf7e314a9cad6a6bdff33b3351dcba74eb15fae67f1227283aa4cdd64bcadf8f19358333f8549b596f4350297b5c65274565869d497398339947b9d3d064e5b06d39d34b436d8a41c1a3880de10bd26c3b1c5b4e2a49b0d4d07b8d90cd9e92bc611564d19ea8ec33099e92033caf21f5307dbeaa4708b99eb313bff99e2081ac25fd12d6a72e8335e0724f6718fe023cd0ad0d6e6a6309f09c9c391eec2bc08e9c3210a043c08e1759f354c121f6517fff4d6e20711a871e41285d48d930352fddffb92c96ba57df045ce99f8bfdfa8edc0969ce68a51e9fbb4f54b956d9df74a9e4af27ed2b27839bce1cffeca8333c0aaee81a570217442f9029ba8fedb84a2cf4be4d910982d891ea00e816c7fb98e8020e896a9c6fdd9106611da0a99dde18df1b7a8f6327acb1eed9ad93314451e48cb0dfb9571728521ca3db2ac0968159d5622556a55d51a422d11995b650949aaefc5d24c16080446dfc4fbc10353f9f93ce161ab513367bb89ab83988e0630b689e174e27bcfcc31996ee7b0bca909e251b82d69a28fee5a5d662e127508cd19dbbe5097b7d5b62a49203d66764197a527e472e2627e44a93d44177dace9d60e7d0e03305ddf4cfe47cdf2362e14de79ef46a6763ce696cd7854a48d9419a0817507a4713ffd4977b906d4f2b5fb6dbe1bd15bc505d5fea582190bf531a45d5ee026da8918547fd5105f15e5d061c7b0cf80a34990366ed8e91e13c2f0d85e5dad537298808d193cf54b7eaac33f10051f74cb6b75e52f81618c36f03d86aef613ba237a1a793ba1539938a38f62ccaf7bd5f6c5e0ce53cde4012fcf2b758214a0422d2faaa798e86e19d7481b42df2b36a73d287ff28c20cce01ce598771fec16a8f1f00305c06010126013a6c1de9f589b4e79d693717cd88ad1c42a2d99fa96617ba0bc6365b68e21a70ebc447904aa27979e1514433cfd83bfec09f137c747d47582cb63eb28f873fb94cf7a59ff764ddfbb687d79a58bb10f85949269f7f72c611a5e0fbb52adfa298ff060ec2eb7216fd7302ea8fb07798cbb3be25cb53ac8161aac2b5bbcfbcfb01c113d28bd1cb0333fb89ac82a95930f7abded0a2f5a623cc6a1f62bf3f38ef1b81c1e50a634f657dbb6770e4af45879e2fb1e00c742e7b52205c8015b5c0f5b1e40186ff9aa7288ab3e01a51fb87761f9bc6837082af109b39cc9f620")
require.NoError(t, err)
metadata, err := sniff.QUICClientHello(context.Background(), pkt)
var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientChromium)
require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
require.NoError(t, err)
require.Equal(t, metadata.Domain, "cloudflare-quic.com")
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.NoError(t, err)
require.Equal(t, metadata.Domain, "google.com")
}
func TestSniffQUICFragment(t *testing.T) {
func TestSniffUQUICChrome115(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("cc00000001082e3d5d1b64040c55000044d0ccea69e773f6631c1d18b04ae9ee75fcfc34ef74fa62533c93534338a86f101a05d70e0697fb483063fa85db1c59ccfbda5c35234931d8524d8aac37eaaad649470a67794cd754b23c98695238b8363452333bc8c4858376b4166e001da2006e35cf98a91e11a56419b2786775284942d0f7163982f7c248867d12dd374957481dbc564013ff785e1916195eef671f725908f761099d992d69231336ba81d9e25fe2fa3a6eff4318a6ccf10176fc841a1b315f7b35c5b292266fc869d76ca533e7d14e86d82db2e22eacd350977e47d2e012d8a5891c5aaf2a0f4c2b2dae897c161e5b68cbb4dee952472bdc1e21504b8f02534ec4366ce3f8bf86efc78e0232778fbd554457567112abdcafcf6d4d8fcf35083c25d9495679614aba21696e338c62b585046cc55ba8c09c844361d889a47c3ea703b4e23545a9ab2c0bb369693a9ddfb5daffa85cf80fdd6ad66738664e5b0a551729b4955cff7255afcb04dee88c2f072c9de7400947a1bd9327ac5d012a33000ada021d4c03d249fb017d6ac9200b2f9436beab8183ddfbe2d8aee31ffb7df9e1cc181c1af80c39a89965d18ed12da8e3ebe2ae1fbe4b348f83ba19e3e3d1c9b22bcf03ab6ad9b30fe180623faa291ebad83bcd71d7b57f2f5e2f3b8e81d24fb70b2f2159239e8f21ffafef2747aba47d97ab4081e603c018b10678cf99cab1fb42156a14486fa435153979d7279fd22cd40af7088bfc7eff41af2f4b3c0c8864d0040d74dff427f7bffdb8c278474ea00311326cf4925471a8cf596cb92119f19e0f789490ba9cb77b98015a987d93e0324cf1a38b55109f00c3e6ddc5180fb107bf468323afec9bb49fd6a86418569789d66cafe3b8253c2aebb3af3782c1c54dd560487d031d28e6a6e23e159581bb1d47efc4da3fe1d169f9ffb0ca9ba61af0a38a92fde5bc5e6ec026e8378a6315a7b95abf1d2da790a391306ce74d0baf8e2ce648ca74c487f2c0a76a28a80cdf5bd34316eb607684fe7e6d9e83824a00e07660d0b90e3cddd61ebf10748263474afa88c300549e64ce2e90560bb1a12dee7e9484f729a8a4ee7c5651adb5194b3b3ae38e501567c7dbf36e7bb37a2c20b74655f47f2d9af18e52e9d4c9c9eee8e63745779b8f0b06f3a09d846ba62eb978ad77c85de1ee2fee3fbb4c2d283c73e1ccba56a4658e48a2665d200f7f9342f8e84c2ba490094a4f94feec89e42d2f654f564c2beb2997bafa1fc2c68ad8e160b63587d49abc31b834878d52acfb05fb73d0e059b206162e3c90b40c4bc08407ffcb3c08431895b691a3fea923f1f3b48db75d3e6b91fd319ffe4d486e0e14bd5c6affc838dee63d9e0b80f169b5e6c02c7321dcb20deb2b8e707b60e345a308d505bbf26a93d8f18b39d62632e9a77cbe48b3b32eb8819d6311a49820d40f5acbf0273c91c36b2269a03e72ee64df3dfb10ddefe73c64ef60870b2b77bd99dea655f5fe791b538a929a14d99f6d69685d72431ea5f0f4b27a044f2f575ab474fcc3857895934de1ca2581798eaef2c17fe5aaf2e6add97fa32997c7026f15c1b1ad0e6043ae506027a7c0242546fdc851cca39a204e56879f2cef838be8ec66e0f2292f8c862e06f810eb9b80c7a467ce6e90155206352c7f82b1173ba3b98d35bb72c259a60db20dd1a43fe6d7aef0265e6eaa5caafd9b64b448ff745a2046acbdb65cf2a5007809808a4828dc99097feedc734c236260c584")
pkt, err := hex.DecodeString("cb0000000108181e17c387120abc000044d0705b6a3ef9ee37a8d3949a7d393ed078243c2ee2c3627fad1c3f107c117f4f071131ad61848068fcbbe5c65803c147f7f8ec5e2cd77b77beea23ba779d936dccac540f8396400e3190ea35cc2942af4171a04cb14272491920f90124959f44e80143678c0b52f5d31af319aaa589db2f940f004562724d0af40f737e1bb0002a071e6a1dbc9f52c64f070806a5010abed0298053634d9c9126bd7949ae5087998ade762c0ad06691d99c0875a38c601fc1ee77bfc3b8c11381829f2c9bdd022f4499c43ff1d6aee1a0d296861461dda217d22c568b276016ef3929e59d2f7d7ddf7809920fb7dc805641608949f3f8466ab3d37149aac501f0b107d808f3add4acfc657e4a82e2b88e97a6c74a00c419548760ab3414ba13915c78a1ca79dceee8d59fbe299f20b671ac44823218368b2a026baa55170cf549519ac21dbb6d31d248bd339438a4e663bcdca1fe3ae3f045a5dc19b122e9db9d7af9757076666dda4e9ace1c67def77fa14786f0cab3ebf7a270ea6e2b37838318c95779f80c3b8471948d0046c3614b3a13477c939a39a7855d85d13522a45ae0765739cd5eedef87237e824a929983ace27640c6495dbf5a72fa0b96893dc5d28f3988249a57bdb458d460b4a57043de3da750a76b6e5d2259247ca27cd864ea18f0d09aa62ab6eb7c014fb43179b2a1963d170b756cce83eeaebff78a828d025c811848e16ff862a8080d093478cd2208c8ab0803178325bc0d9d6bb25e62fa50c4ad15cf80916da6578796932036c72e43eb480d1e423ed812ac75a97722f8416529b82ba8ee2219c535012282bb17066bd53e78b87a71abdb7ebdb2a7c2766ff8397962e87d0f85485b64b4ee81cc84f99c47f33f2b0872716441992773f59186e38d32dbf5609a6fda94cb928cd25f5a7a3ab736b5a4236b6d5409ab18892c6a4d3480fc2350abfdf0bab1cedb55bdf0760fdb703e6688f4de596254eed4ed3e67eb03d0717b8e15b31e735214e588c87ae36bc6c310e1894b4c15143e4ccf287b2dbc707a946bf9671ae3c574f9486b2c82eec784bba4cbc76113cbe0f97ac8c13cfa38f2925ab9d06887a612ce48280a91d7e074e6caf898d88e2bbf71360899abf48a03f9a70cf2891199f2d63b116f4871af0ebb4f4906792f66cc21d1609f189138532875c129a68c73e7bcd3b5d8100beac1d8ac4b20d94a59ac8df5a5af58a9acb20413eadf97189f5f19ff889155f0c4d37514ec184eb6903967ff38a41fc087abb0f2cad3761d6e3f95f92a09a72f5c065b16e188088b87460241f27ecdb1bc6ece92c8d36b2d68b58d0fb4d4b3c928c579ade8ae5a995833aadd297c30a37f7bc35440fc97070e1b198e0fac00157452177d16d2803b4239997452b4ad3a951173bdec47a033fd7f8a7942accaa9aaa905b3c5a2175e7c3e07c48bf25331727fd69cd1e64d74d8c9d4a6f8f4491adb7bc911505cb19877083d8f21a12475e313fccf57877ff3556318e81ed9145dd9427f2b65275440893035f417481f721c69215af8ae103530cd0a1d35bf2cb5a27628f8d44d7c6f5ec12ce79d0a8333e0eb48771115d0a191304e46b8db19bbe5c40f1c346dde98e76ff5e21ff38d2c34e60cb07766ed529dd6d2cbacd7fbf1ed8a0e6e40decad0ca5021e91552be87c156d3ae2fffef41c65b14ba6d488f2c3227a1ab11ffce0e2dc47723a69da27a67a7f26e1cb13a7103af9b87a8db8e18ea")
require.NoError(t, err)
metadata, err := sniff.QUICClientHello(context.Background(), pkt)
var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.NoError(t, err)
require.Equal(t, metadata.Domain, "cloudflare-quic.com")
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientQUICGo)
require.Equal(t, metadata.Domain, "www.google.com")
}
func TestSniffQUICFirefox(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("c8000000010867f174d7ebfe1b0803cd9c20004286de068f7963cf1736349ee6ebe0ddcd3e4cd0041a51ced3f7ce9eea1fb595458e74bdb4b792b16449bd8cae71419862c4fcbe766eaec7d1af65cd298e1dd46f8bd94a77ab4ca28c54b8e9773de3f02d7cb2463c9f7dcacfb311f024b0266ec6ab7bfb615b4148333fb4d4ece7c4cd90029ca30c2cbae2216b428499ec873fa125797e71c5a5da85087760ad37ca610020f71b76e82651c47576e20bf33cf676cb2d400b8c09d3c8cb4e21c47d2b21f6b68732bef30c8cefd5c723fc23eb29e6f7f65a5e52aad9055c1fb3d8b1811f0380b38d7e2eee8eb37dd5bd5d4ca4b66540175d916289d88a9df7c161964d713999c5057d27edb298ef5164352568b0d4bac3c15d90456e8fd460e41b81d0ec1b1e94b87d3333cc6908b018e0914ae1f214d73e75398da3d55a0106161d3a75897b4eb66e98c59010fae75f0d367d38be48c3a5c58bc8a30773c3fff50690ac9d487822f85d4f5713d626baa92d36e858dd21259cf814bce0b90d18da88a1ade40113e5a088cdb304a2558879152a8cf15c1839e056378aa41acba6fcb9974dee54bd50b5d4eb2c475654e06c0ec06b7f18f4462c808684843a1071041b9bfb2688324e0120144944416e30e83eedbbbcbc275b1f53762d3db18f0998ce54f0e1c512946b4098f07781d49264fa148f4c8220a3b02e73d7f15554aa370aafeff73cb75c52c494edf90f0261abfdd32a4d670f729de50266162687aa8efe14b8506f313b058b02aaaab5825428f5f4510b8e49451fdcb7b5a4af4b59c831afcb89fb4f64dba78e3b38387e87e9e8cdaa1f3b700a87c7d442388863b8950296e5773b38f308d62f52548c0bbf308e40540747cca5bf99b1345bc0d70b8f0e69a83b85a8d69f795b87f93e2bfccf52b529afea4ff6fd456957000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientFirefox)
require.Equal(t, metadata.Domain, "www.google.com")
}
func TestSniffQUICSafari(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("c70000000108e4e75af2e223198a0000449ef2d83cb4473a62765eba67424cd4a5817315cbf55a9e8daaca360904b0bae60b1629cfeba11e2dfbbf5ea4c588cb134e31af36fd7a409fb0fcc0187e9b56037ac37964ed20a8c1ca19fd6cfd53398324b3d0c71537294f769db208fa998b6811234a4a7eb3b5eceb457ae92e3a2d98f7c110702db8064b5c29fa3298eb1d0529fd445a84a5fd6ff8709be90f8af4f94998d8a8f2953bb05ad08c80668eca784c6aec959114e68e5b827e7c41c79f2277c716a967e7fcc8d1b77442e6cb18329dbedb34b473516b468cba5fc20659e655fbe37f36408289b9a475fcee091bd82828d3be00367e9e5cec9423bb97854abdada1d7562a3777756eb3bddef826ddc1ef46137cb01bb504a54d410d9bcb74cd5f959050c84edf343fa6a49708c228a758ee7adbbadf260b2f1984911489712e2cb364a3d6520badba4b7e539b9c163eeddfd96c0abb0de151e47496bb9750be76ee17ccdb61d35d2c6795174037d6f9d282c3f36c4d9a90b64f3b6ddd0cf4d9ed8e6f7805e25928fa04b087e63ae02761df30720cc01dfc32b64c575c8a66ef82e9a17400ff80cd8609b93ba16d668f4aa734e71c4a5d145f14ee1151bec970214e0ff83fc3e1e85d8694f2975f9155c57c18b7b69bb6a36832a9435f1f4b346a7be188f3a75f9ad2cc6ad0a3d26d6fa7d4c1179bd49bd5989d15ba43ff602890107db96484695086627356750d7b2b3b714ba65d564654e8f60ac10f5b6d3bfb507e8eaa31bab1da2d676195046d165c7f8b32829c9f9b68d97b2af7ac04a1369357e4b65de2b2f24eaf27cc8d95e05db001adebe726f927a94e43e62ce671e6e306e16f05aafcbe6c49080e80286d7939f375023d110a5ad9069364ae928ca480454a9dcddd61bc48b7efeb716a5bd6c7cd39c486ceb20c738af6abf22ba1ddd8b4a3b781fc2f251173409e1aadccbd7514e97106d0ebfc3af6e59445f74cd733a1ba99b10fce3fb4e9f7c88f5e25b567f5ba2b8dabacd375e7faf7634bfa178cbe51aee63032c5126b196ea47b02385fc3062a000fb7e4b4d0d12e74579f8830ede20d10829496032b2cc56743287f9a9b4d5091877a82fea44deb2cffac8a379f78a151d99e28cbc74d732c083bf06d50584e3f18f254e71a48d6ababaf6fff6f425e9be001510dfbe6a32a27792c00ada036b62ddb90c706d7b882c76a7072f5dd11c69a1f49d4ba183cb0b57545419fa27b9b9706098848935ae9c9e8fbe9fac165d1339128b991a73d20e7795e8d6a8c6adfbf20bf13ada43f2aef3ba78c14697910507132623f721387dce60c4707225b84d9782d469a5d9eaa099f35d6a590ef142ddef766495cf3337815ceef5ff2b3ed352637e72b5c23a2a8ff7d7440236a19b981d47f8e519a0431ebfbc0b78d8a36798b4c060c0c6793499f1e2e818862560a5b501c8d02ba1517be1941da2af5b174e0189c62978d878eb0f9c9db3a9221c28fb94645cf6e85ff2eea8c65ba3083a7382b131b83102dd67aa5453ad7375a4eb8c69fc479fbd29dab8924f801d253f2c997120b705c6e5217fb74702e2f1038917dd5fb0eeb7ae1bf7a668fc7d50c034b4cd5a057a8482e6bc9c921297f44e76967265623a167cd9883eb6e64bc77856dc333bd605d7df3bed0e5cecb5a99fe8b62873d58530f")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientSafari)
require.Equal(t, metadata.Domain, "www.google.com")
}
func FuzzSniffQUIC(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
sniff.QUICClientHello(context.Background(), data)
var metadata adapter.InboundContext
err := sniff.QUICClientHello(context.Background(), &metadata, data)
require.Error(t, err)
})
}

View File

@@ -14,11 +14,11 @@ import (
)
type (
StreamSniffer = func(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error)
PacketSniffer = func(ctx context.Context, packet []byte) (*adapter.InboundContext, error)
StreamSniffer = func(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error
PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error
)
func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) (*adapter.InboundContext, error) {
func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) error {
if timeout == 0 {
timeout = C.ReadPayloadTimeout
}
@@ -28,7 +28,7 @@ func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout
for i := 0; i < 3; i++ {
err := conn.SetReadDeadline(deadline)
if err != nil {
return nil, E.Cause(err, "set read deadline")
return E.Cause(err, "set read deadline")
}
_, err = buffer.ReadOnceFrom(conn)
err = E.Errors(err, conn.SetReadDeadline(time.Time{}))
@@ -36,27 +36,27 @@ func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout
if i > 0 {
break
}
return nil, E.Cause(err, "read payload")
return E.Cause(err, "read payload")
}
for _, sniffer := range sniffers {
metadata, err := sniffer(ctx, bytes.NewReader(buffer.Bytes()))
if metadata != nil {
return metadata, nil
err = sniffer(ctx, metadata, bytes.NewReader(buffer.Bytes()))
if err == nil {
return nil
}
errors = append(errors, err)
}
}
return nil, E.Errors(errors...)
return E.Errors(errors...)
}
func PeekPacket(ctx context.Context, packet []byte, sniffers ...PacketSniffer) (*adapter.InboundContext, error) {
func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error {
var errors []error
for _, sniffer := range sniffers {
metadata, err := sniffer(ctx, packet)
if metadata != nil {
return metadata, nil
err := sniffer(ctx, metadata, packet)
if err == nil {
return nil
}
errors = append(errors, err)
}
return nil, E.Errors(errors...)
return E.Errors(errors...)
}

26
common/sniff/ssh.go Normal file
View File

@@ -0,0 +1,26 @@
package sniff
import (
"bufio"
"context"
"io"
"os"
"strings"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
)
func SSH(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
scanner := bufio.NewScanner(reader)
if !scanner.Scan() {
return os.ErrInvalid
}
fistLine := scanner.Text()
if !strings.HasPrefix(fistLine, "SSH-2.0-") {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolSSH
metadata.Client = fistLine[8:]
return nil
}

24
common/sniff/ssh_test.go Normal file
View File

@@ -0,0 +1,24 @@
package sniff_test
import (
"bytes"
"context"
"encoding/hex"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require"
"testing"
)
func TestSniffSSH(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("5353482d322e302d64726f70626561720d0a000001a40a1492892570d1223aef61b0d647972c8bd30000009f637572766532353531392d7368613235362c637572766532353531392d736861323536406c69627373682e6f72672c6469666669652d68656c6c6d616e2d67726f757031342d7368613235362c6469666669652d68656c6c6d616e2d67726f757031342d736861312c6b6578677565737332406d6174742e7563632e61736e2e61752c6b65782d7374726963742d732d763030406f70656e7373682e636f6d000000207373682d656432353531392c7273612d736861322d3235362c7373682d7273610000003363686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d6374722c6165733235362d6374720000003363686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d6374722c6165733235362d63747200000017686d61632d736861312c686d61632d736861322d32353600000017686d61632d736861312c686d61632d736861322d323536000000046e6f6e65000000046e6f6e65000000000000000000000000002aa6ed090585b7d635b6")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt))
require.NoError(t, err)
require.Equal(t, C.ProtocolSSH, metadata.Protocol)
require.Equal(t, "dropbear", metadata.Client)
}

View File

@@ -9,16 +9,17 @@ import (
C "github.com/sagernet/sing-box/constant"
)
func STUNMessage(ctx context.Context, packet []byte) (*adapter.InboundContext, error) {
func STUNMessage(_ context.Context, metadata *adapter.InboundContext, packet []byte) error {
pLen := len(packet)
if pLen < 20 {
return nil, os.ErrInvalid
return os.ErrInvalid
}
if binary.BigEndian.Uint32(packet[4:8]) != 0x2112A442 {
return nil, os.ErrInvalid
return os.ErrInvalid
}
if len(packet) < 20+int(binary.BigEndian.Uint16(packet[2:4])) {
return nil, os.ErrInvalid
return os.ErrInvalid
}
return &adapter.InboundContext{Protocol: C.ProtocolSTUN}, nil
metadata.Protocol = C.ProtocolSTUN
return nil
}

View File

@@ -5,6 +5,7 @@ import (
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
@@ -15,14 +16,16 @@ func TestSniffSTUN(t *testing.T) {
t.Parallel()
packet, err := hex.DecodeString("000100002112a44224b1a025d0c180c484341306")
require.NoError(t, err)
metadata, err := sniff.STUNMessage(context.Background(), packet)
var metadata adapter.InboundContext
err = sniff.STUNMessage(context.Background(), &metadata, packet)
require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolSTUN)
}
func FuzzSniffSTUN(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
if _, err := sniff.STUNMessage(context.Background(), data); err == nil {
var metadata adapter.InboundContext
if err := sniff.STUNMessage(context.Background(), &metadata, data); err == nil {
t.Fail()
}
})

View File

@@ -10,7 +10,7 @@ import (
"github.com/sagernet/sing/common/bufio"
)
func TLSClientHello(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error) {
func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
var clientHello *tls.ClientHelloInfo
err := tls.Server(bufio.NewReadOnlyConn(reader), &tls.Config{
GetConfigForClient: func(argHello *tls.ClientHelloInfo) (*tls.Config, error) {
@@ -19,7 +19,9 @@ func TLSClientHello(ctx context.Context, reader io.Reader) (*adapter.InboundCont
},
}).HandshakeContext(ctx)
if clientHello != nil {
return &adapter.InboundContext{Protocol: C.ProtocolTLS, Domain: clientHello.ServerName}, nil
metadata.Protocol = C.ProtocolTLS
metadata.Domain = clientHello.ServerName
return nil
}
return nil, err
return err
}

View File

@@ -1,6 +1,7 @@
package srs
import (
"bufio"
"compress/zlib"
"encoding/binary"
"io"
@@ -11,7 +12,7 @@ import (
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/domain"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/varbin"
"go4.org/netipx"
)
@@ -35,17 +36,18 @@ const (
ruleItemPackageName
ruleItemWIFISSID
ruleItemWIFIBSSID
ruleItemAdGuardDomain
ruleItemFinal uint8 = 0xFF
)
func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) {
func Read(reader io.Reader, recover bool) (ruleSet option.PlainRuleSet, err error) {
var magicBytes [3]byte
_, err = io.ReadFull(reader, magicBytes[:])
if err != nil {
return
}
if magicBytes != MagicBytes {
err = E.New("invalid sing-box rule set file")
err = E.New("invalid sing-box rule-set file")
return
}
var version uint8
@@ -53,20 +55,21 @@ func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err err
if err != nil {
return ruleSet, err
}
if version != 1 {
if version > C.RuleSetVersion2 {
return ruleSet, E.New("unsupported version: ", version)
}
zReader, err := zlib.NewReader(reader)
compressReader, err := zlib.NewReader(reader)
if err != nil {
return
}
length, err := rw.ReadUVariant(zReader)
bReader := bufio.NewReader(compressReader)
length, err := binary.ReadUvarint(bReader)
if err != nil {
return
}
ruleSet.Rules = make([]option.HeadlessRule, length)
for i := uint64(0); i < length; i++ {
ruleSet.Rules[i], err = readRule(zReader, recovery)
ruleSet.Rules[i], err = readRule(bReader, recover)
if err != nil {
err = E.Cause(err, "read rule[", i, "]")
return
@@ -75,33 +78,44 @@ func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err err
return
}
func Write(writer io.Writer, ruleSet option.PlainRuleSet) error {
func Write(writer io.Writer, ruleSet option.PlainRuleSet, generateUnstable bool) error {
_, err := writer.Write(MagicBytes[:])
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, uint8(1))
var version uint8
if generateUnstable {
version = C.RuleSetVersion2
} else {
version = C.RuleSetVersion1
}
err = binary.Write(writer, binary.BigEndian, version)
if err != nil {
return err
}
zWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression)
compressWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression)
if err != nil {
return err
}
err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules)))
bWriter := bufio.NewWriter(compressWriter)
_, err = varbin.WriteUvarint(bWriter, uint64(len(ruleSet.Rules)))
if err != nil {
return err
}
for _, rule := range ruleSet.Rules {
err = writeRule(zWriter, rule)
err = writeRule(bWriter, rule, generateUnstable)
if err != nil {
return err
}
}
return zWriter.Close()
err = bWriter.Flush()
if err != nil {
return err
}
return compressWriter.Close()
}
func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) {
func readRule(reader varbin.Reader, recover bool) (rule option.HeadlessRule, err error) {
var ruleType uint8
err = binary.Read(reader, binary.BigEndian, &ruleType)
if err != nil {
@@ -110,28 +124,28 @@ func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err er
switch ruleType {
case 0:
rule.Type = C.RuleTypeDefault
rule.DefaultOptions, err = readDefaultRule(reader, recovery)
rule.DefaultOptions, err = readDefaultRule(reader, recover)
case 1:
rule.Type = C.RuleTypeLogical
rule.LogicalOptions, err = readLogicalRule(reader, recovery)
rule.LogicalOptions, err = readLogicalRule(reader, recover)
default:
err = E.New("unknown rule type: ", ruleType)
}
return
}
func writeRule(writer io.Writer, rule option.HeadlessRule) error {
func writeRule(writer varbin.Writer, rule option.HeadlessRule, generateUnstable bool) error {
switch rule.Type {
case C.RuleTypeDefault:
return writeDefaultRule(writer, rule.DefaultOptions)
return writeDefaultRule(writer, rule.DefaultOptions, generateUnstable)
case C.RuleTypeLogical:
return writeLogicalRule(writer, rule.LogicalOptions)
return writeLogicalRule(writer, rule.LogicalOptions, generateUnstable)
default:
panic("unknown rule type: " + rule.Type)
}
}
func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) {
func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHeadlessRule, err error) {
var lastItemType uint8
for {
var itemType uint8
@@ -158,6 +172,9 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle
return
}
rule.DomainMatcher = matcher
if recover {
rule.Domain, rule.DomainSuffix = matcher.Dump()
}
case ruleItemDomainKeyword:
rule.DomainKeyword, err = readRuleItemString(reader)
case ruleItemDomainRegex:
@@ -167,7 +184,7 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle
if err != nil {
return
}
if recovery {
if recover {
rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String)
}
case ruleItemIPCIDR:
@@ -175,7 +192,7 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle
if err != nil {
return
}
if recovery {
if recover {
rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String)
}
case ruleItemSourcePort:
@@ -196,6 +213,17 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle
rule.WIFISSID, err = readRuleItemString(reader)
case ruleItemWIFIBSSID:
rule.WIFIBSSID, err = readRuleItemString(reader)
case ruleItemAdGuardDomain:
if recover {
err = E.New("unable to decompile binary AdGuard rules to rule-set")
return
}
var matcher *domain.AdGuardMatcher
matcher, err = domain.ReadAdGuardMatcher(reader)
if err != nil {
return
}
rule.AdGuardDomainMatcher = matcher
case ruleItemFinal:
err = binary.Read(reader, binary.BigEndian, &rule.Invert)
return
@@ -209,7 +237,7 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle
}
}
func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error {
func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, generateUnstable bool) error {
err := binary.Write(writer, binary.BigEndian, uint8(0))
if err != nil {
return err
@@ -233,7 +261,7 @@ func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error {
if err != nil {
return err
}
err = domain.NewMatcher(rule.Domain, rule.DomainSuffix).Write(writer)
err = domain.NewMatcher(rule.Domain, rule.DomainSuffix, !generateUnstable).Write(writer)
if err != nil {
return err
}
@@ -316,6 +344,16 @@ func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error {
return err
}
}
if len(rule.AdGuardDomain) > 0 {
err = binary.Write(writer, binary.BigEndian, ruleItemAdGuardDomain)
if err != nil {
return err
}
err = domain.NewAdGuardMatcher(rule.AdGuardDomain).Write(writer)
if err != nil {
return err
}
}
err = binary.Write(writer, binary.BigEndian, ruleItemFinal)
if err != nil {
return err
@@ -327,73 +365,31 @@ func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error {
return nil
}
func readRuleItemString(reader io.Reader) ([]string, error) {
length, err := rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
value := make([]string, length)
for i := uint64(0); i < length; i++ {
value[i], err = rw.ReadVString(reader)
if err != nil {
return nil, err
}
}
return value, nil
func readRuleItemString(reader varbin.Reader) ([]string, error) {
return varbin.ReadValue[[]string](reader, binary.BigEndian)
}
func writeRuleItemString(writer io.Writer, itemType uint8, value []string) error {
err := binary.Write(writer, binary.BigEndian, itemType)
func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) error {
err := writer.WriteByte(itemType)
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(value)))
return varbin.Write(writer, binary.BigEndian, value)
}
func readRuleItemUint16(reader varbin.Reader) ([]uint16, error) {
return varbin.ReadValue[[]uint16](reader, binary.BigEndian)
}
func writeRuleItemUint16(writer varbin.Writer, itemType uint8, value []uint16) error {
err := writer.WriteByte(itemType)
if err != nil {
return err
}
for _, item := range value {
err = rw.WriteVString(writer, item)
if err != nil {
return err
}
}
return nil
return varbin.Write(writer, binary.BigEndian, value)
}
func readRuleItemUint16(reader io.Reader) ([]uint16, error) {
length, err := rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
value := make([]uint16, length)
for i := uint64(0); i < length; i++ {
err = binary.Read(reader, binary.BigEndian, &value[i])
if err != nil {
return nil, err
}
}
return value, nil
}
func writeRuleItemUint16(writer io.Writer, itemType uint8, value []uint16) error {
err := binary.Write(writer, binary.BigEndian, itemType)
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(value)))
if err != nil {
return err
}
for _, item := range value {
err = binary.Write(writer, binary.BigEndian, item)
if err != nil {
return err
}
}
return nil
}
func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error {
func writeRuleItemCIDR(writer varbin.Writer, itemType uint8, value []string) error {
var builder netipx.IPSetBuilder
for i, prefixString := range value {
prefix, err := netip.ParsePrefix(prefixString)
@@ -419,9 +415,8 @@ func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error {
return writeIPSet(writer, ipSet)
}
func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) {
var mode uint8
err = binary.Read(reader, binary.BigEndian, &mode)
func readLogicalRule(reader varbin.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) {
mode, err := reader.ReadByte()
if err != nil {
return
}
@@ -434,7 +429,7 @@ func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.Logica
err = E.New("unknown logical mode: ", mode)
return
}
length, err := rw.ReadUVariant(reader)
length, err := binary.ReadUvarint(reader)
if err != nil {
return
}
@@ -453,7 +448,7 @@ func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.Logica
return
}
func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error {
func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRule, generateUnstable bool) error {
err := binary.Write(writer, binary.BigEndian, uint8(1))
if err != nil {
return err
@@ -469,12 +464,12 @@ func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule)
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules)))
_, err = varbin.WriteUvarint(writer, uint64(len(logicalRule.Rules)))
if err != nil {
return err
}
for _, rule := range logicalRule.Rules {
err = writeRule(writer, rule)
err = writeRule(writer, rule, generateUnstable)
if err != nil {
return err
}

View File

@@ -2,11 +2,13 @@ package srs
import (
"encoding/binary"
"io"
"net/netip"
"os"
"unsafe"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/varbin"
"go4.org/netipx"
)
@@ -20,94 +22,57 @@ type myIPRange struct {
to netip.Addr
}
func readIPSet(reader io.Reader) (*netipx.IPSet, error) {
var version uint8
err := binary.Read(reader, binary.BigEndian, &version)
type myIPRangeData struct {
From []byte
To []byte
}
func readIPSet(reader varbin.Reader) (*netipx.IPSet, error) {
version, err := reader.ReadByte()
if err != nil {
return nil, err
}
if version != 1 {
return nil, os.ErrInvalid
}
// WTF why using uint64 here
var length uint64
err = binary.Read(reader, binary.BigEndian, &length)
if err != nil {
return nil, err
}
mySet := &myIPSet{
rr: make([]myIPRange, length),
ranges := make([]myIPRangeData, length)
err = varbin.Read(reader, binary.BigEndian, &ranges)
if err != nil {
return nil, err
}
for i := uint64(0); i < length; i++ {
var (
fromLen uint64
toLen uint64
fromAddr netip.Addr
toAddr netip.Addr
)
fromLen, err = rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
fromBytes := make([]byte, fromLen)
_, err = io.ReadFull(reader, fromBytes)
if err != nil {
return nil, err
}
err = fromAddr.UnmarshalBinary(fromBytes)
if err != nil {
return nil, err
}
toLen, err = rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
toBytes := make([]byte, toLen)
_, err = io.ReadFull(reader, toBytes)
if err != nil {
return nil, err
}
err = toAddr.UnmarshalBinary(toBytes)
if err != nil {
return nil, err
}
mySet.rr[i] = myIPRange{fromAddr, toAddr}
mySet := &myIPSet{
rr: make([]myIPRange, len(ranges)),
}
for i, rangeData := range ranges {
mySet.rr[i].from = M.AddrFromIP(rangeData.From)
mySet.rr[i].to = M.AddrFromIP(rangeData.To)
}
return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil
}
func writeIPSet(writer io.Writer, set *netipx.IPSet) error {
err := binary.Write(writer, binary.BigEndian, uint8(1))
func writeIPSet(writer varbin.Writer, set *netipx.IPSet) error {
err := writer.WriteByte(1)
if err != nil {
return err
}
mySet := (*myIPSet)(unsafe.Pointer(set))
err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr)))
dataList := common.Map((*myIPSet)(unsafe.Pointer(set)).rr, func(rr myIPRange) myIPRangeData {
return myIPRangeData{
From: rr.from.AsSlice(),
To: rr.to.AsSlice(),
}
})
err = binary.Write(writer, binary.BigEndian, uint64(len(dataList)))
if err != nil {
return err
}
for _, rr := range mySet.rr {
var (
fromBinary []byte
toBinary []byte
)
fromBinary, err = rr.from.MarshalBinary()
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(fromBinary)))
if err != nil {
return err
}
_, err = writer.Write(fromBinary)
if err != nil {
return err
}
toBinary, err = rr.to.MarshalBinary()
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(toBinary)))
if err != nil {
return err
}
_, err = writer.Write(toBinary)
for _, data := range dataList {
err = varbin.Write(writer, binary.BigEndian, data)
if err != nil {
return err
}

View File

@@ -27,11 +27,10 @@ func (c *echClientConfig) DialEarly(ctx context.Context, conn net.PacketConn, ad
return quic.DialEarly(ctx, conn, addr, c.config, config)
}
func (c *echClientConfig) CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config, enableDatagrams bool) http.RoundTripper {
func (c *echClientConfig) CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config) http.RoundTripper {
return &http3.RoundTripper{
TLSClientConfig: c.config,
QuicConfig: quicConfig,
EnableDatagrams: enableDatagrams,
QUICConfig: quicConfig,
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
quicConn, err := quic.DialEarly(ctx, conn, serverAddr.UDPAddr(), tlsCfg, cfg)
if err != nil {

View File

@@ -11,12 +11,11 @@ import (
"strings"
cftls "github.com/sagernet/cloudflare-tls"
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
"github.com/fsnotify/fsnotify"
)
type echServerConfig struct {
@@ -26,9 +25,8 @@ type echServerConfig struct {
key []byte
certificatePath string
keyPath string
watcher *fsnotify.Watcher
echKeyPath string
echWatcher *fsnotify.Watcher
watcher *fswatch.Watcher
}
func (c *echServerConfig) ServerName() string {
@@ -66,146 +64,84 @@ func (c *echServerConfig) Clone() Config {
}
func (c *echServerConfig) Start() error {
if c.certificatePath != "" && c.keyPath != "" {
err := c.startWatcher()
if err != nil {
c.logger.Warn("create fsnotify watcher: ", err)
}
}
if c.echKeyPath != "" {
err := c.startECHWatcher()
if err != nil {
c.logger.Warn("create fsnotify watcher: ", err)
}
err := c.startWatcher()
if err != nil {
c.logger.Warn("create credentials watcher: ", err)
}
return nil
}
func (c *echServerConfig) startWatcher() error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
var watchPath []string
if c.certificatePath != "" {
err = watcher.Add(c.certificatePath)
if err != nil {
return err
}
watchPath = append(watchPath, c.certificatePath)
}
if c.keyPath != "" {
err = watcher.Add(c.keyPath)
if err != nil {
return err
}
watchPath = append(watchPath, c.keyPath)
}
if c.echKeyPath != "" {
watchPath = append(watchPath, c.echKeyPath)
}
if len(watchPath) == 0 {
return nil
}
watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: watchPath,
Callback: func(path string) {
err := c.credentialsUpdated(path)
if err != nil {
c.logger.Error(E.Cause(err, "reload credentials from ", path))
}
},
})
if err != nil {
return err
}
c.watcher = watcher
go c.loopUpdate()
return nil
}
func (c *echServerConfig) loopUpdate() {
for {
select {
case event, ok := <-c.watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write != fsnotify.Write {
continue
}
err := c.reloadKeyPair()
func (c *echServerConfig) credentialsUpdated(path string) error {
if path == c.certificatePath || path == c.keyPath {
if path == c.certificatePath {
certificate, err := os.ReadFile(c.certificatePath)
if err != nil {
c.logger.Error(E.Cause(err, "reload TLS key pair"))
return err
}
case err, ok := <-c.watcher.Errors:
if !ok {
return
}
c.logger.Error(E.Cause(err, "fsnotify error"))
}
}
}
func (c *echServerConfig) reloadKeyPair() error {
if c.certificatePath != "" {
certificate, err := os.ReadFile(c.certificatePath)
if err != nil {
return E.Cause(err, "reload certificate from ", c.certificatePath)
}
c.certificate = certificate
}
if c.keyPath != "" {
key, err := os.ReadFile(c.keyPath)
if err != nil {
return E.Cause(err, "reload key from ", c.keyPath)
}
c.key = key
}
keyPair, err := cftls.X509KeyPair(c.certificate, c.key)
if err != nil {
return E.Cause(err, "reload key pair")
}
c.config.Certificates = []cftls.Certificate{keyPair}
c.logger.Info("reloaded TLS certificate")
return nil
}
func (c *echServerConfig) startECHWatcher() error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
err = watcher.Add(c.echKeyPath)
if err != nil {
return err
}
c.echWatcher = watcher
go c.loopECHUpdate()
return nil
}
func (c *echServerConfig) loopECHUpdate() {
for {
select {
case event, ok := <-c.echWatcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write != fsnotify.Write {
continue
}
err := c.reloadECHKey()
c.certificate = certificate
} else {
key, err := os.ReadFile(c.keyPath)
if err != nil {
c.logger.Error(E.Cause(err, "reload ECH key"))
return err
}
case err, ok := <-c.echWatcher.Errors:
if !ok {
return
}
c.logger.Error(E.Cause(err, "fsnotify error"))
c.key = key
}
keyPair, err := cftls.X509KeyPair(c.certificate, c.key)
if err != nil {
return E.Cause(err, "parse key pair")
}
c.config.Certificates = []cftls.Certificate{keyPair}
c.logger.Info("reloaded TLS certificate")
} else {
echKeyContent, err := os.ReadFile(c.echKeyPath)
if err != nil {
return err
}
block, rest := pem.Decode(echKeyContent)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return E.New("invalid ECH keys pem")
}
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
if err != nil {
return E.Cause(err, "parse ECH keys")
}
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
if err != nil {
return E.Cause(err, "create ECH key set")
}
c.config.ServerECHProvider = echKeySet
c.logger.Info("reloaded ECH keys")
}
}
func (c *echServerConfig) reloadECHKey() error {
echKeyContent, err := os.ReadFile(c.echKeyPath)
if err != nil {
return err
}
block, rest := pem.Decode(echKeyContent)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return E.New("invalid ECH keys pem")
}
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
if err != nil {
return E.Cause(err, "parse ECH keys")
}
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
if err != nil {
return E.Cause(err, "create ECH key set")
}
c.config.ServerECHProvider = echKeySet
c.logger.Info("reloaded ECH keys")
return nil
}
@@ -213,12 +149,7 @@ func (c *echServerConfig) Close() error {
var err error
if c.watcher != nil {
err = E.Append(err, c.watcher.Close(), func(err error) error {
return E.Cause(err, "close certificate watcher")
})
}
if c.echWatcher != nil {
err = E.Append(err, c.echWatcher.Close(), func(err error) error {
return E.Cause(err, "close ECH key watcher")
return E.Cause(err, "close credentials watcher")
})
}
return err

View File

@@ -7,14 +7,13 @@ import (
"os"
"strings"
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
"github.com/fsnotify/fsnotify"
)
var errInsecureUnused = E.New("tls: insecure unused")
@@ -27,7 +26,7 @@ type STDServerConfig struct {
key []byte
certificatePath string
keyPath string
watcher *fsnotify.Watcher
watcher *fswatch.Watcher
}
func (c *STDServerConfig) ServerName() string {
@@ -88,59 +87,37 @@ func (c *STDServerConfig) Start() error {
}
func (c *STDServerConfig) startWatcher() error {
watcher, err := fsnotify.NewWatcher()
var watchPath []string
if c.certificatePath != "" {
watchPath = append(watchPath, c.certificatePath)
}
if c.keyPath != "" {
watchPath = append(watchPath, c.keyPath)
}
watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: watchPath,
Callback: func(path string) {
err := c.certificateUpdated(path)
if err != nil {
c.logger.Error(err)
}
},
})
if err != nil {
return err
}
if c.certificatePath != "" {
err = watcher.Add(c.certificatePath)
if err != nil {
return err
}
}
if c.keyPath != "" {
err = watcher.Add(c.keyPath)
if err != nil {
return err
}
}
c.watcher = watcher
go c.loopUpdate()
return nil
}
func (c *STDServerConfig) loopUpdate() {
for {
select {
case event, ok := <-c.watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write != fsnotify.Write {
continue
}
err := c.reloadKeyPair()
if err != nil {
c.logger.Error(E.Cause(err, "reload TLS key pair"))
}
case err, ok := <-c.watcher.Errors:
if !ok {
return
}
c.logger.Error(E.Cause(err, "fsnotify error"))
}
}
}
func (c *STDServerConfig) reloadKeyPair() error {
if c.certificatePath != "" {
func (c *STDServerConfig) certificateUpdated(path string) error {
if path == c.certificatePath {
certificate, err := os.ReadFile(c.certificatePath)
if err != nil {
return E.Cause(err, "reload certificate from ", c.certificatePath)
}
c.certificate = certificate
}
if c.keyPath != "" {
} else if path == c.keyPath {
key, err := os.ReadFile(c.keyPath)
if err != nil {
return E.Cause(err, "reload key from ", c.keyPath)

View File

@@ -13,14 +13,14 @@ var resourcePaths []string
func FindPath(name string) (string, bool) {
name = os.ExpandEnv(name)
if rw.FileExists(name) {
if rw.IsFile(name) {
return name, true
}
for _, dir := range resourcePaths {
if path := filepath.Join(dir, dirName, name); rw.FileExists(path) {
if path := filepath.Join(dir, dirName, name); rw.IsFile(path) {
return path, true
}
if path := filepath.Join(dir, name); rw.FileExists(path) {
if path := filepath.Join(dir, name); rw.IsFile(path) {
return path, true
}
}

View File

@@ -1,9 +1,20 @@
package constant
const (
ProtocolTLS = "tls"
ProtocolHTTP = "http"
ProtocolQUIC = "quic"
ProtocolDNS = "dns"
ProtocolSTUN = "stun"
ProtocolTLS = "tls"
ProtocolHTTP = "http"
ProtocolQUIC = "quic"
ProtocolDNS = "dns"
ProtocolSTUN = "stun"
ProtocolBitTorrent = "bittorrent"
ProtocolDTLS = "dtls"
ProtocolSSH = "ssh"
)
const (
ClientChromium = "chromium"
ClientSafari = "safari"
ClientFirefox = "firefox"
ClientQUICGo = "quic-go"
ClientUnknown = "unknown"
)

View File

@@ -32,6 +32,12 @@ const (
func ProxyDisplayName(proxyType string) string {
switch proxyType {
case TypeTun:
return "TUN"
case TypeRedirect:
return "Redirect"
case TypeTProxy:
return "TProxy"
case TypeDirect:
return "Direct"
case TypeBlock:
@@ -42,6 +48,8 @@ func ProxyDisplayName(proxyType string) string {
return "SOCKS"
case TypeHTTP:
return "HTTP"
case TypeMixed:
return "Mixed"
case TypeShadowsocks:
return "Shadowsocks"
case TypeVMess:

5
constant/quic.go Normal file
View File

@@ -0,0 +1,5 @@
//go:build with_quic
package constant
const WithQUIC = true

5
constant/quic_stub.go Normal file
View File

@@ -0,0 +1,5 @@
//go:build !with_quic
package constant
const WithQUIC = false

View File

@@ -11,9 +11,14 @@ const (
)
const (
RuleSetTypeInline = "inline"
RuleSetTypeLocal = "local"
RuleSetTypeRemote = "remote"
RuleSetVersion1 = 1
RuleSetFormatSource = "source"
RuleSetFormatBinary = "binary"
)
const (
RuleSetVersion1 = 1 + iota
RuleSetVersion2
)

View File

@@ -3,6 +3,8 @@ package constant
import "time"
const (
TCPKeepAliveInitial = 10 * time.Minute
TCPKeepAliveInterval = 75 * time.Second
TCPTimeout = 5 * time.Second
ReadPayloadTimeout = 300 * time.Millisecond
DNSTimeout = 10 * time.Second

View File

@@ -1,5 +1,3 @@
//go:build go1.19
package box
import (

View File

@@ -1,36 +0,0 @@
//go:build !go1.19
package box
import (
"runtime/debug"
"github.com/sagernet/sing-box/common/conntrack"
"github.com/sagernet/sing-box/option"
)
func applyDebugOptions(options option.DebugOptions) {
applyDebugListenOption(options)
if options.GCPercent != nil {
debug.SetGCPercent(*options.GCPercent)
}
if options.MaxStack != nil {
debug.SetMaxStack(*options.MaxStack)
}
if options.MaxThreads != nil {
debug.SetMaxThreads(*options.MaxThreads)
}
if options.PanicOnFault != nil {
debug.SetPanicOnFault(*options.PanicOnFault)
}
if options.TraceBack != "" {
debug.SetTraceback(options.TraceBack)
}
if options.MemoryLimit != 0 {
// debug.SetMemoryLimit(int64(options.MemoryLimit))
conntrack.MemoryLimit = uint64(options.MemoryLimit)
}
if options.OOMKiller != nil {
conntrack.KillerEnabled = *options.OOMKiller
}
}

View File

@@ -2,6 +2,365 @@
icon: material/alert-decagram
---
!!! failure "Help needed"
Due to problems with our Apple developer account, sing-box apps on Apple platforms are temporarily unavailable for download or update.
If your company or organization is willing to help us return to the App Store, please [contact us](mailto:contact@sagernet.org).
#### 1.10.0-beta.3
* Add SSH sniffer
* Fixes and improvements
#### 1.10.0-beta.2
* Build with go1.23
* Fixes and improvements
### 1.9.4
* Update quic-go to v0.46.0
* Update Hysteria2 BBR congestion control
* Filter HTTPS ipv4hint/ipv6hint with domain strategy
* Fix crash on Android when using process rules
* Fix non-IP queries accepted by address filter rules
* Fix UDP server for shadowsocks AEAD multi-user inbounds
* Fix default next protos for v2ray QUIC transport
* Fix default end value of port range configuration options
* Fix reset v2ray transports
* Fix panic caused by rule-set generation of duplicate keys for `domain_suffix`
* Fix UDP connnection leak when sniffing
* Fixes and improvements
#### 1.10.0-alpha.29
* Update quic-go to v0.46.0
* Fixes and improvements
#### 1.10.0-alpha.25
* Add AdGuard DNS Filter support **1**
**1**:
The new feature allows you to use AdGuard DNS Filter lists in a sing-box without AdGuard Home.
See [AdGuard DNS Filter](/configuration/rule-set/adguard/).
#### 1.10.0-alpha.23
* Add Chromium support for QUIC sniffer
* Add client type detect support for QUIC sniffer **1**
* Fixes and improvements
**1**:
Now the QUIC sniffer can correctly extract the server name from Chromium requests and
can identify common QUIC clients, including
Chromium, Safari, Firefox, quic-go (including uquic disguised as Chrome).
See [Protocol Sniff](/configuration/route/sniff/) and [Route Rule](/configuration/route/rule/#client).
#### 1.10.0-alpha.22
* Optimize memory usages of rule-sets **1**
* Fixes and improvements
**1**:
See [Source Format](/configuration/rule-set/source-format/#version).
#### 1.10.0-alpha.20
* Add DTLS sniffer
* Fixes and improvements
#### 1.10.0-alpha.19
* Add `rule-set decompile` command
* Add IP address support for `rule-set match` command
* Fixes and improvements
#### 1.10.0-alpha.18
* Add new `inline` rule-set type **1**
* Add auto reload support for local rule-set
* Update fsnotify usages **2**
* Fixes and improvements
**1**:
The new [rule-set] type inline (which also becomes the default type)
allows you to write headless rules directly without creating a rule-set file.
[rule-set]: /configuration/rule-set/
**2**:
sing-box now uses fsnotify correctly and will not cancel watching
if the target file is deleted or recreated via rename (e.g. `mv`).
This affects all path options that support reload, including
`tls.certificate_path`, `tls.key_path`, `tls.ech.key_path` and `rule_set.path`.
#### 1.10.0-alpha.17
* Some chaotic changes **1**
* `rule_set_ipcidr_match_source` rule items are renamed **2**
* Add `rule_set_ip_cidr_accept_empty` DNS address filter rule item **3**
* Update quic-go to v0.45.1
* Fixes and improvements
**1**:
Something may be broken, please actively report problems with this version.
**2**:
`rule_set_ipcidr_match_source` route and DNS rule items are renamed to
`rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0.
**3**:
See [DNS Rule](/configuration/dns/rule/#rule_set_ip_cidr_accept_empty).
#### 1.10.0-alpha.16
* Add custom options for `auto-route` and `auto-redirect` **1**
* Fixes and improvements
**1**:
See [iproute2_table_index](/configuration/inbound/tun/#iproute2_table_index),
[iproute2_rule_index](/configuration/inbound/tun/#iproute2_rule_index),
[auto_redirect_input_mark](/configuration/inbound/tun/#auto_redirect_input_mark) and
[auto_redirect_output_mark](/configuration/inbound/tun/#auto_redirect_output_mark).
#### 1.10.0-alpha.13
* TUN address fields are merged **1**
* Add route address set support for auto-redirect **2**
**1**:
See [Migration](/migration/#tun-address-fields-are-merged).
**2**:
The new feature will allow you to configure the destination IP CIDR rules
in the specified rule-sets to the firewall automatically.
Specified or unspecified destinations will bypass the sing-box routes to get better performance
(for example, keep hardware offloading of direct traffics on the router).
See [route_address_set](/configuration/inbound/tun/#route_address_set)
and [route_exclude_address_set](/configuration/inbound/tun/#route_exclude_address_set).
#### 1.10.0-alpha.12
* Fix auto-redirect not configuring nftables forward chain correctly
* Fixes and improvements
### 1.9.3
* Fixes and improvements
#### 1.10.0-alpha.10
* Fixes and improvements
### 1.9.2
* Fixes and improvements
#### 1.10.0-alpha.8
* Drop support for go1.18 and go1.19 **1**
* Update quic-go to v0.45.0
* Update Hysteria2 BBR congestion control
* Fixes and improvements
**1**:
Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile.
### 1.9.1
* Fixes and improvements
#### 1.10.0-alpha.7
* Fixes and improvements
#### 1.10.0-alpha.5
* Improve auto-redirect **1**
**1**:
nftables support and DNS hijacking has been added.
Tun inbounds with `auto_route` and `auto_redirect` now works as expected on routers **without intervention**.
#### 1.10.0-alpha.4
* Fix auto-redirect **1**
* Improve auto-route on linux **2**
**1**:
Tun inbounds with `auto_route` and `auto_redirect` now works as expected on routers.
**2**:
Tun inbounds with `auto_route` and `strict_route` now works as expected on routers and servers,
but the usages of [exclude_interface](/configuration/inbound/tun/#exclude_interface) need to be updated.
#### 1.10.0-alpha.2
* Move auto-redirect to Tun **1**
* Fixes and improvements
**1**:
Linux support are added.
See [Tun](/configuration/inbound/tun/#auto_redirect).
#### 1.10.0-alpha.1
* Add tailing comma support in JSON configuration
* Add simple auto-redirect for Android **1**
* Add BitTorrent sniffer **2**
**1**:
It allows you to use redirect inbound in the sing-box Android client
and automatically configures IPv4 TCP redirection via su.
This may alleviate the symptoms of some OCD patients who think that
redirect can effectively save power compared to the system HTTP Proxy.
See [Redirect](/configuration/inbound/redirect/).
**2**:
See [Protocol Sniff](/configuration/route/sniff/).
### 1.9.0
* Fixes and improvements
Important changes since 1.8:
* `domain_suffix` behavior update **1**
* `process_path` format update on Windows **2**
* Add address filter DNS rule items **3**
* Add support for `client-subnet` DNS options **4**
* Add rejected DNS response cache support **5**
* Add `bypass_domain` and `search_domain` platform HTTP proxy options **6**
* Fix missing `rule_set_ipcidr_match_source` item in DNS rules **7**
* Handle Windows power events
* Always disable cache for fake-ip DNS transport if `dns.independent_cache` disabled
* Improve DNS truncate behavior
* Update Hysteria protocol
* Update quic-go to v0.43.1
* Update gVisor to 20240422.0
* Mitigating TunnelVision attacks **8**
**1**:
See [Migration](/migration/#domain_suffix-behavior-update).
**2**:
See [Migration](/migration/#process_path-format-update-on-windows).
**3**:
The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS
if using this method.
See [Address Filter Fields](/configuration/dns/rule#address-filter-fields).
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.
**4**:
See [DNS](/configuration/dns), [DNS Server](/configuration/dns/server) and [DNS Rules](/configuration/dns/rule).
Since this feature makes the scenario mentioned in `alpha.1` no longer leak DNS requests,
the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) has been updated.
**5**:
The new feature allows you to cache the check results of
[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration.
**6**:
See [TUN](/configuration/inbound/tun) inbound.
**7**:
See [DNS Rule](/configuration/dns/rule/).
**8**:
See [TunnelVision](/manual/misc/tunnelvision).
#### 1.9.0-rc.22
* Fixes and improvements
#### 1.9.0-rc.20
* Prioritize `*_route_address` in linux auto-route
* Fix `*_route_address` in darwin auto-route
#### 1.8.14
* Fix hysteria2 panic
* Fixes and improvements
#### 1.9.0-rc.18
* Add custom prefix support in EDNS0 client subnet options
* Fix hysteria2 crash
* Fix `store_rdrc` corrupted
* Update quic-go to v0.43.1
* Fixes and improvements
#### 1.9.0-rc.16
* Mitigating TunnelVision attacks **1**
* Fixes and improvements
**1**:
See [TunnelVision](/manual/misc/tunnelvision).
#### 1.9.0-rc.15
* Fixes and improvements
#### 1.8.13
* Fix fake-ip mapping
* Fixes and improvements
#### 1.9.0-rc.14
* Fixes and improvements
#### 1.9.0-rc.13
* Update Hysteria protocol
* Update quic-go to v0.43.0
* Update gVisor to 20240422.0
* Fixes and improvements
#### 1.8.12
* Now we have official APT and DNF repositories **1**
@@ -12,6 +371,10 @@ icon: material/alert-decagram
Including stable and beta versions, see https://sing-box.sagernet.org/installation/package-manager/
#### 1.9.0-rc.11
* Fixes and improvements
#### 1.8.11
* Fixes and improvements
@@ -20,6 +383,24 @@ Including stable and beta versions, see https://sing-box.sagernet.org/installati
* Fixes and improvements
#### 1.9.0-beta.17
* Update `quic-go` to v0.42.0
* Fixes and improvements
#### 1.9.0-beta.16
* Fixes and improvements
_Our Testflight distribution has been temporarily blocked by Apple (possibly due to too many beta versions)
and you cannot join the test, install or update the sing-box beta app right now.
Please wait patiently for processing._
#### 1.9.0-beta.14
* Update gVisor to 20240212.0-65-g71212d503
* Fixes and improvements
#### 1.8.9
* Fixes and improvements
@@ -28,14 +409,125 @@ Including stable and beta versions, see https://sing-box.sagernet.org/installati
* Fixes and improvements
#### 1.9.0-beta.7
* Fixes and improvements
#### 1.9.0-beta.6
* Fix address filter DNS rule items **1**
* Fix DNS outbound responding with wrong data
* Fixes and improvements
**1**:
Fixed an issue where address filter DNS rule was incorrectly rejected under certain circumstances.
If you have enabled `store_rdrc` to save results, consider clearing the cache file.
#### 1.8.7
* Fixes and improvements
#### 1.9.0-alpha.15
* Fixes and improvements
#### 1.9.0-alpha.14
* Improve DNS truncate behavior
* Fixes and improvements
#### 1.9.0-alpha.13
* Fixes and improvements
#### 1.8.6
* Fixes and improvements
#### 1.9.0-alpha.12
* Handle Windows power events
* Always disable cache for fake-ip DNS transport if `dns.independent_cache` disabled
* Fixes and improvements
#### 1.9.0-alpha.11
* Fix missing `rule_set_ipcidr_match_source` item in DNS rules **1**
* Fixes and improvements
**1**:
See [DNS Rule](/configuration/dns/rule/).
#### 1.9.0-alpha.10
* Add `bypass_domain` and `search_domain` platform HTTP proxy options **1**
* Fixes and improvements
**1**:
See [TUN](/configuration/inbound/tun) inbound.
#### 1.9.0-alpha.8
* Add rejected DNS response cache support **1**
* Fixes and improvements
**1**:
The new feature allows you to cache the check results of
[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration.
#### 1.9.0-alpha.7
* Update gVisor to 20240206.0
* Fixes and improvements
#### 1.9.0-alpha.6
* Fixes and improvements
#### 1.9.0-alpha.3
* Update `quic-go` to v0.41.0
* Fixes and improvements
#### 1.9.0-alpha.2
* Add support for `client-subnet` DNS options **1**
* Fixes and improvements
**1**:
See [DNS](/configuration/dns), [DNS Server](/configuration/dns/server) and [DNS Rules](/configuration/dns/rule).
Since this feature makes the scenario mentioned in `alpha.1` no longer leak DNS requests,
the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) has been updated.
#### 1.9.0-alpha.1
* `domain_suffix` behavior update **1**
* `process_path` format update on Windows **2**
* Add address filter DNS rule items **3**
**1**:
See [Migration](/migration/#domain_suffix-behavior-update).
**2**:
See [Migration](/migration/#process_path-format-update-on-windows).
**3**:
The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS
if using this method.
See [Address Filter Fields](/configuration/dns/rule#address-filter-fields).
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.
#### 1.8.5
* Fixes and improvements
@@ -52,14 +544,14 @@ Including stable and beta versions, see https://sing-box.sagernet.org/installati
* Fixes and improvements
#### 1.8.0
### 1.8.0
* Fixes and improvements
Important changes since 1.7:
* Migrate cache file from Clash API to independent options **1**
* Introducing [Rule Set](/configuration/rule-set/) **2**
* Introducing [rule-set](/configuration/rule-set/) **2**
* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3**
* Allow nested logical rules **4**
* Independent `source_ip_is_private` and `ip_is_private` rules **5**
@@ -79,7 +571,7 @@ See [Cache File](/configuration/experimental/cache-file/) and
**2**:
Rule set is independent collections of rules that can be compiled into binaries to improve performance.
rule-set is independent collections of rules that can be compiled into binaries to improve performance.
Compared to legacy GeoIP and Geosite resources,
it can include more types of rules, load faster,
use less memory, and update automatically.
@@ -87,16 +579,16 @@ use less memory, and update automatically.
See [Route#rule_set](/configuration/route/#rule_set),
[Route Rule](/configuration/route/rule/),
[DNS Rule](/configuration/dns/rule/),
[Rule Set](/configuration/rule-set/),
[rule-set](/configuration/rule-set/),
[Source Format](/configuration/rule-set/source-format/) and
[Headless Rule](/configuration/rule-set/headless-rule/).
For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and
[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets).
For GEO resources migration, see [Migrate GeoIP to rule-sets](/migration/#migrate-geoip-to-rule-sets) and
[Migrate Geosite to rule-sets](/migration/#migrate-geosite-to-rule-sets).
**3**:
New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets.
New commands manage GeoIP, Geosite and rule-set resources, and help you migrate GEO resources to rule-sets.
**4**:
@@ -293,7 +785,7 @@ This change is intended to break incorrect usage and essentially requires no act
**1**:
Now the rules in the `rule_set` rule item can be logically considered to be merged into the rule using rule sets,
Now the rules in the `rule_set` rule item can be logically considered to be merged into the rule using rule-sets,
rather than completely following the AND logic.
#### 1.8.0-alpha.5
@@ -309,7 +801,7 @@ Since GeoIP was deprecated, we made this rule independent, see [Migration](/migr
#### 1.8.0-alpha.1
* Migrate cache file from Clash API to independent options **1**
* Introducing [Rule Set](/configuration/rule-set/) **2**
* Introducing [rule-set](/configuration/rule-set/) **2**
* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3**
* Allow nested logical rules **4**
@@ -320,7 +812,7 @@ See [Cache File](/configuration/experimental/cache-file/) and
**2**:
Rule set is independent collections of rules that can be compiled into binaries to improve performance.
rule-set is independent collections of rules that can be compiled into binaries to improve performance.
Compared to legacy GeoIP and Geosite resources,
it can include more types of rules, load faster,
use less memory, and update automatically.
@@ -328,22 +820,22 @@ use less memory, and update automatically.
See [Route#rule_set](/configuration/route/#rule_set),
[Route Rule](/configuration/route/rule/),
[DNS Rule](/configuration/dns/rule/),
[Rule Set](/configuration/rule-set/),
[rule-set](/configuration/rule-set/),
[Source Format](/configuration/rule-set/source-format/) and
[Headless Rule](/configuration/rule-set/headless-rule/).
For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and
[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets).
For GEO resources migration, see [Migrate GeoIP to rule-sets](/migration/#migrate-geoip-to-rule-sets) and
[Migrate Geosite to rule-sets](/migration/#migrate-geosite-to-rule-sets).
**3**:
New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets.
New commands manage GeoIP, Geosite and rule-set resources, and help you migrate GEO resources to rule-sets.
**4**:
Logical rules in route rules, DNS rules, and the new headless rule now allow nesting of logical rules.
#### 1.7.0
### 1.7.0
* Fixes and improvements
@@ -385,7 +877,7 @@ see [TCP Brutal](/configuration/shared/tcp-brutal/) for details.
**5**:
Only supported in graphical clients on Android and iOS.
Only supported in graphical clients on Android and Apple platforms.
#### 1.7.0-rc.3
@@ -422,7 +914,7 @@ Only supported in graphical clients on Android and iOS.
**1**:
Only supported in graphical clients on Android and iOS.
Only supported in graphical clients on Android and Apple platforms.
#### 1.7.0-beta.3
@@ -503,7 +995,7 @@ Introduced in V2Ray 5.10.0.
The new HTTPUpgrade transport has better performance than WebSocket and is better suited for CDN abuse.
#### 1.6.0
### 1.6.0
* Fixes and improvements
@@ -682,7 +1174,7 @@ introduce new issues.
None of the existing Golang BBR congestion control implementations have been reviewed or unit tested.
This update is intended to address the multi-send defects of the old implementation and may introduce new issues.
#### 1.5.0
### 1.5.0
* Fixes and improvements
@@ -876,7 +1368,7 @@ All inbounds and outbounds are supported, including `Naiveproxy`, `Hysteria`, `T
* Fixes and improvements
#### 1.4.0
### 1.4.0
* Fix bugs and update dependencies
@@ -1018,7 +1510,7 @@ The old testflight link and app are no longer valid.
* Fixes and improvements
#### 1.3.0
### 1.3.0
* Fix bugs and update dependencies
@@ -1210,7 +1702,7 @@ to `domain` rule.
* Flush DNS cache for macOS when tun start/close
* Fix tun's DNS hijacking compatibility with systemd-resolved
#### 1.2.0
### 1.2.0
* Fix bugs and update dependencies

View File

@@ -7,6 +7,13 @@ icon: material/apple
SFI/SFM/SFT allows users to manage and run local or remote sing-box configuration files, and provides
platform-specific function implementation, such as TUN transparent proxy implementation.
!!! failure "Unavailable"
Due to problems with our Apple developer account, sing-box apps on Apple platforms are temporarily unavailable for download or update.
If your company or organization is willing to help us return to the App Store, please [contact us](mailto:contact@sagernet.org).
## :material-graph: Requirements
* iOS 15.0+ / macOS 13.0+ / Apple tvOS 17.0+
@@ -15,7 +22,12 @@ platform-specific function implementation, such as TUN transparent proxy impleme
## :material-download: Download
* [App Store](https://apps.apple.com/us/app/sing-box/id6451272673)
* [TestFlight (Beta)](https://testflight.apple.com/join/AcqO44FH)
* ~~TestFlight (Beta)~~
TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai)
(one-time sponsorships are accepted).
Once you donate, you can get an invitation by sending us your Apple ID [via email](mailto:contact@sagernet.org),
or join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot).
## :material-file-download: Download (macOS standalone version)

View File

@@ -3,9 +3,9 @@
Maintained by Project S to provide a unified experience and platform-specific functionality.
| Platform | Client |
|---------------------------------------|------------------------------------------|
| ------------------------------------- | ---------------------------------------- |
| :material-android: Android | [sing-box for Android](./android/) |
| :material-apple: iOS/macOS/Apple tvOS | [sing-box for Apple platforms](./apple/) |
| :material-apple: iOS/macOS/Apple tvOS | :material-alert: [Unavailable](./apple/) |
| :material-laptop: Desktop | Working in progress |
Some third-party projects that claim to use sing-box or use sing-box as a selling point are not listed here. The core

View File

@@ -2,11 +2,11 @@
由 Project S 维护,提供统一的体验与平台特定的功能。
| 平台 | 客户端 |
|---------------------------------------|-----------------------------------------|
| :material-android: Android | [sing-box for Android](./android/) |
| :material-apple: iOS/macOS/Apple tvOS | [sing-box for Apple platforms](./apple/) |
| :material-laptop: Desktop | 施工中 |
| 平台 | 客户端 |
| ------------------------------------- | ----------------------------------- |
| :material-android: Android | [sing-box for Android](./android/) |
| :material-apple: iOS/macOS/Apple tvOS | :material-alert: [不可用](./apple/) |
| :material-laptop: Desktop | 施工中 |
此处没有列出一些声称使用或以 sing-box 为卖点的第三方项目。此类项目维护者的动机是获得更多用户,即使它们提供友好的商业
VPN 客户端功能, 但代码质量很差且包含广告。

View File

@@ -1,3 +1,11 @@
---
icon: material/new-box
---
!!! quote "Changes in sing-box 1.9.0"
:material-plus: [client_subnet](#client_subnet)
# DNS
### Structure
@@ -13,6 +21,7 @@
"disable_expire": false,
"independent_cache": false,
"reverse_mapping": false,
"client_subnet": "",
"fakeip": {}
}
}
@@ -21,8 +30,8 @@
### Fields
| Key | Format |
|----------|--------------------------------|
| Key | Format |
|----------|---------------------------------|
| `server` | List of [DNS Server](./server/) |
| `rules` | List of [DNS Rule](./rule/) |
| `fakeip` | [FakeIP](./fakeip/) |
@@ -60,6 +69,12 @@ Stores a reverse mapping of IP addresses after responding to a DNS query in orde
Since this process relies on the act of resolving domain names by an application before making a request, it can be
problematic in environments such as macOS, where DNS is proxied and cached by the system.
#### fakeip
#### client_subnet
[FakeIP](./fakeip/) settings.
!!! question "Since sing-box 1.9.0"
Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default.
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
Can be overrides by `servers.[].client_subnet` or `rules.[].client_subnet`.

View File

@@ -1,3 +1,11 @@
---
icon: material/new-box
---
!!! quote "sing-box 1.9.0 中的更改"
:material-plus: [client_subnet](#client_subnet)
# DNS
### 结构
@@ -13,6 +21,7 @@
"disable_expire": false,
"independent_cache": false,
"reverse_mapping": false,
"client_subnet": "",
"fakeip": {}
}
}
@@ -58,6 +67,16 @@
由于此过程依赖于应用程序在发出请求之前解析域名的行为,因此在 macOS 等 DNS 由系统代理和缓存的环境中可能会出现问题。
#### client_subnet
!!! question "自 sing-box 1.9.0 起"
默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。
如果值是 IP 地址而不是前缀,则会自动附加 `/32``/128`
可以被 `servers.[].client_subnet``rules.[].client_subnet` 覆盖。
#### fakeip
[FakeIP](./fakeip/) 设置。

View File

@@ -1,7 +1,21 @@
---
icon: material/alert-decagram
icon: material/new-box
---
!!! quote "Changes in sing-box 1.10.0"
:material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
:material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source)
:material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
!!! quote "Changes in sing-box 1.9.0"
:material-plus: [geoip](#geoip)
:material-plus: [ip_cidr](#ip_cidr)
:material-plus: [ip_is_private](#ip_is_private)
:material-plus: [client_subnet](#client_subnet)
:material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
!!! quote "Changes in sing-box 1.8.0"
:material-plus: [rule_set](#rule_set)
@@ -53,11 +67,19 @@ icon: material/alert-decagram
"source_geoip": [
"private"
],
"geoip": [
"cn"
],
"source_ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"source_ip_is_private": false,
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_is_private": false,
"source_port": [
12345
],
@@ -101,13 +123,18 @@ icon: material/alert-decagram
"geoip-cn",
"geosite-cn"
],
// deprecated
"rule_set_ipcidr_match_source": false,
"rule_set_ip_cidr_match_source": false,
"rule_set_ip_cidr_accept_empty": false,
"invert": false,
"outbound": [
"direct"
],
"server": "local",
"disable_cache": false,
"rewrite_ttl": 100
"rewrite_ttl": 100,
"client_subnet": "127.0.0.1/24"
},
{
"type": "logical",
@@ -115,7 +142,8 @@ icon: material/alert-decagram
"rules": [],
"server": "local",
"disable_cache": false,
"rewrite_ttl": 100
"rewrite_ttl": 100,
"client_subnet": "127.0.0.1/24"
}
]
}
@@ -138,7 +166,7 @@ icon: material/alert-decagram
(`source_port` || `source_port_range`) &&
`other fields`
Additionally, included rule sets can be considered merged rather than as a single rule sub-item.
Additionally, included rule-sets can be considered merged rather than as a single rule sub-item.
#### inbound
@@ -266,11 +294,9 @@ Match Clash mode.
#### wifi_ssid
<!-- md:version 1.7.0-beta.4 -->
!!! quote ""
Only supported in graphical clients on Android and iOS.
Only supported in graphical clients on Android and Apple platforms.
Match WiFi SSID.
@@ -278,7 +304,7 @@ Match WiFi SSID.
!!! quote ""
Only supported in graphical clients on Android and iOS.
Only supported in graphical clients on Android and Apple platforms.
Match WiFi BSSID.
@@ -286,7 +312,23 @@ Match WiFi BSSID.
!!! question "Since sing-box 1.8.0"
Match [Rule Set](/configuration/route/#rule_set).
Match [rule-set](/configuration/route/#rule_set).
#### rule_set_ipcidr_match_source
!!! question "Since sing-box 1.9.0"
!!! failure "Deprecated in sing-box 1.10.0"
`rule_set_ipcidr_match_source` is renamed to `rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0.
Make `ip_cidr` rule items in rule-sets match the source IP.
#### rule_set_ip_cidr_match_source
!!! question "Since sing-box 1.10.0"
Make `ip_cidr` rule items in rule-sets match the source IP.
#### invert
@@ -312,6 +354,52 @@ Disable cache and save cache in this query.
Rewrite TTL in DNS responses.
#### client_subnet
!!! question "Since sing-box 1.9.0"
Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default.
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
Will overrides `dns.client_subnet` and `servers.[].client_subnet`.
### Address Filter Fields
Only takes effect for address requests (A/AAAA/HTTPS). When the query results do not match the address filtering rule items, the current rule will be skipped.
!!! info ""
`ip_cidr` items in included rule-sets also takes effect as an address filtering field.
!!! note ""
Enable `experimental.cache_file.store_rdrc` to cache results.
#### geoip
!!! question "Since sing-box 1.9.0"
Match GeoIP with query response.
#### ip_cidr
!!! question "Since sing-box 1.9.0"
Match IP CIDR with query response.
#### ip_is_private
!!! question "Since sing-box 1.9.0"
Match private IP with query response.
#### rule_set_ip_cidr_accept_empty
!!! question "Since sing-box 1.10.0"
Make `ip_cidr` rules in rule-sets accept empty query response.
### Logical Fields
#### type

View File

@@ -1,7 +1,21 @@
---
icon: material/alert-decagram
icon: material/new-box
---
!!! quote "sing-box 1.10.0 中的更改"
:material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
:material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source)
:material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
!!! quote "sing-box 1.9.0 中的更改"
:material-plus: [geoip](#geoip)
:material-plus: [ip_cidr](#ip_cidr)
:material-plus: [ip_is_private](#ip_is_private)
:material-plus: [client_subnet](#client_subnet)
:material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
!!! quote "sing-box 1.8.0 中的更改"
:material-plus: [rule_set](#rule_set)
@@ -53,10 +67,19 @@ icon: material/alert-decagram
"source_geoip": [
"private"
],
"geoip": [
"cn"
],
"source_ip_cidr": [
"10.0.0.0/24"
"10.0.0.0/24",
"192.168.0.1"
],
"source_ip_is_private": false,
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_is_private": false,
"source_port": [
12345
],
@@ -100,19 +123,25 @@ icon: material/alert-decagram
"geoip-cn",
"geosite-cn"
],
// 已弃用
"rule_set_ipcidr_match_source": false,
"rule_set_ip_cidr_match_source": false,
"rule_set_ip_cidr_accept_empty": false,
"invert": false,
"outbound": [
"direct"
],
"server": "local",
"disable_cache": false
"disable_cache": false,
"client_subnet": "127.0.0.1/24"
},
{
"type": "logical",
"mode": "and",
"rules": [],
"server": "local",
"disable_cache": false
"disable_cache": false,
"client_subnet": "127.0.0.1/24"
}
]
}
@@ -265,7 +294,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
!!! quote ""
仅在 Android 与 iOS 的图形客户端中支持。
仅在 Android 与 Apple 平台图形客户端中支持。
匹配 WiFi SSID。
@@ -273,7 +302,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
!!! quote ""
仅在 Android 与 iOS 的图形客户端中支持。
仅在 Android 与 Apple 平台图形客户端中支持。
匹配 WiFi BSSID。
@@ -283,6 +312,22 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
匹配[规则集](/zh/configuration/route/#rule_set)。
#### rule_set_ipcidr_match_source
!!! question "自 sing-box 1.9.0 起"
!!! failure "已在 sing-box 1.10.0 废弃"
`rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 移除。
使规则集中的 `ip_cidr` 规则匹配源 IP。
#### rule_set_ip_cidr_match_source
!!! question "自 sing-box 1.10.0 起"
使规则集中的 `ip_cidr` 规则匹配源 IP。
#### invert
反选匹配结果。
@@ -307,6 +352,52 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
重写 DNS 回应中的 TTL。
#### client_subnet
!!! question "自 sing-box 1.9.0 起"
默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。
如果值是 IP 地址而不是前缀,则会自动附加 `/32``/128`
将覆盖 `dns.client_subnet``servers.[].client_subnet`
### 地址筛选字段
仅对地址请求 (A/AAAA/HTTPS) 生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。
!!! info ""
引用的规则集中的 `ip_cidr` 项也作为地址筛选字段生效。
!!! note ""
启用 `experimental.cache_file.store_rdrc` 以缓存结果。
#### geoip
!!! question "自 sing-box 1.9.0 起"
与查询响应匹配 GeoIP。
#### ip_cidr
!!! question "自 sing-box 1.9.0 起"
与查询响应匹配 IP CIDR。
#### ip_is_private
!!! question "自 sing-box 1.9.0 起"
与查询响应匹配非公开 IP。
#### rule_set_ip_cidr_accept_empty
!!! question "自 sing-box 1.10.0 起"
使规则集中的 `ip_cidr` 规则接受空查询响应。
### 逻辑字段
#### type
@@ -319,4 +410,4 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
#### rules
包括的规则。
包括的规则。

View File

@@ -1,3 +1,11 @@
---
icon: material/new-box
---
!!! quote "Changes in sing-box 1.9.0"
:material-plus: [client_subnet](#client_subnet)
### Structure
```json
@@ -5,17 +13,17 @@
"dns": {
"servers": [
{
"tag": "google",
"address": "tls://dns.google",
"address_resolver": "local",
"address_strategy": "prefer_ipv4",
"strategy": "ipv4_only",
"detour": "direct"
"tag": "",
"address": "",
"address_resolver": "",
"address_strategy": "",
"strategy": "",
"detour": "",
"client_subnet": ""
}
]
}
}
```
### Fields
@@ -80,10 +88,22 @@ Default domain strategy for resolving the domain names.
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
Take no effect if override by other settings.
Take no effect if overridden by other settings.
#### detour
Tag of an outbound for connecting to the dns server.
Default outbound will be used if empty.
#### client_subnet
!!! question "Since sing-box 1.9.0"
Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default.
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
Can be overrides by `rules.[].client_subnet`.
Will overrides `dns.client_subnet`.

View File

@@ -1,3 +1,11 @@
---
icon: material/new-box
---
!!! quote "sing-box 1.9.0 中的更改"
:material-plus: [client_subnet](#client_subnet)
### 结构
```json
@@ -5,17 +13,17 @@
"dns": {
"servers": [
{
"tag": "google",
"address": "tls://dns.google",
"address_resolver": "local",
"address_strategy": "prefer_ipv4",
"strategy": "ipv4_only",
"detour": "direct"
"tag": "",
"address": "",
"address_resolver": "",
"address_strategy": "",
"strategy": "",
"detour": "",
"client_subnet": ""
}
]
}
}
```
### 字段
@@ -87,3 +95,15 @@ DNS 服务器的地址。
用于连接到 DNS 服务器的出站的标签。
如果为空,将使用默认出站。
#### client_subnet
!!! question "自 sing-box 1.9.0 起"
默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。
如果值是 IP 地址而不是前缀,则会自动附加 `/32``/128`
可以被 `rules.[].client_subnet` 覆盖。
将覆盖 `dns.client_subnet`

View File

@@ -4,6 +4,11 @@ icon: material/new-box
!!! question "Since sing-box 1.8.0"
!!! quote "Changes in sing-box 1.9.0"
:material-plus: [store_rdrc](#store_rdrc)
:material-plus: [rdrc_timeout](#rdrc_timeout)
### Structure
```json
@@ -11,7 +16,9 @@ icon: material/new-box
"enabled": true,
"path": "",
"cache_id": "",
"store_fakeip": false
"store_fakeip": false,
"store_rdrc": false,
"rdrc_timeout": ""
}
```
@@ -29,6 +36,23 @@ Path to the cache file.
#### cache_id
Identifier in cache file.
Identifier in the cache file
If not empty, configuration specified data will use a separate store keyed by it.
#### store_fakeip
Store fakeip in the cache file
#### store_rdrc
Store rejected DNS response cache in the cache file
The check results of [Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields)
will be cached until expiration.
#### rdrc_timeout
Timeout of rejected DNS response cache.
`7d` is used by default.

View File

@@ -4,6 +4,11 @@ icon: material/new-box
!!! question "自 sing-box 1.8.0 起"
!!! quote "sing-box 1.9.0 中的更改"
:material-plus: [store_rdrc](#store_rdrc)
:material-plus: [rdrc_timeout](#rdrc_timeout)
### 结构
```json
@@ -11,7 +16,9 @@ icon: material/new-box
"enabled": true,
"path": "",
"cache_id": "",
"store_fakeip": false
"store_fakeip": false,
"store_rdrc": false,
"rdrc_timeout": ""
}
```
@@ -30,3 +37,19 @@ icon: material/new-box
缓存文件中的标识符。
如果不为空,配置特定的数据将使用由其键控的单独存储。
#### store_fakeip
将 fakeip 存储在缓存文件中。
#### store_rdrc
将拒绝的 DNS 响应缓存存储在缓存文件中。
[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#_3) 的检查结果将被缓存至过期。
#### rdrc_timeout
拒绝的 DNS 响应缓存超时。
默认使用 `7d`

View File

@@ -1,7 +1,3 @@
---
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.8.0"
:material-delete-alert: [store_mode](#store_mode)

View File

@@ -1,7 +1,3 @@
---
icon: material/alert-decagram
---
!!! quote "sing-box 1.8.0 中的更改"
:material-delete-alert: [store_mode](#store_mode)

View File

@@ -1,7 +1,3 @@
---
icon: material/alert-decagram
---
# Experimental
!!! quote "Changes in sing-box 1.8.0"

View File

@@ -1,7 +1,3 @@
---
icon: material/alert-decagram
---
# 实验性
!!! quote "sing-box 1.8.0 中的更改"

View File

@@ -42,6 +42,6 @@ No authentication required if empty.
!!! warning ""
To work on Android and iOS without privileges, use tun.platform.http_proxy instead.
To work on Android and Apple platforms without privileges, use tun.platform.http_proxy instead.
Automatically set system proxy configuration when start and clean up when stop.

View File

@@ -15,24 +15,24 @@
### Fields
| Type | Format | Injectable |
|---------------|-------------------------------|------------|
| `direct` | [Direct](./direct/) | X |
| `mixed` | [Mixed](./mixed/) | TCP |
| `socks` | [SOCKS](./socks/) | TCP |
| `http` | [HTTP](./http/) | TCP |
| `shadowsocks` | [Shadowsocks](./shadowsocks/) | TCP |
| `vmess` | [VMess](./vmess/) | TCP |
| `trojan` | [Trojan](./trojan/) | TCP |
| `naive` | [Naive](./naive/) | X |
| `hysteria` | [Hysteria](./hysteria/) | X |
| `shadowtls` | [ShadowTLS](./shadowtls/) | TCP |
| `tuic` | [TUIC](./tuic/) | X |
| `hysteria2` | [Hysteria2](./hysteria2/) | X |
| `vless` | [VLESS](./vless/) | TCP |
| `tun` | [Tun](./tun/) | X |
| `redirect` | [Redirect](./redirect/) | X |
| `tproxy` | [TProxy](./tproxy/) | X |
| Type | Format | Injectable |
|---------------|-------------------------------|------------------|
| `direct` | [Direct](./direct/) | :material-close: |
| `mixed` | [Mixed](./mixed/) | TCP |
| `socks` | [SOCKS](./socks/) | TCP |
| `http` | [HTTP](./http/) | TCP |
| `shadowsocks` | [Shadowsocks](./shadowsocks/) | TCP |
| `vmess` | [VMess](./vmess/) | TCP |
| `trojan` | [Trojan](./trojan/) | TCP |
| `naive` | [Naive](./naive/) | :material-close: |
| `hysteria` | [Hysteria](./hysteria/) | :material-close: |
| `shadowtls` | [ShadowTLS](./shadowtls/) | TCP |
| `tuic` | [TUIC](./tuic/) | :material-close: |
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
| `vless` | [VLESS](./vless/) | TCP |
| `tun` | [Tun](./tun/) | :material-close: |
| `redirect` | [Redirect](./redirect/) | :material-close: |
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
#### tag

View File

@@ -15,24 +15,24 @@
### 字段
| 类型 | 格式 | 注入支持 |
|---------------|------------------------------|------|
| `direct` | [Direct](./direct/) | X |
| `mixed` | [Mixed](./mixed/) | TCP |
| `socks` | [SOCKS](./socks/) | TCP |
| `http` | [HTTP](./http/) | TCP |
| `shadowsocks` | [Shadowsocks](./shadowsocks/) | TCP |
| `vmess` | [VMess](./vmess/) | TCP |
| `trojan` | [Trojan](./trojan/) | TCP |
| `naive` | [Naive](./naive/) | X |
| `hysteria` | [Hysteria](./hysteria/) | X |
| `shadowtls` | [ShadowTLS](./shadowtls/) | TCP |
| `tuic` | [TUIC](./tuic/) | X |
| `hysteria2` | [Hysteria2](./hysteria2/) | X |
| `vless` | [VLESS](./vless/) | TCP |
| `tun` | [Tun](./tun/) | X |
| `redirect` | [Redirect](./redirect/) | X |
| `tproxy` | [TProxy](./tproxy/) | X |
| 类型 | 格式 | 注入支持 |
|---------------|-------------------------------|------------------|
| `direct` | [Direct](./direct/) | :material-close: |
| `mixed` | [Mixed](./mixed/) | TCP |
| `socks` | [SOCKS](./socks/) | TCP |
| `http` | [HTTP](./http/) | TCP |
| `shadowsocks` | [Shadowsocks](./shadowsocks/) | TCP |
| `vmess` | [VMess](./vmess/) | TCP |
| `trojan` | [Trojan](./trojan/) | TCP |
| `naive` | [Naive](./naive/) | :material-close: |
| `hysteria` | [Hysteria](./hysteria/) | :material-close: |
| `shadowtls` | [ShadowTLS](./shadowtls/) | TCP |
| `tuic` | [TUIC](./tuic/) | :material-close: |
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
| `vless` | [VLESS](./vless/) | TCP |
| `tun` | [Tun](./tun/) | :material-close: |
| `redirect` | [Redirect](./redirect/) | :material-close: |
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
#### tag

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