mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-06-26 04:13:13 +03:00
Compare commits
88 Commits
v1.13.14-e
...
v1.12.22-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
290dbed7b8 | ||
|
|
d7a8207f44 | ||
|
|
57c5ca13eb | ||
|
|
7fc33134fb | ||
|
|
881ab6d436 | ||
|
|
0443b93328 | ||
|
|
75557830a8 | ||
|
|
9d5273ba1e | ||
|
|
5f2a65f01b | ||
|
|
06a519db27 | ||
|
|
65e73fe817 | ||
|
|
c0aa3480c5 | ||
|
|
69f6c75dd7 | ||
|
|
b62000e924 | ||
|
|
a03af44c61 | ||
|
|
aa103fdfc6 | ||
|
|
c82e613c52 | ||
|
|
3d16078651 | ||
|
|
18b1101fbe | ||
|
|
4ebe870306 | ||
|
|
50c5e9df0d | ||
|
|
c8a993834e | ||
|
|
260bbbfb45 | ||
|
|
82337299b9 | ||
|
|
c229c79dcc | ||
|
|
f63091d14d | ||
|
|
1c4a01ee90 | ||
|
|
4d7f99310c | ||
|
|
6fc511f56e | ||
|
|
d18d2b352a | ||
|
|
534128bba9 | ||
|
|
736a7368c6 | ||
|
|
e7a9c90213 | ||
|
|
0f3774e501 | ||
|
|
2f8e656522 | ||
|
|
3ba30e3f00 | ||
|
|
f2639a5829 | ||
|
|
69bebbda82 | ||
|
|
00b2c042ee | ||
|
|
d9eb8f3ab6 | ||
|
|
58025a01f8 | ||
|
|
99cad72ea8 | ||
|
|
6e96d620fe | ||
|
|
596291567f | ||
|
|
a2a5f46cb6 | ||
|
|
f6da8e52b4 | ||
|
|
b27d707668 | ||
|
|
287fe834db | ||
|
|
d7f0cea4ff | ||
|
|
d8b470d1ba | ||
|
|
984fc295b3 | ||
|
|
6e4b7ed744 | ||
|
|
855d400654 | ||
|
|
4c5e2c6645 | ||
|
|
725eccdea8 | ||
|
|
2ff042abd2 | ||
|
|
ffb282e47e | ||
|
|
91f9134379 | ||
|
|
5a4de7b242 | ||
|
|
bc91312a73 | ||
|
|
1d603e24fd | ||
|
|
9dc526ea1f | ||
|
|
93eb435e26 | ||
|
|
e22416f0d9 | ||
|
|
89497dbfd5 | ||
|
|
8388abbb77 | ||
|
|
180b7c4134 | ||
|
|
deda2cca5e | ||
|
|
691ecd45a9 | ||
|
|
209b89a4a3 | ||
|
|
765111a552 | ||
|
|
824eac453b | ||
|
|
85a1a8a53b | ||
|
|
ae9e7aa5f4 | ||
|
|
52b71c6f00 | ||
|
|
5d04673783 | ||
|
|
307c429715 | ||
|
|
6801b58d96 | ||
|
|
6272596ebc | ||
|
|
7c4c2d5ca8 | ||
|
|
6768c77fa0 | ||
|
|
0dff811977 | ||
|
|
e1a58d12fe | ||
|
|
4e74a4108d | ||
|
|
0238c54261 | ||
|
|
5c911c97d8 | ||
|
|
2cfc8092ad | ||
|
|
26bd698462 |
2
.github/setup_go_for_windows7.sh
vendored
2
.github/setup_go_for_windows7.sh
vendored
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
VERSION="1.25.6"
|
||||
VERSION="1.25.7"
|
||||
|
||||
mkdir -p $HOME/go
|
||||
cd $HOME/go
|
||||
|
||||
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.6
|
||||
go-version: ^1.25.7
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.6
|
||||
go-version: ^1.25.7
|
||||
- name: Setup Go 1.24
|
||||
if: matrix.legacy_go124
|
||||
uses: actions/setup-go@v5
|
||||
@@ -300,7 +300,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.6
|
||||
go-version: ^1.25.7
|
||||
- name: Setup Android NDK
|
||||
id: setup-ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
@@ -350,7 +350,7 @@ jobs:
|
||||
mkdir clients/android/app/libs
|
||||
cp libbox.aar clients/android/app/libs
|
||||
cd clients/android
|
||||
./gradlew :app:assemblePlayRelease :app:assembleOtherRelease
|
||||
./gradlew :app:assemblePlayRelease
|
||||
env:
|
||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
@@ -380,7 +380,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.6
|
||||
go-version: ^1.25.7
|
||||
- name: Setup Android NDK
|
||||
id: setup-ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
@@ -479,7 +479,7 @@ jobs:
|
||||
if: matrix.if
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.6
|
||||
go-version: ^1.25.7
|
||||
- name: Set tag
|
||||
if: matrix.if
|
||||
run: |-
|
||||
|
||||
4
.github/workflows/linux.yml
vendored
4
.github/workflows/linux.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.6
|
||||
go-version: ^1.25.7
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.6
|
||||
go-version: ^1.25.7
|
||||
- name: Setup Android NDK
|
||||
if: matrix.os == 'android'
|
||||
uses: nttld/setup-ndk@v1
|
||||
|
||||
150
.goreleaser.yaml
150
.goreleaser.yaml
@@ -31,6 +31,54 @@ builds:
|
||||
- linux_arm_7
|
||||
- linux_s390x
|
||||
- linux_riscv64
|
||||
- linux_mips
|
||||
- linux_mips_softfloat
|
||||
- linux_mipsle
|
||||
- linux_mipsle_softfloat
|
||||
- linux_mips64
|
||||
- linux_mips64le
|
||||
- windows_amd64_v1
|
||||
- windows_386
|
||||
- windows_arm64
|
||||
- darwin_amd64_v1
|
||||
- darwin_arm64
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
- id: manager
|
||||
main: ./cmd/sing-box
|
||||
flags:
|
||||
- -v
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
||||
- -s
|
||||
- -buildid=
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOTOOLCHAIN=local
|
||||
targets:
|
||||
- linux_386
|
||||
- linux_amd64_v1
|
||||
- linux_arm64
|
||||
- linux_arm_6
|
||||
- linux_arm_7
|
||||
- linux_s390x
|
||||
- linux_riscv64
|
||||
- linux_mips
|
||||
- linux_mips_softfloat
|
||||
- linux_mipsle
|
||||
- linux_mipsle_softfloat
|
||||
- linux_mips64
|
||||
- linux_mips64le
|
||||
- windows_amd64_v1
|
||||
- windows_386
|
||||
@@ -51,8 +99,6 @@ builds:
|
||||
- with_tailscale
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOROOT={{ .Env.GOPATH }}/go_legacy
|
||||
tool: "{{ .Env.GOPATH }}/go_legacy/bin/go"
|
||||
targets:
|
||||
- windows_amd64_v1
|
||||
- windows_386
|
||||
@@ -104,91 +150,25 @@ archives:
|
||||
wrap_in_directory: true
|
||||
files:
|
||||
- LICENSE
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}_{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}-{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||
- id: archive_with_manager
|
||||
builds:
|
||||
- manager
|
||||
formats:
|
||||
- tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats:
|
||||
- zip
|
||||
wrap_in_directory: true
|
||||
files:
|
||||
- LICENSE
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-with-manager-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}-{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||
- id: archive-legacy
|
||||
<<: *template
|
||||
builds:
|
||||
- legacy
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-legacy'
|
||||
nfpms:
|
||||
- id: package
|
||||
package_name: sing-box
|
||||
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}_{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||
builds:
|
||||
- main
|
||||
homepage: https://sing-box.sagernet.org/
|
||||
maintainer: nekohasekai <contact-git@sekai.icu>
|
||||
description: The universal proxy platform.
|
||||
license: GPLv3 or later
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
- archlinux
|
||||
# - apk
|
||||
# - ipk
|
||||
priority: extra
|
||||
contents:
|
||||
- src: release/config/config.json
|
||||
dst: /etc/sing-box/config.json
|
||||
type: "config|noreplace"
|
||||
|
||||
- src: release/config/sing-box.service
|
||||
dst: /usr/lib/systemd/system/sing-box.service
|
||||
- src: release/config/sing-box@.service
|
||||
dst: /usr/lib/systemd/system/sing-box@.service
|
||||
- src: release/config/sing-box.sysusers
|
||||
dst: /usr/lib/sysusers.d/sing-box.conf
|
||||
- src: release/config/sing-box.rules
|
||||
dst: /usr/share/polkit-1/rules.d/sing-box.rules
|
||||
- src: release/config/sing-box-split-dns.xml
|
||||
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
|
||||
|
||||
- src: release/completions/sing-box.bash
|
||||
dst: /usr/share/bash-completion/completions/sing-box.bash
|
||||
- src: release/completions/sing-box.fish
|
||||
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
|
||||
- src: release/completions/sing-box.zsh
|
||||
dst: /usr/share/zsh/site-functions/_sing-box
|
||||
|
||||
- src: LICENSE
|
||||
dst: /usr/share/licenses/sing-box/LICENSE
|
||||
deb:
|
||||
signature:
|
||||
key_file: "{{ .Env.NFPM_KEY_PATH }}"
|
||||
fields:
|
||||
Bugs: https://github.com/SagerNet/sing-box/issues
|
||||
rpm:
|
||||
signature:
|
||||
key_file: "{{ .Env.NFPM_KEY_PATH }}"
|
||||
overrides:
|
||||
apk:
|
||||
contents:
|
||||
- src: release/config/config.json
|
||||
dst: /etc/sing-box/config.json
|
||||
type: config
|
||||
|
||||
- src: release/config/sing-box.initd
|
||||
dst: /etc/init.d/sing-box
|
||||
|
||||
- src: release/completions/sing-box.bash
|
||||
dst: /usr/share/bash-completion/completions/sing-box.bash
|
||||
- src: release/completions/sing-box.fish
|
||||
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
|
||||
- src: release/completions/sing-box.zsh
|
||||
dst: /usr/share/zsh/site-functions/_sing-box
|
||||
|
||||
- src: LICENSE
|
||||
dst: /usr/share/licenses/sing-box/LICENSE
|
||||
ipk:
|
||||
contents:
|
||||
- src: release/config/config.json
|
||||
dst: /etc/sing-box/config.json
|
||||
type: config
|
||||
|
||||
- src: release/config/openwrt.init
|
||||
dst: /etc/init.d/sing-box
|
||||
- src: release/config/openwrt.conf
|
||||
dst: /etc/config/sing-box
|
||||
source:
|
||||
enabled: false
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}.source'
|
||||
@@ -200,8 +180,8 @@ signs:
|
||||
- artifacts: checksum
|
||||
release:
|
||||
github:
|
||||
owner: SagerNet
|
||||
name: sing-box
|
||||
owner: shtorm-7
|
||||
name: sing-box-extended
|
||||
draft: true
|
||||
prerelease: auto
|
||||
mode: replace
|
||||
@@ -209,5 +189,3 @@ release:
|
||||
- archive
|
||||
- package
|
||||
skip_upload: true
|
||||
partial:
|
||||
by: target
|
||||
24
DONATE.md
Normal file
24
DONATE.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Support the project
|
||||
|
||||
If you want to support the project, you can donate to the following addresses.
|
||||
|
||||
### TRX (Tron)
|
||||
```
|
||||
TSWU6VUZ4FcUghYDmbbhK15gRVvhvBgW3F
|
||||
```
|
||||
### TON
|
||||
```
|
||||
UQAyD2UuT5kCP6lZQlhFL0hyNibDXNE4nIo_RSLVSYAtD7N1
|
||||
```
|
||||
### Solana
|
||||
```
|
||||
CJu8ickwRCwNE71uVFjYf1UveyCkRp9Xo44rhPcQpeFL
|
||||
```
|
||||
### Bitcoin
|
||||
```
|
||||
bc1qqx97p8k4dchqkyd47s4vf74hrqdfnmhqvcja7x
|
||||
```
|
||||
### Ethereum
|
||||
```
|
||||
0xAcc5919C22F2B3fAa0ec7E8BaD142da5B375FBF6
|
||||
```
|
||||
@@ -1,5 +1,5 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
|
||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||
LABEL maintainer="shtorm-7"
|
||||
COPY . /go/src/github.com/sagernet/sing-box
|
||||
WORKDIR /go/src/github.com/sagernet/sing-box
|
||||
ARG TARGETOS TARGETARCH
|
||||
@@ -18,7 +18,7 @@ RUN set -ex \
|
||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
||||
./cmd/sing-box
|
||||
FROM --platform=$TARGETPLATFORM alpine AS dist
|
||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||
LABEL maintainer="shtorm-7"
|
||||
RUN set -ex \
|
||||
&& apk add --no-cache --upgrade bash tzdata ca-certificates nftables
|
||||
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
|
||||
|
||||
12
Makefile
12
Makefile
@@ -1,6 +1,6 @@
|
||||
NAME = sing-box
|
||||
COMMIT = $(shell git rev-parse --short HEAD)
|
||||
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale
|
||||
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_manager,with_admin_panel
|
||||
|
||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
||||
@@ -64,14 +64,10 @@ update_certificates:
|
||||
go run ./cmd/internal/update_certificates
|
||||
|
||||
release:
|
||||
go run ./cmd/internal/build goreleaser release --clean --skip publish
|
||||
go run ./cmd/internal/build goreleaser release --skip=validate --clean -p 3 --skip publish
|
||||
mkdir dist/release
|
||||
mv dist/*.tar.gz \
|
||||
dist/*.zip \
|
||||
dist/*.deb \
|
||||
dist/*.rpm \
|
||||
dist/*_amd64.pkg.tar.zst \
|
||||
dist/*_arm64.pkg.tar.zst \
|
||||
dist/release
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
||||
rm -r dist/release
|
||||
@@ -86,7 +82,7 @@ update_android_version:
|
||||
go run ./cmd/internal/update_android_version
|
||||
|
||||
build_android:
|
||||
cd ../sing-box-for-android && ./gradlew :app:clean :app:assemblePlayRelease :app:assembleOtherRelease && ./gradlew --stop
|
||||
cd ../sing-box-for-android && ./gradlew :app:clean :app:assemblePlayRelease && ./gradlew --stop
|
||||
|
||||
upload_android:
|
||||
mkdir -p dist/release_android
|
||||
@@ -95,7 +91,7 @@ upload_android:
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
|
||||
rm -rf dist/release_android
|
||||
|
||||
release_android: lib_android update_android_version build_android upload_android
|
||||
release_android: lib_android update_android_version build_android
|
||||
|
||||
publish_android:
|
||||
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop
|
||||
|
||||
72
README.md
72
README.md
@@ -1,22 +1,70 @@
|
||||
> Sponsored by [Warp](https://go.warp.dev/sing-box), built for coding with multiple AI agents
|
||||
# sing-box-extended
|
||||
|
||||
<a href="https://go.warp.dev/sing-box">
|
||||
<img alt="Warp sponsorship" width="400" src="https://github.com/warpdotdev/brand-assets/raw/refs/heads/main/Github/Sponsor/Warp-Github-LG-02.png">
|
||||
</a>
|
||||
Sing-box with extended features.
|
||||
|
||||
---
|
||||
## 🔥 Features
|
||||
|
||||
# sing-box
|
||||
### 🌐 Outbounds
|
||||
- **WARP** — Cloudflare WARP integration through WireGuard
|
||||
- **Tunnel** — Protocol for creating tunnels across nodes
|
||||
- **Bond** — Link aggregation for increased throughput
|
||||
- **Mieru** — Secure, hard to classify, hard to probe network protocol
|
||||
- **Failover** — Automatic outbound switching for high availability
|
||||
|
||||
The universal proxy platform.
|
||||
### 🚦 Limiters
|
||||
- **Bandwidth Limiter** — Upload / download rate limiting
|
||||
- **Connection Limiter** — Concurrent connection control
|
||||
|
||||
[](https://repology.org/project/sing-box/versions)
|
||||
### 🛡 Encryption & Obfuscation
|
||||
- **Amnezia 1.5** — WireGuard traffic obfuscation
|
||||
- **VLESS encryption** — XRAY encryption for VLESS protocol
|
||||
|
||||
## Documentation
|
||||
### 🔄 Transports
|
||||
- **mKCP** — Reliable UDP-based transport
|
||||
- **XHTTP** — Modern XRAY transport
|
||||
|
||||
https://sing-box.sagernet.org
|
||||
### 🛠 Services
|
||||
- **Admin Panel** — Web-based management interface
|
||||
- **Manager** — Management service for configuring squads, nodes, users, limiters
|
||||
- **Node Manager** — Service for connecting nodes to remote manager
|
||||
|
||||
## License
|
||||
### ⚙ Miscellaneous
|
||||
- **SDNS (DNSCrypt)** — Encrypted DNS queries for enhanced privacy
|
||||
- **Extended WireGuard options** — Advanced configuration capabilities
|
||||
- **Unified Delay** — Unified latency measurement
|
||||
|
||||
## 📚 Examples
|
||||
|
||||
Configuration examples are available here:
|
||||
|
||||
https://github.com/shtorm-7/sing-box-extended/tree/extended/examples
|
||||
|
||||
## Support the Project
|
||||
|
||||
If you want to support the project, you can donate to the following addresses.
|
||||
|
||||
### TRX (Tron)
|
||||
```
|
||||
TSWU6VUZ4FcUghYDmbbhK15gRVvhvBgW3F
|
||||
```
|
||||
### TON
|
||||
```
|
||||
UQAyD2UuT5kCP6lZQlhFL0hyNibDXNE4nIo_RSLVSYAtD7N1
|
||||
```
|
||||
### Solana
|
||||
```
|
||||
CJu8ickwRCwNE71uVFjYf1UveyCkRp9Xo44rhPcQpeFL
|
||||
```
|
||||
### Bitcoin
|
||||
```
|
||||
bc1qqx97p8k4dchqkyd47s4vf74hrqdfnmhqvcja7x
|
||||
```
|
||||
### Ethereum
|
||||
```
|
||||
0xAcc5919C22F2B3fAa0ec7E8BaD142da5B375FBF6
|
||||
```
|
||||
|
||||
## 📄 License
|
||||
|
||||
```
|
||||
Copyright (C) 2022 by nekohasekai <contact-sagernet@sekai.icu>
|
||||
@@ -36,4 +84,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
In addition, no derivative work may use the name or imply association
|
||||
with this application without prior consent.
|
||||
```
|
||||
```
|
||||
|
||||
@@ -35,7 +35,6 @@ func NewManager(logger log.ContextLogger, registry adapter.EndpointRegistry) *Ma
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
@@ -43,9 +42,12 @@ func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.stage = stage
|
||||
if stage == adapter.StartStateStart {
|
||||
// started with outbound manager
|
||||
m.access.Unlock()
|
||||
return nil
|
||||
}
|
||||
for _, endpoint := range m.endpoints {
|
||||
endpoints := m.endpoints
|
||||
m.access.Unlock()
|
||||
for _, endpoint := range endpoints {
|
||||
err := adapter.LegacyStart(endpoint, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
|
||||
|
||||
@@ -44,6 +44,8 @@ type CacheFile interface {
|
||||
StoreRDRC() bool
|
||||
RDRCStore
|
||||
|
||||
StoreWARPConfig() bool
|
||||
|
||||
LoadMode() string
|
||||
StoreMode(mode string) error
|
||||
LoadSelected(group string) string
|
||||
@@ -52,6 +54,8 @@ type CacheFile interface {
|
||||
StoreGroupExpand(group string, expand bool) error
|
||||
LoadRuleSet(tag string) *SavedBinary
|
||||
SaveRuleSet(tag string, set *SavedBinary) error
|
||||
LoadWARPConfig(tag string) *SavedBinary
|
||||
SaveWARPConfig(tag string, set *SavedBinary) error
|
||||
}
|
||||
|
||||
type SavedBinary struct {
|
||||
|
||||
@@ -31,6 +31,7 @@ type UDPInjectableInbound interface {
|
||||
type InboundRegistry interface {
|
||||
option.InboundOptionsRegistry
|
||||
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error)
|
||||
UnsafeCreate(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error)
|
||||
}
|
||||
|
||||
type InboundManager interface {
|
||||
@@ -42,14 +43,16 @@ type InboundManager interface {
|
||||
}
|
||||
|
||||
type InboundContext struct {
|
||||
Inbound string
|
||||
InboundType string
|
||||
IPVersion uint8
|
||||
Network string
|
||||
Source M.Socksaddr
|
||||
Destination M.Socksaddr
|
||||
User string
|
||||
Outbound string
|
||||
Inbound string
|
||||
InboundType string
|
||||
IPVersion uint8
|
||||
Network string
|
||||
Source M.Socksaddr
|
||||
Destination M.Socksaddr
|
||||
TunnelSource string
|
||||
TunnelDestination string
|
||||
User string
|
||||
Outbound string
|
||||
|
||||
// sniffer
|
||||
|
||||
|
||||
@@ -57,6 +57,10 @@ func (m *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||
func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return m.UnsafeCreate(ctx, router, logger, tag, outboundType, options)
|
||||
}
|
||||
|
||||
func (m *Registry) UnsafeCreate(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
|
||||
constructor, loaded := m.constructor[outboundType]
|
||||
if !loaded {
|
||||
return nil, E.New("outbound type not found: " + outboundType)
|
||||
|
||||
@@ -21,6 +21,7 @@ type Outbound interface {
|
||||
type OutboundRegistry interface {
|
||||
option.OutboundOptionsRegistry
|
||||
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
|
||||
UnsafeCreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
|
||||
}
|
||||
|
||||
type OutboundManager interface {
|
||||
|
||||
@@ -57,6 +57,10 @@ func (r *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||
func (r *Registry) CreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
return r.UnsafeCreateOutbound(ctx, router, logger, tag, outboundType, options)
|
||||
}
|
||||
|
||||
func (r *Registry) UnsafeCreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) {
|
||||
constructor, loaded := r.constructors[outboundType]
|
||||
if !loaded {
|
||||
return nil, E.New("outbound type not found: " + outboundType)
|
||||
|
||||
5
box.go
5
box.go
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport/local"
|
||||
@@ -139,6 +140,9 @@ func New(options Options) (*Box, error) {
|
||||
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
||||
needV2RayAPI = true
|
||||
}
|
||||
if experimentalOptions.UnifiedDelay != nil && experimentalOptions.UnifiedDelay.Enabled {
|
||||
ctx = urltest.ContextWithIsUnifiedDelay(ctx)
|
||||
}
|
||||
platformInterface := service.FromContext[platform.Interface](ctx)
|
||||
var defaultLogWriter io.Writer
|
||||
if platformInterface != nil {
|
||||
@@ -155,6 +159,7 @@ func New(options Options) (*Box, error) {
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create log factory")
|
||||
}
|
||||
service.MustRegister[log.Factory](ctx, logFactory)
|
||||
|
||||
var internalServices []adapter.LifecycleService
|
||||
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||
|
||||
Submodule clients/android updated: 39e961fcbc...eb87216961
@@ -1,5 +1,3 @@
|
||||
//go:build with_quic
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
74
common/cloudflare/api.go
Normal file
74
common/cloudflare/api.go
Normal file
@@ -0,0 +1,74 @@
|
||||
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)
|
||||
}
|
||||
17
common/cloudflare/option.go
Normal file
17
common/cloudflare/option.go
Normal file
@@ -0,0 +1,17 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
64
common/cloudflare/profile.go
Normal file
64
common/cloudflare/profile.go
Normal file
@@ -0,0 +1,64 @@
|
||||
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"`
|
||||
}
|
||||
@@ -52,14 +52,6 @@ func (d *DetourDialer) init() {
|
||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package interrupt
|
||||
import (
|
||||
"net"
|
||||
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
)
|
||||
|
||||
@@ -73,3 +74,32 @@ func (c *PacketConn) WriterReplaceable() bool {
|
||||
func (c *PacketConn) Upstream() any {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
)
|
||||
|
||||
@@ -36,6 +37,13 @@ func (g *Group) NewPacketConn(conn net.PacketConn, isExternal bool) net.PacketCo
|
||||
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) {
|
||||
g.access.Lock()
|
||||
defer g.access.Unlock()
|
||||
|
||||
@@ -38,6 +38,9 @@ func NewRouterWithOptions(router adapter.ConnectionRouterEx, logger logger.Conte
|
||||
}
|
||||
}
|
||||
service, err := mux.NewService(mux.ServiceOptions{
|
||||
NewConnectionContext: func(ctx context.Context, conn net.Conn) context.Context {
|
||||
return log.ContextWithNewMuxID(ctx)
|
||||
},
|
||||
NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context {
|
||||
return log.ContextWithNewID(ctx)
|
||||
},
|
||||
|
||||
13
common/urltest/context.go
Normal file
13
common/urltest/context.go
Normal file
@@ -0,0 +1,13 @@
|
||||
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
|
||||
}
|
||||
@@ -128,6 +128,15 @@ func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err e
|
||||
return
|
||||
}
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
25
common/vision/hook.go
Normal file
25
common/vision/hook.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package vision
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
)
|
||||
|
||||
type Hook func(net.Conn)
|
||||
|
||||
type hookKey struct{}
|
||||
|
||||
func WithHook(ctx context.Context, hook Hook) context.Context {
|
||||
if hook == nil {
|
||||
return ctx
|
||||
}
|
||||
return context.WithValue(ctx, hookKey{}, hook)
|
||||
}
|
||||
|
||||
func HookFromContext(ctx context.Context) (Hook, bool) {
|
||||
if ctx == nil {
|
||||
return nil, false
|
||||
}
|
||||
hook, ok := ctx.Value(hookKey{}).(Hook)
|
||||
return hook, ok
|
||||
}
|
||||
348
common/xray/buf/buffer.go
Normal file
348
common/xray/buf/buffer.go
Normal file
@@ -0,0 +1,348 @@
|
||||
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 ErrBufferFull = E.New("buffer is full")
|
||||
|
||||
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 size of this buffer.
|
||||
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
|
||||
clear(ext)
|
||||
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 {
|
||||
clear(b.v[oldEnd:b.end])
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// Available returns the available capacity of the buffer content.
|
||||
func (b *Buffer) Available() int32 {
|
||||
if b == nil {
|
||||
return 0
|
||||
}
|
||||
return int32(len(b.v)) - b.end
|
||||
}
|
||||
|
||||
// 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)
|
||||
if nBytes < len(data) {
|
||||
return nBytes, ErrBufferFull
|
||||
}
|
||||
return nBytes, nil
|
||||
}
|
||||
|
||||
// WriteByte writes a single byte into the buffer.
|
||||
func (b *Buffer) WriteByte(v byte) error {
|
||||
if b.IsFull() {
|
||||
return ErrBufferFull
|
||||
}
|
||||
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())
|
||||
}
|
||||
124
common/xray/buf/copy.go
Normal file
124
common/xray/buf/copy.go
Normal file
@@ -0,0 +1,124 @@
|
||||
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)
|
||||
}
|
||||
127
common/xray/buf/io.go
Normal file
127
common/xray/buf/io.go
Normal file
@@ -0,0 +1,127 @@
|
||||
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 {
|
||||
Reader
|
||||
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,
|
||||
}
|
||||
}
|
||||
310
common/xray/buf/multi_buffer.go
Normal file
310
common/xray/buf/multi_buffer.go
Normal file
@@ -0,0 +1,310 @@
|
||||
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 curr.Len() > last.Available() {
|
||||
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
|
||||
}
|
||||
38
common/xray/buf/override.go
Normal file
38
common/xray/buf/override.go
Normal file
@@ -0,0 +1,38 @@
|
||||
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)
|
||||
}
|
||||
175
common/xray/buf/reader.go
Normal file
175
common/xray/buf/reader.go
Normal file
@@ -0,0 +1,175 @@
|
||||
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
|
||||
}
|
||||
284
common/xray/buf/writer.go
Normal file
284
common/xray/buf/writer.go
Normal file
@@ -0,0 +1,284 @@
|
||||
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
|
||||
flushNext 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if w.flushNext {
|
||||
w.buffered = false
|
||||
w.flushNext = false
|
||||
return w.flushInternal()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// SetFlushNext will wait the next WriteMultiBuffer to flush and set buffered = false
|
||||
func (w *BufferedWriter) SetFlushNext() {
|
||||
w.Lock()
|
||||
defer w.Unlock()
|
||||
w.flushNext = true
|
||||
}
|
||||
|
||||
// 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)
|
||||
)
|
||||
72
common/xray/bytespool/pool.go
Normal file
72
common/xray/bytespool/pool.go
Normal file
@@ -0,0 +1,72 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
32
common/xray/common.go
Normal file
32
common/xray/common.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package common
|
||||
|
||||
import "reflect"
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// CloseIfExists call obj.Close() if obj is not nil.
|
||||
func CloseIfExists(obj any) error {
|
||||
if obj != nil {
|
||||
v := reflect.ValueOf(obj)
|
||||
if !v.IsNil() {
|
||||
return Close(obj)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
18
common/xray/cpuid/cpuid.go
Normal file
18
common/xray/cpuid/cpuid.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package cpuid
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/sys/cpu"
|
||||
)
|
||||
|
||||
var (
|
||||
// Keep in sync with crypto/tls/cipher_suites.go.
|
||||
hasGCMAsmAMD64 = cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ
|
||||
hasGCMAsmARM64 = cpu.ARM64.HasAES && cpu.ARM64.HasPMULL
|
||||
hasGCMAsmS390X = cpu.S390X.HasAES && cpu.S390X.HasAESCBC && cpu.S390X.HasGHASH
|
||||
hasGCMAsmPPC64 = runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le"
|
||||
|
||||
// HasAESGCM indicates whether the CPU has AES-GCM hardware acceleration.
|
||||
HasAESGCM = hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X || hasGCMAsmPPC64
|
||||
)
|
||||
17
common/xray/crypto/crypto.go
Normal file
17
common/xray/crypto/crypto.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
func RandBetween(from int64, to int64) int64 {
|
||||
if from == to {
|
||||
return from
|
||||
}
|
||||
if from > to {
|
||||
from, to = to, from
|
||||
}
|
||||
bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
|
||||
return from + bigInt.Int64()
|
||||
}
|
||||
25
common/xray/errors/errors.go
Normal file
25
common/xray/errors/errors.go
Normal file
@@ -0,0 +1,25 @@
|
||||
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
|
||||
}
|
||||
52
common/xray/interfaces.go
Normal file
52
common/xray/interfaces.go
Normal file
@@ -0,0 +1,52 @@
|
||||
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{}
|
||||
}
|
||||
76
common/xray/json/badoption/range.go
Normal file
76
common/xray/json/badoption/range.go
Normal file
@@ -0,0 +1,76 @@
|
||||
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) {
|
||||
if c.From == c.To {
|
||||
return json.Marshal(c.From)
|
||||
}
|
||||
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 {
|
||||
var int32Value int32
|
||||
err := json.Unmarshal(content, &int32Value)
|
||||
if err == nil {
|
||||
rangeValue.From, rangeValue.To = int32Value, int32Value
|
||||
} else {
|
||||
err := json.Unmarshal(content, &rangeValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if rangeValue.From > rangeValue.To {
|
||||
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)))
|
||||
}
|
||||
181
common/xray/net/address.go
Normal file
181
common/xray/net/address.go
Normal file
@@ -0,0 +1,181 @@
|
||||
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()
|
||||
}
|
||||
146
common/xray/net/destination.go
Normal file
146
common/xray/net/destination.go
Normal file
@@ -0,0 +1,146 @@
|
||||
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
|
||||
}
|
||||
13
common/xray/net/net.go
Normal file
13
common/xray/net/net.go
Normal file
@@ -0,0 +1,13 @@
|
||||
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
|
||||
33
common/xray/net/network.go
Normal file
33
common/xray/net/network.go
Normal file
@@ -0,0 +1,33 @@
|
||||
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
|
||||
}
|
||||
55
common/xray/net/port.go
Normal file
55
common/xray/net/port.go
Normal file
@@ -0,0 +1,55 @@
|
||||
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
|
||||
}
|
||||
84
common/xray/net/system.go
Normal file
84
common/xray/net/system.go
Normal file
@@ -0,0 +1,84 @@
|
||||
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
|
||||
209
common/xray/pipe/impl.go
Normal file
209
common/xray/pipe/impl.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package pipe
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"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
|
||||
} else {
|
||||
p.data, _ = buf.MergeMulti(p.data, mb)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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 == errBufferFull {
|
||||
if p.option.discardOverflow {
|
||||
buf.ReleaseMulti(mb)
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-p.writeSignal.Wait():
|
||||
continue
|
||||
case <-p.done.Wait():
|
||||
buf.ReleaseMulti(mb)
|
||||
return io.ErrClosedPipe
|
||||
}
|
||||
}
|
||||
|
||||
buf.ReleaseMulti(mb)
|
||||
p.readSignal.Signal()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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.data.IsEmpty() {
|
||||
buf.ReleaseMulti(p.data)
|
||||
p.data = nil
|
||||
if p.state == closed {
|
||||
p.state = errord
|
||||
}
|
||||
}
|
||||
|
||||
if p.state == closed || p.state == errord {
|
||||
return
|
||||
}
|
||||
|
||||
p.state = errord
|
||||
|
||||
common.Must(p.done.Close())
|
||||
}
|
||||
53
common/xray/pipe/pipe.go
Normal file
53
common/xray/pipe/pipe.go
Normal file
@@ -0,0 +1,53 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
41
common/xray/pipe/reader.go
Normal file
41
common/xray/pipe/reader.go
Normal file
@@ -0,0 +1,41 @@
|
||||
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
|
||||
}
|
||||
29
common/xray/pipe/writer.go
Normal file
29
common/xray/pipe/writer.go
Normal file
@@ -0,0 +1,29 @@
|
||||
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()
|
||||
}
|
||||
29
common/xray/serial/serial.go
Normal file
29
common/xray/serial/serial.go
Normal file
@@ -0,0 +1,29 @@
|
||||
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[:])
|
||||
}
|
||||
35
common/xray/serial/string.go
Normal file
35
common/xray/serial/string.go
Normal file
@@ -0,0 +1,35 @@
|
||||
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()
|
||||
}
|
||||
49
common/xray/signal/done/done.go
Normal file
49
common/xray/signal/done/done.go
Normal file
@@ -0,0 +1,49 @@
|
||||
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
|
||||
}
|
||||
26
common/xray/signal/notifier.go
Normal file
26
common/xray/signal/notifier.go
Normal file
@@ -0,0 +1,26 @@
|
||||
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
|
||||
}
|
||||
105
common/xray/signal/pubsub/pubsub.go
Normal file
105
common/xray/signal/pubsub/pubsub.go
Normal file
@@ -0,0 +1,105 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
27
common/xray/signal/semaphore/semaphore.go
Normal file
27
common/xray/signal/semaphore/semaphore.go
Normal file
@@ -0,0 +1,27 @@
|
||||
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{}{}
|
||||
}
|
||||
85
common/xray/signal/timer.go
Normal file
85
common/xray/signal/timer.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package signal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/xray"
|
||||
"github.com/sagernet/sing-box/common/xray/task"
|
||||
)
|
||||
|
||||
type ActivityUpdater interface {
|
||||
Update()
|
||||
}
|
||||
|
||||
type ActivityTimer struct {
|
||||
mu sync.RWMutex
|
||||
updated chan struct{}
|
||||
checkTask *task.Periodic
|
||||
onTimeout func()
|
||||
consumed atomic.Bool
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
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.once.Do(func() {
|
||||
t.consumed.Store(true)
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
common.CloseIfExists(t.checkTask)
|
||||
t.onTimeout()
|
||||
})
|
||||
}
|
||||
|
||||
func (t *ActivityTimer) SetTimeout(timeout time.Duration) {
|
||||
if t.consumed.Load() {
|
||||
return
|
||||
}
|
||||
if timeout == 0 {
|
||||
t.finish()
|
||||
return
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
// double check, just in case
|
||||
if t.consumed.Load() {
|
||||
return
|
||||
}
|
||||
newCheckTask := &task.Periodic{
|
||||
Interval: timeout,
|
||||
Execute: t.check,
|
||||
}
|
||||
common.CloseIfExists(t.checkTask)
|
||||
t.checkTask = newCheckTask
|
||||
t.Update()
|
||||
common.Must(newCheckTask.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
|
||||
}
|
||||
34
common/xray/stat/connection.go
Normal file
34
common/xray/stat/connection.go
Normal file
@@ -0,0 +1,34 @@
|
||||
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
|
||||
}
|
||||
13
common/xray/stats/stats.go
Normal file
13
common/xray/stats/stats.go
Normal file
@@ -0,0 +1,13 @@
|
||||
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
|
||||
}
|
||||
10
common/xray/task/common.go
Normal file
10
common/xray/task/common.go
Normal file
@@ -0,0 +1,10 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
85
common/xray/task/periodic.go
Normal file
85
common/xray/task/periodic.go
Normal file
@@ -0,0 +1,85 @@
|
||||
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
|
||||
}
|
||||
64
common/xray/task/task.go
Normal file
64
common/xray/task/task.go
Normal file
@@ -0,0 +1,64 @@
|
||||
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
|
||||
}
|
||||
28
common/xray/utils/browser.go
Normal file
28
common/xray/utils/browser.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/cpuid/v2"
|
||||
)
|
||||
|
||||
func ChromeVersion() int {
|
||||
// Use only CPU info as seed for PRNG
|
||||
seed := int64(cpuid.CPU.Family + cpuid.CPU.Model + cpuid.CPU.PhysicalCores + cpuid.CPU.LogicalCores + cpuid.CPU.CacheLine)
|
||||
rng := rand.New(rand.NewSource(seed))
|
||||
// Start from Chrome 144 released on 2026.1.13
|
||||
releaseDate := time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC)
|
||||
version := 144
|
||||
now := time.Now()
|
||||
// Each version has random 25-45 day interval
|
||||
for releaseDate.Before(now) {
|
||||
releaseDate = releaseDate.AddDate(0, 0, rng.Intn(21)+25)
|
||||
version++
|
||||
}
|
||||
return version - 1
|
||||
}
|
||||
|
||||
// ChromeUA provides default browser User-Agent based on CPU-seeded PRNG.
|
||||
var ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + strconv.Itoa(ChromeVersion()) + ".0.0.0 Safari/537.36"
|
||||
24
common/xray/utils/padding.go
Normal file
24
common/xray/utils/padding.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
// 8 ÷ (397/62)
|
||||
h2packCorrectionFactor = 1.2493702770780857
|
||||
base62TotalCharsNum = 62
|
||||
base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
)
|
||||
|
||||
// H2Base62Pad generates a base62 padding string for HTTP/2 header
|
||||
// The total len will be slightly longer than the input to match the length after h2(h3 also) header huffman encoding
|
||||
func H2Base62Pad[T int32 | int64 | int](expectedLen T) string {
|
||||
actualLenFloat := float64(expectedLen) * h2packCorrectionFactor
|
||||
actualLen := int(actualLenFloat)
|
||||
result := make([]byte, actualLen)
|
||||
for i := range actualLen {
|
||||
result[i] = base62Chars[rand.N(base62TotalCharsNum)]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
105
common/xray/uuid/uuid.go
Normal file
105
common/xray/uuid/uuid.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/sagernet/sing-box/common/xray"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var byteGroups = []int{8, 4, 4, 4, 12}
|
||||
|
||||
type UUID [16]byte
|
||||
|
||||
// String returns the string representation of this UUID.
|
||||
func (u *UUID) String() string {
|
||||
bytes := u.Bytes()
|
||||
result := hex.EncodeToString(bytes[0 : byteGroups[0]/2])
|
||||
start := byteGroups[0] / 2
|
||||
for i := 1; i < len(byteGroups); i++ {
|
||||
nBytes := byteGroups[i] / 2
|
||||
result += "-"
|
||||
result += hex.EncodeToString(bytes[start : start+nBytes])
|
||||
start += nBytes
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Bytes returns the bytes representation of this UUID.
|
||||
func (u *UUID) Bytes() []byte {
|
||||
return u[:]
|
||||
}
|
||||
|
||||
// Equals returns true if this UUID equals another UUID by value.
|
||||
func (u *UUID) Equals(another *UUID) bool {
|
||||
if u == nil && another == nil {
|
||||
return true
|
||||
}
|
||||
if u == nil || another == nil {
|
||||
return false
|
||||
}
|
||||
return bytes.Equal(u.Bytes(), another.Bytes())
|
||||
}
|
||||
|
||||
// New creates a UUID with random value.
|
||||
func New() UUID {
|
||||
var uuid UUID
|
||||
common.Must2(rand.Read(uuid.Bytes()))
|
||||
uuid[6] = (uuid[6] & 0x0f) | (4 << 4)
|
||||
uuid[8] = (uuid[8]&(0xff>>2) | (0x02 << 6))
|
||||
return uuid
|
||||
}
|
||||
|
||||
// ParseBytes converts a UUID in byte form to object.
|
||||
func ParseBytes(b []byte) (UUID, error) {
|
||||
var uuid UUID
|
||||
if len(b) != 16 {
|
||||
return uuid, E.New("invalid UUID: ", b)
|
||||
}
|
||||
copy(uuid[:], b)
|
||||
return uuid, nil
|
||||
}
|
||||
|
||||
// ParseString converts a UUID in string form to object.
|
||||
func ParseString(str string) (UUID, error) {
|
||||
var uuid UUID
|
||||
|
||||
text := []byte(str)
|
||||
if l := len(text); l < 32 || l > 36 {
|
||||
if l == 0 || l > 30 {
|
||||
return uuid, E.New("invalid UUID: ", str)
|
||||
}
|
||||
h := sha1.New()
|
||||
h.Write(uuid[:])
|
||||
h.Write(text)
|
||||
u := h.Sum(nil)[:16]
|
||||
u[6] = (u[6] & 0x0f) | (5 << 4)
|
||||
u[8] = (u[8]&(0xff>>2) | (0x02 << 6))
|
||||
copy(uuid[:], u)
|
||||
return uuid, nil
|
||||
}
|
||||
|
||||
b := uuid.Bytes()
|
||||
|
||||
for _, byteGroup := range byteGroups {
|
||||
if len(text) > 0 && text[0] == '-' {
|
||||
text = text[1:]
|
||||
}
|
||||
|
||||
if len(text) < byteGroup {
|
||||
return uuid, E.New("invalid UUID: ", str)
|
||||
}
|
||||
|
||||
if _, err := hex.Decode(b[:byteGroup/2], text[:byteGroup]); err != nil {
|
||||
return uuid, err
|
||||
}
|
||||
|
||||
text = text[byteGroup:]
|
||||
b = b[byteGroup/2:]
|
||||
}
|
||||
|
||||
return uuid, nil
|
||||
}
|
||||
@@ -28,6 +28,7 @@ const (
|
||||
DNSTypeFakeIP = "fakeip"
|
||||
DNSTypeDHCP = "dhcp"
|
||||
DNSTypeTailscale = "tailscale"
|
||||
DNSTypeSDNS = "sdns"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -1,36 +1,50 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
TypeTun = "tun"
|
||||
TypeRedirect = "redirect"
|
||||
TypeTProxy = "tproxy"
|
||||
TypeDirect = "direct"
|
||||
TypeBlock = "block"
|
||||
TypeDNS = "dns"
|
||||
TypeSOCKS = "socks"
|
||||
TypeHTTP = "http"
|
||||
TypeMixed = "mixed"
|
||||
TypeShadowsocks = "shadowsocks"
|
||||
TypeVMess = "vmess"
|
||||
TypeTrojan = "trojan"
|
||||
TypeNaive = "naive"
|
||||
TypeWireGuard = "wireguard"
|
||||
TypeHysteria = "hysteria"
|
||||
TypeTor = "tor"
|
||||
TypeSSH = "ssh"
|
||||
TypeShadowTLS = "shadowtls"
|
||||
TypeAnyTLS = "anytls"
|
||||
TypeShadowsocksR = "shadowsocksr"
|
||||
TypeVLESS = "vless"
|
||||
TypeTUIC = "tuic"
|
||||
TypeHysteria2 = "hysteria2"
|
||||
TypeTailscale = "tailscale"
|
||||
TypeDERP = "derp"
|
||||
TypeResolved = "resolved"
|
||||
TypeSSMAPI = "ssm-api"
|
||||
TypeTun = "tun"
|
||||
TypeRedirect = "redirect"
|
||||
TypeTProxy = "tproxy"
|
||||
TypeDirect = "direct"
|
||||
TypeBlock = "block"
|
||||
TypeDNS = "dns"
|
||||
TypeSOCKS = "socks"
|
||||
TypeHTTP = "http"
|
||||
TypeMixed = "mixed"
|
||||
TypeShadowsocks = "shadowsocks"
|
||||
TypeVMess = "vmess"
|
||||
TypeTrojan = "trojan"
|
||||
TypeNaive = "naive"
|
||||
TypeWireGuard = "wireguard"
|
||||
TypeWARP = "warp"
|
||||
TypeHysteria = "hysteria"
|
||||
TypeTor = "tor"
|
||||
TypeSSH = "ssh"
|
||||
TypeShadowTLS = "shadowtls"
|
||||
TypeMieru = "mieru"
|
||||
TypeAnyTLS = "anytls"
|
||||
TypeShadowsocksR = "shadowsocksr"
|
||||
TypeVLESS = "vless"
|
||||
TypeTUIC = "tuic"
|
||||
TypeHysteria2 = "hysteria2"
|
||||
TypeBond = "bond"
|
||||
TypeTunnelServer = "tunnel-server"
|
||||
TypeTunnelClient = "tunnel-client"
|
||||
TypeTailscale = "tailscale"
|
||||
TypeConnectionLimiter = "connection-limiter"
|
||||
TypeBandwidthLimiter = "bandwidth-limiter"
|
||||
TypeTrafficLimiter = "traffic-limiter"
|
||||
TypeAdminPanel = "admin-panel"
|
||||
TypeNodeManagerServer = "node-manager-server"
|
||||
TypeNodeManagerClient = "node-manager-client"
|
||||
TypeDERP = "derp"
|
||||
TypeManager = "manager"
|
||||
TypeNode = "node"
|
||||
TypeResolved = "resolved"
|
||||
TypeSSMAPI = "ssm-api"
|
||||
)
|
||||
|
||||
const (
|
||||
TypeFailover = "failover"
|
||||
TypeSelector = "selector"
|
||||
TypeURLTest = "urltest"
|
||||
)
|
||||
@@ -65,6 +79,8 @@ func ProxyDisplayName(proxyType string) string {
|
||||
return "Naive"
|
||||
case TypeWireGuard:
|
||||
return "WireGuard"
|
||||
case TypeWARP:
|
||||
return "WARP"
|
||||
case TypeHysteria:
|
||||
return "Hysteria"
|
||||
case TypeTor:
|
||||
@@ -81,12 +97,18 @@ func ProxyDisplayName(proxyType string) string {
|
||||
return "TUIC"
|
||||
case TypeHysteria2:
|
||||
return "Hysteria2"
|
||||
case TypeMieru:
|
||||
return "Mieru"
|
||||
case TypeAnyTLS:
|
||||
return "AnyTLS"
|
||||
case TypeSelector:
|
||||
return "Selector"
|
||||
case TypeURLTest:
|
||||
return "URLTest"
|
||||
case TypeTunnelClient:
|
||||
return "Tunnel Client"
|
||||
case TypeTunnelServer:
|
||||
return "Tunnel Server"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
@@ -6,4 +6,6 @@ const (
|
||||
V2RayTransportTypeQUIC = "quic"
|
||||
V2RayTransportTypeGRPC = "grpc"
|
||||
V2RayTransportTypeHTTPUpgrade = "httpupgrade"
|
||||
V2RayTransportTypeXHTTP = "xhttp"
|
||||
V2RayTransportTypeKCP = "mkcp"
|
||||
)
|
||||
|
||||
20
constant/warp.go
Normal file
20
constant/warp.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package constant
|
||||
|
||||
type WARPConfig struct {
|
||||
PrivateKey string `json:"private_key"`
|
||||
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"`
|
||||
}
|
||||
@@ -144,7 +144,11 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
if c.cache != nil {
|
||||
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
|
||||
if loaded {
|
||||
<-cond
|
||||
select {
|
||||
case <-cond:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
} else {
|
||||
defer func() {
|
||||
c.cacheLock.Delete(question)
|
||||
@@ -154,7 +158,11 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
} else if c.transportCache != nil {
|
||||
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
|
||||
if loaded {
|
||||
<-cond
|
||||
select {
|
||||
case <-cond:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
} else {
|
||||
defer func() {
|
||||
c.transportCacheLock.Delete(question)
|
||||
|
||||
@@ -378,9 +378,11 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
||||
case *R.RuleActionReject:
|
||||
return nil, &R.RejectedError{Cause: action.Error(ctx)}
|
||||
case *R.RuleActionPredefined:
|
||||
responseAddrs = nil
|
||||
if action.Rcode != mDNS.RcodeSuccess {
|
||||
err = RcodeError(action.Rcode)
|
||||
} else {
|
||||
err = nil
|
||||
for _, answer := range action.Answer {
|
||||
switch record := answer.(type) {
|
||||
case *mDNS.A:
|
||||
|
||||
71
dns/transport/sdns.go
Normal file
71
dns/transport/sdns.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ameshkov/dnscrypt/v2"
|
||||
mDNS "github.com/miekg/dns"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
var _ adapter.DNSTransport = (*SDNSTransport)(nil)
|
||||
|
||||
func RegisterSDNS(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.SDNSDNSServerOptions](registry, C.DNSTypeSDNS, NewSDNSTransport)
|
||||
}
|
||||
|
||||
type SDNSTransport struct {
|
||||
dns.TransportAdapter
|
||||
client *dnscrypt.Client
|
||||
name string
|
||||
stamp string
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewSDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.SDNSDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SDNSTransport{
|
||||
client: &dnscrypt.Client{
|
||||
Net: "udp",
|
||||
Timeout: 10 * time.Second,
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return transportDialer.DialContext(ctx, N.NetworkName(network), M.ParseSocksaddr(addr))
|
||||
},
|
||||
},
|
||||
stamp: options.Stamp,
|
||||
}, err
|
||||
}
|
||||
|
||||
func (t *SDNSTransport) Name() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func (t *SDNSTransport) Start(adapter.StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *SDNSTransport) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *SDNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
resolverInfo, err := t.client.Dial(t.stamp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.client.Exchange(message, resolverInfo)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,34 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.12.22
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.21
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.20
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.19
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.18
|
||||
|
||||
* Add fallback routing rule for `auto_redirect` **1**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default),
|
||||
ensuring traffic is routed to the sing-box table when no route is found in system tables.
|
||||
|
||||
The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768).
|
||||
|
||||
#### 1.12.17
|
||||
|
||||
* Update uTLS to v1.8.2 **1**
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.18"
|
||||
|
||||
:material-plus: [auto_redirect_iproute2_fallback_rule_index](#auto_redirect_iproute2_fallback_rule_index)
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-plus: [loopback_address](#loopback_address)
|
||||
@@ -63,6 +67,7 @@ icon: material/new-box
|
||||
"auto_redirect": true,
|
||||
"auto_redirect_input_mark": "0x2023",
|
||||
"auto_redirect_output_mark": "0x2024",
|
||||
"auto_redirect_iproute2_fallback_rule_index": 32768,
|
||||
"loopback_address": [
|
||||
"10.7.0.1"
|
||||
],
|
||||
@@ -278,6 +283,17 @@ Connection output mark used by `auto_redirect`.
|
||||
|
||||
`0x2024` is used by default.
|
||||
|
||||
#### auto_redirect_iproute2_fallback_rule_index
|
||||
|
||||
!!! question "Since sing-box 1.12.18"
|
||||
|
||||
Linux iproute2 fallback rule index generated by `auto_redirect`.
|
||||
|
||||
This rule is checked after system default rules (32766: main, 32767: default),
|
||||
routing traffic to the sing-box table only when no route is found in system tables.
|
||||
|
||||
`32768` is used by default.
|
||||
|
||||
#### loopback_address
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.12.18 中的更改"
|
||||
|
||||
:material-plus: [auto_redirect_iproute2_fallback_rule_index](#auto_redirect_iproute2_fallback_rule_index)
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-plus: [loopback_address](#loopback_address)
|
||||
@@ -63,6 +67,7 @@ icon: material/new-box
|
||||
"auto_redirect": true,
|
||||
"auto_redirect_input_mark": "0x2023",
|
||||
"auto_redirect_output_mark": "0x2024",
|
||||
"auto_redirect_iproute2_fallback_rule_index": 32768,
|
||||
"loopback_address": [
|
||||
"10.7.0.1"
|
||||
],
|
||||
@@ -277,6 +282,17 @@ tun 接口的 IPv6 前缀。
|
||||
|
||||
默认使用 `0x2024`。
|
||||
|
||||
#### auto_redirect_iproute2_fallback_rule_index
|
||||
|
||||
!!! question "自 sing-box 1.12.18 起"
|
||||
|
||||
`auto_redirect` 生成的 iproute2 回退规则索引。
|
||||
|
||||
此规则在系统默认规则(32766: main,32767: default)之后检查,
|
||||
仅当系统路由表中未找到路由时才将流量路由到 sing-box 路由表。
|
||||
|
||||
默认使用 `32768`。
|
||||
|
||||
#### loopback_address
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
| `shadowtls` | [ShadowTLS](./shadowtls/) |
|
||||
| `tuic` | [TUIC](./tuic/) |
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
||||
| `mieru` | [Mieru](./mieru/) |
|
||||
| `anytls` | [AnyTLS](./anytls/) |
|
||||
| `tor` | [Tor](./tor/) |
|
||||
| `ssh` | [SSH](./ssh/) |
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
| `shadowtls` | [ShadowTLS](./shadowtls/) |
|
||||
| `tuic` | [TUIC](./tuic/) |
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
||||
| `mieru` | [Mieru](./mieru/) |
|
||||
| `anytls` | [AnyTLS](./anytls/) |
|
||||
| `tor` | [Tor](./tor/) |
|
||||
| `ssh` | [SSH](./ssh/) |
|
||||
|
||||
71
docs/configuration/outbound/mieru.md
Normal file
71
docs/configuration/outbound/mieru.md
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "mieru",
|
||||
"tag": "mieru-out",
|
||||
|
||||
"server": "127.0.0.1",
|
||||
"server_port": 1080,
|
||||
"server_ports": [
|
||||
"9000-9010",
|
||||
"9020-9030"
|
||||
],
|
||||
"transport": "TCP",
|
||||
"username": "asdf",
|
||||
"password": "hjkl",
|
||||
"multiplexing": "MULTIPLEXING_LOW",
|
||||
|
||||
... // Dial Fields
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### server
|
||||
|
||||
==Required==
|
||||
|
||||
The server address.
|
||||
|
||||
#### server_port
|
||||
|
||||
The server port.
|
||||
|
||||
Must set at least one field between `server_port` and `server_ports`.
|
||||
|
||||
#### server_ports
|
||||
|
||||
Server port range list.
|
||||
|
||||
Must set at least one field between `server_port` and `server_ports`.
|
||||
|
||||
#### transport
|
||||
|
||||
==Required==
|
||||
|
||||
Transmission protocol. The only allowed value is `TCP`.
|
||||
|
||||
#### username
|
||||
|
||||
==Required==
|
||||
|
||||
mieru user name.
|
||||
|
||||
#### password
|
||||
|
||||
==Required==
|
||||
|
||||
mieru password.
|
||||
|
||||
#### multiplexing
|
||||
|
||||
Multiplexing level. Supported values are `MULTIPLEXING_OFF`, `MULTIPLEXING_LOW`, `MULTIPLEXING_MIDDLE`, `MULTIPLEXING_HIGH`. `MULTIPLEXING_OFF` disables multiplexing.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
71
docs/configuration/outbound/mieru.zh.md
Normal file
71
docs/configuration/outbound/mieru.zh.md
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "mieru",
|
||||
"tag": "mieru-out",
|
||||
|
||||
"server": "127.0.0.1",
|
||||
"server_port": 1080,
|
||||
"server_ports": [
|
||||
"9000-9010",
|
||||
"9020-9030"
|
||||
],
|
||||
"transport": "TCP",
|
||||
"username": "asdf",
|
||||
"password": "hjkl",
|
||||
"multiplexing": "MULTIPLEXING_LOW",
|
||||
|
||||
... // 拨号字段
|
||||
}
|
||||
```
|
||||
|
||||
### 字段
|
||||
|
||||
#### server
|
||||
|
||||
==必填==
|
||||
|
||||
服务器地址。
|
||||
|
||||
#### server_port
|
||||
|
||||
服务器端口。
|
||||
|
||||
必须填写 `server_port` 和 `server_ports` 中至少一项。
|
||||
|
||||
#### server_ports
|
||||
|
||||
服务器端口范围列表。
|
||||
|
||||
必须填写 `server_port` 和 `server_ports` 中至少一项。
|
||||
|
||||
#### transport
|
||||
|
||||
==必填==
|
||||
|
||||
通信协议。仅可设为 `TCP`。
|
||||
|
||||
#### username
|
||||
|
||||
==必填==
|
||||
|
||||
mieru 用户名。
|
||||
|
||||
#### password
|
||||
|
||||
==必填==
|
||||
|
||||
mieru 密码。
|
||||
|
||||
#### multiplexing
|
||||
|
||||
多路复用设置。可以设为 `MULTIPLEXING_OFF`,`MULTIPLEXING_LOW`,`MULTIPLEXING_MIDDLE`,`MULTIPLEXING_HIGH`。其中 `MULTIPLEXING_OFF` 会关闭多路复用功能。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
||||
71
examples/amnezia/client.json
Normal file
71
examples/amnezia/client.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "wireguard",
|
||||
"tag": "wireguard-out",
|
||||
"mtu": 1408,
|
||||
"address": null,
|
||||
"private_key": "",
|
||||
"listen_port": 10000,
|
||||
"peers": [
|
||||
{
|
||||
"address": "example.com",
|
||||
"port": 10001,
|
||||
"reserved": "AAAA"
|
||||
}
|
||||
],
|
||||
"udp_timeout": "5m0s",
|
||||
"amnezia": {
|
||||
"jc": 120,
|
||||
"jmin": 23,
|
||||
"jmax": 911,
|
||||
"s1": 1,
|
||||
"s2": 2,
|
||||
"s3": 3,
|
||||
"s4": 4,
|
||||
"h1": 1,
|
||||
"h2": 2,
|
||||
"h3": 3,
|
||||
"h4": 4,
|
||||
"i1": "<b 0xc70000000108...",
|
||||
"i2": "<b 0xc70000000108...",
|
||||
"i3": "<b 0xc70000000108...",
|
||||
"i4": "<b 0xc70000000108...",
|
||||
"i5": "<b 0xc70000000108...",
|
||||
"j1": "<b 0xc70000000108...",
|
||||
"j2": "<b 0xc70000000108...",
|
||||
"j3": "<b 0xc70000000108...",
|
||||
"itime": 50,
|
||||
}
|
||||
}
|
||||
],
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
],
|
||||
"route": {
|
||||
"final": "wireguard-out",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
57
examples/bandwidth_limiter/connection.json
Normal file
57
examples/bandwidth_limiter/connection.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 443,
|
||||
"transport": {
|
||||
"type": "http"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
},
|
||||
{
|
||||
"name": "user2",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "bandwidth-limiter",
|
||||
"tag": "bandwidth-limiter",
|
||||
"strategy": "connection",
|
||||
"mode": "duplex", // download, upload
|
||||
"connection_type": "hwid", // mux, ip
|
||||
"speed": "1MB", // 100KB, 1GB, etc.
|
||||
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
|
||||
"rules": [],
|
||||
"final": "direct"
|
||||
}
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "bandwidth-limiter",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
56
examples/bandwidth_limiter/global.json
Normal file
56
examples/bandwidth_limiter/global.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 443,
|
||||
"transport": {
|
||||
"type": "http"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
},
|
||||
{
|
||||
"name": "user2",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "bandwidth-limiter",
|
||||
"tag": "bandwidth-limiter",
|
||||
"strategy": "global",
|
||||
"mode": "duplex", // download, upload
|
||||
"speed": "1MB", // 100KB, 1GB, etc.
|
||||
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
|
||||
"rules": [],
|
||||
"final": "direct"
|
||||
}
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "bandwidth-limiter",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
78
examples/bandwidth_limiter/multi.json
Normal file
78
examples/bandwidth_limiter/multi.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 443,
|
||||
"transport": {
|
||||
"type": "http"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
},
|
||||
{
|
||||
"name": "user2",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "bandwidth-limiter",
|
||||
"tag": "duplex-bandwidth-limiter",
|
||||
"strategy": "global",
|
||||
"mode": "duplex",
|
||||
"speed": "5MB",
|
||||
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
|
||||
"rules": [],
|
||||
"final": "direct"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "bandwidth-limiter",
|
||||
"tag": "upload-bandwidth-limiter",
|
||||
"strategy": "global",
|
||||
"mode": "upload",
|
||||
"speed": "3MB",
|
||||
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
|
||||
"rules": [],
|
||||
"final": "duplex-bandwidth-limiter"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "bandwidth-limiter",
|
||||
"tag": "download-bandwidth-limiter",
|
||||
"strategy": "global",
|
||||
"mode": "download",
|
||||
"speed": "3MB",
|
||||
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
|
||||
"rules": [],
|
||||
"final": "upload-bandwidth-limiter"
|
||||
}
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "download-bandwidth-limiter",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
70
examples/bandwidth_limiter/users.json
Normal file
70
examples/bandwidth_limiter/users.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 443,
|
||||
"transport": {
|
||||
"type": "http"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
},
|
||||
{
|
||||
"name": "user2",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "bandwidth-limiter",
|
||||
"tag": "bandwidth-limiter",
|
||||
"strategy": "users",
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"strategy": "connection", // global
|
||||
"mode": "duplex", // download, upload
|
||||
"connection_type": "hwid", // mux, ip
|
||||
"speed": "5MB", // 100KB, 1GB, etc.
|
||||
},
|
||||
{
|
||||
"name": "user2",
|
||||
"strategy": "connection", // global
|
||||
"mode": "duplex", // download, upload
|
||||
"connection_type": "hwid", // mux, ip
|
||||
"speed": "1MB", // 100KB, 1GB, etc.
|
||||
},
|
||||
],
|
||||
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
|
||||
"rules": [],
|
||||
"final": "direct"
|
||||
}
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "bandwidth-limiter",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
61
examples/bond/client.json
Normal file
61
examples/bond/client.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "bond",
|
||||
"tag": "bond-out",
|
||||
"outbounds": [ // sum of download_ratio and upload_ratio must be 100
|
||||
{
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
"server": "0.0.0.0",
|
||||
"server_port": 443,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"network": "tcp",
|
||||
"bind_interface": ""
|
||||
},
|
||||
"download_ratio": 50,
|
||||
"upload_ratio": 50
|
||||
},
|
||||
{
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
"server": "0.0.0.0",
|
||||
"server_port": 444,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"network": "tcp",
|
||||
"bind_interface": ""
|
||||
},
|
||||
"download_ratio": 50,
|
||||
"upload_ratio": 50
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "bond-out",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
49
examples/bond/client_multi.json
Normal file
49
examples/bond/client_multi.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "bond",
|
||||
"tag": "bond-out",
|
||||
"outbounds": [ // sum of download_ratio and upload_ratio must be 100
|
||||
{
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
"server": "0.0.0.0",
|
||||
"server_port": 443,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"network": "tcp",
|
||||
},
|
||||
"download_ratio": 20,
|
||||
"upload_ratio": 20,
|
||||
"count": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "bond-out",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
61
examples/bond/client_split.json
Normal file
61
examples/bond/client_split.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "bond",
|
||||
"tag": "bond-out",
|
||||
"outbounds": [ // sum of download_ratio and upload_ratio must be 100
|
||||
{
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
"server": "0.0.0.0",
|
||||
"server_port": 443,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"network": "tcp",
|
||||
"bind_interface": ""
|
||||
},
|
||||
"download_ratio": 100,
|
||||
"upload_ratio": 0
|
||||
},
|
||||
{
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
"server": "0.0.0.0",
|
||||
"server_port": 444,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"network": "tcp",
|
||||
"bind_interface": ""
|
||||
},
|
||||
"download_ratio": 0,
|
||||
"upload_ratio": 100
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "bond-out",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
54
examples/bond/server.json
Normal file
54
examples/bond/server.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "bond",
|
||||
"tag": "bond-in",
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 443,
|
||||
"users": [
|
||||
{
|
||||
"name": "user",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "vless",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 444,
|
||||
"users": [
|
||||
{
|
||||
"name": "user",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
],
|
||||
"route": {
|
||||
"final": "direct",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
56
examples/connection_limiter/connection.json
Normal file
56
examples/connection_limiter/connection.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 443,
|
||||
"transport": {
|
||||
"type": "http"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
},
|
||||
{
|
||||
"name": "user2",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "connection-limiter",
|
||||
"tag": "connection-limiter",
|
||||
"strategy": "connection",
|
||||
"connection_type": "hwid", // mux, ip
|
||||
"count": 5,
|
||||
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
|
||||
"rules": [],
|
||||
"final": "direct"
|
||||
}
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "connection-limiter",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
68
examples/connection_limiter/users.json
Normal file
68
examples/connection_limiter/users.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 5000,
|
||||
"transport": {
|
||||
"type": "http"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
},
|
||||
{
|
||||
"name": "user2",
|
||||
"uuid": "6c8c7ffc-a909-4699-af34-e9d9bcb3e6d6"
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "connection-limiter",
|
||||
"tag": "connection-limiter",
|
||||
"strategy": "users",
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"strategy": "connection",
|
||||
"connection_type": "hwid", // mux, ip
|
||||
"count": 5,
|
||||
},
|
||||
{
|
||||
"name": "user2",
|
||||
"strategy": "connection",
|
||||
"connection_type": "hwid", // mux, ip
|
||||
"count": 1,
|
||||
},
|
||||
],
|
||||
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
|
||||
"rules": [],
|
||||
"final": "direct"
|
||||
}
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "connection-limiter",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
61
examples/failover/client.json
Normal file
61
examples/failover/client.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-1-out",
|
||||
"server": "example1.com",
|
||||
"server_port": 443,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
},
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-2-out",
|
||||
"server": "example2.com",
|
||||
"server_port": 443,
|
||||
"uuid": "294fd6bc-4f89-43e7-9228-7900aba396af"
|
||||
},
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-3-out",
|
||||
"server": "example3.com",
|
||||
"server_port": 443,
|
||||
"uuid": "257f20d0-294a-4f07-9f2c-9efee9a37400"
|
||||
},
|
||||
{
|
||||
"type": "failover",
|
||||
"tag": "failover-out",
|
||||
"outbounds": [
|
||||
"vless-1-out",
|
||||
"vless-2-out",
|
||||
"vless-3-out"
|
||||
]
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "failover-out",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
70
examples/manager/manager.json
Normal file
70
examples/manager/manager.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"port": 53,
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
],
|
||||
"final": "direct-out"
|
||||
},
|
||||
"services": [
|
||||
{
|
||||
"type": "manager",
|
||||
"tag": "my-manager",
|
||||
"database": {
|
||||
"driver": "postgresql",
|
||||
"dsn": "postgresql://postgres:postgres@localhost:5432/manager?sslmode=disable"
|
||||
}
|
||||
},
|
||||
{ // http://127.0.0.1:8000
|
||||
// Username: admin
|
||||
// Password: admin
|
||||
"type": "admin-panel",
|
||||
"tag": "my-admin-panel",
|
||||
"listen_port": 8000,
|
||||
"manager": "my-manager",
|
||||
"database": {
|
||||
"driver": "postgresql",
|
||||
"dsn": "postgresql://postgres:postgres@localhost:5432/adminpanel?sslmode=disable"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node-manager-server", // for connecting nodes
|
||||
"listen_port": 7000,
|
||||
"manager": "my-manager",
|
||||
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#inbound
|
||||
"enabled": true,
|
||||
"server_name": "example.com",
|
||||
"certificate_path": "/path/to/fullchain.pem",
|
||||
"key_path": "/path/to/privkey.pem"
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
85
examples/manager/node.json
Normal file
85
examples/manager/node.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 443,
|
||||
"transport": {
|
||||
"type": "http"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
},
|
||||
{
|
||||
"type": "bandwidth-limiter",
|
||||
"tag": "bandwidth-limiter",
|
||||
"strategy": "manager",
|
||||
"route": {
|
||||
"final": "direct-out"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "connection-limiter",
|
||||
"tag": "connection-limiter",
|
||||
"strategy": "manager",
|
||||
"route": {
|
||||
"final": "bandwidth-limiter"
|
||||
}
|
||||
},
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"port": 53,
|
||||
"outbound": "dns-out"
|
||||
}
|
||||
],
|
||||
"final": "connection-limiter"
|
||||
},
|
||||
"services": [
|
||||
{
|
||||
"type": "node",
|
||||
"tag": "my-node",
|
||||
"uuid": "e6eceb84-ad66-474b-8641-142499db7c6e",
|
||||
"manager": "node-manager",
|
||||
"inbounds": ["vless-in"],
|
||||
"bandwidth_limiters": ["bandwidth-limiter"],
|
||||
"connection_limiters": ["connection-limiter"],
|
||||
},
|
||||
{
|
||||
"type": "node-manager-client",
|
||||
"tag": "node-manager",
|
||||
"server": "example.com",
|
||||
"server_port": 7000,
|
||||
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#outbound
|
||||
"enabled": true,
|
||||
"server_name": "example.com",
|
||||
"alpn": "h2" // h3 for QUIC
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
43
examples/mieru/client.json
Normal file
43
examples/mieru/client.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "mieru",
|
||||
"tag": "mieru-out",
|
||||
"server": "example.com",
|
||||
"server_port": 27017,
|
||||
"server_ports": "27017-27019",
|
||||
"transport": "TCP",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"multiplexing": "MULTIPLEXING_LOW"
|
||||
// Dial Fields
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "mieru-out",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
43
examples/mkcp/client.json
Normal file
43
examples/mkcp/client.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-out",
|
||||
"server": "example.com",
|
||||
"server_port": 443,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"packet_encoding": "",
|
||||
"transport": {
|
||||
"type": "mkcp",
|
||||
"mtu": 1500
|
||||
}
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "vless-out",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
42
examples/mkcp/server.json
Normal file
42
examples/mkcp/server.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 443,
|
||||
"users": [
|
||||
{
|
||||
"name": "user",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
],
|
||||
"transport": {
|
||||
"type": "mkcp",
|
||||
"mtu": 1500
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "direct",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
32
examples/sdns/client.json
Normal file
32
examples/sdns/client.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "sdns",
|
||||
"stamp": "sdns://AQMAAAAAAAAAETk0LjE0MC4xNS4xNTo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
],
|
||||
"route": {
|
||||
"final": "direct",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
63
examples/tunnel/client-server/client.json
Normal file
63
examples/tunnel/client-server/client.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "info"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel-client",
|
||||
"tag": "tunnel",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe",
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
"tag": "vless-out",
|
||||
"server": "0.0.0.0",
|
||||
"server_port": 8000,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"network": "tcp"
|
||||
}
|
||||
}
|
||||
],
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 10000
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
},
|
||||
{
|
||||
"type": "failover",
|
||||
"tag": "f",
|
||||
"outbounds": ["tunnel", "direct-out"],
|
||||
"interrupt_exist_connections": false,
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"outbound": "f",
|
||||
"override_tunnel_destination": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13"
|
||||
}
|
||||
],
|
||||
"final": "f",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
49
examples/tunnel/client-server/server.json
Normal file
49
examples/tunnel/client-server/server.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "info"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel-server",
|
||||
"tag": "tunnel",
|
||||
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13",
|
||||
"users": [
|
||||
{
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe"
|
||||
}
|
||||
],
|
||||
"inbound": {
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 8000,
|
||||
"users": [
|
||||
{
|
||||
"name": "vless",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "direct-out",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user