Compare commits

..

101 Commits

Author SHA1 Message Date
世界
c3cc010880 documentation: Bump version 2025-06-06 23:17:46 +08:00
世界
1920c191be Fix systemd package 2025-06-06 23:17:46 +08:00
世界
e0ac459204 Fix missing home for derp service 2025-06-06 23:17:46 +08:00
Zero Clover
09fb897805 documentation: Fix services 2025-06-06 23:17:46 +08:00
世界
a1b3d891a3 Fix dns.client_subnet ignored 2025-06-06 23:17:46 +08:00
世界
d866a40469 documentation: Minor fixes 2025-06-06 23:17:46 +08:00
世界
45cd04b07e Fix tailscale forward 2025-06-06 23:17:46 +08:00
世界
2cf0528c4d Minor fixes 2025-06-06 23:17:46 +08:00
世界
905a2ded93 Add SSM API service 2025-06-06 23:17:46 +08:00
世界
cb3c0829c5 Add resolved service and DNS server 2025-06-06 23:17:45 +08:00
世界
1a8f6e053d Add DERP service 2025-06-06 23:17:27 +08:00
世界
99a09a6ce5 Add service component type 2025-06-06 23:17:27 +08:00
世界
01b4c7fcdd Fix tproxy tcp control 2025-06-06 23:17:27 +08:00
愚者
fe89f946c1 release: Fix build tags for android
Signed-off-by: 愚者 <11926619+FansChou@users.noreply.github.com>
2025-06-06 23:17:27 +08:00
世界
6c17c7a8f5 prevent creation of bind and mark controls on unsupported platforms 2025-06-06 23:17:27 +08:00
PuerNya
ea067e5478 documentation: Fix description of reject DNS action behavior 2025-06-06 23:17:27 +08:00
Restia-Ashbell
75af9a824e Fix TLS record fragment 2025-06-06 23:17:27 +08:00
世界
a5d4a42119 Add missing accept_routes option for Tailscale 2025-06-06 23:17:26 +08:00
世界
9821fbc3e3 Add TLS record fragment support 2025-06-06 23:17:26 +08:00
世界
c0408ad1de release: Update Go to 1.24.3 2025-06-06 23:17:26 +08:00
世界
6b0e861afa Fix set edns0 client subnet 2025-06-06 23:17:26 +08:00
世界
e32d686d6c Update minor dependencies 2025-06-06 23:17:26 +08:00
世界
844308e128 Update certmagic and providers 2025-06-06 23:17:26 +08:00
世界
93c14db281 Update protobuf and grpc 2025-06-06 23:17:26 +08:00
世界
b893a27dfc Add control options for listeners 2025-06-06 23:17:25 +08:00
世界
d39960fa23 Update quic-go to v0.52.0 2025-06-06 23:17:25 +08:00
世界
ba0badd4bf Update utls to v1.7.2 2025-06-06 23:17:25 +08:00
世界
cfbb5d63d5 Handle EDNS version downgrade 2025-06-06 23:16:21 +08:00
世界
8447a3edfe documentation: Fix anytls padding scheme description 2025-06-06 23:16:21 +08:00
安容
1a9747a531 Report invalid DNS address early 2025-06-06 23:16:20 +08:00
世界
583ecbea3b Fix wireguard listen_port 2025-06-06 23:16:20 +08:00
世界
bb6c8535a5 clash-api: Add more meta api 2025-06-06 23:16:19 +08:00
世界
10d90e4acc Fix DNS lookup 2025-06-06 23:16:19 +08:00
世界
e625012219 Fix fetch ECH configs 2025-06-06 23:16:19 +08:00
reletor
670863fd5b documentation: Minor fixes 2025-06-06 23:16:19 +08:00
caelansar
f7cf87142f Fix callback deletion in UDP transport 2025-06-06 23:16:18 +08:00
世界
2597a68a01 documentation: Try to make the play review happy 2025-06-06 23:16:18 +08:00
世界
7354332daa Fix missing handling of legacy domain_strategy options 2025-06-06 23:16:18 +08:00
世界
a0d382fc4e Improve local DNS server 2025-06-06 23:16:18 +08:00
anytls
a6da8b6654 Update anytls
Co-authored-by: anytls <anytls>
2025-06-06 23:16:17 +08:00
世界
7385616cca Fix DNS dialer 2025-06-06 23:16:17 +08:00
世界
4b6784b446 release: Skip override version for iOS 2025-06-06 23:16:16 +08:00
iikira
68579bb93b Fix UDP DNS server crash
Signed-off-by: iikira <i2@mail.iikira.com>
2025-06-06 23:16:16 +08:00
ReleTor
6aace7b1b7 Fix fetch ECH configs 2025-06-06 23:16:16 +08:00
世界
148234b742 Allow direct outbounds without domain_resolver 2025-06-06 23:16:16 +08:00
世界
97b7a451be Fix Tailscale dialer 2025-06-06 23:16:15 +08:00
dyhkwong
73b67e0b48 Fix DNS over QUIC stream close 2025-06-06 23:16:15 +08:00
anytls
88b4d04d59 Update anytls
Co-authored-by: anytls <anytls>
2025-06-06 23:16:15 +08:00
Rambling2076
d1ec6c6dd2 Fix missing with_tailscale in Dockerfile
Signed-off-by: Rambling2076 <Rambling2076@proton.me>
2025-06-06 23:16:14 +08:00
世界
523825336a Fail when default DNS server not found 2025-06-06 23:16:14 +08:00
世界
032565a026 Update gVisor to 20250319.0 2025-06-06 23:16:14 +08:00
世界
aeea24ae30 Explicitly reject detour to empty direct outbounds 2025-06-06 23:16:14 +08:00
世界
af22549f1a Add netns support 2025-06-06 23:16:14 +08:00
世界
57b17ceb4b Add wildcard name support for predefined records 2025-06-06 23:16:13 +08:00
世界
3dd308e7c3 Remove map usage in options 2025-06-06 23:16:13 +08:00
世界
7f75195d86 Fix unhandled DNS loop 2025-06-06 23:16:13 +08:00
世界
2fe4cad905 Add wildcard-sni support for shadow-tls inbound 2025-06-06 23:16:12 +08:00
k9982874
f55eb75a53 Add ntp protocol sniffing 2025-06-06 23:16:12 +08:00
世界
5ffb5b6ad2 option: Fix marshal legacy DNS options 2025-06-06 23:16:12 +08:00
世界
a1d5931759 Make domain_resolver optional when only one DNS server is configured 2025-06-06 23:16:12 +08:00
世界
9e68e909cb Fix DNS lookup context pollution 2025-06-06 23:16:11 +08:00
世界
117e8b76cc Fix http3 DNS server connecting to wrong address 2025-06-06 23:16:11 +08:00
Restia-Ashbell
d2f83bfd50 documentation: Fix typo 2025-06-06 23:16:11 +08:00
anytls
eaef13febe Update sing-anytls
Co-authored-by: anytls <anytls>
2025-06-06 23:16:11 +08:00
k9982874
0110c69dc9 Fix hosts DNS server 2025-06-06 23:16:10 +08:00
世界
fb2f5af1fb Fix UDP DNS server crash 2025-06-06 23:16:10 +08:00
世界
1553923118 documentation: Fix missing ip_accept_any DNS rule option 2025-06-06 23:16:10 +08:00
世界
0ada49489d Fix anytls dialer usage 2025-06-06 23:16:10 +08:00
世界
95d5ca9393 Move predefined DNS server to rule action 2025-06-06 23:16:10 +08:00
世界
6cebbb4590 Fix domain resolver on direct outbound 2025-06-06 23:16:09 +08:00
Zephyruso
0ef81bb5ef Fix missing AnyTLS display name 2025-06-06 23:16:09 +08:00
anytls
0d30a1df9d Update sing-anytls
Co-authored-by: anytls <anytls>
2025-06-06 23:16:09 +08:00
Estel
563499d2f9 documentation: Fix typo
Signed-off-by: Estel <callmebedrockdigger@gmail.com>
2025-06-06 23:16:08 +08:00
TargetLocked
f10c0c1c8d Fix parsing legacy DNS options 2025-06-06 23:16:08 +08:00
世界
428074d88b Fix DNS fallback 2025-06-06 23:16:07 +08:00
世界
fa18832ad2 documentation: Fix missing hosts DNS server 2025-06-06 23:16:07 +08:00
anytls
87bce2de29 Add MinIdleSession option to AnyTLS outbound
Co-authored-by: anytls <anytls>
2025-06-06 23:16:06 +08:00
ReleTor
f5020554e4 documentation: Minor fixes 2025-06-06 23:16:06 +08:00
libtry486
31f3623b8a documentation: Fix typo
fix typo

Signed-off-by: libtry486 <89328481+libtry486@users.noreply.github.com>
2025-06-06 23:16:05 +08:00
Alireza Ahmadi
bb42657177 Fix Outbound deadlock 2025-06-06 23:16:05 +08:00
世界
f19ff7eca7 documentation: Fix AnyTLS doc 2025-06-06 23:16:05 +08:00
anytls
8e45133f2e Add AnyTLS protocol 2025-06-06 23:16:04 +08:00
世界
63df88675f Migrate to stdlib ECH support 2025-06-06 23:16:04 +08:00
世界
0423244298 Add fallback local DNS server for iOS 2025-06-06 23:16:03 +08:00
世界
a5f1af9587 Get darwin local DNS server from libresolv 2025-06-06 23:16:03 +08:00
世界
112817c1a4 Improve resolve action 2025-06-06 23:16:02 +08:00
世界
6e91de51f1 Add back port hopping to hysteria 1 2025-06-06 23:16:02 +08:00
xchacha20-poly1305
efc5c542fb Remove single quotes of raw Moziila certs 2025-06-06 23:16:02 +08:00
世界
f1b569c7d1 Add Tailscale endpoint 2025-06-06 23:16:02 +08:00
世界
a752197d5e Build legacy binaries with latest Go 2025-06-06 23:16:01 +08:00
世界
65517d4513 documentation: Remove outdated icons 2025-06-06 23:16:01 +08:00
世界
ccf4fa4d3a documentation: Certificate store 2025-06-06 23:16:01 +08:00
世界
18dbb823a1 documentation: TLS fragment 2025-06-06 23:16:01 +08:00
世界
4ec058e91a documentation: Outbound domain resolver 2025-06-06 23:16:01 +08:00
世界
6eed06b2c2 documentation: Refactor DNS 2025-06-06 23:16:00 +08:00
世界
dd209cc9d5 Add certificate store 2025-06-06 23:16:00 +08:00
世界
b0c0a6b07d Add TLS fragment support 2025-06-06 23:15:59 +08:00
世界
951a8fabbf refactor: Outbound domain resolver 2025-06-06 23:15:59 +08:00
世界
928298b528 refactor: DNS 2025-06-06 23:15:59 +08:00
世界
5b84fa0137 Fix default network strategy 2025-06-06 14:50:38 +08:00
世界
2bb85ac8a1 Fix slowOpenConn 2025-06-06 14:39:40 +08:00
248 changed files with 1552 additions and 10754 deletions

View File

@@ -1,27 +1,25 @@
#!/usr/bin/env bash #!/usr/bin/env bash
VERSION="1.25.3" VERSION="1.23.6"
mkdir -p $HOME/go mkdir -p $HOME/go
cd $HOME/go cd $HOME/go
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz" wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
tar -xzf "go${VERSION}.linux-amd64.tar.gz" tar -xzf "go${VERSION}.linux-amd64.tar.gz"
mv go go_win7 mv go go_legacy
cd go_win7 cd go_legacy
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.25.x # this patch file only works on golang1.23.x
# that means after golang1.26 release it must be changed # that means after golang1.24 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/ # see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
# revert: # revert:
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng" # 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7" # 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround" # 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries" # a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"' curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/21290de8a4c91408de7c2b5b68757b1e90af49dd.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/8cb5472d94c34b88733a81091bd328e70ee565a4.diff | patch --verbose -p 1 curl https://github.com/MetaCubeX/go/commit/6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/6788c4c6f9fafb56729bad6b660f7ee2272d699f.diff | patch --verbose -p 1 curl https://github.com/MetaCubeX/go/commit/69e2eed6dd0f6d815ebf15797761c13f31213dd6.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/a5b2168bb836ed9d6601c626f95e56c07923f906.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/f56f1e23507e646c85243a71bde7b9629b2f970c.diff | patch --verbose -p 1

View File

@@ -40,13 +40,13 @@ jobs:
version: ${{ steps.outputs.outputs.version }} version: ${{ steps.outputs.outputs.version }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.3 go-version: ^1.24.3
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@@ -88,14 +88,13 @@ jobs:
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" } - { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
- { os: windows, arch: amd64 } - { os: windows, arch: amd64 }
- { os: windows, arch: amd64, legacy_win7: true, legacy_name: "windows-7" } - { os: windows, arch: amd64, legacy_go: true }
- { os: windows, arch: "386" } - { os: windows, arch: "386" }
- { os: windows, arch: "386", legacy_win7: true, legacy_name: "windows-7" } - { os: windows, arch: "386", legacy_go: true }
- { os: windows, arch: arm64 } - { os: windows, arch: arm64 }
- { os: darwin, arch: amd64 } - { os: darwin, arch: amd64 }
- { os: darwin, arch: arm64 } - { os: darwin, arch: arm64 }
- { os: darwin, arch: amd64, legacy_go124: true, legacy_name: "macos-11" }
- { os: android, arch: arm64, ndk: "aarch64-linux-android21" } - { os: android, arch: arm64, ndk: "aarch64-linux-android21" }
- { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" } - { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" }
@@ -103,36 +102,31 @@ jobs:
- { os: android, arch: "386", ndk: "i686-linux-android21" } - { os: android, arch: "386", ndk: "i686-linux-android21" }
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
if: ${{ ! (matrix.legacy_go123 || matrix.legacy_go124) }} if: ${{ ! matrix.legacy_go }}
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.3 go-version: ^1.24.3
- name: Setup Go 1.24 - name: Cache Legacy Go
if: matrix.legacy_go124 if: matrix.require_legacy_go
uses: actions/setup-go@v5 id: cache-legacy-go
with:
go-version: ~1.24.6
- name: Cache Go for Windows 7
if: matrix.legacy_win7
id: cache-go-for-windows7
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: | path: |
~/go/go_win7 ~/go/go_legacy
key: go_win7_1253 key: go_legacy_1236
- name: Setup Go for Windows 7 - name: Setup Legacy Go
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true' if: matrix.legacy_go && steps.cache-legacy-go.outputs.cache-hit != 'true'
run: |- run: |-
.github/setup_go_for_windows7.sh .github/setup_legacy_go.sh
- name: Setup Go for Windows 7 - name: Setup Legacy Go 2
if: matrix.legacy_win7 if: matrix.legacy_go
run: |- run: |-
echo "PATH=$HOME/go/go_win7/bin:$PATH" >> $GITHUB_ENV echo "PATH=$HOME/go/go_legacy/bin:$PATH" >> $GITHUB_ENV
echo "GOROOT=$HOME/go/go_win7" >> $GITHUB_ENV echo "GOROOT=$HOME/go/go_legacy" >> $GITHUB_ENV
- name: Setup Android NDK - name: Setup Android NDK
if: matrix.os == 'android' if: matrix.os == 'android'
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -190,8 +184,8 @@ jobs:
DIR_NAME="${DIR_NAME}-${{ matrix.go386 }}" DIR_NAME="${DIR_NAME}-${{ matrix.go386 }}"
elif [[ -n "${{ matrix.gomips }}" && "${{ matrix.gomips }}" != 'hardfloat' ]]; then elif [[ -n "${{ matrix.gomips }}" && "${{ matrix.gomips }}" != 'hardfloat' ]]; then
DIR_NAME="${DIR_NAME}-${{ matrix.gomips }}" DIR_NAME="${DIR_NAME}-${{ matrix.gomips }}"
elif [[ -n "${{ matrix.legacy_name }}" ]]; then elif [[ "${{ matrix.legacy_go }}" == 'true' ]]; then
DIR_NAME="${DIR_NAME}-legacy-${{ matrix.legacy_name }}" DIR_NAME="${DIR_NAME}-legacy"
fi fi
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}" echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
PKG_VERSION="${{ needs.calculate_version.outputs.version }}" PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
@@ -283,7 +277,7 @@ jobs:
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.go386 && format('_{0}', matrix.go386) }}${{ matrix.gomips && format('_{0}', matrix.gomips) }}${{ matrix.legacy_name && format('-legacy-{0}', matrix.legacy_name) }} name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.go386 && format('_{0}', matrix.go386) }}${{ matrix.gomips && format('_{0}', matrix.gomips) }}${{ matrix.legacy_go && '-legacy' || '' }}
path: "dist" path: "dist"
build_android: build_android:
name: Build Android name: Build Android
@@ -293,14 +287,14 @@ jobs:
- calculate_version - calculate_version
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: 'recursive' submodules: 'recursive'
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.3 go-version: ^1.24.3
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -373,14 +367,14 @@ jobs:
- calculate_version - calculate_version
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: 'recursive' submodules: 'recursive'
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.3 go-version: ^1.24.3
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -432,8 +426,7 @@ jobs:
SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }} SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }}
build_apple: build_apple:
name: Build Apple clients name: Build Apple clients
runs-on: macos-26 runs-on: macos-15
if: false
needs: needs:
- calculate_version - calculate_version
strategy: strategy:
@@ -471,7 +464,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
if: matrix.if if: matrix.if
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: 'recursive' submodules: 'recursive'
@@ -479,7 +472,15 @@ jobs:
if: matrix.if if: matrix.if
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.3 go-version: ^1.24.3
- name: Setup Xcode stable
if: matrix.if && github.ref == 'refs/heads/main-next'
run: |-
sudo xcode-select -s /Applications/Xcode_16.2.app
- name: Setup Xcode beta
if: matrix.if && github.ref == 'refs/heads/dev-next'
run: |-
sudo xcode-select -s /Applications/Xcode_16.2.app
- name: Set tag - name: Set tag
if: matrix.if if: matrix.if
run: |- run: |-
@@ -614,7 +615,7 @@ jobs:
path: 'dist' path: 'dist'
upload: upload:
name: Upload builds name: Upload builds
if: "!failure() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone')" if: always() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone')
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- calculate_version - calculate_version
@@ -623,7 +624,7 @@ jobs:
- build_apple - build_apple
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Cache ghr - name: Cache ghr
@@ -646,7 +647,7 @@ jobs:
git tag v${{ needs.calculate_version.outputs.version }} -f git tag v${{ needs.calculate_version.outputs.version }} -f
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV" echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
- name: Download builds - name: Download builds
uses: actions/download-artifact@v5 uses: actions/download-artifact@v4
with: with:
path: dist path: dist
merge-multiple: true merge-multiple: true

View File

@@ -39,7 +39,7 @@ jobs:
echo "ref=$ref" echo "ref=$ref"
echo "ref=$ref" >> $GITHUB_OUTPUT echo "ref=$ref" >> $GITHUB_OUTPUT
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
ref: ${{ steps.ref.outputs.ref }} ref: ${{ steps.ref.outputs.ref }}
fetch-depth: 0 fetch-depth: 0
@@ -107,7 +107,7 @@ jobs:
echo "latest=$latest" echo "latest=$latest"
echo "latest=$latest" >> $GITHUB_OUTPUT echo "latest=$latest" >> $GITHUB_OUTPUT
- name: Download digests - name: Download digests
uses: actions/download-artifact@v5 uses: actions/download-artifact@v4
with: with:
path: /tmp/digests path: /tmp/digests
pattern: digests-* pattern: digests-*

View File

@@ -22,17 +22,17 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.24.6 go-version: ^1.24.3
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v8 uses: golangci/golangci-lint-action@v6
with: with:
version: v2.4.0 version: latest
args: --timeout=30m args: --timeout=30m
install-mode: binary install-mode: binary
verify: false verify: false

View File

@@ -7,11 +7,6 @@ on:
description: "Version name" description: "Version name"
required: true required: true
type: string type: string
forceBeta:
description: "Force beta"
required: false
type: boolean
default: false
release: release:
types: types:
- published - published
@@ -24,13 +19,13 @@ jobs:
version: ${{ steps.outputs.outputs.version }} version: ${{ steps.outputs.outputs.version }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.3 go-version: ^1.24.3
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@@ -65,13 +60,13 @@ jobs:
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 } - { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 }
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.3 go-version: ^1.24.3
- name: Setup Android NDK - name: Setup Android NDK
if: matrix.os == 'android' if: matrix.os == 'android'
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -104,11 +99,11 @@ jobs:
run: |- run: |-
TZ=UTC touch -t '197001010000' dist/sing-box TZ=UTC touch -t '197001010000' dist/sing-box
- name: Set name - name: Set name
if: (! contains(needs.calculate_version.outputs.version, '-')) && !inputs.forceBeta if: ${{ ! contains(needs.calculate_version.outputs.version, '-') }}
run: |- run: |-
echo "NAME=sing-box" >> "$GITHUB_ENV" echo "NAME=sing-box" >> "$GITHUB_ENV"
- name: Set beta name - name: Set beta name
if: contains(needs.calculate_version.outputs.version, '-') || inputs.forceBeta if: contains(needs.calculate_version.outputs.version, '-')
run: |- run: |-
echo "NAME=sing-box-beta" >> "$GITHUB_ENV" echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
- name: Set version - name: Set version
@@ -171,7 +166,7 @@ jobs:
- build - build
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set tag - name: Set tag
@@ -180,7 +175,7 @@ jobs:
git tag v${{ needs.calculate_version.outputs.version }} -f git tag v${{ needs.calculate_version.outputs.version }} -f
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV" echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
- name: Download builds - name: Download builds
uses: actions/download-artifact@v5 uses: actions/download-artifact@v4
with: with:
path: dist path: dist
merge-multiple: true merge-multiple: true

4
.gitignore vendored
View File

@@ -15,6 +15,4 @@
.DS_Store .DS_Store
/config.d/ /config.d/
/venv/ /venv/
CLAUDE.md
AGENTS.md
/.claude/

View File

@@ -1,6 +1,27 @@
version: "2" linters:
disable-all: true
enable:
- gofumpt
- govet
- gci
- staticcheck
- paralleltest
- ineffassign
linters-settings:
gci:
custom-order: true
sections:
- standard
- prefix(github.com/sagernet/)
- default
staticcheck:
checks:
- all
- -SA1003
run: run:
go: "1.24" go: "1.23"
build-tags: build-tags:
- with_gvisor - with_gvisor
- with_quic - with_quic
@@ -9,51 +30,7 @@ run:
- with_utls - with_utls
- with_acme - with_acme
- with_clash_api - with_clash_api
linters:
default: none issues:
enable: exclude-dirs:
- govet
- ineffassign
- paralleltest
- staticcheck
settings:
staticcheck:
checks:
- all
- -S1000
- -S1008
- -S1017
- -ST1003
- -QF1001
- -QF1003
- -QF1008
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- transport/simple-obfs - transport/simple-obfs
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofumpt
settings:
gci:
sections:
- standard
- prefix(github.com/sagernet/)
- default
custom-order: true
exclusions:
generated: lax
paths:
- transport/simple-obfs
- third_party$
- builtin$
- examples$

View File

@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
LABEL maintainer="nekohasekai <contact-git@sekai.icu>" LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
COPY . /go/src/github.com/sagernet/sing-box COPY . /go/src/github.com/sagernet/sing-box
WORKDIR /go/src/github.com/sagernet/sing-box WORKDIR /go/src/github.com/sagernet/sing-box

View File

@@ -17,10 +17,6 @@ build:
export GOTOOLCHAIN=local && \ export GOTOOLCHAIN=local && \
go build $(MAIN_PARAMS) $(MAIN) go build $(MAIN_PARAMS) $(MAIN)
race:
export GOTOOLCHAIN=local && \
go build -race $(MAIN_PARAMS) $(MAIN)
ci_build: ci_build:
export GOTOOLCHAIN=local && \ export GOTOOLCHAIN=local && \
go build $(PARAMS) $(MAIN) && \ go build $(PARAMS) $(MAIN) && \
@@ -38,7 +34,7 @@ fmt:
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" . @gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" .
fmt_install: fmt_install:
go install -v mvdan.cc/gofumpt@v0.8.0 go install -v mvdan.cc/gofumpt@latest
go install -v github.com/daixiang0/gci@latest go install -v github.com/daixiang0/gci@latest
lint: lint:
@@ -49,7 +45,7 @@ lint:
GOOS=freebsd golangci-lint run ./... GOOS=freebsd golangci-lint run ./...
lint_install: lint_install:
go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0 go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@latest
proto: proto:
@go run ./cmd/internal/protogen @go run ./cmd/internal/protogen
@@ -112,16 +108,6 @@ upload_ios_app_store:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
export_ios_ipa:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Export.plist -allowProvisioningUpdates -exportPath build/SFI && \
cp build/SFI/sing-box.ipa dist/SFI.ipa
upload_ios_ipa:
cd dist && \
cp SFI.ipa "SFI-${VERSION}.ipa" && \
ghr --replace --draft --prerelease "v${VERSION}" "SFI-${VERSION}.ipa"
release_ios: build_ios upload_ios_app_store release_ios: build_ios upload_ios_app_store
build_macos: build_macos:
@@ -189,16 +175,6 @@ upload_tvos_app_store:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
export_tvos_ipa:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Export.plist -allowProvisioningUpdates -exportPath build/SFT && \
cp build/SFT/sing-box.ipa dist/SFT.ipa
upload_tvos_ipa:
cd dist && \
cp SFT.ipa "SFT-${VERSION}.ipa" && \
ghr --replace --draft --prerelease "v${VERSION}" "SFT-${VERSION}.ipa"
release_tvos: build_tvos upload_tvos_app_store release_tvos: build_tvos upload_tvos_app_store
update_apple_version: update_apple_version:
@@ -249,8 +225,8 @@ lib:
go run ./cmd/internal/build_libbox -target ios go run ./cmd/internal/build_libbox -target ios
lib_install: lib_install:
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.8 go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.6
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.8 go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.6
docs: docs:
venv/bin/mkdocs serve venv/bin/mkdocs serve

View File

@@ -1,20 +1,12 @@
# sing-box-extended # sing-box
Sing-box with extended features. The universal proxy platform.
## Features [![Packaging status](https://repology.org/badge/vertical-allrepos/sing-box.svg)](https://repology.org/project/sing-box/versions)
* Amnezia 1.5 ## Documentation
* WARP
* Tunneling
* Mieru
* XHTTP
* SDNS (DNSCrypt)
* Unified delay
## Examples https://sing-box.sagernet.org
https://github.com/shtorm-7/sing-box-extended/tree/extended/examples
## License ## License

View File

@@ -35,6 +35,7 @@ func NewManager(logger log.ContextLogger, registry adapter.EndpointRegistry) *Ma
func (m *Manager) Start(stage adapter.StartStage) error { func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock() m.access.Lock()
defer m.access.Unlock()
if m.started && m.stage >= stage { if m.started && m.stage >= stage {
panic("already started") panic("already started")
} }
@@ -42,12 +43,9 @@ func (m *Manager) Start(stage adapter.StartStage) error {
m.stage = stage m.stage = stage
if stage == adapter.StartStateStart { if stage == adapter.StartStateStart {
// started with outbound manager // started with outbound manager
m.access.Unlock()
return nil return nil
} }
endpoints := m.endpoints for _, endpoint := range m.endpoints {
m.access.Unlock()
for _, endpoint := range endpoints {
err := adapter.LegacyStart(endpoint, stage) err := adapter.LegacyStart(endpoint, stage)
if err != nil { if err != nil {
return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]") return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")

View File

@@ -44,8 +44,6 @@ type CacheFile interface {
StoreRDRC() bool StoreRDRC() bool
RDRCStore RDRCStore
StoreWARPConfig() bool
LoadMode() string LoadMode() string
StoreMode(mode string) error StoreMode(mode string) error
LoadSelected(group string) string LoadSelected(group string) string
@@ -54,8 +52,6 @@ type CacheFile interface {
StoreGroupExpand(group string, expand bool) error StoreGroupExpand(group string, expand bool) error
LoadRuleSet(tag string) *SavedBinary LoadRuleSet(tag string) *SavedBinary
SaveRuleSet(tag string, set *SavedBinary) error SaveRuleSet(tag string, set *SavedBinary) error
LoadWARPConfig(tag string) *SavedBinary
SaveWARPConfig(tag string, set *SavedBinary) error
} }
type SavedBinary struct { type SavedBinary struct {

View File

@@ -48,8 +48,6 @@ type InboundContext struct {
Network string Network string
Source M.Socksaddr Source M.Socksaddr
Destination M.Socksaddr Destination M.Socksaddr
TunnelSource string
TunnelDestination string
User string User string
Outbound string Outbound string
@@ -59,8 +57,7 @@ type InboundContext struct {
Domain string Domain string
Client string Client string
SniffContext any SniffContext any
SnifferNames []string PacketSniffError error
SniffError error
// cache // cache
@@ -138,7 +135,8 @@ func ExtendContext(ctx context.Context) (context.Context, *InboundContext) {
func OverrideContext(ctx context.Context) context.Context { func OverrideContext(ctx context.Context) context.Context {
if metadata := ContextFrom(ctx); metadata != nil { if metadata := ContextFrom(ctx); metadata != nil {
newMetadata := *metadata var newMetadata InboundContext
newMetadata = *metadata
return WithContext(ctx, &newMetadata) return WithContext(ctx, &newMetadata)
} }
return ctx return ctx

View File

@@ -20,7 +20,6 @@ type NetworkManager interface {
DefaultOptions() NetworkOptions DefaultOptions() NetworkOptions
RegisterAutoRedirectOutputMark(mark uint32) error RegisterAutoRedirectOutputMark(mark uint32) error
AutoRedirectOutputMark() uint32 AutoRedirectOutputMark() uint32
AutoRedirectOutputMarkFunc() control.Func
NetworkMonitor() tun.NetworkUpdateMonitor NetworkMonitor() tun.NetworkUpdateMonitor
InterfaceMonitor() tun.DefaultInterfaceMonitor InterfaceMonitor() tun.DefaultInterfaceMonitor
PackageManager() tun.PackageManager PackageManager() tun.PackageManager

View File

@@ -30,7 +30,7 @@ type Manager struct {
outboundByTag map[string]adapter.Outbound outboundByTag map[string]adapter.Outbound
dependByTag map[string][]string dependByTag map[string][]string
defaultOutbound adapter.Outbound defaultOutbound adapter.Outbound
defaultOutboundFallback func() (adapter.Outbound, error) defaultOutboundFallback adapter.Outbound
} }
func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager { func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
@@ -44,7 +44,7 @@ func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry,
} }
} }
func (m *Manager) Initialize(defaultOutboundFallback func() (adapter.Outbound, error)) { func (m *Manager) Initialize(defaultOutboundFallback adapter.Outbound) {
m.defaultOutboundFallback = defaultOutboundFallback m.defaultOutboundFallback = defaultOutboundFallback
} }
@@ -55,31 +55,18 @@ func (m *Manager) Start(stage adapter.StartStage) error {
} }
m.started = true m.started = true
m.stage = stage m.stage = stage
outbounds := m.outbounds
m.access.Unlock()
if stage == adapter.StartStateStart { if stage == adapter.StartStateStart {
if m.defaultTag != "" && m.defaultOutbound == nil { if m.defaultTag != "" && m.defaultOutbound == nil {
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag) defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
if !loaded { if !loaded {
m.access.Unlock()
return E.New("default outbound not found: ", m.defaultTag) return E.New("default outbound not found: ", m.defaultTag)
} }
m.defaultOutbound = defaultEndpoint m.defaultOutbound = defaultEndpoint
} }
if m.defaultOutbound == nil {
directOutbound, err := m.defaultOutboundFallback()
if err != nil {
m.access.Unlock()
return E.Cause(err, "create direct outbound for fallback")
}
m.outbounds = append(m.outbounds, directOutbound)
m.outboundByTag[directOutbound.Tag()] = directOutbound
m.defaultOutbound = directOutbound
}
outbounds := m.outbounds
m.access.Unlock()
return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...)) return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
} else { } else {
outbounds := m.outbounds
m.access.Unlock()
for _, outbound := range outbounds { for _, outbound := range outbounds {
err := adapter.LegacyStart(outbound, stage) err := adapter.LegacyStart(outbound, stage)
if err != nil { if err != nil {
@@ -200,7 +187,11 @@ func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
func (m *Manager) Default() adapter.Outbound { func (m *Manager) Default() adapter.Outbound {
m.access.RLock() m.access.RLock()
defer m.access.RUnlock() defer m.access.RUnlock()
if m.defaultOutbound != nil {
return m.defaultOutbound return m.defaultOutbound
} else {
return m.defaultOutboundFallback
}
} }
func (m *Manager) Remove(tag string) error { func (m *Manager) Remove(tag string) error {

View File

@@ -78,8 +78,8 @@ func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
// Deprecated: removed // Deprecated: removed
func UpstreamMetadata(metadata InboundContext) M.Metadata { func UpstreamMetadata(metadata InboundContext) M.Metadata {
return M.Metadata{ return M.Metadata{
Source: metadata.Source.Unwrap(), Source: metadata.Source,
Destination: metadata.Destination.Unwrap(), Destination: metadata.Destination,
} }
} }

14
box.go
View File

@@ -17,7 +17,6 @@ import (
"github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/taskmonitor" "github.com/sagernet/sing-box/common/taskmonitor"
"github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/common/urltest"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport/local" "github.com/sagernet/sing-box/dns/transport/local"
@@ -140,9 +139,6 @@ func New(options Options) (*Box, error) {
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" { if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
needV2RayAPI = true needV2RayAPI = true
} }
if experimentalOptions.UnifiedDelay != nil && experimentalOptions.UnifiedDelay.Enabled {
ctx = urltest.ContextWithIsUnifiedDelay(ctx)
}
platformInterface := service.FromContext[platform.Interface](ctx) platformInterface := service.FromContext[platform.Interface](ctx)
var defaultLogWriter io.Writer var defaultLogWriter io.Writer
if platformInterface != nil { if platformInterface != nil {
@@ -318,15 +314,15 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize service[", i, "]") return nil, E.Cause(err, "initialize service[", i, "]")
} }
} }
outboundManager.Initialize(func() (adapter.Outbound, error) { outboundManager.Initialize(common.Must1(
return direct.NewOutbound( direct.NewOutbound(
ctx, ctx,
router, router,
logFactory.NewLogger("outbound/direct"), logFactory.NewLogger("outbound/direct"),
"direct", "direct",
option.DirectOutboundOptions{}, option.DirectOutboundOptions{},
) ),
}) ))
dnsTransportManager.Initialize(common.Must1( dnsTransportManager.Initialize(common.Must1(
local.NewTransport( local.NewTransport(
ctx, ctx,
@@ -502,7 +498,7 @@ func (s *Box) Close() error {
close(s.done) close(s.done)
} }
err := common.Close( err := common.Close(
s.service, s.endpoint, s.inbound, s.outbound, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network, s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
) )
for _, lifecycleService := range s.internalService { for _, lifecycleService := range s.internalService {
err = E.Append(err, lifecycleService.Close(), func(err error) error { err = E.Append(err, lifecycleService.Close(), func(err error) error {

View File

@@ -134,7 +134,6 @@ func publishTestflight(ctx context.Context) error {
asc.PlatformTVOS, asc.PlatformTVOS,
} }
} }
waitingForProcess := false
for _, platform := range platforms { for _, platform := range platforms {
log.Info(string(platform), " list builds") log.Info(string(platform), " list builds")
for { for {
@@ -146,13 +145,12 @@ func publishTestflight(ctx context.Context) error {
return err return err
} }
build := builds.Data[0] build := builds.Data[0]
if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) { if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute {
log.Info(string(platform), " ", tag, " waiting for process") log.Info(string(platform), " ", tag, " waiting for process")
time.Sleep(15 * time.Second) time.Sleep(15 * time.Second)
continue continue
} }
if *build.Attributes.ProcessingState != "VALID" { if *build.Attributes.ProcessingState != "VALID" {
waitingForProcess = true
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState) log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
time.Sleep(15 * time.Second) time.Sleep(15 * time.Second)
continue continue
@@ -179,7 +177,7 @@ func publishTestflight(ctx context.Context) error {
} }
log.Info(string(platform), " ", tag, " publish") log.Info(string(platform), " ", tag, " publish")
response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID}) response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
if response != nil && (response.StatusCode == http.StatusUnprocessableEntity || response.StatusCode == http.StatusNotFound) { if response != nil && response.StatusCode == http.StatusUnprocessableEntity {
log.Info("waiting for process") log.Info("waiting for process")
time.Sleep(15 * time.Second) time.Sleep(15 * time.Second)
continue continue

View File

@@ -19,14 +19,12 @@ var (
debugEnabled bool debugEnabled bool
target string target string
platform string platform string
withTailscale bool
) )
func init() { func init() {
flag.BoolVar(&debugEnabled, "debug", false, "enable debug") flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
flag.StringVar(&target, "target", "android", "target platform") flag.StringVar(&target, "target", "android", "target platform")
flag.StringVar(&platform, "platform", "", "specify platform") flag.StringVar(&platform, "platform", "", "specify platform")
flag.BoolVar(&withTailscale, "with-tailscale", false, "build tailscale for iOS and tvOS")
} }
func main() { func main() {
@@ -46,9 +44,8 @@ var (
sharedFlags []string sharedFlags []string
debugFlags []string debugFlags []string
sharedTags []string sharedTags []string
darwinTags []string iosTags []string
memcTags []string memcTags []string
notMemcTags []string
debugTags []string debugTags []string
) )
@@ -63,9 +60,8 @@ func init() {
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag) debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack") sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack")
darwinTags = append(darwinTags, "with_dhcp") iosTags = append(iosTags, "with_dhcp", "with_low_memory")
memcTags = append(memcTags, "with_tailscale") memcTags = append(memcTags, "with_tailscale")
notMemcTags = append(notMemcTags, "with_low_memory")
debugTags = append(debugTags, "debug") debugTags = append(debugTags, "debug")
} }
@@ -107,10 +103,8 @@ func buildAndroid() {
} }
if !debugEnabled { if !debugEnabled {
sharedFlags[3] = sharedFlags[3] + " -checklinkname=0"
args = append(args, sharedFlags...) args = append(args, sharedFlags...)
} else { } else {
debugFlags[1] = debugFlags[1] + " -checklinkname=0"
args = append(args, debugFlags...) args = append(args, debugFlags...)
} }
@@ -157,10 +151,7 @@ func buildApple() {
"-v", "-v",
"-target", bindTarget, "-target", bindTarget,
"-libname=box", "-libname=box",
"-tags-not-macos=with_low_memory", "-tags-macos=" + strings.Join(memcTags, ","),
}
if !withTailscale {
args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
} }
if !debugEnabled { if !debugEnabled {
@@ -169,10 +160,7 @@ func buildApple() {
args = append(args, debugFlags...) args = append(args, debugFlags...)
} }
tags := append(sharedTags, darwinTags...) tags := append(sharedTags, iosTags...)
if withTailscale {
tags = append(tags, memcTags...)
}
if debugEnabled { if debugEnabled {
tags = append(tags, debugTags...) tags = append(tags, debugTags...)
} }

View File

@@ -1,284 +0,0 @@
package main
import (
"context"
"fmt"
"io"
"net/netip"
"os"
"os/exec"
"strings"
"syscall"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/include"
"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/json"
"github.com/sagernet/sing/common/shell"
)
var iperf3Path string
func main() {
err := main0()
if err != nil {
log.Fatal(err)
}
}
func main0() error {
err := shell.Exec("sudo", "ls").Run()
if err != nil {
return err
}
results, err := runTests()
if err != nil {
return err
}
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
return encoder.Encode(results)
}
func runTests() ([]TestResult, error) {
boxPaths := []string{
os.ExpandEnv("$HOME/Downloads/sing-box-1.11.15-darwin-arm64/sing-box"),
//"/Users/sekai/Downloads/sing-box-1.11.15-linux-arm64/sing-box",
"./sing-box",
}
stacks := []string{
"gvisor",
"system",
}
mtus := []int{
1500,
4064,
// 16384,
// 32768,
// 49152,
65535,
}
flagList := [][]string{
{},
}
var results []TestResult
for _, boxPath := range boxPaths {
for _, stack := range stacks {
for _, mtu := range mtus {
if strings.HasPrefix(boxPath, ".") {
for _, flags := range flagList {
result, err := testOnce(boxPath, stack, mtu, false, flags)
if err != nil {
return nil, err
}
results = append(results, *result)
}
} else {
result, err := testOnce(boxPath, stack, mtu, false, nil)
if err != nil {
return nil, err
}
results = append(results, *result)
}
}
}
}
return results, nil
}
type TestResult struct {
BoxPath string `json:"box_path"`
Stack string `json:"stack"`
MTU int `json:"mtu"`
Flags []string `json:"flags"`
MultiThread bool `json:"multi_thread"`
UploadSpeed string `json:"upload_speed"`
DownloadSpeed string `json:"download_speed"`
}
func testOnce(boxPath string, stackName string, mtu int, multiThread bool, flags []string) (result *TestResult, err error) {
testAddress := netip.MustParseAddr("1.1.1.1")
testConfig := option.Options{
Inbounds: []option.Inbound{
{
Type: C.TypeTun,
Options: &option.TunInboundOptions{
Address: []netip.Prefix{netip.MustParsePrefix("172.18.0.1/30")},
AutoRoute: true,
MTU: uint32(mtu),
Stack: stackName,
RouteAddress: []netip.Prefix{netip.PrefixFrom(testAddress, testAddress.BitLen())},
},
},
},
Route: &option.RouteOptions{
Rules: []option.Rule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultRule{
RawDefaultRule: option.RawDefaultRule{
IPCIDR: []string{testAddress.String()},
},
RuleAction: option.RuleAction{
Action: C.RuleActionTypeRouteOptions,
RouteOptionsOptions: option.RouteOptionsActionOptions{
OverrideAddress: "127.0.0.1",
},
},
},
},
},
AutoDetectInterface: true,
},
}
ctx := include.Context(context.Background())
tempConfig, err := os.CreateTemp("", "tun-bench-*.json")
if err != nil {
return
}
defer os.Remove(tempConfig.Name())
encoder := json.NewEncoderContext(ctx, tempConfig)
encoder.SetIndent("", " ")
err = encoder.Encode(testConfig)
if err != nil {
return nil, E.Cause(err, "encode test config")
}
tempConfig.Close()
var sudoArgs []string
if len(flags) > 0 {
sudoArgs = append(sudoArgs, "env")
sudoArgs = append(sudoArgs, flags...)
}
sudoArgs = append(sudoArgs, boxPath, "run", "-c", tempConfig.Name())
boxProcess := shell.Exec("sudo", sudoArgs...)
boxProcess.Stdout = &stderrWriter{}
boxProcess.Stderr = io.Discard
err = boxProcess.Start()
if err != nil {
return
}
if C.IsDarwin {
iperf3Path, err = exec.LookPath("iperf3-darwin")
} else {
iperf3Path, err = exec.LookPath("iperf3")
}
if err != nil {
return
}
serverProcess := shell.Exec(iperf3Path, "-s")
serverProcess.Stdout = io.Discard
serverProcess.Stderr = io.Discard
err = serverProcess.Start()
if err != nil {
return nil, E.Cause(err, "start iperf3 server")
}
time.Sleep(time.Second)
args := []string{"-c", testAddress.String()}
if multiThread {
args = append(args, "-P", "10")
}
uploadProcess := shell.Exec(iperf3Path, args...)
output, err := uploadProcess.Read()
if err != nil {
boxProcess.Process.Signal(syscall.SIGKILL)
serverProcess.Process.Signal(syscall.SIGKILL)
println(output)
return
}
uploadResult := common.SubstringBeforeLast(output, "iperf Done.")
uploadResult = common.SubstringBeforeLast(uploadResult, "sender")
uploadResult = common.SubstringBeforeLast(uploadResult, "bits/sec")
uploadResult = common.SubstringAfterLast(uploadResult, "Bytes")
uploadResult = strings.ReplaceAll(uploadResult, " ", "")
result = &TestResult{
BoxPath: boxPath,
Stack: stackName,
MTU: mtu,
Flags: flags,
MultiThread: multiThread,
UploadSpeed: uploadResult,
}
downloadProcess := shell.Exec(iperf3Path, append(args, "-R")...)
output, err = downloadProcess.Read()
if err != nil {
boxProcess.Process.Signal(syscall.SIGKILL)
serverProcess.Process.Signal(syscall.SIGKILL)
println(output)
return
}
downloadResult := common.SubstringBeforeLast(output, "iperf Done.")
downloadResult = common.SubstringBeforeLast(downloadResult, "receiver")
downloadResult = common.SubstringBeforeLast(downloadResult, "bits/sec")
downloadResult = common.SubstringAfterLast(downloadResult, "Bytes")
downloadResult = strings.ReplaceAll(downloadResult, " ", "")
result.DownloadSpeed = downloadResult
printArgs := []any{boxPath, stackName, mtu, "upload", uploadResult, "download", downloadResult}
if len(flags) > 0 {
printArgs = append(printArgs, "flags", strings.Join(flags, " "))
}
if multiThread {
printArgs = append(printArgs, "(-P 10)")
}
fmt.Println(printArgs...)
err = boxProcess.Process.Signal(syscall.SIGTERM)
if err != nil {
return
}
err = serverProcess.Process.Signal(syscall.SIGTERM)
if err != nil {
return
}
boxDone := make(chan struct{})
go func() {
boxProcess.Cmd.Wait()
close(boxDone)
}()
serverDone := make(chan struct{})
go func() {
serverProcess.Process.Wait()
close(serverDone)
}()
select {
case <-boxDone:
case <-time.After(2 * time.Second):
boxProcess.Process.Kill()
case <-time.After(4 * time.Second):
println("box process did not close!")
os.Exit(1)
}
select {
case <-serverDone:
case <-time.After(2 * time.Second):
serverProcess.Process.Kill()
case <-time.After(4 * time.Second):
println("server process did not close!")
os.Exit(1)
}
return
}
type stderrWriter struct{}
func (w *stderrWriter) Write(p []byte) (n int, err error) {
return os.Stderr.Write(p)
}

View File

@@ -5,7 +5,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/sagernet/sing-box/common/convertor/adguard" "github.com/sagernet/sing-box/cmd/sing-box/internal/convertor/adguard"
"github.com/sagernet/sing-box/common/srs" "github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
@@ -54,7 +54,7 @@ func convertRuleSet(sourcePath string) error {
var rules []option.HeadlessRule var rules []option.HeadlessRule
switch flagRuleSetConvertType { switch flagRuleSetConvertType {
case "adguard": case "adguard":
rules, err = adguard.ToOptions(reader, log.StdLogger()) rules, err = adguard.Convert(reader)
case "": case "":
return E.New("source type is required") return E.New("source type is required")
default: default:

View File

@@ -6,10 +6,7 @@ import (
"strings" "strings"
"github.com/sagernet/sing-box/common/srs" "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/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -53,11 +50,6 @@ func decompileRuleSet(sourcePath string) error {
if err != nil { if err != nil {
return err return err
} }
if hasRule(ruleSet.Options.Rules, func(rule option.DefaultHeadlessRule) bool {
return len(rule.AdGuardDomain) > 0
}) {
return E.New("unable to decompile binary AdGuard rules to rule-set.")
}
var outputPath string var outputPath string
if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput { if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".srs") { if strings.HasSuffix(sourcePath, ".srs") {
@@ -83,19 +75,3 @@ func decompileRuleSet(sourcePath string) error {
outputFile.Close() outputFile.Close()
return nil return nil
} }
func hasRule(rules []option.HeadlessRule, cond func(rule option.DefaultHeadlessRule) bool) bool {
for _, rule := range rules {
switch rule.Type {
case C.RuleTypeDefault:
if cond(rule.DefaultOptions) {
return true
}
case C.RuleTypeLogical:
if hasRule(rule.LogicalOptions.Rules, cond) {
return true
}
}
}
return false
}

View File

@@ -2,7 +2,6 @@ package adguard
import ( import (
"bufio" "bufio"
"bytes"
"io" "io"
"net/netip" "net/netip"
"os" "os"
@@ -10,10 +9,10 @@ import (
"strings" "strings"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
) )
@@ -28,7 +27,7 @@ type agdguardRuleLine struct {
isImportant bool isImportant bool
} }
func ToOptions(reader io.Reader, logger logger.Logger) ([]option.HeadlessRule, error) { func Convert(reader io.Reader) ([]option.HeadlessRule, error) {
scanner := bufio.NewScanner(reader) scanner := bufio.NewScanner(reader)
var ( var (
ruleLines []agdguardRuleLine ruleLines []agdguardRuleLine
@@ -37,10 +36,7 @@ func ToOptions(reader io.Reader, logger logger.Logger) ([]option.HeadlessRule, e
parseLine: parseLine:
for scanner.Scan() { for scanner.Scan() {
ruleLine := scanner.Text() ruleLine := scanner.Text()
if ruleLine == "" { if ruleLine == "" || ruleLine[0] == '!' || ruleLine[0] == '#' {
continue
}
if strings.HasPrefix(ruleLine, "!") || strings.HasPrefix(ruleLine, "#") {
continue continue
} }
originRuleLine := ruleLine originRuleLine := ruleLine
@@ -96,7 +92,7 @@ parseLine:
} }
if !ignored { if !ignored {
ignoredLines++ ignoredLines++
logger.Debug("ignored unsupported rule with modifier: ", paramParts[0], ": ", originRuleLine) log.Debug("ignored unsupported rule with modifier: ", paramParts[0], ": ", ruleLine)
continue parseLine continue parseLine
} }
} }
@@ -124,35 +120,27 @@ parseLine:
ruleLine = ruleLine[1 : len(ruleLine)-1] ruleLine = ruleLine[1 : len(ruleLine)-1]
if ignoreIPCIDRRegexp(ruleLine) { if ignoreIPCIDRRegexp(ruleLine) {
ignoredLines++ ignoredLines++
logger.Debug("ignored unsupported rule with IPCIDR regexp: ", originRuleLine) log.Debug("ignored unsupported rule with IPCIDR regexp: ", ruleLine)
continue continue
} }
isRegexp = true isRegexp = true
} else { } else {
if strings.Contains(ruleLine, "://") { if strings.Contains(ruleLine, "://") {
ruleLine = common.SubstringAfter(ruleLine, "://") ruleLine = common.SubstringAfter(ruleLine, "://")
isSuffix = true
} }
if strings.Contains(ruleLine, "/") { if strings.Contains(ruleLine, "/") {
ignoredLines++ ignoredLines++
logger.Debug("ignored unsupported rule with path: ", originRuleLine) log.Debug("ignored unsupported rule with path: ", ruleLine)
continue continue
} }
if strings.Contains(ruleLine, "?") || strings.Contains(ruleLine, "&") { if strings.Contains(ruleLine, "##") {
ignoredLines++ ignoredLines++
logger.Debug("ignored unsupported rule with query: ", originRuleLine) log.Debug("ignored unsupported rule with element hiding: ", ruleLine)
continue continue
} }
if strings.Contains(ruleLine, "[") || strings.Contains(ruleLine, "]") || if strings.Contains(ruleLine, "#$#") {
strings.Contains(ruleLine, "(") || strings.Contains(ruleLine, ")") ||
strings.Contains(ruleLine, "!") || strings.Contains(ruleLine, "#") {
ignoredLines++ ignoredLines++
logger.Debug("ignored unsupported cosmetic filter: ", originRuleLine) log.Debug("ignored unsupported rule with element hiding: ", ruleLine)
continue
}
if strings.Contains(ruleLine, "~") {
ignoredLines++
logger.Debug("ignored unsupported rule modifier: ", originRuleLine)
continue continue
} }
var domainCheck string var domainCheck string
@@ -163,7 +151,7 @@ parseLine:
} }
if ruleLine == "" { if ruleLine == "" {
ignoredLines++ ignoredLines++
logger.Debug("ignored unsupported rule with empty domain", originRuleLine) log.Debug("ignored unsupported rule with empty domain", originRuleLine)
continue continue
} else { } else {
domainCheck = strings.ReplaceAll(domainCheck, "*", "x") domainCheck = strings.ReplaceAll(domainCheck, "*", "x")
@@ -171,13 +159,13 @@ parseLine:
_, ipErr := parseADGuardIPCIDRLine(ruleLine) _, ipErr := parseADGuardIPCIDRLine(ruleLine)
if ipErr == nil { if ipErr == nil {
ignoredLines++ ignoredLines++
logger.Debug("ignored unsupported rule with IPCIDR: ", originRuleLine) log.Debug("ignored unsupported rule with IPCIDR: ", ruleLine)
continue continue
} }
if M.ParseSocksaddr(domainCheck).Port != 0 { if M.ParseSocksaddr(domainCheck).Port != 0 {
logger.Debug("ignored unsupported rule with port: ", originRuleLine) log.Debug("ignored unsupported rule with port: ", ruleLine)
} else { } else {
logger.Debug("ignored unsupported rule with invalid domain: ", originRuleLine) log.Debug("ignored unsupported rule with invalid domain: ", ruleLine)
} }
ignoredLines++ ignoredLines++
continue continue
@@ -295,112 +283,10 @@ parseLine:
}, },
} }
} }
if ignoredLines > 0 { log.Info("parsed rules: ", len(ruleLines), "/", len(ruleLines)+ignoredLines)
logger.Info("parsed rules: ", len(ruleLines), "/", len(ruleLines)+ignoredLines)
}
return []option.HeadlessRule{currentRule}, nil return []option.HeadlessRule{currentRule}, nil
} }
var ErrInvalid = E.New("invalid binary AdGuard rule-set")
func FromOptions(rules []option.HeadlessRule) ([]byte, error) {
if len(rules) != 1 {
return nil, ErrInvalid
}
rule := rules[0]
var (
importantDomain []string
importantDomainRegex []string
importantExcludeDomain []string
importantExcludeDomainRegex []string
domain []string
domainRegex []string
excludeDomain []string
excludeDomainRegex []string
)
parse:
for {
switch rule.Type {
case C.RuleTypeLogical:
if !(len(rule.LogicalOptions.Rules) == 2 && rule.LogicalOptions.Rules[0].Type == C.RuleTypeDefault) {
return nil, ErrInvalid
}
if rule.LogicalOptions.Mode == C.LogicalTypeAnd && rule.LogicalOptions.Rules[0].DefaultOptions.Invert {
if len(importantExcludeDomain) == 0 && len(importantExcludeDomainRegex) == 0 {
importantExcludeDomain = rule.LogicalOptions.Rules[0].DefaultOptions.AdGuardDomain
importantExcludeDomainRegex = rule.LogicalOptions.Rules[0].DefaultOptions.DomainRegex
if len(importantExcludeDomain)+len(importantExcludeDomainRegex) == 0 {
return nil, ErrInvalid
}
} else {
excludeDomain = rule.LogicalOptions.Rules[0].DefaultOptions.AdGuardDomain
excludeDomainRegex = rule.LogicalOptions.Rules[0].DefaultOptions.DomainRegex
if len(excludeDomain)+len(excludeDomainRegex) == 0 {
return nil, ErrInvalid
}
}
} else if rule.LogicalOptions.Mode == C.LogicalTypeOr && !rule.LogicalOptions.Rules[0].DefaultOptions.Invert {
importantDomain = rule.LogicalOptions.Rules[0].DefaultOptions.AdGuardDomain
importantDomainRegex = rule.LogicalOptions.Rules[0].DefaultOptions.DomainRegex
if len(importantDomain)+len(importantDomainRegex) == 0 {
return nil, ErrInvalid
}
} else {
return nil, ErrInvalid
}
rule = rule.LogicalOptions.Rules[1]
case C.RuleTypeDefault:
domain = rule.DefaultOptions.AdGuardDomain
domainRegex = rule.DefaultOptions.DomainRegex
if len(domain)+len(domainRegex) == 0 {
return nil, ErrInvalid
}
break parse
}
}
var output bytes.Buffer
for _, ruleLine := range importantDomain {
output.WriteString(ruleLine)
output.WriteString("$important\n")
}
for _, ruleLine := range importantDomainRegex {
output.WriteString("/")
output.WriteString(ruleLine)
output.WriteString("/$important\n")
}
for _, ruleLine := range importantExcludeDomain {
output.WriteString("@@")
output.WriteString(ruleLine)
output.WriteString("$important\n")
}
for _, ruleLine := range importantExcludeDomainRegex {
output.WriteString("@@/")
output.WriteString(ruleLine)
output.WriteString("/$important\n")
}
for _, ruleLine := range domain {
output.WriteString(ruleLine)
output.WriteString("\n")
}
for _, ruleLine := range domainRegex {
output.WriteString("/")
output.WriteString(ruleLine)
output.WriteString("/\n")
}
for _, ruleLine := range excludeDomain {
output.WriteString("@@")
output.WriteString(ruleLine)
output.WriteString("\n")
}
for _, ruleLine := range excludeDomainRegex {
output.WriteString("@@/")
output.WriteString(ruleLine)
output.WriteString("/\n")
}
return output.Bytes(), nil
}
func ignoreIPCIDRRegexp(ruleLine string) bool { func ignoreIPCIDRRegexp(ruleLine string) bool {
if strings.HasPrefix(ruleLine, "(http?:\\/\\/)") { if strings.HasPrefix(ruleLine, "(http?:\\/\\/)") {
ruleLine = ruleLine[12:] ruleLine = ruleLine[12:]
@@ -408,9 +294,11 @@ func ignoreIPCIDRRegexp(ruleLine string) bool {
ruleLine = ruleLine[13:] ruleLine = ruleLine[13:]
} else if strings.HasPrefix(ruleLine, "^") { } else if strings.HasPrefix(ruleLine, "^") {
ruleLine = ruleLine[1:] ruleLine = ruleLine[1:]
} else {
return false
} }
return common.Error(strconv.ParseUint(common.SubstringBefore(ruleLine, "\\."), 10, 8)) == nil || _, parseErr := strconv.ParseUint(common.SubstringBefore(ruleLine, "\\."), 10, 8)
common.Error(strconv.ParseUint(common.SubstringBefore(ruleLine, "."), 10, 8)) == nil return parseErr == nil
} }
func parseAdGuardHostLine(ruleLine string) (string, error) { func parseAdGuardHostLine(ruleLine string) (string, error) {
@@ -454,5 +342,5 @@ func parseADGuardIPCIDRLine(ruleLine string) (netip.Prefix, error) {
for len(ruleParts) < 4 { for len(ruleParts) < 4 {
ruleParts = append(ruleParts, 0) ruleParts = append(ruleParts, 0)
} }
return netip.PrefixFrom(netip.AddrFrom4([4]byte(ruleParts)), bitLen), nil return netip.PrefixFrom(netip.AddrFrom4(*(*[4]byte)(ruleParts)), bitLen), nil
} }

View File

@@ -7,15 +7,13 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing/common/logger"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestConverter(t *testing.T) { func TestConverter(t *testing.T) {
t.Parallel() t.Parallel()
ruleString := `||sagernet.org^$important rules, err := Convert(strings.NewReader(`
@@|sing-box.sagernet.org^$important
||example.org^ ||example.org^
|example.com^ |example.com^
example.net^ example.net^
@@ -23,9 +21,10 @@ example.net^
||example.edu.tw^ ||example.edu.tw^
|example.gov |example.gov
example.arpa example.arpa
@@|sagernet.example.org^ @@|sagernet.example.org|
` ||sagernet.org^$important
rules, err := ToOptions(strings.NewReader(ruleString), logger.NOP()) @@|sing-box.sagernet.org^$important
`))
require.NoError(t, err) require.NoError(t, err)
require.Len(t, rules, 1) require.Len(t, rules, 1)
rule, err := rule.NewHeadlessRule(context.Background(), rules[0]) rule, err := rule.NewHeadlessRule(context.Background(), rules[0])
@@ -76,18 +75,15 @@ example.arpa
Domain: domain, Domain: domain,
}), domain) }), domain)
} }
ruleFromOptions, err := FromOptions(rules)
require.NoError(t, err)
require.Equal(t, ruleString, string(ruleFromOptions))
} }
func TestHosts(t *testing.T) { func TestHosts(t *testing.T) {
t.Parallel() t.Parallel()
rules, err := ToOptions(strings.NewReader(` rules, err := Convert(strings.NewReader(`
127.0.0.1 localhost 127.0.0.1 localhost
::1 localhost #[IPv6] ::1 localhost #[IPv6]
0.0.0.0 google.com 0.0.0.0 google.com
`), logger.NOP()) `))
require.NoError(t, err) require.NoError(t, err)
require.Len(t, rules, 1) require.Len(t, rules, 1)
rule, err := rule.NewHeadlessRule(context.Background(), rules[0]) rule, err := rule.NewHeadlessRule(context.Background(), rules[0])
@@ -114,10 +110,10 @@ func TestHosts(t *testing.T) {
func TestSimpleHosts(t *testing.T) { func TestSimpleHosts(t *testing.T) {
t.Parallel() t.Parallel()
rules, err := ToOptions(strings.NewReader(` rules, err := Convert(strings.NewReader(`
example.com example.com
www.example.org www.example.org
`), logger.NOP()) `))
require.NoError(t, err) require.NoError(t, err)
require.Len(t, rules, 1) require.Len(t, rules, 1)
rule, err := rule.NewHeadlessRule(context.Background(), rules[0]) rule, err := rule.NewHeadlessRule(context.Background(), rules[0])

View File

@@ -128,10 +128,6 @@ func (c *ReadWaitConn) Upstream() any {
return c.Conn return c.Conn
} }
func (c *ReadWaitConn) ReaderReplaceable() bool {
return true
}
var tlsRegistry []func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) var tlsRegistry []func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error)
func init() { func init() {

View File

@@ -6,26 +6,22 @@ import (
"net" "net"
_ "unsafe" _ "unsafe"
"github.com/sagernet/sing/common"
"github.com/metacubex/utls" "github.com/metacubex/utls"
) )
func init() { func init() {
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) { tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
switch tlsConn := conn.(type) { tlsConn, loaded := common.Cast[*tls.UConn](conn)
case *tls.UConn: if !loaded {
return
}
return true, func() error { return true, func() error {
return utlsReadRecord(tlsConn.Conn) return utlsReadRecord(tlsConn.Conn)
}, func() error { }, func() error {
return utlsHandlePostHandshakeMessage(tlsConn.Conn) return utlsHandlePostHandshakeMessage(tlsConn.Conn)
} }
case *tls.Conn:
return true, func() error {
return utlsReadRecord(tlsConn)
}, func() error {
return utlsHandlePostHandshakeMessage(tlsConn)
}
}
return
}) })
} }

View File

@@ -7,7 +7,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"github.com/sagernet/fswatch" "github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -22,7 +21,6 @@ import (
var _ adapter.CertificateStore = (*Store)(nil) var _ adapter.CertificateStore = (*Store)(nil)
type Store struct { type Store struct {
access sync.RWMutex
systemPool *x509.CertPool systemPool *x509.CertPool
currentPool *x509.CertPool currentPool *x509.CertPool
certificate string certificate string
@@ -117,14 +115,10 @@ func (s *Store) Close() error {
} }
func (s *Store) Pool() *x509.CertPool { func (s *Store) Pool() *x509.CertPool {
s.access.RLock()
defer s.access.RUnlock()
return s.currentPool return s.currentPool
} }
func (s *Store) update() error { func (s *Store) update() error {
s.access.Lock()
defer s.access.Unlock()
var currentPool *x509.CertPool var currentPool *x509.CertPool
if s.systemPool == nil { if s.systemPool == nil {
currentPool = x509.NewCertPool() currentPool = x509.NewCertPool()

View File

@@ -1,74 +0,0 @@
package cloudflare
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/tidwall/gjson"
)
type CloudflareApi struct {
client http.Client
}
func NewCloudflareApi(opts ...CloudflareApiOption) *CloudflareApi {
api := &CloudflareApi{http.Client{Timeout: 30 * time.Second}}
for _, opt := range opts {
opt(api)
}
return api
}
func (api *CloudflareApi) CreateProfile(ctx context.Context, publicKey string) (*CloudflareProfile, error) {
request, err := http.NewRequest("POST", "https://api.cloudflareclient.com/v0i1909051800/reg", strings.NewReader(
fmt.Sprintf(
"{\"install_id\":\"\",\"tos\":\"%s\",\"key\":\"%s\",\"fcm_token\":\"\",\"type\":\"ios\",\"locale\":\"en_US\"}",
time.Now().Format("2006-01-02T15:04:05.000Z"),
publicKey,
),
))
if err != nil {
return nil, err
}
response, err := api.client.Do(request.WithContext(ctx))
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != 200 {
return nil, fmt.Errorf("status code is not 200")
}
content, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
profile := new(CloudflareProfile)
return profile, json.NewDecoder(strings.NewReader(gjson.Get(string(content), "result").Raw)).Decode(profile)
}
func (api *CloudflareApi) GetProfile(ctx context.Context, authToken string, id string) (*CloudflareProfile, error) {
request, err := http.NewRequest("GET", "https://api.cloudflareclient.com/v0i1909051800/reg/"+id, nil)
if err != nil {
return nil, err
}
request.Header.Set("Authorization", "Bearer "+authToken)
response, err := api.client.Do(request.WithContext(ctx))
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != 200 {
return nil, fmt.Errorf("status code is not 200")
}
content, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
profile := new(CloudflareProfile)
return profile, json.NewDecoder(strings.NewReader(gjson.Get(string(content), "result").Raw)).Decode(profile)
}

View File

@@ -1,17 +0,0 @@
package cloudflare
import (
"context"
"net"
"net/http"
)
type CloudflareApiOption func(api *CloudflareApi)
func WithDialContext(dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) CloudflareApiOption {
return func(api *CloudflareApi) {
api.client.Transport = &http.Transport{
DialContext: dialContext,
}
}
}

View File

@@ -1,64 +0,0 @@
package cloudflare
import "time"
type CloudflareProfile struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Key string `json:"key"`
Account struct {
ID string `json:"id"`
AccountType string `json:"account_type"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
PremiumData int `json:"premium_data"`
Quota int `json:"quota"`
Usage int `json:"usage"`
WARPPlus bool `json:"warp_plus"`
ReferralCount int `json:"referral_count"`
ReferralRenewalCountdown int `json:"referral_renewal_countdown"`
Role string `json:"role"`
License string `json:"license"`
TTL time.Time `json:"ttl"`
} `json:"account"`
Config struct {
ClientID string `json:"client_id"`
Interface struct {
Addresses struct {
V4 string `json:"v4"`
V6 string `json:"v6"`
} `json:"addresses"`
} `json:"interface"`
Peers []struct {
PublicKey string `json:"public_key"`
Endpoint struct {
V4 string `json:"v4"`
V6 string `json:"v6"`
Host string `json:"host"`
Ports []int `json:"ports"`
} `json:"endpoint"`
} `json:"peers"`
Services struct {
HTTPProxy string `json:"http_proxy"`
} `json:"services"`
Metrics struct {
Ping int `json:"ping"`
Report int `json:"report"`
} `json:"metrics"`
} `json:"config"`
Token string `json:"token"`
WARPEnabled bool `json:"warp_enabled"`
WaitlistEnabled bool `json:"waitlist_enabled"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Tos time.Time `json:"tos"`
Place int `json:"place"`
Locale string `json:"locale"`
Enabled bool `json:"enabled"`
InstallID string `json:"install_id"`
FcmToken string `json:"fcm_token"`
Policy struct {
TunnelProtocol string `json:"tunnel_protocol"`
} `json:"policy"`
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
@@ -42,7 +43,7 @@ type DefaultDialer struct {
networkType []C.InterfaceType networkType []C.InterfaceType
fallbackNetworkType []C.InterfaceType fallbackNetworkType []C.InterfaceType
networkFallbackDelay time.Duration networkFallbackDelay time.Duration
networkLastFallback common.TypedValue[time.Time] networkLastFallback atomic.TypedValue[time.Time]
} }
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) { func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
@@ -88,11 +89,12 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
if networkManager != nil { if networkManager != nil {
defaultOptions := networkManager.DefaultOptions() defaultOptions := networkManager.DefaultOptions()
if !disableDefaultBind {
if defaultOptions.BindInterface != "" { if defaultOptions.BindInterface != "" {
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1) bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
dialer.Control = control.Append(dialer.Control, bindFunc) dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)
} else if networkManager.AutoDetectInterface() && !disableDefaultBind { } else if networkManager.AutoDetectInterface() {
if platformInterface != nil { if platformInterface != nil {
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy) networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
networkType = common.Map(options.NetworkType, option.InterfaceType.Build) networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
@@ -119,16 +121,12 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)
} }
} }
}
if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 { if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true)) dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true)) listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
} }
} }
if networkManager != nil {
markFunc := networkManager.AutoRedirectOutputMarkFunc()
dialer.Control = control.Append(dialer.Control, markFunc)
listener.Control = control.Append(listener.Control, markFunc)
}
if options.ReuseAddr { if options.ReuseAddr {
listener.Control = control.Append(listener.Control, control.ReuseAddr()) listener.Control = control.Append(listener.Control, control.ReuseAddr())
} }
@@ -273,7 +271,7 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
} else { } else {
dialer = d.udpDialer4 dialer = d.udpDialer4
} }
fastFallback := time.Since(d.networkLastFallback.Load()) < C.TCPTimeout fastFallback := time.Now().Sub(d.networkLastFallback.Load()) < C.TCPTimeout
var ( var (
conn net.Conn conn net.Conn
isPrimary bool isPrimary bool

View File

@@ -52,6 +52,14 @@ func (d *DetourDialer) init() {
d.initErr = E.New("outbound detour not found: ", d.detour) d.initErr = E.New("outbound detour not found: ", d.detour)
return return
} }
if !d.legacyDNSDialer {
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
if directDialer.IsEmpty() {
d.initErr = E.New("detour to an empty direct outbound makes no sense")
return
}
}
}
d.dialer = dialer d.dialer = dialer
} }

View File

@@ -111,7 +111,7 @@ func NewWithOptions(options Options) (N.Dialer, error) {
dnsQueryOptions.Transport = dnsTransport.Default() dnsQueryOptions.Transport = dnsTransport.Default()
} else if options.NewDialer { } else if options.NewDialer {
return nil, E.New("missing domain resolver for domain server address") return nil, E.New("missing domain resolver for domain server address")
} else { } else if !options.DirectOutbound {
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver) deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
} }
} }

View File

@@ -8,7 +8,6 @@ import (
"net" "net"
"os" "os"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
@@ -24,11 +23,9 @@ type slowOpenConn struct {
ctx context.Context ctx context.Context
network string network string
destination M.Socksaddr destination M.Socksaddr
conn atomic.Pointer[net.TCPConn] conn net.Conn
create chan struct{} create chan struct{}
done chan struct{}
access sync.Mutex access sync.Mutex
closeOnce sync.Once
err error err error
} }
@@ -47,30 +44,26 @@ func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, des
network: network, network: network,
destination: destination, destination: destination,
create: make(chan struct{}), create: make(chan struct{}),
done: make(chan struct{}),
}, nil }, nil
} }
func (c *slowOpenConn) Read(b []byte) (n int, err error) { func (c *slowOpenConn) Read(b []byte) (n int, err error) {
conn := c.conn.Load() if c.conn == nil {
if conn != nil {
return conn.Read(b)
}
select { select {
case <-c.create: case <-c.create:
if c.err != nil { if c.err != nil {
return 0, c.err return 0, c.err
} }
return c.conn.Load().Read(b) case <-c.ctx.Done():
case <-c.done: return 0, c.ctx.Err()
return 0, os.ErrClosed
} }
} }
return c.conn.Read(b)
}
func (c *slowOpenConn) Write(b []byte) (n int, err error) { func (c *slowOpenConn) Write(b []byte) (n int, err error) {
tcpConn := c.conn.Load() if c.conn != nil {
if tcpConn != nil { return c.conn.Write(b)
return tcpConn.Write(b)
} }
c.access.Lock() c.access.Lock()
defer c.access.Unlock() defer c.access.Unlock()
@@ -79,16 +72,14 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
if c.err != nil { if c.err != nil {
return 0, c.err return 0, c.err
} }
return c.conn.Load().Write(b) return c.conn.Write(b)
case <-c.done:
return 0, os.ErrClosed
default: default:
} }
conn, err := c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b) conn, err := c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
if err != nil { if err != nil {
c.err = err c.err = err
} else { } else {
c.conn.Store(conn.(*net.TCPConn)) c.conn = conn
} }
n = len(b) n = len(b)
close(c.create) close(c.create)
@@ -96,87 +87,74 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
} }
func (c *slowOpenConn) Close() error { func (c *slowOpenConn) Close() error {
c.closeOnce.Do(func() { return common.Close(c.conn)
close(c.done)
conn := c.conn.Load()
if conn != nil {
conn.Close()
}
})
return nil
} }
func (c *slowOpenConn) LocalAddr() net.Addr { func (c *slowOpenConn) LocalAddr() net.Addr {
conn := c.conn.Load() if c.conn == nil {
if conn == nil {
return M.Socksaddr{} return M.Socksaddr{}
} }
return conn.LocalAddr() return c.conn.LocalAddr()
} }
func (c *slowOpenConn) RemoteAddr() net.Addr { func (c *slowOpenConn) RemoteAddr() net.Addr {
conn := c.conn.Load() if c.conn == nil {
if conn == nil {
return M.Socksaddr{} return M.Socksaddr{}
} }
return conn.RemoteAddr() return c.conn.RemoteAddr()
} }
func (c *slowOpenConn) SetDeadline(t time.Time) error { func (c *slowOpenConn) SetDeadline(t time.Time) error {
conn := c.conn.Load() if c.conn == nil {
if conn == nil {
return os.ErrInvalid return os.ErrInvalid
} }
return conn.SetDeadline(t) return c.conn.SetDeadline(t)
} }
func (c *slowOpenConn) SetReadDeadline(t time.Time) error { func (c *slowOpenConn) SetReadDeadline(t time.Time) error {
conn := c.conn.Load() if c.conn == nil {
if conn == nil {
return os.ErrInvalid return os.ErrInvalid
} }
return conn.SetReadDeadline(t) return c.conn.SetReadDeadline(t)
} }
func (c *slowOpenConn) SetWriteDeadline(t time.Time) error { func (c *slowOpenConn) SetWriteDeadline(t time.Time) error {
conn := c.conn.Load() if c.conn == nil {
if conn == nil {
return os.ErrInvalid return os.ErrInvalid
} }
return conn.SetWriteDeadline(t) return c.conn.SetWriteDeadline(t)
} }
func (c *slowOpenConn) Upstream() any { func (c *slowOpenConn) Upstream() any {
return common.PtrOrNil(c.conn.Load()) return c.conn
} }
func (c *slowOpenConn) ReaderReplaceable() bool { func (c *slowOpenConn) ReaderReplaceable() bool {
return c.conn.Load() != nil return c.conn != nil
} }
func (c *slowOpenConn) WriterReplaceable() bool { func (c *slowOpenConn) WriterReplaceable() bool {
return c.conn.Load() != nil return c.conn != nil
} }
func (c *slowOpenConn) LazyHeadroom() bool { func (c *slowOpenConn) LazyHeadroom() bool {
return c.conn.Load() == nil return c.conn == nil
} }
func (c *slowOpenConn) NeedHandshake() bool { func (c *slowOpenConn) NeedHandshake() bool {
return c.conn.Load() == nil return c.conn == nil
} }
func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) { func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) {
conn := c.conn.Load() if c.conn == nil {
if conn == nil {
select { select {
case <-c.create: case <-c.create:
if c.err != nil { if c.err != nil {
return 0, c.err return 0, c.err
} }
case <-c.done: case <-c.ctx.Done():
return 0, c.err return 0, c.ctx.Err()
} }
} }
return bufio.Copy(w, c.conn.Load()) return bufio.Copy(w, c.conn)
} }

View File

@@ -3,7 +3,6 @@ package interrupt
import ( import (
"net" "net"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/common/x/list"
) )
@@ -74,32 +73,3 @@ func (c *PacketConn) WriterReplaceable() bool {
func (c *PacketConn) Upstream() any { func (c *PacketConn) Upstream() any {
return c.PacketConn return c.PacketConn
} }
type SingPacketConn struct {
N.PacketConn
group *Group
element *list.Element[*groupConnItem]
}
/*func (c *SingPacketConn) MarkAsInternal() {
c.element.Value.internal = true
}*/
func (c *SingPacketConn) Close() error {
c.group.access.Lock()
defer c.group.access.Unlock()
c.group.connections.Remove(c.element)
return c.PacketConn.Close()
}
func (c *SingPacketConn) ReaderReplaceable() bool {
return true
}
func (c *SingPacketConn) WriterReplaceable() bool {
return true
}
func (c *SingPacketConn) Upstream() any {
return c.PacketConn
}

View File

@@ -5,7 +5,6 @@ import (
"net" "net"
"sync" "sync"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/common/x/list"
) )
@@ -37,13 +36,6 @@ func (g *Group) NewPacketConn(conn net.PacketConn, isExternal bool) net.PacketCo
return &PacketConn{PacketConn: conn, group: g, element: item} return &PacketConn{PacketConn: conn, group: g, element: item}
} }
func (g *Group) NewSingPacketConn(conn N.PacketConn, isExternal bool) N.PacketConn {
g.access.Lock()
defer g.access.Unlock()
item := g.connections.PushBack(&groupConnItem{conn, isExternal})
return &SingPacketConn{PacketConn: conn, group: g, element: item}
}
func (g *Group) Interrupt(interruptExternalConnections bool) { func (g *Group) Interrupt(interruptExternalConnections bool) {
g.access.Lock() g.access.Lock()
defer g.access.Unlock() defer g.access.Unlock()

View File

@@ -3,7 +3,6 @@ package listener
import ( import (
"net" "net"
"net/netip" "net/netip"
"strings"
"syscall" "syscall"
"time" "time"
@@ -57,7 +56,7 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
if l.tproxy { if l.tproxy {
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error { listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
return control.Raw(conn, func(fd uintptr) error { return control.Raw(conn, func(fd uintptr) error {
return redir.TProxy(fd, !strings.HasSuffix(network, "4"), false) return redir.TProxy(fd, M.ParseSocksaddr(address).IsIPv6(), false)
}) })
}) })
} }

View File

@@ -5,7 +5,6 @@ import (
"net" "net"
"net/netip" "net/netip"
"os" "os"
"strings"
"syscall" "syscall"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -42,7 +41,7 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
if l.tproxy { if l.tproxy {
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error { listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
return control.Raw(conn, func(fd uintptr) error { return control.Raw(conn, func(fd uintptr) error {
return redir.TProxy(fd, !strings.HasSuffix(network, "4"), true) return redir.TProxy(fd, M.ParseSocksaddr(address).IsIPv6(), true)
}) })
}) })
} }
@@ -165,8 +164,9 @@ func (l *Listener) loopUDPOut() {
if l.shutdown.Load() && E.IsClosed(err) { if l.shutdown.Load() && E.IsClosed(err) {
return return
} }
l.udpConn.Close()
l.logger.Error("udp listener write back: ", destination, ": ", err) l.logger.Error("udp listener write back: ", destination, ": ", err)
continue return
} }
continue continue
case <-l.packetOutboundClosed: case <-l.packetOutboundClosed:

View File

@@ -76,8 +76,6 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
// rup8(sizeof(xtcpcb_n)) // rup8(sizeof(xtcpcb_n))
itemSize += 208 itemSize += 208
} }
var fallbackUDPProcess string
// skip the first xinpgen(24 bytes) block // skip the first xinpgen(24 bytes) block
for i := 24; i+itemSize <= len(buf); i += itemSize { for i := 24; i+itemSize <= len(buf); i += itemSize {
// offset of xinpcb_n and xsocket_n // offset of xinpcb_n and xsocket_n
@@ -92,36 +90,26 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
flag := buf[inp+44] flag := buf[inp+44]
var srcIP netip.Addr var srcIP netip.Addr
srcIsIPv4 := false
switch { switch {
case flag&0x1 > 0 && isIPv4: case flag&0x1 > 0 && isIPv4:
// ipv4 // ipv4
srcIP = netip.AddrFrom4([4]byte(buf[inp+76 : inp+80])) srcIP = netip.AddrFrom4(*(*[4]byte)(buf[inp+76 : inp+80]))
srcIsIPv4 = true
case flag&0x2 > 0 && !isIPv4: case flag&0x2 > 0 && !isIPv4:
// ipv6 // ipv6
srcIP = netip.AddrFrom16([16]byte(buf[inp+64 : inp+80])) srcIP = netip.AddrFrom16(*(*[16]byte)(buf[inp+64 : inp+80]))
default: default:
continue continue
} }
if ip == srcIP { if ip != srcIP {
continue
}
// xsocket_n.so_last_pid // xsocket_n.so_last_pid
pid := readNativeUint32(buf[so+68 : so+72]) pid := readNativeUint32(buf[so+68 : so+72])
return getExecPathFromPID(pid) return getExecPathFromPID(pid)
} }
// udp packet connection may be not equal with srcIP
if network == N.NetworkUDP && srcIP.IsUnspecified() && isIPv4 == srcIsIPv4 {
pid := readNativeUint32(buf[so+68 : so+72])
fallbackUDPProcess, _ = getExecPathFromPID(pid)
}
}
if network == N.NetworkUDP && len(fallbackUDPProcess) > 0 {
return fallbackUDPProcess, nil
}
return "", ErrNotFound return "", ErrNotFound
} }

View File

@@ -17,5 +17,8 @@ var uQUICChrome115 = &ja3.ClientHello{
} }
func maybeUQUIC(fingerprint *ja3.ClientHello) bool { func maybeUQUIC(fingerprint *ja3.ClientHello) bool {
return !uQUICChrome115.Equals(fingerprint, true) if uQUICChrome115.Equals(fingerprint, true) {
return true
}
return false
} }

View File

@@ -56,7 +56,7 @@ func TestSniffUQUICChrome115(t *testing.T) {
err = sniff.QUICClientHello(context.Background(), &metadata, pkt) err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC) require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientChromium) require.Equal(t, metadata.Client, C.ClientQUICGo)
require.Equal(t, metadata.Domain, "www.google.com") require.Equal(t, metadata.Domain, "www.google.com")
} }

View File

@@ -215,15 +215,16 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
case ruleItemWIFIBSSID: case ruleItemWIFIBSSID:
rule.WIFIBSSID, err = readRuleItemString(reader) rule.WIFIBSSID, err = readRuleItemString(reader)
case ruleItemAdGuardDomain: case ruleItemAdGuardDomain:
if recover {
err = E.New("unable to decompile binary AdGuard rules to rule-set")
return
}
var matcher *domain.AdGuardMatcher var matcher *domain.AdGuardMatcher
matcher, err = domain.ReadAdGuardMatcher(reader) matcher, err = domain.ReadAdGuardMatcher(reader)
if err != nil { if err != nil {
return return
} }
rule.AdGuardDomainMatcher = matcher rule.AdGuardDomainMatcher = matcher
if recover {
rule.AdGuardDomain = matcher.Dump()
}
case ruleItemNetworkType: case ruleItemNetworkType:
rule.NetworkType, err = readRuleItemUint8[option.InterfaceType](reader) rule.NetworkType, err = readRuleItemUint8[option.InterfaceType](reader)
case ruleItemNetworkIsExpensive: case ruleItemNetworkIsExpensive:

View File

@@ -5,13 +5,13 @@ package tls
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"os"
"strings" "strings"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/libdns/alidns" "github.com/libdns/alidns"
@@ -37,38 +37,7 @@ func (w *acmeWrapper) Close() error {
return nil return nil
} }
type acmeLogWriter struct { func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
logger logger.Logger
}
func (w *acmeLogWriter) Write(p []byte) (n int, err error) {
logLine := strings.ReplaceAll(string(p), " ", ": ")
switch {
case strings.HasPrefix(logLine, "error: "):
w.logger.Error(logLine[7:])
case strings.HasPrefix(logLine, "warn: "):
w.logger.Warn(logLine[6:])
case strings.HasPrefix(logLine, "info: "):
w.logger.Info(logLine[6:])
case strings.HasPrefix(logLine, "debug: "):
w.logger.Debug(logLine[7:])
default:
w.logger.Debug(logLine)
}
return len(p), nil
}
func (w *acmeLogWriter) Sync() error {
return nil
}
func encoderConfig() zapcore.EncoderConfig {
config := zap.NewProductionEncoderConfig()
config.TimeKey = zapcore.OmitKey
return config
}
func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
var acmeServer string var acmeServer string
switch options.Provider { switch options.Provider {
case "", "letsencrypt": case "", "letsencrypt":
@@ -89,15 +58,14 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
} else { } else {
storage = certmagic.Default.Storage storage = certmagic.Default.Storage
} }
zapLogger := zap.New(zapcore.NewCore(
zapcore.NewConsoleEncoder(encoderConfig()),
&acmeLogWriter{logger: logger},
zap.DebugLevel,
))
config := &certmagic.Config{ config := &certmagic.Config{
DefaultServerName: options.DefaultServerName, DefaultServerName: options.DefaultServerName,
Storage: storage, Storage: storage,
Logger: zapLogger, Logger: zap.New(zapcore.NewCore(
zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()),
os.Stderr,
zap.InfoLevel,
)),
} }
acmeConfig := certmagic.ACMEIssuer{ acmeConfig := certmagic.ACMEIssuer{
CA: acmeServer, CA: acmeServer,
@@ -107,7 +75,7 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
DisableTLSALPNChallenge: options.DisableTLSALPNChallenge, DisableTLSALPNChallenge: options.DisableTLSALPNChallenge,
AltHTTPPort: int(options.AlternativeHTTPPort), AltHTTPPort: int(options.AlternativeHTTPPort),
AltTLSALPNPort: int(options.AlternativeTLSPort), AltTLSALPNPort: int(options.AlternativeTLSPort),
Logger: zapLogger, Logger: config.Logger,
} }
if dnsOptions := options.DNS01Challenge; dnsOptions != nil && dnsOptions.Provider != "" { if dnsOptions := options.DNS01Challenge; dnsOptions != nil && dnsOptions.Provider != "" {
var solver certmagic.DNS01Solver var solver certmagic.DNS01Solver
@@ -135,7 +103,6 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) { GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) {
return config, nil return config, nil
}, },
Logger: zapLogger,
}) })
config = certmagic.New(cache, *config) config = certmagic.New(cache, *config)
var tlsConfig *tls.Config var tlsConfig *tls.Config

View File

@@ -9,9 +9,8 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
) )
func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) { func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
return nil, nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`) return nil, nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`)
} }

View File

@@ -53,48 +53,26 @@ func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, e
return tlsConn, nil return tlsConn, nil
} }
type Dialer interface { type Dialer struct {
N.Dialer
DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error)
}
type defaultDialer struct {
dialer N.Dialer dialer N.Dialer
config Config config Config
} }
func NewDialer(dialer N.Dialer, config Config) Dialer { func NewDialer(dialer N.Dialer, config Config) N.Dialer {
return &defaultDialer{dialer, config} return &Dialer{dialer, config}
} }
func (d *defaultDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { func (d *Dialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if N.NetworkName(network) != N.NetworkTCP { if network != N.NetworkTCP {
return nil, os.ErrInvalid return nil, os.ErrInvalid
} }
return d.DialTLSContext(ctx, destination) conn, err := d.dialer.DialContext(ctx, network, destination)
}
func (d *defaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return nil, os.ErrInvalid
}
func (d *defaultDialer) DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error) {
return d.dialContext(ctx, destination)
}
func (d *defaultDialer) dialContext(ctx context.Context, destination M.Socksaddr) (Conn, error) {
conn, err := d.dialer.DialContext(ctx, N.NetworkTCP, destination)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tlsConn, err := aTLS.ClientHandshake(ctx, conn, d.config) return ClientHandshake(ctx, conn, d.config)
if err != nil {
conn.Close()
return nil, err
}
return tlsConn, nil
} }
func (d *defaultDialer) Upstream() any { func (d *Dialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return d.dialer return nil, os.ErrInvalid
} }

View File

@@ -25,7 +25,7 @@ import (
"golang.org/x/crypto/cryptobyte" "golang.org/x/crypto/cryptobyte"
) )
func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, options option.OutboundTLSOptions) (Config, error) { func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions, tlsConfig *tls.Config) (Config, error) {
var echConfig []byte var echConfig []byte
if len(options.ECH.Config) > 0 { if len(options.ECH.Config) > 0 {
echConfig = []byte(strings.Join(options.ECH.Config, "\n")) echConfig = []byte(strings.Join(options.ECH.Config, "\n"))
@@ -45,11 +45,11 @@ func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, op
if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 { if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 {
return nil, E.New("invalid ECH configs pem") return nil, E.New("invalid ECH configs pem")
} }
clientConfig.SetECHConfigList(block.Bytes) tlsConfig.EncryptedClientHelloConfigList = block.Bytes
return clientConfig, nil return &STDClientConfig{tlsConfig}, nil
} else { } else {
return &ECHClientConfig{ return &STDECHClientConfig{
ECHCapableConfig: clientConfig, STDClientConfig: STDClientConfig{tlsConfig},
dnsRouter: service.FromContext[adapter.DNSRouter](ctx), dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
}, nil }, nil
} }
@@ -69,7 +69,11 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
} else { } else {
return E.New("missing ECH keys") return E.New("missing ECH keys")
} }
echKeys, err := parseECHKeys(echKey) block, rest := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return E.New("invalid ECH keys pem")
}
echKeys, err := UnmarshalECHKeys(block.Bytes)
if err != nil { if err != nil {
return E.Cause(err, "parse ECH keys") return E.Cause(err, "parse ECH keys")
} }
@@ -81,40 +85,32 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
return nil return nil
} }
func (c *STDServerConfig) setECHServerConfig(echKey []byte) error { func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
echKeys, err := parseECHKeys(echKey) echKey, err := os.ReadFile(echKeyPath)
if err != nil { if err != nil {
return err return E.Cause(err, "reload ECH keys from ", echKeyPath)
} }
c.access.Lock()
config := c.config.Clone()
config.EncryptedClientHelloKeys = echKeys
c.config = config
c.access.Unlock()
return nil
}
func parseECHKeys(echKey []byte) ([]tls.EncryptedClientHelloKey, error) {
block, _ := pem.Decode(echKey) block, _ := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" { if block == nil || block.Type != "ECH KEYS" {
return nil, E.New("invalid ECH keys pem") return E.New("invalid ECH keys pem")
} }
echKeys, err := UnmarshalECHKeys(block.Bytes) echKeys, err := UnmarshalECHKeys(block.Bytes)
if err != nil { if err != nil {
return nil, E.Cause(err, "parse ECH keys") return E.Cause(err, "parse ECH keys")
} }
return echKeys, nil tlsConfig.EncryptedClientHelloKeys = echKeys
return nil
} }
type ECHClientConfig struct { type STDECHClientConfig struct {
ECHCapableConfig STDClientConfig
access sync.Mutex access sync.Mutex
dnsRouter adapter.DNSRouter dnsRouter adapter.DNSRouter
lastTTL time.Duration lastTTL time.Duration
lastUpdate time.Time lastUpdate time.Time
} }
func (s *ECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
tlsConn, err := s.fetchAndHandshake(ctx, conn) tlsConn, err := s.fetchAndHandshake(ctx, conn)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -126,17 +122,17 @@ func (s *ECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (a
return tlsConn, nil return tlsConn, nil
} }
func (s *ECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { func (s *STDECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
s.access.Lock() s.access.Lock()
defer s.access.Unlock() defer s.access.Unlock()
if len(s.ECHConfigList()) == 0 || s.lastTTL == 0 || time.Since(s.lastUpdate) > s.lastTTL { if len(s.config.EncryptedClientHelloConfigList) == 0 || s.lastTTL == 0 || time.Now().Sub(s.lastUpdate) > s.lastTTL {
message := &mDNS.Msg{ message := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{ MsgHdr: mDNS.MsgHdr{
RecursionDesired: true, RecursionDesired: true,
}, },
Question: []mDNS.Question{ Question: []mDNS.Question{
{ {
Name: mDNS.Fqdn(s.ServerName()), Name: mDNS.Fqdn(s.config.ServerName),
Qtype: mDNS.TypeHTTPS, Qtype: mDNS.TypeHTTPS,
Qclass: mDNS.ClassINET, Qclass: mDNS.ClassINET,
}, },
@@ -161,21 +157,21 @@ func (s *ECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn)
} }
s.lastTTL = time.Duration(rr.Header().Ttl) * time.Second s.lastTTL = time.Duration(rr.Header().Ttl) * time.Second
s.lastUpdate = time.Now() s.lastUpdate = time.Now()
s.SetECHConfigList(echConfigList) s.config.EncryptedClientHelloConfigList = echConfigList
break match break match
} }
} }
} }
} }
if len(s.ECHConfigList()) == 0 { if len(s.config.EncryptedClientHelloConfigList) == 0 {
return nil, E.New("no ECH config found in DNS records") return nil, E.New("no ECH config found in DNS records")
} }
} }
return s.Client(conn) return s.Client(conn)
} }
func (s *ECHClientConfig) Clone() Config { func (s *STDECHClientConfig) Clone() Config {
return &ECHClientConfig{ECHCapableConfig: s.ECHCapableConfig.Clone().(ECHCapableConfig), dnsRouter: s.dnsRouter, lastUpdate: s.lastUpdate} return &STDECHClientConfig{STDClientConfig: STDClientConfig{s.config.Clone()}, dnsRouter: s.dnsRouter, lastUpdate: s.lastUpdate}
} }
func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) { func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) {

155
common/tls/ech_keygen.go Normal file
View File

@@ -0,0 +1,155 @@
package tls
import (
"bytes"
"encoding/binary"
"encoding/pem"
E "github.com/sagernet/sing/common/exceptions"
"github.com/cloudflare/circl/hpke"
"github.com/cloudflare/circl/kem"
)
func ECHKeygenDefault(serverName string) (configPem string, keyPem string, err error) {
cipherSuites := []echCipherSuite{
{
kdf: hpke.KDF_HKDF_SHA256,
aead: hpke.AEAD_AES128GCM,
}, {
kdf: hpke.KDF_HKDF_SHA256,
aead: hpke.AEAD_ChaCha20Poly1305,
},
}
keyConfig := []myECHKeyConfig{
{id: 0, kem: hpke.KEM_X25519_HKDF_SHA256},
}
keyPairs, err := echKeygen(0xfe0d, serverName, keyConfig, cipherSuites)
if err != nil {
return
}
var configBuffer bytes.Buffer
var totalLen uint16
for _, keyPair := range keyPairs {
totalLen += uint16(len(keyPair.rawConf))
}
binary.Write(&configBuffer, binary.BigEndian, totalLen)
for _, keyPair := range keyPairs {
configBuffer.Write(keyPair.rawConf)
}
var keyBuffer bytes.Buffer
for _, keyPair := range keyPairs {
keyBuffer.Write(keyPair.rawKey)
}
configPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBuffer.Bytes()}))
keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBuffer.Bytes()}))
return
}
type echKeyConfigPair struct {
id uint8
rawKey []byte
conf myECHKeyConfig
rawConf []byte
}
type echCipherSuite struct {
kdf hpke.KDF
aead hpke.AEAD
}
type myECHKeyConfig struct {
id uint8
kem hpke.KEM
seed []byte
}
func echKeygen(version uint16, serverName string, conf []myECHKeyConfig, suite []echCipherSuite) ([]echKeyConfigPair, error) {
be := binary.BigEndian
// prepare for future update
if version != 0xfe0d {
return nil, E.New("unsupported ECH version", version)
}
suiteBuf := make([]byte, 0, len(suite)*4+2)
suiteBuf = be.AppendUint16(suiteBuf, uint16(len(suite))*4)
for _, s := range suite {
if !s.kdf.IsValid() || !s.aead.IsValid() {
return nil, E.New("invalid HPKE cipher suite")
}
suiteBuf = be.AppendUint16(suiteBuf, uint16(s.kdf))
suiteBuf = be.AppendUint16(suiteBuf, uint16(s.aead))
}
pairs := []echKeyConfigPair{}
for _, c := range conf {
pair := echKeyConfigPair{}
pair.id = c.id
pair.conf = c
if !c.kem.IsValid() {
return nil, E.New("invalid HPKE KEM")
}
kpGenerator := c.kem.Scheme().GenerateKeyPair
if len(c.seed) > 0 {
kpGenerator = func() (kem.PublicKey, kem.PrivateKey, error) {
pub, sec := c.kem.Scheme().DeriveKeyPair(c.seed)
return pub, sec, nil
}
if len(c.seed) < c.kem.Scheme().PrivateKeySize() {
return nil, E.New("HPKE KEM seed too short")
}
}
pub, sec, err := kpGenerator()
if err != nil {
return nil, E.Cause(err, "generate ECH config key pair")
}
b := []byte{}
b = be.AppendUint16(b, version)
b = be.AppendUint16(b, 0) // length field
// contents
// key config
b = append(b, c.id)
b = be.AppendUint16(b, uint16(c.kem))
pubBuf, err := pub.MarshalBinary()
if err != nil {
return nil, E.Cause(err, "serialize ECH public key")
}
b = be.AppendUint16(b, uint16(len(pubBuf)))
b = append(b, pubBuf...)
b = append(b, suiteBuf...)
// end key config
// max name len, not supported
b = append(b, 0)
// server name
b = append(b, byte(len(serverName)))
b = append(b, []byte(serverName)...)
// extensions, not supported
b = be.AppendUint16(b, 0)
be.PutUint16(b[2:], uint16(len(b)-4))
pair.rawConf = b
secBuf, err := sec.MarshalBinary()
if err != nil {
return nil, E.Cause(err, "serialize ECH private key")
}
sk := []byte{}
sk = be.AppendUint16(sk, uint16(len(secBuf)))
sk = append(sk, secBuf...)
sk = be.AppendUint16(sk, uint16(len(b)))
sk = append(sk, b...)
pair.rawKey = sk
pairs = append(pairs, pair)
}
return pairs, nil
}

View File

@@ -1,81 +0,0 @@
package tls
import (
"crypto/ecdh"
"crypto/rand"
"encoding/pem"
"golang.org/x/crypto/cryptobyte"
)
type ECHCapableConfig interface {
Config
ECHConfigList() []byte
SetECHConfigList([]byte)
}
func ECHKeygenDefault(publicName string) (configPem string, keyPem string, err error) {
echKey, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return
}
echConfig, err := marshalECHConfig(0, echKey.PublicKey().Bytes(), publicName, 0)
if err != nil {
return
}
configBuilder := cryptobyte.NewBuilder(nil)
configBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(echConfig)
})
configBytes, err := configBuilder.Bytes()
if err != nil {
return
}
keyBuilder := cryptobyte.NewBuilder(nil)
keyBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(echKey.Bytes())
})
keyBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(echConfig)
})
keyBytes, err := keyBuilder.Bytes()
if err != nil {
return
}
configPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBytes}))
keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBytes}))
return
}
func marshalECHConfig(id uint8, pubKey []byte, publicName string, maxNameLen uint8) ([]byte, error) {
const extensionEncryptedClientHello = 0xfe0d
const DHKEM_X25519_HKDF_SHA256 = 0x0020
const KDF_HKDF_SHA256 = 0x0001
builder := cryptobyte.NewBuilder(nil)
builder.AddUint16(extensionEncryptedClientHello)
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddUint8(id)
builder.AddUint16(DHKEM_X25519_HKDF_SHA256) // The only DHKEM we support
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(pubKey)
})
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
const (
AEAD_AES_128_GCM = 0x0001
AEAD_AES_256_GCM = 0x0002
AEAD_ChaCha20Poly1305 = 0x0003
)
for _, aeadID := range []uint16{AEAD_AES_128_GCM, AEAD_AES_256_GCM, AEAD_ChaCha20Poly1305} {
builder.AddUint16(KDF_HKDF_SHA256) // The only KDF we support
builder.AddUint16(aeadID)
}
})
builder.AddUint8(maxNameLen)
builder.AddUint8LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes([]byte(publicName))
})
builder.AddUint16(0) // extensions
})
return builder.Bytes()
}

View File

@@ -10,7 +10,7 @@ import (
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
) )
func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, options option.OutboundTLSOptions) (Config, error) { func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions, tlsConfig *tls.Config) (Config, error) {
return nil, E.New("ECH requires go1.24, please recompile your binary.") return nil, E.New("ECH requires go1.24, please recompile your binary.")
} }
@@ -18,6 +18,6 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
return E.New("ECH requires go1.24, please recompile your binary.") return E.New("ECH requires go1.24, please recompile your binary.")
} }
func (c *STDServerConfig) setECHServerConfig(echKey []byte) error { func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
panic("unreachable") return E.New("ECH requires go1.24, please recompile your binary.")
} }

View File

@@ -74,7 +74,7 @@ func NewRealityClient(ctx context.Context, serverAddress string, options option.
if decodedLen > 8 { if decodedLen > 8 {
return nil, E.New("invalid short_id") return nil, E.New("invalid short_id")
} }
return &RealityClientConfig{ctx, uClient.(*UTLSClientConfig), publicKey, shortID}, nil return &RealityClientConfig{ctx, uClient, publicKey, shortID}, nil
} }
func (e *RealityClientConfig) ServerName() string { func (e *RealityClientConfig) ServerName() string {
@@ -307,11 +307,3 @@ func (c *realityClientConnWrapper) Upstream() any {
func (c *realityClientConnWrapper) CloseWrite() error { func (c *realityClientConnWrapper) CloseWrite() error {
return c.Close() return c.Close()
} }
func (c *realityClientConnWrapper) ReaderReplaceable() bool {
return true
}
func (c *realityClientConnWrapper) WriterReplaceable() bool {
return true
}

View File

@@ -206,11 +206,3 @@ func (c *realityConnWrapper) Upstream() any {
func (c *realityConnWrapper) CloseWrite() error { func (c *realityConnWrapper) CloseWrite() error {
return c.Close() return c.Close()
} }
func (c *realityConnWrapper) ReaderReplaceable() bool {
return true
}
func (c *realityConnWrapper) WriterReplaceable() bool {
return true
}

View File

@@ -7,60 +7,43 @@ import (
"net" "net"
"os" "os"
"strings" "strings"
"time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/tlsfragment"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
) )
type STDClientConfig struct { type STDClientConfig struct {
ctx context.Context
config *tls.Config config *tls.Config
fragment bool
fragmentFallbackDelay time.Duration
recordFragment bool
} }
func (c *STDClientConfig) ServerName() string { func (s *STDClientConfig) ServerName() string {
return c.config.ServerName return s.config.ServerName
} }
func (c *STDClientConfig) SetServerName(serverName string) { func (s *STDClientConfig) SetServerName(serverName string) {
c.config.ServerName = serverName s.config.ServerName = serverName
} }
func (c *STDClientConfig) NextProtos() []string { func (s *STDClientConfig) NextProtos() []string {
return c.config.NextProtos return s.config.NextProtos
} }
func (c *STDClientConfig) SetNextProtos(nextProto []string) { func (s *STDClientConfig) SetNextProtos(nextProto []string) {
c.config.NextProtos = nextProto s.config.NextProtos = nextProto
} }
func (c *STDClientConfig) Config() (*STDConfig, error) { func (s *STDClientConfig) Config() (*STDConfig, error) {
return c.config, nil return s.config, nil
} }
func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) { func (s *STDClientConfig) Client(conn net.Conn) (Conn, error) {
if c.recordFragment { return tls.Client(conn, s.config), nil
conn = tf.NewConn(conn, c.ctx, c.fragment, c.recordFragment, c.fragmentFallbackDelay)
}
return tls.Client(conn, c.config), nil
} }
func (c *STDClientConfig) Clone() Config { func (s *STDClientConfig) Clone() Config {
return &STDClientConfig{c.ctx, c.config.Clone(), c.fragment, c.fragmentFallbackDelay, c.recordFragment} return &STDClientConfig{s.config.Clone()}
}
func (c *STDClientConfig) ECHConfigList() []byte {
return c.config.EncryptedClientHelloConfigList
}
func (c *STDClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte) {
c.config.EncryptedClientHelloConfigList = EncryptedClientHelloConfigList
} }
func NewSTDClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) { func NewSTDClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
@@ -77,7 +60,9 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
var tlsConfig tls.Config var tlsConfig tls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx) tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx) tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
if !options.DisableSNI { if options.DisableSNI {
tlsConfig.ServerName = "127.0.0.1"
} else {
tlsConfig.ServerName = serverName tlsConfig.ServerName = serverName
} }
if options.Insecure { if options.Insecure {
@@ -86,16 +71,12 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
tlsConfig.InsecureSkipVerify = true tlsConfig.InsecureSkipVerify = true
tlsConfig.VerifyConnection = func(state tls.ConnectionState) error { tlsConfig.VerifyConnection = func(state tls.ConnectionState) error {
verifyOptions := x509.VerifyOptions{ verifyOptions := x509.VerifyOptions{
Roots: tlsConfig.RootCAs,
DNSName: serverName, DNSName: serverName,
Intermediates: x509.NewCertPool(), Intermediates: x509.NewCertPool(),
} }
for _, cert := range state.PeerCertificates[1:] { for _, cert := range state.PeerCertificates[1:] {
verifyOptions.Intermediates.AddCert(cert) verifyOptions.Intermediates.AddCert(cert)
} }
if tlsConfig.Time != nil {
verifyOptions.CurrentTime = tlsConfig.Time()
}
_, err := state.PeerCertificates[0].Verify(verifyOptions) _, err := state.PeerCertificates[0].Verify(verifyOptions)
return err return err
} }
@@ -146,10 +127,8 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
} }
tlsConfig.RootCAs = certPool tlsConfig.RootCAs = certPool
} }
stdConfig := &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
if options.ECH != nil && options.ECH.Enabled { if options.ECH != nil && options.ECH.Enabled {
return parseECHClientConfig(ctx, stdConfig, options) return parseECHClientConfig(ctx, options, &tlsConfig)
} else {
return stdConfig, nil
} }
return &STDClientConfig{&tlsConfig}, nil
} }

View File

@@ -6,7 +6,6 @@ import (
"net" "net"
"os" "os"
"strings" "strings"
"sync"
"time" "time"
"github.com/sagernet/fswatch" "github.com/sagernet/fswatch"
@@ -21,7 +20,6 @@ import (
var errInsecureUnused = E.New("tls: insecure unused") var errInsecureUnused = E.New("tls: insecure unused")
type STDServerConfig struct { type STDServerConfig struct {
access sync.RWMutex
config *tls.Config config *tls.Config
logger log.Logger logger log.Logger
acmeService adapter.SimpleLifecycle acmeService adapter.SimpleLifecycle
@@ -34,22 +32,14 @@ type STDServerConfig struct {
} }
func (c *STDServerConfig) ServerName() string { func (c *STDServerConfig) ServerName() string {
c.access.RLock()
defer c.access.RUnlock()
return c.config.ServerName return c.config.ServerName
} }
func (c *STDServerConfig) SetServerName(serverName string) { func (c *STDServerConfig) SetServerName(serverName string) {
c.access.Lock() c.config.ServerName = serverName
defer c.access.Unlock()
config := c.config.Clone()
config.ServerName = serverName
c.config = config
} }
func (c *STDServerConfig) NextProtos() []string { func (c *STDServerConfig) NextProtos() []string {
c.access.RLock()
defer c.access.RUnlock()
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol { if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
return c.config.NextProtos[1:] return c.config.NextProtos[1:]
} else { } else {
@@ -58,15 +48,11 @@ func (c *STDServerConfig) NextProtos() []string {
} }
func (c *STDServerConfig) SetNextProtos(nextProto []string) { func (c *STDServerConfig) SetNextProtos(nextProto []string) {
c.access.Lock()
defer c.access.Unlock()
config := c.config.Clone()
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol { if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
config.NextProtos = append(c.config.NextProtos[:1], nextProto...) c.config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
} else { } else {
config.NextProtos = nextProto c.config.NextProtos = nextProto
} }
c.config = config
} }
func (c *STDServerConfig) Config() (*STDConfig, error) { func (c *STDServerConfig) Config() (*STDConfig, error) {
@@ -91,6 +77,9 @@ func (c *STDServerConfig) Start() error {
if c.acmeService != nil { if c.acmeService != nil {
return c.acmeService.Start() return c.acmeService.Start()
} else { } else {
if c.certificatePath == "" && c.keyPath == "" {
return nil
}
err := c.startWatcher() err := c.startWatcher()
if err != nil { if err != nil {
c.logger.Warn("create fsnotify watcher: ", err) c.logger.Warn("create fsnotify watcher: ", err)
@@ -110,9 +99,6 @@ func (c *STDServerConfig) startWatcher() error {
if c.echKeyPath != "" { if c.echKeyPath != "" {
watchPath = append(watchPath, c.echKeyPath) watchPath = append(watchPath, c.echKeyPath)
} }
if len(watchPath) == 0 {
return nil
}
watcher, err := fswatch.NewWatcher(fswatch.Options{ watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: watchPath, Path: watchPath,
Callback: func(path string) { Callback: func(path string) {
@@ -152,18 +138,10 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
if err != nil { if err != nil {
return E.Cause(err, "reload key pair") return E.Cause(err, "reload key pair")
} }
c.access.Lock() c.config.Certificates = []tls.Certificate{keyPair}
config := c.config.Clone()
config.Certificates = []tls.Certificate{keyPair}
c.config = config
c.access.Unlock()
c.logger.Info("reloaded TLS certificate") c.logger.Info("reloaded TLS certificate")
} else if path == c.echKeyPath { } else if path == c.echKeyPath {
echKey, err := os.ReadFile(c.echKeyPath) err := reloadECHKeys(c.echKeyPath, c.config)
if err != nil {
return E.Cause(err, "reload ECH keys from ", c.echKeyPath)
}
err = c.setECHServerConfig(echKey)
if err != nil { if err != nil {
return err return err
} }
@@ -191,7 +169,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
var err error var err error
if options.ACME != nil && len(options.ACME.Domain) > 0 { if options.ACME != nil && len(options.ACME.Domain) > 0 {
//nolint:staticcheck //nolint:staticcheck
tlsConfig, acmeService, err = startACME(ctx, logger, common.PtrValueOrDefault(options.ACME)) tlsConfig, acmeService, err = startACME(ctx, common.PtrValueOrDefault(options.ACME))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -284,7 +262,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
return nil, err return nil, err
} }
} }
serverConfig := &STDServerConfig{ return &STDServerConfig{
config: tlsConfig, config: tlsConfig,
logger: logger, logger: logger,
acmeService: acmeService, acmeService: acmeService,
@@ -293,11 +271,5 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
certificatePath: options.CertificatePath, certificatePath: options.CertificatePath,
keyPath: options.KeyPath, keyPath: options.KeyPath,
echKeyPath: echKeyPath, echKeyPath: echKeyPath,
} }, nil
serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
serverConfig.access.Lock()
defer serverConfig.access.Unlock()
return serverConfig.config, nil
}
return serverConfig, nil
} }

View File

@@ -11,13 +11,10 @@ type TimeServiceWrapper struct {
} }
func (w *TimeServiceWrapper) TimeFunc() func() time.Time { func (w *TimeServiceWrapper) TimeFunc() func() time.Time {
return func() time.Time { if w.TimeService == nil {
if w.TimeService != nil { return nil
return w.TimeService.TimeFunc()()
} else {
return time.Now()
}
} }
return w.TimeService.TimeFunc()
} }
func (w *TimeServiceWrapper) Upstream() any { func (w *TimeServiceWrapper) Upstream() any {

View File

@@ -8,12 +8,11 @@ import (
"crypto/x509" "crypto/x509"
"math/rand" "math/rand"
"net" "net"
"net/netip"
"os" "os"
"strings" "strings"
"time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/tlsfragment"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
@@ -23,62 +22,48 @@ import (
) )
type UTLSClientConfig struct { type UTLSClientConfig struct {
ctx context.Context
config *utls.Config config *utls.Config
id utls.ClientHelloID id utls.ClientHelloID
fragment bool
fragmentFallbackDelay time.Duration
recordFragment bool
} }
func (c *UTLSClientConfig) ServerName() string { func (e *UTLSClientConfig) ServerName() string {
return c.config.ServerName return e.config.ServerName
} }
func (c *UTLSClientConfig) SetServerName(serverName string) { func (e *UTLSClientConfig) SetServerName(serverName string) {
c.config.ServerName = serverName e.config.ServerName = serverName
} }
func (c *UTLSClientConfig) NextProtos() []string { func (e *UTLSClientConfig) NextProtos() []string {
return c.config.NextProtos return e.config.NextProtos
} }
func (c *UTLSClientConfig) SetNextProtos(nextProto []string) { func (e *UTLSClientConfig) SetNextProtos(nextProto []string) {
if len(nextProto) == 1 && nextProto[0] == http2.NextProtoTLS { if len(nextProto) == 1 && nextProto[0] == http2.NextProtoTLS {
nextProto = append(nextProto, "http/1.1") nextProto = append(nextProto, "http/1.1")
} }
c.config.NextProtos = nextProto e.config.NextProtos = nextProto
} }
func (c *UTLSClientConfig) Config() (*STDConfig, error) { func (e *UTLSClientConfig) Config() (*STDConfig, error) {
return nil, E.New("unsupported usage for uTLS") return nil, E.New("unsupported usage for uTLS")
} }
func (c *UTLSClientConfig) Client(conn net.Conn) (Conn, error) { func (e *UTLSClientConfig) Client(conn net.Conn) (Conn, error) {
if c.recordFragment { return &utlsALPNWrapper{utlsConnWrapper{utls.UClient(conn, e.config.Clone(), e.id)}, e.config.NextProtos}, nil
conn = tf.NewConn(conn, c.ctx, c.fragment, c.recordFragment, c.fragmentFallbackDelay)
}
return &utlsALPNWrapper{utlsConnWrapper{utls.UClient(conn, c.config.Clone(), c.id)}, c.config.NextProtos}, nil
} }
func (c *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) { func (e *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) {
c.config.SessionIDGenerator = generator e.config.SessionIDGenerator = generator
} }
func (c *UTLSClientConfig) Clone() Config { func (e *UTLSClientConfig) Clone() Config {
return &UTLSClientConfig{ return &UTLSClientConfig{
c.ctx, c.config.Clone(), c.id, c.fragment, c.fragmentFallbackDelay, c.recordFragment, config: e.config.Clone(),
id: e.id,
} }
} }
func (c *UTLSClientConfig) ECHConfigList() []byte {
return c.config.EncryptedClientHelloConfigList
}
func (c *UTLSClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte) {
c.config.EncryptedClientHelloConfigList = EncryptedClientHelloConfigList
}
type utlsConnWrapper struct { type utlsConnWrapper struct {
*utls.UConn *utls.UConn
} }
@@ -106,14 +91,6 @@ func (c *utlsConnWrapper) Upstream() any {
return c.UConn return c.UConn
} }
func (c *utlsConnWrapper) ReaderReplaceable() bool {
return true
}
func (c *utlsConnWrapper) WriterReplaceable() bool {
return true
}
type utlsALPNWrapper struct { type utlsALPNWrapper struct {
utlsConnWrapper utlsConnWrapper
nextProtocols []string nextProtocols []string
@@ -139,13 +116,15 @@ func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error {
return c.UConn.HandshakeContext(ctx) return c.UConn.HandshakeContext(ctx)
} }
func NewUTLSClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) { func NewUTLSClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (*UTLSClientConfig, error) {
var serverName string var serverName string
if options.ServerName != "" { if options.ServerName != "" {
serverName = options.ServerName serverName = options.ServerName
} else if serverAddress != "" { } else if serverAddress != "" {
if _, err := netip.ParseAddr(serverName); err != nil {
serverName = serverAddress serverName = serverAddress
} }
}
if serverName == "" && !options.Insecure { if serverName == "" && !options.Insecure {
return nil, E.New("missing server_name or insecure=true") return nil, E.New("missing server_name or insecure=true")
} }
@@ -153,16 +132,15 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
var tlsConfig utls.Config var tlsConfig utls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx) tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx) tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
if !options.DisableSNI { if options.DisableSNI {
tlsConfig.ServerName = "127.0.0.1"
} else {
tlsConfig.ServerName = serverName tlsConfig.ServerName = serverName
} }
if options.Insecure { if options.Insecure {
tlsConfig.InsecureSkipVerify = options.Insecure tlsConfig.InsecureSkipVerify = options.Insecure
} else if options.DisableSNI { } else if options.DisableSNI {
if options.Reality != nil && options.Reality.Enabled { return nil, E.New("disable_sni is unsupported in uTLS")
return nil, E.New("disable_sni is unsupported in reality")
}
tlsConfig.InsecureServerNameToVerify = serverName
} }
if len(options.ALPN) > 0 { if len(options.ALPN) > 0 {
tlsConfig.NextProtos = options.ALPN tlsConfig.NextProtos = options.ALPN
@@ -214,15 +192,7 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
if err != nil { if err != nil {
return nil, err return nil, err
} }
uConfig := &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment} return &UTLSClientConfig{&tlsConfig, id}, nil
if options.ECH != nil && options.ECH.Enabled {
if options.Reality != nil && options.Reality.Enabled {
return nil, E.New("Reality is conflict with ECH")
}
return parseECHClientConfig(ctx, uConfig, options)
} else {
return uConfig, nil
}
} }
var ( var (
@@ -250,7 +220,7 @@ func init() {
func uTLSClientHelloID(name string) (utls.ClientHelloID, error) { func uTLSClientHelloID(name string) (utls.ClientHelloID, error) {
switch name { switch name {
case "chrome_psk", "chrome_psk_shuffle", "chrome_padding_psk_shuffle", "chrome_pq", "chrome_pq_psk": case "chrome_psk", "chrome_psk_shuffle", "chrome_padding_psk_shuffle", "chrome_pq":
fallthrough fallthrough
case "chrome", "": case "chrome", "":
return utls.HelloChrome_Auto, nil return utls.HelloChrome_Auto, nil

View File

@@ -9,7 +9,6 @@ import (
"strings" "strings"
"time" "time"
C "github.com/sagernet/sing-box/constant"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"golang.org/x/net/publicsuffix" "golang.org/x/net/publicsuffix"
@@ -20,21 +19,16 @@ type Conn struct {
tcpConn *net.TCPConn tcpConn *net.TCPConn
ctx context.Context ctx context.Context
firstPacketWritten bool firstPacketWritten bool
splitPacket bool
splitRecord bool splitRecord bool
fallbackDelay time.Duration fallbackDelay time.Duration
} }
func NewConn(conn net.Conn, ctx context.Context, splitPacket bool, splitRecord bool, fallbackDelay time.Duration) *Conn { func NewConn(conn net.Conn, ctx context.Context, splitRecord bool, fallbackDelay time.Duration) *Conn {
if fallbackDelay == 0 {
fallbackDelay = C.TLSFragmentFallbackDelay
}
tcpConn, _ := N.UnwrapReader(conn).(*net.TCPConn) tcpConn, _ := N.UnwrapReader(conn).(*net.TCPConn)
return &Conn{ return &Conn{
Conn: conn, Conn: conn,
tcpConn: tcpConn, tcpConn: tcpConn,
ctx: ctx, ctx: ctx,
splitPacket: splitPacket,
splitRecord: splitRecord, splitRecord: splitRecord,
fallbackDelay: fallbackDelay, fallbackDelay: fallbackDelay,
} }
@@ -45,9 +39,9 @@ func (c *Conn) Write(b []byte) (n int, err error) {
defer func() { defer func() {
c.firstPacketWritten = true c.firstPacketWritten = true
}() }()
serverName := IndexTLSServerName(b) serverName := indexTLSServerName(b)
if serverName != nil { if serverName != nil {
if c.splitPacket { if !c.splitRecord {
if c.tcpConn != nil { if c.tcpConn != nil {
err = c.tcpConn.SetNoDelay(true) err = c.tcpConn.SetNoDelay(true)
if err != nil { if err != nil {
@@ -87,19 +81,11 @@ func (c *Conn) Write(b []byte) (n int, err error) {
payload = b[splitIndexes[i-1]:splitIndexes[i]] payload = b[splitIndexes[i-1]:splitIndexes[i]]
} }
if c.splitRecord { if c.splitRecord {
if c.splitPacket {
buffer.Reset()
}
payloadLen := uint16(len(payload)) payloadLen := uint16(len(payload))
buffer.Write(b[:3]) buffer.Write(b[:3])
binary.Write(&buffer, binary.BigEndian, payloadLen) binary.Write(&buffer, binary.BigEndian, payloadLen)
buffer.Write(payload) buffer.Write(payload)
if c.splitPacket { } else if c.tcpConn != nil && i != len(splitIndexes) {
payload = buffer.Bytes()
}
}
if c.splitPacket {
if c.tcpConn != nil && i != len(splitIndexes) {
err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay) err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay)
if err != nil { if err != nil {
return return
@@ -109,24 +95,21 @@ func (c *Conn) Write(b []byte) (n int, err error) {
if err != nil { if err != nil {
return return
} }
if i != len(splitIndexes) {
time.Sleep(c.fallbackDelay)
} }
} }
} if c.splitRecord {
}
if c.splitRecord && !c.splitPacket {
_, err = c.Conn.Write(buffer.Bytes()) _, err = c.Conn.Write(buffer.Bytes())
if err != nil { if err != nil {
return return
} }
} } else {
if c.tcpConn != nil { if c.tcpConn != nil {
err = c.tcpConn.SetNoDelay(false) err = c.tcpConn.SetNoDelay(false)
if err != nil { if err != nil {
return return
} }
} }
}
return len(b), nil return len(b), nil
} }
} }

View File

@@ -15,7 +15,7 @@ func TestTLSFragment(t *testing.T) {
t.Parallel() t.Parallel()
tcpConn, err := net.Dial("tcp", "1.1.1.1:443") tcpConn, err := net.Dial("tcp", "1.1.1.1:443")
require.NoError(t, err) require.NoError(t, err)
tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), true, false, 0), &tls.Config{ tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), false, 0), &tls.Config{
ServerName: "www.cloudflare.com", ServerName: "www.cloudflare.com",
}) })
require.NoError(t, tlsConn.Handshake()) require.NoError(t, tlsConn.Handshake())
@@ -25,17 +25,7 @@ func TestTLSRecordFragment(t *testing.T) {
t.Parallel() t.Parallel()
tcpConn, err := net.Dial("tcp", "1.1.1.1:443") tcpConn, err := net.Dial("tcp", "1.1.1.1:443")
require.NoError(t, err) require.NoError(t, err)
tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), false, true, 0), &tls.Config{ tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), true, 0), &tls.Config{
ServerName: "www.cloudflare.com",
})
require.NoError(t, tlsConn.Handshake())
}
func TestTLS2Fragment(t *testing.T) {
t.Parallel()
tcpConn, err := net.Dial("tcp", "1.1.1.1:443")
require.NoError(t, err)
tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), true, true, 0), &tls.Config{
ServerName: "www.cloudflare.com", ServerName: "www.cloudflare.com",
}) })
require.NoError(t, tlsConn.Handshake()) require.NoError(t, tlsConn.Handshake())

View File

@@ -22,13 +22,13 @@ const (
tls13 uint16 = 0x0304 tls13 uint16 = 0x0304
) )
type MyServerName struct { type myServerName struct {
Index int Index int
Length int Length int
ServerName string ServerName string
} }
func IndexTLSServerName(payload []byte) *MyServerName { func indexTLSServerName(payload []byte) *myServerName {
if len(payload) < recordLayerHeaderLen || payload[0] != contentType { if len(payload) < recordLayerHeaderLen || payload[0] != contentType {
return nil return nil
} }
@@ -36,48 +36,47 @@ func IndexTLSServerName(payload []byte) *MyServerName {
if len(payload) < recordLayerHeaderLen+int(segmentLen) { if len(payload) < recordLayerHeaderLen+int(segmentLen) {
return nil return nil
} }
serverName := indexTLSServerNameFromHandshake(payload[recordLayerHeaderLen:]) serverName := indexTLSServerNameFromHandshake(payload[recordLayerHeaderLen : recordLayerHeaderLen+int(segmentLen)])
if serverName == nil { if serverName == nil {
return nil return nil
} }
serverName.Index += recordLayerHeaderLen serverName.Length += recordLayerHeaderLen
return serverName return serverName
} }
func indexTLSServerNameFromHandshake(handshake []byte) *MyServerName { func indexTLSServerNameFromHandshake(hs []byte) *myServerName {
if len(handshake) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen { if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen {
return nil return nil
} }
if handshake[0] != handshakeType { if hs[0] != handshakeType {
return nil return nil
} }
handshakeLen := uint32(handshake[1])<<16 | uint32(handshake[2])<<8 | uint32(handshake[3]) handshakeLen := uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3])
if len(handshake[4:]) != int(handshakeLen) { if len(hs[4:]) != int(handshakeLen) {
return nil return nil
} }
tlsVersion := uint16(handshake[4])<<8 | uint16(handshake[5]) tlsVersion := uint16(hs[4])<<8 | uint16(hs[5])
if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 { if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 {
return nil return nil
} }
sessionIDLen := handshake[38] sessionIDLen := hs[38]
currentIndex := handshakeHeaderLen + randomDataLen + sessionIDHeaderLen + int(sessionIDLen) if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen) {
if len(handshake) < currentIndex {
return nil return nil
} }
cipherSuites := handshake[currentIndex:] cs := hs[handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen):]
if len(cipherSuites) < cipherSuiteHeaderLen { if len(cs) < cipherSuiteHeaderLen {
return nil return nil
} }
csLen := uint16(cipherSuites[0])<<8 | uint16(cipherSuites[1]) csLen := uint16(cs[0])<<8 | uint16(cs[1])
if len(cipherSuites) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen { if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen {
return nil return nil
} }
compressMethodLen := uint16(cipherSuites[cipherSuiteHeaderLen+int(csLen)]) compressMethodLen := uint16(cs[cipherSuiteHeaderLen+int(csLen)])
currentIndex += cipherSuiteHeaderLen + int(csLen) + compressMethodHeaderLen + int(compressMethodLen) if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen) {
if len(handshake) < currentIndex {
return nil return nil
} }
serverName := indexTLSServerNameFromExtensions(handshake[currentIndex:]) currentIndex := cipherSuiteHeaderLen + int(csLen) + compressMethodHeaderLen + int(compressMethodLen)
serverName := indexTLSServerNameFromExtensions(cs[currentIndex:])
if serverName == nil { if serverName == nil {
return nil return nil
} }
@@ -85,7 +84,7 @@ func indexTLSServerNameFromHandshake(handshake []byte) *MyServerName {
return serverName return serverName
} }
func indexTLSServerNameFromExtensions(exs []byte) *MyServerName { func indexTLSServerNameFromExtensions(exs []byte) *myServerName {
if len(exs) == 0 { if len(exs) == 0 {
return nil return nil
} }
@@ -119,8 +118,7 @@ func indexTLSServerNameFromExtensions(exs []byte) *MyServerName {
} }
sniLen := uint16(sex[3])<<8 | uint16(sex[4]) sniLen := uint16(sex[3])<<8 | uint16(sex[4])
sex = sex[sniExtensionHeaderLen:] sex = sex[sniExtensionHeaderLen:]
return &myServerName{
return &MyServerName{
Index: currentIndex + extensionHeaderLen + sniExtensionHeaderLen, Index: currentIndex + extensionHeaderLen + sniExtensionHeaderLen,
Length: int(sniLen), Length: int(sniLen),
ServerName: string(sex), ServerName: string(sex),

View File

@@ -1,20 +0,0 @@
package tf_test
import (
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/common/tlsfragment"
"github.com/stretchr/testify/require"
)
func TestIndexTLSServerName(t *testing.T) {
t.Parallel()
payload, err := hex.DecodeString("16030105f8010005f403036e35de7389a679c54029cf452611f2211c70d9ac3897271de589ab6155f8e4ab20637d225f1ef969ad87ed78bfb9d171300bcb1703b6f314ccefb964f79b7d0961002a0a0a130213031301c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a01000581baba00000000000f000d00000a6769746875622e636f6d00170000ff01000100000a000e000c3a3a11ec001d001700180019000b000201000010000e000c02683208687474702f312e31000500050100000000000d00160014040308040401050308050805050108060601020100120000003304ef04ed3a3a00010011ec04c0aeb2250c092a3463161cccb29d9183331a424964248579507ed23a180b0ceab2a5f5d9ce41547e497a89055471ea572867ba3a1fc3c9e45025274a20f60c6b60e62476b6afed0403af59ab83660ef4112ae20386a602010d0a5d454c0ed34c84ed4423e750213e6a2baab1bf9c4367a6007ab40a33d95220c2dcaa44f257024a5626b545db0510f4311b1a60714154909c6a61fdfca011fb2626d657aeb6070bf078508babe3b584555013e34acc56198ed4663742b3155a664a9901794c4586820a7dc162c01827291f3792e1237f801a8d1ef096013c181c4a58d2f6859ba75022d18cc4418bd4f351d5c18f83a58857d05af860c4b9ac018a5b63f17184e591532c6bc2cf2215d4a282c8a8a4f6f7aee110422c8bc9ebd3b1d609c568523aaae555db320e6c269473d87af38c256cbb9febc20aea6380c32a8916f7a373c8b1e37554e3260bf6621f6b804ee80b3c516b1d01985bf4c603b6daa9a5991de6a7a29f3a7122b8afb843a7660110fce62b43c615f5bcc2db688ba012649c0952b0a2c031e732d2b454c6b2968683cb8d244be2c9a7fa163222979eaf92722b92b862d81a3d94450c2b60c318421ebb4307c42d1f0473592a5c30e42039cc68cda9721e61aa63f49def17c15221680ed444896340133bbee67556f56b9f9d78a4df715f926a12add0cc9c862e46ea8b7316ae468282c18601b2771c9c9322f982228cf93effaacd3f80cbd12bce5fc36f56e2a3caf91e578a5fae00c9b23a8ed1a66764f4433c3628a70b8f0a6196adc60a4cb4226f07ba4c6b363fe9065563bfc1347452946386bab488686e837ab979c64f9047417fca635fe1bb4f074f256cc8af837c7b455e280426547755af90a61640169ef180aea3a77e662bb6dac1b6c3696027129b1a5edf495314e9c7f4b6110e16378ec893fa24642330a40aba1a85326101acb97c620fd8d71389e69eaed7bdb01bbe1fd428d66191150c7b2cd1ad4257391676a82ba8ce07fb2667c3b289f159003a7c7bc31d361b7b7f49a802961739d950dfcc0fa1c7abce5abdd2245101da391151490862028110465950b9e9c03d08a90998ab83267838d2e74a0593bc81f74cdf734519a05b351c0e5488c68dd810e6e9142ccc1e2f4a7f464297eb340e27acc6b9d64e12e38cce8492b3d939140b5a9e149a75597f10a23874c84323a07cdd657274378f887c85c4259b9c04cd33ba58ed630ef2a744f8e19dd34843dff331d2a6be7e2332c599289cd248a611c73d7481cd4a9bd43449a3836f14b2af18a1739e17999e4c67e85cc5bcecabb14185e5bcaff3c96098f03dc5aba819f29587758f49f940585354a2a780830528d68ccd166920dadcaa25cab5fc1907272a826aba3f08bc6b88757776812ecb6c7cec69a223ec0a13a7b62a2349a0f63ed7a27a3b15ba21d71fe6864ec6e089ae17cadd433fa3138f7ee24353c11365818f8fc34f43a05542d18efaac24bfccc1f748a0cc1a67ad379468b76fd34973dba785f5c91d618333cd810fe0700d1bbc8422029782628070a624c52c5309a4a64d625b11f8033ab28df34a1add297517fcc06b92b6817b3c5144438cf260867c57bde68c8c4b82e6a135ef676a52fbae5708002a404e6189a60e2836de565ad1b29e3819e5ed49f6810bcb28e1bd6de57306f94b79d9dae1cc4624d2a068499beef81cd5fe4b76dcbfff2a2008001d002001976128c6d5a934533f28b9914d2480aab2a8c1ab03d212529ce8b27640a716002d00020101002b000706caca03040303001b00030200015a5a000100")
require.NoError(t, err)
serverName := tf.IndexTLSServerName(payload)
require.NotNil(t, serverName)
require.Equal(t, serverName.ServerName, string(payload[serverName.Index:serverName.Index+serverName.Length]))
require.Equal(t, "github.com", serverName.ServerName)
}

View File

@@ -9,10 +9,6 @@ import (
) )
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error { func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
_, err := conn.Write(payload)
if err != nil {
return err
}
time.Sleep(fallbackDelay) time.Sleep(fallbackDelay)
return nil return nil
} }

View File

@@ -16,9 +16,6 @@ func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fal
err := winiphlpapi.WriteAndWaitAck(ctx, conn, payload) err := winiphlpapi.WriteAndWaitAck(ctx, conn, payload)
if err != nil { if err != nil {
if errors.Is(err, windows.ERROR_ACCESS_DENIED) { if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
if _, err := conn.Write(payload); err != nil {
return err
}
time.Sleep(fallbackDelay) time.Sleep(fallbackDelay)
return nil return nil
} }

View File

@@ -1,13 +0,0 @@
package urltest
import "context"
type contextKeyIsUnifiedDelay struct{}
func ContextWithIsUnifiedDelay(ctx context.Context) context.Context {
return context.WithValue(ctx, contextKeyIsUnifiedDelay{}, true)
}
func IsUnifiedDelayFromContext(ctx context.Context) bool {
return ctx.Value(contextKeyIsUnifiedDelay{}) != nil
}

View File

@@ -47,15 +47,15 @@ func (s *HistoryStorage) LoadURLTestHistory(tag string) *adapter.URLTestHistory
func (s *HistoryStorage) DeleteURLTestHistory(tag string) { func (s *HistoryStorage) DeleteURLTestHistory(tag string) {
s.access.Lock() s.access.Lock()
delete(s.delayHistory, tag) delete(s.delayHistory, tag)
s.notifyUpdated()
s.access.Unlock() s.access.Unlock()
s.notifyUpdated()
} }
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) { func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) {
s.access.Lock() s.access.Lock()
s.delayHistory[tag] = history s.delayHistory[tag] = history
s.notifyUpdated()
s.access.Unlock() s.access.Unlock()
s.notifyUpdated()
} }
func (s *HistoryStorage) notifyUpdated() { func (s *HistoryStorage) notifyUpdated() {
@@ -69,8 +69,6 @@ func (s *HistoryStorage) notifyUpdated() {
} }
func (s *HistoryStorage) Close() error { func (s *HistoryStorage) Close() error {
s.access.Lock()
defer s.access.Unlock()
s.updateHook = nil s.updateHook = nil
return nil return nil
} }
@@ -128,15 +126,6 @@ func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err e
return return
} }
resp.Body.Close() resp.Body.Close()
if IsUnifiedDelayFromContext(ctx) {
second := time.Now()
resp, err = client.Do(req)
if err != nil {
return
}
resp.Body.Close()
start = second
}
t = uint16(time.Since(start) / time.Millisecond) t = uint16(time.Since(start) / time.Millisecond)
return return
} }

View File

@@ -1,337 +0,0 @@
package buf
import (
"io"
"github.com/sagernet/sing-box/common/xray/bytespool"
"github.com/sagernet/sing-box/common/xray/net"
E "github.com/sagernet/sing/common/exceptions"
)
const (
// Size of a regular buffer.
Size = 8192
)
var zero = [Size * 10]byte{0}
var pool = bytespool.GetPool(Size)
// ownership represents the data owner of the buffer.
type ownership uint8
const (
managed ownership = iota
unmanaged
bytespools
)
// Buffer is a recyclable allocation of a byte array. Buffer.Release() recycles
// the buffer into an internal buffer pool, in order to recreate a buffer more
// quickly.
type Buffer struct {
v []byte
start int32
end int32
ownership ownership
UDP *net.Destination
}
// New creates a Buffer with 0 length and 8K capacity, managed.
func New() *Buffer {
buf := pool.Get().([]byte)
if cap(buf) >= Size {
buf = buf[:Size]
} else {
buf = make([]byte, Size)
}
return &Buffer{
v: buf,
}
}
// NewExisted creates a standard size Buffer with an existed bytearray, managed.
func NewExisted(b []byte) *Buffer {
if cap(b) < Size {
panic("Invalid buffer")
}
oLen := len(b)
if oLen < Size {
b = b[:Size]
}
return &Buffer{
v: b,
end: int32(oLen),
}
}
// FromBytes creates a Buffer with an existed bytearray, unmanaged.
func FromBytes(b []byte) *Buffer {
return &Buffer{
v: b,
end: int32(len(b)),
ownership: unmanaged,
}
}
// StackNew creates a new Buffer object on stack, managed.
// This method is for buffers that is released in the same function.
func StackNew() Buffer {
buf := pool.Get().([]byte)
if cap(buf) >= Size {
buf = buf[:Size]
} else {
buf = make([]byte, Size)
}
return Buffer{
v: buf,
}
}
// NewWithSize creates a Buffer with 0 length and capacity with at least the given size, bytespool's.
func NewWithSize(size int32) *Buffer {
return &Buffer{
v: bytespool.Alloc(size),
ownership: bytespools,
}
}
// Release recycles the buffer into an internal buffer pool.
func (b *Buffer) Release() {
if b == nil || b.v == nil || b.ownership == unmanaged {
return
}
p := b.v
b.v = nil
b.Clear()
switch b.ownership {
case managed:
if cap(p) == Size {
pool.Put(p)
}
case bytespools:
bytespool.Free(p)
}
b.UDP = nil
}
// Clear clears the content of the buffer, results an empty buffer with
// Len() = 0.
func (b *Buffer) Clear() {
b.start = 0
b.end = 0
}
// Byte returns the bytes at index.
func (b *Buffer) Byte(index int32) byte {
return b.v[b.start+index]
}
// SetByte sets the byte value at index.
func (b *Buffer) SetByte(index int32, value byte) {
b.v[b.start+index] = value
}
// Bytes returns the content bytes of this Buffer.
func (b *Buffer) Bytes() []byte {
return b.v[b.start:b.end]
}
// Extend increases the buffer size by n bytes, and returns the extended part.
// It panics if result size is larger than buf.Size.
func (b *Buffer) Extend(n int32) []byte {
end := b.end + n
if end > int32(len(b.v)) {
panic("extending out of bound")
}
ext := b.v[b.end:end]
b.end = end
copy(ext, zero[:])
return ext
}
// BytesRange returns a slice of this buffer with given from and to boundary.
func (b *Buffer) BytesRange(from, to int32) []byte {
if from < 0 {
from += b.Len()
}
if to < 0 {
to += b.Len()
}
return b.v[b.start+from : b.start+to]
}
// BytesFrom returns a slice of this Buffer starting from the given position.
func (b *Buffer) BytesFrom(from int32) []byte {
if from < 0 {
from += b.Len()
}
return b.v[b.start+from : b.end]
}
// BytesTo returns a slice of this Buffer from start to the given position.
func (b *Buffer) BytesTo(to int32) []byte {
if to < 0 {
to += b.Len()
}
if to < 0 {
to = 0
}
return b.v[b.start : b.start+to]
}
// Check makes sure that 0 <= b.start <= b.end.
func (b *Buffer) Check() {
if b.start < 0 {
b.start = 0
}
if b.end < 0 {
b.end = 0
}
if b.start > b.end {
b.start = b.end
}
}
// Resize cuts the buffer at the given position.
func (b *Buffer) Resize(from, to int32) {
oldEnd := b.end
if from < 0 {
from += b.Len()
}
if to < 0 {
to += b.Len()
}
if to < from {
panic("Invalid slice")
}
b.end = b.start + to
b.start += from
b.Check()
if b.end > oldEnd {
copy(b.v[oldEnd:b.end], zero[:])
}
}
// Advance cuts the buffer at the given position.
func (b *Buffer) Advance(from int32) {
if from < 0 {
from += b.Len()
}
b.start += from
b.Check()
}
// Len returns the length of the buffer content.
func (b *Buffer) Len() int32 {
if b == nil {
return 0
}
return b.end - b.start
}
// Cap returns the capacity of the buffer content.
func (b *Buffer) Cap() int32 {
if b == nil {
return 0
}
return int32(len(b.v))
}
// IsEmpty returns true if the buffer is empty.
func (b *Buffer) IsEmpty() bool {
return b.Len() == 0
}
// IsFull returns true if the buffer has no more room to grow.
func (b *Buffer) IsFull() bool {
return b != nil && b.end == int32(len(b.v))
}
// Write implements Write method in io.Writer.
func (b *Buffer) Write(data []byte) (int, error) {
nBytes := copy(b.v[b.end:], data)
b.end += int32(nBytes)
return nBytes, nil
}
// WriteByte writes a single byte into the buffer.
func (b *Buffer) WriteByte(v byte) error {
if b.IsFull() {
return E.New("buffer full")
}
b.v[b.end] = v
b.end++
return nil
}
// WriteString implements io.StringWriter.
func (b *Buffer) WriteString(s string) (int, error) {
return b.Write([]byte(s))
}
// ReadByte implements io.ByteReader
func (b *Buffer) ReadByte() (byte, error) {
if b.start == b.end {
return 0, io.EOF
}
nb := b.v[b.start]
b.start++
return nb, nil
}
// ReadBytes implements bufio.Reader.ReadBytes
func (b *Buffer) ReadBytes(length int32) ([]byte, error) {
if b.end-b.start < length {
return nil, io.EOF
}
nb := b.v[b.start : b.start+length]
b.start += length
return nb, nil
}
// Read implements io.Reader.Read().
func (b *Buffer) Read(data []byte) (int, error) {
if b.Len() == 0 {
return 0, io.EOF
}
nBytes := copy(data, b.v[b.start:b.end])
if int32(nBytes) == b.Len() {
b.Clear()
} else {
b.start += int32(nBytes)
}
return nBytes, nil
}
// ReadFrom implements io.ReaderFrom.
func (b *Buffer) ReadFrom(reader io.Reader) (int64, error) {
n, err := reader.Read(b.v[b.end:])
b.end += int32(n)
return int64(n), err
}
// ReadFullFrom reads exact size of bytes from given reader, or until error occurs.
func (b *Buffer) ReadFullFrom(reader io.Reader, size int32) (int64, error) {
end := b.end + size
if end > int32(len(b.v)) {
v := end
return 0, E.New("out of bound: ", v)
}
n, err := io.ReadFull(reader, b.v[b.end:end])
b.end += int32(n)
return int64(n), err
}
// String returns the string form of this Buffer.
func (b *Buffer) String() string {
return string(b.Bytes())
}

View File

@@ -1,124 +0,0 @@
package buf
import (
"io"
"time"
"github.com/sagernet/sing-box/common/xray/errors"
"github.com/sagernet/sing-box/common/xray/signal"
E "github.com/sagernet/sing/common/exceptions"
)
type dataHandler func(MultiBuffer)
type copyHandler struct {
onData []dataHandler
}
// SizeCounter is for counting bytes copied by Copy().
type SizeCounter struct {
Size int64
}
// CopyOption is an option for copying data.
type CopyOption func(*copyHandler)
// UpdateActivity is a CopyOption to update activity on each data copy operation.
func UpdateActivity(timer signal.ActivityUpdater) CopyOption {
return func(handler *copyHandler) {
handler.onData = append(handler.onData, func(MultiBuffer) {
timer.Update()
})
}
}
// CountSize is a CopyOption that sums the total size of data copied into the given SizeCounter.
func CountSize(sc *SizeCounter) CopyOption {
return func(handler *copyHandler) {
handler.onData = append(handler.onData, func(b MultiBuffer) {
sc.Size += int64(b.Len())
})
}
}
type readError struct {
error
}
func (e readError) Error() string {
return e.error.Error()
}
func (e readError) Unwrap() error {
return e.error
}
// IsReadError returns true if the error in Copy() comes from reading.
func IsReadError(err error) bool {
_, ok := err.(readError)
return ok
}
type writeError struct {
error
}
func (e writeError) Error() string {
return e.error.Error()
}
func (e writeError) Unwrap() error {
return e.error
}
// IsWriteError returns true if the error in Copy() comes from writing.
func IsWriteError(err error) bool {
_, ok := err.(writeError)
return ok
}
func copyInternal(reader Reader, writer Writer, handler *copyHandler) error {
for {
buffer, err := reader.ReadMultiBuffer()
if !buffer.IsEmpty() {
for _, handler := range handler.onData {
handler(buffer)
}
if werr := writer.WriteMultiBuffer(buffer); werr != nil {
return writeError{werr}
}
}
if err != nil {
return readError{err}
}
}
}
// Copy dumps all payload from reader to writer or stops when an error occurs. It returns nil when EOF.
func Copy(reader Reader, writer Writer, options ...CopyOption) error {
var handler copyHandler
for _, option := range options {
option(&handler)
}
err := copyInternal(reader, writer, &handler)
if err != nil && errors.Cause(err) != io.EOF {
return err
}
return nil
}
var ErrNotTimeoutReader = E.New("not a TimeoutReader")
func CopyOnceTimeout(reader Reader, writer Writer, timeout time.Duration) error {
timeoutReader, ok := reader.(TimeoutReader)
if !ok {
return ErrNotTimeoutReader
}
mb, err := timeoutReader.ReadMultiBufferTimeout(timeout)
if err != nil {
return err
}
return writer.WriteMultiBuffer(mb)
}

View File

@@ -1,126 +0,0 @@
package buf
import (
"io"
"net"
"syscall"
"time"
"github.com/sagernet/sing-box/common/xray/stat"
"github.com/sagernet/sing-box/common/xray/stats"
E "github.com/sagernet/sing/common/exceptions"
)
// Reader extends io.Reader with MultiBuffer.
type Reader interface {
// ReadMultiBuffer reads content from underlying reader, and put it into a MultiBuffer.
ReadMultiBuffer() (MultiBuffer, error)
}
// ErrReadTimeout is an error that happens with IO timeout.
var ErrReadTimeout = E.New("IO timeout")
// TimeoutReader is a reader that returns error if Read() operation takes longer than the given timeout.
type TimeoutReader interface {
ReadMultiBufferTimeout(time.Duration) (MultiBuffer, error)
}
// Writer extends io.Writer with MultiBuffer.
type Writer interface {
// WriteMultiBuffer writes a MultiBuffer into underlying writer.
WriteMultiBuffer(MultiBuffer) error
}
// WriteAllBytes ensures all bytes are written into the given writer.
func WriteAllBytes(writer io.Writer, payload []byte, c stats.Counter) error {
wc := 0
defer func() {
if c != nil {
c.Add(int64(wc))
}
}()
for len(payload) > 0 {
n, err := writer.Write(payload)
wc += n
if err != nil {
return err
}
payload = payload[n:]
}
return nil
}
func isPacketReader(reader io.Reader) bool {
_, ok := reader.(net.PacketConn)
return ok
}
// NewReader creates a new Reader.
// The Reader instance doesn't take the ownership of reader.
func NewReader(reader io.Reader) Reader {
if mr, ok := reader.(Reader); ok {
return mr
}
if isPacketReader(reader) {
return &PacketReader{
Reader: reader,
}
}
return &SingleReader{
Reader: reader,
}
}
// NewPacketReader creates a new PacketReader based on the given reader.
func NewPacketReader(reader io.Reader) Reader {
if mr, ok := reader.(Reader); ok {
return mr
}
return &PacketReader{
Reader: reader,
}
}
func isPacketWriter(writer io.Writer) bool {
if _, ok := writer.(net.PacketConn); ok {
return true
}
// If the writer doesn't implement syscall.Conn, it is probably not a TCP connection.
if _, ok := writer.(syscall.Conn); !ok {
return true
}
return false
}
// NewWriter creates a new Writer.
func NewWriter(writer io.Writer) Writer {
if mw, ok := writer.(Writer); ok {
return mw
}
iConn := writer
if statConn, ok := writer.(*stat.CounterConnection); ok {
iConn = statConn.Connection
}
if isPacketWriter(iConn) {
return &SequentialWriter{
Writer: writer,
}
}
var counter stats.Counter
if statConn, ok := writer.(*stat.CounterConnection); ok {
counter = statConn.WriteCounter
}
return &BufferToBytesWriter{
Writer: iConn,
counter: counter,
}
}

View File

@@ -1,310 +0,0 @@
package buf
import (
"io"
"github.com/sagernet/sing-box/common/xray"
"github.com/sagernet/sing-box/common/xray/errors"
"github.com/sagernet/sing-box/common/xray/serial"
)
// ReadAllToBytes reads all content from the reader into a byte array, until EOF.
func ReadAllToBytes(reader io.Reader) ([]byte, error) {
mb, err := ReadFrom(reader)
if err != nil {
return nil, err
}
if mb.Len() == 0 {
return nil, nil
}
b := make([]byte, mb.Len())
mb, _ = SplitBytes(mb, b)
ReleaseMulti(mb)
return b, nil
}
// MultiBuffer is a list of Buffers. The order of Buffer matters.
type MultiBuffer []*Buffer
// MergeMulti merges content from src to dest, and returns the new address of dest and src
func MergeMulti(dest MultiBuffer, src MultiBuffer) (MultiBuffer, MultiBuffer) {
dest = append(dest, src...)
for idx := range src {
src[idx] = nil
}
return dest, src[:0]
}
// MergeBytes merges the given bytes into MultiBuffer and return the new address of the merged MultiBuffer.
func MergeBytes(dest MultiBuffer, src []byte) MultiBuffer {
n := len(dest)
if n > 0 && !(dest)[n-1].IsFull() {
nBytes, _ := (dest)[n-1].Write(src)
src = src[nBytes:]
}
for len(src) > 0 {
b := New()
nBytes, _ := b.Write(src)
src = src[nBytes:]
dest = append(dest, b)
}
return dest
}
// ReleaseMulti releases all content of the MultiBuffer, and returns an empty MultiBuffer.
func ReleaseMulti(mb MultiBuffer) MultiBuffer {
for i := range mb {
mb[i].Release()
mb[i] = nil
}
return mb[:0]
}
// Copy copied the beginning part of the MultiBuffer into the given byte array.
func (mb MultiBuffer) Copy(b []byte) int {
total := 0
for _, bb := range mb {
nBytes := copy(b[total:], bb.Bytes())
total += nBytes
if int32(nBytes) < bb.Len() {
break
}
}
return total
}
// ReadFrom reads all content from reader until EOF.
func ReadFrom(reader io.Reader) (MultiBuffer, error) {
mb := make(MultiBuffer, 0, 16)
for {
b := New()
_, err := b.ReadFullFrom(reader, Size)
if b.IsEmpty() {
b.Release()
} else {
mb = append(mb, b)
}
if err != nil {
if errors.Cause(err) == io.EOF || errors.Cause(err) == io.ErrUnexpectedEOF {
return mb, nil
}
return mb, err
}
}
}
// SplitBytes splits the given amount of bytes from the beginning of the MultiBuffer.
// It returns the new address of MultiBuffer leftover, and number of bytes written into the input byte slice.
func SplitBytes(mb MultiBuffer, b []byte) (MultiBuffer, int) {
totalBytes := 0
endIndex := -1
for i := range mb {
pBuffer := mb[i]
nBytes, _ := pBuffer.Read(b)
totalBytes += nBytes
b = b[nBytes:]
if !pBuffer.IsEmpty() {
endIndex = i
break
}
pBuffer.Release()
mb[i] = nil
}
if endIndex == -1 {
mb = mb[:0]
} else {
mb = mb[endIndex:]
}
return mb, totalBytes
}
// SplitFirstBytes splits the first buffer from MultiBuffer, and then copy its content into the given slice.
func SplitFirstBytes(mb MultiBuffer, p []byte) (MultiBuffer, int) {
mb, b := SplitFirst(mb)
if b == nil {
return mb, 0
}
n := copy(p, b.Bytes())
b.Release()
return mb, n
}
// Compact returns another MultiBuffer by merging all content of the given one together.
func Compact(mb MultiBuffer) MultiBuffer {
if len(mb) == 0 {
return mb
}
mb2 := make(MultiBuffer, 0, len(mb))
last := mb[0]
for i := 1; i < len(mb); i++ {
curr := mb[i]
if last.Len()+curr.Len() > Size {
mb2 = append(mb2, last)
last = curr
} else {
common.Must2(last.ReadFrom(curr))
curr.Release()
}
}
mb2 = append(mb2, last)
return mb2
}
// SplitFirst splits the first Buffer from the beginning of the MultiBuffer.
func SplitFirst(mb MultiBuffer) (MultiBuffer, *Buffer) {
if len(mb) == 0 {
return mb, nil
}
b := mb[0]
mb[0] = nil
mb = mb[1:]
return mb, b
}
// SplitSize splits the beginning of the MultiBuffer into another one, for at most size bytes.
func SplitSize(mb MultiBuffer, size int32) (MultiBuffer, MultiBuffer) {
if len(mb) == 0 {
return mb, nil
}
if mb[0].Len() > size {
b := New()
copy(b.Extend(size), mb[0].BytesTo(size))
mb[0].Advance(size)
return mb, MultiBuffer{b}
}
totalBytes := int32(0)
var r MultiBuffer
endIndex := -1
for i := range mb {
if totalBytes+mb[i].Len() > size {
endIndex = i
break
}
totalBytes += mb[i].Len()
r = append(r, mb[i])
mb[i] = nil
}
if endIndex == -1 {
// To reuse mb array
mb = mb[:0]
} else {
mb = mb[endIndex:]
}
return mb, r
}
// SplitMulti splits the beginning of the MultiBuffer into first one, the index i and after into second one
func SplitMulti(mb MultiBuffer, i int) (MultiBuffer, MultiBuffer) {
mb2 := make(MultiBuffer, 0, len(mb))
if i < len(mb) && i >= 0 {
mb2 = append(mb2, mb[i:]...)
for j := i; j < len(mb); j++ {
mb[j] = nil
}
mb = mb[:i]
}
return mb, mb2
}
// WriteMultiBuffer writes all buffers from the MultiBuffer to the Writer one by one, and return error if any, with leftover MultiBuffer.
func WriteMultiBuffer(writer io.Writer, mb MultiBuffer) (MultiBuffer, error) {
for {
mb2, b := SplitFirst(mb)
mb = mb2
if b == nil {
break
}
_, err := writer.Write(b.Bytes())
b.Release()
if err != nil {
return mb, err
}
}
return nil, nil
}
// Len returns the total number of bytes in the MultiBuffer.
func (mb MultiBuffer) Len() int32 {
if mb == nil {
return 0
}
size := int32(0)
for _, b := range mb {
size += b.Len()
}
return size
}
// IsEmpty returns true if the MultiBuffer has no content.
func (mb MultiBuffer) IsEmpty() bool {
for _, b := range mb {
if !b.IsEmpty() {
return false
}
}
return true
}
// String returns the content of the MultiBuffer in string.
func (mb MultiBuffer) String() string {
v := make([]interface{}, len(mb))
for i, b := range mb {
v[i] = b
}
return serial.Concat(v...)
}
// MultiBufferContainer is a ReadWriteCloser wrapper over MultiBuffer.
type MultiBufferContainer struct {
MultiBuffer
}
// Read implements io.Reader.
func (c *MultiBufferContainer) Read(b []byte) (int, error) {
if c.MultiBuffer.IsEmpty() {
return 0, io.EOF
}
mb, nBytes := SplitBytes(c.MultiBuffer, b)
c.MultiBuffer = mb
return nBytes, nil
}
// ReadMultiBuffer implements Reader.
func (c *MultiBufferContainer) ReadMultiBuffer() (MultiBuffer, error) {
mb := c.MultiBuffer
c.MultiBuffer = nil
return mb, nil
}
// Write implements io.Writer.
func (c *MultiBufferContainer) Write(b []byte) (int, error) {
c.MultiBuffer = MergeBytes(c.MultiBuffer, b)
return len(b), nil
}
// WriteMultiBuffer implements Writer.
func (c *MultiBufferContainer) WriteMultiBuffer(b MultiBuffer) error {
mb, _ := MergeMulti(c.MultiBuffer, b)
c.MultiBuffer = mb
return nil
}
// Close implements io.Closer.
func (c *MultiBufferContainer) Close() error {
c.MultiBuffer = ReleaseMulti(c.MultiBuffer)
return nil
}

View File

@@ -1,38 +0,0 @@
package buf
import (
"github.com/sagernet/sing-box/common/xray/net"
)
type EndpointOverrideReader struct {
Reader
Dest net.Address
OriginalDest net.Address
}
func (r *EndpointOverrideReader) ReadMultiBuffer() (MultiBuffer, error) {
mb, err := r.Reader.ReadMultiBuffer()
if err == nil {
for _, b := range mb {
if b.UDP != nil && b.UDP.Address == r.OriginalDest {
b.UDP.Address = r.Dest
}
}
}
return mb, err
}
type EndpointOverrideWriter struct {
Writer
Dest net.Address
OriginalDest net.Address
}
func (w *EndpointOverrideWriter) WriteMultiBuffer(mb MultiBuffer) error {
for _, b := range mb {
if b.UDP != nil && b.UDP.Address == w.Dest {
b.UDP.Address = w.OriginalDest
}
}
return w.Writer.WriteMultiBuffer(mb)
}

View File

@@ -1,175 +0,0 @@
package buf
import (
"io"
"github.com/sagernet/sing-box/common/xray"
"github.com/sagernet/sing-box/common/xray/errors"
E "github.com/sagernet/sing/common/exceptions"
)
func readOneUDP(r io.Reader) (*Buffer, error) {
b := New()
for i := 0; i < 64; i++ {
_, err := b.ReadFrom(r)
if !b.IsEmpty() {
return b, nil
}
if err != nil {
b.Release()
return nil, err
}
}
b.Release()
return nil, E.New("Reader returns too many empty payloads.")
}
// ReadBuffer reads a Buffer from the given reader.
func ReadBuffer(r io.Reader) (*Buffer, error) {
b := New()
n, err := b.ReadFrom(r)
if n > 0 {
return b, err
}
b.Release()
return nil, err
}
// BufferedReader is a Reader that keeps its internal buffer.
type BufferedReader struct {
// Reader is the underlying reader to be read from
Reader Reader
// Buffer is the internal buffer to be read from first
Buffer MultiBuffer
// Splitter is a function to read bytes from MultiBuffer
Splitter func(MultiBuffer, []byte) (MultiBuffer, int)
}
// BufferedBytes returns the number of bytes that is cached in this reader.
func (r *BufferedReader) BufferedBytes() int32 {
return r.Buffer.Len()
}
// ReadByte implements io.ByteReader.
func (r *BufferedReader) ReadByte() (byte, error) {
var b [1]byte
_, err := r.Read(b[:])
return b[0], err
}
// Read implements io.Reader. It reads from internal buffer first (if available) and then reads from the underlying reader.
func (r *BufferedReader) Read(b []byte) (int, error) {
spliter := r.Splitter
if spliter == nil {
spliter = SplitBytes
}
if !r.Buffer.IsEmpty() {
buffer, nBytes := spliter(r.Buffer, b)
r.Buffer = buffer
if r.Buffer.IsEmpty() {
r.Buffer = nil
}
return nBytes, nil
}
mb, err := r.Reader.ReadMultiBuffer()
if err != nil {
return 0, err
}
mb, nBytes := spliter(mb, b)
if !mb.IsEmpty() {
r.Buffer = mb
}
return nBytes, nil
}
// ReadMultiBuffer implements Reader.
func (r *BufferedReader) ReadMultiBuffer() (MultiBuffer, error) {
if !r.Buffer.IsEmpty() {
mb := r.Buffer
r.Buffer = nil
return mb, nil
}
return r.Reader.ReadMultiBuffer()
}
// ReadAtMost returns a MultiBuffer with at most size.
func (r *BufferedReader) ReadAtMost(size int32) (MultiBuffer, error) {
if r.Buffer.IsEmpty() {
mb, err := r.Reader.ReadMultiBuffer()
if mb.IsEmpty() && err != nil {
return nil, err
}
r.Buffer = mb
}
rb, mb := SplitSize(r.Buffer, size)
r.Buffer = rb
if r.Buffer.IsEmpty() {
r.Buffer = nil
}
return mb, nil
}
func (r *BufferedReader) writeToInternal(writer io.Writer) (int64, error) {
mbWriter := NewWriter(writer)
var sc SizeCounter
if r.Buffer != nil {
sc.Size = int64(r.Buffer.Len())
if err := mbWriter.WriteMultiBuffer(r.Buffer); err != nil {
return 0, err
}
r.Buffer = nil
}
err := Copy(r.Reader, mbWriter, CountSize(&sc))
return sc.Size, err
}
// WriteTo implements io.WriterTo.
func (r *BufferedReader) WriteTo(writer io.Writer) (int64, error) {
nBytes, err := r.writeToInternal(writer)
if errors.Cause(err) == io.EOF {
return nBytes, nil
}
return nBytes, err
}
// Interrupt implements common.Interruptible.
func (r *BufferedReader) Interrupt() {
common.Interrupt(r.Reader)
}
// Close implements io.Closer.
func (r *BufferedReader) Close() error {
return common.Close(r.Reader)
}
// SingleReader is a Reader that read one Buffer every time.
type SingleReader struct {
io.Reader
}
// ReadMultiBuffer implements Reader.
func (r *SingleReader) ReadMultiBuffer() (MultiBuffer, error) {
b, err := ReadBuffer(r.Reader)
return MultiBuffer{b}, err
}
// PacketReader is a Reader that read one Buffer every time.
type PacketReader struct {
io.Reader
}
// ReadMultiBuffer implements Reader.
func (r *PacketReader) ReadMultiBuffer() (MultiBuffer, error) {
b, err := readOneUDP(r.Reader)
if err != nil {
return nil, err
}
return MultiBuffer{b}, nil
}

View File

@@ -1,270 +0,0 @@
package buf
import (
"io"
"net"
"sync"
"github.com/sagernet/sing-box/common/xray"
"github.com/sagernet/sing-box/common/xray/errors"
"github.com/sagernet/sing-box/common/xray/stats"
)
// BufferToBytesWriter is a Writer that writes alloc.Buffer into underlying writer.
type BufferToBytesWriter struct {
io.Writer
counter stats.Counter
cache [][]byte
}
// WriteMultiBuffer implements Writer. This method takes ownership of the given buffer.
func (w *BufferToBytesWriter) WriteMultiBuffer(mb MultiBuffer) error {
defer ReleaseMulti(mb)
size := mb.Len()
if size == 0 {
return nil
}
if len(mb) == 1 {
return WriteAllBytes(w.Writer, mb[0].Bytes(), w.counter)
}
if cap(w.cache) < len(mb) {
w.cache = make([][]byte, 0, len(mb))
}
bs := w.cache
for _, b := range mb {
bs = append(bs, b.Bytes())
}
defer func() {
for idx := range bs {
bs[idx] = nil
}
}()
nb := net.Buffers(bs)
wc := int64(0)
defer func() {
if w.counter != nil {
w.counter.Add(wc)
}
}()
for size > 0 {
n, err := nb.WriteTo(w.Writer)
wc += n
if err != nil {
return err
}
size -= int32(n)
}
return nil
}
// ReadFrom implements io.ReaderFrom.
func (w *BufferToBytesWriter) ReadFrom(reader io.Reader) (int64, error) {
var sc SizeCounter
err := Copy(NewReader(reader), w, CountSize(&sc))
return sc.Size, err
}
// BufferedWriter is a Writer with internal buffer.
type BufferedWriter struct {
sync.Mutex
writer Writer
buffer *Buffer
buffered bool
}
// NewBufferedWriter creates a new BufferedWriter.
func NewBufferedWriter(writer Writer) *BufferedWriter {
return &BufferedWriter{
writer: writer,
buffer: New(),
buffered: true,
}
}
// WriteByte implements io.ByteWriter.
func (w *BufferedWriter) WriteByte(c byte) error {
return common.Error2(w.Write([]byte{c}))
}
// Write implements io.Writer.
func (w *BufferedWriter) Write(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
w.Lock()
defer w.Unlock()
if !w.buffered {
if writer, ok := w.writer.(io.Writer); ok {
return writer.Write(b)
}
}
totalBytes := 0
for len(b) > 0 {
if w.buffer == nil {
w.buffer = New()
}
nBytes, err := w.buffer.Write(b)
totalBytes += nBytes
if err != nil {
return totalBytes, err
}
if !w.buffered || w.buffer.IsFull() {
if err := w.flushInternal(); err != nil {
return totalBytes, err
}
}
b = b[nBytes:]
}
return totalBytes, nil
}
// WriteMultiBuffer implements Writer. It takes ownership of the given MultiBuffer.
func (w *BufferedWriter) WriteMultiBuffer(b MultiBuffer) error {
if b.IsEmpty() {
return nil
}
w.Lock()
defer w.Unlock()
if !w.buffered {
return w.writer.WriteMultiBuffer(b)
}
reader := MultiBufferContainer{
MultiBuffer: b,
}
defer reader.Close()
for !reader.MultiBuffer.IsEmpty() {
if w.buffer == nil {
w.buffer = New()
}
common.Must2(w.buffer.ReadFrom(&reader))
if w.buffer.IsFull() {
if err := w.flushInternal(); err != nil {
return err
}
}
}
return nil
}
// Flush flushes buffered content into underlying writer.
func (w *BufferedWriter) Flush() error {
w.Lock()
defer w.Unlock()
return w.flushInternal()
}
func (w *BufferedWriter) flushInternal() error {
if w.buffer.IsEmpty() {
return nil
}
b := w.buffer
w.buffer = nil
if writer, ok := w.writer.(io.Writer); ok {
err := WriteAllBytes(writer, b.Bytes(), nil)
b.Release()
return err
}
return w.writer.WriteMultiBuffer(MultiBuffer{b})
}
// SetBuffered sets whether the internal buffer is used. If set to false, Flush() will be called to clear the buffer.
func (w *BufferedWriter) SetBuffered(f bool) error {
w.Lock()
defer w.Unlock()
w.buffered = f
if !f {
return w.flushInternal()
}
return nil
}
// ReadFrom implements io.ReaderFrom.
func (w *BufferedWriter) ReadFrom(reader io.Reader) (int64, error) {
if err := w.SetBuffered(false); err != nil {
return 0, err
}
var sc SizeCounter
err := Copy(NewReader(reader), w, CountSize(&sc))
return sc.Size, err
}
// Close implements io.Closable.
func (w *BufferedWriter) Close() error {
if err := w.Flush(); err != nil {
return err
}
return common.Close(w.writer)
}
// SequentialWriter is a Writer that writes MultiBuffer sequentially into the underlying io.Writer.
type SequentialWriter struct {
io.Writer
}
// WriteMultiBuffer implements Writer.
func (w *SequentialWriter) WriteMultiBuffer(mb MultiBuffer) error {
mb, err := WriteMultiBuffer(w.Writer, mb)
ReleaseMulti(mb)
return err
}
type noOpWriter byte
func (noOpWriter) WriteMultiBuffer(b MultiBuffer) error {
ReleaseMulti(b)
return nil
}
func (noOpWriter) Write(b []byte) (int, error) {
return len(b), nil
}
func (noOpWriter) ReadFrom(reader io.Reader) (int64, error) {
b := New()
defer b.Release()
totalBytes := int64(0)
for {
b.Clear()
_, err := b.ReadFrom(reader)
totalBytes += int64(b.Len())
if err != nil {
if errors.Cause(err) == io.EOF {
return totalBytes, nil
}
return totalBytes, err
}
}
}
var (
// Discard is a Writer that swallows all contents written in.
Discard Writer = noOpWriter(0)
// DiscardBytes is an io.Writer that swallows all contents written in.
DiscardBytes io.Writer = noOpWriter(0)
)

View File

@@ -1,72 +0,0 @@
package bytespool
import "sync"
func createAllocFunc(size int32) func() interface{} {
return func() interface{} {
return make([]byte, size)
}
}
// The following parameters controls the size of buffer pools.
// There are numPools pools. Starting from 2k size, the size of each pool is sizeMulti of the previous one.
// Package buf is guaranteed to not use buffers larger than the largest pool.
// Other packets may use larger buffers.
const (
numPools = 4
sizeMulti = 4
)
var (
pool [numPools]sync.Pool
poolSize [numPools]int32
)
func init() {
size := int32(2048)
for i := 0; i < numPools; i++ {
pool[i] = sync.Pool{
New: createAllocFunc(size),
}
poolSize[i] = size
size *= sizeMulti
}
}
// GetPool returns a sync.Pool that generates bytes array with at least the given size.
// It may return nil if no such pool exists.
//
// xray:api:stable
func GetPool(size int32) *sync.Pool {
for idx, ps := range poolSize {
if size <= ps {
return &pool[idx]
}
}
return nil
}
// Alloc returns a byte slice with at least the given size. Minimum size of returned slice is 2048.
//
// xray:api:stable
func Alloc(size int32) []byte {
pool := GetPool(size)
if pool != nil {
return pool.Get().([]byte)
}
return make([]byte, size)
}
// Free puts a byte slice into the internal pool.
//
// xray:api:stable
func Free(b []byte) {
size := int32(cap(b))
b = b[0:cap(b)]
for i := numPools - 1; i >= 0; i-- {
if size >= poolSize[i] {
pool[i].Put(b)
return
}
}
}

View File

@@ -1,19 +0,0 @@
package common
// Must panics if err is not nil.
func Must(err error) {
if err != nil {
panic(err)
}
}
// Must2 panics if the second parameter is not nil, otherwise returns the first parameter.
func Must2(v interface{}, err error) interface{} {
Must(err)
return v
}
// Error2 returns the err from the 2nd parameter.
func Error2(v interface{}, err error) error {
return err
}

View File

@@ -1,14 +0,0 @@
package crypto
import (
"crypto/rand"
"math/big"
)
func RandBetween(from int64, to int64) int64 {
if from == to {
return from
}
bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
return from + bigInt.Int64()
}

View File

@@ -1,25 +0,0 @@
package errors
type hasInnerError interface {
// Unwrap returns the underlying error of this one.
Unwrap() error
}
func Cause(err error) error {
if err == nil {
return nil
}
L:
for {
switch inner := err.(type) {
case hasInnerError:
if inner.Unwrap() == nil {
break L
}
err = inner.Unwrap()
default:
break L
}
}
return err
}

View File

@@ -1,52 +0,0 @@
package common
// Closable is the interface for objects that can release its resources.
//
// xray:api:beta
type Closable interface {
// Close release all resources used by this object, including goroutines.
Close() error
}
// Interruptible is an interface for objects that can be stopped before its completion.
//
// xray:api:beta
type Interruptible interface {
Interrupt()
}
// Close closes the obj if it is a Closable.
//
// xray:api:beta
func Close(obj interface{}) error {
if c, ok := obj.(Closable); ok {
return c.Close()
}
return nil
}
// Interrupt calls Interrupt() if object implements Interruptible interface, or Close() if the object implements Closable interface.
//
// xray:api:beta
func Interrupt(obj interface{}) error {
if c, ok := obj.(Interruptible); ok {
c.Interrupt()
return nil
}
return Close(obj)
}
// Runnable is the interface for objects that can start to work and stop on demand.
type Runnable interface {
// Start starts the runnable object. Upon the method returning nil, the object begins to function properly.
Start() error
Closable
}
// HasType is the interface for objects that knows its type.
type HasType interface {
// Type returns the type of the object.
// Usually it returns (*Type)(nil) of the object.
Type() interface{}
}

View File

@@ -1,67 +0,0 @@
package badoption
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/sagernet/sing-box/common/xray/crypto"
E "github.com/sagernet/sing/common/exceptions"
)
type Range struct {
From int32 `json:"from"`
To int32 `json:"to"`
}
func (c *Range) Build() *Range {
return (*Range)(c)
}
func (c *Range) MarshalJSON() ([]byte, error) {
return json.Marshal(fmt.Sprintf("%d-%d", c.From, c.To))
}
func (c *Range) UnmarshalJSON(content []byte) error {
var rangeValue struct {
From int32 `json:"from"`
To int32 `json:"to"`
}
var stringValue string
err := json.Unmarshal(content, &stringValue)
if err == nil {
parts := strings.Split(stringValue, "-")
if len(parts) != 2 {
from, err := strconv.ParseInt(parts[0], 10, 32)
if err != nil {
return err
}
rangeValue.From, rangeValue.To = int32(from), int32(from)
} else {
from, err := strconv.ParseInt(parts[0], 10, 32)
if err != nil {
return err
}
to, err := strconv.ParseInt(parts[1], 10, 32)
if err != nil {
return err
}
rangeValue.From, rangeValue.To = int32(from), int32(to)
}
} else {
err := json.Unmarshal(content, &rangeValue)
if err != nil {
return err
}
}
if rangeValue.From > rangeValue.To {
return E.New("invalid range")
}
*c = Range{rangeValue.From, rangeValue.To}
return nil
}
func (c Range) Rand() int32 {
return int32(crypto.RandBetween(int64(c.From), int64(c.To)))
}

View File

@@ -1,181 +0,0 @@
package net
import (
"bytes"
"net"
"strings"
)
var (
// LocalHostIP is a constant value for localhost IP in IPv4.
LocalHostIP = IPAddress([]byte{127, 0, 0, 1})
// AnyIP is a constant value for any IP in IPv4.
AnyIP = IPAddress([]byte{0, 0, 0, 0})
// LocalHostDomain is a constant value for localhost domain.
LocalHostDomain = DomainAddress("localhost")
// LocalHostIPv6 is a constant value for localhost IP in IPv6.
LocalHostIPv6 = IPAddress([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
// AnyIPv6 is a constant value for any IP in IPv6.
AnyIPv6 = IPAddress([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
)
// AddressFamily is the type of address.
type AddressFamily byte
const (
// AddressFamilyIPv4 represents address as IPv4
AddressFamilyIPv4 = AddressFamily(0)
// AddressFamilyIPv6 represents address as IPv6
AddressFamilyIPv6 = AddressFamily(1)
// AddressFamilyDomain represents address as Domain
AddressFamilyDomain = AddressFamily(2)
)
// IsIPv4 returns true if current AddressFamily is IPv4.
func (af AddressFamily) IsIPv4() bool {
return af == AddressFamilyIPv4
}
// IsIPv6 returns true if current AddressFamily is IPv6.
func (af AddressFamily) IsIPv6() bool {
return af == AddressFamilyIPv6
}
// IsIP returns true if current AddressFamily is IPv6 or IPv4.
func (af AddressFamily) IsIP() bool {
return af == AddressFamilyIPv4 || af == AddressFamilyIPv6
}
// IsDomain returns true if current AddressFamily is Domain.
func (af AddressFamily) IsDomain() bool {
return af == AddressFamilyDomain
}
// Address represents a network address to be communicated with. It may be an IP address or domain
// address, not both. This interface doesn't resolve IP address for a given domain.
type Address interface {
IP() net.IP // IP of this Address
Domain() string // Domain of this Address
Family() AddressFamily
String() string // String representation of this Address
}
func isAlphaNum(c byte) bool {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
// ParseAddress parses a string into an Address. The return value will be an IPAddress when
// the string is in the form of IPv4 or IPv6 address, or a DomainAddress otherwise.
func ParseAddress(addr string) Address {
// Handle IPv6 address in form as "[2001:4860:0:2001::68]"
lenAddr := len(addr)
if lenAddr > 0 && addr[0] == '[' && addr[lenAddr-1] == ']' {
addr = addr[1 : lenAddr-1]
lenAddr -= 2
}
if lenAddr > 0 && (!isAlphaNum(addr[0]) || !isAlphaNum(addr[len(addr)-1])) {
addr = strings.TrimSpace(addr)
}
ip := net.ParseIP(addr)
if ip != nil {
return IPAddress(ip)
}
return DomainAddress(addr)
}
var bytes0 = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
// IPAddress creates an Address with given IP.
func IPAddress(ip []byte) Address {
switch len(ip) {
case net.IPv4len:
var addr ipv4Address = [4]byte{ip[0], ip[1], ip[2], ip[3]}
return addr
case net.IPv6len:
if bytes.Equal(ip[:10], bytes0) && ip[10] == 0xff && ip[11] == 0xff {
return IPAddress(ip[12:16])
}
var addr ipv6Address = [16]byte{
ip[0], ip[1], ip[2], ip[3],
ip[4], ip[5], ip[6], ip[7],
ip[8], ip[9], ip[10], ip[11],
ip[12], ip[13], ip[14], ip[15],
}
return addr
default:
return nil
}
}
// DomainAddress creates an Address with given domain.
// This is an internal function that forcibly converts a string to domain.
// It's mainly used in test files and mux.
// Unless you have a specific reason, use net.ParseAddress instead,
// as this function does not check whether the input is an IP address.
// Otherwise, you will get strange results like domain: 1.1.1.1
func DomainAddress(domain string) Address {
return domainAddress(domain)
}
type ipv4Address [4]byte
func (a ipv4Address) IP() net.IP {
return net.IP(a[:])
}
func (ipv4Address) Domain() string {
panic("Calling Domain() on an IPv4Address.")
}
func (ipv4Address) Family() AddressFamily {
return AddressFamilyIPv4
}
func (a ipv4Address) String() string {
return a.IP().String()
}
type ipv6Address [16]byte
func (a ipv6Address) IP() net.IP {
return net.IP(a[:])
}
func (ipv6Address) Domain() string {
panic("Calling Domain() on an IPv6Address.")
}
func (ipv6Address) Family() AddressFamily {
return AddressFamilyIPv6
}
func (a ipv6Address) String() string {
return "[" + a.IP().String() + "]"
}
type domainAddress string
func (domainAddress) IP() net.IP {
panic("Calling IP() on a DomainAddress.")
}
func (a domainAddress) Domain() string {
return string(a)
}
func (domainAddress) Family() AddressFamily {
return AddressFamilyDomain
}
func (a domainAddress) String() string {
return a.Domain()
}

View File

@@ -1,146 +0,0 @@
package net
import (
"net"
"strings"
)
// Destination represents a network destination including address and protocol (tcp / udp).
type Destination struct {
Address Address
Port Port
Network Network
}
// DestinationFromAddr generates a Destination from a net address.
func DestinationFromAddr(addr net.Addr) Destination {
switch addr := addr.(type) {
case *net.TCPAddr:
return TCPDestination(IPAddress(addr.IP), Port(addr.Port))
case *net.UDPAddr:
return UDPDestination(IPAddress(addr.IP), Port(addr.Port))
case *net.UnixAddr:
return UnixDestination(DomainAddress(addr.Name))
default:
panic("Net: Unknown address type.")
}
}
// ParseDestination converts a destination from its string presentation.
func ParseDestination(dest string) (Destination, error) {
d := Destination{
Address: AnyIP,
Port: Port(0),
}
if strings.HasPrefix(dest, "tcp:") {
d.Network = Network_TCP
dest = dest[4:]
} else if strings.HasPrefix(dest, "udp:") {
d.Network = Network_UDP
dest = dest[4:]
} else if strings.HasPrefix(dest, "unix:") {
d = UnixDestination(DomainAddress(dest[5:]))
return d, nil
}
hstr, pstr, err := SplitHostPort(dest)
if err != nil {
return d, err
}
if len(hstr) > 0 {
d.Address = ParseAddress(hstr)
}
if len(pstr) > 0 {
port, err := PortFromString(pstr)
if err != nil {
return d, err
}
d.Port = port
}
return d, nil
}
// TCPDestination creates a TCP destination with given address
func TCPDestination(address Address, port Port) Destination {
return Destination{
Network: Network_TCP,
Address: address,
Port: port,
}
}
// UDPDestination creates a UDP destination with given address
func UDPDestination(address Address, port Port) Destination {
return Destination{
Network: Network_UDP,
Address: address,
Port: port,
}
}
// UnixDestination creates a Unix destination with given address
func UnixDestination(address Address) Destination {
return Destination{
Network: Network_UNIX,
Address: address,
}
}
// NetAddr returns the network address in this Destination in string form.
func (d Destination) NetAddr() string {
addr := ""
if d.Network == Network_TCP || d.Network == Network_UDP {
addr = d.Address.String() + ":" + d.Port.String()
} else if d.Network == Network_UNIX {
addr = d.Address.String()
}
return addr
}
// RawNetAddr converts a net.Addr from its Destination presentation.
func (d Destination) RawNetAddr() net.Addr {
var addr net.Addr
switch d.Network {
case Network_TCP:
if d.Address.Family().IsIP() {
addr = &net.TCPAddr{
IP: d.Address.IP(),
Port: int(d.Port),
}
}
case Network_UDP:
if d.Address.Family().IsIP() {
addr = &net.UDPAddr{
IP: d.Address.IP(),
Port: int(d.Port),
}
}
case Network_UNIX:
if d.Address.Family().IsDomain() {
addr = &net.UnixAddr{
Name: d.Address.String(),
Net: d.Network.SystemString(),
}
}
}
return addr
}
// String returns the strings form of this Destination.
func (d Destination) String() string {
prefix := "unknown:"
switch d.Network {
case Network_TCP:
prefix = "tcp:"
case Network_UDP:
prefix = "udp:"
case Network_UNIX:
prefix = "unix:"
}
return prefix + d.NetAddr()
}
// IsValid returns true if this Destination is valid.
func (d Destination) IsValid() bool {
return d.Network != Network_Unknown
}

View File

@@ -1,13 +0,0 @@
package net
import "time"
// defines the maximum time an idle TCP session can survive in the tunnel, so
// it should be consistent across HTTP versions and with other transports.
const ConnIdleTimeout = 300 * time.Second
// consistent with quic-go
const QuicgoH3KeepAlivePeriod = 10 * time.Second
// consistent with chrome
const ChromeH2KeepAlivePeriod = 45 * time.Second

View File

@@ -1,33 +0,0 @@
package net
type Network int32
const (
Network_Unknown Network = 0
Network_TCP Network = 2
Network_UDP Network = 3
Network_UNIX Network = 4
)
func (n Network) SystemString() string {
switch n {
case Network_TCP:
return "tcp"
case Network_UDP:
return "udp"
case Network_UNIX:
return "unix"
default:
return "unknown"
}
}
// HasNetwork returns true if the network list has a certain network.
func HasNetwork(list []Network, network Network) bool {
for _, value := range list {
if value == network {
return true
}
}
return false
}

View File

@@ -1,55 +0,0 @@
package net
import (
"encoding/binary"
"strconv"
E "github.com/sagernet/sing/common/exceptions"
)
// Port represents a network port in TCP and UDP protocol.
type Port uint16
// PortFromBytes converts a byte array to a Port, assuming bytes are in big endian order.
// @unsafe Caller must ensure that the byte array has at least 2 elements.
func PortFromBytes(port []byte) Port {
return Port(binary.BigEndian.Uint16(port))
}
// PortFromInt converts an integer to a Port.
// @error when the integer is not positive or larger then 65535
func PortFromInt(val uint32) (Port, error) {
if val > 65535 {
return Port(0), E.New("invalid port range: ", val)
}
return Port(val), nil
}
// PortFromString converts a string to a Port.
// @error when the string is not an integer or the integral value is a not a valid Port.
func PortFromString(s string) (Port, error) {
val, err := strconv.ParseUint(s, 10, 32)
if err != nil {
return Port(0), E.New("invalid port range: ", s)
}
return PortFromInt(uint32(val))
}
// Value return the corresponding uint16 value of a Port.
func (p Port) Value() uint16 {
return uint16(p)
}
// String returns the string presentation of a Port.
func (p Port) String() string {
return strconv.Itoa(int(p))
}
type MemoryPortRange struct {
From Port
To Port
}
func (r MemoryPortRange) Contains(port Port) bool {
return r.From <= port && port <= r.To
}

View File

@@ -1,84 +0,0 @@
package net
import "net"
// DialTCP is an alias of net.DialTCP.
var (
DialTCP = net.DialTCP
DialUDP = net.DialUDP
DialUnix = net.DialUnix
Dial = net.Dial
)
type ListenConfig = net.ListenConfig
var (
Listen = net.Listen
ListenTCP = net.ListenTCP
ListenUDP = net.ListenUDP
ListenUnix = net.ListenUnix
)
var LookupIP = net.LookupIP
var FileConn = net.FileConn
// ParseIP is an alias of net.ParseIP
var ParseIP = net.ParseIP
var SplitHostPort = net.SplitHostPort
var CIDRMask = net.CIDRMask
type (
Addr = net.Addr
Conn = net.Conn
PacketConn = net.PacketConn
)
type (
TCPAddr = net.TCPAddr
TCPConn = net.TCPConn
)
type (
UDPAddr = net.UDPAddr
UDPConn = net.UDPConn
)
type (
UnixAddr = net.UnixAddr
UnixConn = net.UnixConn
)
// IP is an alias for net.IP.
type (
IP = net.IP
IPMask = net.IPMask
IPNet = net.IPNet
)
const (
IPv4len = net.IPv4len
IPv6len = net.IPv6len
)
type (
Error = net.Error
AddrError = net.AddrError
)
type (
Dialer = net.Dialer
Listener = net.Listener
TCPListener = net.TCPListener
UnixListener = net.UnixListener
)
var (
ResolveTCPAddr = net.ResolveTCPAddr
ResolveUDPAddr = net.ResolveUDPAddr
ResolveUnixAddr = net.ResolveUnixAddr
)
type Resolver = net.Resolver

View File

@@ -1,215 +0,0 @@
package pipe
import (
"errors"
"io"
"runtime"
"sync"
"time"
"github.com/sagernet/sing-box/common/xray"
"github.com/sagernet/sing-box/common/xray/buf"
"github.com/sagernet/sing-box/common/xray/signal"
"github.com/sagernet/sing-box/common/xray/signal/done"
)
type state byte
const (
open state = iota
closed
errord
)
type pipeOption struct {
limit int32 // maximum buffer size in bytes
discardOverflow bool
}
func (o *pipeOption) isFull(curSize int32) bool {
return o.limit >= 0 && curSize > o.limit
}
type pipe struct {
sync.Mutex
data buf.MultiBuffer
readSignal *signal.Notifier
writeSignal *signal.Notifier
done *done.Instance
errChan chan error
option pipeOption
state state
}
var (
errBufferFull = errors.New("buffer full")
errSlowDown = errors.New("slow down")
)
func (p *pipe) Len() int32 {
data := p.data
if data == nil {
return 0
}
return data.Len()
}
func (p *pipe) getState(forRead bool) error {
switch p.state {
case open:
if !forRead && p.option.isFull(p.data.Len()) {
return errBufferFull
}
return nil
case closed:
if !forRead {
return io.ErrClosedPipe
}
if !p.data.IsEmpty() {
return nil
}
return io.EOF
case errord:
return io.ErrClosedPipe
default:
panic("impossible case")
}
}
func (p *pipe) readMultiBufferInternal() (buf.MultiBuffer, error) {
p.Lock()
defer p.Unlock()
if err := p.getState(true); err != nil {
return nil, err
}
data := p.data
p.data = nil
return data, nil
}
func (p *pipe) ReadMultiBuffer() (buf.MultiBuffer, error) {
for {
data, err := p.readMultiBufferInternal()
if data != nil || err != nil {
p.writeSignal.Signal()
return data, err
}
select {
case <-p.readSignal.Wait():
case <-p.done.Wait():
case err = <-p.errChan:
return nil, err
}
}
}
func (p *pipe) ReadMultiBufferTimeout(d time.Duration) (buf.MultiBuffer, error) {
timer := time.NewTimer(d)
defer timer.Stop()
for {
data, err := p.readMultiBufferInternal()
if data != nil || err != nil {
p.writeSignal.Signal()
return data, err
}
select {
case <-p.readSignal.Wait():
case <-p.done.Wait():
case <-timer.C:
return nil, buf.ErrReadTimeout
}
}
}
func (p *pipe) writeMultiBufferInternal(mb buf.MultiBuffer) error {
p.Lock()
defer p.Unlock()
if err := p.getState(false); err != nil {
return err
}
if p.data == nil {
p.data = mb
return nil
}
p.data, _ = buf.MergeMulti(p.data, mb)
return errSlowDown
}
func (p *pipe) WriteMultiBuffer(mb buf.MultiBuffer) error {
if mb.IsEmpty() {
return nil
}
for {
err := p.writeMultiBufferInternal(mb)
if err == nil {
p.readSignal.Signal()
return nil
}
if err == errSlowDown {
p.readSignal.Signal()
// Yield current goroutine. Hopefully the reading counterpart can pick up the payload.
runtime.Gosched()
return nil
}
if err == errBufferFull && p.option.discardOverflow {
buf.ReleaseMulti(mb)
return nil
}
if err != errBufferFull {
buf.ReleaseMulti(mb)
p.readSignal.Signal()
return err
}
select {
case <-p.writeSignal.Wait():
case <-p.done.Wait():
return io.ErrClosedPipe
}
}
}
func (p *pipe) Close() error {
p.Lock()
defer p.Unlock()
if p.state == closed || p.state == errord {
return nil
}
p.state = closed
common.Must(p.done.Close())
return nil
}
// Interrupt implements common.Interruptible.
func (p *pipe) Interrupt() {
p.Lock()
defer p.Unlock()
if p.state == closed || p.state == errord {
return
}
p.state = errord
if !p.data.IsEmpty() {
buf.ReleaseMulti(p.data)
p.data = nil
}
common.Must(p.done.Close())
}

View File

@@ -1,53 +0,0 @@
package pipe
import (
"github.com/sagernet/sing-box/common/xray/signal"
"github.com/sagernet/sing-box/common/xray/signal/done"
)
// Option for creating new Pipes.
type Option func(*pipeOption)
// WithoutSizeLimit returns an Option for Pipe to have no size limit.
func WithoutSizeLimit() Option {
return func(opt *pipeOption) {
opt.limit = -1
}
}
// WithSizeLimit returns an Option for Pipe to have the given size limit.
func WithSizeLimit(limit int32) Option {
return func(opt *pipeOption) {
opt.limit = limit
}
}
// DiscardOverflow returns an Option for Pipe to discard writes if full.
func DiscardOverflow() Option {
return func(opt *pipeOption) {
opt.discardOverflow = true
}
}
// New creates a new Reader and Writer that connects to each other.
func New(opts ...Option) (*Reader, *Writer) {
p := &pipe{
readSignal: signal.NewNotifier(),
writeSignal: signal.NewNotifier(),
done: done.New(),
errChan: make(chan error, 1),
option: pipeOption{
limit: -1,
},
}
for _, opt := range opts {
opt(&(p.option))
}
return &Reader{
pipe: p,
}, &Writer{
pipe: p,
}
}

View File

@@ -1,41 +0,0 @@
package pipe
import (
"time"
"github.com/sagernet/sing-box/common/xray/buf"
)
// Reader is a buf.Reader that reads content from a pipe.
type Reader struct {
pipe *pipe
}
// ReadMultiBuffer implements buf.Reader.
func (r *Reader) ReadMultiBuffer() (buf.MultiBuffer, error) {
return r.pipe.ReadMultiBuffer()
}
// ReadMultiBufferTimeout reads content from a pipe within the given duration, or returns buf.ErrTimeout otherwise.
func (r *Reader) ReadMultiBufferTimeout(d time.Duration) (buf.MultiBuffer, error) {
return r.pipe.ReadMultiBufferTimeout(d)
}
// Interrupt implements common.Interruptible.
func (r *Reader) Interrupt() {
r.pipe.Interrupt()
}
// ReturnAnError makes ReadMultiBuffer return an error, only once.
func (r *Reader) ReturnAnError(err error) {
r.pipe.errChan <- err
}
// Recover catches an error set by ReturnAnError, if exists.
func (r *Reader) Recover() (err error) {
select {
case err = <-r.pipe.errChan:
default:
}
return
}

View File

@@ -1,29 +0,0 @@
package pipe
import (
"github.com/sagernet/sing-box/common/xray/buf"
)
// Writer is a buf.Writer that writes data into a pipe.
type Writer struct {
pipe *pipe
}
// WriteMultiBuffer implements buf.Writer.
func (w *Writer) WriteMultiBuffer(mb buf.MultiBuffer) error {
return w.pipe.WriteMultiBuffer(mb)
}
// Close implements io.Closer. After the pipe is closed, writing to the pipe will return io.ErrClosedPipe, while reading will return io.EOF.
func (w *Writer) Close() error {
return w.pipe.Close()
}
func (w *Writer) Len() int32 {
return w.pipe.Len()
}
// Interrupt implements common.Interruptible.
func (w *Writer) Interrupt() {
w.pipe.Interrupt()
}

View File

@@ -1,29 +0,0 @@
package serial
import (
"encoding/binary"
"io"
)
// ReadUint16 reads first two bytes from the reader, and then converts them to an uint16 value.
func ReadUint16(reader io.Reader) (uint16, error) {
var b [2]byte
if _, err := io.ReadFull(reader, b[:]); err != nil {
return 0, err
}
return binary.BigEndian.Uint16(b[:]), nil
}
// WriteUint16 writes an uint16 value into writer.
func WriteUint16(writer io.Writer, value uint16) (int, error) {
var b [2]byte
binary.BigEndian.PutUint16(b[:], value)
return writer.Write(b[:])
}
// WriteUint64 writes an uint64 value into writer.
func WriteUint64(writer io.Writer, value uint64) (int, error) {
var b [8]byte
binary.BigEndian.PutUint64(b[:], value)
return writer.Write(b[:])
}

View File

@@ -1,35 +0,0 @@
package serial
import (
"fmt"
"strings"
)
// ToString serializes an arbitrary value into string.
func ToString(v interface{}) string {
if v == nil {
return ""
}
switch value := v.(type) {
case string:
return value
case *string:
return *value
case fmt.Stringer:
return value.String()
case error:
return value.Error()
default:
return fmt.Sprintf("%+v", value)
}
}
// Concat concatenates all input into a single string.
func Concat(v ...interface{}) string {
builder := strings.Builder{}
for _, value := range v {
builder.WriteString(ToString(value))
}
return builder.String()
}

View File

@@ -1,49 +0,0 @@
package done
import (
"sync"
)
// Instance is a utility for notifications of something being done.
type Instance struct {
access sync.Mutex
c chan struct{}
closed bool
}
// New returns a new Done.
func New() *Instance {
return &Instance{
c: make(chan struct{}),
}
}
// Done returns true if Close() is called.
func (d *Instance) Done() bool {
select {
case <-d.Wait():
return true
default:
return false
}
}
// Wait returns a channel for waiting for done.
func (d *Instance) Wait() <-chan struct{} {
return d.c
}
// Close marks this Done 'done'. This method may be called multiple times. All calls after first call will have no effect on its status.
func (d *Instance) Close() error {
d.access.Lock()
defer d.access.Unlock()
if d.closed {
return nil
}
d.closed = true
close(d.c)
return nil
}

View File

@@ -1,26 +0,0 @@
package signal
// Notifier is a utility for notifying changes. The change producer may notify changes multiple time, and the consumer may get notified asynchronously.
type Notifier struct {
c chan struct{}
}
// NewNotifier creates a new Notifier.
func NewNotifier() *Notifier {
return &Notifier{
c: make(chan struct{}, 1),
}
}
// Signal signals a change, usually by producer. This method never blocks.
func (n *Notifier) Signal() {
select {
case n.c <- struct{}{}:
default:
}
}
// Wait returns a channel for waiting for changes. The returned channel never gets closed.
func (n *Notifier) Wait() <-chan struct{} {
return n.c
}

View File

@@ -1,105 +0,0 @@
package pubsub
import (
"errors"
"sync"
"time"
"github.com/sagernet/sing-box/common/xray"
"github.com/sagernet/sing-box/common/xray/signal/done"
"github.com/sagernet/sing-box/common/xray/task"
)
type Subscriber struct {
buffer chan interface{}
done *done.Instance
}
func (s *Subscriber) push(msg interface{}) {
select {
case s.buffer <- msg:
default:
}
}
func (s *Subscriber) Wait() <-chan interface{} {
return s.buffer
}
func (s *Subscriber) Close() error {
return s.done.Close()
}
func (s *Subscriber) IsClosed() bool {
return s.done.Done()
}
type Service struct {
sync.RWMutex
subs map[string][]*Subscriber
ctask *task.Periodic
}
func NewService() *Service {
s := &Service{
subs: make(map[string][]*Subscriber),
}
s.ctask = &task.Periodic{
Execute: s.Cleanup,
Interval: time.Second * 30,
}
return s
}
// Cleanup cleans up internal caches of subscribers.
// Visible for testing only.
func (s *Service) Cleanup() error {
s.Lock()
defer s.Unlock()
if len(s.subs) == 0 {
return errors.New("nothing to do")
}
for name, subs := range s.subs {
newSub := make([]*Subscriber, 0, len(s.subs))
for _, sub := range subs {
if !sub.IsClosed() {
newSub = append(newSub, sub)
}
}
if len(newSub) == 0 {
delete(s.subs, name)
} else {
s.subs[name] = newSub
}
}
if len(s.subs) == 0 {
s.subs = make(map[string][]*Subscriber)
}
return nil
}
func (s *Service) Subscribe(name string) *Subscriber {
sub := &Subscriber{
buffer: make(chan interface{}, 16),
done: done.New(),
}
s.Lock()
s.subs[name] = append(s.subs[name], sub)
s.Unlock()
common.Must(s.ctask.Start())
return sub
}
func (s *Service) Publish(name string, message interface{}) {
s.RLock()
defer s.RUnlock()
for _, sub := range s.subs[name] {
if !sub.IsClosed() {
sub.push(message)
}
}
}

View File

@@ -1,27 +0,0 @@
package semaphore
// Instance is an implementation of semaphore.
type Instance struct {
token chan struct{}
}
// New create a new Semaphore with n permits.
func New(n int) *Instance {
s := &Instance{
token: make(chan struct{}, n),
}
for i := 0; i < n; i++ {
s.token <- struct{}{}
}
return s
}
// Wait returns a channel for acquiring a permit.
func (s *Instance) Wait() <-chan struct{} {
return s.token
}
// Signal releases a permit into the semaphore.
func (s *Instance) Signal() {
s.token <- struct{}{}
}

View File

@@ -1,82 +0,0 @@
package signal
import (
"context"
"sync"
"time"
"github.com/sagernet/sing-box/common/xray"
"github.com/sagernet/sing-box/common/xray/task"
)
type ActivityUpdater interface {
Update()
}
type ActivityTimer struct {
sync.RWMutex
updated chan struct{}
checkTask *task.Periodic
onTimeout func()
}
func (t *ActivityTimer) Update() {
select {
case t.updated <- struct{}{}:
default:
}
}
func (t *ActivityTimer) check() error {
select {
case <-t.updated:
default:
t.finish()
}
return nil
}
func (t *ActivityTimer) finish() {
t.Lock()
defer t.Unlock()
if t.onTimeout != nil {
t.onTimeout()
t.onTimeout = nil
}
if t.checkTask != nil {
t.checkTask.Close()
t.checkTask = nil
}
}
func (t *ActivityTimer) SetTimeout(timeout time.Duration) {
if timeout == 0 {
t.finish()
return
}
checkTask := &task.Periodic{
Interval: timeout,
Execute: t.check,
}
t.Lock()
if t.checkTask != nil {
t.checkTask.Close()
}
t.checkTask = checkTask
t.Unlock()
t.Update()
common.Must(checkTask.Start())
}
func CancelAfterInactivity(ctx context.Context, cancel context.CancelFunc, timeout time.Duration) *ActivityTimer {
timer := &ActivityTimer{
updated: make(chan struct{}, 1),
onTimeout: cancel,
}
timer.SetTimeout(timeout)
return timer
}

View File

@@ -1,34 +0,0 @@
package stat
import (
"net"
"github.com/sagernet/sing-box/common/xray/stats"
)
type Connection interface {
net.Conn
}
type CounterConnection struct {
Connection
ReadCounter stats.Counter
WriteCounter stats.Counter
}
func (c *CounterConnection) Read(b []byte) (int, error) {
nBytes, err := c.Connection.Read(b)
if c.ReadCounter != nil {
c.ReadCounter.Add(int64(nBytes))
}
return nBytes, err
}
func (c *CounterConnection) Write(b []byte) (int, error) {
nBytes, err := c.Connection.Write(b)
if c.WriteCounter != nil {
c.WriteCounter.Add(int64(nBytes))
}
return nBytes, err
}

View File

@@ -1,13 +0,0 @@
package stats
// Counter is the interface for stats counters.
//
// xray:api:stable
type Counter interface {
// Value is the current value of the counter.
Value() int64
// Set sets a new value to the counter, and returns the previous one.
Set(int64) int64
// Add adds a value to the current counter value, and returns the previous value.
Add(int64) int64
}

View File

@@ -1,10 +0,0 @@
package task
import "github.com/sagernet/sing-box/common/xray"
// Close returns a func() that closes v.
func Close(v interface{}) func() error {
return func() error {
return common.Close(v)
}
}

View File

@@ -1,85 +0,0 @@
package task
import (
"sync"
"time"
)
// Periodic is a task that runs periodically.
type Periodic struct {
// Interval of the task being run
Interval time.Duration
// Execute is the task function
Execute func() error
access sync.Mutex
timer *time.Timer
running bool
}
func (t *Periodic) hasClosed() bool {
t.access.Lock()
defer t.access.Unlock()
return !t.running
}
func (t *Periodic) checkedExecute() error {
if t.hasClosed() {
return nil
}
if err := t.Execute(); err != nil {
t.access.Lock()
t.running = false
t.access.Unlock()
return err
}
t.access.Lock()
defer t.access.Unlock()
if !t.running {
return nil
}
t.timer = time.AfterFunc(t.Interval, func() {
t.checkedExecute()
})
return nil
}
// Start implements common.Runnable.
func (t *Periodic) Start() error {
t.access.Lock()
if t.running {
t.access.Unlock()
return nil
}
t.running = true
t.access.Unlock()
if err := t.checkedExecute(); err != nil {
t.access.Lock()
t.running = false
t.access.Unlock()
return err
}
return nil
}
// Close implements common.Closable.
func (t *Periodic) Close() error {
t.access.Lock()
defer t.access.Unlock()
t.running = false
if t.timer != nil {
t.timer.Stop()
t.timer = nil
}
return nil
}

View File

@@ -1,64 +0,0 @@
package task
import (
"context"
"github.com/sagernet/sing-box/common/xray/signal/semaphore"
)
// OnSuccess executes g() after f() returns nil.
func OnSuccess(f func() error, g func() error) func() error {
return func() error {
if err := f(); err != nil {
return err
}
return g()
}
}
// Run executes a list of tasks in parallel, returns the first error encountered or nil if all tasks pass.
func Run(ctx context.Context, tasks ...func() error) error {
n := len(tasks)
s := semaphore.New(n)
done := make(chan error, 1)
for _, task := range tasks {
<-s.Wait()
go func(f func() error) {
err := f()
if err == nil {
s.Signal()
return
}
select {
case done <- err:
default:
}
}(task)
}
/*
if altctx := ctx.Value("altctx"); altctx != nil {
ctx = altctx.(context.Context)
}
*/
for i := 0; i < n; i++ {
select {
case err := <-done:
return err
case <-ctx.Done():
return ctx.Err()
case <-s.Wait():
}
}
/*
if cancel := ctx.Value("cancel"); cancel != nil {
cancel.(context.CancelFunc)()
}
*/
return nil
}

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