mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-06-09 21:08:15 +03:00
Compare commits
12 Commits
v1.13.12-e
...
v1.13.12-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f5ccf43d4 | ||
|
|
6f6af8e902 | ||
|
|
6cddffd546 | ||
|
|
a59e9ec9e3 | ||
|
|
9ebec50a72 | ||
|
|
9ff7a84afe | ||
|
|
7a0354699a | ||
|
|
978b951169 | ||
|
|
e363c2ff78 | ||
|
|
0577c28120 | ||
|
|
195a33379d | ||
|
|
9b3da79c32 |
@@ -1,12 +1,16 @@
|
||||
-s dir
|
||||
--name sing-box
|
||||
--name sing-box-extended
|
||||
--category net
|
||||
--license GPL-3.0-or-later
|
||||
--description "The universal proxy platform."
|
||||
--description "The universal proxy platform (extended)."
|
||||
--url "https://sing-box.sagernet.org/"
|
||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||
--no-deb-generate-changes
|
||||
|
||||
--provides sing-box
|
||||
--conflicts sing-box
|
||||
--replaces sing-box
|
||||
|
||||
--config-files /etc/config/sing-box
|
||||
--config-files /etc/sing-box/config.json
|
||||
|
||||
|
||||
12
.github/build_openwrt_apk.sh
vendored
12
.github/build_openwrt_apk.sh
vendored
@@ -27,10 +27,7 @@ fi
|
||||
PROJECT=$(cd "$(dirname "$0")/.."; pwd)
|
||||
|
||||
# Convert version to APK format:
|
||||
# 1.13.0-beta.8 -> 1.13.0_beta8-r0
|
||||
# 1.13.0-rc.3 -> 1.13.0_rc3-r0
|
||||
# 1.13.0 -> 1.13.0-r0
|
||||
APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/')
|
||||
APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/' | sed -E 's/-[a-z]+-/./g')
|
||||
APK_VERSION="${APK_VERSION}-r0"
|
||||
|
||||
ROOT_DIR=$(mktemp -d)
|
||||
@@ -78,15 +75,16 @@ done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static"
|
||||
|
||||
# Build APK
|
||||
apk --root "$APK_ROOT_DIR" mkpkg \
|
||||
--info "name:sing-box" \
|
||||
--info "name:sing-box-extended" \
|
||||
--info "version:${APK_VERSION}" \
|
||||
--info "description:The universal proxy platform." \
|
||||
--info "description:The universal proxy platform (extended)." \
|
||||
--info "arch:${ARCHITECTURE}" \
|
||||
--info "license:GPL-3.0-or-later" \
|
||||
--info "origin:sing-box" \
|
||||
--info "origin:sing-box-extended" \
|
||||
--info "url:https://sing-box.sagernet.org/" \
|
||||
--info "maintainer:nekohasekai <contact-git@sekai.icu>" \
|
||||
--info "depends:ca-bundle kmod-inet-diag kmod-tun firewall4 kmod-nft-queue" \
|
||||
--info "provides:sing-box" \
|
||||
--info "provider-priority:100" \
|
||||
--script "pre-deinstall:${PROJECT}/release/config/openwrt.prerm" \
|
||||
--files "$ROOT_DIR" \
|
||||
|
||||
53
.github/build_openwrt_packages.sh
vendored
Executable file
53
.github/build_openwrt_packages.sh
vendored
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e -o pipefail
|
||||
|
||||
VERSION="$1"
|
||||
TARGET="$2"
|
||||
BINARY_PATH="$3"
|
||||
|
||||
PROJECT=$(cd "$(dirname "$0")/.."; pwd)
|
||||
DIST="$PROJECT/dist"
|
||||
|
||||
case "$TARGET" in
|
||||
linux_amd64*) ARCHITECTURES="x86_64" ;;
|
||||
linux_arm64*) ARCHITECTURES="aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" ;;
|
||||
linux_386*softfloat) ARCHITECTURES="i386_pentium-mmx" ;;
|
||||
linux_386*) ARCHITECTURES="i386_pentium4" ;;
|
||||
linux_arm_7*) ARCHITECTURES="arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" ;;
|
||||
linux_arm_6*) ARCHITECTURES="arm_arm1176jzf-s_vfp" ;;
|
||||
linux_arm_5*) ARCHITECTURES="arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" ;;
|
||||
linux_mips64_*) ARCHITECTURES="mips64_mips64r2 mips64_octeonplus" ;;
|
||||
linux_mips64le*) ARCHITECTURES="mips64el_mips64r2" ;;
|
||||
linux_mipsle*hardfloat) ARCHITECTURES="mipsel_24kc_24kf" ;;
|
||||
linux_mipsle*) ARCHITECTURES="mipsel_24kc mipsel_74kc mipsel_mips32" ;;
|
||||
linux_mips_*) ARCHITECTURES="mips_24kc mips_4kec mips_mips32" ;;
|
||||
linux_riscv64*) ARCHITECTURES="riscv64_generic" ;;
|
||||
linux_loong64*) ARCHITECTURES="loongarch64_generic" ;;
|
||||
*) echo "Unknown target: $TARGET"; exit 1 ;;
|
||||
esac
|
||||
|
||||
PKG_VERSION="${VERSION//-/\~}"
|
||||
|
||||
for ARCH in $ARCHITECTURES; do
|
||||
cp "$PROJECT/.fpm_openwrt" "$PROJECT/.fpm"
|
||||
fpm -t deb \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "$DIST/_openwrt_tmp.deb" \
|
||||
--architecture all \
|
||||
"$BINARY_PATH=/usr/bin/sing-box"
|
||||
rm -f "$PROJECT/.fpm"
|
||||
|
||||
bash "$PROJECT/.github/deb2ipk.sh" \
|
||||
"$ARCH" \
|
||||
"$DIST/_openwrt_tmp.deb" \
|
||||
"$DIST/sing-box-extended_${VERSION}_openwrt_${ARCH}.ipk"
|
||||
rm -f "$DIST/_openwrt_tmp.deb"
|
||||
|
||||
if command -v apk &>/dev/null; then
|
||||
bash "$PROJECT/.github/build_openwrt_apk.sh" \
|
||||
"$ARCH" "$VERSION" "$BINARY_PATH" \
|
||||
"$DIST/sing-box-extended_${VERSION}_openwrt_${ARCH}.apk"
|
||||
fi
|
||||
|
||||
echo "Built: sing-box-extended_${VERSION}_openwrt_${ARCH} (.ipk/.apk)"
|
||||
done
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -22,4 +22,11 @@ CLAUDE.md
|
||||
AGENTS.md
|
||||
/.claude/
|
||||
dist
|
||||
logs
|
||||
logs
|
||||
/*.so
|
||||
/*.log
|
||||
/*.db-shm
|
||||
/*.db-wal
|
||||
/*.db.backup.*
|
||||
/test_download.bin
|
||||
/wget-log
|
||||
126
.goreleaser.yaml
126
.goreleaser.yaml
@@ -11,6 +11,7 @@ builds:
|
||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
||||
- -s
|
||||
- -buildid=
|
||||
- -checklinkname=0
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
@@ -22,8 +23,13 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOTOOLCHAIN=local
|
||||
@@ -54,6 +60,11 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
targets:
|
||||
- linux_mips
|
||||
- linux_mips_softfloat
|
||||
@@ -107,9 +118,14 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_naive_outbound
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_purego
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
@@ -134,9 +150,14 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_naive_outbound
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_purego
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
@@ -161,9 +182,14 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_naive_outbound
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_purego
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
@@ -188,9 +214,14 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_naive_outbound
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_purego
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
@@ -215,8 +246,13 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
@@ -258,8 +294,13 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_musl
|
||||
env:
|
||||
@@ -309,6 +350,11 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
targets:
|
||||
- linux_mips
|
||||
- linux_mips_softfloat
|
||||
@@ -347,6 +393,49 @@ builds:
|
||||
- android_arm64
|
||||
- android_386
|
||||
- android_amd64
|
||||
- id: openwrt
|
||||
<<: *template
|
||||
hooks:
|
||||
post:
|
||||
- cmd: bash .github/build_openwrt_packages.sh "{{ .Version }}" "{{ .Target }}" "{{ .Path }}"
|
||||
targets:
|
||||
- linux_amd64_v1
|
||||
- linux_arm64
|
||||
- linux_386
|
||||
- linux_arm_7
|
||||
- linux_arm_6
|
||||
- linux_riscv64
|
||||
- linux_loong64
|
||||
- id: openwrt-mips
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
hooks:
|
||||
post:
|
||||
- cmd: bash .github/build_openwrt_packages.sh "{{ .Version }}" "{{ .Target }}" "{{ .Path }}"
|
||||
targets:
|
||||
- linux_arm_5
|
||||
- linux_mips_softfloat
|
||||
- linux_mips64_softfloat
|
||||
- linux_mipsle_softfloat
|
||||
- linux_mipsle_hardfloat
|
||||
- linux_mips64le
|
||||
upx:
|
||||
- enabled: true
|
||||
ids:
|
||||
@@ -372,16 +461,6 @@ archives:
|
||||
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 }}'
|
||||
- id: archive-naive-glibc
|
||||
<<: *template
|
||||
builds:
|
||||
- naive-glibc
|
||||
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 }}-glibc'
|
||||
- id: archive-naive-musl
|
||||
<<: *template
|
||||
builds:
|
||||
- naive-musl
|
||||
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 }}-musl'
|
||||
- id: archive-naive-purego-linux-amd64
|
||||
<<: *template
|
||||
builds:
|
||||
@@ -418,11 +497,16 @@ archives:
|
||||
- src: dist/naive-purego-windows-arm64_*/libcronet*
|
||||
strip_parent: true
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-purego'
|
||||
- id: archive-legacy
|
||||
- id: archive-naive-glibc
|
||||
<<: *template
|
||||
builds:
|
||||
- legacy
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-legacy'
|
||||
- naive-glibc
|
||||
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 }}-glibc'
|
||||
- id: archive-naive-musl
|
||||
<<: *template
|
||||
builds:
|
||||
- naive-musl
|
||||
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 }}-musl'
|
||||
- id: archive-compressed
|
||||
<<: *template
|
||||
builds:
|
||||
@@ -430,6 +514,12 @@ archives:
|
||||
- compressed-mips
|
||||
- compressed-android
|
||||
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 }}-compressed'
|
||||
- id: archive-openwrt
|
||||
<<: *template
|
||||
builds:
|
||||
- openwrt
|
||||
- openwrt-mips
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-openwrt-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}-{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||
source:
|
||||
enabled: false
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}.source'
|
||||
@@ -455,5 +545,5 @@ release:
|
||||
- archive-naive-purego-windows-amd64
|
||||
- archive-naive-purego-windows-arm64
|
||||
- archive-compressed
|
||||
- package
|
||||
- archive-openwrt
|
||||
skip_upload: true
|
||||
|
||||
6
Makefile
6
Makefile
@@ -27,7 +27,6 @@ CRONET_GO_PATH ?= $(shell pwd)/cronet-go
|
||||
.PHONY: test release docs build
|
||||
|
||||
build:
|
||||
export GOTOOLCHAIN=local && \
|
||||
go build $(MAIN_PARAMS) $(MAIN)
|
||||
|
||||
build_admin_panel:
|
||||
@@ -94,6 +93,8 @@ release: build_admin_panel build_naive
|
||||
mkdir dist/release
|
||||
mv dist/*.tar.gz \
|
||||
dist/*.zip \
|
||||
dist/*.ipk \
|
||||
dist/*.apk \
|
||||
dist/release
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
||||
./codeberg-release.sh --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
||||
@@ -123,12 +124,11 @@ build_android:
|
||||
upload_android:
|
||||
mkdir -p dist/release_android
|
||||
cp ../sing-box-for-android/app/build/outputs/apk/other/release/*.apk dist/release_android
|
||||
cp ../sing-box-for-android/app/build/outputs/apk/otherLegacy/release/*.apk dist/release_android
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
|
||||
./codeberg-release.sh --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
|
||||
rm -rf dist/release_android
|
||||
|
||||
release_android: lib_android update_android_version build_android
|
||||
release_android: lib_android update_android_version build_android upload_android
|
||||
|
||||
publish_android:
|
||||
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop
|
||||
|
||||
@@ -3,16 +3,21 @@
|
||||
[](LICENSE)
|
||||
[](go.mod)
|
||||
[](https://codeberg.org/shtorm-7/sing-box-extended)
|
||||
[](https://t.me/sing_box_extended)
|
||||
|
||||
Sing-box with extended features.
|
||||
|
||||
## 🔥 Features
|
||||
|
||||
### Outbounds
|
||||
### Protocols
|
||||
- **WARP** — Cloudflare WARP integration through WireGuard
|
||||
- **MASQUE** — Cloudflare MASQUE proxy over QUIC / HTTP-2
|
||||
- **MTProxy** — Telegram MTProxy server with FakeTLS and domain fronting
|
||||
- **Mieru** — Secure, hard to classify, hard to probe network protocol
|
||||
- **OpenVPN** — OpenVPN client with tls-auth, tls-crypt and tls-crypt-v2 support
|
||||
- **TrustTunnel** — AdGuard's obfuscated VPN protocol, indistinguishable from HTTPS traffic
|
||||
- **Sudoku** — Traffic obfuscation protocol based on 4×4 Sudoku puzzles with low-entropy fingerprints
|
||||
- **SSH** — SSH client and server with certificate authentication and upstream fallback
|
||||
- **VPN** — Routed tunnel over any TCP sing-box protocol
|
||||
- **Bond** — Link aggregation for increasing throughput
|
||||
- **Fallback** — Outbound group with priority-based switching
|
||||
|
||||
@@ -233,7 +233,7 @@ func (m *Manager) Remove(tag string) error {
|
||||
if m.defaultOutbound == outbound {
|
||||
if len(m.outbounds) > 0 {
|
||||
m.defaultOutbound = m.outbounds[0]
|
||||
m.logger.Info("updated default outbound to ", m.defaultOutbound.Tag())
|
||||
m.logger.Notice("updated default outbound to ", m.defaultOutbound.Tag())
|
||||
} else {
|
||||
m.defaultOutbound = nil
|
||||
}
|
||||
@@ -303,7 +303,7 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
|
||||
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) {
|
||||
m.defaultOutbound = outbound
|
||||
if m.started {
|
||||
m.logger.Info("updated default outbound to ", outbound.Tag())
|
||||
m.logger.Notice("updated default outbound to ", outbound.Tag())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -3,6 +3,8 @@ package provider
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -34,10 +36,11 @@ type Adapter struct {
|
||||
callbackAccess sync.Mutex
|
||||
callbacks list.List[adapter.ProviderUpdateCallback]
|
||||
|
||||
link string
|
||||
enabled bool
|
||||
timeout time.Duration
|
||||
interval time.Duration
|
||||
link string
|
||||
enabled bool
|
||||
removeEmojis bool
|
||||
timeout time.Duration
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
func NewAdapter(ctx context.Context, router adapter.Router, outbound adapter.OutboundManager, logFactory log.Factory, logger log.ContextLogger, providerTag string, providerType string, options option.ProviderHealthCheckOptions) Adapter {
|
||||
@@ -68,6 +71,10 @@ func NewAdapter(ctx context.Context, router adapter.Router, outbound adapter.Out
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) SetRemoveEmojis(remove bool) {
|
||||
a.removeEmojis = remove
|
||||
}
|
||||
|
||||
func (a *Adapter) Start() error {
|
||||
a.history = service.FromContext[adapter.URLTestHistoryStorage](a.ctx)
|
||||
if a.history == nil {
|
||||
@@ -102,6 +109,10 @@ func (a *Adapter) Outbound(tag string) (adapter.Outbound, bool) {
|
||||
}
|
||||
|
||||
func (a *Adapter) UpdateOutbounds(oldOpts []option.Outbound, newOpts []option.Outbound) {
|
||||
if a.removeEmojis {
|
||||
removeEmojisFromTags(newOpts)
|
||||
}
|
||||
uniquifyTags(newOpts)
|
||||
a.removeUseless(newOpts)
|
||||
var (
|
||||
oldOptByTag = make(map[string]option.Outbound)
|
||||
@@ -265,3 +276,34 @@ func (a *Adapter) removeUseless(newOpts []option.Outbound) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func uniquifyTags(opts []option.Outbound) {
|
||||
count := make(map[string]int)
|
||||
for i, opt := range opts {
|
||||
count[opt.Tag]++
|
||||
if count[opt.Tag] > 1 {
|
||||
opts[i].Tag = F.ToString(opt.Tag, " #", count[opt.Tag])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeEmojisFromTags(opts []option.Outbound) {
|
||||
for i, opt := range opts {
|
||||
cleaned := flagRegex.ReplaceAllStringFunc(opt.Tag, flagToCountryCode)
|
||||
cleaned = emojiRegex.ReplaceAllString(cleaned, "")
|
||||
cleaned = multiSpaceRegex.ReplaceAllString(cleaned, " ")
|
||||
opts[i].Tag = strings.TrimSpace(cleaned)
|
||||
}
|
||||
}
|
||||
|
||||
func flagToCountryCode(flag string) string {
|
||||
runes := []rune(flag)
|
||||
if len(runes) == 2 {
|
||||
return string(rune(runes[0]-0x1F1E6+'A')) + string(rune(runes[1]-0x1F1E6+'A')) + " "
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var flagRegex = regexp.MustCompile(`[\x{1F1E6}-\x{1F1FF}]{2}`)
|
||||
var emojiRegex = regexp.MustCompile(`[\x{1F1E0}-\x{1F1FF}\x{1F300}-\x{1F9FF}\x{2600}-\x{27BF}\x{FE00}-\x{FE0F}\x{200D}]+`)
|
||||
var multiSpaceRegex = regexp.MustCompile(`\s{2,}`)
|
||||
|
||||
45
adapter/provider/adapter_test.go
Normal file
45
adapter/provider/adapter_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
func TestFlagToCountryCodeAllFlags(t *testing.T) {
|
||||
for first := 'A'; first <= 'Z'; first++ {
|
||||
for second := 'A'; second <= 'Z'; second++ {
|
||||
flag := string(rune(0x1F1E6+(first-'A'))) + string(rune(0x1F1E6+(second-'A')))
|
||||
expected := string(first) + string(second)
|
||||
result := flagToCountryCode(flag)
|
||||
// flagToCountryCode appends a space
|
||||
if result != expected+" " {
|
||||
t.Errorf("flagToCountryCode(%q) = %q, want %q", expected, result, expected+" ")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveEmojisFromTags(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"🇺🇸 United States", "US United States"},
|
||||
{"🇷🇺 Россия", "RU Россия"},
|
||||
{"🇩🇪 Germany 🚀", "DE Germany"},
|
||||
{"🇫🇷🇬🇧 France-UK", "FR GB France-UK"},
|
||||
{"No emojis here", "No emojis here"},
|
||||
{"🌍 World", "World"},
|
||||
{"🇯🇵 Tokyo ⚡ Fast", "JP Tokyo Fast"},
|
||||
{"Germany 🇩🇪", "Germany DE"},
|
||||
{"Server 🇺🇸 Node", "Server US Node"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
opts := []option.Outbound{{Tag: tt.input}}
|
||||
removeEmojisFromTags(opts)
|
||||
if opts[0].Tag != tt.expected {
|
||||
t.Errorf("removeEmojisFromTags(%q) = %q, want %q", tt.input, opts[0].Tag, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
4
box.go
4
box.go
@@ -458,7 +458,7 @@ func (s *Box) PreStart() error {
|
||||
s.Close()
|
||||
return err
|
||||
}
|
||||
s.logger.Info("sing-box pre-started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
s.logger.Notice("sing-box pre-started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -477,7 +477,7 @@ func (s *Box) Start() error {
|
||||
s.Close()
|
||||
return err
|
||||
}
|
||||
s.logger.Info("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
s.logger.Notice("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ func init() {
|
||||
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0")
|
||||
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0")
|
||||
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_masque", "with_mtproxy", "with_utls", "with_naive_outbound", "with_clash_api", "badlinkname", "tfogo_checklinkname0")
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_masque", "with_mtproxy", "with_trusttunnel", "with_openvpn", "with_sudoku", "with_utls", "with_naive_outbound", "with_clash_api", "badlinkname", "tfogo_checklinkname0")
|
||||
darwinTags = append(darwinTags, "with_dhcp", "grpcnotrace")
|
||||
// memcTags = append(memcTags, "with_tailscale")
|
||||
sharedTags = append(sharedTags, "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird")
|
||||
|
||||
@@ -179,6 +179,7 @@ func run() error {
|
||||
for {
|
||||
osSignal := <-osSignals
|
||||
if osSignal == syscall.SIGHUP {
|
||||
log.Notice("received SIGHUP, reloading...")
|
||||
err = check()
|
||||
if err != nil {
|
||||
log.Error(E.Cause(err, "reload service"))
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
package byteformats
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
var (
|
||||
unitNames = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
|
||||
iUnitNames = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
||||
kUnitNames = []string{"kB", "MB", "GB", "TB", "PB", "EB"}
|
||||
kiUnitNames = []string{"KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
||||
)
|
||||
|
||||
func formatBytes(s uint64, base float64, sizes []string) string {
|
||||
if s < 10 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
e := math.Floor(logn(float64(s), base))
|
||||
suffix := sizes[int(e)]
|
||||
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
}
|
||||
|
||||
func formatKBytes(s uint64, base float64, sizes []string) string {
|
||||
if s == 0 {
|
||||
return fmt.Sprintf("0 %s", sizes[0])
|
||||
}
|
||||
e := math.Floor(logn(float64(s), base))
|
||||
if e < 1 {
|
||||
e = 1
|
||||
}
|
||||
suffix := sizes[int(e)-1]
|
||||
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
}
|
||||
|
||||
func logn(n, b float64) float64 {
|
||||
return math.Log(n) / math.Log(b)
|
||||
}
|
||||
|
||||
func FormatBytes(s uint64) string {
|
||||
return formatBytes(s, 1000, unitNames)
|
||||
}
|
||||
|
||||
func FormatMemoryBytes(s uint64) string {
|
||||
return formatBytes(s, 1024, unitNames)
|
||||
}
|
||||
|
||||
func FormatIBytes(s uint64) string {
|
||||
return formatBytes(s, 1024, iUnitNames)
|
||||
}
|
||||
|
||||
func FormatKBytes(s uint64) string {
|
||||
return formatKBytes(s, 1000, kUnitNames)
|
||||
}
|
||||
|
||||
func FormatMemoryKBytes(s uint64) string {
|
||||
return formatKBytes(s, 1024, kUnitNames)
|
||||
}
|
||||
|
||||
func FormatKIBytes(s uint64) string {
|
||||
return formatKBytes(s, 1024, kiUnitNames)
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
package byteformats
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
Byte = 1 << (iota * 10)
|
||||
KiByte
|
||||
MiByte
|
||||
GiByte
|
||||
TiByte
|
||||
PiByte
|
||||
EiByte
|
||||
)
|
||||
|
||||
const (
|
||||
KByte = Byte * 1000
|
||||
MByte = KByte * 1000
|
||||
GByte = MByte * 1000
|
||||
TByte = GByte * 1000
|
||||
PByte = TByte * 1000
|
||||
EByte = PByte * 1000
|
||||
)
|
||||
|
||||
var unitValueTable = map[string]uint64{
|
||||
"b": Byte,
|
||||
"k": KByte,
|
||||
"kb": KByte,
|
||||
"ki": KiByte,
|
||||
"kib": KiByte,
|
||||
"m": MByte,
|
||||
"mb": MByte,
|
||||
"mi": MiByte,
|
||||
"mib": MiByte,
|
||||
"g": GByte,
|
||||
"gb": GByte,
|
||||
"gi": GiByte,
|
||||
"gib": GiByte,
|
||||
"t": TByte,
|
||||
"tb": TByte,
|
||||
"ti": TiByte,
|
||||
"tib": TiByte,
|
||||
"p": PByte,
|
||||
"pb": PByte,
|
||||
"pi": PiByte,
|
||||
"pib": PiByte,
|
||||
"e": EByte,
|
||||
"eb": EByte,
|
||||
"ei": EiByte,
|
||||
"eib": EiByte,
|
||||
}
|
||||
|
||||
var memoryUnitValueTable = map[string]uint64{
|
||||
"b": Byte,
|
||||
"k": KiByte,
|
||||
"kb": KiByte,
|
||||
"m": MiByte,
|
||||
"mb": MiByte,
|
||||
"g": GiByte,
|
||||
"gb": GiByte,
|
||||
"t": TiByte,
|
||||
"tb": TiByte,
|
||||
"p": PiByte,
|
||||
"pb": PiByte,
|
||||
"e": EiByte,
|
||||
"eb": EiByte,
|
||||
}
|
||||
|
||||
var networkUnitValueTable = map[string]uint64{
|
||||
"Bps": Byte,
|
||||
"Kbps": KByte / 8,
|
||||
"KBps": KByte,
|
||||
"Mbps": MByte / 8,
|
||||
"MBps": MByte,
|
||||
"Gbps": GByte / 8,
|
||||
"GBps": GByte,
|
||||
"Tbps": TByte / 8,
|
||||
"TBps": TByte,
|
||||
"Pbps": PByte / 8,
|
||||
"PBps": PByte,
|
||||
"Ebps": EByte / 8,
|
||||
"EBps": EByte,
|
||||
}
|
||||
|
||||
type rawBytes struct {
|
||||
value uint64
|
||||
unit string
|
||||
unitValue uint64
|
||||
}
|
||||
|
||||
func (b rawBytes) MarshalJSON() ([]byte, error) {
|
||||
if b.unit == "" {
|
||||
return json.Marshal(b.value)
|
||||
}
|
||||
return json.Marshal(strconv.FormatUint(b.value/b.unitValue, 10) + b.unit)
|
||||
}
|
||||
|
||||
func parseUnit(b *rawBytes, unitTable map[string]uint64, caseSensitive bool, bytes []byte) error {
|
||||
var intValue int64
|
||||
err := json.Unmarshal(bytes, &intValue)
|
||||
if err == nil {
|
||||
b.value = uint64(intValue)
|
||||
b.unit = ""
|
||||
b.unitValue = 1
|
||||
return nil
|
||||
}
|
||||
var stringValue string
|
||||
err = json.Unmarshal(bytes, &stringValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(stringValue) == "" {
|
||||
b.value = 0
|
||||
b.unit = ""
|
||||
b.unitValue = 1
|
||||
return nil
|
||||
}
|
||||
unitIndex := 0
|
||||
for i, c := range stringValue {
|
||||
if c < '0' || c > '9' {
|
||||
unitIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if unitIndex == 0 {
|
||||
return fmt.Errorf("invalid format: %s", stringValue)
|
||||
}
|
||||
value, err := strconv.ParseUint(stringValue[:unitIndex], 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse %s: %w", stringValue[:unitIndex], err)
|
||||
}
|
||||
rawUnit := stringValue[unitIndex:]
|
||||
var unit string
|
||||
if caseSensitive {
|
||||
unit = strings.TrimSpace(rawUnit)
|
||||
} else {
|
||||
unit = strings.TrimSpace(strings.ToLower(rawUnit))
|
||||
}
|
||||
unitValue, loaded := unitTable[unit]
|
||||
if !loaded {
|
||||
return fmt.Errorf("unsupported unit: %s", rawUnit)
|
||||
}
|
||||
b.value = value * unitValue
|
||||
b.unit = rawUnit
|
||||
b.unitValue = unitValue
|
||||
return nil
|
||||
}
|
||||
|
||||
type Bytes struct {
|
||||
rawBytes
|
||||
}
|
||||
|
||||
func (b *Bytes) Value() uint64 {
|
||||
if b == nil {
|
||||
return 0
|
||||
}
|
||||
return b.value
|
||||
}
|
||||
|
||||
func (b *Bytes) UnmarshalJSON(bytes []byte) error {
|
||||
return parseUnit(&b.rawBytes, unitValueTable, false, bytes)
|
||||
}
|
||||
|
||||
type MemoryBytes struct {
|
||||
rawBytes
|
||||
}
|
||||
|
||||
func (m *MemoryBytes) Value() uint64 {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
return m.value
|
||||
}
|
||||
|
||||
func (m *MemoryBytes) UnmarshalJSON(bytes []byte) error {
|
||||
return parseUnit(&m.rawBytes, memoryUnitValueTable, false, bytes)
|
||||
}
|
||||
|
||||
type NetworkBytes struct {
|
||||
rawBytes
|
||||
}
|
||||
|
||||
func (n *NetworkBytes) Value() uint64 {
|
||||
if n == nil {
|
||||
return 0
|
||||
}
|
||||
return n.value
|
||||
}
|
||||
|
||||
func (n *NetworkBytes) UnmarshalJSON(bytes []byte) error {
|
||||
return parseUnit(&n.rawBytes, networkUnitValueTable, true, bytes)
|
||||
}
|
||||
|
||||
type NetworkBytesCompat struct {
|
||||
rawBytes
|
||||
}
|
||||
|
||||
func (n *NetworkBytesCompat) Value() uint64 {
|
||||
if n == nil {
|
||||
return 0
|
||||
}
|
||||
return n.value
|
||||
}
|
||||
|
||||
func (n *NetworkBytesCompat) UnmarshalJSON(bytes []byte) error {
|
||||
err := parseUnit(&n.rawBytes, networkUnitValueTable, true, bytes)
|
||||
if err != nil {
|
||||
newErr := parseUnit(&n.rawBytes, unitValueTable, false, bytes)
|
||||
if newErr == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package byteformats_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/common/byteformats"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNetworkBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMap := map[string]uint64{
|
||||
"1 Bps": byteformats.Byte,
|
||||
"1 Kbps": byteformats.KByte / 8,
|
||||
"1 KBps": byteformats.KByte,
|
||||
"1 Mbps": byteformats.MByte / 8,
|
||||
"1 MBps": byteformats.MByte,
|
||||
"1 Gbps": byteformats.GByte / 8,
|
||||
"1 GBps": byteformats.GByte,
|
||||
"1 Tbps": byteformats.TByte / 8,
|
||||
"1 TBps": byteformats.TByte,
|
||||
"1 Pbps": byteformats.PByte / 8,
|
||||
"1 PBps": byteformats.PByte,
|
||||
"1k": byteformats.KByte,
|
||||
"1m": byteformats.MByte,
|
||||
}
|
||||
for k, v := range testMap {
|
||||
var nb byteformats.NetworkBytesCompat
|
||||
require.NoError(t, json.Unmarshal([]byte("\""+k+"\""), &nb))
|
||||
require.Equal(t, v, nb.Value())
|
||||
b, err := json.Marshal(nb)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "\""+k+"\"", string(b))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemoryBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMap := map[string]uint64{
|
||||
"1 B": byteformats.Byte,
|
||||
"1 KB": byteformats.KiByte,
|
||||
"1 MB": byteformats.MiByte,
|
||||
"1 GB": byteformats.GiByte,
|
||||
"1 TB": byteformats.TiByte,
|
||||
"1 PB": byteformats.PiByte,
|
||||
}
|
||||
for k, v := range testMap {
|
||||
var mb byteformats.MemoryBytes
|
||||
require.NoError(t, json.Unmarshal([]byte("\""+k+"\""), &mb))
|
||||
require.Equal(t, v, mb.Value())
|
||||
b, err := json.Marshal(mb)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "\""+k+"\"", string(b))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMap := map[string]uint64{
|
||||
"1 B": byteformats.Byte,
|
||||
"1 KB": byteformats.KByte,
|
||||
"1 KiB": byteformats.KiByte,
|
||||
"1 MB": byteformats.MByte,
|
||||
"1 MiB": byteformats.MiByte,
|
||||
"1 GB": byteformats.GByte,
|
||||
"1 GiB": byteformats.GiByte,
|
||||
"1 TB": byteformats.TByte,
|
||||
"1 TiB": byteformats.TiByte,
|
||||
"1 PB": byteformats.PByte,
|
||||
"1 PiB": byteformats.PiByte,
|
||||
"1 EB": byteformats.EByte,
|
||||
"1 EiB": byteformats.EiByte,
|
||||
"1k": byteformats.KByte,
|
||||
"1m": byteformats.MByte,
|
||||
"1g": byteformats.GByte,
|
||||
"1t": byteformats.TByte,
|
||||
"1p": byteformats.PByte,
|
||||
"1e": byteformats.EByte,
|
||||
"1K": byteformats.KByte,
|
||||
"1M": byteformats.MByte,
|
||||
"1G": byteformats.GByte,
|
||||
"1T": byteformats.TByte,
|
||||
"1P": byteformats.PByte,
|
||||
"1E": byteformats.EByte,
|
||||
"1Ki": byteformats.KiByte,
|
||||
"1Mi": byteformats.MiByte,
|
||||
"1Gi": byteformats.GiByte,
|
||||
"1Ti": byteformats.TiByte,
|
||||
"1Pi": byteformats.PiByte,
|
||||
"1Ei": byteformats.EiByte,
|
||||
"1KiB": byteformats.KiByte,
|
||||
"1MiB": byteformats.MiByte,
|
||||
"1GiB": byteformats.GiByte,
|
||||
"1TiB": byteformats.TiByte,
|
||||
"1PiB": byteformats.PiByte,
|
||||
"1EiB": byteformats.EiByte,
|
||||
"1kB": byteformats.KByte,
|
||||
"1mB": byteformats.MByte,
|
||||
"1gB": byteformats.GByte,
|
||||
"1tB": byteformats.TByte,
|
||||
"1pB": byteformats.PByte,
|
||||
"1eB": byteformats.EByte,
|
||||
}
|
||||
for k, v := range testMap {
|
||||
var mb byteformats.Bytes
|
||||
require.NoError(t, json.Unmarshal([]byte("\""+k+"\""), &mb))
|
||||
require.Equal(t, v, mb.Value())
|
||||
b, err := json.Marshal(mb)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "\""+k+"\"", string(b))
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.logger.Info("tcp server started at ", tcpListener.Addr())
|
||||
l.logger.Notice("tcp server started at ", tcpListener.Addr())
|
||||
l.tcpListener = tcpListener
|
||||
return tcpListener, err
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
|
||||
}
|
||||
l.udpConn = udpConn.(*net.UDPConn)
|
||||
l.udpAddr = bindAddr
|
||||
l.logger.Info("udp server started at ", udpConn.LocalAddr())
|
||||
l.logger.Notice("udp server started at ", udpConn.LocalAddr())
|
||||
return udpConn, err
|
||||
}
|
||||
|
||||
|
||||
38
common/onclose/conn.go
Normal file
38
common/onclose/conn.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package onclose
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type CloseHandlerFunc = func()
|
||||
|
||||
type Conn struct {
|
||||
net.Conn
|
||||
onClose func()
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func NewConn(conn net.Conn, onClose func()) *Conn {
|
||||
return &Conn{Conn: conn, onClose: onClose}
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
c.once.Do(c.onClose)
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
type PacketConn struct {
|
||||
net.PacketConn
|
||||
onClose func()
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func NewPacketConn(conn net.PacketConn, onClose func()) *PacketConn {
|
||||
return &PacketConn{PacketConn: conn, onClose: onClose}
|
||||
}
|
||||
|
||||
func (c *PacketConn) Close() error {
|
||||
c.once.Do(c.onClose)
|
||||
return c.PacketConn.Close()
|
||||
}
|
||||
135
common/tls/openvpn_client.go
Normal file
135
common/tls/openvpn_client.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
func NewOpenVPNClient(ctx context.Context, logger logger.ContextLogger, options option.OpenVPNTLSOptions) (Config, error) {
|
||||
ca := options.CA
|
||||
if ca == "" && options.CAPath != "" {
|
||||
data, err := os.ReadFile(options.CAPath)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read ca_path")
|
||||
}
|
||||
ca = string(data)
|
||||
}
|
||||
certificate := options.Certificate
|
||||
if certificate == "" && options.CertificatePath != "" {
|
||||
data, err := os.ReadFile(options.CertificatePath)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read certificate_path")
|
||||
}
|
||||
certificate = string(data)
|
||||
}
|
||||
key := options.Key
|
||||
if key == "" && options.KeyPath != "" {
|
||||
data, err := os.ReadFile(options.KeyPath)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read key_path")
|
||||
}
|
||||
key = string(data)
|
||||
}
|
||||
if strings.TrimSpace(ca) == "" {
|
||||
return nil, E.New("openvpn: missing ca certificate")
|
||||
}
|
||||
if block, _ := pem.Decode([]byte(ca)); block == nil {
|
||||
return nil, E.New("openvpn: ca is not valid PEM")
|
||||
}
|
||||
hasCert := strings.TrimSpace(certificate) != "" || strings.TrimSpace(key) != ""
|
||||
if hasCert {
|
||||
if strings.TrimSpace(certificate) == "" || strings.TrimSpace(key) == "" {
|
||||
return nil, E.New("openvpn: certificate and key must both be set")
|
||||
}
|
||||
if block, _ := pem.Decode([]byte(certificate)); block == nil {
|
||||
return nil, E.New("openvpn: certificate is not valid PEM")
|
||||
}
|
||||
if block, _ := pem.Decode([]byte(key)); block == nil {
|
||||
return nil, E.New("openvpn: key is not valid PEM")
|
||||
}
|
||||
}
|
||||
roots := x509.NewCertPool()
|
||||
if !roots.AppendCertsFromPEM([]byte(ca)) {
|
||||
return nil, E.New("openvpn: failed to parse ca certificate")
|
||||
}
|
||||
var tlsConfig tls.Config
|
||||
tlsConfig.RootCAs = roots
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
if options.CipherSuites != nil {
|
||||
find:
|
||||
for _, cipherSuite := range options.CipherSuites {
|
||||
for _, tlsCipherSuite := range tls.CipherSuites() {
|
||||
if cipherSuite == tlsCipherSuite.Name {
|
||||
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID)
|
||||
continue find
|
||||
}
|
||||
}
|
||||
return nil, E.New("unknown cipher_suite: ", cipherSuite)
|
||||
}
|
||||
}
|
||||
tlsConfig.VerifyConnection = func(cs tls.ConnectionState) error {
|
||||
if len(cs.PeerCertificates) == 0 {
|
||||
return E.New("openvpn: server did not provide certificate")
|
||||
}
|
||||
cert := cs.PeerCertificates[0]
|
||||
intermediates := x509.NewCertPool()
|
||||
for _, intermediate := range cs.PeerCertificates[1:] {
|
||||
intermediates.AddCert(intermediate)
|
||||
}
|
||||
_, err := cert.Verify(x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
Intermediates: intermediates,
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if options.VerifyX509Name != "" {
|
||||
cn := cert.Subject.CommonName
|
||||
switch options.VerifyX509NameMode {
|
||||
case "name-prefix":
|
||||
if !strings.HasPrefix(cn, options.VerifyX509Name) {
|
||||
return E.New("openvpn: server CN ", cn, " does not match prefix ", options.VerifyX509Name)
|
||||
}
|
||||
case "name-suffix":
|
||||
if !strings.HasSuffix(cn, options.VerifyX509Name) {
|
||||
return E.New("openvpn: server CN ", cn, " does not match suffix ", options.VerifyX509Name)
|
||||
}
|
||||
default:
|
||||
if cn != options.VerifyX509Name {
|
||||
return E.New("openvpn: server CN ", cn, " does not match ", options.VerifyX509Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if hasCert {
|
||||
cert, err := tls.X509KeyPair([]byte(certificate), []byte(key))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "openvpn: parse client certificate/key")
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
var config Config = &STDClientConfig{ctx, &tlsConfig, false, 0, false}
|
||||
if options.KernelRx || options.KernelTx {
|
||||
if !C.IsLinux {
|
||||
return nil, E.New("kTLS is only supported on Linux")
|
||||
}
|
||||
config = &KTLSClientConfig{
|
||||
Config: config,
|
||||
logger: logger,
|
||||
kernelTx: options.KernelTx,
|
||||
kernelRx: options.KernelRx,
|
||||
}
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
@@ -164,7 +164,7 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
|
||||
config.Certificates = []tls.Certificate{keyPair}
|
||||
c.config = config
|
||||
c.access.Unlock()
|
||||
c.logger.Info("reloaded TLS certificate")
|
||||
c.logger.Notice("reloaded TLS certificate")
|
||||
} else if common.Contains(c.clientCertificatePath, path) {
|
||||
clientCertificateCA := x509.NewCertPool()
|
||||
var reloaded bool
|
||||
@@ -188,7 +188,7 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
|
||||
config.ClientCAs = clientCertificateCA
|
||||
c.config = config
|
||||
c.access.Unlock()
|
||||
c.logger.Info("reloaded client certificates")
|
||||
c.logger.Notice("reloaded client certificates")
|
||||
} else if path == c.echKeyPath {
|
||||
echKey, err := os.ReadFile(c.echKeyPath)
|
||||
if err != nil {
|
||||
@@ -198,7 +198,7 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.logger.Info("reloaded ECH keys")
|
||||
c.logger.Notice("reloaded ECH keys")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -13,10 +13,12 @@ const (
|
||||
TypeShadowsocks = "shadowsocks"
|
||||
TypeVMess = "vmess"
|
||||
TypeTrojan = "trojan"
|
||||
TypeTrustTunnel = "trusttunnel"
|
||||
TypeNaive = "naive"
|
||||
TypeWireGuard = "wireguard"
|
||||
TypeWARP = "warp"
|
||||
TypeMASQUE = "masque"
|
||||
TypeOpenVPN = "openvpn"
|
||||
TypeMTProxy = "mtproxy"
|
||||
TypeParser = "parser"
|
||||
TypeHysteria = "hysteria"
|
||||
@@ -25,6 +27,7 @@ const (
|
||||
TypeShadowTLS = "shadowtls"
|
||||
TypeMieru = "mieru"
|
||||
TypeAnyTLS = "anytls"
|
||||
TypeSudoku = "sudoku"
|
||||
TypeShadowsocksR = "shadowsocksr"
|
||||
TypeVLESS = "vless"
|
||||
TypeTUIC = "tuic"
|
||||
@@ -84,6 +87,8 @@ func ProxyDisplayName(proxyType string) string {
|
||||
return "VMess"
|
||||
case TypeTrojan:
|
||||
return "Trojan"
|
||||
case TypeTrustTunnel:
|
||||
return "TrustTunnel"
|
||||
case TypeNaive:
|
||||
return "Naive"
|
||||
case TypeWireGuard:
|
||||
@@ -92,6 +97,8 @@ func ProxyDisplayName(proxyType string) string {
|
||||
return "WARP"
|
||||
case TypeMASQUE:
|
||||
return "MASQUE"
|
||||
case TypeOpenVPN:
|
||||
return "OpenVPN"
|
||||
case TypeMTProxy:
|
||||
return "MTProxy"
|
||||
case TypeParser:
|
||||
@@ -120,6 +127,8 @@ func ProxyDisplayName(proxyType string) string {
|
||||
return "Mieru"
|
||||
case TypeAnyTLS:
|
||||
return "AnyTLS"
|
||||
case TypeSudoku:
|
||||
return "Sudoku"
|
||||
case TypeFallback:
|
||||
return "Fallback"
|
||||
case TypeTailscale:
|
||||
|
||||
@@ -20,13 +20,14 @@ const (
|
||||
type LogLevel int32
|
||||
|
||||
const (
|
||||
LogLevel_PANIC LogLevel = 0
|
||||
LogLevel_FATAL LogLevel = 1
|
||||
LogLevel_ERROR LogLevel = 2
|
||||
LogLevel_WARN LogLevel = 3
|
||||
LogLevel_INFO LogLevel = 4
|
||||
LogLevel_DEBUG LogLevel = 5
|
||||
LogLevel_TRACE LogLevel = 6
|
||||
LogLevel_PANIC LogLevel = 0
|
||||
LogLevel_FATAL LogLevel = 1
|
||||
LogLevel_ERROR LogLevel = 2
|
||||
LogLevel_WARN LogLevel = 3
|
||||
LogLevel_NOTICE LogLevel = 4
|
||||
LogLevel_INFO LogLevel = 5
|
||||
LogLevel_DEBUG LogLevel = 6
|
||||
LogLevel_TRACE LogLevel = 7
|
||||
)
|
||||
|
||||
// Enum value maps for LogLevel.
|
||||
@@ -36,18 +37,20 @@ var (
|
||||
1: "FATAL",
|
||||
2: "ERROR",
|
||||
3: "WARN",
|
||||
4: "INFO",
|
||||
5: "DEBUG",
|
||||
6: "TRACE",
|
||||
4: "NOTICE",
|
||||
5: "INFO",
|
||||
6: "DEBUG",
|
||||
7: "TRACE",
|
||||
}
|
||||
LogLevel_value = map[string]int32{
|
||||
"PANIC": 0,
|
||||
"FATAL": 1,
|
||||
"ERROR": 2,
|
||||
"WARN": 3,
|
||||
"INFO": 4,
|
||||
"DEBUG": 5,
|
||||
"TRACE": 6,
|
||||
"PANIC": 0,
|
||||
"FATAL": 1,
|
||||
"ERROR": 2,
|
||||
"WARN": 3,
|
||||
"NOTICE": 4,
|
||||
"INFO": 5,
|
||||
"DEBUG": 6,
|
||||
"TRACE": 7,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -59,9 +59,10 @@ enum LogLevel {
|
||||
FATAL = 1;
|
||||
ERROR = 2;
|
||||
WARN = 3;
|
||||
INFO = 4;
|
||||
DEBUG = 5;
|
||||
TRACE = 6;
|
||||
NOTICE = 4;
|
||||
INFO = 5;
|
||||
DEBUG = 6;
|
||||
TRACE = 7;
|
||||
}
|
||||
|
||||
message Log {
|
||||
|
||||
@@ -186,7 +186,7 @@ func (t *Transport) updateServers() error {
|
||||
return E.Cause(err, "dhcp: prepare interface")
|
||||
}
|
||||
|
||||
t.logger.Info("dhcp: query DNS servers on ", iface.Name)
|
||||
t.logger.Notice("dhcp: query DNS servers on ", iface.Name)
|
||||
fetchCtx, cancel := context.WithTimeout(t.ctx, C.DHCPTimeout)
|
||||
err = t.fetchServers0(fetchCtx, iface)
|
||||
cancel()
|
||||
@@ -303,7 +303,7 @@ func (t *Transport) recreateServers(iface *control.Interface, dhcpPacket *dhcpv4
|
||||
return M.SocksaddrFrom(M.AddrFromIP(it), 53)
|
||||
})
|
||||
if len(serverAddrs) > 0 && !slices.Equal(t.servers, serverAddrs) {
|
||||
t.logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, M.Socksaddr.String), ","), "], search: [", strings.Join(t.search, ","), "]")
|
||||
t.logger.Notice("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, M.Socksaddr.String), ","), "], search: [", strings.Join(t.search, ","), "]")
|
||||
}
|
||||
t.servers = serverAddrs
|
||||
return nil
|
||||
|
||||
@@ -217,7 +217,7 @@ func (m *TransportManager) Remove(tag string) error {
|
||||
return E.New("default server cannot be fakeip")
|
||||
}
|
||||
m.defaultTransport = nextTransport
|
||||
m.logger.Info("updated default server to ", m.defaultTransport.Tag())
|
||||
m.logger.Notice("updated default server to ", m.defaultTransport.Tag())
|
||||
} else {
|
||||
m.defaultTransport = nil
|
||||
}
|
||||
@@ -287,7 +287,7 @@ func (m *TransportManager) Create(ctx context.Context, logger log.ContextLogger,
|
||||
}
|
||||
m.defaultTransport = transport
|
||||
if m.started {
|
||||
m.logger.Info("updated default server to ", transport.Tag())
|
||||
m.logger.Notice("updated default server to ", transport.Tag())
|
||||
}
|
||||
}
|
||||
if transport.Type() == C.DNSTypeFakeIP {
|
||||
|
||||
@@ -31,10 +31,11 @@
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
|
||||
| `vless` | [VLESS](./vless/) | TCP |
|
||||
| `anytls` | [AnyTLS](./anytls/) | TCP |
|
||||
| `mieru` | [Mieru](./mieru/) | :material-close: |
|
||||
| `tun` | [Tun](./tun/) | :material-close: |
|
||||
| `redirect` | [Redirect](./redirect/) | :material-close: |
|
||||
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
|
||||
|
||||
#### tag
|
||||
|
||||
The tag of the inbound.
|
||||
The tag of the inbound.
|
||||
|
||||
@@ -31,10 +31,11 @@
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
|
||||
| `vless` | [VLESS](./vless/) | TCP |
|
||||
| `anytls` | [AnyTLS](./anytls/) | TCP |
|
||||
| `mieru` | [Mieru](./mieru/) | :material-close: |
|
||||
| `tun` | [Tun](./tun/) | :material-close: |
|
||||
| `redirect` | [Redirect](./redirect/) | :material-close: |
|
||||
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
|
||||
|
||||
#### tag
|
||||
|
||||
入站的标签。
|
||||
入站的标签。
|
||||
|
||||
49
docs/configuration/inbound/mieru.md
Normal file
49
docs/configuration/inbound/mieru.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "mieru",
|
||||
"tag": "mieru-in",
|
||||
|
||||
... // Listen Fields
|
||||
|
||||
"transport": "TCP",
|
||||
"users": [
|
||||
{
|
||||
"name": "asdf",
|
||||
"password": "hjkl"
|
||||
}
|
||||
],
|
||||
"traffic_pattern": "GgQIARAK",
|
||||
}
|
||||
```
|
||||
|
||||
### Listen Fields
|
||||
|
||||
See [Listen Fields](/configuration/shared/listen/) for details.
|
||||
|
||||
### Fields
|
||||
|
||||
#### transport
|
||||
|
||||
==Required==
|
||||
|
||||
Transmission protocol. Allowed values are `TCP` and `UDP`.
|
||||
|
||||
#### users
|
||||
|
||||
==Required==
|
||||
|
||||
A list of mieru user name and password.
|
||||
|
||||
#### traffic_pattern
|
||||
|
||||
A base64 string to fine tune network behavior.
|
||||
|
||||
#### user_hint_is_mandatory
|
||||
|
||||
If proxy client doesn't sent user hint, proxy server will refuse the connection.
|
||||
49
docs/configuration/inbound/mieru.zh.md
Normal file
49
docs/configuration/inbound/mieru.zh.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "mieru",
|
||||
"tag": "mieru-in",
|
||||
|
||||
... // 监听字段
|
||||
|
||||
"transport": "TCP",
|
||||
"users": [
|
||||
{
|
||||
"name": "asdf",
|
||||
"password": "hjkl"
|
||||
}
|
||||
],
|
||||
"traffic_pattern": "GgQIARAK",
|
||||
}
|
||||
```
|
||||
|
||||
### 监听字段
|
||||
|
||||
参阅 [监听字段](/zh/configuration/shared/listen/)。
|
||||
|
||||
### 字段
|
||||
|
||||
#### transport
|
||||
|
||||
==必填==
|
||||
|
||||
通信协议。可设为 `TCP` 或 `UDP`。
|
||||
|
||||
#### users
|
||||
|
||||
==必填==
|
||||
|
||||
一组 mieru 用户名和密码。
|
||||
|
||||
#### traffic_pattern
|
||||
|
||||
一个 base64 字符串用于微调网络行为。
|
||||
|
||||
#### user_hint_is_mandatory
|
||||
|
||||
客户端若不发送用户提示,代理服务器将拒绝连接。
|
||||
@@ -32,6 +32,7 @@
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
||||
| `mieru` | [Mieru](./mieru/) |
|
||||
| `anytls` | [AnyTLS](./anytls/) |
|
||||
| `mieru` | [Mieru](./mieru/) |
|
||||
| `tor` | [Tor](./tor/) |
|
||||
| `ssh` | [SSH](./ssh/) |
|
||||
| `dns` | [DNS](./dns/) |
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
||||
| `mieru` | [Mieru](./mieru/) |
|
||||
| `anytls` | [AnyTLS](./anytls/) |
|
||||
| `mieru` | [Mieru](./mieru/) |
|
||||
| `tor` | [Tor](./tor/) |
|
||||
| `ssh` | [SSH](./ssh/) |
|
||||
| `dns` | [DNS](./dns/) |
|
||||
|
||||
@@ -19,6 +19,7 @@ icon: material/new-box
|
||||
"username": "asdf",
|
||||
"password": "hjkl",
|
||||
"multiplexing": "MULTIPLEXING_LOW",
|
||||
"traffic_pattern": "GgQIARAK",
|
||||
|
||||
... // Dial Fields
|
||||
}
|
||||
@@ -48,7 +49,7 @@ Must set at least one field between `server_port` and `server_ports`.
|
||||
|
||||
==Required==
|
||||
|
||||
Transmission protocol. The only allowed value is `TCP`.
|
||||
Transmission protocol. Allowed values are `TCP` and `UDP`.
|
||||
|
||||
#### username
|
||||
|
||||
@@ -66,6 +67,10 @@ mieru password.
|
||||
|
||||
Multiplexing level. Supported values are `MULTIPLEXING_OFF`, `MULTIPLEXING_LOW`, `MULTIPLEXING_MIDDLE`, `MULTIPLEXING_HIGH`. `MULTIPLEXING_OFF` disables multiplexing.
|
||||
|
||||
#### traffic_pattern
|
||||
|
||||
A base64 string to fine tune network behavior.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
|
||||
@@ -19,6 +19,7 @@ icon: material/new-box
|
||||
"username": "asdf",
|
||||
"password": "hjkl",
|
||||
"multiplexing": "MULTIPLEXING_LOW",
|
||||
"traffic_pattern": "GgQIARAK",
|
||||
|
||||
... // 拨号字段
|
||||
}
|
||||
@@ -48,7 +49,7 @@ icon: material/new-box
|
||||
|
||||
==必填==
|
||||
|
||||
通信协议。仅可设为 `TCP`。
|
||||
通信协议。可设为 `TCP` 或 `UDP`。
|
||||
|
||||
#### username
|
||||
|
||||
@@ -66,6 +67,10 @@ mieru 密码。
|
||||
|
||||
多路复用设置。可以设为 `MULTIPLEXING_OFF`,`MULTIPLEXING_LOW`,`MULTIPLEXING_MIDDLE`,`MULTIPLEXING_HIGH`。其中 `MULTIPLEXING_OFF` 会关闭多路复用功能。
|
||||
|
||||
#### traffic_pattern
|
||||
|
||||
一个 base64 字符串用于微调网络行为。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"tag": "my-manager",
|
||||
"database": {
|
||||
"driver": "sqlite",
|
||||
"dsn": "file:manager.db?_pragma=foreign_keys(on)&_pragma=journal_mode(wal)&_pragma=busy_timeout(5000)" // also supported Postgresql
|
||||
"dsn": "file:manager.db?_pragma=foreign_keys(on)&_pragma=journal_mode(wal)&_pragma=busy_timeout(5000)&_time_format=sqlite" // also supported Postgresql
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -64,6 +64,8 @@
|
||||
"listen_port": 7000,
|
||||
"manager": "my-manager",
|
||||
"api_key": "change-me-secret",
|
||||
"keep_alive": "10s",
|
||||
"keep_alive_timeout": "5s",
|
||||
// Enable TLS for production deployments (the node connects via gRPC over h2):
|
||||
// "tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#inbound
|
||||
// "enabled": true,
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
"address": "example.com",
|
||||
"port": 10001,
|
||||
"public_key": "3nk7jdnkcL95Fc/z+GCiH7jOovEKhFkLIGPT+U/uLEQ=",
|
||||
"allowed_ips": ["0.0.0.0/0"],
|
||||
"reserved": "AAAA"
|
||||
"allowed_ips": ["0.0.0.0/0"]
|
||||
}
|
||||
],
|
||||
"udp_timeout": "5m0s",
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
{
|
||||
"type": "masque",
|
||||
"tag": "masque-out",
|
||||
"system": false,
|
||||
"name": "masque0",
|
||||
"use_http2": false,
|
||||
"use_ipv6": false,
|
||||
"profile": {
|
||||
@@ -37,14 +39,15 @@
|
||||
"udp_keepalive_period": "30s",
|
||||
"udp_initial_packet_size": 0,
|
||||
"reconnect_delay": "5s",
|
||||
// TLS fields for HTTP2
|
||||
"insecure": false,
|
||||
"cipher_suites": [],
|
||||
"curve_preferences": [],
|
||||
"fragment": false,
|
||||
"record_fragment": false,
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false
|
||||
"tls": { // TLS fields for HTTP2
|
||||
"insecure": false,
|
||||
"cipher_suites": [],
|
||||
"curve_preferences": [],
|
||||
"fragment": false,
|
||||
"record_fragment": false,
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false
|
||||
}
|
||||
// Dial Fields
|
||||
}
|
||||
],
|
||||
@@ -53,4 +56,4 @@
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,14 +27,16 @@
|
||||
"tag": "mieru-out",
|
||||
"server": "example.com",
|
||||
"server_port": 27017,
|
||||
"server_ports": "27017-27019",
|
||||
"server_ports": [
|
||||
"27017-27019"
|
||||
],
|
||||
"transport": "TCP",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
// valid: MULTIPLEXING_DEFAULT / MULTIPLEXING_OFF / MULTIPLEXING_LOW
|
||||
// MULTIPLEXING_MIDDLE / MULTIPLEXING_HIGH
|
||||
"multiplexing": "MULTIPLEXING_LOW"
|
||||
// Dial Fields
|
||||
"multiplexing": "MULTIPLEXING_LOW",
|
||||
"traffic_pattern": "GgQIARAK"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
|
||||
32
examples/mieru/server.json
Normal file
32
examples/mieru/server.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mieru",
|
||||
"tag": "mieru-in",
|
||||
"listen_port": 27017,
|
||||
"listen_ports": [
|
||||
"27017-27019"
|
||||
],
|
||||
"transport": "TCP",
|
||||
"users": [
|
||||
{
|
||||
"name": "username",
|
||||
"password": "password"
|
||||
}
|
||||
],
|
||||
"traffic_pattern": "GgQIARAK"
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "direct"
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,15 @@
|
||||
"packet_encoding": "",
|
||||
"transport": {
|
||||
"type": "mkcp",
|
||||
"mtu": 1500
|
||||
"mtu": 1350, // 576-1460
|
||||
"tti": 50, // 10-100, ms
|
||||
"uplink_capacity": 12, // MB/s
|
||||
"downlink_capacity": 100, // MB/s
|
||||
"congestion": false,
|
||||
"read_buffer_size": 1, // MB
|
||||
"write_buffer_size": 1, // MB
|
||||
"header_type": "none", // none, srtp, utp, wechat-video, dtls, wireguard
|
||||
"seed": "password"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -24,7 +24,15 @@
|
||||
],
|
||||
"transport": {
|
||||
"type": "mkcp",
|
||||
"mtu": 1500
|
||||
"mtu": 1350, // 576-1460
|
||||
"tti": 50, // 10-100, ms
|
||||
"uplink_capacity": 12, // MB/s
|
||||
"downlink_capacity": 100, // MB/s
|
||||
"congestion": false,
|
||||
"read_buffer_size": 1, // MB
|
||||
"write_buffer_size": 1, // MB
|
||||
"header_type": "none", // none, srtp, utp, wechat-video, dtls, wireguard
|
||||
"seed": "password"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
"concurrency": 8192,
|
||||
// domain_fronting_port is a port we use to connect to a fronting domain.
|
||||
"domain_fronting_port": 443,
|
||||
// domain_fronting_ip is an IP address to use when connecting to the fronting
|
||||
// domain instead of resolving the hostname from the secret via DNS.
|
||||
"domain_fronting_ip": "",
|
||||
// domain_fronting_host is the address (IP or hostname) to use when connecting
|
||||
// to the fronting domain instead of resolving the hostname from the secret via DNS.
|
||||
"domain_fronting_host": "",
|
||||
// domain_fronting_proxy_protocol is used if communication between upstream
|
||||
// endpoint and sing-box supports proxy protocol.
|
||||
"domain_fronting_proxy_protocol": false,
|
||||
|
||||
48
examples/openvpn/auth-user-pass.json
Normal file
48
examples/openvpn/auth-user-pass.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "info"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "openvpn",
|
||||
"tag": "openvpn-out",
|
||||
"system": false,
|
||||
"name": "openvpn0",
|
||||
"servers": [
|
||||
{
|
||||
"server": "vpn.example.com",
|
||||
"server_port": 1194
|
||||
}
|
||||
],
|
||||
"proto": "udp", // udp, tcp
|
||||
"username": "myuser",
|
||||
"password": "mypassword",
|
||||
"tls_crypt": "-----BEGIN OpenVPN Static key V1-----\n...\n-----END OpenVPN Static key V1-----",
|
||||
// or: "tls_crypt_path": "/path/to/ta.key",
|
||||
"tls": {
|
||||
"ca": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
||||
// or: "ca_path": "/path/to/ca.crt",
|
||||
"cipher_suites": [],
|
||||
"verify_x509_name": "",
|
||||
"verify_x509_name_mode": "", // name-prefix, name-suffix, exact (default)
|
||||
"fragment": false,
|
||||
"fragment_fallback_delay": "300ms",
|
||||
"record_fragment": false,
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false
|
||||
}
|
||||
// Dial Fields
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "openvpn-out",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
53
examples/openvpn/tls-auth.json
Normal file
53
examples/openvpn/tls-auth.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "info"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "openvpn",
|
||||
"tag": "openvpn-out",
|
||||
"system": false,
|
||||
"name": "openvpn0",
|
||||
"servers": [
|
||||
{
|
||||
"server": "vpn.example.com",
|
||||
"server_port": 1194
|
||||
}
|
||||
],
|
||||
"proto": "udp", // udp, tcp
|
||||
"cipher": "AES-256-CBC",
|
||||
"auth": "SHA1",
|
||||
"tls_auth": "-----BEGIN OpenVPN Static key V1-----\n...\n-----END OpenVPN Static key V1-----",
|
||||
// or: "tls_auth_path": "/path/to/ta.key",
|
||||
"key_direction": 1,
|
||||
"tls": {
|
||||
"certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
||||
// or: "certificate_path": "/path/to/client.crt",
|
||||
"key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----",
|
||||
// or: "key_path": "/path/to/client.key",
|
||||
"ca": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
||||
// or: "ca_path": "/path/to/ca.crt",
|
||||
"cipher_suites": [],
|
||||
"verify_x509_name": "",
|
||||
"verify_x509_name_mode": "", // name-prefix, name-suffix, exact (default)
|
||||
"fragment": false,
|
||||
"fragment_fallback_delay": "300ms",
|
||||
"record_fragment": false,
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false
|
||||
}
|
||||
// Dial Fields
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "openvpn-out",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
51
examples/openvpn/tls-crypt-v2.json
Normal file
51
examples/openvpn/tls-crypt-v2.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "info"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "openvpn",
|
||||
"tag": "openvpn-out",
|
||||
"system": false,
|
||||
"name": "openvpn0",
|
||||
"servers": [
|
||||
{
|
||||
"server": "vpn.example.com",
|
||||
"server_port": 1194
|
||||
}
|
||||
],
|
||||
"proto": "udp", // udp, tcp
|
||||
"tls_crypt": "-----BEGIN OpenVPN tls-crypt-v2 client key-----\n...\n-----END OpenVPN tls-crypt-v2 client key-----",
|
||||
// or: "tls_crypt_path": "/path/to/tls-crypt-v2.key",
|
||||
"tls_crypt_v2": true,
|
||||
"tls": {
|
||||
"certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
||||
// or: "certificate_path": "/path/to/client.crt",
|
||||
"key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----",
|
||||
// or: "key_path": "/path/to/client.key",
|
||||
"ca": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
||||
// or: "ca_path": "/path/to/ca.crt",
|
||||
"cipher_suites": [],
|
||||
"verify_x509_name": "",
|
||||
"verify_x509_name_mode": "", // name-prefix, name-suffix, exact (default)
|
||||
"fragment": false,
|
||||
"fragment_fallback_delay": "300ms",
|
||||
"record_fragment": false,
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false
|
||||
}
|
||||
// Dial Fields
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "openvpn-out",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
54
examples/openvpn/tls-crypt.json
Normal file
54
examples/openvpn/tls-crypt.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "info"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "openvpn",
|
||||
"tag": "openvpn-out",
|
||||
"system": false,
|
||||
"name": "openvpn0",
|
||||
"servers": [
|
||||
{
|
||||
"server": "vpn.example.com",
|
||||
"server_port": 1194
|
||||
}
|
||||
],
|
||||
"proto": "udp", // udp, tcp
|
||||
"cipher": "AES-256-GCM", // AES-128-GCM, AES-192-GCM, AES-256-GCM, AES-128-CBC, AES-192-CBC, AES-256-CBC, CHACHA20-POLY1305
|
||||
"auth": "SHA256", // SHA1, SHA256, SHA384, SHA512 (ignored for AEAD ciphers)
|
||||
"tls_crypt": "-----BEGIN OpenVPN Static key V1-----\n...\n-----END OpenVPN Static key V1-----",
|
||||
// or: "tls_crypt_path": "/path/to/ta.key",
|
||||
"ping_interval": "10s",
|
||||
"reconnect_delay": "30s",
|
||||
"tls": {
|
||||
"certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
||||
// or: "certificate_path": "/path/to/client.crt",
|
||||
"key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----",
|
||||
// or: "key_path": "/path/to/client.key",
|
||||
"ca": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
||||
// or: "ca_path": "/path/to/ca.crt",
|
||||
"cipher_suites": [],
|
||||
"verify_x509_name": "",
|
||||
"verify_x509_name_mode": "", // name-prefix, name-suffix, exact (default)
|
||||
"fragment": false,
|
||||
"fragment_fallback_delay": "300ms",
|
||||
"record_fragment": false,
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false
|
||||
}
|
||||
// Dial Fields
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "openvpn-out",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,8 @@
|
||||
// - SIP008 (shadowsocks)
|
||||
// - Raw shareable links (vless://, vmess://, ss://, trojan://, ...)
|
||||
"path": "subscriptions/my-sub.txt",
|
||||
// Remove emoji flags from proxy names.
|
||||
"remove_emojis": true,
|
||||
"health_check": {
|
||||
"enabled": true,
|
||||
"url": "https://www.gstatic.com/generate_204",
|
||||
|
||||
@@ -56,6 +56,8 @@
|
||||
// "exclude" wins over "include" when both match.
|
||||
"exclude": "(?i)expire|流量|官网",
|
||||
"include": "(?i)hk|jp|sg|us",
|
||||
// Remove emoji flags from proxy names.
|
||||
"remove_emojis": true,
|
||||
"health_check": {
|
||||
"enabled": true,
|
||||
"url": "https://www.gstatic.com/generate_204",
|
||||
|
||||
52
examples/ssh/client.json
Normal file
52
examples/ssh/client.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "ssh",
|
||||
"tag": "ssh-out",
|
||||
"server": "example.com",
|
||||
"server_port": 2222,
|
||||
"user": "user",
|
||||
// Authentication: password or private key
|
||||
"password": "password",
|
||||
"private_key": [
|
||||
"-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----"
|
||||
],
|
||||
// or: "private_key_path": "/path/to/id_ed25519",
|
||||
"private_key_passphrase": "",
|
||||
// Pin server host key (optional)
|
||||
"host_key": [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA..."
|
||||
],
|
||||
"host_key_algorithms": ["ssh-ed25519"],
|
||||
"client_version": "SSH-2.0-OpenSSH_9.6"
|
||||
// Dial Fields
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "ssh-out",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
76
examples/ssh/server.json
Normal file
76
examples/ssh/server.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "info"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "ssh",
|
||||
"tag": "ssh-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 2222,
|
||||
"host_key": [
|
||||
"-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----"
|
||||
],
|
||||
// or: "host_key_path": ["/etc/sing-box/ssh_host_ed25519_key"],
|
||||
"server_version": "SSH-2.0-OpenSSH_9.6",
|
||||
"max_auth_tries": 3,
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"password": "password1"
|
||||
},
|
||||
{
|
||||
"name": "user2",
|
||||
"authorized_keys": [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... user2@host"
|
||||
]
|
||||
}
|
||||
],
|
||||
"fallback": {
|
||||
"server": "10.0.0.2",
|
||||
"server_port": 22,
|
||||
"ca": {
|
||||
"private_key": [
|
||||
"-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----"
|
||||
],
|
||||
// or: "private_key_path": "/etc/sing-box/ca_key",
|
||||
"private_key_passphrase": ""
|
||||
},
|
||||
// Optional: separate CA for issuing upstream certs (defaults to ca)
|
||||
"issue_ca": {
|
||||
"private_key": [
|
||||
"-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----"
|
||||
],
|
||||
// or: "private_key_path": "/etc/sing-box/issue_ca_key",
|
||||
"private_key_passphrase": ""
|
||||
},
|
||||
"host_key": [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... upstream-host-key"
|
||||
],
|
||||
// or: "host_key_path": ["/etc/sing-box/upstream_host_key.pub"],
|
||||
"host_key_algorithms": ["ssh-ed25519"],
|
||||
"client_version": "SSH-2.0-OpenSSH_9.6"
|
||||
// Dial Fields
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "direct",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
31
examples/sudoku/client.json
Normal file
31
examples/sudoku/client.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"listen": "127.0.0.1",
|
||||
"listen_port": 1080
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "sudoku",
|
||||
"server": "your-server.com",
|
||||
"server_port": 443,
|
||||
"key": "your-secret-key"
|
||||
// "aead_method": "chacha20-poly1305", // chacha20-poly1305 | aes-128-gcm | none
|
||||
// "table_type": "prefer_ascii", // prefer_ascii | prefer_entropy | up_ascii_down_entropy | up_entropy_down_ascii
|
||||
// "padding_min": 10, // 0-100
|
||||
// "padding_max": 30, // 0-100, >= padding_min
|
||||
// "enable_pure_downlink": true, // true | false
|
||||
// "custom_table": "xpxvvpvv", // 8 chars: 2x, 2p, 4v
|
||||
// "custom_tables": ["xpxvvpvv", "vxpvxvvp"],
|
||||
// "http_mask": {
|
||||
// "enabled": true, // true | false
|
||||
// "mode": "stream", // legacy | stream | poll | auto | ws
|
||||
// "host": "cdn.example.com", // optional, Host header / SNI override
|
||||
// "path_root": "secret", // optional, URL path prefix (single segment)
|
||||
// "multiplex": "auto" // off | auto | on
|
||||
// }
|
||||
}
|
||||
]
|
||||
}
|
||||
35
examples/sudoku/client_tls.json
Normal file
35
examples/sudoku/client_tls.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"listen": "127.0.0.1",
|
||||
"listen_port": 1080
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "sudoku",
|
||||
"server": "your-server.com",
|
||||
"server_port": 443,
|
||||
"key": "your-secret-key",
|
||||
// "aead_method": "chacha20-poly1305", // chacha20-poly1305 | aes-128-gcm | none
|
||||
// "table_type": "prefer_ascii", // prefer_ascii | prefer_entropy | up_ascii_down_entropy | up_entropy_down_ascii
|
||||
// "padding_min": 10, // 0-100
|
||||
// "padding_max": 30, // 0-100, >= padding_min
|
||||
// "enable_pure_downlink": true, // true | false
|
||||
// "custom_table": "xpxvvpvv", // 8 chars: 2x, 2p, 4v
|
||||
// "custom_tables": ["xpxvvpvv", "vxpvxvvp"],
|
||||
"http_mask": {
|
||||
"enabled": true, // true | false
|
||||
"mode": "stream", // legacy | stream | poll | auto | ws
|
||||
"host": "cdn.example.com", // optional, Host header / SNI override
|
||||
"path_root": "secret", // optional, URL path prefix (single segment)
|
||||
"multiplex": "auto", // off | auto | on
|
||||
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#outbound
|
||||
"enabled": true,
|
||||
"server_name": "cdn.example.com",
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
27
examples/sudoku/server.json
Normal file
27
examples/sudoku/server.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "sudoku",
|
||||
"listen": "::",
|
||||
"listen_port": 443,
|
||||
"key": "your-secret-key"
|
||||
// "aead_method": "chacha20-poly1305", // chacha20-poly1305 | aes-128-gcm | none
|
||||
// "table_type": "prefer_ascii", // prefer_ascii | prefer_entropy | up_ascii_down_entropy | up_entropy_down_ascii
|
||||
// "padding_min": 10, // 0-100
|
||||
// "padding_max": 30, // 0-100, >= padding_min
|
||||
// "enable_pure_downlink": true, // true | false
|
||||
// "handshake_timeout": 5, // seconds
|
||||
// "custom_table": "xpxvvpvv", // 8 chars: 2x, 2p, 4v
|
||||
// "custom_tables": ["xpxvvpvv", "vxpvxvvp"],
|
||||
// "disable_http_mask": false, // true | false
|
||||
// "http_mask_mode": "legacy", // legacy | stream | poll | auto | ws
|
||||
// "path_root": "secret", // optional, URL path prefix (single segment)
|
||||
// "fallback": "127.0.0.1:8080" // optional, fallback address for rejected connections
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct"
|
||||
}
|
||||
]
|
||||
}
|
||||
22
examples/sudoku/server_advanced.json
Normal file
22
examples/sudoku/server_advanced.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "sudoku",
|
||||
"listen": "::",
|
||||
"listen_port": 443,
|
||||
"key": "your-secret-key",
|
||||
"aead_method": "aes-128-gcm",
|
||||
"table_type": "prefer_entropy",
|
||||
"padding_min": 10,
|
||||
"padding_max": 50,
|
||||
"http_mask_mode": "stream",
|
||||
"path_root": "secret",
|
||||
"fallback": "127.0.0.1:8080"
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct"
|
||||
}
|
||||
]
|
||||
}
|
||||
78
examples/trusttunnel/client.json
Normal file
78
examples/trusttunnel/client.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "trusttunnel",
|
||||
"tag": "trusttunnel-h2",
|
||||
"server": "example.com",
|
||||
"server_port": 443,
|
||||
"username": "user1",
|
||||
"password": "password1",
|
||||
"network": ["tcp", "udp"],
|
||||
"health_check": true,
|
||||
"multiplex": {
|
||||
"enabled": true,
|
||||
"max_connections": 8,
|
||||
"min_streams": 5
|
||||
},
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": "example.com"
|
||||
}
|
||||
// Dial Fields
|
||||
},
|
||||
{
|
||||
"type": "trusttunnel",
|
||||
"tag": "trusttunnel-quic",
|
||||
"server": "example.com",
|
||||
"server_port": 443,
|
||||
"username": "user1",
|
||||
"password": "password1",
|
||||
"network": ["tcp", "udp"],
|
||||
"health_check": true,
|
||||
"quic": true,
|
||||
"congestion_controller": "bbr", // bbr, bbr_standard, bbr2, bbr2_variant, cubic, reno
|
||||
"bbr_profile": "standard", // standard, conservative, aggressive
|
||||
"cwnd": 32,
|
||||
"multiplex": {
|
||||
"enabled": true,
|
||||
"max_connections": 8,
|
||||
"min_streams": 5
|
||||
},
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": "example.com"
|
||||
}
|
||||
// Dial Fields
|
||||
},
|
||||
{
|
||||
"type": "selector",
|
||||
"tag": "trusttunnel-selector",
|
||||
"outbounds": ["trusttunnel-h2", "trusttunnel-quic"],
|
||||
"default": "trusttunnel-h2"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "trusttunnel-selector",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
32
examples/trusttunnel/server.json
Normal file
32
examples/trusttunnel/server.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "trusttunnel",
|
||||
"tag": "trusttunnel-in",
|
||||
"listen": "::",
|
||||
"listen_port": 443,
|
||||
"network": ["tcp", "udp"],
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"password": "password1"
|
||||
}
|
||||
],
|
||||
"congestion_controller": "bbr", // bbr, bbr_standard, bbr2, bbr2_variant, cubic, reno
|
||||
"bbr_profile": "standard", // standard, conservative, aggressive
|
||||
"cwnd": 32,
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"alpn": ["h2", "h3"],
|
||||
"certificate_path": "/path/to/cert.pem",
|
||||
"key_path": "/path/to/key.pem"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -23,8 +23,7 @@
|
||||
"address": "example.com",
|
||||
"port": 10001,
|
||||
"public_key": "3nk7jdnkcL95Fc/z+GCiH7jOovEKhFkLIGPT+U/uLEQ=",
|
||||
"allowed_ips": ["0.0.0.0/0"],
|
||||
"reserved": "AAAA"
|
||||
"allowed_ips": ["0.0.0.0/0"]
|
||||
}
|
||||
],
|
||||
"udp_timeout": "5m0s",
|
||||
|
||||
@@ -30,7 +30,7 @@ func updateExternalUI(server *Server) func(w http.ResponseWriter, r *http.Reques
|
||||
render.JSON(w, r, newError(err.Error()))
|
||||
return
|
||||
}
|
||||
server.logger.Info("updated external UI")
|
||||
server.logger.Notice("updated external UI")
|
||||
render.JSON(w, r, render.M{"status": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ func (s *Server) Start(stage adapter.StartStage) error {
|
||||
if err != nil {
|
||||
return E.Cause(err, "external controller listen error")
|
||||
}
|
||||
s.logger.Info("restful api listening at ", listener.Addr())
|
||||
s.logger.Notice("restful api listening at ", listener.Addr())
|
||||
go func() {
|
||||
err = s.httpServer.Serve(listener)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
@@ -234,7 +234,7 @@ func (s *Server) SetMode(newMode string) {
|
||||
s.logger.Error(E.Cause(err, "save mode"))
|
||||
}
|
||||
}
|
||||
s.logger.Info("updated mode: ", newMode)
|
||||
s.logger.Notice("updated mode: ", newMode)
|
||||
}
|
||||
|
||||
func (s *Server) HistoryStorage() adapter.URLTestHistoryStorage {
|
||||
|
||||
@@ -56,7 +56,7 @@ func (s *Server) Start(stage adapter.StartStage) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.logger.Info("grpc server started at ", listener.Addr())
|
||||
s.logger.Notice("grpc server started at ", listener.Addr())
|
||||
s.tcpListener = listener
|
||||
go func() {
|
||||
err = s.grpcServer.Serve(listener)
|
||||
|
||||
28
go.mod
28
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/sagernet/sing-box
|
||||
|
||||
go 1.26.1
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
github.com/AliRizaAynaci/gorl/v2 v2.2.0
|
||||
@@ -11,10 +11,11 @@ require (
|
||||
github.com/coder/websocket v1.8.14
|
||||
github.com/cretz/bine v0.2.0
|
||||
github.com/database64128/tfo-go/v2 v2.3.2
|
||||
github.com/enfein/mieru/v3 v3.17.1
|
||||
github.com/enfein/mieru/v3 v3.33.0
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/go-playground/validator/v10 v10.30.1
|
||||
github.com/gobwas/ws v1.4.0
|
||||
github.com/godbus/dbus/v5 v5.2.2
|
||||
github.com/gofrs/uuid/v5 v5.4.0
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||
@@ -53,7 +54,7 @@ require (
|
||||
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c
|
||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
||||
github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.0.2
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
|
||||
github.com/shtorm-7/workerpool v0.5.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/vishvananda/netns v0.0.5
|
||||
@@ -75,17 +76,24 @@ require (
|
||||
|
||||
require (
|
||||
github.com/OneOfOne/xxhash v1.2.8 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/docker/docker v28.5.2+incompatible // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/dunglas/httpsfv v1.1.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/panjf2000/ants/v2 v2.12.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/redis/go-redis/v9 v9.8.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b // indirect
|
||||
github.com/zeebo/assert v1.3.0 // indirect
|
||||
go.opentelemetry.io/otel v1.41.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20260408064518-65a410b0d584 // indirect
|
||||
modernc.org/libc v1.72.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
@@ -93,14 +101,14 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
filippo.io/edwards25519 v1.1.0
|
||||
github.com/AdguardTeam/golibs v0.32.7 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/akutz/memconn v0.1.0 // indirect
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
|
||||
github.com/ameshkov/dnscrypt/v2 v2.4.0
|
||||
github.com/ameshkov/dnsstamps v1.0.3 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/caddyserver/zerossl v0.1.5 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
|
||||
@@ -144,7 +152,7 @@ require (
|
||||
github.com/mdlayher/netlink v1.9.0 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.25 // indirect
|
||||
github.com/pires/go-proxyproto v0.11.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus-community/pro-bing v0.4.0 // indirect
|
||||
@@ -211,7 +219,7 @@ require (
|
||||
lukechampine.com/blake3 v1.4.1
|
||||
)
|
||||
|
||||
replace github.com/sagernet/wireguard-go => github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.4.2
|
||||
replace github.com/sagernet/wireguard-go => github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.4.3
|
||||
|
||||
replace github.com/sagernet/tailscale => github.com/shtorm-7/tailscale v1.92.4-sing-box-1.13-mod.7-extended-1.0.2
|
||||
|
||||
@@ -219,10 +227,12 @@ replace github.com/sagernet/sing-mux => github.com/shtorm-7/sing-mux v0.3.4-exte
|
||||
|
||||
replace github.com/ameshkov/dnscrypt/v2 => github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0
|
||||
|
||||
replace github.com/sagernet/sing-vmess => github.com/starifly/sing-vmess v0.2.7-mod.9
|
||||
replace github.com/sagernet/sing-vmess => github.com/shtorm-7/sing-vmess v0.2.7-extended-1.0.0
|
||||
|
||||
replace github.com/dolonet/mtg-multi => github.com/shtorm-7/mtg-multi v1.8.0-extended-1.0.1
|
||||
replace github.com/dolonet/mtg-multi => github.com/shtorm-7/mtg-multi v1.11.0-extended-1.0.0
|
||||
|
||||
replace github.com/Diniboy1123/connect-ip-go => github.com/shtorm-7/connect-ip-go v1.0.0-extended-1.0.0
|
||||
|
||||
replace github.com/shtorm-7/go-cache/v2 => github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.1.0
|
||||
|
||||
replace github.com/sagernet/sing => github.com/shtorm-7/sing v0.8.10-extended-1.1.0
|
||||
|
||||
72
go.sum
72
go.sum
@@ -20,14 +20,16 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
|
||||
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY=
|
||||
github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q=
|
||||
github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc=
|
||||
github.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0=
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
@@ -75,10 +77,10 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54=
|
||||
@@ -87,8 +89,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/enfein/mieru/v3 v3.17.1 h1:pIKbspsKRYNyUrORVI33t1/yz2syaaUkIanskAbGBHY=
|
||||
github.com/enfein/mieru/v3 v3.17.1/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/enfein/mieru/v3 v3.33.0 h1:hv2jK8nqYHwpSG86U2rpZR2I8Aff1/J3ifRmd9NBbFc=
|
||||
github.com/enfein/mieru/v3 v3.33.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE=
|
||||
@@ -129,12 +131,12 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
|
||||
github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
@@ -243,16 +245,16 @@ github.com/openai/openai-go/v3 v3.26.0 h1:bRt6H/ozMNt/dDkN4gobnLqaEGrRGBzmbVs0xx
|
||||
github.com/openai/openai-go/v3 v3.26.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||
github.com/panjf2000/ants/v2 v2.12.0 h1:u9JhESo83i/GkZnhfTNuFMMWcNt7mnV1bGJ6FT4wXH8=
|
||||
github.com/panjf2000/ants/v2 v2.12.0/go.mod h1:tSQuaNQ6r6NRhPt+IZVUevvDyFMTs+eS4ztZc52uJTY=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
|
||||
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
|
||||
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -357,8 +359,6 @@ github.com/sagernet/nftables v0.3.0-mod.2 h1:ck2KMU02OxL1eDFgGaWYglMDpoOZ7OHzxje
|
||||
github.com/sagernet/nftables v0.3.0-mod.2/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ=
|
||||
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
|
||||
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
|
||||
github.com/sagernet/sing v0.8.10 h1:V5VZffy8rm4dtBVKIpKa8vibRR2SiJprtu/10DFUalU=
|
||||
github.com/sagernet/sing v0.8.10/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
|
||||
github.com/sagernet/sing-quic v0.6.1 h1:lx0tcm99wIA1RkyvILNzRSsMy1k7TTQYIhx71E/WBlw=
|
||||
github.com/sagernet/sing-quic v0.6.1/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
|
||||
@@ -379,23 +379,25 @@ github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0 h1:e5s7RKBd2rIPR0StbvZ2vTV
|
||||
github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI=
|
||||
github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.1.0 h1:PLZ/YHqnApPx13wt6MX3ItqESp4ueBr1tGSi0bEGqYw=
|
||||
github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.1.0/go.mod h1:Ek4yz5OK6stwhLKgLsRRYDI+FA+ZWvRJiWLjsi/vMM4=
|
||||
github.com/shtorm-7/mtg-multi v1.8.0-extended-1.0.1 h1:UeJkrCJJmIjTBywErVMx7fCSoBf4gh6QgT9bp9o1ajM=
|
||||
github.com/shtorm-7/mtg-multi v1.8.0-extended-1.0.1/go.mod h1:3rvdhwdPABkwKBdvgMt3VwMn9uSq8hpoHRezZ5jRJU0=
|
||||
github.com/shtorm-7/mtg-multi v1.11.0-extended-1.0.0 h1:iBLll4ZZG8ULQcHWs6gGslZWtBN72Zo1zjySzMVHF7g=
|
||||
github.com/shtorm-7/mtg-multi v1.11.0-extended-1.0.0/go.mod h1:3rvdhwdPABkwKBdvgMt3VwMn9uSq8hpoHRezZ5jRJU0=
|
||||
github.com/shtorm-7/sing v0.8.10-extended-1.1.0 h1:P4JL2cugjvEvnYu8tMmpR30SE1qsS45RcnNEwzDz5as=
|
||||
github.com/shtorm-7/sing v0.8.10-extended-1.1.0/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
|
||||
github.com/shtorm-7/sing-mux v0.3.4-extended-1.0.0 h1:a5OoXr3e2ACbM6vDIaaGL44IdHQ6wPjcSoU13vfC0Sw=
|
||||
github.com/shtorm-7/sing-mux v0.3.4-extended-1.0.0/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
|
||||
github.com/shtorm-7/sing-vmess v0.2.7-extended-1.0.0 h1:WVheKmQH5hSQbJU1ZTKthKSutkTLWSb2hp4JuQhJBow=
|
||||
github.com/shtorm-7/sing-vmess v0.2.7-extended-1.0.0/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
|
||||
github.com/shtorm-7/tailscale v1.92.4-sing-box-1.13-mod.7-extended-1.0.2 h1:hSMjh97OszszOd8HrzpaYUQH9dWRRBluJCbwQyz8ZOk=
|
||||
github.com/shtorm-7/tailscale v1.92.4-sing-box-1.13-mod.7-extended-1.0.2/go.mod h1:TYIIqO5sZpWq873rLIeO2usszSMUpR3h6WdqVVs65ug=
|
||||
github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.4.2 h1:oEvk13VPsypigqNK/rlcqC63gTj0ANJAnzUwlZkOia4=
|
||||
github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.4.2/go.mod h1:Me2JlCDYHxnd0mnuX7L5LXAeDHCltI7vSKq3eTE6SVE=
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
|
||||
github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.4.3 h1:jtOA73D4F5qRV70//ahOt20KBnWvQimAFjtIiOtt0ps=
|
||||
github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.4.3/go.mod h1:Me2JlCDYHxnd0mnuX7L5LXAeDHCltI7vSKq3eTE6SVE=
|
||||
github.com/shtorm-7/workerpool v0.5.0 h1:NPZuNgyH0EUm4aQsTL09xR1iV+7GCFw6jX9Z4aAVp2s=
|
||||
github.com/shtorm-7/workerpool v0.5.0/go.mod h1:NI0pUZgmGu0BdKO9j3mct1DNZmgXbyTS9foorljdH6E=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/starifly/sing-vmess v0.2.7-mod.9 h1:xobAmejSbBQ0A3f/EtJ9cJd3m6gK7dDPccPdeGz7tXY=
|
||||
github.com/starifly/sing-vmess v0.2.7-mod.9/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
@@ -443,12 +445,14 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
|
||||
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||
@@ -457,16 +461,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
|
||||
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
|
||||
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
|
||||
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
|
||||
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
@@ -521,8 +525,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
|
||||
|
||||
12
include/openvpn.go
Normal file
12
include/openvpn.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build with_openvpn
|
||||
|
||||
package include
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/protocol/openvpn"
|
||||
)
|
||||
|
||||
func registerOpenVPNOutbound(registry *outbound.Registry) {
|
||||
openvpn.RegisterOutbound(registry)
|
||||
}
|
||||
20
include/openvpn_stub.go
Normal file
20
include/openvpn_stub.go
Normal file
@@ -0,0 +1,20 @@
|
||||
//go:build !with_openvpn
|
||||
|
||||
package include
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func registerOpenVPNOutbound(registry *outbound.Registry) {
|
||||
outbound.Register[option.OpenVPNOutboundOptions](registry, C.TypeOpenVPN, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.OpenVPNOutboundOptions) (adapter.Outbound, error) {
|
||||
return nil, E.New(`OpenVPN outbound is not included in this build, rebuild with -tags with_openvpn`)
|
||||
})
|
||||
}
|
||||
@@ -80,13 +80,17 @@ func InboundRegistry() *inbound.Registry {
|
||||
shadowtls.RegisterInbound(registry)
|
||||
vless.RegisterInbound(registry)
|
||||
anytls.RegisterInbound(registry)
|
||||
mieru.RegisterInbound(registry)
|
||||
ssh.RegisterInbound(registry)
|
||||
|
||||
bond.RegisterInbound(registry)
|
||||
failover.RegisterInbound(registry)
|
||||
registerTrustTunnelInbound(registry)
|
||||
|
||||
registerQUICInbounds(registry)
|
||||
registerStubForRemovedInbounds(registry)
|
||||
registerMTProxyInbound(registry)
|
||||
registerSudokuInbound(registry)
|
||||
|
||||
return registry
|
||||
}
|
||||
@@ -115,9 +119,11 @@ func OutboundRegistry() *outbound.Registry {
|
||||
mieru.RegisterOutbound(registry)
|
||||
anytls.RegisterOutbound(registry)
|
||||
registerMASQUEOutbound(registry)
|
||||
registerOpenVPNOutbound(registry)
|
||||
|
||||
bond.RegisterOutbound(registry)
|
||||
failover.RegisterOutbound(registry)
|
||||
registerTrustTunnelOutbound(registry)
|
||||
|
||||
bandwidth.RegisterOutbound(registry)
|
||||
connection.RegisterOutbound(registry)
|
||||
@@ -128,6 +134,7 @@ func OutboundRegistry() *outbound.Registry {
|
||||
|
||||
registerQUICOutbounds(registry)
|
||||
registerStubForRemovedOutbounds(registry)
|
||||
registerSudokuOutbound(registry)
|
||||
|
||||
return registry
|
||||
}
|
||||
|
||||
17
include/sudoku.go
Normal file
17
include/sudoku.go
Normal file
@@ -0,0 +1,17 @@
|
||||
//go:build with_sudoku
|
||||
|
||||
package include
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/protocol/sudoku"
|
||||
)
|
||||
|
||||
func registerSudokuInbound(registry *inbound.Registry) {
|
||||
sudoku.RegisterInbound(registry)
|
||||
}
|
||||
|
||||
func registerSudokuOutbound(registry *outbound.Registry) {
|
||||
sudoku.RegisterOutbound(registry)
|
||||
}
|
||||
27
include/sudoku_stub.go
Normal file
27
include/sudoku_stub.go
Normal file
@@ -0,0 +1,27 @@
|
||||
//go:build !with_sudoku
|
||||
|
||||
package include
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func registerSudokuInbound(registry *inbound.Registry) {
|
||||
inbound.Register[option.SudokuInboundOptions](registry, C.TypeSudoku, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SudokuInboundOptions) (adapter.Inbound, error) {
|
||||
return nil, E.New(`Sudoku is not included in this build, rebuild with -tags with_sudoku`)
|
||||
})
|
||||
}
|
||||
|
||||
func registerSudokuOutbound(registry *outbound.Registry) {
|
||||
outbound.Register[option.SudokuOutboundOptions](registry, C.TypeSudoku, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SudokuOutboundOptions) (adapter.Outbound, error) {
|
||||
return nil, E.New(`Sudoku is not included in this build, rebuild with -tags with_sudoku`)
|
||||
})
|
||||
}
|
||||
17
include/trusttunnel.go
Normal file
17
include/trusttunnel.go
Normal file
@@ -0,0 +1,17 @@
|
||||
//go:build with_trusttunnel
|
||||
|
||||
package include
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/protocol/trusttunnel"
|
||||
)
|
||||
|
||||
func registerTrustTunnelInbound(registry *inbound.Registry) {
|
||||
trusttunnel.RegisterInbound(registry)
|
||||
}
|
||||
|
||||
func registerTrustTunnelOutbound(registry *outbound.Registry) {
|
||||
trusttunnel.RegisterOutbound(registry)
|
||||
}
|
||||
27
include/trusttunnel_stub.go
Normal file
27
include/trusttunnel_stub.go
Normal file
@@ -0,0 +1,27 @@
|
||||
//go:build !with_trusttunnel
|
||||
|
||||
package include
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func registerTrustTunnelInbound(registry *inbound.Registry) {
|
||||
inbound.Register[option.TrustTunnelInboundOptions](registry, C.TypeTrustTunnel, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrustTunnelInboundOptions) (adapter.Inbound, error) {
|
||||
return nil, E.New(`TrustTunnel is not included in this build, rebuild with -tags with_trusttunnel`)
|
||||
})
|
||||
}
|
||||
|
||||
func registerTrustTunnelOutbound(registry *outbound.Registry) {
|
||||
outbound.Register[option.TrustTunnelOutboundOptions](registry, C.TypeTrustTunnel, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrustTunnelOutboundOptions) (adapter.Outbound, error) {
|
||||
return nil, E.New(`TrustTunnel is not included in this build, rebuild with -tags with_trusttunnel`)
|
||||
})
|
||||
}
|
||||
@@ -39,6 +39,10 @@ func Info(args ...any) {
|
||||
std.Info(args...)
|
||||
}
|
||||
|
||||
func Notice(args ...any) {
|
||||
std.Notice(args...)
|
||||
}
|
||||
|
||||
func Warn(args ...any) {
|
||||
std.Warn(args...)
|
||||
}
|
||||
@@ -67,6 +71,10 @@ func InfoContext(ctx context.Context, args ...any) {
|
||||
std.InfoContext(ctx, args...)
|
||||
}
|
||||
|
||||
func NoticeContext(ctx context.Context, args ...any) {
|
||||
std.NoticeContext(ctx, args...)
|
||||
}
|
||||
|
||||
func WarnContext(ctx context.Context, args ...any) {
|
||||
std.WarnContext(ctx, args...)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ func (f Formatter) Format(ctx context.Context, level Level, tag string, message
|
||||
levelString = aurora.White(levelString).String()
|
||||
case LevelInfo:
|
||||
levelString = aurora.Cyan(levelString).String()
|
||||
case LevelNotice:
|
||||
levelString = aurora.Green(levelString).String()
|
||||
case LevelWarn:
|
||||
levelString = aurora.Yellow(levelString).String()
|
||||
case LevelError, LevelFatal, LevelPanic:
|
||||
@@ -97,6 +99,8 @@ func (f Formatter) FormatWithSimple(ctx context.Context, level Level, tag string
|
||||
levelString = aurora.White(levelString).String()
|
||||
case LevelInfo:
|
||||
levelString = aurora.Cyan(levelString).String()
|
||||
case LevelNotice:
|
||||
levelString = aurora.Green(levelString).String()
|
||||
case LevelWarn:
|
||||
levelString = aurora.Yellow(levelString).String()
|
||||
case LevelError, LevelFatal, LevelPanic:
|
||||
|
||||
@@ -11,6 +11,7 @@ const (
|
||||
LevelFatal
|
||||
LevelError
|
||||
LevelWarn
|
||||
LevelNotice
|
||||
LevelInfo
|
||||
LevelDebug
|
||||
LevelTrace
|
||||
@@ -24,6 +25,8 @@ func FormatLevel(level Level) string {
|
||||
return "debug"
|
||||
case LevelInfo:
|
||||
return "info"
|
||||
case LevelNotice:
|
||||
return "notice"
|
||||
case LevelWarn:
|
||||
return "warn"
|
||||
case LevelError:
|
||||
@@ -45,6 +48,8 @@ func ParseLevel(level string) (Level, error) {
|
||||
return LevelDebug, nil
|
||||
case "info":
|
||||
return LevelInfo, nil
|
||||
case "notice":
|
||||
return LevelNotice, nil
|
||||
case "warn", "warning":
|
||||
return LevelWarn, nil
|
||||
case "error":
|
||||
|
||||
@@ -47,6 +47,9 @@ func (f *nopFactory) Debug(args ...any) {
|
||||
func (f *nopFactory) Info(args ...any) {
|
||||
}
|
||||
|
||||
func (f *nopFactory) Notice(args ...any) {
|
||||
}
|
||||
|
||||
func (f *nopFactory) Warn(args ...any) {
|
||||
}
|
||||
|
||||
@@ -68,6 +71,9 @@ func (f *nopFactory) DebugContext(ctx context.Context, args ...any) {
|
||||
func (f *nopFactory) InfoContext(ctx context.Context, args ...any) {
|
||||
}
|
||||
|
||||
func (f *nopFactory) NoticeContext(ctx context.Context, args ...any) {
|
||||
}
|
||||
|
||||
func (f *nopFactory) WarnContext(ctx context.Context, args ...any) {
|
||||
}
|
||||
|
||||
|
||||
@@ -154,6 +154,10 @@ func (l *observableLogger) Info(args ...any) {
|
||||
l.InfoContext(context.Background(), args...)
|
||||
}
|
||||
|
||||
func (l *observableLogger) Notice(args ...any) {
|
||||
l.NoticeContext(context.Background(), args...)
|
||||
}
|
||||
|
||||
func (l *observableLogger) Warn(args ...any) {
|
||||
l.WarnContext(context.Background(), args...)
|
||||
}
|
||||
@@ -182,6 +186,10 @@ func (l *observableLogger) InfoContext(ctx context.Context, args ...any) {
|
||||
l.Log(ctx, LevelInfo, args)
|
||||
}
|
||||
|
||||
func (l *observableLogger) NoticeContext(ctx context.Context, args ...any) {
|
||||
l.Log(ctx, LevelNotice, args)
|
||||
}
|
||||
|
||||
func (l *observableLogger) WarnContext(ctx context.Context, args ...any) {
|
||||
l.Log(ctx, LevelWarn, args)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
)
|
||||
|
||||
type MASQUEOutboundOptions struct {
|
||||
UseHTTP2 bool `json:"use_http2,omitempty"`
|
||||
UseIPv6 bool `json:"use_ipv6,omitempty"`
|
||||
Profile CloudflareProfile `json:"profile,omitempty"`
|
||||
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
|
||||
UDPKeepalivePeriod badoption.Duration `json:"udp_keepalive_period,omitempty"`
|
||||
UDPInitialPacketSize uint16 `json:"udp_initial_packet_size,omitempty"`
|
||||
ReconnectDelay badoption.Duration `json:"reconnect_delay,omitempty"`
|
||||
MASQUEOutboundTLSOptions
|
||||
DialerOptions
|
||||
System bool `json:"system,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
AllowedIPs badoption.Listable[netip.Prefix] `json:"allowed_ips,omitempty"`
|
||||
UseHTTP2 bool `json:"use_http2,omitempty"`
|
||||
UseIPv6 bool `json:"use_ipv6,omitempty"`
|
||||
Profile CloudflareProfile `json:"profile,omitempty"`
|
||||
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
|
||||
UDPKeepalivePeriod badoption.Duration `json:"udp_keepalive_period,omitempty"`
|
||||
UDPInitialPacketSize uint16 `json:"udp_initial_packet_size,omitempty"`
|
||||
ReconnectDelay badoption.Duration `json:"reconnect_delay,omitempty"`
|
||||
MASQUEOutboundTLSOptionsContainer
|
||||
}
|
||||
|
||||
type MASQUEOutboundTLSOptions struct {
|
||||
@@ -28,5 +33,5 @@ type MASQUEOutboundTLSOptions struct {
|
||||
}
|
||||
|
||||
type MASQUEOutboundTLSOptionsContainer struct {
|
||||
TLS *OutboundTLSOptions `json:"tls,omitempty"`
|
||||
TLS *MASQUEOutboundTLSOptions `json:"tls,omitempty"`
|
||||
}
|
||||
|
||||
@@ -10,4 +10,19 @@ type MieruOutboundOptions struct {
|
||||
UserName string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Multiplexing string `json:"multiplexing,omitempty"`
|
||||
TrafficPattern string `json:"traffic_pattern,omitempty"`
|
||||
}
|
||||
|
||||
type MieruInboundOptions struct {
|
||||
ListenOptions
|
||||
ListenPorts badoption.Listable[string] `json:"listen_ports,omitempty"`
|
||||
Users []MieruUser `json:"users,omitempty"`
|
||||
Transport string `json:"transport,omitempty"`
|
||||
TrafficPattern string `json:"traffic_pattern,omitempty"`
|
||||
UserHintIsMandatory bool `json:"user_hint_is_mandatory,omitempty"`
|
||||
}
|
||||
|
||||
type MieruUser struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ type MTProxyInboundOptions struct {
|
||||
Users []MTProxyUser `json:"users,omitempty"`
|
||||
Concurrency uint `json:"concurrency,omitempty"`
|
||||
DomainFrontingPort uint `json:"domain_fronting_port,omitempty"`
|
||||
DomainFrontingIP string `json:"domain_fronting_ip,omitempty"`
|
||||
DomainFrontingHost string `json:"domain_fronting_host,omitempty"`
|
||||
DomainFrontingProxyProtocol bool `json:"domain_fronting_proxy_protocol,omitempty"`
|
||||
PreferIP string `json:"prefer_ip,omitempty"`
|
||||
AutoUpdate bool `json:"auto_update,omitempty"`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package option
|
||||
|
||||
type NodeServiceOptions struct {
|
||||
UUID string
|
||||
UUID string `json:"uuid"`
|
||||
Inbounds []string `json:"inbounds"`
|
||||
ConnectionLimiters []string `json:"connection_limiters"`
|
||||
BandwidthLimiters []string `json:"bandwidth_limiters"`
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
)
|
||||
|
||||
type _NodeManagerAPIOptions struct {
|
||||
@@ -57,8 +58,10 @@ func (o *NodeManagerAPIOptions) UnmarshalJSON(bytes []byte) error {
|
||||
type NodeManagerAPIServerOptions struct {
|
||||
ListenOptions
|
||||
InboundTLSOptionsContainer
|
||||
Manager string `json:"manager"`
|
||||
APIKey string `json:"api_key"`
|
||||
Manager string `json:"manager"`
|
||||
APIKey string `json:"api_key"`
|
||||
KeepAlive badoption.Duration `json:"keep_alive,omitempty"`
|
||||
KeepAliveTimeout badoption.Duration `json:"keep_alive_timeout,omitempty"`
|
||||
}
|
||||
|
||||
type NodeManagerAPIClientOptions struct {
|
||||
|
||||
47
option/openvpn.go
Normal file
47
option/openvpn.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
)
|
||||
|
||||
type OpenVPNOutboundOptions struct {
|
||||
DialerOptions
|
||||
System bool `json:"system,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
AllowedIPs badoption.Listable[netip.Prefix] `json:"allowed_ips,omitempty"`
|
||||
Servers []ServerOptions `json:"servers"`
|
||||
Proto string `json:"proto,omitempty"`
|
||||
Cipher string `json:"cipher,omitempty"`
|
||||
Auth string `json:"auth,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
TLSCrypt string `json:"tls_crypt,omitempty"`
|
||||
TLSCryptPath string `json:"tls_crypt_path,omitempty"`
|
||||
TLSCryptV2 bool `json:"tls_crypt_v2,omitempty"`
|
||||
TLSAuth string `json:"tls_auth,omitempty"`
|
||||
TLSAuthPath string `json:"tls_auth_path,omitempty"`
|
||||
KeyDirection int `json:"key_direction,omitempty"`
|
||||
ReconnectDelay badoption.Duration `json:"reconnect_delay,omitempty"`
|
||||
PingInterval badoption.Duration `json:"ping_interval,omitempty"`
|
||||
OpenVPNOutboundTLSOptionsContainer
|
||||
}
|
||||
|
||||
type OpenVPNTLSOptions struct {
|
||||
Certificate string `json:"certificate,omitempty"`
|
||||
CertificatePath string `json:"certificate_path,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
KeyPath string `json:"key_path,omitempty"`
|
||||
CA string `json:"ca,omitempty"`
|
||||
CAPath string `json:"ca_path,omitempty"`
|
||||
CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"`
|
||||
VerifyX509Name string `json:"verify_x509_name,omitempty"`
|
||||
VerifyX509NameMode string `json:"verify_x509_name_mode,omitempty"`
|
||||
KernelTx bool `json:"kernel_tx,omitempty"`
|
||||
KernelRx bool `json:"kernel_rx,omitempty"`
|
||||
}
|
||||
|
||||
type OpenVPNOutboundTLSOptionsContainer struct {
|
||||
TLS *OpenVPNTLSOptions `json:"tls,omitempty"`
|
||||
}
|
||||
@@ -47,8 +47,9 @@ func (h *Provider) UnmarshalJSONContext(ctx context.Context, content []byte) err
|
||||
}
|
||||
|
||||
type ProviderLocalOptions struct {
|
||||
Path string `json:"path"`
|
||||
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
|
||||
Path string `json:"path"`
|
||||
RemoveEmojis bool `json:"remove_emojis,omitempty"`
|
||||
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
|
||||
}
|
||||
|
||||
type ProviderRemoteOptions struct {
|
||||
@@ -57,14 +58,16 @@ type ProviderRemoteOptions struct {
|
||||
DownloadDetour string `json:"download_detour,omitempty"`
|
||||
UpdateInterval badoption.Duration `json:"update_interval,omitempty"`
|
||||
|
||||
Exclude *badoption.Regexp `json:"exclude,omitempty"`
|
||||
Include *badoption.Regexp `json:"include,omitempty"`
|
||||
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
|
||||
Exclude *badoption.Regexp `json:"exclude,omitempty"`
|
||||
Include *badoption.Regexp `json:"include,omitempty"`
|
||||
RemoveEmojis bool `json:"remove_emojis,omitempty"`
|
||||
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
|
||||
}
|
||||
|
||||
type ProviderInlineOptions struct {
|
||||
Outbounds []Outbound `json:"outbounds,omitempty"`
|
||||
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
|
||||
Outbounds []Outbound `json:"outbounds,omitempty"`
|
||||
RemoveEmojis bool `json:"remove_emojis,omitempty"`
|
||||
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
|
||||
}
|
||||
|
||||
type ProviderHealthCheckOptions struct {
|
||||
|
||||
@@ -2,6 +2,39 @@ package option
|
||||
|
||||
import "github.com/sagernet/sing/common/json/badoption"
|
||||
|
||||
type SSHInboundOptions struct {
|
||||
ListenOptions
|
||||
Users []SSHUser `json:"users,omitempty"`
|
||||
HostKey badoption.Listable[string] `json:"host_key,omitempty"`
|
||||
HostKeyPath badoption.Listable[string] `json:"host_key_path,omitempty"`
|
||||
ServerVersion string `json:"server_version,omitempty"`
|
||||
MaxAuthTries int `json:"max_auth_tries,omitempty"`
|
||||
Fallback *SSHFallbackServerOptions `json:"fallback,omitempty"`
|
||||
}
|
||||
|
||||
type SSHFallbackServerOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
CA *SSHCAOptions `json:"ca,omitempty"`
|
||||
IssueCA *SSHCAOptions `json:"issue_ca,omitempty"`
|
||||
HostKey badoption.Listable[string] `json:"host_key,omitempty"`
|
||||
HostKeyPath badoption.Listable[string] `json:"host_key_path,omitempty"`
|
||||
HostKeyAlgorithms badoption.Listable[string] `json:"host_key_algorithms,omitempty"`
|
||||
ClientVersion string `json:"client_version,omitempty"`
|
||||
}
|
||||
|
||||
type SSHCAOptions struct {
|
||||
PrivateKey badoption.Listable[string] `json:"private_key,omitempty"`
|
||||
PrivateKeyPath string `json:"private_key_path,omitempty"`
|
||||
PrivateKeyPassphrase string `json:"private_key_passphrase,omitempty"`
|
||||
}
|
||||
|
||||
type SSHUser struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
AuthorizedKeys badoption.Listable[string] `json:"authorized_keys,omitempty"`
|
||||
}
|
||||
|
||||
type SSHOutboundOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
|
||||
41
option/sudoku.go
Normal file
41
option/sudoku.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package option
|
||||
|
||||
type SudokuOutboundOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
Key string `json:"key"`
|
||||
AEADMethod string `json:"aead_method,omitempty"`
|
||||
PaddingMin *int `json:"padding_min,omitempty"`
|
||||
PaddingMax *int `json:"padding_max,omitempty"`
|
||||
TableType string `json:"table_type,omitempty"`
|
||||
EnablePureDownlink *bool `json:"enable_pure_downlink,omitempty"`
|
||||
CustomTable string `json:"custom_table,omitempty"`
|
||||
CustomTables []string `json:"custom_tables,omitempty"`
|
||||
HTTPMask *SudokuHTTPMask `json:"http_mask,omitempty"`
|
||||
}
|
||||
|
||||
type SudokuHTTPMask struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
PathRoot string `json:"path_root,omitempty"`
|
||||
Multiplex string `json:"multiplex,omitempty"`
|
||||
OutboundTLSOptionsContainer
|
||||
}
|
||||
|
||||
type SudokuInboundOptions struct {
|
||||
ListenOptions
|
||||
Key string `json:"key"`
|
||||
AEADMethod string `json:"aead_method,omitempty"`
|
||||
PaddingMin *int `json:"padding_min,omitempty"`
|
||||
PaddingMax *int `json:"padding_max,omitempty"`
|
||||
TableType string `json:"table_type,omitempty"`
|
||||
HandshakeTimeout *int `json:"handshake_timeout,omitempty"`
|
||||
EnablePureDownlink *bool `json:"enable_pure_downlink,omitempty"`
|
||||
CustomTable string `json:"custom_table,omitempty"`
|
||||
CustomTables []string `json:"custom_tables,omitempty"`
|
||||
DisableHTTPMask bool `json:"disable_http_mask,omitempty"`
|
||||
HTTPMaskMode string `json:"http_mask_mode,omitempty"`
|
||||
PathRoot string `json:"path_root,omitempty"`
|
||||
Fallback string `json:"fallback,omitempty"`
|
||||
}
|
||||
38
option/trusttunnel.go
Normal file
38
option/trusttunnel.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package option
|
||||
|
||||
type TrustTunnelInboundOptions struct {
|
||||
ListenOptions
|
||||
InboundTLSOptionsContainer
|
||||
Users []TrustTunnelUser `json:"users,omitempty"`
|
||||
Network NetworkList `json:"network,omitempty"`
|
||||
CongestionController string `json:"congestion_controller,omitempty"`
|
||||
BBRProfile string `json:"bbr_profile,omitempty"`
|
||||
CWND int `json:"cwnd,omitempty"`
|
||||
}
|
||||
|
||||
type TrustTunnelUser struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
type TrustTunnelMultiplexOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
MaxConnections int `json:"max_connections,omitempty"`
|
||||
MinStreams int `json:"min_streams,omitempty"`
|
||||
MaxStreams int `json:"max_streams,omitempty"`
|
||||
}
|
||||
|
||||
type TrustTunnelOutboundOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
OutboundTLSOptionsContainer
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Network NetworkList `json:"network,omitempty"`
|
||||
HealthCheck bool `json:"health_check,omitempty"`
|
||||
QUIC bool `json:"quic,omitempty"`
|
||||
CongestionController string `json:"congestion_controller,omitempty"`
|
||||
BBRProfile string `json:"bbr_profile,omitempty"`
|
||||
CWND int `json:"cwnd,omitempty"`
|
||||
Multiplex *TrustTunnelMultiplexOptions `json:"multiplex,omitempty"`
|
||||
}
|
||||
@@ -96,6 +96,12 @@ func (h *Inbound) Close() error {
|
||||
return common.Close(h.listener, h.tlsConfig)
|
||||
}
|
||||
|
||||
func (h *Inbound) UpdateUsers(users []option.AnyTLSUser) {
|
||||
h.service.UpdateUsers(common.Map(users, func(it option.AnyTLSUser) anytls.User {
|
||||
return anytls.User(it)
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
if h.tlsConfig != nil {
|
||||
tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig)
|
||||
|
||||
@@ -22,7 +22,7 @@ func NewBondedConn(conns []net.Conn, downloadRatios, uploadRatios []uint8) *bond
|
||||
conns: conns,
|
||||
downloadRatios: downloadRatios,
|
||||
uploadRatios: uploadRatios,
|
||||
readBuffer: bytes.NewBuffer(make([]byte, 0, 65536)),
|
||||
readBuffer: bytes.NewBuffer(make([]byte, 0, 4096)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,13 +37,13 @@ type failoverConn struct {
|
||||
func NewFailoverConn(ctx context.Context, conn net.Conn, dial dial, onClose func()) *failoverConn {
|
||||
var writeBuffers [BufferSize][]byte
|
||||
for i := range BufferSize {
|
||||
writeBuffers[i] = make([]byte, 0, 1000)
|
||||
writeBuffers[i] = make([]byte, 0, 1024)
|
||||
}
|
||||
return &failoverConn{
|
||||
Conn: conn,
|
||||
ctx: ctx,
|
||||
dial: dial,
|
||||
readBuffer: bytes.NewBuffer(make([]byte, 0, 1000)),
|
||||
readBuffer: bytes.NewBuffer(make([]byte, 0, 1024)),
|
||||
writeBuffers: writeBuffers,
|
||||
onClose: onClose,
|
||||
}
|
||||
|
||||
@@ -86,6 +86,10 @@ func (h *Inbound) Close() error {
|
||||
)
|
||||
}
|
||||
|
||||
func (h *Inbound) UpdateUsers(users []auth.User) {
|
||||
h.authenticator.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
if h.tlsConfig != nil {
|
||||
tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig)
|
||||
|
||||
@@ -3,15 +3,17 @@ package bandwidth
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/common/onclose"
|
||||
)
|
||||
|
||||
type connWithDownloadBandwidthLimiter struct {
|
||||
net.Conn
|
||||
ctx context.Context
|
||||
limiter Limiter
|
||||
limiter BandwidthLimiter
|
||||
}
|
||||
|
||||
func NewConnWithDownloadBandwidthLimiter(ctx context.Context, conn net.Conn, limiter Limiter) *connWithDownloadBandwidthLimiter {
|
||||
func NewConnWithDownloadBandwidthLimiter(ctx context.Context, conn net.Conn, limiter BandwidthLimiter) *connWithDownloadBandwidthLimiter {
|
||||
return &connWithDownloadBandwidthLimiter{conn, ctx, limiter}
|
||||
}
|
||||
|
||||
@@ -26,10 +28,10 @@ func (conn *connWithDownloadBandwidthLimiter) Write(p []byte) (n int, err error)
|
||||
type connWithUploadBandwidthLimiter struct {
|
||||
net.Conn
|
||||
ctx context.Context
|
||||
limiter Limiter
|
||||
limiter BandwidthLimiter
|
||||
}
|
||||
|
||||
func NewConnWithUploadBandwidthLimiter(ctx context.Context, conn net.Conn, limiter Limiter) *connWithUploadBandwidthLimiter {
|
||||
func NewConnWithUploadBandwidthLimiter(ctx context.Context, conn net.Conn, limiter BandwidthLimiter) *connWithUploadBandwidthLimiter {
|
||||
return &connWithUploadBandwidthLimiter{conn, ctx, limiter}
|
||||
}
|
||||
|
||||
@@ -47,10 +49,10 @@ func (conn *connWithUploadBandwidthLimiter) Read(p []byte) (n int, err error) {
|
||||
|
||||
type connWithCloseHandler struct {
|
||||
net.Conn
|
||||
onClose CloseHandlerFunc
|
||||
onClose onclose.CloseHandlerFunc
|
||||
}
|
||||
|
||||
func NewConnWithCloseHandler(conn net.Conn, onClose CloseHandlerFunc) *connWithCloseHandler {
|
||||
func NewConnWithCloseHandler(conn net.Conn, onClose onclose.CloseHandlerFunc) *connWithCloseHandler {
|
||||
return &connWithCloseHandler{conn, onClose}
|
||||
}
|
||||
|
||||
@@ -62,10 +64,10 @@ func (conn *connWithCloseHandler) Close() error {
|
||||
type packetConnWithDownloadBandwidthLimiter struct {
|
||||
net.PacketConn
|
||||
ctx context.Context
|
||||
limiter Limiter
|
||||
limiter BandwidthLimiter
|
||||
}
|
||||
|
||||
func NewPacketConnWithDownloadBandwidthLimiter(ctx context.Context, conn net.PacketConn, limiter Limiter) *packetConnWithDownloadBandwidthLimiter {
|
||||
func NewPacketConnWithDownloadBandwidthLimiter(ctx context.Context, conn net.PacketConn, limiter BandwidthLimiter) *packetConnWithDownloadBandwidthLimiter {
|
||||
return &packetConnWithDownloadBandwidthLimiter{conn, ctx, limiter}
|
||||
}
|
||||
|
||||
@@ -80,10 +82,10 @@ func (conn *packetConnWithDownloadBandwidthLimiter) WriteTo(p []byte, addr net.A
|
||||
type packetConnWithUploadBandwidthLimiter struct {
|
||||
net.PacketConn
|
||||
ctx context.Context
|
||||
limiter Limiter
|
||||
limiter BandwidthLimiter
|
||||
}
|
||||
|
||||
func NewPacketConnWithUploadBandwidthLimiter(ctx context.Context, conn net.PacketConn, limiter Limiter) *packetConnWithUploadBandwidthLimiter {
|
||||
func NewPacketConnWithUploadBandwidthLimiter(ctx context.Context, conn net.PacketConn, limiter BandwidthLimiter) *packetConnWithUploadBandwidthLimiter {
|
||||
return &packetConnWithUploadBandwidthLimiter{conn, ctx, limiter}
|
||||
}
|
||||
|
||||
@@ -101,10 +103,10 @@ func (conn *packetConnWithUploadBandwidthLimiter) ReadFrom(p []byte) (n int, add
|
||||
|
||||
type packetConnWithCloseHandler struct {
|
||||
net.PacketConn
|
||||
onClose CloseHandlerFunc
|
||||
onClose onclose.CloseHandlerFunc
|
||||
}
|
||||
|
||||
func NewPacketConnWithCloseHandler(conn net.PacketConn, onClose CloseHandlerFunc) *packetConnWithCloseHandler {
|
||||
func NewPacketConnWithCloseHandler(conn net.PacketConn, onClose onclose.CloseHandlerFunc) *packetConnWithCloseHandler {
|
||||
return &packetConnWithCloseHandler{conn, onClose}
|
||||
}
|
||||
|
||||
@@ -113,38 +115,38 @@ func (conn *packetConnWithCloseHandler) Close() error {
|
||||
return conn.PacketConn.Close()
|
||||
}
|
||||
|
||||
func connWithDownloadBandwidthWrapper(ctx context.Context, conn net.Conn, limiter Limiter, reverse bool) net.Conn {
|
||||
func connWithDownloadBandwidthWrapper(ctx context.Context, conn net.Conn, limiter BandwidthLimiter, reverse bool) net.Conn {
|
||||
if reverse {
|
||||
return NewConnWithUploadBandwidthLimiter(ctx, conn, limiter)
|
||||
}
|
||||
return NewConnWithDownloadBandwidthLimiter(ctx, conn, limiter)
|
||||
}
|
||||
|
||||
func connWithUploadBandwidthWrapper(ctx context.Context, conn net.Conn, limiter Limiter, reverse bool) net.Conn {
|
||||
func connWithUploadBandwidthWrapper(ctx context.Context, conn net.Conn, limiter BandwidthLimiter, reverse bool) net.Conn {
|
||||
if reverse {
|
||||
return NewConnWithDownloadBandwidthLimiter(ctx, conn, limiter)
|
||||
}
|
||||
return NewConnWithUploadBandwidthLimiter(ctx, conn, limiter)
|
||||
}
|
||||
|
||||
func connWithBidirectionalBandwidthWrapper(ctx context.Context, conn net.Conn, limiter Limiter, reverse bool) net.Conn {
|
||||
func connWithBidirectionalBandwidthWrapper(ctx context.Context, conn net.Conn, limiter BandwidthLimiter, reverse bool) net.Conn {
|
||||
return NewConnWithUploadBandwidthLimiter(ctx, NewConnWithDownloadBandwidthLimiter(ctx, conn, limiter), limiter)
|
||||
}
|
||||
|
||||
func packetConnWithDownloadBandwidthWrapper(ctx context.Context, conn net.PacketConn, limiter Limiter, reverse bool) net.PacketConn {
|
||||
func packetConnWithDownloadBandwidthWrapper(ctx context.Context, conn net.PacketConn, limiter BandwidthLimiter, reverse bool) net.PacketConn {
|
||||
if reverse {
|
||||
return NewPacketConnWithUploadBandwidthLimiter(ctx, conn, limiter)
|
||||
}
|
||||
return NewPacketConnWithDownloadBandwidthLimiter(ctx, conn, limiter)
|
||||
}
|
||||
|
||||
func packetConnWithUploadBandwidthWrapper(ctx context.Context, conn net.PacketConn, limiter Limiter, reverse bool) net.PacketConn {
|
||||
func packetConnWithUploadBandwidthWrapper(ctx context.Context, conn net.PacketConn, limiter BandwidthLimiter, reverse bool) net.PacketConn {
|
||||
if reverse {
|
||||
return NewPacketConnWithDownloadBandwidthLimiter(ctx, conn, limiter)
|
||||
}
|
||||
return NewPacketConnWithUploadBandwidthLimiter(ctx, conn, limiter)
|
||||
}
|
||||
|
||||
func packetConnWithBidirectionalBandwidthWrapper(ctx context.Context, conn net.PacketConn, limiter Limiter, reverse bool) net.PacketConn {
|
||||
func packetConnWithBidirectionalBandwidthWrapper(ctx context.Context, conn net.PacketConn, limiter BandwidthLimiter, reverse bool) net.PacketConn {
|
||||
return NewPacketConnWithUploadBandwidthLimiter(ctx, NewPacketConnWithDownloadBandwidthLimiter(ctx, conn, limiter), limiter)
|
||||
}
|
||||
|
||||
@@ -9,12 +9,13 @@ import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
)
|
||||
|
||||
type Limiter interface {
|
||||
type BandwidthLimiter interface {
|
||||
WaitN(ctx context.Context, n int) (err error)
|
||||
SetSpeed(speed uint64)
|
||||
}
|
||||
|
||||
type FlowKeysLimiter struct {
|
||||
limiter Limiter
|
||||
limiter BandwidthLimiter
|
||||
connIDGetter ConnIDGetter
|
||||
|
||||
waits map[string][]*wait
|
||||
@@ -25,7 +26,7 @@ type FlowKeysLimiter struct {
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewFlowKeysLimiter(connIDGetter ConnIDGetter, limiter Limiter) *FlowKeysLimiter {
|
||||
func NewFlowKeysLimiter(connIDGetter ConnIDGetter, limiter BandwidthLimiter) *FlowKeysLimiter {
|
||||
return &FlowKeysLimiter{
|
||||
limiter: limiter,
|
||||
connIDGetter: connIDGetter,
|
||||
@@ -36,6 +37,10 @@ func NewFlowKeysLimiter(connIDGetter ConnIDGetter, limiter Limiter) *FlowKeysLim
|
||||
}
|
||||
}
|
||||
|
||||
func (l *FlowKeysLimiter) SetSpeed(speed uint64) {
|
||||
l.limiter.SetSpeed(speed)
|
||||
}
|
||||
|
||||
func (l *FlowKeysLimiter) WaitN(ctx context.Context, n int) error {
|
||||
id, _ := l.connIDGetter(ctx, adapter.ContextFrom(ctx))
|
||||
mainWait := &wait{ctx, make(chan struct{}), n}
|
||||
|
||||
@@ -7,16 +7,16 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/onclose"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type (
|
||||
CloseHandlerFunc = func()
|
||||
ConnIDGetter = func(context.Context, *adapter.InboundContext) (string, bool)
|
||||
ConnWrapper = func(ctx context.Context, conn net.Conn, limiter Limiter, reverse bool) net.Conn
|
||||
PacketConnWrapper = func(ctx context.Context, conn net.PacketConn, limiter Limiter, reverse bool) net.PacketConn
|
||||
ConnWrapper = func(ctx context.Context, conn net.Conn, limiter BandwidthLimiter, reverse bool) net.Conn
|
||||
PacketConnWrapper = func(ctx context.Context, conn net.PacketConn, limiter BandwidthLimiter, reverse bool) net.PacketConn
|
||||
)
|
||||
|
||||
type BandwidthStrategy interface {
|
||||
@@ -24,8 +24,12 @@ type BandwidthStrategy interface {
|
||||
wrapPacketConn(ctx context.Context, conn net.PacketConn, metadata *adapter.InboundContext, reverse bool) (net.PacketConn, error)
|
||||
}
|
||||
|
||||
type SpeedUpdater interface {
|
||||
SetSpeed(speed uint64)
|
||||
}
|
||||
|
||||
type BandwidthLimiterStrategy interface {
|
||||
getLimiter(ctx context.Context, metadata *adapter.InboundContext) (Limiter, CloseHandlerFunc, error)
|
||||
getLimiter(ctx context.Context, metadata *adapter.InboundContext) (BandwidthLimiter, onclose.CloseHandlerFunc, error)
|
||||
}
|
||||
|
||||
type DefaultWrapStrategy struct {
|
||||
@@ -54,8 +58,14 @@ func (s *DefaultWrapStrategy) wrapPacketConn(ctx context.Context, conn net.Packe
|
||||
return NewPacketConnWithCloseHandler(s.packetConnWrapper(ctx, conn, limiter, reverse), onClose), nil
|
||||
}
|
||||
|
||||
func (s *DefaultWrapStrategy) SetSpeed(speed uint64) {
|
||||
if updater, ok := s.limiterStrategy.(SpeedUpdater); ok {
|
||||
updater.SetSpeed(speed)
|
||||
}
|
||||
}
|
||||
|
||||
type GlobalBandwidthStrategy struct {
|
||||
limiter Limiter
|
||||
limiter BandwidthLimiter
|
||||
}
|
||||
|
||||
func NewGlobalBandwidthStrategy(speed uint64, flowKeys []string) (*GlobalBandwidthStrategy, error) {
|
||||
@@ -68,12 +78,16 @@ func NewGlobalBandwidthStrategy(speed uint64, flowKeys []string) (*GlobalBandwid
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *GlobalBandwidthStrategy) getLimiter(ctx context.Context, metadata *adapter.InboundContext) (Limiter, CloseHandlerFunc, error) {
|
||||
func (s *GlobalBandwidthStrategy) getLimiter(ctx context.Context, metadata *adapter.InboundContext) (BandwidthLimiter, onclose.CloseHandlerFunc, error) {
|
||||
return s.limiter, func() {}, nil
|
||||
}
|
||||
|
||||
func (s *GlobalBandwidthStrategy) SetSpeed(speed uint64) {
|
||||
s.limiter.SetSpeed(speed)
|
||||
}
|
||||
|
||||
type idBandwidthLimiter struct {
|
||||
limiter Limiter
|
||||
limiter BandwidthLimiter
|
||||
handles uint32
|
||||
}
|
||||
|
||||
@@ -94,7 +108,7 @@ func NewConnectionBandwidthStrategy(connIDGetter ConnIDGetter, speed uint64, flo
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ConnectionBandwidthStrategy) getLimiter(ctx context.Context, metadata *adapter.InboundContext) (Limiter, CloseHandlerFunc, error) {
|
||||
func (s *ConnectionBandwidthStrategy) getLimiter(ctx context.Context, metadata *adapter.InboundContext) (BandwidthLimiter, onclose.CloseHandlerFunc, error) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
id, ok := s.connIDGetter(ctx, metadata)
|
||||
@@ -126,6 +140,15 @@ func (s *ConnectionBandwidthStrategy) getLimiter(ctx context.Context, metadata *
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ConnectionBandwidthStrategy) SetSpeed(speed uint64) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
s.speed = speed
|
||||
for _, limiter := range s.limiters {
|
||||
limiter.limiter.SetSpeed(speed)
|
||||
}
|
||||
}
|
||||
|
||||
type UsersBandwidthStrategy struct {
|
||||
strategies map[string]BandwidthStrategy
|
||||
mtx sync.Mutex
|
||||
@@ -167,20 +190,84 @@ func (s *UsersBandwidthStrategy) getStrategy(ctx context.Context, metadata *adap
|
||||
return nil, E.New("user strategy not found: ", user)
|
||||
}
|
||||
|
||||
type bwConnEntry struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
type ManagerBandwidthStrategy struct {
|
||||
*UsersBandwidthStrategy
|
||||
strategies map[string]BandwidthStrategy
|
||||
conns map[string][]*bwConnEntry
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewManagerBandwidthStrategy() *ManagerBandwidthStrategy {
|
||||
return &ManagerBandwidthStrategy{
|
||||
UsersBandwidthStrategy: NewUsersBandwidthStrategy(map[string]BandwidthStrategy{}),
|
||||
strategies: make(map[string]BandwidthStrategy),
|
||||
conns: make(map[string][]*bwConnEntry),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ManagerBandwidthStrategy) wrapConn(ctx context.Context, conn net.Conn, metadata *adapter.InboundContext, reverse bool) (net.Conn, error) {
|
||||
s.mtx.Lock()
|
||||
var user string
|
||||
if metadata != nil {
|
||||
user = metadata.User
|
||||
}
|
||||
strategy, ok := s.strategies[user]
|
||||
s.mtx.Unlock()
|
||||
if !ok {
|
||||
return nil, E.New("user strategy not found: ", user)
|
||||
}
|
||||
wrapped, err := strategy.wrapConn(ctx, conn, metadata, reverse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry := &bwConnEntry{conn: conn}
|
||||
s.mtx.Lock()
|
||||
s.conns[user] = append(s.conns[user], entry)
|
||||
s.mtx.Unlock()
|
||||
return onclose.NewConn(wrapped, func() {
|
||||
s.mtx.Lock()
|
||||
entries := s.conns[user]
|
||||
for i, e := range entries {
|
||||
if e == entry {
|
||||
s.conns[user] = append(entries[:i], entries[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mtx.Unlock()
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (s *ManagerBandwidthStrategy) wrapPacketConn(ctx context.Context, conn net.PacketConn, metadata *adapter.InboundContext, reverse bool) (net.PacketConn, error) {
|
||||
s.mtx.Lock()
|
||||
var user string
|
||||
if metadata != nil {
|
||||
user = metadata.User
|
||||
}
|
||||
strategy, ok := s.strategies[user]
|
||||
s.mtx.Unlock()
|
||||
if !ok {
|
||||
return nil, E.New("user strategy not found: ", user)
|
||||
}
|
||||
return strategy.wrapPacketConn(ctx, conn, metadata, reverse)
|
||||
}
|
||||
|
||||
func (s *ManagerBandwidthStrategy) UpdateStrategies(strategies map[string]BandwidthStrategy) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
var closedEntries []*bwConnEntry
|
||||
for user, entries := range s.conns {
|
||||
if _, exists := strategies[user]; !exists {
|
||||
closedEntries = append(closedEntries, entries...)
|
||||
delete(s.conns, user)
|
||||
}
|
||||
}
|
||||
s.strategies = strategies
|
||||
s.mtx.Unlock()
|
||||
for _, entry := range closedEntries {
|
||||
entry.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type BypassBandwidthStrategy struct{}
|
||||
@@ -263,8 +350,8 @@ func CreateStrategy(strategy string, mode string, connectionType string, speed u
|
||||
return NewDefaultWrapStrategy(limiterStrategy, connWrapper, packetConnWrapper), nil
|
||||
}
|
||||
|
||||
func createSpeedLimiter(speed uint64, flowKeys []string) (Limiter, error) {
|
||||
var limiter Limiter = rate.NewLimiter(rate.Limit(float64(speed)), 65536)
|
||||
func createSpeedLimiter(speed uint64, flowKeys []string) (BandwidthLimiter, error) {
|
||||
var limiter BandwidthLimiter = &speedLimiter{limiter: rate.NewLimiter(rate.Limit(float64(speed)), 65536)}
|
||||
for i := len(flowKeys) - 1; i >= 0; i-- {
|
||||
getter, err := flowKeysConnIDGetter(flowKeys[i])
|
||||
if err != nil {
|
||||
@@ -275,16 +362,24 @@ func createSpeedLimiter(speed uint64, flowKeys []string) (Limiter, error) {
|
||||
return limiter, nil
|
||||
}
|
||||
|
||||
type speedLimiter struct {
|
||||
limiter *rate.Limiter
|
||||
}
|
||||
|
||||
func (r *speedLimiter) WaitN(ctx context.Context, n int) error {
|
||||
return r.limiter.WaitN(ctx, n)
|
||||
}
|
||||
|
||||
func (r *speedLimiter) SetSpeed(speed uint64) {
|
||||
r.limiter.SetLimit(rate.Limit(float64(speed)))
|
||||
}
|
||||
|
||||
func flowKeysConnIDGetter(name string) (ConnIDGetter, error) {
|
||||
switch name {
|
||||
case "user":
|
||||
return func(ctx context.Context, metadata *adapter.InboundContext) (string, bool) {
|
||||
return metadata.User, true
|
||||
}, nil
|
||||
case "destination":
|
||||
return func(ctx context.Context, metadata *adapter.InboundContext) (string, bool) {
|
||||
return metadata.Destination.String(), true
|
||||
}, nil
|
||||
case "source_ip":
|
||||
return func(ctx context.Context, metadata *adapter.InboundContext) (string, bool) {
|
||||
return metadata.Source.IPAddr().String(), true
|
||||
@@ -302,6 +397,14 @@ func flowKeysConnIDGetter(name string) (ConnIDGetter, error) {
|
||||
}
|
||||
return strconv.FormatUint(uint64(id.ID), 10), ok
|
||||
}, nil
|
||||
case "protocol":
|
||||
return func(ctx context.Context, metadata *adapter.InboundContext) (string, bool) {
|
||||
return metadata.Protocol, metadata.Protocol != ""
|
||||
}, nil
|
||||
case "destination":
|
||||
return func(ctx context.Context, metadata *adapter.InboundContext) (string, bool) {
|
||||
return metadata.Destination.String(), true
|
||||
}, nil
|
||||
default:
|
||||
return nil, E.New("flow key not found: ", name)
|
||||
}
|
||||
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/common/onclose"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func NewDefaultLock(max uint32) LockIDGetter {
|
||||
locks := make(map[string]*uint32)
|
||||
mtx := sync.Mutex{}
|
||||
return func(id string) (CloseHandlerFunc, context.Context, error) {
|
||||
return func(id string) (onclose.CloseHandlerFunc, context.Context, error) {
|
||||
mtx.Lock()
|
||||
defer mtx.Unlock()
|
||||
handles, ok := locks[id]
|
||||
@@ -22,16 +23,13 @@ func NewDefaultLock(max uint32) LockIDGetter {
|
||||
locks[id] = handles
|
||||
}
|
||||
*handles++
|
||||
var once sync.Once
|
||||
return func() {
|
||||
once.Do(func() {
|
||||
mtx.Lock()
|
||||
defer mtx.Unlock()
|
||||
*handles--
|
||||
if *handles == 0 {
|
||||
delete(locks, id)
|
||||
}
|
||||
})
|
||||
mtx.Lock()
|
||||
defer mtx.Unlock()
|
||||
*handles--
|
||||
if *handles == 0 {
|
||||
delete(locks, id)
|
||||
}
|
||||
}, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/common/onclose"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@@ -110,7 +111,7 @@ func (h *Outbound) DialContext(ctx context.Context, network string, destination
|
||||
onClose()
|
||||
return nil, err
|
||||
}
|
||||
conn = newConnWithCloseHandlerFunc(conn, onClose)
|
||||
conn = onclose.NewConn(conn, onClose)
|
||||
if lockCtx != nil {
|
||||
go connChecker(lockCtx, conn.Close)
|
||||
}
|
||||
@@ -127,7 +128,7 @@ func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
|
||||
onClose()
|
||||
return nil, err
|
||||
}
|
||||
conn = newPacketConnWithCloseHandlerFunc(conn, onClose)
|
||||
conn = onclose.NewPacketConn(conn, onClose)
|
||||
if lockCtx != nil {
|
||||
go connChecker(lockCtx, conn.Close)
|
||||
}
|
||||
@@ -141,7 +142,7 @@ func (h *Outbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
return
|
||||
}
|
||||
conn = newConnWithCloseHandlerFunc(conn, limiterOnClose)
|
||||
conn = onclose.NewConn(conn, limiterOnClose)
|
||||
if lockCtx != nil {
|
||||
go connChecker(lockCtx, conn.Close)
|
||||
}
|
||||
@@ -158,7 +159,7 @@ func (h *Outbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
return
|
||||
}
|
||||
conn = bufio.NewPacketConn(newPacketConnWithCloseHandlerFunc(bufio.NewNetPacketConn(conn), limiterOnClose))
|
||||
conn = bufio.NewPacketConn(onclose.NewPacketConn(bufio.NewNetPacketConn(conn), limiterOnClose))
|
||||
if lockCtx != nil {
|
||||
go connChecker(lockCtx, conn.Close)
|
||||
}
|
||||
@@ -172,34 +173,6 @@ func (h *Outbound) GetStrategy() ConnectionStrategy {
|
||||
return h.strategy
|
||||
}
|
||||
|
||||
type connWithCloseHandlerFunc struct {
|
||||
net.Conn
|
||||
onClose CloseHandlerFunc
|
||||
}
|
||||
|
||||
func newConnWithCloseHandlerFunc(conn net.Conn, onClose CloseHandlerFunc) *connWithCloseHandlerFunc {
|
||||
return &connWithCloseHandlerFunc{conn, onClose}
|
||||
}
|
||||
|
||||
func (conn *connWithCloseHandlerFunc) Close() error {
|
||||
conn.onClose()
|
||||
return conn.Conn.Close()
|
||||
}
|
||||
|
||||
type packetConnWithCloseHandlerFunc struct {
|
||||
net.PacketConn
|
||||
onClose CloseHandlerFunc
|
||||
}
|
||||
|
||||
func newPacketConnWithCloseHandlerFunc(conn net.PacketConn, onClose CloseHandlerFunc) *packetConnWithCloseHandlerFunc {
|
||||
return &packetConnWithCloseHandlerFunc{conn, onClose}
|
||||
}
|
||||
|
||||
func (conn *packetConnWithCloseHandlerFunc) Close() error {
|
||||
conn.onClose()
|
||||
return conn.PacketConn.Close()
|
||||
}
|
||||
|
||||
func connChecker(ctx context.Context, closeFunc func() error) {
|
||||
<-ctx.Done()
|
||||
closeFunc()
|
||||
|
||||
@@ -6,18 +6,17 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/onclose"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type (
|
||||
CloseHandlerFunc = func()
|
||||
|
||||
ConnIDGetter = func(context.Context, *adapter.InboundContext) (string, bool)
|
||||
LockIDGetter = func(string) (CloseHandlerFunc, context.Context, error)
|
||||
LockIDGetter = func(string) (onclose.CloseHandlerFunc, context.Context, error)
|
||||
|
||||
ConnectionStrategy interface {
|
||||
request(ctx context.Context, metadata *adapter.InboundContext) (onClose CloseHandlerFunc, lockCtx context.Context, err error)
|
||||
request(ctx context.Context, metadata *adapter.InboundContext) (onClose onclose.CloseHandlerFunc, lockCtx context.Context, err error)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -36,7 +35,7 @@ func NewDefaultConnectionStrategy(connIDGetter ConnIDGetter, lockIDGetter LockID
|
||||
return outbound
|
||||
}
|
||||
|
||||
func (s *DefaultConnectionStrategy) request(ctx context.Context, metadata *adapter.InboundContext) (CloseHandlerFunc, context.Context, error) {
|
||||
func (s *DefaultConnectionStrategy) request(ctx context.Context, metadata *adapter.InboundContext) (onclose.CloseHandlerFunc, context.Context, error) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
id, ok := s.connIDGetter(ctx, metadata)
|
||||
@@ -57,7 +56,7 @@ func NewUsersConnectionStrategy(strategies map[string]ConnectionStrategy) *Users
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UsersConnectionStrategy) request(ctx context.Context, metadata *adapter.InboundContext) (CloseHandlerFunc, context.Context, error) {
|
||||
func (s *UsersConnectionStrategy) request(ctx context.Context, metadata *adapter.InboundContext) (onclose.CloseHandlerFunc, context.Context, error) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
var user string
|
||||
@@ -71,20 +70,78 @@ func (s *UsersConnectionStrategy) request(ctx context.Context, metadata *adapter
|
||||
return nil, nil, E.New("user strategy not found: ", user)
|
||||
}
|
||||
|
||||
type cancelEntry struct {
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type ManagerConnectionStrategy struct {
|
||||
*UsersConnectionStrategy
|
||||
strategies map[string]ConnectionStrategy
|
||||
cancels map[string][]*cancelEntry
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewManagerConnectionStrategy() *ManagerConnectionStrategy {
|
||||
return &ManagerConnectionStrategy{
|
||||
UsersConnectionStrategy: NewUsersConnectionStrategy(map[string]ConnectionStrategy{}),
|
||||
strategies: make(map[string]ConnectionStrategy),
|
||||
cancels: make(map[string][]*cancelEntry),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ManagerConnectionStrategy) request(ctx context.Context, metadata *adapter.InboundContext) (onclose.CloseHandlerFunc, context.Context, error) {
|
||||
s.mtx.Lock()
|
||||
var user string
|
||||
if metadata != nil {
|
||||
user = metadata.User
|
||||
}
|
||||
strategy, ok := s.strategies[user]
|
||||
if !ok {
|
||||
s.mtx.Unlock()
|
||||
return nil, nil, E.New("user strategy not found: ", user)
|
||||
}
|
||||
s.mtx.Unlock()
|
||||
onClose, _, err := strategy.request(ctx, metadata)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
cancelCtx, cancel := context.WithCancel(context.Background())
|
||||
entry := &cancelEntry{cancel: cancel}
|
||||
s.mtx.Lock()
|
||||
s.cancels[user] = append(s.cancels[user], entry)
|
||||
s.mtx.Unlock()
|
||||
originalOnClose := onClose
|
||||
wrappedOnClose := func() {
|
||||
s.mtx.Lock()
|
||||
entries := s.cancels[user]
|
||||
for i, e := range entries {
|
||||
if e == entry {
|
||||
s.cancels[user] = append(entries[:i], entries[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mtx.Unlock()
|
||||
cancel()
|
||||
if originalOnClose != nil {
|
||||
originalOnClose()
|
||||
}
|
||||
}
|
||||
return wrappedOnClose, cancelCtx, nil
|
||||
}
|
||||
|
||||
func (s *ManagerConnectionStrategy) UpdateStrategies(strategies map[string]ConnectionStrategy) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
var entries []*cancelEntry
|
||||
for user, cancels := range s.cancels {
|
||||
if _, exists := strategies[user]; !exists {
|
||||
entries = append(entries, cancels...)
|
||||
delete(s.cancels, user)
|
||||
}
|
||||
}
|
||||
s.strategies = strategies
|
||||
s.mtx.Unlock()
|
||||
for _, entry := range entries {
|
||||
entry.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
type BypassConnectionStrategy struct{}
|
||||
@@ -93,7 +150,7 @@ func NewBypassConnectionStrategy() *BypassConnectionStrategy {
|
||||
return &BypassConnectionStrategy{}
|
||||
}
|
||||
|
||||
func (s *BypassConnectionStrategy) request(ctx context.Context, metadata *adapter.InboundContext) (CloseHandlerFunc, context.Context, error) {
|
||||
func (s *BypassConnectionStrategy) request(ctx context.Context, metadata *adapter.InboundContext) (onclose.CloseHandlerFunc, context.Context, error) {
|
||||
return func() {}, nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/onclose"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type (
|
||||
CloseHandlerFunc = func()
|
||||
ConnWrapper = func(ctx context.Context, conn net.Conn, limiter TrafficLimiter, reverse bool) net.Conn
|
||||
PacketConnWrapper = func(ctx context.Context, conn net.PacketConn, limiter TrafficLimiter, reverse bool) net.PacketConn
|
||||
)
|
||||
@@ -72,32 +72,58 @@ func (s *GlobalTrafficStrategy) getLimiter(ctx context.Context, metadata *adapte
|
||||
return s.limiter, nil
|
||||
}
|
||||
|
||||
type connEntry struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
type ManagerTrafficStrategy struct {
|
||||
strategies map[string]TrafficStrategy
|
||||
mtx sync.Mutex
|
||||
conns map[string][]*connEntry
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewManagerTrafficStrategy() *ManagerTrafficStrategy {
|
||||
return &ManagerTrafficStrategy{}
|
||||
return &ManagerTrafficStrategy{
|
||||
conns: make(map[string][]*connEntry),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ManagerTrafficStrategy) wrapConn(ctx context.Context, conn net.Conn, metadata *adapter.InboundContext, reverse bool) (net.Conn, error) {
|
||||
strategy, err := s.getStrategy(ctx, metadata)
|
||||
strategy, user, err := s.getStrategy(ctx, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return strategy.wrapConn(ctx, conn, metadata, reverse)
|
||||
wrapped, err := strategy.wrapConn(ctx, conn, metadata, reverse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry := &connEntry{conn: conn}
|
||||
s.mtx.Lock()
|
||||
s.conns[user] = append(s.conns[user], entry)
|
||||
s.mtx.Unlock()
|
||||
return onclose.NewConn(wrapped, func() {
|
||||
s.mtx.Lock()
|
||||
entries := s.conns[user]
|
||||
for i, e := range entries {
|
||||
if e == entry {
|
||||
s.conns[user] = append(entries[:i], entries[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mtx.Unlock()
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (s *ManagerTrafficStrategy) wrapPacketConn(ctx context.Context, conn net.PacketConn, metadata *adapter.InboundContext, reverse bool) (net.PacketConn, error) {
|
||||
strategy, err := s.getStrategy(ctx, metadata)
|
||||
strategy, _, err := s.getStrategy(ctx, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return strategy.wrapPacketConn(ctx, conn, metadata, reverse)
|
||||
}
|
||||
|
||||
func (s *ManagerTrafficStrategy) getStrategy(ctx context.Context, metadata *adapter.InboundContext) (TrafficStrategy, error) {
|
||||
func (s *ManagerTrafficStrategy) getStrategy(ctx context.Context, metadata *adapter.InboundContext) (TrafficStrategy, string, error) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
var user string
|
||||
@@ -106,15 +132,25 @@ func (s *ManagerTrafficStrategy) getStrategy(ctx context.Context, metadata *adap
|
||||
}
|
||||
strategy, ok := s.strategies[user]
|
||||
if ok {
|
||||
return strategy, nil
|
||||
return strategy, user, nil
|
||||
}
|
||||
return nil, E.New("user strategy not found: ", user)
|
||||
return nil, user, E.New("user strategy not found: ", user)
|
||||
}
|
||||
|
||||
func (s *ManagerTrafficStrategy) UpdateStrategies(strategies map[string]TrafficStrategy) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
var closedEntries []*connEntry
|
||||
for user, entries := range s.conns {
|
||||
if _, exists := strategies[user]; !exists {
|
||||
closedEntries = append(closedEntries, entries...)
|
||||
delete(s.conns, user)
|
||||
}
|
||||
}
|
||||
s.strategies = strategies
|
||||
s.mtx.Unlock()
|
||||
for _, entry := range closedEntries {
|
||||
entry.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type BypassTrafficStrategy struct{}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/transport/masque"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
@@ -99,7 +100,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
logger.ErrorContext(ctx, E.New("failed to generate cert: ", err))
|
||||
return
|
||||
}
|
||||
tlsConfig, err := tls.NewMASQUEClient(ctx, logger, "consumer-masque.cloudflareclient.com", cert, privKey, peerPubKey, options.MASQUEOutboundTLSOptions)
|
||||
tlsConfig, err := tls.NewMASQUEClient(ctx, logger, "consumer-masque.cloudflareclient.com", cert, privKey, peerPubKey, common.PtrValueOrDefault(options.TLS))
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, E.New("failed to prepare TLS config: ", err))
|
||||
return
|
||||
@@ -135,11 +136,19 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
ctx,
|
||||
logger,
|
||||
masque.TunnelOptions{
|
||||
System: options.System,
|
||||
Name: options.Name,
|
||||
CreateDialer: func(interfaceName string) N.Dialer {
|
||||
return common.Must1(dialer.NewDefault(ctx, option.DialerOptions{
|
||||
BindInterface: interfaceName,
|
||||
}))
|
||||
},
|
||||
Dialer: outboundDialer,
|
||||
Address: []netip.Prefix{
|
||||
netip.MustParsePrefix(appConfig.IPv4 + "/32"),
|
||||
netip.MustParsePrefix(appConfig.IPv6 + "/128"),
|
||||
},
|
||||
AllowedAddress: options.AllowedIPs,
|
||||
Endpoint: endpoint,
|
||||
TLSConfig: tlsConfig,
|
||||
UseHTTP2: options.UseHTTP2,
|
||||
|
||||
347
protocol/mieru/inbound.go
Normal file
347
protocol/mieru/inbound.go
Normal file
@@ -0,0 +1,347 @@
|
||||
package mieru
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
"github.com/sagernet/sing-box/common/uot"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
mierucommon "github.com/enfein/mieru/v3/apis/common"
|
||||
mieruconstant "github.com/enfein/mieru/v3/apis/constant"
|
||||
mierumodel "github.com/enfein/mieru/v3/apis/model"
|
||||
mieruserver "github.com/enfein/mieru/v3/apis/server"
|
||||
mierutp "github.com/enfein/mieru/v3/apis/trafficpattern"
|
||||
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func RegisterInbound(registry *inbound.Registry) {
|
||||
inbound.Register[option.MieruInboundOptions](registry, C.TypeMieru, NewInbound)
|
||||
}
|
||||
|
||||
type Inbound struct {
|
||||
inbound.Adapter
|
||||
ctx context.Context
|
||||
router adapter.ConnectionRouterEx
|
||||
logger log.ContextLogger
|
||||
listener *listener.Listener
|
||||
server mieruserver.Server
|
||||
userNames []string
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.MieruInboundOptions) (adapter.Inbound, error) {
|
||||
config, userNames, err := buildMieruServerConfig(ctx, options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build mieru server config: %w", err)
|
||||
}
|
||||
|
||||
s := mieruserver.NewServer()
|
||||
if err := s.Store(config); err != nil {
|
||||
return nil, fmt.Errorf("failed to store mieru server config: %w", err)
|
||||
}
|
||||
|
||||
inboundInstance := &Inbound{
|
||||
Adapter: inbound.NewAdapter(C.TypeMieru, tag),
|
||||
ctx: ctx,
|
||||
router: uot.NewRouter(router, logger),
|
||||
logger: logger,
|
||||
server: s,
|
||||
userNames: userNames,
|
||||
}
|
||||
inboundInstance.listener = listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Network: []string{N.NetworkTCP, N.NetworkUDP},
|
||||
Listen: options.ListenOptions,
|
||||
})
|
||||
|
||||
return inboundInstance, nil
|
||||
}
|
||||
|
||||
func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if err := h.server.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start mieru server: %w", err)
|
||||
}
|
||||
|
||||
h.logger.Notice("mieru server is started")
|
||||
go h.acceptLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Inbound) Close() error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if h.server.IsRunning() {
|
||||
return h.server.Stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Inbound) acceptLoop() {
|
||||
for {
|
||||
conn, request, err := h.server.Accept()
|
||||
if err != nil {
|
||||
if !h.server.IsRunning() {
|
||||
return
|
||||
}
|
||||
h.logger.Debug("failed to accept mieru connection: ", err)
|
||||
continue
|
||||
}
|
||||
go h.handleConnection(conn, request)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Inbound) handleConnection(conn net.Conn, request *mierumodel.Request) {
|
||||
ctx := log.ContextWithNewID(h.ctx)
|
||||
|
||||
// Send fake SOCKS5 response back to proxy client.
|
||||
resp := &mierumodel.Response{
|
||||
Reply: mieruconstant.Socks5ReplySuccess,
|
||||
BindAddr: mierumodel.AddrSpec{
|
||||
IP: net.IPv4zero,
|
||||
Port: 0,
|
||||
},
|
||||
}
|
||||
if err := resp.WriteToSocks5(conn); err != nil {
|
||||
conn.Close()
|
||||
h.logger.DebugContext(ctx, "failed to write mieru response: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Build metadata.
|
||||
var metadata adapter.InboundContext
|
||||
metadata.Inbound = h.Tag()
|
||||
metadata.InboundType = h.Type()
|
||||
//nolint:staticcheck
|
||||
metadata.InboundDetour = h.listener.ListenOptions().Detour
|
||||
metadata.UDPDisableDomainUnmapping = h.listener.ListenOptions().UDPDisableDomainUnmapping
|
||||
|
||||
// Parse source address.
|
||||
if remoteAddr := conn.RemoteAddr(); remoteAddr != nil {
|
||||
metadata.Source = M.SocksaddrFromNet(remoteAddr)
|
||||
}
|
||||
|
||||
// Parse destination from request.
|
||||
if request.DstAddr.FQDN != "" {
|
||||
metadata.Destination = M.Socksaddr{
|
||||
Fqdn: request.DstAddr.FQDN,
|
||||
Port: uint16(request.DstAddr.Port),
|
||||
}
|
||||
} else if request.DstAddr.IP != nil {
|
||||
addr, _ := netip.AddrFromSlice(request.DstAddr.IP)
|
||||
metadata.Destination = M.Socksaddr{
|
||||
Addr: addr.Unmap(),
|
||||
Port: uint16(request.DstAddr.Port),
|
||||
}
|
||||
}
|
||||
|
||||
// Get username from connection.
|
||||
if userCtx, ok := conn.(mierucommon.UserContext); ok {
|
||||
metadata.User = userCtx.UserName()
|
||||
}
|
||||
|
||||
// Handle request.
|
||||
switch request.Command {
|
||||
case mieruconstant.Socks5ConnectCmd:
|
||||
h.logger.InfoContext(ctx, "inbound TCP connection from ", metadata.Source, " to ", metadata.Destination)
|
||||
if metadata.User != "" {
|
||||
h.logger.InfoContext(ctx, "[", metadata.User, "] inbound TCP connection")
|
||||
}
|
||||
h.router.RouteConnectionEx(ctx, conn, metadata, nil)
|
||||
case mieruconstant.Socks5UDPAssociateCmd:
|
||||
h.logger.InfoContext(ctx, "inbound UDP connection from ", metadata.Source, " to ", metadata.Destination)
|
||||
if metadata.User != "" {
|
||||
h.logger.InfoContext(ctx, "[", metadata.User, "] inbound UDP connection")
|
||||
}
|
||||
h.handleUDP(ctx, conn, metadata)
|
||||
default:
|
||||
conn.Close()
|
||||
h.logger.WarnContext(ctx, "unsupported mieru command: ", request.Command)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Inbound) handleUDP(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) {
|
||||
pc := mierucommon.NewPacketOverStreamTunnel(conn)
|
||||
packetConn := &mieruPacketConn{
|
||||
PacketConn: pc,
|
||||
destination: metadata.Destination,
|
||||
}
|
||||
h.router.RoutePacketConnectionEx(ctx, packetConn, metadata, nil)
|
||||
}
|
||||
|
||||
// mieruPacketConn wraps mieru's PacketConn to implement N.PacketConn
|
||||
type mieruPacketConn struct {
|
||||
net.PacketConn
|
||||
destination M.Socksaddr
|
||||
}
|
||||
|
||||
var _ N.PacketConn = (*mieruPacketConn)(nil)
|
||||
|
||||
// ReadPacket parses the SOCKS5 UDP header and returns the destination address.
|
||||
func (c *mieruPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||
n, _, err := c.PacketConn.ReadFrom(buffer.FreeBytes())
|
||||
if err != nil {
|
||||
return M.Socksaddr{}, err
|
||||
}
|
||||
buffer.Truncate(n)
|
||||
if buffer.Len() < 3 {
|
||||
return M.Socksaddr{}, io.ErrShortBuffer
|
||||
}
|
||||
|
||||
// Skip RSV (2 bytes) and FRAG (1 byte).
|
||||
buffer.Advance(3)
|
||||
|
||||
var addr mierumodel.AddrSpec
|
||||
if err := addr.ReadFromSocks5(buffer); err != nil {
|
||||
return M.Socksaddr{}, err
|
||||
}
|
||||
if addr.FQDN != "" {
|
||||
destination = M.Socksaddr{
|
||||
Fqdn: addr.FQDN,
|
||||
Port: uint16(addr.Port),
|
||||
}
|
||||
} else if addr.IP != nil {
|
||||
netAddr, _ := netip.AddrFromSlice(addr.IP)
|
||||
destination = M.Socksaddr{
|
||||
Addr: netAddr.Unmap(),
|
||||
Port: uint16(addr.Port),
|
||||
}
|
||||
}
|
||||
return destination, nil
|
||||
}
|
||||
|
||||
// WritePacket writes the SOCKS5 UDP header and the payload.
|
||||
func (c *mieruPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||
header := buf.NewSize(3 + M.MaxSocksaddrLength)
|
||||
defer header.Release()
|
||||
|
||||
// RSV (2 bytes) + FRAG (1 byte)
|
||||
common.Must(header.WriteZeroN(3))
|
||||
|
||||
var addr mierumodel.AddrSpec
|
||||
if destination.IsFqdn() {
|
||||
addr.FQDN = destination.Fqdn
|
||||
} else {
|
||||
addr.IP = destination.Addr.AsSlice()
|
||||
}
|
||||
addr.Port = int(destination.Port)
|
||||
if err := addr.WriteToSocks5(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
packet := buf.NewSize(header.Len() + buffer.Len())
|
||||
defer packet.Release()
|
||||
common.Must1(packet.Write(header.Bytes()))
|
||||
common.Must1(packet.Write(buffer.Bytes()))
|
||||
_, err := c.PacketConn.WriteTo(packet.Bytes(), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func buildMieruServerConfig(_ context.Context, options option.MieruInboundOptions) (*mieruserver.ServerConfig, []string, error) {
|
||||
if err := validateMieruInboundOptions(options); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to validate mieru options: %w", err)
|
||||
}
|
||||
|
||||
var transportProtocol *mierupb.TransportProtocol
|
||||
switch options.Transport {
|
||||
case "TCP":
|
||||
transportProtocol = mierupb.TransportProtocol_TCP.Enum()
|
||||
case "UDP":
|
||||
transportProtocol = mierupb.TransportProtocol_UDP.Enum()
|
||||
}
|
||||
|
||||
if options.ListenOptions.ListenPort == 0 && len(options.ListenPorts) == 0 {
|
||||
return nil, nil, E.New("either listen_port or listen_ports must be set")
|
||||
}
|
||||
var portBindings []*mierupb.PortBinding
|
||||
if options.ListenOptions.ListenPort != 0 {
|
||||
portBindings = append(portBindings, &mierupb.PortBinding{
|
||||
Port: proto.Int32(int32(options.ListenOptions.ListenPort)),
|
||||
Protocol: transportProtocol,
|
||||
})
|
||||
}
|
||||
for _, pr := range options.ListenPorts {
|
||||
portBindings = append(portBindings, &mierupb.PortBinding{
|
||||
PortRange: proto.String(pr),
|
||||
Protocol: transportProtocol,
|
||||
})
|
||||
}
|
||||
|
||||
var users []*mierupb.User
|
||||
var userNames []string
|
||||
for _, user := range options.Users {
|
||||
users = append(users, &mierupb.User{
|
||||
Name: proto.String(user.Name),
|
||||
Password: proto.String(user.Password),
|
||||
})
|
||||
userNames = append(userNames, user.Name)
|
||||
}
|
||||
var trafficPattern *mierupb.TrafficPattern
|
||||
trafficPattern, _ = mierutp.Decode(options.TrafficPattern)
|
||||
var advancedSettings *mierupb.ServerAdvancedSettings
|
||||
if options.UserHintIsMandatory {
|
||||
advancedSettings = &mierupb.ServerAdvancedSettings{
|
||||
UserHintIsMandatory: proto.Bool(true),
|
||||
}
|
||||
}
|
||||
return &mieruserver.ServerConfig{
|
||||
Config: &mierupb.ServerConfig{
|
||||
PortBindings: portBindings,
|
||||
Users: users,
|
||||
TrafficPattern: trafficPattern,
|
||||
AdvancedSettings: advancedSettings,
|
||||
},
|
||||
}, userNames, nil
|
||||
}
|
||||
|
||||
func validateMieruInboundOptions(options option.MieruInboundOptions) error {
|
||||
if options.Transport != "TCP" && options.Transport != "UDP" {
|
||||
return E.New("transport must be TCP or UDP")
|
||||
}
|
||||
if len(options.Users) == 0 {
|
||||
return E.New("users is empty")
|
||||
}
|
||||
for _, user := range options.Users {
|
||||
if user.Name == "" {
|
||||
return E.New("username is empty")
|
||||
}
|
||||
if user.Password == "" {
|
||||
return E.New("password is empty")
|
||||
}
|
||||
}
|
||||
if options.TrafficPattern != "" {
|
||||
trafficPattern, err := mierutp.Decode(options.TrafficPattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode traffic pattern %q: %w", options.TrafficPattern, err)
|
||||
}
|
||||
if err := mierutp.Validate(trafficPattern); err != nil {
|
||||
return fmt.Errorf("invalid traffic pattern %q: %w", options.TrafficPattern, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
mieruclient "github.com/enfein/mieru/v3/apis/client"
|
||||
mierucommon "github.com/enfein/mieru/v3/apis/common"
|
||||
mierumodel "github.com/enfein/mieru/v3/apis/model"
|
||||
mierutp "github.com/enfein/mieru/v3/apis/trafficpattern"
|
||||
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
@@ -36,7 +37,7 @@ func RegisterOutbound(registry *outbound.Registry) {
|
||||
}
|
||||
|
||||
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.MieruOutboundOptions) (adapter.Outbound, error) {
|
||||
outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
|
||||
outboundDialer, err := dialer.New(ctx, options.DialerOptions, M.IsDomainName(options.Server))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -52,7 +53,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
if err := c.Start(); err != nil {
|
||||
return nil, fmt.Errorf("failed to start mieru client: %w", err)
|
||||
}
|
||||
logger.InfoContext(ctx, "mieru client is started")
|
||||
logger.NoticeContext(ctx, "mieru client is started")
|
||||
|
||||
return &Outbound{
|
||||
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeMieru, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
|
||||
@@ -123,7 +124,15 @@ func (md mieruDialer) DialContext(ctx context.Context, network, address string)
|
||||
return md.dialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
|
||||
var _ mierucommon.Dialer = (*mieruDialer)(nil)
|
||||
func (md mieruDialer) ListenPacket(ctx context.Context, network, laddr, raddr string) (net.PacketConn, error) {
|
||||
addr := M.ParseSocksaddr(raddr)
|
||||
return md.dialer.ListenPacket(ctx, addr)
|
||||
}
|
||||
|
||||
var (
|
||||
_ mierucommon.Dialer = (*mieruDialer)(nil)
|
||||
_ mierucommon.PacketDialer = (*mieruDialer)(nil)
|
||||
)
|
||||
|
||||
// streamer converts a net.PacketConn to a net.Conn.
|
||||
type streamer struct {
|
||||
@@ -161,7 +170,13 @@ func buildMieruClientConfig(options option.MieruOutboundOptions, dialer mieruDia
|
||||
return nil, fmt.Errorf("failed to validate mieru options: %w", err)
|
||||
}
|
||||
|
||||
transportProtocol := mierupb.TransportProtocol_TCP.Enum()
|
||||
var transportProtocol *mierupb.TransportProtocol
|
||||
switch options.Transport {
|
||||
case "TCP":
|
||||
transportProtocol = mierupb.TransportProtocol_TCP.Enum()
|
||||
case "UDP":
|
||||
transportProtocol = mierupb.TransportProtocol_UDP.Enum()
|
||||
}
|
||||
server := &mierupb.ServerEndpoint{}
|
||||
if options.ServerPort != 0 {
|
||||
server.PortBindings = append(server.PortBindings, &mierupb.PortBinding{
|
||||
@@ -189,13 +204,21 @@ func buildMieruClientConfig(options option.MieruOutboundOptions, dialer mieruDia
|
||||
},
|
||||
Servers: []*mierupb.ServerEndpoint{server},
|
||||
},
|
||||
Dialer: dialer,
|
||||
Dialer: dialer,
|
||||
PacketDialer: dialer,
|
||||
DNSConfig: &mierucommon.ClientDNSConfig{
|
||||
BypassDialerDNS: true,
|
||||
},
|
||||
}
|
||||
if multiplexing, ok := mierupb.MultiplexingLevel_value[options.Multiplexing]; ok {
|
||||
config.Profile.Multiplexing = &mierupb.MultiplexingConfig{
|
||||
Level: mierupb.MultiplexingLevel(multiplexing).Enum(),
|
||||
}
|
||||
}
|
||||
if options.TrafficPattern != "" {
|
||||
trafficPattern, _ := mierutp.Decode(options.TrafficPattern)
|
||||
config.Profile.TrafficPattern = trafficPattern
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
@@ -221,8 +244,8 @@ func validateMieruOptions(options option.MieruOutboundOptions) error {
|
||||
return fmt.Errorf("begin port must be less than or equal to end port")
|
||||
}
|
||||
}
|
||||
if options.Transport != "TCP" {
|
||||
return fmt.Errorf("transport must be TCP")
|
||||
if options.Transport != "TCP" && options.Transport != "UDP" {
|
||||
return fmt.Errorf("transport must be TCP or UDP")
|
||||
}
|
||||
if options.UserName == "" {
|
||||
return fmt.Errorf("username is empty")
|
||||
@@ -235,6 +258,15 @@ func validateMieruOptions(options option.MieruOutboundOptions) error {
|
||||
return fmt.Errorf("invalid multiplexing level: %s", options.Multiplexing)
|
||||
}
|
||||
}
|
||||
if options.TrafficPattern != "" {
|
||||
trafficPattern, err := mierutp.Decode(options.TrafficPattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode traffic pattern %q: %w", options.TrafficPattern, err)
|
||||
}
|
||||
if err := mierutp.Validate(trafficPattern); err != nil {
|
||||
return fmt.Errorf("invalid traffic pattern %q: %w", options.TrafficPattern, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,10 @@ func (h *Inbound) Close() error {
|
||||
)
|
||||
}
|
||||
|
||||
func (h *Inbound) UpdateUsers(users []auth.User) {
|
||||
h.authenticator.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
err := h.newConnection(ctx, conn, metadata, onClose)
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
|
||||
@@ -63,7 +63,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
Secrets: secrets,
|
||||
Concurrency: options.GetConcurrency(),
|
||||
DomainFrontingPort: options.GetDomainFrontingPort(),
|
||||
DomainFrontingIP: options.DomainFrontingIP,
|
||||
DomainFrontingHost: options.DomainFrontingHost,
|
||||
DomainFrontingProxyProtocol: options.DomainFrontingProxyProtocol,
|
||||
PreferIP: options.GetPreferIP(),
|
||||
AutoUpdate: options.AutoUpdate,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user