mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-06-17 00:32:01 +03:00
Compare commits
13 Commits
v1.13.12-e
...
extended
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cbc7691f0 | ||
|
|
d174962a04 | ||
|
|
9c80cf371c | ||
|
|
9f5ccf43d4 | ||
|
|
6f6af8e902 | ||
|
|
6cddffd546 | ||
|
|
a59e9ec9e3 | ||
|
|
9ebec50a72 | ||
|
|
9ff7a84afe | ||
|
|
7a0354699a | ||
|
|
978b951169 | ||
|
|
e363c2ff78 | ||
|
|
0577c28120 |
@@ -1,12 +1,16 @@
|
|||||||
-s dir
|
-s dir
|
||||||
--name sing-box
|
--name sing-box-extended
|
||||||
--category net
|
--category net
|
||||||
--license GPL-3.0-or-later
|
--license GPL-3.0-or-later
|
||||||
--description "The universal proxy platform."
|
--description "The universal proxy platform (extended)."
|
||||||
--url "https://sing-box.sagernet.org/"
|
--url "https://sing-box.sagernet.org/"
|
||||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||||
--no-deb-generate-changes
|
--no-deb-generate-changes
|
||||||
|
|
||||||
|
--provides sing-box
|
||||||
|
--conflicts sing-box
|
||||||
|
--replaces sing-box
|
||||||
|
|
||||||
--config-files /etc/config/sing-box
|
--config-files /etc/config/sing-box
|
||||||
--config-files /etc/sing-box/config.json
|
--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)
|
PROJECT=$(cd "$(dirname "$0")/.."; pwd)
|
||||||
|
|
||||||
# Convert version to APK format:
|
# Convert version to APK format:
|
||||||
# 1.13.0-beta.8 -> 1.13.0_beta8-r0
|
APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/' | sed -E 's/-[a-z]+-/./g')
|
||||||
# 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="${APK_VERSION}-r0"
|
APK_VERSION="${APK_VERSION}-r0"
|
||||||
|
|
||||||
ROOT_DIR=$(mktemp -d)
|
ROOT_DIR=$(mktemp -d)
|
||||||
@@ -78,15 +75,16 @@ done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static"
|
|||||||
|
|
||||||
# Build APK
|
# Build APK
|
||||||
apk --root "$APK_ROOT_DIR" mkpkg \
|
apk --root "$APK_ROOT_DIR" mkpkg \
|
||||||
--info "name:sing-box" \
|
--info "name:sing-box-extended" \
|
||||||
--info "version:${APK_VERSION}" \
|
--info "version:${APK_VERSION}" \
|
||||||
--info "description:The universal proxy platform." \
|
--info "description:The universal proxy platform (extended)." \
|
||||||
--info "arch:${ARCHITECTURE}" \
|
--info "arch:${ARCHITECTURE}" \
|
||||||
--info "license:GPL-3.0-or-later" \
|
--info "license:GPL-3.0-or-later" \
|
||||||
--info "origin:sing-box" \
|
--info "origin:sing-box-extended" \
|
||||||
--info "url:https://sing-box.sagernet.org/" \
|
--info "url:https://sing-box.sagernet.org/" \
|
||||||
--info "maintainer:nekohasekai <contact-git@sekai.icu>" \
|
--info "maintainer:nekohasekai <contact-git@sekai.icu>" \
|
||||||
--info "depends:ca-bundle kmod-inet-diag kmod-tun firewall4 kmod-nft-queue" \
|
--info "depends:ca-bundle kmod-inet-diag kmod-tun firewall4 kmod-nft-queue" \
|
||||||
|
--info "provides:sing-box" \
|
||||||
--info "provider-priority:100" \
|
--info "provider-priority:100" \
|
||||||
--script "pre-deinstall:${PROJECT}/release/config/openwrt.prerm" \
|
--script "pre-deinstall:${PROJECT}/release/config/openwrt.prerm" \
|
||||||
--files "$ROOT_DIR" \
|
--files "$ROOT_DIR" \
|
||||||
|
|||||||
57
.github/build_openwrt_packages.sh
vendored
Executable file
57
.github/build_openwrt_packages.sh
vendored
Executable file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/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//-/\~}"
|
||||||
|
|
||||||
|
FPM_DIR=$(mktemp -d)
|
||||||
|
sed "s|release/|$PROJECT/release/|g;s|^LICENSE|$PROJECT/LICENSE|" "$PROJECT/.fpm_openwrt" > "$FPM_DIR/.fpm"
|
||||||
|
trap 'rm -rf "$FPM_DIR"' EXIT
|
||||||
|
|
||||||
|
for ARCH in $ARCHITECTURES; do
|
||||||
|
TMP_DEB=$(mktemp -p "$DIST" _openwrt_XXXXXX.deb)
|
||||||
|
rm -f "$TMP_DEB"
|
||||||
|
(cd "$FPM_DIR" && fpm -t deb \
|
||||||
|
-v "$PKG_VERSION" \
|
||||||
|
-p "$TMP_DEB" \
|
||||||
|
--architecture all \
|
||||||
|
"$BINARY_PATH=/usr/bin/sing-box")
|
||||||
|
|
||||||
|
bash "$PROJECT/.github/deb2ipk.sh" \
|
||||||
|
"$ARCH" \
|
||||||
|
"$TMP_DEB" \
|
||||||
|
"$DIST/sing-box-extended_${VERSION}_openwrt_${ARCH}.ipk"
|
||||||
|
rm -f "$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
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -23,3 +23,10 @@ AGENTS.md
|
|||||||
/.claude/
|
/.claude/
|
||||||
dist
|
dist
|
||||||
logs
|
logs
|
||||||
|
/*.so
|
||||||
|
/*.log
|
||||||
|
/*.db-shm
|
||||||
|
/*.db-wal
|
||||||
|
/*.db.backup.*
|
||||||
|
/test_download.bin
|
||||||
|
/wget-log
|
||||||
@@ -28,6 +28,7 @@ builds:
|
|||||||
- with_sudoku
|
- with_sudoku
|
||||||
- with_manager
|
- with_manager
|
||||||
- with_admin_panel
|
- with_admin_panel
|
||||||
|
- with_profiler
|
||||||
- badlinkname
|
- badlinkname
|
||||||
- tfogo_checklinkname0
|
- tfogo_checklinkname0
|
||||||
env:
|
env:
|
||||||
@@ -63,6 +64,7 @@ builds:
|
|||||||
- with_openvpn
|
- with_openvpn
|
||||||
- with_trusttunnel
|
- with_trusttunnel
|
||||||
- with_sudoku
|
- with_sudoku
|
||||||
|
- with_profiler
|
||||||
- badlinkname
|
- badlinkname
|
||||||
- tfogo_checklinkname0
|
- tfogo_checklinkname0
|
||||||
targets:
|
targets:
|
||||||
@@ -123,6 +125,7 @@ builds:
|
|||||||
- with_sudoku
|
- with_sudoku
|
||||||
- with_manager
|
- with_manager
|
||||||
- with_admin_panel
|
- with_admin_panel
|
||||||
|
- with_profiler
|
||||||
- badlinkname
|
- badlinkname
|
||||||
- tfogo_checklinkname0
|
- tfogo_checklinkname0
|
||||||
- with_naive_outbound
|
- with_naive_outbound
|
||||||
@@ -155,6 +158,7 @@ builds:
|
|||||||
- with_sudoku
|
- with_sudoku
|
||||||
- with_manager
|
- with_manager
|
||||||
- with_admin_panel
|
- with_admin_panel
|
||||||
|
- with_profiler
|
||||||
- badlinkname
|
- badlinkname
|
||||||
- tfogo_checklinkname0
|
- tfogo_checklinkname0
|
||||||
- with_naive_outbound
|
- with_naive_outbound
|
||||||
@@ -187,6 +191,7 @@ builds:
|
|||||||
- with_sudoku
|
- with_sudoku
|
||||||
- with_manager
|
- with_manager
|
||||||
- with_admin_panel
|
- with_admin_panel
|
||||||
|
- with_profiler
|
||||||
- badlinkname
|
- badlinkname
|
||||||
- tfogo_checklinkname0
|
- tfogo_checklinkname0
|
||||||
- with_naive_outbound
|
- with_naive_outbound
|
||||||
@@ -219,6 +224,7 @@ builds:
|
|||||||
- with_sudoku
|
- with_sudoku
|
||||||
- with_manager
|
- with_manager
|
||||||
- with_admin_panel
|
- with_admin_panel
|
||||||
|
- with_profiler
|
||||||
- badlinkname
|
- badlinkname
|
||||||
- tfogo_checklinkname0
|
- tfogo_checklinkname0
|
||||||
- with_naive_outbound
|
- with_naive_outbound
|
||||||
@@ -251,6 +257,7 @@ builds:
|
|||||||
- with_sudoku
|
- with_sudoku
|
||||||
- with_manager
|
- with_manager
|
||||||
- with_admin_panel
|
- with_admin_panel
|
||||||
|
- with_profiler
|
||||||
- badlinkname
|
- badlinkname
|
||||||
- tfogo_checklinkname0
|
- tfogo_checklinkname0
|
||||||
- with_naive_outbound
|
- with_naive_outbound
|
||||||
@@ -299,6 +306,7 @@ builds:
|
|||||||
- with_sudoku
|
- with_sudoku
|
||||||
- with_manager
|
- with_manager
|
||||||
- with_admin_panel
|
- with_admin_panel
|
||||||
|
- with_profiler
|
||||||
- badlinkname
|
- badlinkname
|
||||||
- tfogo_checklinkname0
|
- tfogo_checklinkname0
|
||||||
- with_naive_outbound
|
- with_naive_outbound
|
||||||
@@ -353,6 +361,7 @@ builds:
|
|||||||
- with_openvpn
|
- with_openvpn
|
||||||
- with_trusttunnel
|
- with_trusttunnel
|
||||||
- with_sudoku
|
- with_sudoku
|
||||||
|
- with_profiler
|
||||||
- badlinkname
|
- badlinkname
|
||||||
- tfogo_checklinkname0
|
- tfogo_checklinkname0
|
||||||
targets:
|
targets:
|
||||||
@@ -393,6 +402,50 @@ builds:
|
|||||||
- android_arm64
|
- android_arm64
|
||||||
- android_386
|
- android_386
|
||||||
- android_amd64
|
- 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
|
||||||
|
- with_profiler
|
||||||
|
- 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:
|
upx:
|
||||||
- enabled: true
|
- enabled: true
|
||||||
ids:
|
ids:
|
||||||
@@ -496,5 +549,5 @@ release:
|
|||||||
- archive-naive-purego-windows-amd64
|
- archive-naive-purego-windows-amd64
|
||||||
- archive-naive-purego-windows-arm64
|
- archive-naive-purego-windows-arm64
|
||||||
- archive-compressed
|
- archive-compressed
|
||||||
- package
|
- archive-openwrt
|
||||||
skip_upload: true
|
skip_upload: true
|
||||||
|
|||||||
3
Makefile
3
Makefile
@@ -27,7 +27,6 @@ CRONET_GO_PATH ?= $(shell pwd)/cronet-go
|
|||||||
.PHONY: test release docs build
|
.PHONY: test release docs build
|
||||||
|
|
||||||
build:
|
build:
|
||||||
export GOTOOLCHAIN=local && \
|
|
||||||
go build $(MAIN_PARAMS) $(MAIN)
|
go build $(MAIN_PARAMS) $(MAIN)
|
||||||
|
|
||||||
build_admin_panel:
|
build_admin_panel:
|
||||||
@@ -94,6 +93,8 @@ release: build_admin_panel build_naive
|
|||||||
mkdir dist/release
|
mkdir dist/release
|
||||||
mv dist/*.tar.gz \
|
mv dist/*.tar.gz \
|
||||||
dist/*.zip \
|
dist/*.zip \
|
||||||
|
dist/*.ipk \
|
||||||
|
dist/*.apk \
|
||||||
dist/release
|
dist/release
|
||||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
||||||
./codeberg-release.sh --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
./codeberg-release.sh --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
||||||
|
|||||||
@@ -3,19 +3,21 @@
|
|||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](go.mod)
|
[](go.mod)
|
||||||
[](https://codeberg.org/shtorm-7/sing-box-extended)
|
[](https://codeberg.org/shtorm-7/sing-box-extended)
|
||||||
|
[](https://t.me/sing_box_extended)
|
||||||
|
|
||||||
Sing-box with extended features.
|
Sing-box with extended features.
|
||||||
|
|
||||||
## 🔥 Features
|
## 🔥 Features
|
||||||
|
|
||||||
### Outbounds
|
### Protocols
|
||||||
- **WARP** — Cloudflare WARP integration through WireGuard
|
- **WARP** — Cloudflare WARP integration through WireGuard
|
||||||
- **MASQUE** — Cloudflare MASQUE proxy over QUIC / HTTP-2
|
- **MASQUE** — Cloudflare MASQUE proxy over QUIC / HTTP-2
|
||||||
- **MTProxy** — Telegram MTProxy server with FakeTLS and domain fronting
|
- **MTProxy** — Telegram MTProxy server with FakeTLS and domain fronting
|
||||||
- **Mieru** — Secure, hard to classify, hard to probe network protocol
|
- **Mieru** — Secure, hard to classify, hard to probe network protocol
|
||||||
- **OpenVPN** — OpenVPN client with tls-auth, tls-crypt, and tls-crypt-v2 support
|
- **OpenVPN** — OpenVPN client with tls-auth, tls-crypt and tls-crypt-v2 support
|
||||||
- **TrustTunnel** — AdGuard's obfuscated VPN protocol, indistinguishable from HTTPS traffic
|
- **TrustTunnel** — AdGuard's obfuscated VPN protocol, indistinguishable from HTTPS traffic
|
||||||
- **Sudoku** — Traffic obfuscation protocol based on 4×4 Sudoku puzzles with low-entropy fingerprints
|
- **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
|
- **VPN** — Routed tunnel over any TCP sing-box protocol
|
||||||
- **Bond** — Link aggregation for increasing throughput
|
- **Bond** — Link aggregation for increasing throughput
|
||||||
- **Fallback** — Outbound group with priority-based switching
|
- **Fallback** — Outbound group with priority-based switching
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ func (m *Manager) Remove(tag string) error {
|
|||||||
if m.defaultOutbound == outbound {
|
if m.defaultOutbound == outbound {
|
||||||
if len(m.outbounds) > 0 {
|
if len(m.outbounds) > 0 {
|
||||||
m.defaultOutbound = 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 {
|
} else {
|
||||||
m.defaultOutbound = nil
|
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) {
|
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) {
|
||||||
m.defaultOutbound = outbound
|
m.defaultOutbound = outbound
|
||||||
if m.started {
|
if m.started {
|
||||||
m.logger.Info("updated default outbound to ", outbound.Tag())
|
m.logger.Notice("updated default outbound to ", outbound.Tag())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -289,11 +289,21 @@ func uniquifyTags(opts []option.Outbound) {
|
|||||||
|
|
||||||
func removeEmojisFromTags(opts []option.Outbound) {
|
func removeEmojisFromTags(opts []option.Outbound) {
|
||||||
for i, opt := range opts {
|
for i, opt := range opts {
|
||||||
cleaned := emojiRegex.ReplaceAllString(opt.Tag, "")
|
cleaned := flagRegex.ReplaceAllStringFunc(opt.Tag, flagToCountryCode)
|
||||||
|
cleaned = emojiRegex.ReplaceAllString(cleaned, "")
|
||||||
cleaned = multiSpaceRegex.ReplaceAllString(cleaned, " ")
|
cleaned = multiSpaceRegex.ReplaceAllString(cleaned, " ")
|
||||||
opts[i].Tag = strings.TrimSpace(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 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,}`)
|
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()
|
s.Close()
|
||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,7 +477,7 @@ func (s *Box) Start() error {
|
|||||||
s.Close()
|
s.Close()
|
||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ func run() error {
|
|||||||
for {
|
for {
|
||||||
osSignal := <-osSignals
|
osSignal := <-osSignals
|
||||||
if osSignal == syscall.SIGHUP {
|
if osSignal == syscall.SIGHUP {
|
||||||
|
log.Notice("received SIGHUP, reloading...")
|
||||||
err = check()
|
err = check()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(E.Cause(err, "reload service"))
|
log.Error(E.Cause(err, "reload service"))
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
l.logger.Info("tcp server started at ", tcpListener.Addr())
|
l.logger.Notice("tcp server started at ", tcpListener.Addr())
|
||||||
l.tcpListener = tcpListener
|
l.tcpListener = tcpListener
|
||||||
return tcpListener, err
|
return tcpListener, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
|
|||||||
}
|
}
|
||||||
l.udpConn = udpConn.(*net.UDPConn)
|
l.udpConn = udpConn.(*net.UDPConn)
|
||||||
l.udpAddr = bindAddr
|
l.udpAddr = bindAddr
|
||||||
l.logger.Info("udp server started at ", udpConn.LocalAddr())
|
l.logger.Notice("udp server started at ", udpConn.LocalAddr())
|
||||||
return udpConn, err
|
return udpConn, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
|
|||||||
config.Certificates = []tls.Certificate{keyPair}
|
config.Certificates = []tls.Certificate{keyPair}
|
||||||
c.config = config
|
c.config = config
|
||||||
c.access.Unlock()
|
c.access.Unlock()
|
||||||
c.logger.Info("reloaded TLS certificate")
|
c.logger.Notice("reloaded TLS certificate")
|
||||||
} else if common.Contains(c.clientCertificatePath, path) {
|
} else if common.Contains(c.clientCertificatePath, path) {
|
||||||
clientCertificateCA := x509.NewCertPool()
|
clientCertificateCA := x509.NewCertPool()
|
||||||
var reloaded bool
|
var reloaded bool
|
||||||
@@ -188,7 +188,7 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
|
|||||||
config.ClientCAs = clientCertificateCA
|
config.ClientCAs = clientCertificateCA
|
||||||
c.config = config
|
c.config = config
|
||||||
c.access.Unlock()
|
c.access.Unlock()
|
||||||
c.logger.Info("reloaded client certificates")
|
c.logger.Notice("reloaded client certificates")
|
||||||
} else if path == c.echKeyPath {
|
} else if path == c.echKeyPath {
|
||||||
echKey, err := os.ReadFile(c.echKeyPath)
|
echKey, err := os.ReadFile(c.echKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -198,7 +198,7 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.logger.Info("reloaded ECH keys")
|
c.logger.Notice("reloaded ECH keys")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,13 +20,14 @@ const (
|
|||||||
type LogLevel int32
|
type LogLevel int32
|
||||||
|
|
||||||
const (
|
const (
|
||||||
LogLevel_PANIC LogLevel = 0
|
LogLevel_PANIC LogLevel = 0
|
||||||
LogLevel_FATAL LogLevel = 1
|
LogLevel_FATAL LogLevel = 1
|
||||||
LogLevel_ERROR LogLevel = 2
|
LogLevel_ERROR LogLevel = 2
|
||||||
LogLevel_WARN LogLevel = 3
|
LogLevel_WARN LogLevel = 3
|
||||||
LogLevel_INFO LogLevel = 4
|
LogLevel_NOTICE LogLevel = 4
|
||||||
LogLevel_DEBUG LogLevel = 5
|
LogLevel_INFO LogLevel = 5
|
||||||
LogLevel_TRACE LogLevel = 6
|
LogLevel_DEBUG LogLevel = 6
|
||||||
|
LogLevel_TRACE LogLevel = 7
|
||||||
)
|
)
|
||||||
|
|
||||||
// Enum value maps for LogLevel.
|
// Enum value maps for LogLevel.
|
||||||
@@ -36,18 +37,20 @@ var (
|
|||||||
1: "FATAL",
|
1: "FATAL",
|
||||||
2: "ERROR",
|
2: "ERROR",
|
||||||
3: "WARN",
|
3: "WARN",
|
||||||
4: "INFO",
|
4: "NOTICE",
|
||||||
5: "DEBUG",
|
5: "INFO",
|
||||||
6: "TRACE",
|
6: "DEBUG",
|
||||||
|
7: "TRACE",
|
||||||
}
|
}
|
||||||
LogLevel_value = map[string]int32{
|
LogLevel_value = map[string]int32{
|
||||||
"PANIC": 0,
|
"PANIC": 0,
|
||||||
"FATAL": 1,
|
"FATAL": 1,
|
||||||
"ERROR": 2,
|
"ERROR": 2,
|
||||||
"WARN": 3,
|
"WARN": 3,
|
||||||
"INFO": 4,
|
"NOTICE": 4,
|
||||||
"DEBUG": 5,
|
"INFO": 5,
|
||||||
"TRACE": 6,
|
"DEBUG": 6,
|
||||||
|
"TRACE": 7,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -59,9 +59,10 @@ enum LogLevel {
|
|||||||
FATAL = 1;
|
FATAL = 1;
|
||||||
ERROR = 2;
|
ERROR = 2;
|
||||||
WARN = 3;
|
WARN = 3;
|
||||||
INFO = 4;
|
NOTICE = 4;
|
||||||
DEBUG = 5;
|
INFO = 5;
|
||||||
TRACE = 6;
|
DEBUG = 6;
|
||||||
|
TRACE = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Log {
|
message Log {
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ func (t *Transport) updateServers() error {
|
|||||||
return E.Cause(err, "dhcp: prepare interface")
|
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)
|
fetchCtx, cancel := context.WithTimeout(t.ctx, C.DHCPTimeout)
|
||||||
err = t.fetchServers0(fetchCtx, iface)
|
err = t.fetchServers0(fetchCtx, iface)
|
||||||
cancel()
|
cancel()
|
||||||
@@ -303,7 +303,7 @@ func (t *Transport) recreateServers(iface *control.Interface, dhcpPacket *dhcpv4
|
|||||||
return M.SocksaddrFrom(M.AddrFromIP(it), 53)
|
return M.SocksaddrFrom(M.AddrFromIP(it), 53)
|
||||||
})
|
})
|
||||||
if len(serverAddrs) > 0 && !slices.Equal(t.servers, serverAddrs) {
|
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
|
t.servers = serverAddrs
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ func (m *TransportManager) Remove(tag string) error {
|
|||||||
return E.New("default server cannot be fakeip")
|
return E.New("default server cannot be fakeip")
|
||||||
}
|
}
|
||||||
m.defaultTransport = nextTransport
|
m.defaultTransport = nextTransport
|
||||||
m.logger.Info("updated default server to ", m.defaultTransport.Tag())
|
m.logger.Notice("updated default server to ", m.defaultTransport.Tag())
|
||||||
} else {
|
} else {
|
||||||
m.defaultTransport = nil
|
m.defaultTransport = nil
|
||||||
}
|
}
|
||||||
@@ -287,7 +287,7 @@ func (m *TransportManager) Create(ctx context.Context, logger log.ContextLogger,
|
|||||||
}
|
}
|
||||||
m.defaultTransport = transport
|
m.defaultTransport = transport
|
||||||
if m.started {
|
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 {
|
if transport.Type() == C.DNSTypeFakeIP {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
|
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
|
||||||
| `vless` | [VLESS](./vless/) | TCP |
|
| `vless` | [VLESS](./vless/) | TCP |
|
||||||
| `anytls` | [AnyTLS](./anytls/) | TCP |
|
| `anytls` | [AnyTLS](./anytls/) | TCP |
|
||||||
|
| `mieru` | [Mieru](./mieru/) | :material-close: |
|
||||||
| `tun` | [Tun](./tun/) | :material-close: |
|
| `tun` | [Tun](./tun/) | :material-close: |
|
||||||
| `redirect` | [Redirect](./redirect/) | :material-close: |
|
| `redirect` | [Redirect](./redirect/) | :material-close: |
|
||||||
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
|
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
|
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
|
||||||
| `vless` | [VLESS](./vless/) | TCP |
|
| `vless` | [VLESS](./vless/) | TCP |
|
||||||
| `anytls` | [AnyTLS](./anytls/) | TCP |
|
| `anytls` | [AnyTLS](./anytls/) | TCP |
|
||||||
|
| `mieru` | [Mieru](./mieru/) | :material-close: |
|
||||||
| `tun` | [Tun](./tun/) | :material-close: |
|
| `tun` | [Tun](./tun/) | :material-close: |
|
||||||
| `redirect` | [Redirect](./redirect/) | :material-close: |
|
| `redirect` | [Redirect](./redirect/) | :material-close: |
|
||||||
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
|
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
|
||||||
|
|||||||
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/) |
|
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
||||||
| `mieru` | [Mieru](./mieru/) |
|
| `mieru` | [Mieru](./mieru/) |
|
||||||
| `anytls` | [AnyTLS](./anytls/) |
|
| `anytls` | [AnyTLS](./anytls/) |
|
||||||
|
| `mieru` | [Mieru](./mieru/) |
|
||||||
| `tor` | [Tor](./tor/) |
|
| `tor` | [Tor](./tor/) |
|
||||||
| `ssh` | [SSH](./ssh/) |
|
| `ssh` | [SSH](./ssh/) |
|
||||||
| `dns` | [DNS](./dns/) |
|
| `dns` | [DNS](./dns/) |
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
||||||
| `mieru` | [Mieru](./mieru/) |
|
| `mieru` | [Mieru](./mieru/) |
|
||||||
| `anytls` | [AnyTLS](./anytls/) |
|
| `anytls` | [AnyTLS](./anytls/) |
|
||||||
|
| `mieru` | [Mieru](./mieru/) |
|
||||||
| `tor` | [Tor](./tor/) |
|
| `tor` | [Tor](./tor/) |
|
||||||
| `ssh` | [SSH](./ssh/) |
|
| `ssh` | [SSH](./ssh/) |
|
||||||
| `dns` | [DNS](./dns/) |
|
| `dns` | [DNS](./dns/) |
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ icon: material/new-box
|
|||||||
"username": "asdf",
|
"username": "asdf",
|
||||||
"password": "hjkl",
|
"password": "hjkl",
|
||||||
"multiplexing": "MULTIPLEXING_LOW",
|
"multiplexing": "MULTIPLEXING_LOW",
|
||||||
|
"traffic_pattern": "GgQIARAK",
|
||||||
|
|
||||||
... // Dial Fields
|
... // Dial Fields
|
||||||
}
|
}
|
||||||
@@ -48,7 +49,7 @@ Must set at least one field between `server_port` and `server_ports`.
|
|||||||
|
|
||||||
==Required==
|
==Required==
|
||||||
|
|
||||||
Transmission protocol. The only allowed value is `TCP`.
|
Transmission protocol. Allowed values are `TCP` and `UDP`.
|
||||||
|
|
||||||
#### username
|
#### username
|
||||||
|
|
||||||
@@ -66,6 +67,10 @@ mieru password.
|
|||||||
|
|
||||||
Multiplexing level. Supported values are `MULTIPLEXING_OFF`, `MULTIPLEXING_LOW`, `MULTIPLEXING_MIDDLE`, `MULTIPLEXING_HIGH`. `MULTIPLEXING_OFF` disables multiplexing.
|
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
|
### Dial Fields
|
||||||
|
|
||||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ icon: material/new-box
|
|||||||
"username": "asdf",
|
"username": "asdf",
|
||||||
"password": "hjkl",
|
"password": "hjkl",
|
||||||
"multiplexing": "MULTIPLEXING_LOW",
|
"multiplexing": "MULTIPLEXING_LOW",
|
||||||
|
"traffic_pattern": "GgQIARAK",
|
||||||
|
|
||||||
... // 拨号字段
|
... // 拨号字段
|
||||||
}
|
}
|
||||||
@@ -48,7 +49,7 @@ icon: material/new-box
|
|||||||
|
|
||||||
==必填==
|
==必填==
|
||||||
|
|
||||||
通信协议。仅可设为 `TCP`。
|
通信协议。可设为 `TCP` 或 `UDP`。
|
||||||
|
|
||||||
#### username
|
#### username
|
||||||
|
|
||||||
@@ -66,6 +67,10 @@ mieru 密码。
|
|||||||
|
|
||||||
多路复用设置。可以设为 `MULTIPLEXING_OFF`,`MULTIPLEXING_LOW`,`MULTIPLEXING_MIDDLE`,`MULTIPLEXING_HIGH`。其中 `MULTIPLEXING_OFF` 会关闭多路复用功能。
|
多路复用设置。可以设为 `MULTIPLEXING_OFF`,`MULTIPLEXING_LOW`,`MULTIPLEXING_MIDDLE`,`MULTIPLEXING_HIGH`。其中 `MULTIPLEXING_OFF` 会关闭多路复用功能。
|
||||||
|
|
||||||
|
#### traffic_pattern
|
||||||
|
|
||||||
|
一个 base64 字符串用于微调网络行为。
|
||||||
|
|
||||||
### 拨号字段
|
### 拨号字段
|
||||||
|
|
||||||
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"tag": "my-manager",
|
"tag": "my-manager",
|
||||||
"database": {
|
"database": {
|
||||||
"driver": "sqlite",
|
"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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,10 +25,13 @@
|
|||||||
{
|
{
|
||||||
"type": "masque",
|
"type": "masque",
|
||||||
"tag": "masque-out",
|
"tag": "masque-out",
|
||||||
|
"system": false,
|
||||||
|
"name": "masque0",
|
||||||
"use_http2": false,
|
"use_http2": false,
|
||||||
"use_ipv6": false,
|
"use_ipv6": false,
|
||||||
"profile": {
|
"profile": {
|
||||||
"detour": "direct",
|
"detour": "direct",
|
||||||
|
// For getting existing MASQUE device profile, else sing-box will create new profile
|
||||||
"id": "",
|
"id": "",
|
||||||
"auth_token": ""
|
"auth_token": ""
|
||||||
},
|
},
|
||||||
@@ -36,7 +39,7 @@
|
|||||||
"udp_keepalive_period": "30s",
|
"udp_keepalive_period": "30s",
|
||||||
"udp_initial_packet_size": 0,
|
"udp_initial_packet_size": 0,
|
||||||
"reconnect_delay": "5s",
|
"reconnect_delay": "5s",
|
||||||
"tls": {
|
"tls": { // TLS fields for HTTP2
|
||||||
"insecure": false,
|
"insecure": false,
|
||||||
"cipher_suites": [],
|
"cipher_suites": [],
|
||||||
"curve_preferences": [],
|
"curve_preferences": [],
|
||||||
|
|||||||
@@ -27,14 +27,16 @@
|
|||||||
"tag": "mieru-out",
|
"tag": "mieru-out",
|
||||||
"server": "example.com",
|
"server": "example.com",
|
||||||
"server_port": 27017,
|
"server_port": 27017,
|
||||||
"server_ports": "27017-27019",
|
"server_ports": [
|
||||||
|
"27017-27019"
|
||||||
|
],
|
||||||
"transport": "TCP",
|
"transport": "TCP",
|
||||||
"username": "username",
|
"username": "username",
|
||||||
"password": "password",
|
"password": "password",
|
||||||
// valid: MULTIPLEXING_DEFAULT / MULTIPLEXING_OFF / MULTIPLEXING_LOW
|
// valid: MULTIPLEXING_DEFAULT / MULTIPLEXING_OFF / MULTIPLEXING_LOW
|
||||||
// MULTIPLEXING_MIDDLE / MULTIPLEXING_HIGH
|
// MULTIPLEXING_MIDDLE / MULTIPLEXING_HIGH
|
||||||
"multiplexing": "MULTIPLEXING_LOW"
|
"multiplexing": "MULTIPLEXING_LOW",
|
||||||
// Dial Fields
|
"traffic_pattern": "GgQIARAK"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"route": {
|
"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": "",
|
"packet_encoding": "",
|
||||||
"transport": {
|
"transport": {
|
||||||
"type": "mkcp",
|
"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": {
|
"transport": {
|
||||||
"type": "mkcp",
|
"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,
|
"concurrency": 8192,
|
||||||
// domain_fronting_port is a port we use to connect to a fronting domain.
|
// domain_fronting_port is a port we use to connect to a fronting domain.
|
||||||
"domain_fronting_port": 443,
|
"domain_fronting_port": 443,
|
||||||
// domain_fronting_ip is an IP address to use when connecting to the fronting
|
// domain_fronting_host is the address (IP or hostname) to use when connecting
|
||||||
// domain instead of resolving the hostname from the secret via DNS.
|
// to the fronting domain instead of resolving the hostname from the secret via DNS.
|
||||||
"domain_fronting_ip": "",
|
"domain_fronting_host": "",
|
||||||
// domain_fronting_proxy_protocol is used if communication between upstream
|
// domain_fronting_proxy_protocol is used if communication between upstream
|
||||||
// endpoint and sing-box supports proxy protocol.
|
// endpoint and sing-box supports proxy protocol.
|
||||||
"domain_fronting_proxy_protocol": false,
|
"domain_fronting_proxy_protocol": false,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
{
|
{
|
||||||
"type": "openvpn",
|
"type": "openvpn",
|
||||||
"tag": "openvpn-out",
|
"tag": "openvpn-out",
|
||||||
|
"system": false,
|
||||||
|
"name": "openvpn0",
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
"server": "vpn.example.com",
|
"server": "vpn.example.com",
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
{
|
{
|
||||||
"type": "openvpn",
|
"type": "openvpn",
|
||||||
"tag": "openvpn-out",
|
"tag": "openvpn-out",
|
||||||
|
"system": false,
|
||||||
|
"name": "openvpn0",
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
"server": "vpn.example.com",
|
"server": "vpn.example.com",
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
{
|
{
|
||||||
"type": "openvpn",
|
"type": "openvpn",
|
||||||
"tag": "openvpn-out",
|
"tag": "openvpn-out",
|
||||||
|
"system": false,
|
||||||
|
"name": "openvpn0",
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
"server": "vpn.example.com",
|
"server": "vpn.example.com",
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
{
|
{
|
||||||
"type": "openvpn",
|
"type": "openvpn",
|
||||||
"tag": "openvpn-out",
|
"tag": "openvpn-out",
|
||||||
|
"system": false,
|
||||||
|
"name": "openvpn0",
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
"server": "vpn.example.com",
|
"server": "vpn.example.com",
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,20 @@
|
|||||||
"server": "your-server.com",
|
"server": "your-server.com",
|
||||||
"server_port": 443,
|
"server_port": 443,
|
||||||
"key": "your-secret-key"
|
"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
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,17 +12,23 @@
|
|||||||
"server": "your-server.com",
|
"server": "your-server.com",
|
||||||
"server_port": 443,
|
"server_port": 443,
|
||||||
"key": "your-secret-key",
|
"key": "your-secret-key",
|
||||||
"tls": {
|
// "aead_method": "chacha20-poly1305", // chacha20-poly1305 | aes-128-gcm | none
|
||||||
"enabled": true,
|
// "table_type": "prefer_ascii", // prefer_ascii | prefer_entropy | up_ascii_down_entropy | up_entropy_down_ascii
|
||||||
"fragment": true,
|
// "padding_min": 10, // 0-100
|
||||||
"fragment_fallback_delay": "300ms"
|
// "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": {
|
"http_mask": {
|
||||||
"enabled": true,
|
"enabled": true, // true | false
|
||||||
"mode": "stream",
|
"mode": "stream", // legacy | stream | poll | auto | ws
|
||||||
"host": "cdn.example.com",
|
"host": "cdn.example.com", // optional, Host header / SNI override
|
||||||
"path_root": "secret",
|
"path_root": "secret", // optional, URL path prefix (single segment)
|
||||||
"multiplex": "auto"
|
"multiplex": "auto", // off | auto | on
|
||||||
|
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#outbound
|
||||||
|
"enabled": true,
|
||||||
|
"server_name": "cdn.example.com",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,6 +5,18 @@
|
|||||||
"listen": "::",
|
"listen": "::",
|
||||||
"listen_port": 443,
|
"listen_port": 443,
|
||||||
"key": "your-secret-key"
|
"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": [
|
"outbounds": [
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func updateExternalUI(server *Server) func(w http.ResponseWriter, r *http.Reques
|
|||||||
render.JSON(w, r, newError(err.Error()))
|
render.JSON(w, r, newError(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
server.logger.Info("updated external UI")
|
server.logger.Notice("updated external UI")
|
||||||
render.JSON(w, r, render.M{"status": "ok"})
|
render.JSON(w, r, render.M{"status": "ok"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ func (s *Server) Start(stage adapter.StartStage) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "external controller listen error")
|
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() {
|
go func() {
|
||||||
err = s.httpServer.Serve(listener)
|
err = s.httpServer.Serve(listener)
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
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.Error(E.Cause(err, "save mode"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.logger.Info("updated mode: ", newMode)
|
s.logger.Notice("updated mode: ", newMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) HistoryStorage() adapter.URLTestHistoryStorage {
|
func (s *Server) HistoryStorage() adapter.URLTestHistoryStorage {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func (s *Server) Start(stage adapter.StartStage) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.logger.Info("grpc server started at ", listener.Addr())
|
s.logger.Notice("grpc server started at ", listener.Addr())
|
||||||
s.tcpListener = listener
|
s.tcpListener = listener
|
||||||
go func() {
|
go func() {
|
||||||
err = s.grpcServer.Serve(listener)
|
err = s.grpcServer.Serve(listener)
|
||||||
|
|||||||
8
go.mod
8
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module github.com/sagernet/sing-box
|
module github.com/sagernet/sing-box
|
||||||
|
|
||||||
go 1.26.1
|
go 1.26.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AliRizaAynaci/gorl/v2 v2.2.0
|
github.com/AliRizaAynaci/gorl/v2 v2.2.0
|
||||||
@@ -11,7 +11,7 @@ require (
|
|||||||
github.com/coder/websocket v1.8.14
|
github.com/coder/websocket v1.8.14
|
||||||
github.com/cretz/bine v0.2.0
|
github.com/cretz/bine v0.2.0
|
||||||
github.com/database64128/tfo-go/v2 v2.3.2
|
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/chi/v5 v5.2.5
|
||||||
github.com/go-chi/render v1.0.3
|
github.com/go-chi/render v1.0.3
|
||||||
github.com/go-playground/validator/v10 v10.30.1
|
github.com/go-playground/validator/v10 v10.30.1
|
||||||
@@ -229,10 +229,10 @@ replace github.com/ameshkov/dnscrypt/v2 => github.com/shtorm-7/dnscrypt/v2 v2.4.
|
|||||||
|
|
||||||
replace github.com/sagernet/sing-vmess => github.com/shtorm-7/sing-vmess v0.2.7-extended-1.0.0
|
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/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/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.0.0
|
replace github.com/sagernet/sing => github.com/shtorm-7/sing v0.8.10-extended-1.1.0
|
||||||
|
|||||||
12
go.sum
12
go.sum
@@ -89,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/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 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
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.33.0 h1:hv2jK8nqYHwpSG86U2rpZR2I8Aff1/J3ifRmd9NBbFc=
|
||||||
github.com/enfein/mieru/v3 v3.17.1/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
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 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE=
|
github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE=
|
||||||
@@ -379,10 +379,10 @@ 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/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 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/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.11.0-extended-1.0.0 h1:iBLll4ZZG8ULQcHWs6gGslZWtBN72Zo1zjySzMVHF7g=
|
||||||
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/go.mod h1:3rvdhwdPABkwKBdvgMt3VwMn9uSq8hpoHRezZ5jRJU0=
|
||||||
github.com/shtorm-7/sing v0.8.10-extended-1.0.0 h1:mAkyycCQOzCttPOR5fcHkJaZvXMQXeu3mbEfr8D+7A8=
|
github.com/shtorm-7/sing v0.8.10-extended-1.1.0 h1:P4JL2cugjvEvnYu8tMmpR30SE1qsS45RcnNEwzDz5as=
|
||||||
github.com/shtorm-7/sing v0.8.10-extended-1.0.0/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
|
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 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-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 h1:WVheKmQH5hSQbJU1ZTKthKSutkTLWSb2hp4JuQhJBow=
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import (
|
|||||||
"github.com/sagernet/sing-box/protocol/limiter/rate"
|
"github.com/sagernet/sing-box/protocol/limiter/rate"
|
||||||
"github.com/sagernet/sing-box/protocol/limiter/traffic"
|
"github.com/sagernet/sing-box/protocol/limiter/traffic"
|
||||||
"github.com/sagernet/sing-box/protocol/mieru"
|
"github.com/sagernet/sing-box/protocol/mieru"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/protocol/mixed"
|
"github.com/sagernet/sing-box/protocol/mixed"
|
||||||
"github.com/sagernet/sing-box/protocol/naive"
|
"github.com/sagernet/sing-box/protocol/naive"
|
||||||
"github.com/sagernet/sing-box/protocol/parser"
|
"github.com/sagernet/sing-box/protocol/parser"
|
||||||
@@ -81,6 +80,8 @@ func InboundRegistry() *inbound.Registry {
|
|||||||
shadowtls.RegisterInbound(registry)
|
shadowtls.RegisterInbound(registry)
|
||||||
vless.RegisterInbound(registry)
|
vless.RegisterInbound(registry)
|
||||||
anytls.RegisterInbound(registry)
|
anytls.RegisterInbound(registry)
|
||||||
|
mieru.RegisterInbound(registry)
|
||||||
|
ssh.RegisterInbound(registry)
|
||||||
|
|
||||||
bond.RegisterInbound(registry)
|
bond.RegisterInbound(registry)
|
||||||
failover.RegisterInbound(registry)
|
failover.RegisterInbound(registry)
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ func Info(args ...any) {
|
|||||||
std.Info(args...)
|
std.Info(args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Notice(args ...any) {
|
||||||
|
std.Notice(args...)
|
||||||
|
}
|
||||||
|
|
||||||
func Warn(args ...any) {
|
func Warn(args ...any) {
|
||||||
std.Warn(args...)
|
std.Warn(args...)
|
||||||
}
|
}
|
||||||
@@ -67,6 +71,10 @@ func InfoContext(ctx context.Context, args ...any) {
|
|||||||
std.InfoContext(ctx, args...)
|
std.InfoContext(ctx, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NoticeContext(ctx context.Context, args ...any) {
|
||||||
|
std.NoticeContext(ctx, args...)
|
||||||
|
}
|
||||||
|
|
||||||
func WarnContext(ctx context.Context, args ...any) {
|
func WarnContext(ctx context.Context, args ...any) {
|
||||||
std.WarnContext(ctx, args...)
|
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()
|
levelString = aurora.White(levelString).String()
|
||||||
case LevelInfo:
|
case LevelInfo:
|
||||||
levelString = aurora.Cyan(levelString).String()
|
levelString = aurora.Cyan(levelString).String()
|
||||||
|
case LevelNotice:
|
||||||
|
levelString = aurora.Green(levelString).String()
|
||||||
case LevelWarn:
|
case LevelWarn:
|
||||||
levelString = aurora.Yellow(levelString).String()
|
levelString = aurora.Yellow(levelString).String()
|
||||||
case LevelError, LevelFatal, LevelPanic:
|
case LevelError, LevelFatal, LevelPanic:
|
||||||
@@ -97,6 +99,8 @@ func (f Formatter) FormatWithSimple(ctx context.Context, level Level, tag string
|
|||||||
levelString = aurora.White(levelString).String()
|
levelString = aurora.White(levelString).String()
|
||||||
case LevelInfo:
|
case LevelInfo:
|
||||||
levelString = aurora.Cyan(levelString).String()
|
levelString = aurora.Cyan(levelString).String()
|
||||||
|
case LevelNotice:
|
||||||
|
levelString = aurora.Green(levelString).String()
|
||||||
case LevelWarn:
|
case LevelWarn:
|
||||||
levelString = aurora.Yellow(levelString).String()
|
levelString = aurora.Yellow(levelString).String()
|
||||||
case LevelError, LevelFatal, LevelPanic:
|
case LevelError, LevelFatal, LevelPanic:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const (
|
|||||||
LevelFatal
|
LevelFatal
|
||||||
LevelError
|
LevelError
|
||||||
LevelWarn
|
LevelWarn
|
||||||
|
LevelNotice
|
||||||
LevelInfo
|
LevelInfo
|
||||||
LevelDebug
|
LevelDebug
|
||||||
LevelTrace
|
LevelTrace
|
||||||
@@ -24,6 +25,8 @@ func FormatLevel(level Level) string {
|
|||||||
return "debug"
|
return "debug"
|
||||||
case LevelInfo:
|
case LevelInfo:
|
||||||
return "info"
|
return "info"
|
||||||
|
case LevelNotice:
|
||||||
|
return "notice"
|
||||||
case LevelWarn:
|
case LevelWarn:
|
||||||
return "warn"
|
return "warn"
|
||||||
case LevelError:
|
case LevelError:
|
||||||
@@ -45,6 +48,8 @@ func ParseLevel(level string) (Level, error) {
|
|||||||
return LevelDebug, nil
|
return LevelDebug, nil
|
||||||
case "info":
|
case "info":
|
||||||
return LevelInfo, nil
|
return LevelInfo, nil
|
||||||
|
case "notice":
|
||||||
|
return LevelNotice, nil
|
||||||
case "warn", "warning":
|
case "warn", "warning":
|
||||||
return LevelWarn, nil
|
return LevelWarn, nil
|
||||||
case "error":
|
case "error":
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ func (f *nopFactory) Debug(args ...any) {
|
|||||||
func (f *nopFactory) Info(args ...any) {
|
func (f *nopFactory) Info(args ...any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *nopFactory) Notice(args ...any) {
|
||||||
|
}
|
||||||
|
|
||||||
func (f *nopFactory) Warn(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) InfoContext(ctx context.Context, args ...any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *nopFactory) NoticeContext(ctx context.Context, args ...any) {
|
||||||
|
}
|
||||||
|
|
||||||
func (f *nopFactory) WarnContext(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...)
|
l.InfoContext(context.Background(), args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *observableLogger) Notice(args ...any) {
|
||||||
|
l.NoticeContext(context.Background(), args...)
|
||||||
|
}
|
||||||
|
|
||||||
func (l *observableLogger) Warn(args ...any) {
|
func (l *observableLogger) Warn(args ...any) {
|
||||||
l.WarnContext(context.Background(), args...)
|
l.WarnContext(context.Background(), args...)
|
||||||
}
|
}
|
||||||
@@ -182,6 +186,10 @@ func (l *observableLogger) InfoContext(ctx context.Context, args ...any) {
|
|||||||
l.Log(ctx, LevelInfo, args)
|
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) {
|
func (l *observableLogger) WarnContext(ctx context.Context, args ...any) {
|
||||||
l.Log(ctx, LevelWarn, args)
|
l.Log(ctx, LevelWarn, args)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ type ManagerServiceDatabase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ManagerServiceOptions struct {
|
type ManagerServiceOptions struct {
|
||||||
Inbounds []string `json:"inbounds"`
|
Inbounds []string `json:"inbounds"`
|
||||||
Database ManagerServiceDatabase `json:"database"`
|
Database ManagerServiceDatabase `json:"database"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
package option
|
package option
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/json/badoption"
|
"github.com/sagernet/sing/common/json/badoption"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MASQUEOutboundOptions struct {
|
type MASQUEOutboundOptions struct {
|
||||||
DialerOptions
|
DialerOptions
|
||||||
UseHTTP2 bool `json:"use_http2,omitempty"`
|
System bool `json:"system,omitempty"`
|
||||||
UseIPv6 bool `json:"use_ipv6,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Profile CloudflareProfile `json:"profile,omitempty"`
|
AllowedIPs badoption.Listable[netip.Prefix] `json:"allowed_ips,omitempty"`
|
||||||
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
|
UseHTTP2 bool `json:"use_http2,omitempty"`
|
||||||
UDPKeepalivePeriod badoption.Duration `json:"udp_keepalive_period,omitempty"`
|
UseIPv6 bool `json:"use_ipv6,omitempty"`
|
||||||
UDPInitialPacketSize uint16 `json:"udp_initial_packet_size,omitempty"`
|
Profile CloudflareProfile `json:"profile,omitempty"`
|
||||||
ReconnectDelay badoption.Duration `json:"reconnect_delay,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
|
MASQUEOutboundTLSOptionsContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,4 +10,19 @@ type MieruOutboundOptions struct {
|
|||||||
UserName string `json:"username,omitempty"`
|
UserName string `json:"username,omitempty"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
Multiplexing string `json:"multiplexing,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"`
|
Users []MTProxyUser `json:"users,omitempty"`
|
||||||
Concurrency uint `json:"concurrency,omitempty"`
|
Concurrency uint `json:"concurrency,omitempty"`
|
||||||
DomainFrontingPort uint `json:"domain_fronting_port,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"`
|
DomainFrontingProxyProtocol bool `json:"domain_fronting_proxy_protocol,omitempty"`
|
||||||
PreferIP string `json:"prefer_ip,omitempty"`
|
PreferIP string `json:"prefer_ip,omitempty"`
|
||||||
AutoUpdate bool `json:"auto_update,omitempty"`
|
AutoUpdate bool `json:"auto_update,omitempty"`
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package option
|
package option
|
||||||
|
|
||||||
type NodeServiceOptions struct {
|
type NodeServiceOptions struct {
|
||||||
UUID string `json:"uuid"`
|
UUID string `json:"uuid"`
|
||||||
Inbounds []string `json:"inbounds"`
|
Inbounds []string `json:"inbounds"`
|
||||||
ConnectionLimiters []string `json:"connection_limiters"`
|
ConnectionLimiters []string `json:"connection_limiters"`
|
||||||
BandwidthLimiters []string `json:"bandwidth_limiters"`
|
BandwidthLimiters []string `json:"bandwidth_limiters"`
|
||||||
TrafficLimiters []string `json:"traffic_limiters"`
|
TrafficLimiters []string `json:"traffic_limiters"`
|
||||||
RateLimiters []string `json:"rate_limiters"`
|
RateLimiters []string `json:"rate_limiters"`
|
||||||
Manager string `json:"manager"`
|
Manager string `json:"manager"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,45 @@
|
|||||||
package option
|
package option
|
||||||
|
|
||||||
import "github.com/sagernet/sing/common/json/badoption"
|
import (
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common/json/badoption"
|
||||||
|
)
|
||||||
|
|
||||||
type OpenVPNOutboundOptions struct {
|
type OpenVPNOutboundOptions struct {
|
||||||
DialerOptions
|
DialerOptions
|
||||||
Servers []ServerOptions `json:"servers"`
|
System bool `json:"system,omitempty"`
|
||||||
Proto string `json:"proto,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Cipher string `json:"cipher,omitempty"`
|
AllowedIPs badoption.Listable[netip.Prefix] `json:"allowed_ips,omitempty"`
|
||||||
Auth string `json:"auth,omitempty"`
|
Servers []ServerOptions `json:"servers"`
|
||||||
Username string `json:"username,omitempty"`
|
Proto string `json:"proto,omitempty"`
|
||||||
Password string `json:"password,omitempty"`
|
Cipher string `json:"cipher,omitempty"`
|
||||||
TLSCrypt string `json:"tls_crypt,omitempty"`
|
Auth string `json:"auth,omitempty"`
|
||||||
TLSCryptPath string `json:"tls_crypt_path,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
TLSCryptV2 bool `json:"tls_crypt_v2,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
TLSAuth string `json:"tls_auth,omitempty"`
|
TLSCrypt string `json:"tls_crypt,omitempty"`
|
||||||
TLSAuthPath string `json:"tls_auth_path,omitempty"`
|
TLSCryptPath string `json:"tls_crypt_path,omitempty"`
|
||||||
KeyDirection int `json:"key_direction,omitempty"`
|
TLSCryptV2 bool `json:"tls_crypt_v2,omitempty"`
|
||||||
ReconnectDelay badoption.Duration `json:"reconnect_delay,omitempty"`
|
TLSAuth string `json:"tls_auth,omitempty"`
|
||||||
PingInterval badoption.Duration `json:"ping_interval,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
|
OpenVPNOutboundTLSOptionsContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenVPNTLSOptions struct {
|
type OpenVPNTLSOptions struct {
|
||||||
Certificate string `json:"certificate,omitempty"`
|
Certificate string `json:"certificate,omitempty"`
|
||||||
CertificatePath string `json:"certificate_path,omitempty"`
|
CertificatePath string `json:"certificate_path,omitempty"`
|
||||||
Key string `json:"key,omitempty"`
|
Key string `json:"key,omitempty"`
|
||||||
KeyPath string `json:"key_path,omitempty"`
|
KeyPath string `json:"key_path,omitempty"`
|
||||||
CA string `json:"ca,omitempty"`
|
CA string `json:"ca,omitempty"`
|
||||||
CAPath string `json:"ca_path,omitempty"`
|
CAPath string `json:"ca_path,omitempty"`
|
||||||
CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"`
|
CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"`
|
||||||
VerifyX509Name string `json:"verify_x509_name,omitempty"`
|
VerifyX509Name string `json:"verify_x509_name,omitempty"`
|
||||||
VerifyX509NameMode string `json:"verify_x509_name_mode,omitempty"`
|
VerifyX509NameMode string `json:"verify_x509_name_mode,omitempty"`
|
||||||
KernelTx bool `json:"kernel_tx,omitempty"`
|
KernelTx bool `json:"kernel_tx,omitempty"`
|
||||||
KernelRx bool `json:"kernel_rx,omitempty"`
|
KernelRx bool `json:"kernel_rx,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenVPNOutboundTLSOptionsContainer struct {
|
type OpenVPNOutboundTLSOptionsContainer struct {
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
package option
|
package option
|
||||||
|
|
||||||
import "github.com/sagernet/sing/common/json/badoption"
|
|
||||||
|
|
||||||
type ProfilerServiceOptions struct {
|
type ProfilerServiceOptions struct {
|
||||||
Listen string `json:"listen,omitempty"`
|
ListenOptions
|
||||||
ReadTimeout badoption.Duration `json:"read_timeout,omitempty"`
|
|
||||||
WriteTimeout badoption.Duration `json:"write_timeout,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,39 @@ package option
|
|||||||
|
|
||||||
import "github.com/sagernet/sing/common/json/badoption"
|
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 {
|
type SSHOutboundOptions struct {
|
||||||
DialerOptions
|
DialerOptions
|
||||||
ServerOptions
|
ServerOptions
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
package option
|
package option
|
||||||
|
|
||||||
import "github.com/sagernet/sing/common/json/badoption"
|
|
||||||
|
|
||||||
type SudokuOutboundOptions struct {
|
type SudokuOutboundOptions struct {
|
||||||
DialerOptions
|
DialerOptions
|
||||||
ServerOptions
|
ServerOptions
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
AEADMethod string `json:"aead_method,omitempty"`
|
AEADMethod string `json:"aead_method,omitempty"`
|
||||||
PaddingMin *int `json:"padding_min,omitempty"`
|
PaddingMin *int `json:"padding_min,omitempty"`
|
||||||
PaddingMax *int `json:"padding_max,omitempty"`
|
PaddingMax *int `json:"padding_max,omitempty"`
|
||||||
TableType string `json:"table_type,omitempty"`
|
TableType string `json:"table_type,omitempty"`
|
||||||
EnablePureDownlink *bool `json:"enable_pure_downlink,omitempty"`
|
EnablePureDownlink *bool `json:"enable_pure_downlink,omitempty"`
|
||||||
CustomTable string `json:"custom_table,omitempty"`
|
CustomTable string `json:"custom_table,omitempty"`
|
||||||
CustomTables []string `json:"custom_tables,omitempty"`
|
CustomTables []string `json:"custom_tables,omitempty"`
|
||||||
HTTPMask *SudokuHTTPMask `json:"http_mask,omitempty"`
|
HTTPMask *SudokuHTTPMask `json:"http_mask,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SudokuHTTPMask struct {
|
type SudokuHTTPMask struct {
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
Mode string `json:"mode,omitempty"`
|
Mode string `json:"mode,omitempty"`
|
||||||
Host string `json:"host,omitempty"`
|
Host string `json:"host,omitempty"`
|
||||||
PathRoot string `json:"path_root,omitempty"`
|
PathRoot string `json:"path_root,omitempty"`
|
||||||
Multiplex string `json:"multiplex,omitempty"`
|
Multiplex string `json:"multiplex,omitempty"`
|
||||||
TLS *SudokuOutboundTLSOptions `json:"tls,omitempty"`
|
OutboundTLSOptionsContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
type SudokuInboundOptions struct {
|
type SudokuInboundOptions struct {
|
||||||
@@ -41,14 +39,3 @@ type SudokuInboundOptions struct {
|
|||||||
PathRoot string `json:"path_root,omitempty"`
|
PathRoot string `json:"path_root,omitempty"`
|
||||||
Fallback string `json:"fallback,omitempty"`
|
Fallback string `json:"fallback,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SudokuOutboundTLSOptions struct {
|
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
|
||||||
Fragment bool `json:"fragment,omitempty"`
|
|
||||||
FragmentFallbackDelay badoption.Duration `json:"fragment_fallback_delay,omitempty"`
|
|
||||||
RecordFragment bool `json:"record_fragment,omitempty"`
|
|
||||||
KernelTx bool `json:"kernel_tx,omitempty"`
|
|
||||||
KernelRx bool `json:"kernel_rx,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ type TrustTunnelOutboundOptions struct {
|
|||||||
DialerOptions
|
DialerOptions
|
||||||
ServerOptions
|
ServerOptions
|
||||||
OutboundTLSOptionsContainer
|
OutboundTLSOptionsContainer
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
Network NetworkList `json:"network,omitempty"`
|
Network NetworkList `json:"network,omitempty"`
|
||||||
HealthCheck bool `json:"health_check,omitempty"`
|
HealthCheck bool `json:"health_check,omitempty"`
|
||||||
QUIC bool `json:"quic,omitempty"`
|
QUIC bool `json:"quic,omitempty"`
|
||||||
CongestionController string `json:"congestion_controller,omitempty"`
|
CongestionController string `json:"congestion_controller,omitempty"`
|
||||||
BBRProfile string `json:"bbr_profile,omitempty"`
|
BBRProfile string `json:"bbr_profile,omitempty"`
|
||||||
CWND int `json:"cwnd,omitempty"`
|
CWND int `json:"cwnd,omitempty"`
|
||||||
Multiplex *TrustTunnelMultiplexOptions `json:"multiplex,omitempty"`
|
Multiplex *TrustTunnelMultiplexOptions `json:"multiplex,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func NewBondedConn(conns []net.Conn, downloadRatios, uploadRatios []uint8) *bond
|
|||||||
conns: conns,
|
conns: conns,
|
||||||
downloadRatios: downloadRatios,
|
downloadRatios: downloadRatios,
|
||||||
uploadRatios: uploadRatios,
|
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 {
|
func NewFailoverConn(ctx context.Context, conn net.Conn, dial dial, onClose func()) *failoverConn {
|
||||||
var writeBuffers [BufferSize][]byte
|
var writeBuffers [BufferSize][]byte
|
||||||
for i := range BufferSize {
|
for i := range BufferSize {
|
||||||
writeBuffers[i] = make([]byte, 0, 1000)
|
writeBuffers[i] = make([]byte, 0, 1024)
|
||||||
}
|
}
|
||||||
return &failoverConn{
|
return &failoverConn{
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
dial: dial,
|
dial: dial,
|
||||||
readBuffer: bytes.NewBuffer(make([]byte, 0, 1000)),
|
readBuffer: bytes.NewBuffer(make([]byte, 0, 1024)),
|
||||||
writeBuffers: writeBuffers,
|
writeBuffers: writeBuffers,
|
||||||
onClose: onClose,
|
onClose: onClose,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,8 +194,6 @@ type bwConnEntry struct {
|
|||||||
conn net.Conn
|
conn net.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type ManagerBandwidthStrategy struct {
|
type ManagerBandwidthStrategy struct {
|
||||||
strategies map[string]BandwidthStrategy
|
strategies map[string]BandwidthStrategy
|
||||||
conns map[string][]*bwConnEntry
|
conns map[string][]*bwConnEntry
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/adapter/outbound"
|
"github.com/sagernet/sing-box/adapter/outbound"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/common/onclose"
|
"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/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-box/route"
|
"github.com/sagernet/sing-box/route"
|
||||||
@@ -173,8 +173,6 @@ func (h *Outbound) GetStrategy() ConnectionStrategy {
|
|||||||
return h.strategy
|
return h.strategy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func connChecker(ctx context.Context, closeFunc func() error) {
|
func connChecker(ctx context.Context, closeFunc func() error) {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
closeFunc()
|
closeFunc()
|
||||||
|
|||||||
@@ -76,8 +76,6 @@ type connEntry struct {
|
|||||||
conn net.Conn
|
conn net.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type ManagerTrafficStrategy struct {
|
type ManagerTrafficStrategy struct {
|
||||||
strategies map[string]TrafficStrategy
|
strategies map[string]TrafficStrategy
|
||||||
conns map[string][]*connEntry
|
conns map[string][]*connEntry
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing-box/adapter/outbound"
|
"github.com/sagernet/sing-box/adapter/outbound"
|
||||||
"github.com/sagernet/sing-box/common/cloudflare"
|
"github.com/sagernet/sing-box/common/cloudflare"
|
||||||
"github.com/sagernet/sing-box/common/dialer"
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
@@ -18,6 +17,7 @@ import (
|
|||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-box/transport/masque"
|
"github.com/sagernet/sing-box/transport/masque"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
@@ -136,11 +136,19 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
ctx,
|
ctx,
|
||||||
logger,
|
logger,
|
||||||
masque.TunnelOptions{
|
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,
|
Dialer: outboundDialer,
|
||||||
Address: []netip.Prefix{
|
Address: []netip.Prefix{
|
||||||
netip.MustParsePrefix(appConfig.IPv4 + "/32"),
|
netip.MustParsePrefix(appConfig.IPv4 + "/32"),
|
||||||
netip.MustParsePrefix(appConfig.IPv6 + "/128"),
|
netip.MustParsePrefix(appConfig.IPv6 + "/128"),
|
||||||
},
|
},
|
||||||
|
AllowedAddress: options.AllowedIPs,
|
||||||
Endpoint: endpoint,
|
Endpoint: endpoint,
|
||||||
TLSConfig: tlsConfig,
|
TLSConfig: tlsConfig,
|
||||||
UseHTTP2: options.UseHTTP2,
|
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"
|
mieruclient "github.com/enfein/mieru/v3/apis/client"
|
||||||
mierucommon "github.com/enfein/mieru/v3/apis/common"
|
mierucommon "github.com/enfein/mieru/v3/apis/common"
|
||||||
mierumodel "github.com/enfein/mieru/v3/apis/model"
|
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"
|
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
|
||||||
"google.golang.org/protobuf/proto"
|
"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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -52,7 +53,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
if err := c.Start(); err != nil {
|
if err := c.Start(); err != nil {
|
||||||
return nil, fmt.Errorf("failed to start mieru client: %w", err)
|
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{
|
return &Outbound{
|
||||||
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeMieru, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
|
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)
|
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.
|
// streamer converts a net.PacketConn to a net.Conn.
|
||||||
type streamer struct {
|
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)
|
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{}
|
server := &mierupb.ServerEndpoint{}
|
||||||
if options.ServerPort != 0 {
|
if options.ServerPort != 0 {
|
||||||
server.PortBindings = append(server.PortBindings, &mierupb.PortBinding{
|
server.PortBindings = append(server.PortBindings, &mierupb.PortBinding{
|
||||||
@@ -189,13 +204,21 @@ func buildMieruClientConfig(options option.MieruOutboundOptions, dialer mieruDia
|
|||||||
},
|
},
|
||||||
Servers: []*mierupb.ServerEndpoint{server},
|
Servers: []*mierupb.ServerEndpoint{server},
|
||||||
},
|
},
|
||||||
Dialer: dialer,
|
Dialer: dialer,
|
||||||
|
PacketDialer: dialer,
|
||||||
|
DNSConfig: &mierucommon.ClientDNSConfig{
|
||||||
|
BypassDialerDNS: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
if multiplexing, ok := mierupb.MultiplexingLevel_value[options.Multiplexing]; ok {
|
if multiplexing, ok := mierupb.MultiplexingLevel_value[options.Multiplexing]; ok {
|
||||||
config.Profile.Multiplexing = &mierupb.MultiplexingConfig{
|
config.Profile.Multiplexing = &mierupb.MultiplexingConfig{
|
||||||
Level: mierupb.MultiplexingLevel(multiplexing).Enum(),
|
Level: mierupb.MultiplexingLevel(multiplexing).Enum(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if options.TrafficPattern != "" {
|
||||||
|
trafficPattern, _ := mierutp.Decode(options.TrafficPattern)
|
||||||
|
config.Profile.TrafficPattern = trafficPattern
|
||||||
|
}
|
||||||
return config, nil
|
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")
|
return fmt.Errorf("begin port must be less than or equal to end port")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if options.Transport != "TCP" {
|
if options.Transport != "TCP" && options.Transport != "UDP" {
|
||||||
return fmt.Errorf("transport must be TCP")
|
return fmt.Errorf("transport must be TCP or UDP")
|
||||||
}
|
}
|
||||||
if options.UserName == "" {
|
if options.UserName == "" {
|
||||||
return fmt.Errorf("username is empty")
|
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)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||||||
Secrets: secrets,
|
Secrets: secrets,
|
||||||
Concurrency: options.GetConcurrency(),
|
Concurrency: options.GetConcurrency(),
|
||||||
DomainFrontingPort: options.GetDomainFrontingPort(),
|
DomainFrontingPort: options.GetDomainFrontingPort(),
|
||||||
DomainFrontingIP: options.DomainFrontingIP,
|
DomainFrontingHost: options.DomainFrontingHost,
|
||||||
DomainFrontingProxyProtocol: options.DomainFrontingProxyProtocol,
|
DomainFrontingProxyProtocol: options.DomainFrontingProxyProtocol,
|
||||||
PreferIP: options.GetPreferIP(),
|
PreferIP: options.GetPreferIP(),
|
||||||
AutoUpdate: options.AutoUpdate,
|
AutoUpdate: options.AutoUpdate,
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ func (h *Outbound) Start(stage adapter.StartStage) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
h.logger.Info("NaiveProxy started, version: ", h.client.Engine().Version())
|
h.logger.Notice("NaiveProxy started, version: ", h.client.Engine().Version())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ package openvpn
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/adapter/outbound"
|
"github.com/sagernet/sing-box/adapter/outbound"
|
||||||
@@ -90,10 +90,18 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
tunnel, err := ovpn.NewTunnel(ctx, logger, ovpn.TunnelOptions{
|
tunnel, err := ovpn.NewTunnel(ctx, logger, ovpn.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,
|
Dialer: outboundDialer,
|
||||||
Servers: options.Servers,
|
Servers: options.Servers,
|
||||||
TLSConfig: tlsConfig,
|
TLSConfig: tlsConfig,
|
||||||
Config: clientConfig,
|
Config: clientConfig,
|
||||||
|
AllowedAddress: options.AllowedIPs,
|
||||||
ReconnectDelay: time.Duration(options.ReconnectDelay),
|
ReconnectDelay: time.Duration(options.ReconnectDelay),
|
||||||
PingInterval: time.Duration(options.PingInterval),
|
PingInterval: time.Duration(options.PingInterval),
|
||||||
})
|
})
|
||||||
|
|||||||
91
protocol/ssh/certificate.go
Normal file
91
protocol/ssh/certificate.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseCAKey(options *option.SSHCAOptions) (ssh.Signer, error) {
|
||||||
|
var keyData []byte
|
||||||
|
var err error
|
||||||
|
if len(options.PrivateKey) > 0 {
|
||||||
|
keyData = []byte(strings.Join(options.PrivateKey, "\n"))
|
||||||
|
} else if options.PrivateKeyPath != "" {
|
||||||
|
keyData, err = os.ReadFile(os.ExpandEnv(options.PrivateKeyPath))
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "read CA private key")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, E.New("missing CA private key")
|
||||||
|
}
|
||||||
|
if options.PrivateKeyPassphrase == "" {
|
||||||
|
return ssh.ParsePrivateKey(keyData)
|
||||||
|
}
|
||||||
|
return ssh.ParsePrivateKeyWithPassphrase(keyData, []byte(options.PrivateKeyPassphrase))
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyCertificate(signer ssh.Signer, metadata ssh.ConnMetadata, key ssh.PublicKey) bool {
|
||||||
|
if signer == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
certificate, ok := key.(*ssh.Certificate)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
checker := &ssh.CertChecker{
|
||||||
|
IsUserAuthority: func(auth ssh.PublicKey) bool {
|
||||||
|
return bytes.Equal(auth.Marshal(), signer.PublicKey().Marshal())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !checker.IsUserAuthority(certificate.SignatureKey) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return checker.CheckCert(metadata.User(), certificate) == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func issueCertificate(signer ssh.Signer, user string) (ssh.Signer, error) {
|
||||||
|
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ephemeral, err := ssh.NewSignerFromSigner(privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
certificate := &ssh.Certificate{
|
||||||
|
Key: ephemeral.PublicKey(),
|
||||||
|
Serial: uint64(now.UnixNano()),
|
||||||
|
CertType: ssh.UserCert,
|
||||||
|
KeyId: user,
|
||||||
|
ValidPrincipals: []string{user},
|
||||||
|
ValidAfter: uint64(now.Add(-1 * time.Minute).Unix()),
|
||||||
|
ValidBefore: uint64(now.Add(5 * time.Minute).Unix()),
|
||||||
|
Permissions: ssh.Permissions{
|
||||||
|
Extensions: map[string]string{
|
||||||
|
"permit-pty": "",
|
||||||
|
"permit-port-forwarding": "",
|
||||||
|
"permit-agent-forwarding": "",
|
||||||
|
"permit-X11-forwarding": "",
|
||||||
|
"permit-user-rc": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := certificate.SignCert(rand.Reader, signer); err != nil {
|
||||||
|
return nil, E.Cause(err, "sign certificate")
|
||||||
|
}
|
||||||
|
certSigner, err := ssh.NewCertSigner(certificate, ephemeral)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "create certificate signer")
|
||||||
|
}
|
||||||
|
return certSigner, nil
|
||||||
|
}
|
||||||
322
protocol/ssh/fallback.go
Normal file
322
protocol/ssh/fallback.go
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
|
"github.com/sagernet/sing-box/common/onclose"
|
||||||
|
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"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Service = (*Fallback)(nil)
|
||||||
|
|
||||||
|
type Fallback struct {
|
||||||
|
Service
|
||||||
|
ctx context.Context
|
||||||
|
logger logger.ContextLogger
|
||||||
|
dialer N.Dialer
|
||||||
|
serverAddr M.Socksaddr
|
||||||
|
clientVersion string
|
||||||
|
mainSigner ssh.Signer
|
||||||
|
issueSigner ssh.Signer
|
||||||
|
hostKeys []ssh.PublicKey
|
||||||
|
keyAlgorithms []string
|
||||||
|
pending map[string]*upstreamConn
|
||||||
|
mtx sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type upstreamConn struct {
|
||||||
|
conn net.Conn
|
||||||
|
client ssh.Conn
|
||||||
|
channels <-chan ssh.NewChannel
|
||||||
|
requests <-chan *ssh.Request
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFallback(ctx context.Context, logger logger.ContextLogger, inner Service, options *option.SSHFallbackServerOptions) (*Fallback, error) {
|
||||||
|
serverAddr := options.Build()
|
||||||
|
if serverAddr.Port == 0 {
|
||||||
|
serverAddr.Port = 22
|
||||||
|
}
|
||||||
|
if !serverAddr.Addr.IsValid() && serverAddr.Fqdn == "" {
|
||||||
|
return nil, E.New("missing upstream server address")
|
||||||
|
}
|
||||||
|
upstreamDialer, err := dialer.New(ctx, options.DialerOptions, serverAddr.IsFqdn())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fallback := &Fallback{
|
||||||
|
Service: inner,
|
||||||
|
ctx: ctx,
|
||||||
|
logger: logger,
|
||||||
|
dialer: upstreamDialer,
|
||||||
|
serverAddr: serverAddr,
|
||||||
|
clientVersion: options.ClientVersion,
|
||||||
|
keyAlgorithms: options.HostKeyAlgorithms,
|
||||||
|
pending: make(map[string]*upstreamConn),
|
||||||
|
}
|
||||||
|
if fallback.clientVersion == "" {
|
||||||
|
fallback.clientVersion = "SSH-2.0-OpenSSH_9.6"
|
||||||
|
}
|
||||||
|
if options.CA != nil {
|
||||||
|
signer, err := parseCAKey(options.CA)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse CA")
|
||||||
|
}
|
||||||
|
fallback.mainSigner = signer
|
||||||
|
}
|
||||||
|
if options.IssueCA != nil {
|
||||||
|
signer, err := parseCAKey(options.IssueCA)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse issue CA")
|
||||||
|
}
|
||||||
|
fallback.issueSigner = signer
|
||||||
|
}
|
||||||
|
if fallback.issueSigner == nil && fallback.mainSigner != nil {
|
||||||
|
fallback.issueSigner = fallback.mainSigner
|
||||||
|
}
|
||||||
|
for _, hostKey := range options.HostKey {
|
||||||
|
key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(hostKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.New("parse upstream host key ", hostKey)
|
||||||
|
}
|
||||||
|
fallback.hostKeys = append(fallback.hostKeys, key)
|
||||||
|
}
|
||||||
|
for _, hostKeyPath := range options.HostKeyPath {
|
||||||
|
content, err := os.ReadFile(os.ExpandEnv(hostKeyPath))
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "read upstream host key ", hostKeyPath)
|
||||||
|
}
|
||||||
|
key, _, _, _, err := ssh.ParseAuthorizedKey(content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse upstream host key ", hostKeyPath)
|
||||||
|
}
|
||||||
|
fallback.hostKeys = append(fallback.hostKeys, key)
|
||||||
|
}
|
||||||
|
return fallback, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fallback) PasswordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
||||||
|
if permissions, err := f.Service.PasswordCallback(conn, password); err == nil {
|
||||||
|
return permissions, nil
|
||||||
|
}
|
||||||
|
if err := f.dial(string(conn.SessionID()), conn.User(), ssh.Password(string(password))); err != nil {
|
||||||
|
return nil, E.Cause(err, "upstream authentication failed for user ", conn.User())
|
||||||
|
}
|
||||||
|
return &ssh.Permissions{Extensions: map[string]string{"user": conn.User(), "fallback": "1"}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fallback) PublicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||||
|
if permissions, err := f.Service.PublicKeyCallback(conn, key); err == nil {
|
||||||
|
return permissions, nil
|
||||||
|
}
|
||||||
|
if verifyCertificate(f.mainSigner, conn, key) {
|
||||||
|
signer, err := issueCertificate(f.issueSigner, conn.User())
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "upstream authentication failed for user ", conn.User())
|
||||||
|
}
|
||||||
|
if err := f.dial(string(conn.SessionID()), conn.User(), ssh.PublicKeys(signer)); err != nil {
|
||||||
|
return nil, E.Cause(err, "upstream authentication failed for user ", conn.User())
|
||||||
|
}
|
||||||
|
return &ssh.Permissions{Extensions: map[string]string{"user": conn.User(), "fallback": "1"}}, nil
|
||||||
|
}
|
||||||
|
return nil, E.New("public key authentication failed for user ", conn.User())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fallback) Handle(ctx context.Context, serverConn *ssh.ServerConn, channels <-chan ssh.NewChannel, requests <-chan *ssh.Request, metadata adapter.InboundContext, user string) {
|
||||||
|
if serverConn.Permissions == nil || serverConn.Permissions.Extensions["fallback"] != "1" {
|
||||||
|
f.Service.Handle(ctx, serverConn, channels, requests, metadata, user)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sessionID := string(serverConn.SessionID())
|
||||||
|
f.mtx.Lock()
|
||||||
|
upstream := f.pending[sessionID]
|
||||||
|
delete(f.pending, sessionID)
|
||||||
|
f.mtx.Unlock()
|
||||||
|
if upstream == nil {
|
||||||
|
serverConn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.logger.InfoContext(ctx, "[", user, "] forwarded SSH connection from ", metadata.Source)
|
||||||
|
go proxyDownstreamRequests(requests, upstream.client)
|
||||||
|
go proxyGlobalRequests(upstream.requests, serverConn)
|
||||||
|
go func() {
|
||||||
|
for newChannel := range upstream.channels {
|
||||||
|
go proxyChannel(newChannel, serverConn)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for newChannel := range channels {
|
||||||
|
wg.Go(func() {
|
||||||
|
proxyChannel(newChannel, upstream.client)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
upstream.client.Close()
|
||||||
|
upstream.conn.Close()
|
||||||
|
serverConn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fallback) Close() error {
|
||||||
|
f.mtx.Lock()
|
||||||
|
connections := make([]net.Conn, 0, len(f.pending))
|
||||||
|
for id, upstream := range f.pending {
|
||||||
|
if upstream != nil {
|
||||||
|
connections = append(connections, upstream.conn)
|
||||||
|
}
|
||||||
|
delete(f.pending, id)
|
||||||
|
}
|
||||||
|
f.mtx.Unlock()
|
||||||
|
for _, conn := range connections {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
return f.Service.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fallback) dial(sessionID string, user string, auth ssh.AuthMethod) error {
|
||||||
|
f.mtx.Lock()
|
||||||
|
if _, attempted := f.pending[sessionID]; attempted {
|
||||||
|
f.mtx.Unlock()
|
||||||
|
return E.New("fallback already attempted")
|
||||||
|
}
|
||||||
|
f.pending[sessionID] = nil
|
||||||
|
f.mtx.Unlock()
|
||||||
|
conn, err := f.dialer.DialContext(f.ctx, N.NetworkTCP, f.serverAddr)
|
||||||
|
if err != nil {
|
||||||
|
f.mtx.Lock()
|
||||||
|
delete(f.pending, sessionID)
|
||||||
|
f.mtx.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn = onclose.NewConn(conn, func() {
|
||||||
|
f.mtx.Lock()
|
||||||
|
delete(f.pending, sessionID)
|
||||||
|
f.mtx.Unlock()
|
||||||
|
})
|
||||||
|
config := &ssh.ClientConfig{
|
||||||
|
User: user,
|
||||||
|
Auth: []ssh.AuthMethod{auth},
|
||||||
|
ClientVersion: f.clientVersion,
|
||||||
|
HostKeyAlgorithms: f.keyAlgorithms,
|
||||||
|
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||||
|
if len(f.hostKeys) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
serverKey := key.Marshal()
|
||||||
|
for _, hostKey := range f.hostKeys {
|
||||||
|
if bytes.Equal(serverKey, hostKey.Marshal()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return E.New("upstream host key mismatch, server sent ", key.Type(), " ", base64.StdEncoding.EncodeToString(serverKey))
|
||||||
|
},
|
||||||
|
Timeout: C.TCPTimeout,
|
||||||
|
}
|
||||||
|
client, channels, requests, err := ssh.NewClientConn(conn, f.serverAddr.String(), config)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.mtx.Lock()
|
||||||
|
f.pending[sessionID] = &upstreamConn{conn: conn, client: client, channels: channels, requests: requests}
|
||||||
|
f.mtx.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func proxyChannel(newChannel ssh.NewChannel, target ssh.Conn) {
|
||||||
|
targetChannel, targetRequests, err := target.OpenChannel(newChannel.ChannelType(), newChannel.ExtraData())
|
||||||
|
if err != nil {
|
||||||
|
if openErr, ok := err.(*ssh.OpenChannelError); ok {
|
||||||
|
newChannel.Reject(openErr.Reason, openErr.Message)
|
||||||
|
} else {
|
||||||
|
newChannel.Reject(ssh.ConnectionFailed, err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sourceChannel, sourceRequests, err := newChannel.Accept()
|
||||||
|
if err != nil {
|
||||||
|
targetChannel.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go proxyChannelRequests(sourceRequests, targetChannel)
|
||||||
|
go io.Copy(targetChannel.Stderr(), sourceChannel.Stderr())
|
||||||
|
go io.Copy(sourceChannel.Stderr(), targetChannel.Stderr())
|
||||||
|
go func() {
|
||||||
|
io.Copy(targetChannel, sourceChannel)
|
||||||
|
targetChannel.CloseWrite()
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
io.Copy(sourceChannel, targetChannel)
|
||||||
|
sourceChannel.CloseWrite()
|
||||||
|
}()
|
||||||
|
proxyChannelRequests(targetRequests, sourceChannel)
|
||||||
|
sourceChannel.Close()
|
||||||
|
targetChannel.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func proxyGlobalRequests(requests <-chan *ssh.Request, target ssh.Conn) {
|
||||||
|
for request := range requests {
|
||||||
|
if request.Type == "hostkeys-00@openssh.com" {
|
||||||
|
if request.WantReply {
|
||||||
|
request.Reply(false, nil)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ok, payload, err := target.SendRequest(request.Type, request.WantReply, request.Payload)
|
||||||
|
if request.WantReply {
|
||||||
|
if err != nil {
|
||||||
|
request.Reply(false, nil)
|
||||||
|
} else {
|
||||||
|
request.Reply(ok, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func proxyDownstreamRequests(requests <-chan *ssh.Request, target ssh.Conn) {
|
||||||
|
for request := range requests {
|
||||||
|
switch request.Type {
|
||||||
|
case "no-more-sessions@openssh.com", "hostkeys-prove-00@openssh.com":
|
||||||
|
if request.WantReply {
|
||||||
|
request.Reply(false, nil)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ok, payload, err := target.SendRequest(request.Type, request.WantReply, request.Payload)
|
||||||
|
if request.WantReply {
|
||||||
|
if err != nil {
|
||||||
|
request.Reply(false, nil)
|
||||||
|
} else {
|
||||||
|
request.Reply(ok, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func proxyChannelRequests(requests <-chan *ssh.Request, target ssh.Channel) {
|
||||||
|
for request := range requests {
|
||||||
|
ok, err := target.SendRequest(request.Type, request.WantReply, request.Payload)
|
||||||
|
if request.WantReply {
|
||||||
|
if err != nil {
|
||||||
|
request.Reply(false, nil)
|
||||||
|
} else {
|
||||||
|
request.Reply(ok, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
152
protocol/ssh/inbound.go
Normal file
152
protocol/ssh/inbound.go
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/adapter/inbound"
|
||||||
|
"github.com/sagernet/sing-box/common/listener"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterInbound(registry *inbound.Registry) {
|
||||||
|
inbound.Register[option.SSHInboundOptions](registry, C.TypeSSH, NewInbound)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
|
||||||
|
|
||||||
|
type Inbound struct {
|
||||||
|
inbound.Adapter
|
||||||
|
logger logger.ContextLogger
|
||||||
|
listener *listener.Listener
|
||||||
|
serverConfig *ssh.ServerConfig
|
||||||
|
service Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SSHInboundOptions) (adapter.Inbound, error) {
|
||||||
|
if len(options.Users) == 0 && options.Fallback == nil {
|
||||||
|
return nil, E.New("missing users")
|
||||||
|
}
|
||||||
|
inbound := &Inbound{
|
||||||
|
Adapter: inbound.NewAdapter(C.TypeSSH, tag),
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
defaultService := newService(router, logger, options.Users)
|
||||||
|
if options.Fallback != nil {
|
||||||
|
fallback, err := NewFallback(ctx, logger, defaultService, options.Fallback)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inbound.service = fallback
|
||||||
|
} else {
|
||||||
|
inbound.service = defaultService
|
||||||
|
}
|
||||||
|
serverVersion := options.ServerVersion
|
||||||
|
if serverVersion == "" {
|
||||||
|
serverVersion = "SSH-2.0-OpenSSH_9.6"
|
||||||
|
}
|
||||||
|
serverConfig := &ssh.ServerConfig{
|
||||||
|
ServerVersion: serverVersion,
|
||||||
|
MaxAuthTries: options.MaxAuthTries,
|
||||||
|
PasswordCallback: inbound.service.PasswordCallback,
|
||||||
|
PublicKeyCallback: inbound.service.PublicKeyCallback,
|
||||||
|
}
|
||||||
|
var hostKeys []ssh.Signer
|
||||||
|
for _, hostKey := range options.HostKey {
|
||||||
|
signer, err := ssh.ParsePrivateKey([]byte(hostKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse host key")
|
||||||
|
}
|
||||||
|
hostKeys = append(hostKeys, signer)
|
||||||
|
}
|
||||||
|
for _, hostKeyPath := range options.HostKeyPath {
|
||||||
|
content, err := os.ReadFile(os.ExpandEnv(hostKeyPath))
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "read host key ", hostKeyPath)
|
||||||
|
}
|
||||||
|
signer, err := ssh.ParsePrivateKey(content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse host key ", hostKeyPath)
|
||||||
|
}
|
||||||
|
hostKeys = append(hostKeys, signer)
|
||||||
|
}
|
||||||
|
if len(hostKeys) == 0 {
|
||||||
|
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "generate host key")
|
||||||
|
}
|
||||||
|
signer, err := ssh.NewSignerFromSigner(privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "generate host key")
|
||||||
|
}
|
||||||
|
hostKeys = append(hostKeys, signer)
|
||||||
|
}
|
||||||
|
for _, hostKey := range hostKeys {
|
||||||
|
serverConfig.AddHostKey(hostKey)
|
||||||
|
}
|
||||||
|
inbound.serverConfig = serverConfig
|
||||||
|
inbound.listener = listener.New(listener.Options{
|
||||||
|
Context: ctx,
|
||||||
|
Logger: logger,
|
||||||
|
Network: []string{N.NetworkTCP},
|
||||||
|
Listen: options.ListenOptions,
|
||||||
|
ConnectionHandler: inbound,
|
||||||
|
})
|
||||||
|
return inbound, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||||
|
if stage != adapter.StartStateStart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return h.listener.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Inbound) Close() error {
|
||||||
|
return E.Errors(h.service.Close(), h.listener.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Inbound) UpdateUsers(users []option.SSHUser) {
|
||||||
|
h.service.UpdateUsers(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||||
|
metadata.Inbound = h.Tag()
|
||||||
|
metadata.InboundType = h.Type()
|
||||||
|
serverConn, channels, requests, err := ssh.NewServerConn(conn, h.serverConfig)
|
||||||
|
if err != nil {
|
||||||
|
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||||
|
if E.IsClosedOrCanceled(err) {
|
||||||
|
h.logger.DebugContext(ctx, "connection closed: ", err)
|
||||||
|
} else {
|
||||||
|
h.logger.DebugContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var user string
|
||||||
|
if serverConn.Permissions != nil {
|
||||||
|
user = serverConn.Permissions.Extensions["user"]
|
||||||
|
}
|
||||||
|
if user == "" {
|
||||||
|
user = serverConn.User()
|
||||||
|
}
|
||||||
|
if user != "" {
|
||||||
|
metadata.User = user
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
serverConn.Wait()
|
||||||
|
conn.Close()
|
||||||
|
}()
|
||||||
|
h.service.Handle(ctx, serverConn, channels, requests, metadata, user)
|
||||||
|
}
|
||||||
165
protocol/ssh/service.go
Normal file
165
protocol/ssh/service.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/listener"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common/bufio/deadline"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
PasswordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error)
|
||||||
|
PublicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error)
|
||||||
|
UpdateUsers(users []option.SSHUser)
|
||||||
|
Handle(ctx context.Context, serverConn *ssh.ServerConn, channels <-chan ssh.NewChannel, requests <-chan *ssh.Request, metadata adapter.InboundContext, user string)
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Service = (*service)(nil)
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
router adapter.ConnectionRouterEx
|
||||||
|
logger logger.ContextLogger
|
||||||
|
listener *listener.Listener
|
||||||
|
users []option.SSHUser
|
||||||
|
mtx sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newService(router adapter.ConnectionRouterEx, logger logger.ContextLogger, users []option.SSHUser) *service {
|
||||||
|
return &service{
|
||||||
|
router: router,
|
||||||
|
logger: logger,
|
||||||
|
users: users,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *service) PasswordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
||||||
|
h.mtx.RLock()
|
||||||
|
users := h.users
|
||||||
|
h.mtx.RUnlock()
|
||||||
|
for _, user := range users {
|
||||||
|
if user.Name != "" && user.Name != conn.User() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if user.Password != "" && user.Password == string(password) {
|
||||||
|
return &ssh.Permissions{Extensions: map[string]string{"user": user.Name}}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, E.New("password authentication failed for user ", conn.User())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *service) PublicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||||
|
h.mtx.RLock()
|
||||||
|
users := h.users
|
||||||
|
h.mtx.RUnlock()
|
||||||
|
for _, user := range users {
|
||||||
|
if user.Name != "" && user.Name != conn.User() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, authorizedKey := range user.AuthorizedKeys {
|
||||||
|
parsed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(authorizedKey))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if bytes.Equal(parsed.Marshal(), key.Marshal()) {
|
||||||
|
return &ssh.Permissions{Extensions: map[string]string{"user": user.Name}}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, E.New("public key authentication failed for user ", conn.User())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *service) UpdateUsers(users []option.SSHUser) {
|
||||||
|
h.mtx.Lock()
|
||||||
|
h.users = users
|
||||||
|
h.mtx.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *service) Handle(ctx context.Context, serverConn *ssh.ServerConn, channels <-chan ssh.NewChannel, requests <-chan *ssh.Request, metadata adapter.InboundContext, user string) {
|
||||||
|
h.logger.InfoContext(ctx, "[", user, "] authenticated SSH connection from ", metadata.Source)
|
||||||
|
go ssh.DiscardRequests(requests)
|
||||||
|
for newChannel := range channels {
|
||||||
|
switch newChannel.ChannelType() {
|
||||||
|
case "direct-tcpip":
|
||||||
|
go h.handleDirectChannel(ctx, metadata, newChannel)
|
||||||
|
default:
|
||||||
|
newChannel.Reject(ssh.UnknownChannelType, "only direct-tcpip is supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *service) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *service) handleDirectChannel(ctx context.Context, metadata adapter.InboundContext, newChannel ssh.NewChannel) {
|
||||||
|
var payload directTCPIPData
|
||||||
|
if err := ssh.Unmarshal(newChannel.ExtraData(), &payload); err != nil {
|
||||||
|
newChannel.Reject(ssh.ConnectionFailed, "invalid direct-tcpip payload")
|
||||||
|
h.logger.ErrorContext(ctx, E.Cause(err, "parse direct-tcpip payload"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
channel, requests, err := newChannel.Accept()
|
||||||
|
if err != nil {
|
||||||
|
h.logger.ErrorContext(ctx, E.Cause(err, "accept direct-tcpip channel"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go ssh.DiscardRequests(requests)
|
||||||
|
connMetadata := metadata
|
||||||
|
connMetadata.Destination = M.ParseSocksaddrHostPort(payload.HostToConnect, uint16(payload.PortToConnect))
|
||||||
|
conn := deadline.NewConn(&channelConn{
|
||||||
|
Channel: channel,
|
||||||
|
localAddr: metadata.OriginDestination.TCPAddr(),
|
||||||
|
remoteAddr: metadata.Source.TCPAddr(),
|
||||||
|
})
|
||||||
|
h.logger.InfoContext(ctx, "[", metadata.User, "] inbound connection to ", connMetadata.Destination)
|
||||||
|
h.router.RouteConnectionEx(ctx, conn, connMetadata, N.OnceClose(func(it error) {
|
||||||
|
channel.Close()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
type directTCPIPData struct {
|
||||||
|
HostToConnect string
|
||||||
|
PortToConnect uint32
|
||||||
|
OriginatorAddress string
|
||||||
|
OriginatorPort uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type channelConn struct {
|
||||||
|
ssh.Channel
|
||||||
|
localAddr net.Addr
|
||||||
|
remoteAddr net.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *channelConn) LocalAddr() net.Addr {
|
||||||
|
return c.localAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *channelConn) RemoteAddr() net.Addr {
|
||||||
|
return c.remoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *channelConn) SetDeadline(t time.Time) error {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *channelConn) SetReadDeadline(t time.Time) error {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *channelConn) SetWriteDeadline(t time.Time) error {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
@@ -67,27 +67,27 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||||||
protoConf.AEADMethod = options.AEADMethod
|
protoConf.AEADMethod = options.AEADMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
in := &Inbound{
|
inbound := &Inbound{
|
||||||
Adapter: inbound.NewAdapter(C.TypeSudoku, tag),
|
Adapter: inbound.NewAdapter(C.TypeSudoku, tag),
|
||||||
router: router,
|
router: router,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
protoConf: protoConf,
|
protoConf: protoConf,
|
||||||
fallback: strings.TrimSpace(options.Fallback),
|
fallback: strings.TrimSpace(options.Fallback),
|
||||||
}
|
}
|
||||||
if in.fallback != "" {
|
if inbound.fallback != "" {
|
||||||
in.tunnelSrv = sudoku.NewHTTPMaskTunnelServerWithFallback(&in.protoConf)
|
inbound.tunnelSrv = sudoku.NewHTTPMaskTunnelServerWithFallback(&inbound.protoConf)
|
||||||
} else {
|
} else {
|
||||||
in.tunnelSrv = sudoku.NewHTTPMaskTunnelServer(&in.protoConf)
|
inbound.tunnelSrv = sudoku.NewHTTPMaskTunnelServer(&inbound.protoConf)
|
||||||
}
|
}
|
||||||
|
|
||||||
in.listener = listener.New(listener.Options{
|
inbound.listener = listener.New(listener.Options{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
Network: []string{N.NetworkTCP},
|
Network: []string{N.NetworkTCP},
|
||||||
Listen: options.ListenOptions,
|
Listen: options.ListenOptions,
|
||||||
ConnectionHandler: in,
|
ConnectionHandler: inbound,
|
||||||
})
|
})
|
||||||
return in, nil
|
return inbound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Inbound) Start(stage adapter.StartStage) error {
|
func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||||
@@ -173,6 +173,7 @@ func (h *Inbound) routeTCP(ctx context.Context, conn net.Conn, target string, me
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Inbound) handleUoT(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
func (h *Inbound) handleUoT(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||||
|
h.logger.InfoContext(ctx, "inbound packet connection")
|
||||||
packetConn := sudoku.NewUoTPacketConn(conn)
|
packetConn := sudoku.NewUoTPacketConn(conn)
|
||||||
h.router.RoutePacketConnectionEx(ctx, bufio.NewPacketConn(packetConn), metadata, onClose)
|
h.router.RoutePacketConnectionEx(ctx, bufio.NewPacketConn(packetConn), metadata, onClose)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ package sudoku
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/adapter/outbound"
|
"github.com/sagernet/sing-box/adapter/outbound"
|
||||||
@@ -15,7 +13,6 @@ import (
|
|||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-box/transport/sudoku"
|
"github.com/sagernet/sing-box/transport/sudoku"
|
||||||
"github.com/sagernet/sing-box/transport/sudoku/obfs/httpmask"
|
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
@@ -31,16 +28,8 @@ func RegisterOutbound(registry *outbound.Registry) {
|
|||||||
type Outbound struct {
|
type Outbound struct {
|
||||||
outbound.Adapter
|
outbound.Adapter
|
||||||
logger logger.ContextLogger
|
logger logger.ContextLogger
|
||||||
dialer N.Dialer
|
client *sudoku.Client
|
||||||
tlsConfig tls.Config
|
tlsConfig tls.Config
|
||||||
baseConf sudoku.ProtocolConfig
|
|
||||||
|
|
||||||
muxMu sync.Mutex
|
|
||||||
muxClient *sudoku.MultiplexClient
|
|
||||||
|
|
||||||
httpMaskMu sync.Mutex
|
|
||||||
httpMaskClient *httpmask.TunnelClient
|
|
||||||
httpMaskKey string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SudokuOutboundOptions) (adapter.Outbound, error) {
|
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SudokuOutboundOptions) (adapter.Outbound, error) {
|
||||||
@@ -105,33 +94,31 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
baseConf.Tables = tables
|
baseConf.Tables = tables
|
||||||
}
|
}
|
||||||
|
|
||||||
out := &Outbound{
|
var tlsConfig tls.Config
|
||||||
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSudoku, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
|
|
||||||
logger: logger,
|
|
||||||
dialer: outboundDialer,
|
|
||||||
baseConf: baseConf,
|
|
||||||
}
|
|
||||||
if hm := options.HTTPMask; !disableHTTPMask && hm != nil && hm.TLS != nil && hm.TLS.Enabled {
|
if hm := options.HTTPMask; !disableHTTPMask && hm != nil && hm.TLS != nil && hm.TLS.Enabled {
|
||||||
tlsOptions := option.OutboundTLSOptions{
|
tlsConfig, err = tls.NewClientWithOptions(tls.ClientOptions{
|
||||||
Enabled: true,
|
|
||||||
ServerName: options.Server,
|
|
||||||
Fragment: hm.TLS.Fragment,
|
|
||||||
FragmentFallbackDelay: hm.TLS.FragmentFallbackDelay,
|
|
||||||
RecordFragment: hm.TLS.RecordFragment,
|
|
||||||
KernelTx: hm.TLS.KernelTx,
|
|
||||||
KernelRx: hm.TLS.KernelRx,
|
|
||||||
}
|
|
||||||
out.tlsConfig, err = tls.NewClientWithOptions(tls.ClientOptions{
|
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
ServerAddress: options.Server,
|
ServerAddress: options.Server,
|
||||||
Options: tlsOptions,
|
Options: *hm.TLS,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out, nil
|
|
||||||
|
client := sudoku.NewClient(sudoku.ClientOptions{
|
||||||
|
Dialer: outboundDialer,
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
|
Config: baseConf,
|
||||||
|
})
|
||||||
|
|
||||||
|
return &Outbound{
|
||||||
|
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSudoku, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
|
||||||
|
logger: logger,
|
||||||
|
client: client,
|
||||||
|
tlsConfig: tlsConfig,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
@@ -144,35 +131,7 @@ func (h *Outbound) DialContext(ctx context.Context, network string, destination
|
|||||||
ctx, metadata := adapter.ExtendContext(ctx)
|
ctx, metadata := adapter.ExtendContext(ctx)
|
||||||
metadata.Outbound = h.Tag()
|
metadata.Outbound = h.Tag()
|
||||||
metadata.Destination = destination
|
metadata.Destination = destination
|
||||||
|
return h.client.DialContext(ctx, network, destination)
|
||||||
cfg := h.baseConf
|
|
||||||
cfg.TargetAddress = destination.String()
|
|
||||||
|
|
||||||
muxMode := normalizeHTTPMaskMultiplex(cfg.HTTPMaskMultiplex)
|
|
||||||
if muxMode == "on" && !cfg.DisableHTTPMask && httpTunnelModeEnabled(cfg.HTTPMaskMode) {
|
|
||||||
stream, err := h.dialMultiplex(ctx, cfg.TargetAddress)
|
|
||||||
if err == nil {
|
|
||||||
return stream, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := h.dialAndHandshake(ctx, &cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
addrBuf, err := sudoku.EncodeAddress(cfg.TargetAddress)
|
|
||||||
if err != nil {
|
|
||||||
c.Close()
|
|
||||||
return nil, E.Cause(err, "encode target address")
|
|
||||||
}
|
|
||||||
if err = sudoku.WriteKIPMessage(c, sudoku.KIPTypeOpenTCP, addrBuf); err != nil {
|
|
||||||
c.Close()
|
|
||||||
return nil, E.Cause(err, "send target address")
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
@@ -180,222 +139,18 @@ func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
|
|||||||
ctx, metadata := adapter.ExtendContext(ctx)
|
ctx, metadata := adapter.ExtendContext(ctx)
|
||||||
metadata.Outbound = h.Tag()
|
metadata.Outbound = h.Tag()
|
||||||
metadata.Destination = destination
|
metadata.Destination = destination
|
||||||
|
conn, err := h.client.DialContext(ctx, N.NetworkUDP, destination)
|
||||||
cfg := h.baseConf
|
|
||||||
cfg.TargetAddress = destination.String()
|
|
||||||
|
|
||||||
c, err := h.dialAndHandshake(ctx, &cfg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return bufio.NewBindPacketConn(sudoku.NewUoTPacketConn(conn), destination), nil
|
||||||
if err = sudoku.WriteKIPMessage(c, sudoku.KIPTypeStartUoT, nil); err != nil {
|
|
||||||
c.Close()
|
|
||||||
return nil, E.Cause(err, "start uot")
|
|
||||||
}
|
|
||||||
|
|
||||||
return bufio.NewBindPacketConn(sudoku.NewUoTPacketConn(c), destination), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Outbound) Close() error {
|
func (h *Outbound) Close() error {
|
||||||
h.resetMuxClient()
|
h.client.Close()
|
||||||
h.resetHTTPMaskClient()
|
|
||||||
return common.Close(h.tlsConfig)
|
return common.Close(h.tlsConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Outbound) InterfaceUpdated() {
|
func (h *Outbound) InterfaceUpdated() {
|
||||||
h.resetMuxClient()
|
h.client.Close()
|
||||||
h.resetHTTPMaskClient()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Outbound) dialAndHandshake(ctx context.Context, cfg *sudoku.ProtocolConfig) (net.Conn, error) {
|
|
||||||
handshakeCfg := *cfg
|
|
||||||
if !handshakeCfg.DisableHTTPMask && httpTunnelModeEnabled(handshakeCfg.HTTPMaskMode) {
|
|
||||||
handshakeCfg.DisableHTTPMask = true
|
|
||||||
}
|
|
||||||
|
|
||||||
upgrade := func(raw net.Conn) (net.Conn, error) {
|
|
||||||
return sudoku.ClientHandshake(raw, &handshakeCfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
var c net.Conn
|
|
||||||
var err error
|
|
||||||
var handshakeDone bool
|
|
||||||
|
|
||||||
if !cfg.DisableHTTPMask && httpTunnelModeEnabled(cfg.HTTPMaskMode) {
|
|
||||||
muxMode := normalizeHTTPMaskMultiplex(cfg.HTTPMaskMultiplex)
|
|
||||||
if muxMode == "auto" && strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) != "ws" {
|
|
||||||
if client, cerr := h.getOrCreateHTTPMaskClient(cfg); cerr == nil && client != nil {
|
|
||||||
c, err = client.DialTunnel(ctx, httpmask.TunnelDialOptions{
|
|
||||||
Mode: cfg.HTTPMaskMode,
|
|
||||||
TLSConfig: h.httpMaskTLSConfig(),
|
|
||||||
HostOverride: cfg.HTTPMaskHost,
|
|
||||||
PathRoot: cfg.HTTPMaskPathRoot,
|
|
||||||
AuthKey: sudoku.ClientAEADSeed(cfg.Key),
|
|
||||||
Upgrade: upgrade,
|
|
||||||
Multiplex: cfg.HTTPMaskMultiplex,
|
|
||||||
DialContext: h.dialRaw,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
h.resetHTTPMaskClient()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c == nil && err == nil {
|
|
||||||
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, h.dialRaw, upgrade)
|
|
||||||
}
|
|
||||||
if err == nil && c != nil {
|
|
||||||
handshakeDone = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c == nil && err == nil {
|
|
||||||
c, err = h.dialer.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddr(cfg.ServerAddress))
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "connect to ", cfg.ServerAddress)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !handshakeDone {
|
|
||||||
c, err = sudoku.ClientHandshake(c, &handshakeCfg)
|
|
||||||
if err != nil {
|
|
||||||
common.Close(c)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Outbound) dialRaw(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
return h.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Outbound) httpMaskTLSConfig() httpmask.TLSClientConfig {
|
|
||||||
if h.tlsConfig == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return tlsConfigAdapter{h.tlsConfig}
|
|
||||||
}
|
|
||||||
|
|
||||||
type tlsConfigAdapter struct {
|
|
||||||
config tls.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a tlsConfigAdapter) Client(conn net.Conn) (net.Conn, error) {
|
|
||||||
return a.config.Client(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Outbound) dialMultiplex(ctx context.Context, targetAddress string) (net.Conn, error) {
|
|
||||||
for attempt := 0; attempt < 2; attempt++ {
|
|
||||||
client, err := h.getOrCreateMuxClient(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
stream, err := client.Dial(ctx, targetAddress)
|
|
||||||
if err != nil {
|
|
||||||
h.resetMuxClient()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return stream, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("multiplex open stream failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Outbound) getOrCreateMuxClient(ctx context.Context) (*sudoku.MultiplexClient, error) {
|
|
||||||
h.muxMu.Lock()
|
|
||||||
defer h.muxMu.Unlock()
|
|
||||||
|
|
||||||
if h.muxClient != nil && !h.muxClient.IsClosed() {
|
|
||||||
return h.muxClient, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
baseCfg := h.baseConf
|
|
||||||
baseConn, err := h.dialAndHandshake(ctx, &baseCfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := sudoku.StartMultiplexClient(baseConn)
|
|
||||||
if err != nil {
|
|
||||||
baseConn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
h.muxClient = client
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Outbound) resetMuxClient() {
|
|
||||||
h.muxMu.Lock()
|
|
||||||
defer h.muxMu.Unlock()
|
|
||||||
if h.muxClient != nil {
|
|
||||||
h.muxClient.Close()
|
|
||||||
h.muxClient = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Outbound) getOrCreateHTTPMaskClient(cfg *sudoku.ProtocolConfig) (*httpmask.TunnelClient, error) {
|
|
||||||
key := cfg.ServerAddress + "|" + fmt.Sprint(h.tlsConfig != nil) + "|" + strings.TrimSpace(cfg.HTTPMaskHost)
|
|
||||||
|
|
||||||
h.httpMaskMu.Lock()
|
|
||||||
if h.httpMaskClient != nil && h.httpMaskKey == key {
|
|
||||||
client := h.httpMaskClient
|
|
||||||
h.httpMaskMu.Unlock()
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
h.httpMaskMu.Unlock()
|
|
||||||
|
|
||||||
client, err := httpmask.NewTunnelClient(cfg.ServerAddress, httpmask.TunnelClientOptions{
|
|
||||||
TLSConfig: h.httpMaskTLSConfig(),
|
|
||||||
HostOverride: cfg.HTTPMaskHost,
|
|
||||||
DialContext: h.dialRaw,
|
|
||||||
MaxIdleConns: 32,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
h.httpMaskMu.Lock()
|
|
||||||
defer h.httpMaskMu.Unlock()
|
|
||||||
if h.httpMaskClient != nil && h.httpMaskKey == key {
|
|
||||||
client.CloseIdleConnections()
|
|
||||||
return h.httpMaskClient, nil
|
|
||||||
}
|
|
||||||
if h.httpMaskClient != nil {
|
|
||||||
h.httpMaskClient.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
h.httpMaskClient = client
|
|
||||||
h.httpMaskKey = key
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Outbound) resetHTTPMaskClient() {
|
|
||||||
h.httpMaskMu.Lock()
|
|
||||||
defer h.httpMaskMu.Unlock()
|
|
||||||
if h.httpMaskClient != nil {
|
|
||||||
h.httpMaskClient.CloseIdleConnections()
|
|
||||||
h.httpMaskClient = nil
|
|
||||||
h.httpMaskKey = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeHTTPMaskMultiplex(mode string) string {
|
|
||||||
switch strings.ToLower(strings.TrimSpace(mode)) {
|
|
||||||
case "", "off":
|
|
||||||
return "off"
|
|
||||||
case "auto":
|
|
||||||
return "auto"
|
|
||||||
case "on":
|
|
||||||
return "on"
|
|
||||||
default:
|
|
||||||
return "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func httpTunnelModeEnabled(mode string) bool {
|
|
||||||
switch strings.ToLower(strings.TrimSpace(mode)) {
|
|
||||||
case "stream", "poll", "auto", "ws":
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,10 +151,10 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(defaultResolvers) > 0 {
|
if len(defaultResolvers) > 0 {
|
||||||
t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts, default resolvers: ",
|
t.logger.Notice("updated ", len(routes), " routes, ", len(hosts), " hosts, default resolvers: ",
|
||||||
strings.Join(common.Map(dnsConfig.DefaultResolvers, func(it *dnstype.Resolver) string { return it.Addr }), " "))
|
strings.Join(common.Map(dnsConfig.DefaultResolvers, func(it *dnstype.Resolver) string { return it.Addr }), " "))
|
||||||
} else {
|
} else {
|
||||||
t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts")
|
t.logger.Notice("updated ", len(routes), " routes, ", len(hosts), " hosts")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ func (t *Endpoint) watchState() {
|
|||||||
}
|
}
|
||||||
authURL := localBackend.StatusWithoutPeers().AuthURL
|
authURL := localBackend.StatusWithoutPeers().AuthURL
|
||||||
if authURL != "" {
|
if authURL != "" {
|
||||||
t.logger.Info("Waiting for authentication: ", authURL)
|
t.logger.Notice("Waiting for authentication: ", authURL)
|
||||||
if t.platformInterface != nil {
|
if t.platformInterface != nil {
|
||||||
err := t.platformInterface.SendNotification(&adapter.Notification{
|
err := t.platformInterface.SendNotification(&adapter.Notification{
|
||||||
Identifier: "tailscale-authentication",
|
Identifier: "tailscale-authentication",
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/sagernet/quic-go"
|
||||||
|
"github.com/sagernet/quic-go/http3"
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/adapter/inbound"
|
"github.com/sagernet/sing-box/adapter/inbound"
|
||||||
"github.com/sagernet/sing-box/common/listener"
|
"github.com/sagernet/sing-box/common/listener"
|
||||||
@@ -14,11 +16,13 @@ import (
|
|||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-box/transport/trusttunnel"
|
"github.com/sagernet/sing-box/transport/trusttunnel"
|
||||||
|
"github.com/sagernet/sing-quic"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/auth"
|
"github.com/sagernet/sing/common/auth"
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
aTLS "github.com/sagernet/sing/common/tls"
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
|
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
@@ -31,36 +35,34 @@ func RegisterInbound(registry *inbound.Registry) {
|
|||||||
|
|
||||||
type Inbound struct {
|
type Inbound struct {
|
||||||
inbound.Adapter
|
inbound.Adapter
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
router adapter.Router
|
router adapter.Router
|
||||||
logger logger.ContextLogger
|
logger logger.ContextLogger
|
||||||
listener *listener.Listener
|
options option.TrustTunnelInboundOptions
|
||||||
tlsConfig tls.ServerConfig
|
listener *listener.Listener
|
||||||
service *trusttunnel.Service
|
service *trusttunnel.Service
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
quicService *trusttunnel.QUICService
|
http3Server *http3.Server
|
||||||
network []string
|
httpTLSConfig tls.ServerConfig
|
||||||
|
http3TLSConfig tls.ServerConfig
|
||||||
|
network []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrustTunnelInboundOptions) (adapter.Inbound, error) {
|
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrustTunnelInboundOptions) (adapter.Inbound, error) {
|
||||||
if options.TLS == nil || !options.TLS.Enabled {
|
if options.TLS == nil || !options.TLS.Enabled {
|
||||||
return nil, C.ErrTLSRequired
|
return nil, C.ErrTLSRequired
|
||||||
}
|
}
|
||||||
tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
networkList := options.Network.Build()
|
networkList := options.Network.Build()
|
||||||
if len(networkList) == 0 {
|
if len(networkList) == 0 {
|
||||||
networkList = []string{N.NetworkTCP}
|
networkList = []string{N.NetworkTCP}
|
||||||
}
|
}
|
||||||
inbound := &Inbound{
|
inbound := &Inbound{
|
||||||
Adapter: inbound.NewAdapter(C.TypeTrustTunnel, tag),
|
Adapter: inbound.NewAdapter(C.TypeTrustTunnel, tag),
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
router: router,
|
router: router,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
tlsConfig: tlsConfig,
|
options: options,
|
||||||
network: networkList,
|
network: networkList,
|
||||||
listener: listener.New(listener.Options{
|
listener: listener.New(listener.Options{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
@@ -78,9 +80,6 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||||||
}
|
}
|
||||||
service.UpdateUsers(userMap)
|
service.UpdateUsers(userMap)
|
||||||
inbound.service = service
|
inbound.service = service
|
||||||
if common.Contains(networkList, N.NetworkUDP) {
|
|
||||||
inbound.quicService = trusttunnel.NewQUICService(service, options.CongestionController, options.CWND, options.BBRProfile)
|
|
||||||
}
|
|
||||||
return inbound, nil
|
return inbound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,14 +87,18 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
|||||||
if stage != adapter.StartStateStart {
|
if stage != adapter.StartStateStart {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if h.tlsConfig != nil {
|
var err error
|
||||||
err := h.tlsConfig.Start()
|
if common.Contains(h.network, N.NetworkTCP) {
|
||||||
|
h.httpTLSConfig, err = tls.NewServer(h.ctx, h.logger, common.PtrValueOrDefault(h.options.TLS))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
if len(h.httpTLSConfig.NextProtos()) == 0 {
|
||||||
if common.Contains(h.network, N.NetworkTCP) {
|
h.httpTLSConfig.SetNextProtos([]string{http2.NextProtoTLS})
|
||||||
tcpListener, err := h.listener.ListenTCP()
|
} else if !common.Contains(h.httpTLSConfig.NextProtos(), http2.NextProtoTLS) {
|
||||||
|
h.httpTLSConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, h.httpTLSConfig.NextProtos()...))
|
||||||
|
}
|
||||||
|
listener, err := h.listener.ListenTCP()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -105,31 +108,61 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
|||||||
return h.ctx
|
return h.ctx
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
err = h.httpTLSConfig.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
listener = aTLS.NewListener(listener, h.httpTLSConfig)
|
||||||
go func() {
|
go func() {
|
||||||
var l net.Listener = tcpListener
|
sErr := h.httpServer.Serve(listener)
|
||||||
if h.tlsConfig != nil {
|
|
||||||
if len(h.tlsConfig.NextProtos()) == 0 {
|
|
||||||
h.tlsConfig.SetNextProtos([]string{http2.NextProtoTLS})
|
|
||||||
} else if !common.Contains(h.tlsConfig.NextProtos(), http2.NextProtoTLS) {
|
|
||||||
h.tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, h.tlsConfig.NextProtos()...))
|
|
||||||
}
|
|
||||||
l = aTLS.NewListener(tcpListener, h.tlsConfig)
|
|
||||||
}
|
|
||||||
sErr := h.httpServer.Serve(l)
|
|
||||||
if sErr != nil && !errors.Is(sErr, http.ErrServerClosed) {
|
if sErr != nil && !errors.Is(sErr, http.ErrServerClosed) {
|
||||||
h.logger.Error("HTTP server error: ", sErr)
|
h.logger.Error("HTTP server error: ", sErr)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
if common.Contains(h.network, N.NetworkUDP) {
|
if common.Contains(h.network, N.NetworkUDP) {
|
||||||
|
h.http3TLSConfig, err = tls.NewServer(h.ctx, h.logger, common.PtrValueOrDefault(h.options.TLS))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := qtls.ConfigureHTTP3(h.http3TLSConfig); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = h.http3TLSConfig.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
udpConn, err := h.listener.ListenUDP()
|
udpConn, err := h.listener.ListenUDP()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = h.quicService.Start(h.ctx, udpConn, h.tlsConfig)
|
congestionControlFactory, err := trusttunnel.NewCongestionControl(
|
||||||
|
h.options.CongestionController,
|
||||||
|
h.options.CWND,
|
||||||
|
h.options.BBRProfile,
|
||||||
|
ntp.TimeFuncFromContext(h.ctx),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
h.http3Server = &http3.Server{
|
||||||
|
Handler: h.service,
|
||||||
|
ConnContext: func(ctx context.Context, conn *quic.Conn) context.Context {
|
||||||
|
conn.SetCongestionControl(congestionControlFactory(conn))
|
||||||
|
return ctx
|
||||||
|
},
|
||||||
|
}
|
||||||
|
quicListener, err := qtls.ListenEarly(udpConn, h.http3TLSConfig, &quic.Config{
|
||||||
|
MaxIdleTimeout: trusttunnel.DefaultSessionTimeout * 2,
|
||||||
|
MaxIncomingStreams: 1 << 60,
|
||||||
|
Allow0RTT: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
_ = h.http3Server.ServeListener(quicListener)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -138,8 +171,9 @@ func (h *Inbound) Close() error {
|
|||||||
return common.Close(
|
return common.Close(
|
||||||
h.listener,
|
h.listener,
|
||||||
common.PtrOrNil(h.httpServer),
|
common.PtrOrNil(h.httpServer),
|
||||||
h.quicService,
|
common.PtrOrNil(h.http3Server),
|
||||||
h.tlsConfig,
|
h.httpTLSConfig,
|
||||||
|
h.http3TLSConfig,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ import (
|
|||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
"golang.org/x/net/http2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterOutbound(registry *outbound.Registry) {
|
func RegisterOutbound(registry *outbound.Registry) {
|
||||||
@@ -42,7 +40,13 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
}
|
}
|
||||||
serverAddr := options.ServerOptions.Build()
|
serverAddr := options.ServerOptions.Build()
|
||||||
networkList := options.Network.Build()
|
networkList := options.Network.Build()
|
||||||
|
tlsConfig, err := tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
clientOpts := trusttunnel.ClientOptions{
|
clientOpts := trusttunnel.ClientOptions{
|
||||||
|
Dialer: outboundDialer,
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
Server: serverAddr,
|
Server: serverAddr,
|
||||||
Username: options.Username,
|
Username: options.Username,
|
||||||
Password: options.Password,
|
Password: options.Password,
|
||||||
@@ -52,26 +56,6 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
BBRProfile: options.BBRProfile,
|
BBRProfile: options.BBRProfile,
|
||||||
HealthCheck: options.HealthCheck,
|
HealthCheck: options.HealthCheck,
|
||||||
}
|
}
|
||||||
if options.QUIC {
|
|
||||||
tlsConfig, err := tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(tlsConfig.NextProtos()) == 0 {
|
|
||||||
tlsConfig.SetNextProtos([]string{"h3"})
|
|
||||||
}
|
|
||||||
clientOpts.QUICDialer = outboundDialer
|
|
||||||
clientOpts.QUICTLSConfig = tlsConfig
|
|
||||||
} else {
|
|
||||||
tlsConfig, err := tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(tlsConfig.NextProtos()) == 0 {
|
|
||||||
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS})
|
|
||||||
}
|
|
||||||
clientOpts.TLSDialer = tls.NewDialer(outboundDialer, tlsConfig)
|
|
||||||
}
|
|
||||||
var client trusttunnel.Dialer
|
var client trusttunnel.Dialer
|
||||||
if options.Multiplex != nil && options.Multiplex.Enabled {
|
if options.Multiplex != nil && options.Multiplex.Enabled {
|
||||||
clientOpts.MaxConnections = options.Multiplex.MaxConnections
|
clientOpts.MaxConnections = options.Multiplex.MaxConnections
|
||||||
|
|||||||
@@ -388,7 +388,7 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
t.tunStack = tunStack
|
t.tunStack = tunStack
|
||||||
t.logger.Info("started at ", t.tunOptions.Name)
|
t.logger.Notice("started at ", t.tunOptions.Name)
|
||||||
case adapter.StartStatePostStart:
|
case adapter.StartStatePostStart:
|
||||||
monitor := taskmonitor.New(t.logger, C.StartTimeout)
|
monitor := taskmonitor.New(t.logger, C.StartTimeout)
|
||||||
monitor.Start("starting tun stack")
|
monitor.Start("starting tun stack")
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
stdtls "crypto/tls"
|
stdtls "crypto/tls"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -96,7 +95,10 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
return nil, E.New("unknown packet encoding: ", options.PacketEncoding)
|
return nil, E.New("unknown packet encoding: ", options.PacketEncoding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Parse encryption configuration
|
muxOpts := common.PtrValueOrDefault(options.Multiplex)
|
||||||
|
if muxOpts.Enabled {
|
||||||
|
options.Flow = ""
|
||||||
|
}
|
||||||
if options.Encryption != "" && options.Encryption != "none" {
|
if options.Encryption != "" && options.Encryption != "none" {
|
||||||
encryptionConfig, err := parseClientEncryption(options.Encryption)
|
encryptionConfig, err := parseClientEncryption(options.Encryption)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -109,10 +111,6 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
logger.Debug("encryption initialized: keys=", len(encryptionConfig.keys), " xorMode=", encryptionConfig.xorMode, " seconds=", encryptionConfig.seconds, " padding=", encryptionConfig.padding)
|
logger.Debug("encryption initialized: keys=", len(encryptionConfig.keys), " xorMode=", encryptionConfig.xorMode, " seconds=", encryptionConfig.seconds, " padding=", encryptionConfig.padding)
|
||||||
}
|
}
|
||||||
|
|
||||||
muxOpts := common.PtrValueOrDefault(options.Multiplex)
|
|
||||||
if muxOpts.Enabled {
|
|
||||||
options.Flow = ""
|
|
||||||
}
|
|
||||||
outbound.client, err = vless.NewClient(options.UUID, options.Flow, logger)
|
outbound.client, err = vless.NewClient(options.UUID, options.Flow, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -191,7 +189,6 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
|
|||||||
conn, err = h.transport.DialContext(ctx)
|
conn, err = h.transport.DialContext(ctx)
|
||||||
if err == nil && h.vision {
|
if err == nil && h.vision {
|
||||||
if baseConn == nil {
|
if baseConn == nil {
|
||||||
// Only set baseConn if the transport delivered a TLS-capable connection
|
|
||||||
if isVisionTLSConn(conn) {
|
if isVisionTLSConn(conn) {
|
||||||
h.logger.Warn("Vision enabled but hook was not called by transport, using fallback")
|
h.logger.Warn("Vision enabled but hook was not called by transport, using fallback")
|
||||||
baseConn = conn
|
baseConn = conn
|
||||||
@@ -210,7 +207,6 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply encryption if configured
|
|
||||||
if h.encryption != nil {
|
if h.encryption != nil {
|
||||||
conn, err = h.encryption.Handshake(conn)
|
conn, err = h.encryption.Handshake(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -218,36 +214,12 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For Vision: wrap the connection to expose the TLS/encryption connection for vless client
|
var visionBaseConn net.Conn
|
||||||
var visionBaseConn net.Conn // The connection to pass to Vision (TLS or encryption layer)
|
|
||||||
var visionCanSplice bool
|
var visionCanSplice bool
|
||||||
if h.vision {
|
if h.vision {
|
||||||
isRAWTransport := h.transport == nil
|
conn, visionBaseConn, visionCanSplice, err = h.setupVision(conn, baseConn)
|
||||||
|
if err != nil {
|
||||||
if baseConn != nil && !isVisionTLSConn(baseConn) {
|
return nil, err
|
||||||
baseConn = nil
|
|
||||||
}
|
|
||||||
if baseConn != nil {
|
|
||||||
// Has TLS/Reality: use baseConn (TLS connection)
|
|
||||||
visionBaseConn = baseConn
|
|
||||||
visionCanSplice = isRAWTransport
|
|
||||||
conn = newVisionConnWrapper(conn, baseConn)
|
|
||||||
} else if h.encryption != nil {
|
|
||||||
// Only has encryption (no TLS/Reality): use encryption layer itself
|
|
||||||
encConn := findEncryptionLayer(conn)
|
|
||||||
if encConn != nil {
|
|
||||||
visionBaseConn = encConn
|
|
||||||
if h.encryption.IsFullRandomXorMode() {
|
|
||||||
visionCanSplice = false
|
|
||||||
} else {
|
|
||||||
visionCanSplice = isRAWTransport
|
|
||||||
}
|
|
||||||
conn = newVisionConnWrapper(conn, encConn)
|
|
||||||
} else {
|
|
||||||
return nil, E.New("Vision: failed to find encryption layer")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, E.New("Vision requires either TLS/Reality or Encryption")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,8 +227,6 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
|
|||||||
case N.NetworkTCP:
|
case N.NetworkTCP:
|
||||||
h.logger.InfoContext(ctx, "outbound connection to ", destination)
|
h.logger.InfoContext(ctx, "outbound connection to ", destination)
|
||||||
if h.vision && visionBaseConn != nil {
|
if h.vision && visionBaseConn != nil {
|
||||||
// For Vision, we need to pass the base connection (TLS or encryption layer)
|
|
||||||
// to prepareConn so it can properly initialize VisionConn
|
|
||||||
return h.client.DialEarlyConnWithOptions(conn, visionBaseConn, destination, visionCanSplice)
|
return h.client.DialEarlyConnWithOptions(conn, visionBaseConn, destination, visionCanSplice)
|
||||||
}
|
}
|
||||||
return h.client.DialEarlyConn(conn, destination)
|
return h.client.DialEarlyConn(conn, destination)
|
||||||
@@ -281,6 +251,29 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *vlessDialer) setupVision(conn net.Conn, baseConn net.Conn) (net.Conn, net.Conn, bool, error) {
|
||||||
|
isRAWTransport := h.transport == nil
|
||||||
|
|
||||||
|
if baseConn != nil && !isVisionTLSConn(baseConn) {
|
||||||
|
baseConn = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if baseConn != nil {
|
||||||
|
return newVisionConnWrapper(conn, baseConn), baseConn, isRAWTransport, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.encryption != nil {
|
||||||
|
encConn := findEncryptionLayer(conn)
|
||||||
|
if encConn == nil {
|
||||||
|
return nil, nil, false, E.New("Vision: failed to find encryption layer")
|
||||||
|
}
|
||||||
|
canSplice := isRAWTransport && !h.encryption.IsFullRandomXorMode()
|
||||||
|
return newVisionConnWrapper(conn, encConn), encConn, canSplice, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, false, E.New("Vision requires either TLS/Reality or Encryption")
|
||||||
|
}
|
||||||
|
|
||||||
func (h *vlessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
func (h *vlessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||||||
ctx, metadata := adapter.ExtendContext(ctx)
|
ctx, metadata := adapter.ExtendContext(ctx)
|
||||||
@@ -299,7 +292,6 @@ func (h *vlessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr)
|
|||||||
common.Close(conn)
|
common.Close(conn)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Apply encryption if configured
|
|
||||||
if h.encryption != nil {
|
if h.encryption != nil {
|
||||||
conn, err = h.encryption.Handshake(conn)
|
conn, err = h.encryption.Handshake(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -362,7 +354,6 @@ func (c *visionConnWrapper) WriterReplaceable() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// isVisionTLSConn returns true when the provided connection exposes TLS semantics Vision expects.
|
|
||||||
func isVisionTLSConn(conn net.Conn) bool {
|
func isVisionTLSConn(conn net.Conn) bool {
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
return false
|
return false
|
||||||
@@ -373,16 +364,6 @@ func isVisionTLSConn(conn net.Conn) bool {
|
|||||||
if _, ok := conn.(interface{ Handshake() error }); ok {
|
if _, ok := conn.(interface{ Handshake() error }); ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
connType := reflect.TypeOf(conn)
|
|
||||||
if connType == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if connType.Kind() == reflect.Ptr {
|
|
||||||
pkgPath := connType.Elem().PkgPath()
|
|
||||||
if pkgPath == "crypto/tls" || strings.Contains(pkgPath, "utls") || strings.Contains(pkgPath, "shadowtls") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ func (s *ProviderRemote) fetch(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.logger.Info("update outbound provider ", s.Tag(), ": not modified")
|
s.logger.Notice("update outbound provider ", s.Tag(), ": not modified")
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return E.New("unexpected status: ", resp.Status)
|
return E.New("unexpected status: ", resp.Status)
|
||||||
@@ -262,7 +262,7 @@ func (s *ProviderRemote) fetch(ctx context.Context) error {
|
|||||||
s.logger.Error("save outbound provider cache file: ", err)
|
s.logger.Error("save outbound provider cache file: ", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.logger.Info("updated outbound provider ", s.Tag())
|
s.logger.Notice("updated outbound provider ", s.Tag())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ func (m *ConnectionManager) connectionCopy(ctx context.Context, source net.Conn,
|
|||||||
if !direction {
|
if !direction {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
m.logger.DebugContext(ctx, "connection upload finished")
|
m.logger.DebugContext(ctx, "connection upload finished")
|
||||||
} else if !E.IsClosedOrCanceled(err) && !strings.Contains(err.Error(), "NO_ERROR") {
|
} else if !E.IsClosedOrCanceled(err) && !strings.Contains(err.Error(), "NO_ERROR") && !strings.Contains(err.Error(), "CANCEL") && !strings.Contains(err.Error(), "body closed") {
|
||||||
m.logger.ErrorContext(ctx, "connection upload closed: ", err)
|
m.logger.ErrorContext(ctx, "connection upload closed: ", err)
|
||||||
} else {
|
} else {
|
||||||
m.logger.TraceContext(ctx, "connection upload closed")
|
m.logger.TraceContext(ctx, "connection upload closed")
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ func (r *NetworkManager) UpdateInterfaces() error {
|
|||||||
oldInterface.Expensive == newInterface.Expensive &&
|
oldInterface.Expensive == newInterface.Expensive &&
|
||||||
oldInterface.Constrained == newInterface.Constrained
|
oldInterface.Constrained == newInterface.Constrained
|
||||||
}) {
|
}) {
|
||||||
r.logger.Info("updated available networks: ", strings.Join(common.Map(newInterfaces, func(it adapter.NetworkInterface) string {
|
r.logger.Notice("updated available networks: ", strings.Join(common.Map(newInterfaces, func(it adapter.NetworkInterface) string {
|
||||||
var options []string
|
var options []string
|
||||||
options = append(options, F.ToString(it.Type))
|
options = append(options, F.ToString(it.Type))
|
||||||
if it.Expensive {
|
if it.Expensive {
|
||||||
@@ -430,9 +430,9 @@ func (r *NetworkManager) onWIFIStateChanged(state adapter.WIFIState) {
|
|||||||
r.wifiState = state
|
r.wifiState = state
|
||||||
r.wifiStateMutex.Unlock()
|
r.wifiStateMutex.Unlock()
|
||||||
if state.SSID != "" {
|
if state.SSID != "" {
|
||||||
r.logger.Info("WIFI state changed: SSID=", state.SSID, ", BSSID=", state.BSSID)
|
r.logger.Notice("WIFI state changed: SSID=", state.SSID, ", BSSID=", state.BSSID)
|
||||||
} else {
|
} else {
|
||||||
r.logger.Info("WIFI disconnected")
|
r.logger.Notice("WIFI disconnected")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
r.wifiStateMutex.Unlock()
|
r.wifiStateMutex.Unlock()
|
||||||
@@ -512,7 +512,7 @@ func (r *NetworkManager) notifyInterfaceUpdate(defaultInterface *control.Interfa
|
|||||||
options = append(options, "constrained")
|
options = append(options, "constrained")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r.logger.Info("updated default interface ", defaultInterface.Name, ", ", strings.Join(options, ", "))
|
r.logger.Notice("updated default interface ", defaultInterface.Name, ", ", strings.Join(options, ", "))
|
||||||
r.UpdateWIFIState()
|
r.UpdateWIFIState()
|
||||||
|
|
||||||
if !r.started {
|
if !r.started {
|
||||||
@@ -538,5 +538,5 @@ func (r *NetworkManager) notifyWindowsPowerEvent(event int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *NetworkManager) OnPackagesUpdated(packages int, sharedUsers int) {
|
func (r *NetworkManager) OnPackagesUpdated(packages int, sharedUsers int) {
|
||||||
r.logger.Info("updated packages list: ", packages, " packages, ", sharedUsers, " shared users")
|
r.logger.Notice("updated packages list: ", packages, " packages, ", sharedUsers, " shared users")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ func (s *RemoteRuleSet) fetch(ctx context.Context, startContext *adapter.HTTPSta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.logger.Info("update rule-set ", s.options.Tag, ": not modified")
|
s.logger.Notice("update rule-set ", s.options.Tag, ": not modified")
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return E.New("unexpected status: ", response.Status)
|
return E.New("unexpected status: ", response.Status)
|
||||||
@@ -308,7 +308,7 @@ func (s *RemoteRuleSet) fetch(ctx context.Context, startContext *adapter.HTTPSta
|
|||||||
s.logger.Error("save rule-set cache: ", err)
|
s.logger.Error("save rule-set cache: ", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.logger.Info("updated rule-set ", s.options.Tag)
|
s.logger.Notice("updated rule-set ", s.options.Tag)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export type UserType =
|
|||||||
| "mtproxy"
|
| "mtproxy"
|
||||||
| "naive"
|
| "naive"
|
||||||
| "socks"
|
| "socks"
|
||||||
|
| "ssh"
|
||||||
| "trojan"
|
| "trojan"
|
||||||
| "trusttunnel"
|
| "trusttunnel"
|
||||||
| "tuic"
|
| "tuic"
|
||||||
@@ -57,6 +58,7 @@ export interface User {
|
|||||||
uuid: string;
|
uuid: string;
|
||||||
password: string;
|
password: string;
|
||||||
secret: string;
|
secret: string;
|
||||||
|
authorized_keys: string[];
|
||||||
flow: string;
|
flow: string;
|
||||||
alter_id: number;
|
alter_id: number;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
@@ -70,6 +72,7 @@ export interface UserCreate {
|
|||||||
uuid?: string;
|
uuid?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
secret?: string;
|
secret?: string;
|
||||||
|
authorized_keys?: string[];
|
||||||
flow?: string;
|
flow?: string;
|
||||||
alter_id?: number;
|
alter_id?: number;
|
||||||
}
|
}
|
||||||
@@ -77,6 +80,7 @@ export interface UserUpdate {
|
|||||||
uuid?: string;
|
uuid?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
secret?: string;
|
secret?: string;
|
||||||
|
authorized_keys?: string[];
|
||||||
flow?: string;
|
flow?: string;
|
||||||
alter_id?: number;
|
alter_id?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ import type { Listable } from "../api/types";
|
|||||||
import { notifyApiError, useNotify } from "../notifications/NotificationsProvider";
|
import { notifyApiError, useNotify } from "../notifications/NotificationsProvider";
|
||||||
import { PageHeader } from "./PageHeader";
|
import { PageHeader } from "./PageHeader";
|
||||||
|
|
||||||
export type FieldType = "text" | "number" | "select" | "multiselect" | "ids" | "uuid";
|
export type FieldType = "text" | "number" | "select" | "multiselect" | "ids" | "uuid" | "string-list";
|
||||||
|
|
||||||
// FILTER_WIDTH is the fixed CSS width (px) of a single filter cell in the
|
// FILTER_WIDTH is the fixed CSS width (px) of a single filter cell in the
|
||||||
// filter panel. FILTER_GAP is the flex gap between cells (matches the
|
// filter panel. FILTER_GAP is the flex gap between cells (matches the
|
||||||
@@ -126,7 +126,7 @@ export interface FieldSpec<TValue = unknown> {
|
|||||||
// spec, matching what `emptyForm` would produce.
|
// spec, matching what `emptyForm` would produce.
|
||||||
function emptyValueForField(f: FieldSpec): unknown {
|
function emptyValueForField(f: FieldSpec): unknown {
|
||||||
if (f.defaultValue !== undefined) return f.defaultValue;
|
if (f.defaultValue !== undefined) return f.defaultValue;
|
||||||
if (f.type === "multiselect") return [];
|
if (f.type === "multiselect" || f.type === "string-list") return [];
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,7 +575,7 @@ function fieldVisible(
|
|||||||
// strings, missing selections, and empty arrays for multi-select / ids.
|
// strings, missing selections, and empty arrays for multi-select / ids.
|
||||||
function isFieldEmpty(f: FieldSpec, value: unknown): boolean {
|
function isFieldEmpty(f: FieldSpec, value: unknown): boolean {
|
||||||
if (value === undefined || value === null) return true;
|
if (value === undefined || value === null) return true;
|
||||||
if (f.type === "multiselect" || f.type === "ids") {
|
if (f.type === "multiselect" || f.type === "ids" || f.type === "string-list") {
|
||||||
if (Array.isArray(value)) return value.length === 0;
|
if (Array.isArray(value)) return value.length === 0;
|
||||||
if (typeof value === "string") return value.trim() === "";
|
if (typeof value === "string") return value.trim() === "";
|
||||||
return true;
|
return true;
|
||||||
@@ -4764,6 +4764,40 @@ function CrudDialog({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (f.type === "string-list") {
|
||||||
|
const arr = Array.isArray(value) ? (value as string[]) : [];
|
||||||
|
return (
|
||||||
|
<Box key={f.name} sx={{ mt: -1 }}>
|
||||||
|
<Typography variant="caption" color={errored ? "error" : "textSecondary"} sx={{ mb: 0.5, ml: "14px", display: "block" }}>
|
||||||
|
{f.label}{f.required ? " *" : ""}
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
{arr.map((item, idx) => (
|
||||||
|
<Stack key={idx} direction="row" spacing={1} alignItems="center">
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
value={item}
|
||||||
|
placeholder={`Key ${idx + 1}`}
|
||||||
|
onChange={(e) => {
|
||||||
|
const next = [...arr];
|
||||||
|
next[idx] = e.target.value;
|
||||||
|
set(f.name, next);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton size="small" color="error" onClick={() => set(f.name, arr.filter((_, i) => i !== idx))}>
|
||||||
|
<DeleteIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
<Button size="small" startIcon={<AddIcon />} sx={{ mt: 1 }} onClick={() => set(f.name, [...arr, ""])}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
{fieldErr && <Typography variant="caption" color="error" sx={{ mt: 0.5, display: "block" }}>{fieldErr}</Typography>}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
const isNumber = f.type === "number";
|
const isNumber = f.type === "number";
|
||||||
// Numeric value of the current cell. Falls back to 0 for the
|
// Numeric value of the current cell. Falls back to 0 for the
|
||||||
// empty state so the up-arrow always has a sensible base to
|
// empty state so the up-arrow always has a sensible base to
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const USER_TYPES: { value: UserType; label: string }[] = [
|
|||||||
{ value: "mtproxy", label: "MTProxy" },
|
{ value: "mtproxy", label: "MTProxy" },
|
||||||
{ value: "naive", label: "Naive" },
|
{ value: "naive", label: "Naive" },
|
||||||
{ value: "socks", label: "SOCKS" },
|
{ value: "socks", label: "SOCKS" },
|
||||||
|
{ value: "ssh", label: "SSH" },
|
||||||
{ value: "trojan", label: "Trojan" },
|
{ value: "trojan", label: "Trojan" },
|
||||||
{ value: "trusttunnel", label: "TrustTunnel" },
|
{ value: "trusttunnel", label: "TrustTunnel" },
|
||||||
{ value: "tuic", label: "TUIC" },
|
{ value: "tuic", label: "TUIC" },
|
||||||
@@ -44,8 +45,9 @@ const FLOW_OPTIONS: { value: string; label: string }[] = [
|
|||||||
// same rule up-front (required fields invisible for the current type
|
// same rule up-front (required fields invisible for the current type
|
||||||
// are filtered out before validateRequired runs).
|
// are filtered out before validateRequired runs).
|
||||||
const SHOW_UUID = new Set<UserType>(["vless", "vmess", "tuic"]);
|
const SHOW_UUID = new Set<UserType>(["vless", "vmess", "tuic"]);
|
||||||
const SHOW_PASSWORD = new Set<UserType>(["anytls", "http", "hysteria", "hysteria2", "mixed", "naive", "socks", "trojan", "trusttunnel", "tuic"]);
|
const SHOW_PASSWORD = new Set<UserType>(["anytls", "http", "hysteria", "hysteria2", "mixed", "naive", "socks", "ssh", "trojan", "trusttunnel", "tuic"]);
|
||||||
const SHOW_SECRET = new Set<UserType>(["mtproxy"]);
|
const SHOW_SECRET = new Set<UserType>(["mtproxy"]);
|
||||||
|
const SHOW_AUTHORIZED_KEYS = new Set<UserType>(["ssh"]);
|
||||||
const SHOW_FLOW = new Set<UserType>(["vless"]);
|
const SHOW_FLOW = new Set<UserType>(["vless"]);
|
||||||
const SHOW_ALTER_ID = new Set<UserType>(["vmess"]);
|
const SHOW_ALTER_ID = new Set<UserType>(["vmess"]);
|
||||||
|
|
||||||
@@ -103,7 +105,7 @@ export function UsersPage() {
|
|||||||
options: USER_TYPES,
|
options: USER_TYPES,
|
||||||
// Switching the user type wipes every credential field so the form
|
// Switching the user type wipes every credential field so the form
|
||||||
// matches the legacy admin's behaviour of starting fresh.
|
// matches the legacy admin's behaviour of starting fresh.
|
||||||
clears: ["uuid", "password", "secret", "flow", "alter_id"],
|
clears: ["uuid", "password", "secret", "authorized_keys", "flow", "alter_id"],
|
||||||
},
|
},
|
||||||
// Credential fields: the Go struct validator reports "required" for
|
// Credential fields: the Go struct validator reports "required" for
|
||||||
// whichever of these is missing once the type is chosen, so each one
|
// whichever of these is missing once the type is chosen, so each one
|
||||||
@@ -111,8 +113,9 @@ export function UsersPage() {
|
|||||||
// are filtered out before validateRequired runs, so e.g. Password is
|
// are filtered out before validateRequired runs, so e.g. Password is
|
||||||
// only enforced for hysteria/hysteria2/trojan/tuic and not for vless.
|
// only enforced for hysteria/hysteria2/trojan/tuic and not for vless.
|
||||||
{ name: "uuid", label: "UUID", type: "uuid", required: true, visibleWhen: showFor(SHOW_UUID) },
|
{ name: "uuid", label: "UUID", type: "uuid", required: true, visibleWhen: showFor(SHOW_UUID) },
|
||||||
{ name: "password", label: "Password", type: "text", required: true, visibleWhen: showFor(SHOW_PASSWORD) },
|
{ name: "password", label: "Password", type: "text", visibleWhen: showFor(SHOW_PASSWORD) },
|
||||||
{ name: "secret", label: "Secret", type: "text", required: true, visibleWhen: showFor(SHOW_SECRET) },
|
{ name: "secret", label: "Secret", type: "text", required: true, visibleWhen: showFor(SHOW_SECRET) },
|
||||||
|
{ name: "authorized_keys", label: "Authorized Keys", type: "string-list", visibleWhen: showFor(SHOW_AUTHORIZED_KEYS) },
|
||||||
{
|
{
|
||||||
name: "flow",
|
name: "flow",
|
||||||
label: "Flow",
|
label: "Flow",
|
||||||
@@ -135,6 +138,7 @@ export function UsersPage() {
|
|||||||
uuid: u.uuid,
|
uuid: u.uuid,
|
||||||
password: u.password,
|
password: u.password,
|
||||||
secret: u.secret,
|
secret: u.secret,
|
||||||
|
authorized_keys: u.authorized_keys ?? [],
|
||||||
flow: u.flow,
|
flow: u.flow,
|
||||||
alter_id: u.alter_id,
|
alter_id: u.alter_id,
|
||||||
}),
|
}),
|
||||||
@@ -146,6 +150,7 @@ export function UsersPage() {
|
|||||||
uuid: f.uuid ? String(f.uuid).trim() : undefined,
|
uuid: f.uuid ? String(f.uuid).trim() : undefined,
|
||||||
password: f.password ? String(f.password) : undefined,
|
password: f.password ? String(f.password) : undefined,
|
||||||
secret: f.secret ? String(f.secret) : undefined,
|
secret: f.secret ? String(f.secret) : undefined,
|
||||||
|
authorized_keys: Array.isArray(f.authorized_keys) ? (f.authorized_keys as string[]).filter(Boolean) : undefined,
|
||||||
flow: f.flow ? String(f.flow) : undefined,
|
flow: f.flow ? String(f.flow) : undefined,
|
||||||
alter_id: f.alter_id !== undefined && f.alter_id !== "" ? Number(f.alter_id) : undefined,
|
alter_id: f.alter_id !== undefined && f.alter_id !== "" ? Number(f.alter_id) : undefined,
|
||||||
}),
|
}),
|
||||||
@@ -154,6 +159,7 @@ export function UsersPage() {
|
|||||||
if (f.uuid && String(f.uuid).trim() !== "") out.uuid = String(f.uuid).trim();
|
if (f.uuid && String(f.uuid).trim() !== "") out.uuid = String(f.uuid).trim();
|
||||||
if (f.password !== undefined && f.password !== "") out.password = String(f.password);
|
if (f.password !== undefined && f.password !== "") out.password = String(f.password);
|
||||||
if (f.secret !== undefined && f.secret !== "") out.secret = String(f.secret);
|
if (f.secret !== undefined && f.secret !== "") out.secret = String(f.secret);
|
||||||
|
if (Array.isArray(f.authorized_keys) && (f.authorized_keys as string[]).filter(Boolean).length > 0) out.authorized_keys = (f.authorized_keys as string[]).filter(Boolean);
|
||||||
if (f.flow !== undefined && f.flow !== "") out.flow = String(f.flow);
|
if (f.flow !== undefined && f.flow !== "") out.flow = String(f.flow);
|
||||||
if (f.alter_id !== undefined && f.alter_id !== "") out.alter_id = Number(f.alter_id);
|
if (f.alter_id !== undefined && f.alter_id !== "") out.alter_id = Number(f.alter_id);
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
@@ -49,46 +49,50 @@ type BaseNode struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int `json:"id" validate:"required"`
|
ID int `json:"id" validate:"required"`
|
||||||
SquadIDs []int `json:"squad_ids" validate:"required,min=1"`
|
SquadIDs []int `json:"squad_ids" validate:"required,min=1"`
|
||||||
Username string `json:"username" validate:"required"`
|
Username string `json:"username" validate:"required"`
|
||||||
Inbound string `json:"inbound" validate:"required"`
|
Inbound string `json:"inbound" validate:"required"`
|
||||||
Type string `json:"type" validate:"required"`
|
Type string `json:"type" validate:"required"`
|
||||||
UUID string `json:"uuid" validate:"required"`
|
UUID string `json:"uuid" validate:"required"`
|
||||||
Password string `json:"password" validate:"required"`
|
Password string `json:"password" validate:"required"`
|
||||||
Secret string `json:"secret" validate:"required"`
|
Secret string `json:"secret" validate:"required"`
|
||||||
Flow string `json:"flow" validate:"required"`
|
AuthorizedKeys []string `json:"authorized_keys" validate:"omitempty"`
|
||||||
AlterID int `json:"alter_id" validate:"required"`
|
Flow string `json:"flow" validate:"required"`
|
||||||
CreatedAt time.Time `json:"created_at" validate:"required"`
|
AlterID int `json:"alter_id" validate:"required"`
|
||||||
UpdatedAt time.Time `json:"updated_at" validate:"required"`
|
CreatedAt time.Time `json:"created_at" validate:"required"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserCreate struct {
|
type UserCreate struct {
|
||||||
SquadIDs []int `json:"squad_ids" validate:"required,min=1"`
|
SquadIDs []int `json:"squad_ids" validate:"required,min=1"`
|
||||||
Username string `json:"username" validate:"required"`
|
Username string `json:"username" validate:"required"`
|
||||||
Inbound string `json:"inbound" validate:"required"`
|
Inbound string `json:"inbound" validate:"required"`
|
||||||
Type string `json:"type" validate:"required,oneof=anytls http hysteria hysteria2 mixed mtproxy naive socks trojan trusttunnel tuic vless vmess"`
|
Type string `json:"type" validate:"required,oneof=anytls http hysteria hysteria2 mixed mtproxy naive socks ssh trojan trusttunnel tuic vless vmess"`
|
||||||
UUID string `json:"uuid" validate:"omitempty,uuid4"`
|
UUID string `json:"uuid" validate:"omitempty,uuid4"`
|
||||||
Password string `json:"password" validate:"omitempty"`
|
Password string `json:"password" validate:"omitempty"`
|
||||||
Secret string `json:"secret" validate:"omitempty"`
|
Secret string `json:"secret" validate:"omitempty"`
|
||||||
Flow string `json:"flow" validate:"omitempty"`
|
AuthorizedKeys []string `json:"authorized_keys" validate:"omitempty"`
|
||||||
AlterID int `json:"alter_id" validate:"omitempty"`
|
Flow string `json:"flow" validate:"omitempty"`
|
||||||
|
AlterID int `json:"alter_id" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserUpdate struct {
|
type UserUpdate struct {
|
||||||
UUID string `json:"uuid" validate:"omitempty,uuid4"`
|
UUID string `json:"uuid" validate:"omitempty,uuid4"`
|
||||||
Password string `json:"password" validate:"omitempty"`
|
Password string `json:"password" validate:"omitempty"`
|
||||||
Secret string `json:"secret" validate:"omitempty"`
|
Secret string `json:"secret" validate:"omitempty"`
|
||||||
Flow string `json:"flow" validate:"omitempty"`
|
AuthorizedKeys []string `json:"authorized_keys" validate:"omitempty"`
|
||||||
AlterID int `json:"alter_id" validate:"omitempty"`
|
Flow string `json:"flow" validate:"omitempty"`
|
||||||
|
AlterID int `json:"alter_id" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseUser struct {
|
type BaseUser struct {
|
||||||
UUID string `json:"uuid" validate:"omitempty,uuid4"`
|
UUID string `json:"uuid" validate:"omitempty,uuid4"`
|
||||||
Password string `json:"password" validate:"omitempty"`
|
Password string `json:"password" validate:"omitempty"`
|
||||||
Secret string `json:"secret" validate:"omitempty"`
|
Secret string `json:"secret" validate:"omitempty"`
|
||||||
Flow string `json:"flow" validate:"omitempty"`
|
AuthorizedKeys []string `json:"authorized_keys" validate:"omitempty"`
|
||||||
AlterID int `json:"alter_id" validate:"omitempty"`
|
Flow string `json:"flow" validate:"omitempty"`
|
||||||
|
AlterID int `json:"alter_id" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConnectionLimiter struct {
|
type ConnectionLimiter struct {
|
||||||
@@ -261,5 +265,3 @@ type BaseRateLimiter struct {
|
|||||||
Count uint32 `json:"count" validate:"excluded_if=Strategy bypass,required_unless=Strategy bypass"`
|
Count uint32 `json:"count" validate:"excluded_if=Strategy bypass,required_unless=Strategy bypass"`
|
||||||
Interval string `json:"interval" validate:"excluded_if=Strategy bypass,required_unless=Strategy bypass"`
|
Interval string `json:"interval" validate:"excluded_if=Strategy bypass,required_unless=Strategy bypass"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -51,5 +51,3 @@ type Repository interface {
|
|||||||
UpdateRateLimiter(id int, limiter RateLimiterUpdate) (RateLimiter, error)
|
UpdateRateLimiter(id int, limiter RateLimiterUpdate) (RateLimiter, error)
|
||||||
DeleteRateLimiter(id int) (RateLimiter, error)
|
DeleteRateLimiter(id int) (RateLimiter, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -331,6 +331,12 @@ var migrations = map[string]string{
|
|||||||
DROP TABLE IF EXISTS traffic_limiter_to_squad;
|
DROP TABLE IF EXISTS traffic_limiter_to_squad;
|
||||||
DROP TABLE IF EXISTS traffic_limiters;
|
DROP TABLE IF EXISTS traffic_limiters;
|
||||||
`,
|
`,
|
||||||
|
"3_add_authorized_keys.up.sql": `
|
||||||
|
ALTER TABLE users ADD COLUMN authorized_keys JSONB NOT NULL DEFAULT '[]'::jsonb;
|
||||||
|
`,
|
||||||
|
"3_add_authorized_keys.down.sql": `
|
||||||
|
ALTER TABLE users DROP COLUMN authorized_keys;
|
||||||
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
func Migrate(db *sql.DB) error {
|
func Migrate(db *sql.DB) error {
|
||||||
|
|||||||
@@ -515,6 +515,11 @@ func (r *PostgreSQLRepository) CreateUser(user constant.UserCreate) (constant.Us
|
|||||||
}
|
}
|
||||||
defer tx.Rollback(r.ctx)
|
defer tx.Rollback(r.ctx)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
authorizedKeysJSON, err := marshalStringSlice(user.AuthorizedKeys)
|
||||||
|
if err != nil {
|
||||||
|
return u, err
|
||||||
|
}
|
||||||
|
var authorizedKeys stringSliceJSON
|
||||||
err = tx.QueryRow(
|
err = tx.QueryRow(
|
||||||
r.ctx, `
|
r.ctx, `
|
||||||
INSERT INTO users (
|
INSERT INTO users (
|
||||||
@@ -524,12 +529,13 @@ func (r *PostgreSQLRepository) CreateUser(user constant.UserCreate) (constant.Us
|
|||||||
uuid,
|
uuid,
|
||||||
password,
|
password,
|
||||||
secret,
|
secret,
|
||||||
|
authorized_keys,
|
||||||
flow,
|
flow,
|
||||||
alter_id,
|
alter_id,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||||
RETURNING
|
RETURNING
|
||||||
id,
|
id,
|
||||||
username,
|
username,
|
||||||
@@ -538,6 +544,7 @@ func (r *PostgreSQLRepository) CreateUser(user constant.UserCreate) (constant.Us
|
|||||||
uuid,
|
uuid,
|
||||||
password,
|
password,
|
||||||
secret,
|
secret,
|
||||||
|
authorized_keys,
|
||||||
flow,
|
flow,
|
||||||
alter_id,
|
alter_id,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -549,6 +556,7 @@ func (r *PostgreSQLRepository) CreateUser(user constant.UserCreate) (constant.Us
|
|||||||
user.UUID,
|
user.UUID,
|
||||||
user.Password,
|
user.Password,
|
||||||
user.Secret,
|
user.Secret,
|
||||||
|
authorizedKeysJSON,
|
||||||
user.Flow,
|
user.Flow,
|
||||||
user.AlterID,
|
user.AlterID,
|
||||||
now,
|
now,
|
||||||
@@ -561,6 +569,7 @@ func (r *PostgreSQLRepository) CreateUser(user constant.UserCreate) (constant.Us
|
|||||||
&u.UUID,
|
&u.UUID,
|
||||||
&u.Password,
|
&u.Password,
|
||||||
&u.Secret,
|
&u.Secret,
|
||||||
|
&authorizedKeys,
|
||||||
&u.Flow,
|
&u.Flow,
|
||||||
&u.AlterID,
|
&u.AlterID,
|
||||||
&u.CreatedAt,
|
&u.CreatedAt,
|
||||||
@@ -569,6 +578,7 @@ func (r *PostgreSQLRepository) CreateUser(user constant.UserCreate) (constant.Us
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return u, err
|
return u, err
|
||||||
}
|
}
|
||||||
|
u.AuthorizedKeys = []string(authorizedKeys)
|
||||||
rows := make([][]any, len(user.SquadIDs))
|
rows := make([][]any, len(user.SquadIDs))
|
||||||
for i, squadID := range user.SquadIDs {
|
for i, squadID := range user.SquadIDs {
|
||||||
rows[i] = []any{u.ID, squadID}
|
rows[i] = []any{u.ID, squadID}
|
||||||
@@ -605,6 +615,7 @@ func (r *PostgreSQLRepository) GetUsers(filters map[string][]string) ([]constant
|
|||||||
"uuid",
|
"uuid",
|
||||||
"password",
|
"password",
|
||||||
"secret",
|
"secret",
|
||||||
|
"authorized_keys",
|
||||||
"flow",
|
"flow",
|
||||||
"alter_id",
|
"alter_id",
|
||||||
"created_at",
|
"created_at",
|
||||||
@@ -636,6 +647,7 @@ func (r *PostgreSQLRepository) GetUsers(filters map[string][]string) ([]constant
|
|||||||
&u.UUID,
|
&u.UUID,
|
||||||
&u.Password,
|
&u.Password,
|
||||||
&u.Secret,
|
&u.Secret,
|
||||||
|
&u.AuthorizedKeys,
|
||||||
&u.Flow,
|
&u.Flow,
|
||||||
&u.AlterID,
|
&u.AlterID,
|
||||||
&u.CreatedAt,
|
&u.CreatedAt,
|
||||||
@@ -681,6 +693,7 @@ func (r *PostgreSQLRepository) GetUser(id int) (constant.User, error) {
|
|||||||
uuid,
|
uuid,
|
||||||
password,
|
password,
|
||||||
secret,
|
secret,
|
||||||
|
authorized_keys,
|
||||||
flow,
|
flow,
|
||||||
alter_id,
|
alter_id,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -696,6 +709,7 @@ func (r *PostgreSQLRepository) GetUser(id int) (constant.User, error) {
|
|||||||
&u.UUID,
|
&u.UUID,
|
||||||
&u.Password,
|
&u.Password,
|
||||||
&u.Secret,
|
&u.Secret,
|
||||||
|
&u.AuthorizedKeys,
|
||||||
&u.Flow,
|
&u.Flow,
|
||||||
&u.AlterID,
|
&u.AlterID,
|
||||||
&u.CreatedAt,
|
&u.CreatedAt,
|
||||||
@@ -706,17 +720,22 @@ func (r *PostgreSQLRepository) GetUser(id int) (constant.User, error) {
|
|||||||
|
|
||||||
func (r *PostgreSQLRepository) UpdateUser(id int, user constant.UserUpdate) (constant.User, error) {
|
func (r *PostgreSQLRepository) UpdateUser(id int, user constant.UserUpdate) (constant.User, error) {
|
||||||
var u constant.User
|
var u constant.User
|
||||||
err := r.db.QueryRow(
|
authorizedKeysJSON, err := marshalStringSlice(user.AuthorizedKeys)
|
||||||
|
if err != nil {
|
||||||
|
return u, err
|
||||||
|
}
|
||||||
|
err = r.db.QueryRow(
|
||||||
r.ctx, `
|
r.ctx, `
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET
|
SET
|
||||||
uuid = $1,
|
uuid = $1,
|
||||||
password = $2,
|
password = $2,
|
||||||
secret = $3,
|
secret = $3,
|
||||||
flow = $4,
|
authorized_keys = $4,
|
||||||
alter_id = $5,
|
flow = $5,
|
||||||
updated_at = $6
|
alter_id = $6,
|
||||||
WHERE id = $7
|
updated_at = $7
|
||||||
|
WHERE id = $8
|
||||||
RETURNING
|
RETURNING
|
||||||
id,
|
id,
|
||||||
ARRAY(
|
ARRAY(
|
||||||
@@ -730,6 +749,7 @@ func (r *PostgreSQLRepository) UpdateUser(id int, user constant.UserUpdate) (con
|
|||||||
uuid,
|
uuid,
|
||||||
password,
|
password,
|
||||||
secret,
|
secret,
|
||||||
|
authorized_keys,
|
||||||
flow,
|
flow,
|
||||||
alter_id,
|
alter_id,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -738,6 +758,7 @@ func (r *PostgreSQLRepository) UpdateUser(id int, user constant.UserUpdate) (con
|
|||||||
user.UUID,
|
user.UUID,
|
||||||
user.Password,
|
user.Password,
|
||||||
user.Secret,
|
user.Secret,
|
||||||
|
authorizedKeysJSON,
|
||||||
user.Flow,
|
user.Flow,
|
||||||
user.AlterID,
|
user.AlterID,
|
||||||
time.Now(),
|
time.Now(),
|
||||||
@@ -751,6 +772,7 @@ func (r *PostgreSQLRepository) UpdateUser(id int, user constant.UserUpdate) (con
|
|||||||
&u.UUID,
|
&u.UUID,
|
||||||
&u.Password,
|
&u.Password,
|
||||||
&u.Secret,
|
&u.Secret,
|
||||||
|
&u.AuthorizedKeys,
|
||||||
&u.Flow,
|
&u.Flow,
|
||||||
&u.AlterID,
|
&u.AlterID,
|
||||||
&u.CreatedAt,
|
&u.CreatedAt,
|
||||||
@@ -777,6 +799,7 @@ func (r *PostgreSQLRepository) DeleteUser(id int) (constant.User, error) {
|
|||||||
uuid,
|
uuid,
|
||||||
password,
|
password,
|
||||||
secret,
|
secret,
|
||||||
|
authorized_keys,
|
||||||
flow,
|
flow,
|
||||||
alter_id,
|
alter_id,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -790,6 +813,7 @@ func (r *PostgreSQLRepository) DeleteUser(id int) (constant.User, error) {
|
|||||||
&u.UUID,
|
&u.UUID,
|
||||||
&u.Password,
|
&u.Password,
|
||||||
&u.Secret,
|
&u.Secret,
|
||||||
|
&u.AuthorizedKeys,
|
||||||
&u.Flow,
|
&u.Flow,
|
||||||
&u.AlterID,
|
&u.AlterID,
|
||||||
&u.CreatedAt,
|
&u.CreatedAt,
|
||||||
@@ -2143,11 +2167,11 @@ func init() {
|
|||||||
"updated_at_end": LessThanFilter("updated_at"),
|
"updated_at_end": LessThanFilter("updated_at"),
|
||||||
"sort_asc": ReplacedSortAscFilter(
|
"sort_asc": ReplacedSortAscFilter(
|
||||||
map[string]string{"speed": "raw_speed"},
|
map[string]string{"speed": "raw_speed"},
|
||||||
[]string{"id", "username", "outbound", "strategy", "mode", "raw_speed", "created_at", "updated_at"},
|
[]string{"id", "username", "outbound", "strategy", "connection_type", "mode", "raw_speed", "created_at", "updated_at"},
|
||||||
),
|
),
|
||||||
"sort_desc": ReplacedSortDescFilter(
|
"sort_desc": ReplacedSortDescFilter(
|
||||||
map[string]string{"speed": "raw_speed"},
|
map[string]string{"speed": "raw_speed"},
|
||||||
[]string{"id", "username", "outbound", "strategy", "mode", "raw_speed", "created_at", "updated_at"},
|
[]string{"id", "username", "outbound", "strategy", "connection_type", "mode", "raw_speed", "created_at", "updated_at"},
|
||||||
),
|
),
|
||||||
"offset": OffsetFilter(),
|
"offset": OffsetFilter(),
|
||||||
"limit": LimitFilter(),
|
"limit": LimitFilter(),
|
||||||
|
|||||||
@@ -213,6 +213,12 @@ var migrations = map[string]string{
|
|||||||
DROP TABLE IF EXISTS nodes;
|
DROP TABLE IF EXISTS nodes;
|
||||||
DROP TABLE IF EXISTS squads;
|
DROP TABLE IF EXISTS squads;
|
||||||
`,
|
`,
|
||||||
|
"2_add_authorized_keys.up.sql": `
|
||||||
|
ALTER TABLE users ADD COLUMN authorized_keys TEXT NOT NULL DEFAULT '[]';
|
||||||
|
`,
|
||||||
|
"2_add_authorized_keys.down.sql": `
|
||||||
|
ALTER TABLE users DROP COLUMN authorized_keys;
|
||||||
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
func Migrate(db *sql.DB) error {
|
func Migrate(db *sql.DB) error {
|
||||||
|
|||||||
@@ -510,6 +510,11 @@ func (r *SQLiteRepository) CreateUser(user constant.UserCreate) (constant.User,
|
|||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
authorizedKeysJSON, err := marshalStringSlice(user.AuthorizedKeys)
|
||||||
|
if err != nil {
|
||||||
|
return u, err
|
||||||
|
}
|
||||||
|
var authorizedKeys stringSliceJSON
|
||||||
err = tx.QueryRowContext(
|
err = tx.QueryRowContext(
|
||||||
r.ctx, `
|
r.ctx, `
|
||||||
INSERT INTO users (
|
INSERT INTO users (
|
||||||
@@ -519,12 +524,13 @@ func (r *SQLiteRepository) CreateUser(user constant.UserCreate) (constant.User,
|
|||||||
uuid,
|
uuid,
|
||||||
password,
|
password,
|
||||||
secret,
|
secret,
|
||||||
|
authorized_keys,
|
||||||
flow,
|
flow,
|
||||||
alter_id,
|
alter_id,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
RETURNING
|
RETURNING
|
||||||
id,
|
id,
|
||||||
username,
|
username,
|
||||||
@@ -533,6 +539,7 @@ func (r *SQLiteRepository) CreateUser(user constant.UserCreate) (constant.User,
|
|||||||
uuid,
|
uuid,
|
||||||
password,
|
password,
|
||||||
secret,
|
secret,
|
||||||
|
authorized_keys,
|
||||||
flow,
|
flow,
|
||||||
alter_id,
|
alter_id,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -544,6 +551,7 @@ func (r *SQLiteRepository) CreateUser(user constant.UserCreate) (constant.User,
|
|||||||
user.UUID,
|
user.UUID,
|
||||||
user.Password,
|
user.Password,
|
||||||
user.Secret,
|
user.Secret,
|
||||||
|
authorizedKeysJSON,
|
||||||
user.Flow,
|
user.Flow,
|
||||||
user.AlterID,
|
user.AlterID,
|
||||||
now,
|
now,
|
||||||
@@ -556,6 +564,7 @@ func (r *SQLiteRepository) CreateUser(user constant.UserCreate) (constant.User,
|
|||||||
&u.UUID,
|
&u.UUID,
|
||||||
&u.Password,
|
&u.Password,
|
||||||
&u.Secret,
|
&u.Secret,
|
||||||
|
&authorizedKeys,
|
||||||
&u.Flow,
|
&u.Flow,
|
||||||
&u.AlterID,
|
&u.AlterID,
|
||||||
&u.CreatedAt,
|
&u.CreatedAt,
|
||||||
@@ -564,6 +573,7 @@ func (r *SQLiteRepository) CreateUser(user constant.UserCreate) (constant.User,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return u, err
|
return u, err
|
||||||
}
|
}
|
||||||
|
u.AuthorizedKeys = []string(authorizedKeys)
|
||||||
stmt, err := tx.PrepareContext(r.ctx, `INSERT INTO user_to_squad (user_id, squad_id) VALUES (?, ?)`)
|
stmt, err := tx.PrepareContext(r.ctx, `INSERT INTO user_to_squad (user_id, squad_id) VALUES (?, ?)`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return u, err
|
return u, err
|
||||||
@@ -596,6 +606,7 @@ func (r *SQLiteRepository) GetUsers(filters map[string][]string) ([]constant.Use
|
|||||||
"uuid",
|
"uuid",
|
||||||
"password",
|
"password",
|
||||||
"secret",
|
"secret",
|
||||||
|
"authorized_keys",
|
||||||
"flow",
|
"flow",
|
||||||
"alter_id",
|
"alter_id",
|
||||||
"created_at",
|
"created_at",
|
||||||
@@ -619,6 +630,7 @@ func (r *SQLiteRepository) GetUsers(filters map[string][]string) ([]constant.Use
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var u constant.User
|
var u constant.User
|
||||||
var squadIDs intSliceJSON
|
var squadIDs intSliceJSON
|
||||||
|
var authorizedKeys stringSliceJSON
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&u.ID,
|
&u.ID,
|
||||||
&squadIDs,
|
&squadIDs,
|
||||||
@@ -628,6 +640,7 @@ func (r *SQLiteRepository) GetUsers(filters map[string][]string) ([]constant.Use
|
|||||||
&u.UUID,
|
&u.UUID,
|
||||||
&u.Password,
|
&u.Password,
|
||||||
&u.Secret,
|
&u.Secret,
|
||||||
|
&authorizedKeys,
|
||||||
&u.Flow,
|
&u.Flow,
|
||||||
&u.AlterID,
|
&u.AlterID,
|
||||||
&u.CreatedAt,
|
&u.CreatedAt,
|
||||||
@@ -636,6 +649,7 @@ func (r *SQLiteRepository) GetUsers(filters map[string][]string) ([]constant.Use
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
u.SquadIDs = []int(squadIDs)
|
u.SquadIDs = []int(squadIDs)
|
||||||
|
u.AuthorizedKeys = []string(authorizedKeys)
|
||||||
result = append(result, u)
|
result = append(result, u)
|
||||||
}
|
}
|
||||||
return result, rows.Err()
|
return result, rows.Err()
|
||||||
@@ -661,6 +675,7 @@ func (r *SQLiteRepository) GetUsersCount(filters map[string][]string) (int, erro
|
|||||||
func (r *SQLiteRepository) GetUser(id int) (constant.User, error) {
|
func (r *SQLiteRepository) GetUser(id int) (constant.User, error) {
|
||||||
var u constant.User
|
var u constant.User
|
||||||
var squadIDs intSliceJSON
|
var squadIDs intSliceJSON
|
||||||
|
var authorizedKeys stringSliceJSON
|
||||||
err := r.db.QueryRowContext(r.ctx, `
|
err := r.db.QueryRowContext(r.ctx, `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
@@ -675,6 +690,7 @@ func (r *SQLiteRepository) GetUser(id int) (constant.User, error) {
|
|||||||
uuid,
|
uuid,
|
||||||
password,
|
password,
|
||||||
secret,
|
secret,
|
||||||
|
authorized_keys,
|
||||||
flow,
|
flow,
|
||||||
alter_id,
|
alter_id,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -690,25 +706,33 @@ func (r *SQLiteRepository) GetUser(id int) (constant.User, error) {
|
|||||||
&u.UUID,
|
&u.UUID,
|
||||||
&u.Password,
|
&u.Password,
|
||||||
&u.Secret,
|
&u.Secret,
|
||||||
|
&authorizedKeys,
|
||||||
&u.Flow,
|
&u.Flow,
|
||||||
&u.AlterID,
|
&u.AlterID,
|
||||||
&u.CreatedAt,
|
&u.CreatedAt,
|
||||||
&u.UpdatedAt,
|
&u.UpdatedAt,
|
||||||
)
|
)
|
||||||
u.SquadIDs = []int(squadIDs)
|
u.SquadIDs = []int(squadIDs)
|
||||||
|
u.AuthorizedKeys = []string(authorizedKeys)
|
||||||
return u, notFoundErr(err)
|
return u, notFoundErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SQLiteRepository) UpdateUser(id int, user constant.UserUpdate) (constant.User, error) {
|
func (r *SQLiteRepository) UpdateUser(id int, user constant.UserUpdate) (constant.User, error) {
|
||||||
var u constant.User
|
var u constant.User
|
||||||
var squadIDs intSliceJSON
|
var squadIDs intSliceJSON
|
||||||
err := r.db.QueryRowContext(
|
var authorizedKeys stringSliceJSON
|
||||||
|
authorizedKeysJSON, err := marshalStringSlice(user.AuthorizedKeys)
|
||||||
|
if err != nil {
|
||||||
|
return u, err
|
||||||
|
}
|
||||||
|
err = r.db.QueryRowContext(
|
||||||
r.ctx, `
|
r.ctx, `
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET
|
SET
|
||||||
uuid = ?,
|
uuid = ?,
|
||||||
password = ?,
|
password = ?,
|
||||||
secret = ?,
|
secret = ?,
|
||||||
|
authorized_keys = ?,
|
||||||
flow = ?,
|
flow = ?,
|
||||||
alter_id = ?,
|
alter_id = ?,
|
||||||
updated_at = ?
|
updated_at = ?
|
||||||
@@ -726,6 +750,7 @@ func (r *SQLiteRepository) UpdateUser(id int, user constant.UserUpdate) (constan
|
|||||||
uuid,
|
uuid,
|
||||||
password,
|
password,
|
||||||
secret,
|
secret,
|
||||||
|
authorized_keys,
|
||||||
flow,
|
flow,
|
||||||
alter_id,
|
alter_id,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -734,6 +759,7 @@ func (r *SQLiteRepository) UpdateUser(id int, user constant.UserUpdate) (constan
|
|||||||
user.UUID,
|
user.UUID,
|
||||||
user.Password,
|
user.Password,
|
||||||
user.Secret,
|
user.Secret,
|
||||||
|
authorizedKeysJSON,
|
||||||
user.Flow,
|
user.Flow,
|
||||||
user.AlterID,
|
user.AlterID,
|
||||||
time.Now(),
|
time.Now(),
|
||||||
@@ -747,18 +773,21 @@ func (r *SQLiteRepository) UpdateUser(id int, user constant.UserUpdate) (constan
|
|||||||
&u.UUID,
|
&u.UUID,
|
||||||
&u.Password,
|
&u.Password,
|
||||||
&u.Secret,
|
&u.Secret,
|
||||||
|
&authorizedKeys,
|
||||||
&u.Flow,
|
&u.Flow,
|
||||||
&u.AlterID,
|
&u.AlterID,
|
||||||
&u.CreatedAt,
|
&u.CreatedAt,
|
||||||
&u.UpdatedAt,
|
&u.UpdatedAt,
|
||||||
)
|
)
|
||||||
u.SquadIDs = []int(squadIDs)
|
u.SquadIDs = []int(squadIDs)
|
||||||
|
u.AuthorizedKeys = []string(authorizedKeys)
|
||||||
return u, err
|
return u, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SQLiteRepository) DeleteUser(id int) (constant.User, error) {
|
func (r *SQLiteRepository) DeleteUser(id int) (constant.User, error) {
|
||||||
var u constant.User
|
var u constant.User
|
||||||
var squadIDs intSliceJSON
|
var squadIDs intSliceJSON
|
||||||
|
var authorizedKeys stringSliceJSON
|
||||||
err := r.db.QueryRowContext(r.ctx, `
|
err := r.db.QueryRowContext(r.ctx, `
|
||||||
DELETE FROM users
|
DELETE FROM users
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
@@ -775,6 +804,7 @@ func (r *SQLiteRepository) DeleteUser(id int) (constant.User, error) {
|
|||||||
uuid,
|
uuid,
|
||||||
password,
|
password,
|
||||||
secret,
|
secret,
|
||||||
|
authorized_keys,
|
||||||
flow,
|
flow,
|
||||||
alter_id,
|
alter_id,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -788,12 +818,14 @@ func (r *SQLiteRepository) DeleteUser(id int) (constant.User, error) {
|
|||||||
&u.UUID,
|
&u.UUID,
|
||||||
&u.Password,
|
&u.Password,
|
||||||
&u.Secret,
|
&u.Secret,
|
||||||
|
&authorizedKeys,
|
||||||
&u.Flow,
|
&u.Flow,
|
||||||
&u.AlterID,
|
&u.AlterID,
|
||||||
&u.CreatedAt,
|
&u.CreatedAt,
|
||||||
&u.UpdatedAt,
|
&u.UpdatedAt,
|
||||||
)
|
)
|
||||||
u.SquadIDs = []int(squadIDs)
|
u.SquadIDs = []int(squadIDs)
|
||||||
|
u.AuthorizedKeys = []string(authorizedKeys)
|
||||||
return u, err
|
return u, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2160,11 +2192,11 @@ func init() {
|
|||||||
"updated_at_end": LessThanFilter("updated_at"),
|
"updated_at_end": LessThanFilter("updated_at"),
|
||||||
"sort_asc": ReplacedSortAscFilter(
|
"sort_asc": ReplacedSortAscFilter(
|
||||||
map[string]string{"speed": "raw_speed"},
|
map[string]string{"speed": "raw_speed"},
|
||||||
[]string{"id", "username", "outbound", "strategy", "mode", "raw_speed", "created_at", "updated_at"},
|
[]string{"id", "username", "outbound", "strategy", "connection_type", "mode", "raw_speed", "created_at", "updated_at"},
|
||||||
),
|
),
|
||||||
"sort_desc": ReplacedSortDescFilter(
|
"sort_desc": ReplacedSortDescFilter(
|
||||||
map[string]string{"speed": "raw_speed"},
|
map[string]string{"speed": "raw_speed"},
|
||||||
[]string{"id", "username", "outbound", "strategy", "mode", "raw_speed", "created_at", "updated_at"},
|
[]string{"id", "username", "outbound", "strategy", "connection_type", "mode", "raw_speed", "created_at", "updated_at"},
|
||||||
),
|
),
|
||||||
"offset": OffsetFilter(),
|
"offset": OffsetFilter(),
|
||||||
"limit": LimitFilter(),
|
"limit": LimitFilter(),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user