mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-07-02 14:47:27 +03:00
Compare commits
72 Commits
v1.12.0-be
...
dev-wip
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
847c80a1fe | ||
|
|
40be0627f5 | ||
|
|
ae3b943b93 | ||
|
|
7da9b40c66 | ||
|
|
48772992fa | ||
|
|
a66772fa14 | ||
|
|
3b49ed8a71 | ||
|
|
1c34ee26dc | ||
|
|
0ca4d8f327 | ||
|
|
7e69892694 | ||
|
|
0494541057 | ||
|
|
b55d5914e0 | ||
|
|
584fd40d59 | ||
|
|
3c595f205b | ||
|
|
f75bd13383 | ||
|
|
99effc44f8 | ||
|
|
7abe17cea0 | ||
|
|
04f0ae6c00 | ||
|
|
8f79ee6919 | ||
|
|
29150fc0f7 | ||
|
|
30ac4ca62c | ||
|
|
819f14eca0 | ||
|
|
24df55660d | ||
|
|
02943cefb3 | ||
|
|
9f5a403532 | ||
|
|
9bcab743fc | ||
|
|
af47fc2543 | ||
|
|
07160909d7 | ||
|
|
74c37a6f9c | ||
|
|
0dd11223ef | ||
|
|
f635cdd3ef | ||
|
|
dccf5c56fb | ||
|
|
12141bb74a | ||
|
|
657bd4725f | ||
|
|
c50c8815f0 | ||
|
|
3026b37734 | ||
|
|
ecbd8f55f6 | ||
|
|
743b00f748 | ||
|
|
0249fc5e67 | ||
|
|
51e63cbeba | ||
|
|
b6a9e0ce0c | ||
|
|
3114db2450 | ||
|
|
ac29d4ca9c | ||
|
|
70bb012989 | ||
|
|
c6090a32dc | ||
|
|
e7d0227c90 | ||
|
|
6bbab76cc7 | ||
|
|
0af1236d8f | ||
|
|
488e26aebc | ||
|
|
8a2a65133a | ||
|
|
5e6cc89c65 | ||
|
|
3145df4b6c | ||
|
|
4b2f455e1e | ||
|
|
02619e3dfa | ||
|
|
00082ab6c8 | ||
|
|
ce26077d83 | ||
|
|
089e13451b | ||
|
|
d3184a7997 | ||
|
|
65f5767180 | ||
|
|
c594f33dc4 | ||
|
|
e65a9d7380 | ||
|
|
dd304bebfc | ||
|
|
6a6c6a5ba5 | ||
|
|
b99dc4609d | ||
|
|
e59dab657e | ||
|
|
908174ddf6 | ||
|
|
34c48fa421 | ||
|
|
1fe5b8fd9d | ||
|
|
4bef6ea0e8 | ||
|
|
941049d21b | ||
|
|
bdc1724975 | ||
|
|
942511ad6d |
@@ -1,18 +1,20 @@
|
||||
-s dir
|
||||
--name sing-box
|
||||
--category net
|
||||
--license GPL-3.0-or-later
|
||||
--license GPLv3-or-later
|
||||
--description "The universal proxy platform."
|
||||
--url "https://sing-box.sagernet.org/"
|
||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
|
||||
--no-deb-generate-changes
|
||||
--config-files /etc/sing-box/config.json
|
||||
|
||||
release/config/config.json=/etc/sing-box/config.json
|
||||
|
||||
release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service
|
||||
release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
|
||||
release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf
|
||||
release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules
|
||||
release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf
|
||||
|
||||
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
|
||||
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
|
||||
30
.fpm_openwrt
30
.fpm_openwrt
@@ -1,30 +0,0 @@
|
||||
-s dir
|
||||
--name sing-box
|
||||
--category net
|
||||
--license GPL-3.0-or-later
|
||||
--description "The universal proxy platform."
|
||||
--url "https://sing-box.sagernet.org/"
|
||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||
--no-deb-generate-changes
|
||||
|
||||
--config-files /etc/config/sing-box
|
||||
--config-files /etc/sing-box/config.json
|
||||
|
||||
--depends ca-bundle
|
||||
--depends kmod-inet-diag
|
||||
--depends kmod-tun
|
||||
--depends firewall4
|
||||
|
||||
--before-remove release/config/openwrt.prerm
|
||||
|
||||
release/config/config.json=/etc/sing-box/config.json
|
||||
|
||||
release/config/openwrt.conf=/etc/config/sing-box
|
||||
release/config/openwrt.init=/etc/init.d/sing-box
|
||||
release/config/openwrt.keep=/lib/upgrade/keep.d/sing-box
|
||||
|
||||
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
|
||||
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
|
||||
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
|
||||
|
||||
LICENSE=/usr/share/licenses/sing-box/LICENSE
|
||||
28
.github/deb2ipk.sh
vendored
28
.github/deb2ipk.sh
vendored
@@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# mod from https://gist.github.com/pldubouilh/c5703052986bfdd404005951dee54683
|
||||
|
||||
set -e -o pipefail
|
||||
|
||||
PROJECT=$(dirname "$0")/../..
|
||||
TMP_PATH=`mktemp -d`
|
||||
cp $2 $TMP_PATH
|
||||
pushd $TMP_PATH
|
||||
|
||||
DEB_NAME=`ls *.deb`
|
||||
ar x $DEB_NAME
|
||||
|
||||
mkdir control
|
||||
pushd control
|
||||
tar xf ../control.tar.gz
|
||||
rm md5sums
|
||||
sed "s/Architecture:\\ \w*/Architecture:\\ $1/g" ./control -i
|
||||
cat control
|
||||
tar czf ../control.tar.gz ./*
|
||||
popd
|
||||
|
||||
DEB_NAME=${DEB_NAME%.deb}
|
||||
tar czf $DEB_NAME.ipk control.tar.gz data.tar.gz debian-binary
|
||||
popd
|
||||
|
||||
cp $TMP_PATH/$DEB_NAME.ipk $3
|
||||
rm -r $TMP_PATH
|
||||
87
.github/workflows/build.yml
vendored
87
.github/workflows/build.yml
vendored
@@ -68,38 +68,31 @@ jobs:
|
||||
- calculate_version
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ linux, windows, darwin, android ]
|
||||
arch: [ "386", amd64, arm64 ]
|
||||
legacy_go: [ false ]
|
||||
include:
|
||||
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" }
|
||||
- { os: linux, arch: "386", go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" }
|
||||
- { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" }
|
||||
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
|
||||
- { os: linux, arch: arm, goarm: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" }
|
||||
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl, openwrt: "arm_arm1176jzf-s_vfp" }
|
||||
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, openwrt: "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" }
|
||||
- { os: linux, arch: mips, gomips: softfloat, openwrt: "mips_24kc mips_4kec mips_mips32" }
|
||||
- { os: linux, arch: mipsle, gomips: hardfloat, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc_24kf" }
|
||||
- { os: linux, arch: mipsle, gomips: softfloat, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
|
||||
- { os: linux, arch: mips64, gomips: softfloat, openwrt: "mips64_mips64r2 mips64_octeonplus" }
|
||||
- { os: linux, arch: mips64le, gomips: hardfloat, debian: mips64el, rpm: mips64el }
|
||||
- { os: linux, arch: mips64le, gomips: softfloat, openwrt: "mips64el_mips64r2" }
|
||||
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64 }
|
||||
- { os: linux, arch: "386", debian: i386, rpm: i386 }
|
||||
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
|
||||
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl }
|
||||
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64 }
|
||||
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
|
||||
- { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel }
|
||||
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
||||
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
|
||||
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
|
||||
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64 }
|
||||
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 }
|
||||
|
||||
- { os: windows, arch: amd64 }
|
||||
- { os: windows, arch: amd64, legacy_go: true }
|
||||
- { os: windows, arch: "386" }
|
||||
- { os: windows, arch: "386", legacy_go: true }
|
||||
- { os: windows, arch: arm64 }
|
||||
|
||||
- { os: darwin, arch: amd64 }
|
||||
- { os: darwin, arch: arm64 }
|
||||
- { os: windows, arch: amd64, legacy_go: true }
|
||||
|
||||
- { os: android, arch: "386", ndk: "i686-linux-android21" }
|
||||
- { os: android, arch: amd64, ndk: "x86_64-linux-android21" }
|
||||
- { os: android, arch: arm64, ndk: "aarch64-linux-android21" }
|
||||
- { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" }
|
||||
- { os: android, arch: amd64, ndk: "x86_64-linux-android21" }
|
||||
- { os: android, arch: "386", ndk: "i686-linux-android21" }
|
||||
exclude:
|
||||
- { os: darwin, arch: "386" }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
@@ -154,10 +147,7 @@ jobs:
|
||||
CGO_ENABLED: "0"
|
||||
GOOS: ${{ matrix.os }}
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
GO386: ${{ matrix.go386 }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
GOMIPS: ${{ matrix.gomips }}
|
||||
GOMIPS64: ${{ matrix.gomips }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build Android
|
||||
if: matrix.os == 'android'
|
||||
@@ -177,17 +167,12 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set name
|
||||
run: |-
|
||||
DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-${{ matrix.os }}-${{ matrix.arch }}"
|
||||
if [[ -n "${{ matrix.goarm }}" ]]; then
|
||||
DIR_NAME="${DIR_NAME}v${{ matrix.goarm }}"
|
||||
elif [[ -n "${{ matrix.go386 }}" && "${{ matrix.go386 }}" != 'sse2' ]]; then
|
||||
DIR_NAME="${DIR_NAME}-${{ matrix.go386 }}"
|
||||
elif [[ -n "${{ matrix.gomips }}" && "${{ matrix.gomips }}" != 'hardfloat' ]]; then
|
||||
DIR_NAME="${DIR_NAME}-${{ matrix.gomips }}"
|
||||
elif [[ "${{ matrix.legacy_go }}" == 'true' ]]; then
|
||||
DIR_NAME="${DIR_NAME}-legacy"
|
||||
fi
|
||||
ARM_VERSION=$([ -n '${{ matrix.goarm}}' ] && echo 'v${{ matrix.goarm}}' || true)
|
||||
LEGACY=$([ '${{ matrix.legacy_go }}' = 'true' ] && echo "-legacy" || true)
|
||||
DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-${{ matrix.os }}-${{ matrix.arch }}${ARM_VERSION}${LEGACY}"
|
||||
PKG_NAME="sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.arch }}${ARM_VERSION}"
|
||||
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
|
||||
echo "PKG_NAME=${PKG_NAME}" >> "${GITHUB_ENV}"
|
||||
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
||||
PKG_VERSION="${PKG_VERSION//-/\~}"
|
||||
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
|
||||
@@ -196,12 +181,10 @@ jobs:
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y debsigs
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t deb \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.debian }}.deb" \
|
||||
-p "dist/${PKG_NAME}.deb" \
|
||||
--architecture ${{ matrix.debian }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
|
||||
@@ -216,10 +199,9 @@ jobs:
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t rpm \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.rpm }}.rpm" \
|
||||
-p "dist/${PKG_NAME}.rpm" \
|
||||
--architecture ${{ matrix.rpm }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
cat > $HOME/.rpmmacros <<EOF
|
||||
@@ -235,29 +217,12 @@ jobs:
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libarchive-tools
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t pacman \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.pacman }}.pkg.tar.zst" \
|
||||
-p "dist/${PKG_NAME}.pkg.tar.zst" \
|
||||
--architecture ${{ matrix.pacman }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
- name: Package OpenWrt
|
||||
if: matrix.openwrt != ''
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
cp .fpm_openwrt .fpm
|
||||
fpm -t deb \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/openwrt.deb" \
|
||||
--architecture all \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
for architecture in ${{ matrix.openwrt }}; do
|
||||
.github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.ipk"
|
||||
done
|
||||
rm "dist/openwrt.deb"
|
||||
- name: Archive
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
@@ -277,7 +242,7 @@ jobs:
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.go386 && format('_{0}', matrix.go386) }}${{ matrix.gomips && format('_{0}', matrix.gomips) }}${{ matrix.legacy_go && '-legacy' || '' }}
|
||||
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.legacy_go && '-legacy' || '' }}
|
||||
path: "dist"
|
||||
build_android:
|
||||
name: Build Android
|
||||
|
||||
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@@ -120,7 +120,6 @@ jobs:
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
sudo apt-get install -y debsigs
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t deb \
|
||||
--name "${NAME}" \
|
||||
-v "$PKG_VERSION" \
|
||||
@@ -139,7 +138,6 @@ jobs:
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t rpm \
|
||||
--name "${NAME}" \
|
||||
-v "$PKG_VERSION" \
|
||||
|
||||
@@ -56,6 +56,12 @@ nfpms:
|
||||
dst: /usr/lib/systemd/system/sing-box.service
|
||||
- src: release/config/sing-box@.service
|
||||
dst: /usr/lib/systemd/system/sing-box@.service
|
||||
- src: release/config/sing-box.sysusers
|
||||
dst: /usr/lib/sysusers.d/sing-box.conf
|
||||
- src: release/config/sing-box.rules
|
||||
dst: /usr/share/polkit-1/rules.d/sing-box.rules
|
||||
- src: release/config/sing-box-split-dns.xml
|
||||
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
|
||||
|
||||
- src: release/completions/sing-box.bash
|
||||
dst: /usr/share/bash-completion/completions/sing-box.bash
|
||||
|
||||
@@ -138,6 +138,12 @@ nfpms:
|
||||
dst: /usr/lib/systemd/system/sing-box.service
|
||||
- src: release/config/sing-box@.service
|
||||
dst: /usr/lib/systemd/system/sing-box@.service
|
||||
- src: release/config/sing-box.sysusers
|
||||
dst: /usr/lib/sysusers.d/sing-box.conf
|
||||
- src: release/config/sing-box.rules
|
||||
dst: /usr/share/polkit-1/rules.d/sing-box.rules
|
||||
- src: release/config/sing-box-split-dns.xml
|
||||
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
|
||||
|
||||
- src: release/completions/sing-box.bash
|
||||
dst: /usr/share/bash-completion/completions/sing-box.bash
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
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"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
@@ -31,11 +33,30 @@ type DNSClient interface {
|
||||
}
|
||||
|
||||
type DNSQueryOptions struct {
|
||||
Transport DNSTransport
|
||||
Strategy C.DomainStrategy
|
||||
DisableCache bool
|
||||
RewriteTTL *uint32
|
||||
ClientSubnet netip.Prefix
|
||||
Transport DNSTransport
|
||||
Strategy C.DomainStrategy
|
||||
LookupStrategy C.DomainStrategy
|
||||
DisableCache bool
|
||||
RewriteTTL *uint32
|
||||
ClientSubnet netip.Prefix
|
||||
}
|
||||
|
||||
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
|
||||
if options == nil {
|
||||
return &DNSQueryOptions{}, nil
|
||||
}
|
||||
transportManager := service.FromContext[DNSTransportManager](ctx)
|
||||
transport, loaded := transportManager.Transport(options.Server)
|
||||
if !loaded {
|
||||
return nil, E.New("domain resolver not found: " + options.Server)
|
||||
}
|
||||
return &DNSQueryOptions{
|
||||
Transport: transport,
|
||||
Strategy: C.DomainStrategy(options.Strategy),
|
||||
DisableCache: options.DisableCache,
|
||||
RewriteTTL: options.RewriteTTL,
|
||||
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type RDRCStore interface {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
type FakeIPStore interface {
|
||||
Service
|
||||
SimpleLifecycle
|
||||
Contains(address netip.Addr) bool
|
||||
Create(domain string, isIPv6 bool) (netip.Addr, error)
|
||||
Lookup(address netip.Addr) (string, bool)
|
||||
|
||||
@@ -37,13 +37,14 @@ func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endp
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
m.started = true
|
||||
m.stage = stage
|
||||
for _, inbound := range m.inbounds {
|
||||
inbounds := m.inbounds
|
||||
m.access.Unlock()
|
||||
for _, inbound := range inbounds {
|
||||
err := adapter.LegacyStart(inbound, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]")
|
||||
|
||||
@@ -2,6 +2,11 @@ package adapter
|
||||
|
||||
import E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
type SimpleLifecycle interface {
|
||||
Start() error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type StartStage uint8
|
||||
|
||||
const (
|
||||
|
||||
@@ -28,14 +28,14 @@ func LegacyStart(starter any, stage StartStage) error {
|
||||
}
|
||||
|
||||
type lifecycleServiceWrapper struct {
|
||||
Service
|
||||
SimpleLifecycle
|
||||
name string
|
||||
}
|
||||
|
||||
func NewLifecycleService(service Service, name string) LifecycleService {
|
||||
func NewLifecycleService(service SimpleLifecycle, name string) LifecycleService {
|
||||
return &lifecycleServiceWrapper{
|
||||
Service: service,
|
||||
name: name,
|
||||
SimpleLifecycle: service,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,9 +44,9 @@ func (l *lifecycleServiceWrapper) Name() string {
|
||||
}
|
||||
|
||||
func (l *lifecycleServiceWrapper) Start(stage StartStage) error {
|
||||
return LegacyStart(l.Service, stage)
|
||||
return LegacyStart(l.SimpleLifecycle, stage)
|
||||
}
|
||||
|
||||
func (l *lifecycleServiceWrapper) Close() error {
|
||||
return l.Service.Close()
|
||||
return l.SimpleLifecycle.Close()
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ type HeadlessRule interface {
|
||||
|
||||
type Rule interface {
|
||||
HeadlessRule
|
||||
Service
|
||||
SimpleLifecycle
|
||||
Type() string
|
||||
Action() RuleAction
|
||||
}
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
Start() error
|
||||
Close() error
|
||||
Lifecycle
|
||||
Type() string
|
||||
Tag() string
|
||||
}
|
||||
|
||||
type ServiceRegistry interface {
|
||||
option.ServiceOptionsRegistry
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) (Service, error)
|
||||
}
|
||||
|
||||
type ServiceManager interface {
|
||||
Lifecycle
|
||||
Services() []Service
|
||||
Get(tag string) (Service, bool)
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error
|
||||
}
|
||||
|
||||
21
adapter/service/adapter.go
Normal file
21
adapter/service/adapter.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package service
|
||||
|
||||
type Adapter struct {
|
||||
serviceType string
|
||||
serviceTag string
|
||||
}
|
||||
|
||||
func NewAdapter(serviceType string, serviceTag string) Adapter {
|
||||
return Adapter{
|
||||
serviceType: serviceType,
|
||||
serviceTag: serviceTag,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) Type() string {
|
||||
return a.serviceType
|
||||
}
|
||||
|
||||
func (a *Adapter) Tag() string {
|
||||
return a.serviceTag
|
||||
}
|
||||
144
adapter/service/manager.go
Normal file
144
adapter/service/manager.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.ServiceManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.ServiceRegistry
|
||||
access sync.Mutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
services []adapter.Service
|
||||
serviceByTag map[string]adapter.Service
|
||||
}
|
||||
|
||||
func NewManager(logger log.ContextLogger, registry adapter.ServiceRegistry) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
serviceByTag: make(map[string]adapter.Service),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.access.Lock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
m.started = true
|
||||
m.stage = stage
|
||||
services := m.services
|
||||
m.access.Unlock()
|
||||
for _, service := range services {
|
||||
err := adapter.LegacyStart(service, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if !m.started {
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
services := m.services
|
||||
m.services = nil
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
var err error
|
||||
for _, service := range services {
|
||||
monitor.Start("close service/", service.Type(), "[", service.Tag(), "]")
|
||||
err = E.Append(err, service.Close(), func(err error) error {
|
||||
return E.Cause(err, "close service/", service.Type(), "[", service.Tag(), "]")
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Services() []adapter.Service {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return m.services
|
||||
}
|
||||
|
||||
func (m *Manager) Get(tag string) (adapter.Service, bool) {
|
||||
m.access.Lock()
|
||||
service, found := m.serviceByTag[tag]
|
||||
m.access.Unlock()
|
||||
return service, found
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.access.Lock()
|
||||
service, found := m.serviceByTag[tag]
|
||||
if !found {
|
||||
m.access.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.serviceByTag, tag)
|
||||
index := common.Index(m.services, func(it adapter.Service) bool {
|
||||
return it == service
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid service index")
|
||||
}
|
||||
m.services = append(m.services[:index], m.services[index+1:]...)
|
||||
started := m.started
|
||||
m.access.Unlock()
|
||||
if started {
|
||||
return service.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error {
|
||||
service, err := m.registry.Create(ctx, logger, tag, serviceType, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.started {
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
err = adapter.LegacyStart(service, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
if existsService, loaded := m.serviceByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = existsService.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "close service/", existsService.Type(), "[", existsService.Tag(), "]")
|
||||
}
|
||||
}
|
||||
existsIndex := common.Index(m.services, func(it adapter.Service) bool {
|
||||
return it == existsService
|
||||
})
|
||||
if existsIndex == -1 {
|
||||
panic("invalid service index")
|
||||
}
|
||||
m.services = append(m.services[:existsIndex], m.services[existsIndex+1:]...)
|
||||
}
|
||||
m.services = append(m.services, service)
|
||||
m.serviceByTag[tag] = service
|
||||
return nil
|
||||
}
|
||||
72
adapter/service/registry.go
Normal file
72
adapter/service/registry.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type ConstructorFunc[T any] func(ctx context.Context, logger log.ContextLogger, tag string, options T) (adapter.Service, error)
|
||||
|
||||
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
|
||||
registry.register(outboundType, func() any {
|
||||
return new(Options)
|
||||
}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.Service, error) {
|
||||
var options *Options
|
||||
if rawOptions != nil {
|
||||
options = rawOptions.(*Options)
|
||||
}
|
||||
return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
|
||||
})
|
||||
}
|
||||
|
||||
var _ adapter.ServiceRegistry = (*Registry)(nil)
|
||||
|
||||
type (
|
||||
optionsConstructorFunc func() any
|
||||
constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.Service, error)
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
access sync.Mutex
|
||||
optionsType map[string]optionsConstructorFunc
|
||||
constructor map[string]constructorFunc
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
optionsType: make(map[string]optionsConstructorFunc),
|
||||
constructor: make(map[string]constructorFunc),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
optionsConstructor, loaded := m.optionsType[outboundType]
|
||||
if !loaded {
|
||||
return nil, false
|
||||
}
|
||||
return optionsConstructor(), true
|
||||
}
|
||||
|
||||
func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Service, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
constructor, loaded := m.constructor[outboundType]
|
||||
if !loaded {
|
||||
return nil, E.New("outbound type not found: " + outboundType)
|
||||
}
|
||||
return constructor(ctx, logger, tag, options)
|
||||
}
|
||||
|
||||
func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.optionsType[outboundType] = optionsConstructor
|
||||
m.constructor[outboundType] = constructor
|
||||
}
|
||||
18
adapter/ssm.go
Normal file
18
adapter/ssm.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type ManagedSSMServer interface {
|
||||
Inbound
|
||||
SetTracker(tracker SSMTracker)
|
||||
UpdateUsers(users []string, uPSKs []string) error
|
||||
}
|
||||
|
||||
type SSMTracker interface {
|
||||
TrackConnection(conn net.Conn, metadata InboundContext) net.Conn
|
||||
TrackPacketConnection(conn N.PacketConn, metadata InboundContext) N.PacketConn
|
||||
}
|
||||
@@ -3,6 +3,6 @@ package adapter
|
||||
import "time"
|
||||
|
||||
type TimeService interface {
|
||||
Service
|
||||
SimpleLifecycle
|
||||
TimeFunc() func() time.Time
|
||||
}
|
||||
|
||||
123
box.go
123
box.go
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/certificate"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
@@ -34,22 +35,23 @@ import (
|
||||
"github.com/sagernet/sing/service/pause"
|
||||
)
|
||||
|
||||
var _ adapter.Service = (*Box)(nil)
|
||||
var _ adapter.SimpleLifecycle = (*Box)(nil)
|
||||
|
||||
type Box struct {
|
||||
createdAt time.Time
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
network *route.NetworkManager
|
||||
endpoint *endpoint.Manager
|
||||
inbound *inbound.Manager
|
||||
outbound *outbound.Manager
|
||||
dnsTransport *dns.TransportManager
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
services []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
createdAt time.Time
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
network *route.NetworkManager
|
||||
endpoint *endpoint.Manager
|
||||
inbound *inbound.Manager
|
||||
outbound *outbound.Manager
|
||||
service *boxService.Manager
|
||||
dnsTransport *dns.TransportManager
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
internalService []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
@@ -64,6 +66,7 @@ func Context(
|
||||
outboundRegistry adapter.OutboundRegistry,
|
||||
endpointRegistry adapter.EndpointRegistry,
|
||||
dnsTransportRegistry adapter.DNSTransportRegistry,
|
||||
serviceRegistry adapter.ServiceRegistry,
|
||||
) context.Context {
|
||||
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
|
||||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
|
||||
@@ -84,6 +87,10 @@ func Context(
|
||||
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
|
||||
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
|
||||
}
|
||||
if service.FromContext[adapter.ServiceRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
|
||||
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -99,6 +106,7 @@ func New(options Options) (*Box, error) {
|
||||
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
|
||||
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
||||
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
|
||||
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
|
||||
|
||||
if endpointRegistry == nil {
|
||||
return nil, E.New("missing endpoint registry in context")
|
||||
@@ -109,6 +117,12 @@ func New(options Options) (*Box, error) {
|
||||
if outboundRegistry == nil {
|
||||
return nil, E.New("missing outbound registry in context")
|
||||
}
|
||||
if dnsTransportRegistry == nil {
|
||||
return nil, E.New("missing DNS transport registry in context")
|
||||
}
|
||||
if serviceRegistry == nil {
|
||||
return nil, E.New("missing service registry in context")
|
||||
}
|
||||
|
||||
ctx = pause.WithDefaultManager(ctx)
|
||||
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
||||
@@ -142,7 +156,7 @@ func New(options Options) (*Box, error) {
|
||||
return nil, E.Cause(err, "create log factory")
|
||||
}
|
||||
|
||||
var services []adapter.LifecycleService
|
||||
var internalServices []adapter.LifecycleService
|
||||
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||
len(certificateOptions.Certificate) > 0 ||
|
||||
@@ -153,7 +167,7 @@ func New(options Options) (*Box, error) {
|
||||
return nil, err
|
||||
}
|
||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||
services = append(services, certificateStore)
|
||||
internalServices = append(internalServices, certificateStore)
|
||||
}
|
||||
|
||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||
@@ -162,10 +176,12 @@ func New(options Options) (*Box, error) {
|
||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
|
||||
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
||||
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
|
||||
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
||||
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
||||
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
||||
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
||||
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
||||
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
|
||||
@@ -280,6 +296,24 @@ func New(options Options) (*Box, error) {
|
||||
return nil, E.Cause(err, "initialize outbound[", i, "]")
|
||||
}
|
||||
}
|
||||
for i, serviceOptions := range options.Services {
|
||||
var tag string
|
||||
if serviceOptions.Tag != "" {
|
||||
tag = serviceOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = serviceManager.Create(
|
||||
ctx,
|
||||
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
serviceOptions.Type,
|
||||
serviceOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize service[", i, "]")
|
||||
}
|
||||
}
|
||||
outboundManager.Initialize(common.Must1(
|
||||
direct.NewOutbound(
|
||||
ctx,
|
||||
@@ -305,7 +339,7 @@ func New(options Options) (*Box, error) {
|
||||
if needCacheFile {
|
||||
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||
services = append(services, cacheFile)
|
||||
internalServices = append(internalServices, cacheFile)
|
||||
}
|
||||
if needClashAPI {
|
||||
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
||||
@@ -316,7 +350,7 @@ func New(options Options) (*Box, error) {
|
||||
}
|
||||
router.AppendTracker(clashServer)
|
||||
service.MustRegister[adapter.ClashServer](ctx, clashServer)
|
||||
services = append(services, clashServer)
|
||||
internalServices = append(internalServices, clashServer)
|
||||
}
|
||||
if needV2RayAPI {
|
||||
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
|
||||
@@ -325,7 +359,7 @@ func New(options Options) (*Box, error) {
|
||||
}
|
||||
if v2rayServer.StatsService() != nil {
|
||||
router.AppendTracker(v2rayServer.StatsService())
|
||||
services = append(services, v2rayServer)
|
||||
internalServices = append(internalServices, v2rayServer)
|
||||
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
||||
}
|
||||
}
|
||||
@@ -343,22 +377,23 @@ func New(options Options) (*Box, error) {
|
||||
WriteToSystem: ntpOptions.WriteToSystem,
|
||||
})
|
||||
timeService.TimeService = ntpService
|
||||
services = append(services, adapter.NewLifecycleService(ntpService, "ntp service"))
|
||||
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
|
||||
}
|
||||
return &Box{
|
||||
network: networkManager,
|
||||
endpoint: endpointManager,
|
||||
inbound: inboundManager,
|
||||
outbound: outboundManager,
|
||||
dnsTransport: dnsTransportManager,
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
services: services,
|
||||
done: make(chan struct{}),
|
||||
network: networkManager,
|
||||
endpoint: endpointManager,
|
||||
inbound: inboundManager,
|
||||
outbound: outboundManager,
|
||||
dnsTransport: dnsTransportManager,
|
||||
service: serviceManager,
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
internalService: internalServices,
|
||||
done: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -408,11 +443,11 @@ func (s *Box) preStart() error {
|
||||
if err != nil {
|
||||
return E.Cause(err, "start logger")
|
||||
}
|
||||
err = adapter.StartNamed(adapter.StartStateInitialize, s.services) // cache-file clash-api v2ray-api
|
||||
err = adapter.StartNamed(adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
|
||||
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -428,31 +463,27 @@ func (s *Box) start() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(adapter.StartStateStart, s.services)
|
||||
err = adapter.StartNamed(adapter.StartStateStart, s.internalService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.inbound.Start(adapter.StartStateStart)
|
||||
err = adapter.Start(adapter.StartStateStart, s.inbound, s.endpoint, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(adapter.StartStateStart, s.endpoint)
|
||||
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint)
|
||||
err = adapter.StartNamed(adapter.StartStatePostStart, s.internalService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(adapter.StartStatePostStart, s.services)
|
||||
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(adapter.StartStateStarted, s.services)
|
||||
err = adapter.StartNamed(adapter.StartStateStarted, s.internalService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -469,7 +500,7 @@ func (s *Box) Close() error {
|
||||
err := common.Close(
|
||||
s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
||||
)
|
||||
for _, lifecycleService := range s.services {
|
||||
for _, lifecycleService := range s.internalService {
|
||||
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", lifecycleService.Name())
|
||||
})
|
||||
|
||||
Submodule clients/android updated: 55f31c29bb...8354b78e5d
@@ -69,5 +69,5 @@ func preRun(cmd *cobra.Command, args []string) {
|
||||
configPaths = append(configPaths, "config.json")
|
||||
}
|
||||
globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))
|
||||
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry())
|
||||
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry(), include.ServiceRegistry())
|
||||
}
|
||||
|
||||
@@ -83,7 +83,6 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
||||
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
|
||||
//nolint:staticcheck
|
||||
strategy = C.DomainStrategy(dialOptions.DomainStrategy)
|
||||
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions)
|
||||
}
|
||||
server = dialOptions.DomainResolver.Server
|
||||
dnsQueryOptions = adapter.DNSQueryOptions{
|
||||
@@ -96,31 +95,22 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||
} else if options.DirectResolver {
|
||||
return nil, E.New("missing domain resolver for domain server address")
|
||||
} else {
|
||||
if defaultOptions.DomainResolver != "" {
|
||||
dnsQueryOptions = defaultOptions.DomainResolveOptions
|
||||
transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver)
|
||||
if !loaded {
|
||||
return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver)
|
||||
}
|
||||
dnsQueryOptions.Transport = transport
|
||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||
} else {
|
||||
transports := dnsTransport.Transports()
|
||||
if len(transports) < 2 {
|
||||
dnsQueryOptions.Transport = dnsTransport.Default()
|
||||
} else if options.NewDialer {
|
||||
return nil, E.New("missing domain resolver for domain server address")
|
||||
} else if !options.DirectOutbound {
|
||||
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
|
||||
}
|
||||
} else if defaultOptions.DomainResolver != "" {
|
||||
dnsQueryOptions = defaultOptions.DomainResolveOptions
|
||||
transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver)
|
||||
if !loaded {
|
||||
return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver)
|
||||
}
|
||||
if
|
||||
//nolint:staticcheck
|
||||
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
|
||||
//nolint:staticcheck
|
||||
dnsQueryOptions.Strategy = C.DomainStrategy(dialOptions.DomainStrategy)
|
||||
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions)
|
||||
dnsQueryOptions.Transport = transport
|
||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||
} else {
|
||||
transports := dnsTransport.Transports()
|
||||
if len(transports) < 2 {
|
||||
dnsQueryOptions.Transport = dnsTransport.Default()
|
||||
} else if options.NewDialer {
|
||||
return nil, E.New("missing domain resolver for domain server address")
|
||||
} else if !options.DirectOutbound {
|
||||
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
|
||||
}
|
||||
}
|
||||
dialer = NewResolveDialer(
|
||||
|
||||
@@ -31,18 +31,13 @@ func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.R
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
const header = "BitTorrent protocol"
|
||||
var protocol [19]byte
|
||||
var n int
|
||||
n, err = reader.Read(protocol[:])
|
||||
if string(protocol[:n]) != header[:n] {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
_, err = reader.Read(protocol[:])
|
||||
if err != nil {
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
if n < 19 {
|
||||
return ErrNeedMoreData
|
||||
if string(protocol[:]) != "BitTorrent protocol" {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
metadata.Protocol = C.ProtocolBitTorrent
|
||||
|
||||
@@ -32,27 +32,6 @@ func TestSniffBittorrent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSniffIncompleteBittorrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pkt, err := hex.DecodeString("13426974546f7272656e74")
|
||||
require.NoError(t, err)
|
||||
var metadata adapter.InboundContext
|
||||
err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt))
|
||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
||||
}
|
||||
|
||||
func TestSniffNotBittorrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pkt, err := hex.DecodeString("13426974546f7272656e75")
|
||||
require.NoError(t, err)
|
||||
var metadata adapter.InboundContext
|
||||
err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt))
|
||||
require.NotEmpty(t, err)
|
||||
require.NotErrorIs(t, err, sniff.ErrNeedMoreData)
|
||||
}
|
||||
|
||||
func TestSniffUTP(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -20,36 +20,22 @@ func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundCon
|
||||
if err != nil {
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
if length < 12 {
|
||||
if length == 0 {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
buffer := buf.NewSize(int(length))
|
||||
defer buffer.Release()
|
||||
var n int
|
||||
n, err = buffer.ReadFullFrom(reader, buffer.FreeLen())
|
||||
packet := buffer.Bytes()
|
||||
if n > 2 && packet[2]&0x80 != 0 { // QR
|
||||
return os.ErrInvalid
|
||||
}
|
||||
if n > 5 && packet[4] == 0 && packet[5] == 0 { // QDCOUNT
|
||||
return os.ErrInvalid
|
||||
}
|
||||
for i := 6; i < 10; i++ {
|
||||
// ANCOUNT, NSCOUNT
|
||||
if n > i && packet[i] != 0 {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
}
|
||||
_, err = buffer.ReadFullFrom(reader, buffer.FreeLen())
|
||||
if err != nil {
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
return DomainNameQuery(readCtx, metadata, packet)
|
||||
return DomainNameQuery(readCtx, metadata, buffer.Bytes())
|
||||
}
|
||||
|
||||
func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
||||
var msg mDNS.Msg
|
||||
err := msg.Unpack(packet)
|
||||
if err != nil || msg.Response || len(msg.Question) == 0 || len(msg.Answer) > 0 || len(msg.Ns) > 0 {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
metadata.Protocol = C.ProtocolDNS
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package sniff_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
@@ -22,32 +21,3 @@ func TestSniffDNS(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, C.ProtocolDNS, metadata.Protocol)
|
||||
}
|
||||
|
||||
func TestSniffStreamDNS(t *testing.T) {
|
||||
t.Parallel()
|
||||
query, err := hex.DecodeString("001e740701000001000000000000012a06676f6f676c6503636f6d0000010001")
|
||||
require.NoError(t, err)
|
||||
var metadata adapter.InboundContext
|
||||
err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, C.ProtocolDNS, metadata.Protocol)
|
||||
}
|
||||
|
||||
func TestSniffIncompleteStreamDNS(t *testing.T) {
|
||||
t.Parallel()
|
||||
query, err := hex.DecodeString("001e740701000001000000000000")
|
||||
require.NoError(t, err)
|
||||
var metadata adapter.InboundContext
|
||||
err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query))
|
||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
||||
}
|
||||
|
||||
func TestSniffNotStreamDNS(t *testing.T) {
|
||||
t.Parallel()
|
||||
query, err := hex.DecodeString("001e740701000000000000000000")
|
||||
require.NoError(t, err)
|
||||
var metadata adapter.InboundContext
|
||||
err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query))
|
||||
require.NotEmpty(t, err)
|
||||
require.NotErrorIs(t, err, sniff.ErrNeedMoreData)
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.
|
||||
}
|
||||
sniffError = E.Errors(sniffError, err)
|
||||
}
|
||||
if !errors.Is(sniffError, ErrNeedMoreData) {
|
||||
if !errors.Is(err, ErrNeedMoreData) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,10 @@ func SSH(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
||||
const sshPrefix = "SSH-2.0-"
|
||||
bReader := bufio.NewReader(reader)
|
||||
prefix, err := bReader.Peek(len(sshPrefix))
|
||||
if string(prefix[:]) != sshPrefix[:len(prefix)] {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
if err != nil {
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
} else if string(prefix) != sshPrefix {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
fistLine, _, err := bReader.ReadLine()
|
||||
if err != nil {
|
||||
|
||||
@@ -24,24 +24,3 @@ func TestSniffSSH(t *testing.T) {
|
||||
require.Equal(t, C.ProtocolSSH, metadata.Protocol)
|
||||
require.Equal(t, "dropbear", metadata.Client)
|
||||
}
|
||||
|
||||
func TestSniffIncompleteSSH(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pkt, err := hex.DecodeString("5353482d322e30")
|
||||
require.NoError(t, err)
|
||||
var metadata adapter.InboundContext
|
||||
err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt))
|
||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
||||
}
|
||||
|
||||
func TestSniffNotSSH(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pkt, err := hex.DecodeString("5353482d322e31")
|
||||
require.NoError(t, err)
|
||||
var metadata adapter.InboundContext
|
||||
err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt))
|
||||
require.NotEmpty(t, err)
|
||||
require.NotErrorIs(t, err, sniff.ErrNeedMoreData)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func (w *acmeWrapper) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) {
|
||||
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
|
||||
var acmeServer string
|
||||
switch options.Provider {
|
||||
case "", "letsencrypt":
|
||||
|
||||
@@ -11,6 +11,6 @@ import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) {
|
||||
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
|
||||
return nil, nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`)
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
@@ -48,10 +46,7 @@ func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions
|
||||
tlsConfig.EncryptedClientHelloConfigList = block.Bytes
|
||||
return &STDClientConfig{tlsConfig}, nil
|
||||
} else {
|
||||
return &STDECHClientConfig{
|
||||
STDClientConfig: STDClientConfig{tlsConfig},
|
||||
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
|
||||
}, nil
|
||||
return &STDECHClientConfig{STDClientConfig{tlsConfig}, service.FromContext[adapter.DNSRouter](ctx)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,28 +99,11 @@ func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
|
||||
|
||||
type STDECHClientConfig struct {
|
||||
STDClientConfig
|
||||
access sync.Mutex
|
||||
dnsRouter adapter.DNSRouter
|
||||
lastTTL time.Duration
|
||||
lastUpdate time.Time
|
||||
dnsRouter adapter.DNSRouter
|
||||
}
|
||||
|
||||
func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
|
||||
tlsConn, err := s.fetchAndHandshake(ctx, conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = tlsConn.HandshakeContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
func (s *STDECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
if len(s.config.EncryptedClientHelloConfigList) == 0 || s.lastTTL == 0 || time.Now().Sub(s.lastUpdate) > s.lastTTL {
|
||||
if len(s.config.EncryptedClientHelloConfigList) == 0 {
|
||||
message := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
RecursionDesired: true,
|
||||
@@ -155,8 +133,6 @@ func (s *STDECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Con
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "decode ECH config")
|
||||
}
|
||||
s.lastTTL = time.Duration(rr.Header().Ttl) * time.Second
|
||||
s.lastUpdate = time.Now()
|
||||
s.config.EncryptedClientHelloConfigList = echConfigList
|
||||
break match
|
||||
}
|
||||
@@ -167,11 +143,19 @@ func (s *STDECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Con
|
||||
return nil, E.New("no ECH config found in DNS records")
|
||||
}
|
||||
}
|
||||
return s.Client(conn)
|
||||
tlsConn, err := s.Client(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = tlsConn.HandshakeContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
func (s *STDECHClientConfig) Clone() Config {
|
||||
return &STDECHClientConfig{STDClientConfig: STDClientConfig{s.config.Clone()}, dnsRouter: s.dnsRouter, lastUpdate: s.lastUpdate}
|
||||
return &STDECHClientConfig{STDClientConfig{s.config.Clone()}, s.dnsRouter}
|
||||
}
|
||||
|
||||
func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) {
|
||||
|
||||
@@ -22,7 +22,7 @@ var errInsecureUnused = E.New("tls: insecure unused")
|
||||
type STDServerConfig struct {
|
||||
config *tls.Config
|
||||
logger log.Logger
|
||||
acmeService adapter.Service
|
||||
acmeService adapter.SimpleLifecycle
|
||||
certificate []byte
|
||||
key []byte
|
||||
certificatePath string
|
||||
@@ -165,7 +165,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
||||
return nil, nil
|
||||
}
|
||||
var tlsConfig *tls.Config
|
||||
var acmeService adapter.Service
|
||||
var acmeService adapter.SimpleLifecycle
|
||||
var err error
|
||||
if options.ACME != nil && len(options.ACME.Domain) > 0 {
|
||||
//nolint:staticcheck
|
||||
|
||||
@@ -25,6 +25,10 @@ const (
|
||||
TypeTUIC = "tuic"
|
||||
TypeHysteria2 = "hysteria2"
|
||||
TypeTailscale = "tailscale"
|
||||
TypeDERP = "derp"
|
||||
TypeDERPSTUN = "derp-stun"
|
||||
TypeResolved = "resolved"
|
||||
TypeSSMAPI = "ssm-api"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -243,9 +243,15 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
|
||||
domain = FqdnToDomain(domain)
|
||||
dnsName := dns.Fqdn(domain)
|
||||
if options.Strategy == C.DomainStrategyIPv4Only {
|
||||
var strategy C.DomainStrategy
|
||||
if options.LookupStrategy != C.DomainStrategyAsIS {
|
||||
strategy = options.LookupStrategy
|
||||
} else {
|
||||
strategy = options.Strategy
|
||||
}
|
||||
if strategy == C.DomainStrategyIPv4Only {
|
||||
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
|
||||
} else if options.Strategy == C.DomainStrategyIPv6Only {
|
||||
} else if strategy == C.DomainStrategyIPv6Only {
|
||||
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
|
||||
}
|
||||
var response4 []netip.Addr
|
||||
@@ -268,13 +274,10 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
|
||||
return nil
|
||||
})
|
||||
err := group.Run(ctx)
|
||||
if len(response4) > 0 || len(response6) > 0 {
|
||||
return sortAddresses(response4, response6, options.Strategy), nil
|
||||
} else if err != nil {
|
||||
if len(response4) == 0 && len(response6) == 0 {
|
||||
return nil, err
|
||||
} else {
|
||||
return nil, RcodeError(dns.RcodeNameError)
|
||||
}
|
||||
return sortAddresses(response4, response6, strategy), nil
|
||||
}
|
||||
|
||||
func (c *Client) ClearCache() {
|
||||
@@ -486,7 +489,7 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
|
||||
}
|
||||
|
||||
func MessageToAddresses(response *dns.Msg) ([]netip.Addr, error) {
|
||||
if response.Rcode != dns.RcodeSuccess {
|
||||
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
||||
return nil, RcodeError(response.Rcode)
|
||||
}
|
||||
addresses := make([]netip.Addr, 0, len(response.Answer))
|
||||
@@ -530,12 +533,26 @@ func transportTagFromContext(ctx context.Context) (string, bool) {
|
||||
return value, loaded
|
||||
}
|
||||
|
||||
func FixedResponseStatus(message *dns.Msg, rcode int) *dns.Msg {
|
||||
return &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: message.Id,
|
||||
Rcode: rcode,
|
||||
Response: true,
|
||||
},
|
||||
Question: message.Question,
|
||||
}
|
||||
}
|
||||
|
||||
func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, timeToLive uint32) *dns.Msg {
|
||||
response := dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: id,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Response: true,
|
||||
Id: id,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
RecursionDesired: true,
|
||||
RecursionAvailable: true,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Question: []dns.Question{question},
|
||||
}
|
||||
@@ -568,9 +585,12 @@ func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, tim
|
||||
func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToLive uint32) *dns.Msg {
|
||||
response := dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: id,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Response: true,
|
||||
Id: id,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
RecursionDesired: true,
|
||||
RecursionAvailable: true,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Question: []dns.Question{question},
|
||||
Answer: []dns.RR{
|
||||
@@ -591,9 +611,12 @@ func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToL
|
||||
func FixedResponseTXT(id uint16, question dns.Question, records []string, timeToLive uint32) *dns.Msg {
|
||||
response := dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: id,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Response: true,
|
||||
Id: id,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
RecursionDesired: true,
|
||||
RecursionAvailable: true,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Question: []dns.Question{question},
|
||||
Answer: []dns.RR{
|
||||
@@ -614,9 +637,12 @@ func FixedResponseTXT(id uint16, question dns.Question, records []string, timeTo
|
||||
func FixedResponseMX(id uint16, question dns.Question, records []*net.MX, timeToLive uint32) *dns.Msg {
|
||||
response := dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: id,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Response: true,
|
||||
Id: id,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
RecursionDesired: true,
|
||||
RecursionAvailable: true,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Question: []dns.Question{question},
|
||||
}
|
||||
|
||||
@@ -285,7 +285,12 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
||||
} else if errors.Is(err, ErrResponseRejected) {
|
||||
rejected = true
|
||||
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
|
||||
/*} else if responseCheck!= nil && errors.Is(err, RcodeError(mDNS.RcodeNameError)) {
|
||||
rejected = true
|
||||
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
|
||||
*/
|
||||
} else if len(message.Question) > 0 {
|
||||
rejected = true
|
||||
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
|
||||
} else {
|
||||
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
|
||||
@@ -332,7 +337,8 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
||||
r.logger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain))
|
||||
}
|
||||
} else if len(responseAddrs) == 0 {
|
||||
panic("unexpected empty result")
|
||||
r.logger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result")
|
||||
err = RcodeNameError
|
||||
}
|
||||
}
|
||||
responseAddrs, cached = r.client.LookupCache(domain, options.Strategy)
|
||||
|
||||
@@ -57,13 +57,17 @@ func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options o
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 853
|
||||
}
|
||||
return NewTLSRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions), transportDialer, serverAddr, tlsConfig), nil
|
||||
}
|
||||
|
||||
func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr, tlsConfig tls.Config) *TLSTransport {
|
||||
return &TLSTransport{
|
||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions),
|
||||
TransportAdapter: adapter,
|
||||
logger: logger,
|
||||
dialer: transportDialer,
|
||||
dialer: dialer,
|
||||
serverAddr: serverAddr,
|
||||
tlsConfig: tlsConfig,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TLSTransport) Start(stage adapter.StartStage) error {
|
||||
|
||||
@@ -117,7 +117,7 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
|
||||
conn.access.Unlock()
|
||||
defer func() {
|
||||
conn.access.Lock()
|
||||
delete(conn.callbacks, exMessage.Id)
|
||||
delete(conn.callbacks, messageId)
|
||||
conn.access.Unlock()
|
||||
}()
|
||||
rawMessage, err := exMessage.PackBuffer(buffer.FreeBytes())
|
||||
|
||||
@@ -2,22 +2,6 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.12.0-beta.5
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
### 1.11.8
|
||||
|
||||
* Improve `auto_redirect` **1**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
Now `auto_redirect` fixes compatibility issues between TUN and Docker bridge networks,
|
||||
see [Tun](/configuration/inbound/tun/#auto_redirect).
|
||||
|
||||
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._
|
||||
|
||||
#### 1.12.0-beta.3
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
@@ -9,10 +9,6 @@ and the data generated by the software is always on your device.
|
||||
|
||||
## Android
|
||||
|
||||
The broad package (App) visibility (QUERY_ALL_PACKAGES) permission
|
||||
is used to provide per-application proxy features for VPN,
|
||||
sing-box will not collect your app list.
|
||||
|
||||
If your configuration contains `wifi_ssid` or `wifi_bssid` routing rules,
|
||||
sing-box uses the location permission in the background
|
||||
to get information about the connected Wi-Fi network to make them work.
|
||||
|
||||
@@ -27,20 +27,19 @@ icon: material/alert-decagram
|
||||
|
||||
The type of the DNS server.
|
||||
|
||||
| Type | Format |
|
||||
|-----------------|---------------------------|
|
||||
| empty (default) | [Legacy](./legacy/) |
|
||||
| `local` | [Local](./local/) |
|
||||
| `hosts` | [Hosts](./hosts/) |
|
||||
| `tcp` | [TCP](./tcp/) |
|
||||
| `udp` | [UDP](./udp/) |
|
||||
| `tls` | [TLS](./tls/) |
|
||||
| `quic` | [QUIC](./quic/) |
|
||||
| `https` | [HTTPS](./https/) |
|
||||
| `h3` | [HTTP/3](./http3/) |
|
||||
| `dhcp` | [DHCP](./dhcp/) |
|
||||
| `fakeip` | [Fake IP](./fakeip/) |
|
||||
| `tailscale` | [Tailscale](./tailscale/) |
|
||||
| Type | Format |
|
||||
|-----------------|-----------------------------|
|
||||
| empty (default) | [Legacy](./legacy/) |
|
||||
| `tcp` | [TCP](./tcp/) |
|
||||
| `udp` | [UDP](./udp/) |
|
||||
| `tls` | [TLS](./tls/) |
|
||||
| `https` | [HTTPS](./https/) |
|
||||
| `quic` | [QUIC](./quic/) |
|
||||
| `h3` | [HTTP/3](./http3/) |
|
||||
| `predefined` | [Predefined](./predefined/) |
|
||||
| `dhcp` | [DHCP](./dhcp/) |
|
||||
| `fakeip` | [Fake IP](./fakeip/) |
|
||||
| `tailscale` | [Tailscale](./tailscale/) |
|
||||
|
||||
#### tag
|
||||
|
||||
|
||||
@@ -27,20 +27,19 @@ icon: material/alert-decagram
|
||||
|
||||
DNS 服务器的类型。
|
||||
|
||||
| 类型 | 格式 |
|
||||
|-----------------|---------------------------|
|
||||
| empty (default) | [Legacy](./legacy/) |
|
||||
| `local` | [Local](./local/) |
|
||||
| `hosts` | [Hosts](./hosts/) |
|
||||
| `tcp` | [TCP](./tcp/) |
|
||||
| `udp` | [UDP](./udp/) |
|
||||
| `tls` | [TLS](./tls/) |
|
||||
| `quic` | [QUIC](./quic/) |
|
||||
| `https` | [HTTPS](./https/) |
|
||||
| `h3` | [HTTP/3](./http3/) |
|
||||
| `dhcp` | [DHCP](./dhcp/) |
|
||||
| `fakeip` | [Fake IP](./fakeip/) |
|
||||
| `tailscale` | [Tailscale](./tailscale/) |
|
||||
| 类型 | 格式 |
|
||||
|-----------------|-----------------------------|
|
||||
| empty (default) | [Legacy](./legacy/) |
|
||||
| `tcp` | [TCP](./tcp/) |
|
||||
| `udp` | [UDP](./udp/) |
|
||||
| `tls` | [TLS](./tls/) |
|
||||
| `https` | [HTTPS](./https/) |
|
||||
| `quic` | [QUIC](./quic/) |
|
||||
| `h3` | [HTTP/3](./http3/) |
|
||||
| `predefined` | [Predefined](./predefined/) |
|
||||
| `dhcp` | [DHCP](./dhcp/) |
|
||||
| `fakeip` | [Fake IP](./fakeip/) |
|
||||
| `tailscale` | [Tailscale](./tailscale/) |
|
||||
|
||||
#### tag
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
| QUIC 客户端 | 类型 |
|
||||
|:------------------------:|:----------:|
|
||||
| Chromium/Cronet | `chromium` |
|
||||
| Chromium/Cronet | `chrimium` |
|
||||
| Safari/Apple Network API | `safari` |
|
||||
| Firefox / uquic firefox | `firefox` |
|
||||
| quic-go / uquic chrome | `quic-go` |
|
||||
| quic-go / uquic chrome | `quic-go` |
|
||||
@@ -206,7 +206,7 @@ Only take effect when `domain_strategy` or `network_strategy` is set.
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.12.0"
|
||||
|
||||
`domain_strategy` is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-outbound-domain-strategy-option-to-domain-resolver).
|
||||
`domain_strategy` is merged to [domain_resolver](#domain_resolver) in sing-box 1.12.0.
|
||||
|
||||
Available values: `prefer_ipv4`, `prefer_ipv6`, `ipv4_only`, `ipv6_only`.
|
||||
|
||||
|
||||
@@ -194,10 +194,6 @@ icon: material/new-box
|
||||
|
||||
#### domain_strategy
|
||||
|
||||
!!! failure "已在 sing-box 1.12.0 废弃"
|
||||
|
||||
`domain_strategy` 已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-outbound-domain-strategy-option-to-domain-resolver)。
|
||||
|
||||
可选值:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。
|
||||
|
||||
如果设置,域名将在请求发出之前解析为 IP。
|
||||
|
||||
@@ -9,56 +9,43 @@ icon: material/package
|
||||
=== ":material-debian: Debian / APT"
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/apt/keyrings &&
|
||||
sudo curl -fsSL https://sing-box.app/gpg.key -o /etc/apt/keyrings/sagernet.asc &&
|
||||
sudo chmod a+r /etc/apt/keyrings/sagernet.asc &&
|
||||
echo '
|
||||
Types: deb
|
||||
URIs: https://deb.sagernet.org/
|
||||
Suites: *
|
||||
Components: *
|
||||
Enabled: yes
|
||||
Signed-By: /etc/apt/keyrings/sagernet.asc
|
||||
' | sudo tee /etc/apt/sources.list.d/sagernet.sources &&
|
||||
sudo apt-get update &&
|
||||
sudo apt-get install sing-box # or sing-box-beta
|
||||
sudo curl -fsSL https://sing-box.app/gpg.key -o /etc/apt/keyrings/sagernet.asc
|
||||
sudo chmod a+r /etc/apt/keyrings/sagernet.asc
|
||||
echo "deb [arch=`dpkg --print-architecture` signed-by=/etc/apt/keyrings/sagernet.asc] https://deb.sagernet.org/ * *" | \
|
||||
sudo tee /etc/apt/sources.list.d/sagernet.list > /dev/null
|
||||
sudo apt-get update
|
||||
sudo apt-get install sing-box # or sing-box-beta
|
||||
```
|
||||
|
||||
=== ":material-redhat: Redhat / DNF 5"
|
||||
=== ":material-redhat: Redhat / DNF"
|
||||
|
||||
```bash
|
||||
sudo dnf config-manager addrepo --from-repofile=https://sing-box.app/sing-box.repo &&
|
||||
sudo dnf install sing-box # or sing-box-beta
|
||||
```
|
||||
|
||||
=== ":material-redhat: Redhat / DNF 4"
|
||||
|
||||
```bash
|
||||
sudo dnf config-manager --add-repo https://sing-box.app/sing-box.repo &&
|
||||
sudo dnf -y install dnf-plugins-core &&
|
||||
sudo dnf -y install dnf-plugins-core
|
||||
sudo dnf config-manager --add-repo https://sing-box.app/sing-box.repo
|
||||
sudo dnf install sing-box # or sing-box-beta
|
||||
```
|
||||
(This applies to any distribution that uses `dnf` as the package manager: Fedora, CentOS, even OpenSUSE with DNF installed.)
|
||||
|
||||
## :material-download-box: Manual Installation
|
||||
|
||||
The script download and install the latest package from GitHub releases
|
||||
for deb or rpm based Linux distributions, ArchLinux and OpenWrt.
|
||||
=== ":material-debian: Debian / DEB"
|
||||
|
||||
```shell
|
||||
curl -fsSL https://sing-box.app/install.sh | sh
|
||||
```
|
||||
```bash
|
||||
bash <(curl -fsSL https://sing-box.app/deb-install.sh)
|
||||
```
|
||||
|
||||
or latest beta:
|
||||
=== ":material-redhat: Redhat / RPM"
|
||||
|
||||
```shell
|
||||
curl -fsSL https://sing-box.app/install.sh | sh -s -- --beta
|
||||
```
|
||||
```bash
|
||||
bash <(curl -fsSL https://sing-box.app/rpm-install.sh)
|
||||
```
|
||||
(This applies to any distribution that uses `rpm` and `systemd`. Because of how `rpm` defines dependencies, if it installs, it probably works.)
|
||||
|
||||
or specific version:
|
||||
=== ":simple-archlinux: Archlinux / PKG"
|
||||
|
||||
```shell
|
||||
curl -fsSL https://sing-box.app/install.sh | sh -s -- --version <version>
|
||||
```
|
||||
```bash
|
||||
bash <(curl -fsSL https://sing-box.app/arch-install.sh)
|
||||
```
|
||||
|
||||
## :material-book-lock-open: Managed Installation
|
||||
|
||||
|
||||
@@ -9,55 +9,43 @@ icon: material/package
|
||||
=== ":material-debian: Debian / APT"
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/apt/keyrings &&
|
||||
sudo curl -fsSL https://sing-box.app/gpg.key -o /etc/apt/keyrings/sagernet.asc &&
|
||||
sudo chmod a+r /etc/apt/keyrings/sagernet.asc &&
|
||||
echo '
|
||||
Types: deb
|
||||
URIs: https://deb.sagernet.org/
|
||||
Suites: *
|
||||
Components: *
|
||||
Enabled: yes
|
||||
Signed-By: /etc/apt/keyrings/sagernet.asc
|
||||
' | sudo tee /etc/apt/sources.list.d/sagernet.sources &&
|
||||
sudo apt-get update &&
|
||||
sudo apt-get install sing-box # or sing-box-beta
|
||||
sudo curl -fsSL https://sing-box.app/gpg.key -o /etc/apt/keyrings/sagernet.asc
|
||||
sudo chmod a+r /etc/apt/keyrings/sagernet.asc
|
||||
echo "deb [arch=`dpkg --print-architecture` signed-by=/etc/apt/keyrings/sagernet.asc] https://deb.sagernet.org/ * *" | \
|
||||
sudo tee /etc/apt/sources.list.d/sagernet.list > /dev/null
|
||||
sudo apt-get update
|
||||
sudo apt-get install sing-box # or sing-box-beta
|
||||
```
|
||||
|
||||
=== ":material-redhat: Redhat / DNF 5"
|
||||
=== ":material-redhat: Redhat / DNF"
|
||||
|
||||
```bash
|
||||
sudo dnf config-manager addrepo --from-repofile=https://sing-box.app/sing-box.repo &&
|
||||
sudo dnf install sing-box # or sing-box-beta
|
||||
```
|
||||
|
||||
=== ":material-redhat: Redhat / DNF 4"
|
||||
|
||||
```bash
|
||||
sudo dnf config-manager --add-repo https://sing-box.app/sing-box.repo &&
|
||||
sudo dnf -y install dnf-plugins-core &&
|
||||
sudo dnf -y install dnf-plugins-core
|
||||
sudo dnf config-manager --add-repo https://sing-box.app/sing-box.repo
|
||||
sudo dnf install sing-box # or sing-box-beta
|
||||
```
|
||||
(这适用于任何使用 `dnf` 作为包管理器的发行版:Fedora、CentOS,甚至安装了 DNF 的 OpenSUSE。)
|
||||
|
||||
## :material-download-box: 手动安装
|
||||
|
||||
该脚本从 GitHub 发布中下载并安装最新的软件包,适用于基于 deb 或 rpm 的 Linux 发行版、ArchLinux 和 OpenWrt。
|
||||
=== ":material-debian: Debian / DEB"
|
||||
|
||||
```shell
|
||||
curl -fsSL https://sing-box.app/install.sh | sh
|
||||
```
|
||||
```bash
|
||||
bash <(curl -fsSL https://sing-box.app/deb-install.sh)
|
||||
```
|
||||
|
||||
或最新测试版:
|
||||
=== ":material-redhat: Redhat / RPM"
|
||||
|
||||
```shell
|
||||
curl -fsSL https://sing-box.app/install.sh | sh -s -- --beta
|
||||
```
|
||||
```bash
|
||||
bash <(curl -fsSL https://sing-box.app/rpm-install.sh)
|
||||
```
|
||||
(这适用于任何使用 `rpm` 和 `systemd` 的发行版。由于 `rpm` 定义依赖关系的方式,如果安装成功,就多半能用。)
|
||||
|
||||
或指定版本:
|
||||
=== ":simple-archlinux: Archlinux / PKG"
|
||||
|
||||
```shell
|
||||
curl -fsSL https://sing-box.app/install.sh | sh -s -- --version <version>
|
||||
```
|
||||
```bash
|
||||
bash <(curl -fsSL https://sing-box.app/arch-install.sh)
|
||||
```
|
||||
|
||||
## :material-book-lock-open: 托管安装
|
||||
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
download_beta=false
|
||||
download_version=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--beta)
|
||||
download_beta=true
|
||||
shift
|
||||
;;
|
||||
--version)
|
||||
shift
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Missing argument for --version"
|
||||
echo "Usage: $0 [--beta] [--version <version>]"
|
||||
exit 1
|
||||
fi
|
||||
download_version="$1"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1"
|
||||
echo "Usage: $0 [--beta] [--version <version>]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if command -v pacman >/dev/null 2>&1; then
|
||||
os="linux"
|
||||
arch=$(uname -m)
|
||||
package_suffix=".pkg.tar.zst"
|
||||
package_install="pacman -U --noconfirm"
|
||||
elif command -v dpkg >/dev/null 2>&1; then
|
||||
os="linux"
|
||||
arch=$(dpkg --print-architecture)
|
||||
package_suffix=".deb"
|
||||
package_install="dpkg -i"
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
os="linux"
|
||||
arch=$(uname -m)
|
||||
package_suffix=".rpm"
|
||||
package_install="dnf install -y"
|
||||
elif command -v rpm >/dev/null 2>&1; then
|
||||
os="linux"
|
||||
arch=$(uname -m)
|
||||
package_suffix=".rpm"
|
||||
package_install="rpm -i"
|
||||
elif command -v opkg >/dev/null 2>&1; then
|
||||
os="openwrt"
|
||||
. /etc/os-release
|
||||
arch="$OPENWRT_ARCH"
|
||||
package_suffix=".ipk"
|
||||
package_install="opkg update && opkg install"
|
||||
else
|
||||
echo "Missing supported package manager."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$download_version" ]; then
|
||||
if [ "$download_beta" != "true" ]; then
|
||||
if [ -n "$GITHUB_TOKEN" ]; then
|
||||
latest_release=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/SagerNet/sing-box/releases/latest)
|
||||
else
|
||||
latest_release=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases/latest)
|
||||
fi
|
||||
curl_exit_status=$?
|
||||
if [ $curl_exit_status -ne 0 ]; then
|
||||
exit $curl_exit_status
|
||||
fi
|
||||
if [ "$(echo "$latest_release" | grep tag_name | wc -l)" -eq 0 ]; then
|
||||
echo "$latest_release"
|
||||
exit 1
|
||||
fi
|
||||
download_version=$(echo "$latest_release" | grep tag_name | head -n 1 | awk -F: '{print $2}' | sed 's/[", v]//g')
|
||||
else
|
||||
if [ -n "$GITHUB_TOKEN" ]; then
|
||||
latest_release=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/SagerNet/sing-box/releases)
|
||||
else
|
||||
latest_release=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases)
|
||||
fi
|
||||
curl_exit_status=$?
|
||||
if [ $curl_exit_status -ne 0 ]; then
|
||||
exit $curl_exit_status
|
||||
fi
|
||||
if [ "$(echo "$latest_release" | grep tag_name | wc -l)" -eq 0 ]; then
|
||||
echo "$latest_release"
|
||||
exit 1
|
||||
fi
|
||||
download_version=$(echo "$latest_release" | grep tag_name | head -n 1 | awk -F: '{print $2}' | sed 's/[", v]//g')
|
||||
fi
|
||||
fi
|
||||
|
||||
package_name="sing-box_${download_version}_${os}_${arch}${package_suffix}"
|
||||
package_url="https://github.com/SagerNet/sing-box/releases/download/v${download_version}/${package_name}"
|
||||
|
||||
echo "Downloading $package_url"
|
||||
if [ -n "$GITHUB_TOKEN" ]; then
|
||||
curl --fail -Lo "$package_name" -H "Authorization: token ${GITHUB_TOKEN}" "$package_url"
|
||||
else
|
||||
curl --fail -Lo "$package_name" "$package_url"
|
||||
fi
|
||||
|
||||
curl_exit_status=$?
|
||||
if [ $curl_exit_status -ne 0 ]; then
|
||||
exit $curl_exit_status
|
||||
fi
|
||||
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
package_install="sudo $package_install"
|
||||
fi
|
||||
|
||||
echo "$package_install $package_name"
|
||||
sh -c "$package_install \"$package_name\""
|
||||
rm -f "$package_name"
|
||||
@@ -292,7 +292,7 @@ DNS servers are refactored for better performance and scalability.
|
||||
}
|
||||
],
|
||||
"fakeip": {
|
||||
"enabled": true,
|
||||
"enable": true,
|
||||
"inet4_range": "198.18.0.0/15",
|
||||
"inet6_range": "fc00::/18"
|
||||
}
|
||||
@@ -516,13 +516,13 @@ DNS servers are refactored for better performance and scalability.
|
||||
The legacy outbound DNS rules are deprecated and can be replaced by new domain resolver options.
|
||||
|
||||
!!! info "References"
|
||||
|
||||
|
||||
[DNS rule](/configuration/dns/rule/#outbound) /
|
||||
[Dial Fields](/configuration/shared/dial/#domain_resolver) /
|
||||
[Route](/configuration/route/#domain_resolver)
|
||||
|
||||
=== ":material-card-remove: Deprecated"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -556,8 +556,7 @@ The legacy outbound DNS rules are deprecated and can be replaced by new domain r
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "local"
|
||||
"type": "local"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -587,58 +586,6 @@ The legacy outbound DNS rules are deprecated and can be replaced by new domain r
|
||||
}
|
||||
```
|
||||
|
||||
### Migrate outbound domain strategy option to domain resolver
|
||||
|
||||
!!! info "References"
|
||||
|
||||
[Dial Fields](/configuration/shared/dial/#domain_strategy)
|
||||
|
||||
The `domain_strategy` option in Dial Fields has been deprecated and can be replaced with the new domain resolver option.
|
||||
|
||||
Note that due to the use of Dial Fields by some of the new DNS servers introduced in sing-box 1.12,
|
||||
some people mistakenly believe that `domain_strategy` is the same feature as in the legacy DNS servers.
|
||||
|
||||
=== ":material-card-remove: Deprecated"
|
||||
|
||||
```json
|
||||
{
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "socks",
|
||||
"server": "example.org",
|
||||
"server_port": 2080,
|
||||
"domain_strategy": "prefer_ipv4",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-multiple: New"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "local"
|
||||
}
|
||||
]
|
||||
},
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "socks",
|
||||
"server": "example.org",
|
||||
"server_port": 2080,
|
||||
"domain_resolver": {
|
||||
"server": "local",
|
||||
"strategy": "prefer_ipv4"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 1.11.0
|
||||
|
||||
### Migrate legacy special outbounds to rule actions
|
||||
|
||||
@@ -16,7 +16,7 @@ DNS 服务器已经重构。
|
||||
=== "Local"
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -28,9 +28,9 @@ DNS 服务器已经重构。
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -46,7 +46,7 @@ DNS 服务器已经重构。
|
||||
=== "TCP"
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -58,9 +58,9 @@ DNS 服务器已经重构。
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -77,7 +77,7 @@ DNS 服务器已经重构。
|
||||
=== "UDP"
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -89,9 +89,9 @@ DNS 服务器已经重构。
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -108,7 +108,7 @@ DNS 服务器已经重构。
|
||||
=== "TLS"
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -120,9 +120,9 @@ DNS 服务器已经重构。
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -139,7 +139,7 @@ DNS 服务器已经重构。
|
||||
=== "HTTPS"
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -151,9 +151,9 @@ DNS 服务器已经重构。
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -170,7 +170,7 @@ DNS 服务器已经重构。
|
||||
=== "QUIC"
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -182,9 +182,9 @@ DNS 服务器已经重构。
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -201,7 +201,7 @@ DNS 服务器已经重构。
|
||||
=== "HTTP3"
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -213,9 +213,9 @@ DNS 服务器已经重构。
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -232,7 +232,7 @@ DNS 服务器已经重构。
|
||||
=== "DHCP"
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -247,9 +247,9 @@ DNS 服务器已经重构。
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -269,7 +269,7 @@ DNS 服务器已经重构。
|
||||
=== "FakeIP"
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -292,16 +292,16 @@ DNS 服务器已经重构。
|
||||
}
|
||||
],
|
||||
"fakeip": {
|
||||
"enabled": true,
|
||||
"enable": true,
|
||||
"inet4_range": "198.18.0.0/15",
|
||||
"inet6_range": "fc00::/18"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -333,7 +333,7 @@ DNS 服务器已经重构。
|
||||
=== "RCode"
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -345,9 +345,9 @@ DNS 服务器已经重构。
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -368,7 +368,7 @@ DNS 服务器已经重构。
|
||||
=== "带有域名地址的服务器"
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -385,9 +385,9 @@ DNS 服务器已经重构。
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -410,7 +410,7 @@ DNS 服务器已经重构。
|
||||
=== "带有域策略的服务器"
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -434,9 +434,9 @@ DNS 服务器已经重构。
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -466,7 +466,7 @@ DNS 服务器已经重构。
|
||||
=== "带有客户端子网的服务器"
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -483,9 +483,9 @@ DNS 服务器已经重构。
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
@@ -556,8 +556,7 @@ DNS 服务器已经重构。
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "local"
|
||||
"type": "local"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -587,57 +586,6 @@ DNS 服务器已经重构。
|
||||
}
|
||||
```
|
||||
|
||||
### 迁移出站域名策略选项到域名解析器
|
||||
|
||||
拨号字段中的 `domain_strategy` 选项已被弃用,可以用新的域名解析器选项替代。
|
||||
|
||||
请注意,由于 sing-box 1.12 中引入的一些新 DNS 服务器使用了拨号字段,一些人错误地认为 `domain_strategy` 与旧 DNS 服务器中的功能相同。
|
||||
|
||||
!!! info "参考"
|
||||
|
||||
[拨号字段](/configuration/shared/dial/#domain_strategy)
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
```json
|
||||
{
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "socks",
|
||||
"server": "example.org",
|
||||
"server_port": 2080,
|
||||
"domain_strategy": "prefer_ipv4",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "local"
|
||||
}
|
||||
]
|
||||
},
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "socks",
|
||||
"server": "example.org",
|
||||
"server_port": 2080,
|
||||
"domain_resolver": {
|
||||
"server": "local",
|
||||
"strategy": "prefer_ipv4"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 1.11.0
|
||||
|
||||
### 迁移旧的特殊出站到规则动作
|
||||
@@ -653,7 +601,7 @@ DNS 服务器已经重构。
|
||||
=== "Block"
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"outbounds": [
|
||||
@@ -666,7 +614,7 @@ DNS 服务器已经重构。
|
||||
"rules": [
|
||||
{
|
||||
...,
|
||||
|
||||
|
||||
"outbound": "block"
|
||||
}
|
||||
]
|
||||
@@ -675,14 +623,14 @@ DNS 服务器已经重构。
|
||||
```
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
...,
|
||||
|
||||
|
||||
"action": "reject"
|
||||
}
|
||||
]
|
||||
@@ -693,13 +641,13 @@ DNS 服务器已经重构。
|
||||
=== "DNS"
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"inbound": [
|
||||
{
|
||||
...,
|
||||
|
||||
|
||||
"sniff": true
|
||||
}
|
||||
],
|
||||
@@ -719,9 +667,9 @@ DNS 服务器已经重构。
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"route": {
|
||||
@@ -1185,4 +1133,4 @@ sing-box 1.9.0 使 QueryFullProcessImageNameW 输出 Win32 路径(如 `C:\fold
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
|
||||
@@ -13,23 +12,14 @@ import (
|
||||
"github.com/sagernet/ws/wsutil"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
// API created by Clash.Meta
|
||||
|
||||
func (s *Server) setupMetaAPI(r chi.Router) {
|
||||
if s.logDebug {
|
||||
r := chi.NewRouter()
|
||||
r.Put("/gc", func(w http.ResponseWriter, r *http.Request) {
|
||||
debug.FreeOSMemory()
|
||||
})
|
||||
r.Mount("/", middleware.Profiler())
|
||||
}
|
||||
r.Get("/memory", memory(s.trafficManager))
|
||||
r.Mount("/group", groupRouter(s))
|
||||
r.Mount("/upgrade", upgradeRouter(s))
|
||||
}
|
||||
|
||||
type Memory struct {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package clashapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
func upgradeRouter(server *Server) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Post("/ui", updateExternalUI(server))
|
||||
return r
|
||||
}
|
||||
|
||||
func updateExternalUI(server *Server) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if server.externalUI == "" {
|
||||
render.Status(r, http.StatusNotFound)
|
||||
render.JSON(w, r, newError("external UI not enabled"))
|
||||
return
|
||||
}
|
||||
server.logger.Info("upgrading external UI")
|
||||
err := server.downloadExternalUI()
|
||||
if err != nil {
|
||||
server.logger.Error(E.Cause(err, "upgrade external ui"))
|
||||
render.Status(r, http.StatusInternalServerError)
|
||||
render.JSON(w, r, newError(err.Error()))
|
||||
return
|
||||
}
|
||||
server.logger.Info("updated external UI")
|
||||
render.JSON(w, r, render.M{"status": "ok"})
|
||||
}
|
||||
}
|
||||
@@ -49,8 +49,6 @@ type Server struct {
|
||||
httpServer *http.Server
|
||||
trafficManager *trafficontrol.Manager
|
||||
urlTestHistory adapter.URLTestHistoryStorage
|
||||
logDebug bool
|
||||
|
||||
mode string
|
||||
modeList []string
|
||||
modeUpdateHook chan<- struct{}
|
||||
@@ -76,7 +74,6 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
|
||||
Handler: chiRouter,
|
||||
},
|
||||
trafficManager: trafficManager,
|
||||
logDebug: logFactory.Level() >= log.LevelDebug,
|
||||
modeList: options.ModeList,
|
||||
externalController: options.ExternalController != "",
|
||||
externalUIDownloadURL: options.ExternalUIDownloadURL,
|
||||
|
||||
@@ -161,7 +161,6 @@ var OptionLegacyDNSFakeIPOptions = Note{
|
||||
Description: "legacy DNS fakeip options",
|
||||
DeprecatedVersion: "1.12.0",
|
||||
ScheduledVersion: "1.14.0",
|
||||
EnvName: "LEGACY_DNS_FAKEIP_OPTIONS",
|
||||
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-to-new-dns-server-formats",
|
||||
}
|
||||
|
||||
@@ -170,7 +169,6 @@ var OptionOutboundDNSRuleItem = Note{
|
||||
Description: "outbound DNS rule item",
|
||||
DeprecatedVersion: "1.12.0",
|
||||
ScheduledVersion: "1.14.0",
|
||||
EnvName: "OUTBOUND_DNS_RULE_ITEM",
|
||||
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-outbound-dns-rule-items-to-domain-resolver",
|
||||
}
|
||||
|
||||
@@ -179,7 +177,6 @@ var OptionMissingDomainResolver = Note{
|
||||
Description: "missing `route.default_domain_resolver` or `domain_resolver` in dial fields",
|
||||
DeprecatedVersion: "1.12.0",
|
||||
ScheduledVersion: "1.14.0",
|
||||
EnvName: "MISSING_DOMAIN_RESOLVER",
|
||||
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-outbound-dns-rule-items-to-domain-resolver",
|
||||
}
|
||||
|
||||
@@ -188,19 +185,9 @@ var OptionLegacyECHOptions = Note{
|
||||
Description: "legacy ECH options",
|
||||
DeprecatedVersion: "1.12.0",
|
||||
ScheduledVersion: "1.13.0",
|
||||
EnvName: "LEGACY_ECH_OPTIONS",
|
||||
MigrationLink: "https://sing-box.sagernet.org/deprecated/#legacy-ech-fields",
|
||||
}
|
||||
|
||||
var OptionLegacyDomainStrategyOptions = Note{
|
||||
Name: "legacy-domain-strategy-options",
|
||||
Description: "legacy domain strategy options",
|
||||
DeprecatedVersion: "1.12.0",
|
||||
ScheduledVersion: "1.14.0",
|
||||
EnvName: "LEGACY_DOMAIN_STRATEGY_OPTIONS",
|
||||
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-domain-strategy-options",
|
||||
}
|
||||
|
||||
var Options = []Note{
|
||||
OptionBadMatchSource,
|
||||
OptionGEOIP,
|
||||
@@ -217,5 +204,4 @@ var Options = []Note{
|
||||
OptionOutboundDNSRuleItem,
|
||||
OptionMissingDomainResolver,
|
||||
OptionLegacyECHOptions,
|
||||
OptionLegacyDomainStrategyOptions,
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func BaseContext(platformInterface PlatformInterface) context.Context {
|
||||
})
|
||||
}
|
||||
}
|
||||
return box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry)
|
||||
return box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry())
|
||||
}
|
||||
|
||||
func parseConfig(ctx context.Context, configContent string) (option.Options, error) {
|
||||
|
||||
14
go.mod
14
go.mod
@@ -6,9 +6,11 @@ require (
|
||||
github.com/anytls/sing-anytls v0.0.8
|
||||
github.com/caddyserver/certmagic v0.21.7
|
||||
github.com/cloudflare/circl v1.6.0
|
||||
github.com/coder/websocket v1.8.12
|
||||
github.com/cretz/bine v0.2.0
|
||||
github.com/go-chi/chi/v5 v5.2.1
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466
|
||||
github.com/gofrs/uuid/v5 v5.3.2
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905
|
||||
github.com/libdns/alidns v1.0.3
|
||||
@@ -26,18 +28,18 @@ require (
|
||||
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
|
||||
github.com/sagernet/quic-go v0.49.0-beta.1
|
||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
|
||||
github.com/sagernet/sing v0.6.8-0.20250425035333-84184da91a3a
|
||||
github.com/sagernet/sing v0.6.7-0.20250409030945-77e2a1bb577c
|
||||
github.com/sagernet/sing-mux v0.3.1
|
||||
github.com/sagernet/sing-quic v0.4.1-0.20250423030647-0eb05f373a76
|
||||
github.com/sagernet/sing-quic v0.4.1
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.0
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056
|
||||
github.com/sagernet/sing-tun v0.6.5-0.20250412112220-15069fc1c20a
|
||||
github.com/sagernet/sing-vmess v0.2.1
|
||||
github.com/sagernet/sing-vmess v0.2.0
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
|
||||
github.com/sagernet/tailscale v1.80.3-mod.4
|
||||
github.com/sagernet/tailscale v1.80.3-mod.2
|
||||
github.com/sagernet/utls v1.6.7
|
||||
github.com/sagernet/wireguard-go v0.0.1-beta.7
|
||||
github.com/sagernet/wireguard-go v0.0.1-beta.5
|
||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
@@ -66,7 +68,6 @@ require (
|
||||
github.com/bits-and-blooms/bitset v1.13.0 // indirect
|
||||
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/coder/websocket v1.8.12 // indirect
|
||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
|
||||
@@ -80,7 +81,6 @@ require (
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
|
||||
20
go.sum
20
go.sum
@@ -178,12 +178,12 @@ github.com/sagernet/quic-go v0.49.0-beta.1/go.mod h1:uesWD1Ihrldq1M3XtjuEvIUqi8W
|
||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
|
||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
|
||||
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
||||
github.com/sagernet/sing v0.6.8-0.20250425035333-84184da91a3a h1:oE67hmp5rzLlE6clE7FpK4Hg6yLXsa1Zu3A01vcazb0=
|
||||
github.com/sagernet/sing v0.6.8-0.20250425035333-84184da91a3a/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing v0.6.7-0.20250409030945-77e2a1bb577c h1:Zi+WR7f9SQ96yNHmyxj42BtaVb3kTouQ8bQLBHReTSI=
|
||||
github.com/sagernet/sing v0.6.7-0.20250409030945-77e2a1bb577c/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-mux v0.3.1 h1:kvCc8HyGAskDHDQ0yQvoTi/7J4cZPB/VJMsAM3MmdQI=
|
||||
github.com/sagernet/sing-mux v0.3.1/go.mod h1:Mkdz8LnDstthz0HWuA/5foncnDIdcNN5KZ6AdJX+x78=
|
||||
github.com/sagernet/sing-quic v0.4.1-0.20250423030647-0eb05f373a76 h1:iwpCX6H3nZEOGUGwx0q5azcgYOA9f6v9YssihXoRKHk=
|
||||
github.com/sagernet/sing-quic v0.4.1-0.20250423030647-0eb05f373a76/go.mod h1:tqPa0/Wqa19MkkSlKVZZX5sHxtiDR9BROcn4ufcbVdY=
|
||||
github.com/sagernet/sing-quic v0.4.1 h1:pxlMa4efZu/M07RgGagNNDDyl6ZUwpmNUjRTpgHOWK4=
|
||||
github.com/sagernet/sing-quic v0.4.1/go.mod h1:tqPa0/Wqa19MkkSlKVZZX5sHxtiDR9BROcn4ufcbVdY=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg=
|
||||
@@ -192,16 +192,16 @@ github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056 h1:GFNJQ
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056/go.mod h1:HyacBPIFiKihJQR8LQp56FM4hBtd/7MZXnRxxQIOPsc=
|
||||
github.com/sagernet/sing-tun v0.6.5-0.20250412112220-15069fc1c20a h1:2aLxZFD2HPCLrnFGpH+KBuPqMOk0cuaDE2dgEvANuMk=
|
||||
github.com/sagernet/sing-tun v0.6.5-0.20250412112220-15069fc1c20a/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
|
||||
github.com/sagernet/sing-vmess v0.2.1 h1:6izHC2+B68aQCxTagki6eZZc+g5eh4dYwxOV5a2Lhug=
|
||||
github.com/sagernet/sing-vmess v0.2.1/go.mod h1:jDAZ0A0St1zVRkyvhAPRySOFfhC+4SQtO5VYyeFotgA=
|
||||
github.com/sagernet/sing-vmess v0.2.0 h1:pCMGUXN2k7RpikQV65/rtXtDHzb190foTfF9IGTMZrI=
|
||||
github.com/sagernet/sing-vmess v0.2.0/go.mod h1:jDAZ0A0St1zVRkyvhAPRySOFfhC+4SQtO5VYyeFotgA=
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
|
||||
github.com/sagernet/tailscale v1.80.3-mod.4 h1:9UgYq8m9mwX5dbTbueVxbRh+bq7AayxemJGM2PkJQnE=
|
||||
github.com/sagernet/tailscale v1.80.3-mod.4/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
|
||||
github.com/sagernet/tailscale v1.80.3-mod.2 h1:hT0CI74q727EuCcgQ+T4pvon8V0aoi4vTAxah7GsNMQ=
|
||||
github.com/sagernet/tailscale v1.80.3-mod.2/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
|
||||
github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8=
|
||||
github.com/sagernet/utls v1.6.7/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM=
|
||||
github.com/sagernet/wireguard-go v0.0.1-beta.7 h1:ltgBwYHfr+9Wz1eG59NiWnHrYEkDKHG7otNZvu85DXI=
|
||||
github.com/sagernet/wireguard-go v0.0.1-beta.7/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo=
|
||||
github.com/sagernet/wireguard-go v0.0.1-beta.5 h1:aBEsxJUMEONwOZqKPIkuAcv4zJV5p6XlzEN04CF0FXc=
|
||||
github.com/sagernet/wireguard-go v0.0.1-beta.5/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo=
|
||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
|
||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/adapter/service"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport"
|
||||
@@ -33,6 +34,8 @@ import (
|
||||
"github.com/sagernet/sing-box/protocol/tun"
|
||||
"github.com/sagernet/sing-box/protocol/vless"
|
||||
"github.com/sagernet/sing-box/protocol/vmess"
|
||||
"github.com/sagernet/sing-box/service/resolved"
|
||||
"github.com/sagernet/sing-box/service/ssmapi"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
@@ -110,6 +113,7 @@ func DNSTransportRegistry() *dns.TransportRegistry {
|
||||
hosts.RegisterTransport(registry)
|
||||
local.RegisterTransport(registry)
|
||||
fakeip.RegisterTransport(registry)
|
||||
resolved.RegisterTransport(registry)
|
||||
|
||||
registerQUICTransports(registry)
|
||||
registerDHCPTransport(registry)
|
||||
@@ -118,6 +122,17 @@ func DNSTransportRegistry() *dns.TransportRegistry {
|
||||
return registry
|
||||
}
|
||||
|
||||
func ServiceRegistry() *service.Registry {
|
||||
registry := service.NewRegistry()
|
||||
|
||||
resolved.RegisterService(registry)
|
||||
ssmapi.RegisterService(registry)
|
||||
|
||||
registerDERPService(registry)
|
||||
|
||||
return registry
|
||||
}
|
||||
|
||||
func registerStubForRemovedInbounds(registry *inbound.Registry) {
|
||||
inbound.Register[option.ShadowsocksInboundOptions](registry, C.TypeShadowsocksR, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) {
|
||||
return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0")
|
||||
|
||||
@@ -4,8 +4,10 @@ package include
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/protocol/tailscale"
|
||||
"github.com/sagernet/sing-box/service/derp"
|
||||
)
|
||||
|
||||
func registerTailscaleEndpoint(registry *endpoint.Registry) {
|
||||
@@ -15,3 +17,8 @@ func registerTailscaleEndpoint(registry *endpoint.Registry) {
|
||||
func registerTailscaleTransport(registry *dns.TransportRegistry) {
|
||||
tailscale.RegistryTransport(registry)
|
||||
}
|
||||
|
||||
func registerDERPService(registry *service.Registry) {
|
||||
derp.Register(registry)
|
||||
derp.RegisterSTUN(registry)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/service"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -25,3 +26,12 @@ func registerTailscaleTransport(registry *dns.TransportRegistry) {
|
||||
return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`)
|
||||
})
|
||||
}
|
||||
|
||||
func registerDERPService(registry *service.Registry) {
|
||||
service.Register[option.DERPServiceOptions](registry, C.TypeDERP, func(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPServiceOptions) (adapter.Service, error) {
|
||||
return nil, E.New(`DERP is not included in this build, rebuild with -tags with_tailscale`)
|
||||
})
|
||||
service.Register[option.DERPSTUNServiceOptions](registry, C.TypeDERP, func(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPSTUNServiceOptions) (adapter.Service, error) {
|
||||
return nil, E.New(`STUN (DERP) is not included in this build, rebuild with -tags with_tailscale`)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -121,7 +121,6 @@ type LegacyDNSFakeIPOptions struct {
|
||||
type DNSTransportOptionsRegistry interface {
|
||||
CreateOptions(transportType string) (any, bool)
|
||||
}
|
||||
|
||||
type _DNSServerOptions struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
|
||||
@@ -32,11 +32,11 @@ func (h *Endpoint) UnmarshalJSONContext(ctx context.Context, content []byte) err
|
||||
}
|
||||
registry := service.FromContext[EndpointOptionsRegistry](ctx)
|
||||
if registry == nil {
|
||||
return E.New("missing Endpoint fields registry in context")
|
||||
return E.New("missing endpoint fields registry in context")
|
||||
}
|
||||
options, loaded := registry.CreateOptions(h.Type)
|
||||
if !loaded {
|
||||
return E.New("unknown inbound type: ", h.Type)
|
||||
return E.New("unknown endpoint type: ", h.Type)
|
||||
}
|
||||
err = badjson.UnmarshallExcludedContext(ctx, content, (*_Endpoint)(h), options)
|
||||
if err != nil {
|
||||
|
||||
@@ -34,7 +34,7 @@ func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) erro
|
||||
}
|
||||
registry := service.FromContext[InboundOptionsRegistry](ctx)
|
||||
if registry == nil {
|
||||
return E.New("missing Inbound fields registry in context")
|
||||
return E.New("missing inbound fields registry in context")
|
||||
}
|
||||
options, loaded := registry.CreateOptions(h.Type)
|
||||
if !loaded {
|
||||
|
||||
@@ -19,6 +19,7 @@ type _Options struct {
|
||||
Inbounds []Inbound `json:"inbounds,omitempty"`
|
||||
Outbounds []Outbound `json:"outbounds,omitempty"`
|
||||
Route *RouteOptions `json:"route,omitempty"`
|
||||
Services []Service `json:"services,omitempty"`
|
||||
Experimental *ExperimentalOptions `json:"experimental,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
49
option/resolved.go
Normal file
49
option/resolved.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
)
|
||||
|
||||
type _ResolvedServiceOptions struct {
|
||||
ListenOptions
|
||||
}
|
||||
|
||||
type ResolvedServiceOptions _ResolvedServiceOptions
|
||||
|
||||
func (r ResolvedServiceOptions) MarshalJSONContext(ctx context.Context) ([]byte, error) {
|
||||
if r.Listen != nil && netip.Addr(*r.Listen) == (netip.AddrFrom4([4]byte{127, 0, 0, 53})) {
|
||||
r.Listen = nil
|
||||
}
|
||||
if r.ListenPort == 53 {
|
||||
r.ListenPort = 0
|
||||
}
|
||||
return json.MarshalContext(ctx, (*_ResolvedServiceOptions)(&r))
|
||||
}
|
||||
|
||||
func (r *ResolvedServiceOptions) UnmarshalJSONContext(ctx context.Context, bytes []byte) error {
|
||||
err := json.UnmarshalContextDisallowUnknownFields(ctx, bytes, (*_ResolvedServiceOptions)(r))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Listen == nil {
|
||||
r.Listen = (*badoption.Addr)(common.Ptr(netip.AddrFrom4([4]byte{127, 0, 0, 53})))
|
||||
}
|
||||
if r.ListenPort == 0 {
|
||||
r.ListenPort = 53
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ResolvedDNSServerOptions struct {
|
||||
Service string `json:"Service"`
|
||||
AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"`
|
||||
// NDots int `json:"ndots,omitempty"`
|
||||
// Timeout badoption.Duration `json:"timeout,omitempty"`
|
||||
// Attempts int `json:"attempts,omitempty"`
|
||||
// Rotate bool `json:"rotate,omitempty"`
|
||||
}
|
||||
47
option/service.go
Normal file
47
option/service.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
type ServiceOptionsRegistry interface {
|
||||
CreateOptions(serviceType string) (any, bool)
|
||||
}
|
||||
|
||||
type _Service struct {
|
||||
Type string `json:"type"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Options any `json:"-"`
|
||||
}
|
||||
|
||||
type Service _Service
|
||||
|
||||
func (h *Service) MarshalJSONContext(ctx context.Context) ([]byte, error) {
|
||||
return badjson.MarshallObjectsContext(ctx, (*_Service)(h), h.Options)
|
||||
}
|
||||
|
||||
func (h *Service) UnmarshalJSONContext(ctx context.Context, content []byte) error {
|
||||
err := json.UnmarshalContext(ctx, content, (*_Service)(h))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
registry := service.FromContext[ServiceOptionsRegistry](ctx)
|
||||
if registry == nil {
|
||||
return E.New("missing service fields registry in context")
|
||||
}
|
||||
options, loaded := registry.CreateOptions(h.Type)
|
||||
if !loaded {
|
||||
return E.New("unknown inbound type: ", h.Type)
|
||||
}
|
||||
err = badjson.UnmarshallExcludedContext(ctx, content, (*_Service)(h), options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.Options = options
|
||||
return nil
|
||||
}
|
||||
@@ -8,6 +8,7 @@ type ShadowsocksInboundOptions struct {
|
||||
Users []ShadowsocksUser `json:"users,omitempty"`
|
||||
Destinations []ShadowsocksDestination `json:"destinations,omitempty"`
|
||||
Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"`
|
||||
Managed bool `json:"managed,omitempty"`
|
||||
}
|
||||
|
||||
type ShadowsocksUser struct {
|
||||
|
||||
11
option/ssmapi.go
Normal file
11
option/ssmapi.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
)
|
||||
|
||||
type SSMAPIServiceOptions struct {
|
||||
ListenOptions
|
||||
InboundTLSOptionsContainer
|
||||
Servers *badjson.TypedMap[string, string] `json:"servers"`
|
||||
}
|
||||
@@ -2,6 +2,12 @@ package option
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"reflect"
|
||||
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
)
|
||||
|
||||
type TailscaleEndpointOptions struct {
|
||||
@@ -22,3 +28,59 @@ type TailscaleDNSServerOptions struct {
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"`
|
||||
}
|
||||
|
||||
type DERPServiceOptions struct {
|
||||
ListenOptions
|
||||
InboundTLSOptionsContainer
|
||||
ConfigPath string `json:"config_path,omitempty"`
|
||||
VerifyClientEndpoint badoption.Listable[string] `json:"verify_client_endpoint,omitempty"`
|
||||
VerifyClientURL badoption.Listable[*DERPVerifyClientURLOptions] `json:"verify_client_url,omitempty"`
|
||||
MeshWith badoption.Listable[*DERPMeshOptions] `json:"mesh_with,omitempty"`
|
||||
MeshPSK string `json:"mesh_psk,omitempty"`
|
||||
MeshPSKFile string `json:"mesh_psk_file,omitempty"`
|
||||
DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"`
|
||||
}
|
||||
|
||||
type _DERPVerifyClientURLOptions struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
DialerOptions
|
||||
}
|
||||
|
||||
type DERPVerifyClientURLOptions _DERPVerifyClientURLOptions
|
||||
|
||||
func (d DERPVerifyClientURLOptions) ServerIsDomain() bool {
|
||||
verifyURL, err := url.Parse(d.URL)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return M.IsDomainName(verifyURL.Host)
|
||||
}
|
||||
|
||||
func (d DERPVerifyClientURLOptions) MarshalJSON() ([]byte, error) {
|
||||
if reflect.DeepEqual(d, _DERPVerifyClientURLOptions{}) {
|
||||
return json.Marshal(d.URL)
|
||||
} else {
|
||||
return json.Marshal(_DERPVerifyClientURLOptions(d))
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DERPVerifyClientURLOptions) UnmarshalJSON(bytes []byte) error {
|
||||
var stringValue string
|
||||
err := json.Unmarshal(bytes, &stringValue)
|
||||
if err == nil {
|
||||
d.URL = stringValue
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(bytes, (*_DERPVerifyClientURLOptions)(d))
|
||||
}
|
||||
|
||||
type DERPMeshOptions struct {
|
||||
ServerOptions
|
||||
Host string `json:"host,omitempty"`
|
||||
OutboundTLSOptionsContainer
|
||||
DialerOptions
|
||||
}
|
||||
|
||||
type DERPSTUNServiceOptions struct {
|
||||
ListenOptions
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ func (i *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a
|
||||
case 2:
|
||||
destination.Addr = i.overrideDestination.Addr
|
||||
case 3:
|
||||
destination.Port = i.overrideDestination.Port
|
||||
destination.Port = metadata.Destination.Port
|
||||
}
|
||||
metadata.Destination = destination
|
||||
if i.overrideOption != 0 {
|
||||
|
||||
@@ -396,16 +396,12 @@ func (g *URLTestGroup) urlTest(ctx context.Context, force bool) (map[string]uint
|
||||
func (g *URLTestGroup) performUpdateCheck() {
|
||||
var updated bool
|
||||
if outbound, exists := g.Select(N.NetworkTCP); outbound != nil && (g.selectedOutboundTCP == nil || (exists && outbound != g.selectedOutboundTCP)) {
|
||||
if g.selectedOutboundTCP != nil {
|
||||
updated = true
|
||||
}
|
||||
g.selectedOutboundTCP = outbound
|
||||
updated = true
|
||||
}
|
||||
if outbound, exists := g.Select(N.NetworkUDP); outbound != nil && (g.selectedOutboundUDP == nil || (exists && outbound != g.selectedOutboundUDP)) {
|
||||
if g.selectedOutboundUDP != nil {
|
||||
updated = true
|
||||
}
|
||||
g.selectedOutboundUDP = outbound
|
||||
updated = true
|
||||
}
|
||||
if updated {
|
||||
g.interruptGroup.Interrupt(g.interruptExternalConnections)
|
||||
|
||||
@@ -32,8 +32,10 @@ func RegisterInbound(registry *inbound.Registry) {
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) {
|
||||
if len(options.Users) > 0 && len(options.Destinations) > 0 {
|
||||
return nil, E.New("users and destinations options must not be combined")
|
||||
} else if options.Managed && (len(options.Users) > 0 || len(options.Destinations) > 0) {
|
||||
return nil, E.New("users and destinations options are not supported in managed servers")
|
||||
}
|
||||
if len(options.Users) > 0 {
|
||||
if len(options.Users) > 0 || options.Managed {
|
||||
return newMultiInbound(ctx, router, logger, tag, options)
|
||||
} else if len(options.Destinations) > 0 {
|
||||
return newRelayInbound(ctx, router, logger, tag, options)
|
||||
|
||||
@@ -28,7 +28,10 @@ import (
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
)
|
||||
|
||||
var _ adapter.TCPInjectableInbound = (*MultiInbound)(nil)
|
||||
var (
|
||||
_ adapter.TCPInjectableInbound = (*MultiInbound)(nil)
|
||||
_ adapter.ManagedSSMServer = (*MultiInbound)(nil)
|
||||
)
|
||||
|
||||
type MultiInbound struct {
|
||||
inbound.Adapter
|
||||
@@ -38,6 +41,7 @@ type MultiInbound struct {
|
||||
listener *listener.Listener
|
||||
service shadowsocks.MultiService[int]
|
||||
users []option.ShadowsocksUser
|
||||
tracker adapter.SSMTracker
|
||||
}
|
||||
|
||||
func newMultiInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*MultiInbound, error) {
|
||||
@@ -79,13 +83,15 @@ func newMultiInbound(ctx context.Context, router adapter.Router, logger log.Cont
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = service.UpdateUsersWithPasswords(common.MapIndexed(options.Users, func(index int, user option.ShadowsocksUser) int {
|
||||
return index
|
||||
}), common.Map(options.Users, func(user option.ShadowsocksUser) string {
|
||||
return user.Password
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if len(options.Users) > 0 {
|
||||
err = service.UpdateUsersWithPasswords(common.MapIndexed(options.Users, func(index int, user option.ShadowsocksUser) int {
|
||||
return index
|
||||
}), common.Map(options.Users, func(user option.ShadowsocksUser) string {
|
||||
return user.Password
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
inbound.service = service
|
||||
inbound.users = options.Users
|
||||
@@ -112,6 +118,25 @@ func (h *MultiInbound) Close() error {
|
||||
return h.listener.Close()
|
||||
}
|
||||
|
||||
func (h *MultiInbound) SetTracker(tracker adapter.SSMTracker) {
|
||||
h.tracker = tracker
|
||||
}
|
||||
|
||||
func (h *MultiInbound) UpdateUsers(users []string, uPSKs []string) error {
|
||||
err := h.service.UpdateUsersWithPasswords(common.MapIndexed(users, func(index int, user string) int {
|
||||
return index
|
||||
}), uPSKs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.users = common.Map(users, func(user string) option.ShadowsocksUser {
|
||||
return option.ShadowsocksUser{
|
||||
Name: user,
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
func (h *MultiInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata))
|
||||
@@ -151,6 +176,9 @@ func (h *MultiInbound) newConnection(ctx context.Context, conn net.Conn, metadat
|
||||
metadata.InboundDetour = h.listener.ListenOptions().Detour
|
||||
//nolint:staticcheck
|
||||
metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
|
||||
if h.tracker != nil {
|
||||
conn = h.tracker.TrackConnection(conn, metadata)
|
||||
}
|
||||
return h.router.RouteConnection(ctx, conn, metadata)
|
||||
}
|
||||
|
||||
@@ -174,6 +202,9 @@ func (h *MultiInbound) newPacketConnection(ctx context.Context, conn N.PacketCon
|
||||
metadata.InboundDetour = h.listener.ListenOptions().Detour
|
||||
//nolint:staticcheck
|
||||
metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
|
||||
if h.tracker != nil {
|
||||
conn = h.tracker.TrackPacketConnection(conn, metadata)
|
||||
}
|
||||
return h.router.RoutePacketConnection(ctx, conn, metadata)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
@@ -192,29 +191,9 @@ func (s *Outbound) DialContext(ctx context.Context, network string, destination
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := client.Dial(network, destination.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &chanConnWrapper{Conn: conn}, nil
|
||||
return client.Dial(network, destination.String())
|
||||
}
|
||||
|
||||
func (s *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
type chanConnWrapper struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (c *chanConnWrapper) SetDeadline(t time.Time) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (c *chanConnWrapper) SetReadDeadline(t time.Time) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (c *chanConnWrapper) SetWriteDeadline(t time.Time) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@ func RegisterEndpoint(registry *endpoint.Registry) {
|
||||
endpoint.Register[option.WireGuardEndpointOptions](registry, C.TypeWireGuard, NewEndpoint)
|
||||
}
|
||||
|
||||
var (
|
||||
_ adapter.Endpoint = (*Endpoint)(nil)
|
||||
_ adapter.InterfaceUpdateListener = (*Endpoint)(nil)
|
||||
)
|
||||
|
||||
type Endpoint struct {
|
||||
endpoint.Adapter
|
||||
ctx context.Context
|
||||
@@ -122,6 +127,10 @@ func (w *Endpoint) Close() error {
|
||||
return w.endpoint.Close()
|
||||
}
|
||||
|
||||
func (w *Endpoint) InterfaceUpdated() {
|
||||
w.endpoint.BindUpdate()
|
||||
}
|
||||
|
||||
func (w *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr) error {
|
||||
return w.router.PreMatch(adapter.InboundContext{
|
||||
Inbound: w.Tag(),
|
||||
|
||||
@@ -25,6 +25,11 @@ func RegisterOutbound(registry *outbound.Registry) {
|
||||
outbound.Register[option.LegacyWireGuardOutboundOptions](registry, C.TypeWireGuard, NewOutbound)
|
||||
}
|
||||
|
||||
var (
|
||||
_ adapter.Endpoint = (*Endpoint)(nil)
|
||||
_ adapter.InterfaceUpdateListener = (*Endpoint)(nil)
|
||||
)
|
||||
|
||||
type Outbound struct {
|
||||
outbound.Adapter
|
||||
ctx context.Context
|
||||
@@ -126,6 +131,10 @@ func (o *Outbound) Close() error {
|
||||
return o.endpoint.Close()
|
||||
}
|
||||
|
||||
func (o *Outbound) InterfaceUpdated() {
|
||||
o.endpoint.BindUpdate()
|
||||
}
|
||||
|
||||
func (o *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
switch network {
|
||||
case N.NetworkTCP:
|
||||
|
||||
13
release/config/openwrt.init
Executable file → Normal file
13
release/config/openwrt.init
Executable file → Normal file
@@ -1,27 +1,26 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
USE_PROCD=1
|
||||
START=99
|
||||
PROG="/usr/bin/sing-box"
|
||||
|
||||
start_service() {
|
||||
config_load "sing-box"
|
||||
|
||||
local enabled config_file working_directory
|
||||
local log_stderr
|
||||
local log_stdout log_stderr
|
||||
config_get_bool enabled "main" "enabled" "0"
|
||||
[ "$enabled" -eq "1" ] || return 0
|
||||
|
||||
config_get config_file "main" "conffile" "/etc/sing-box/config.json"
|
||||
config_get working_directory "main" "workdir" "/usr/share/sing-box"
|
||||
config_get_bool log_stdout "main" "log_stdout" "1"
|
||||
config_get_bool log_stderr "main" "log_stderr" "1"
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command "$PROG" run -c "$config_file" -D "$working_directory"
|
||||
procd_set_param file "$config_file"
|
||||
procd_swet_param command "$PROG" run -c "$conffile" -D "$workdir"
|
||||
procd_set_param file "$conffile"
|
||||
procd_set_param stderr "$log_stderr"
|
||||
procd_set_param limits core="unlimited"
|
||||
procd_set_param limits nofile="1000000 1000000"
|
||||
sprocd_set_param limits nofile="1000000 1000000"
|
||||
procd_set_param respawn
|
||||
|
||||
procd_close_instance
|
||||
@@ -29,4 +28,4 @@ start_service() {
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "sing-box"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
/etc/sing-box/
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
[ -s ${IPKG_INSTROOT}/lib/functions.sh ] || exit 0
|
||||
. ${IPKG_INSTROOT}/lib/functions.sh
|
||||
default_prerm $0 $@
|
||||
15
release/config/sing-box-split-dns.xml
Normal file
15
release/config/sing-box-split-dns.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE busconfig PUBLIC
|
||||
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
|
||||
<busconfig>
|
||||
<policy user="root">
|
||||
<allow own="org.freedesktop.resolve1"/>
|
||||
<allow send_destination="org.freedesktop.resolve1"/>
|
||||
<allow receive_sender="org.freedesktop.resolve1"/>
|
||||
</policy>
|
||||
<policy user="sing-box">
|
||||
<allow own="org.freedesktop.resolve1"/>
|
||||
<allow send_destination="org.freedesktop.resolve1"/>
|
||||
<allow receive_sender="org.freedesktop.resolve1"/>
|
||||
</policy>
|
||||
</busconfig>
|
||||
8
release/config/sing-box.rules
Normal file
8
release/config/sing-box.rules
Normal file
@@ -0,0 +1,8 @@
|
||||
polkit.addRule(function(action, subject) {
|
||||
if ((action.id == "org.freedesktop.resolve1.set-domains" ||
|
||||
action.id == "org.freedesktop.resolve1.set-default-route" ||
|
||||
action.id == "org.freedesktop.resolve1.set-dns-servers") &&
|
||||
subject.user == "sing-box") {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
});
|
||||
@@ -4,6 +4,8 @@ Documentation=https://sing-box.sagernet.org
|
||||
After=network.target nss-lookup.target network-online.target
|
||||
|
||||
[Service]
|
||||
User=sing-box
|
||||
StateDirectory=sing-box
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||
ExecStart=/usr/bin/sing-box -D /var/lib/sing-box -C /etc/sing-box run
|
||||
|
||||
1
release/config/sing-box.sysusers
Normal file
1
release/config/sing-box.sysusers
Normal file
@@ -0,0 +1 @@
|
||||
u sing-box - "sing-box Service"
|
||||
@@ -4,6 +4,8 @@ Documentation=https://sing-box.sagernet.org
|
||||
After=network.target nss-lookup.target network-online.target
|
||||
|
||||
[Service]
|
||||
User=sing-box
|
||||
StateDirectory=sing-box-%i
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||
ExecStart=/usr/bin/sing-box -D /var/lib/sing-box-%i -c /etc/sing-box/%i.json run
|
||||
|
||||
15
route/dns.go
15
route/dns.go
@@ -27,12 +27,16 @@ func (r *Router) hijackDNSStream(ctx context.Context, conn net.Conn, metadata ad
|
||||
conn.SetReadDeadline(time.Now().Add(C.DNSTimeout))
|
||||
err := dnsOutbound.HandleStreamDNSRequest(ctx, r.dns, conn, metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
if !E.IsClosedOrCanceled(err) {
|
||||
return err
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext) {
|
||||
func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext) error {
|
||||
if natConn, isNatConn := conn.(udpnat.Conn); isNatConn {
|
||||
metadata.Destination = M.Socksaddr{}
|
||||
for _, packet := range packetBuffers {
|
||||
@@ -48,18 +52,19 @@ func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetB
|
||||
ctx: ctx,
|
||||
metadata: metadata,
|
||||
})
|
||||
return
|
||||
return nil
|
||||
}
|
||||
err := dnsOutbound.NewDNSPacketConnection(ctx, r.dns, conn, packetBuffers, metadata)
|
||||
if err != nil && !E.IsClosedOrCanceled(err) {
|
||||
r.logger.ErrorContext(ctx, E.Cause(err, "process DNS packet connection"))
|
||||
return E.Cause(err, "process DNS packet")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExchangeDNSPacket(ctx context.Context, router adapter.DNSRouter, logger logger.ContextLogger, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext, destination M.Socksaddr) {
|
||||
err := exchangeDNSPacket(ctx, router, conn, buffer, metadata, destination)
|
||||
if err != nil && !errors.Is(err, tun.ErrDrop) && !E.IsClosedOrCanceled(err) {
|
||||
logger.ErrorContext(ctx, E.Cause(err, "process DNS packet connection"))
|
||||
logger.ErrorContext(ctx, E.Cause(err, "process DNS packet"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/user"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -113,14 +112,12 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
|
||||
}
|
||||
case *rule.RuleActionReject:
|
||||
buf.ReleaseMulti(buffers)
|
||||
N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx))
|
||||
return nil
|
||||
return action.Error(ctx)
|
||||
case *rule.RuleActionHijackDNS:
|
||||
for _, buffer := range buffers {
|
||||
conn = bufio.NewCachedConn(conn, buffer)
|
||||
}
|
||||
r.hijackDNSStream(ctx, conn, metadata)
|
||||
return nil
|
||||
return r.hijackDNSStream(ctx, conn, metadata)
|
||||
}
|
||||
}
|
||||
if selectedRule == nil {
|
||||
@@ -231,11 +228,10 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
|
||||
}
|
||||
case *rule.RuleActionReject:
|
||||
N.ReleaseMultiPacketBuffer(packetBuffers)
|
||||
N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx))
|
||||
return nil
|
||||
return action.Error(ctx)
|
||||
case *rule.RuleActionHijackDNS:
|
||||
r.hijackDNSPacket(ctx, conn, packetBuffers, metadata)
|
||||
return nil
|
||||
return r.hijackDNSPacket(ctx, conn, packetBuffers, metadata)
|
||||
|
||||
}
|
||||
}
|
||||
if selectedRule == nil || selectReturn {
|
||||
@@ -298,16 +294,16 @@ func (r *Router) matchRule(
|
||||
r.logger.InfoContext(ctx, "failed to search process: ", fErr)
|
||||
} else {
|
||||
if processInfo.ProcessPath != "" {
|
||||
r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath)
|
||||
if processInfo.User != "" {
|
||||
r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user: ", processInfo.User)
|
||||
} else if processInfo.UserId != -1 {
|
||||
r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user id: ", processInfo.UserId)
|
||||
} else {
|
||||
r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath)
|
||||
}
|
||||
} else if processInfo.PackageName != "" {
|
||||
r.logger.InfoContext(ctx, "found package name: ", processInfo.PackageName)
|
||||
} else if processInfo.UserId != -1 {
|
||||
if /*needUserName &&*/ true {
|
||||
osUser, _ := user.LookupId(F.ToString(processInfo.UserId))
|
||||
if osUser != nil {
|
||||
processInfo.User = osUser.Username
|
||||
}
|
||||
}
|
||||
if processInfo.User != "" {
|
||||
r.logger.InfoContext(ctx, "found user: ", processInfo.User)
|
||||
} else {
|
||||
@@ -418,7 +414,6 @@ match:
|
||||
Port: metadata.Destination.Port,
|
||||
Fqdn: routeOptions.OverrideAddress.Fqdn,
|
||||
}
|
||||
metadata.DestinationAddresses = nil
|
||||
}
|
||||
if routeOptions.OverridePort > 0 {
|
||||
metadata.Destination = M.Socksaddr{
|
||||
|
||||
@@ -102,10 +102,7 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 {
|
||||
item, err := NewDomainItem(options.Domain, options.DomainSuffix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item := NewDomainItem(options.Domain, options.DomainSuffix)
|
||||
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
|
||||
@@ -93,10 +93,7 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 {
|
||||
item, err := NewDomainItem(options.Domain, options.DomainSuffix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item := NewDomainItem(options.Domain, options.DomainSuffix)
|
||||
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
|
||||
@@ -47,10 +47,7 @@ func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessR
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 {
|
||||
item, err := NewDomainItem(options.Domain, options.DomainSuffix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item := NewDomainItem(options.Domain, options.DomainSuffix)
|
||||
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
} else if options.DomainMatcher != nil {
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing/common/domain"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ RuleItem = (*DomainItem)(nil)
|
||||
@@ -15,17 +14,7 @@ type DomainItem struct {
|
||||
description string
|
||||
}
|
||||
|
||||
func NewDomainItem(domains []string, domainSuffixes []string) (*DomainItem, error) {
|
||||
for _, domainItem := range domains {
|
||||
if domainItem == "" {
|
||||
return nil, E.New("domain: empty item is not allowed")
|
||||
}
|
||||
}
|
||||
for _, domainSuffixItem := range domainSuffixes {
|
||||
if domainSuffixItem == "" {
|
||||
return nil, E.New("domain_suffix: empty item is not allowed")
|
||||
}
|
||||
}
|
||||
func NewDomainItem(domains []string, domainSuffixes []string) *DomainItem {
|
||||
var description string
|
||||
if dLen := len(domains); dLen > 0 {
|
||||
if dLen == 1 {
|
||||
@@ -51,7 +40,7 @@ func NewDomainItem(domains []string, domainSuffixes []string) (*DomainItem, erro
|
||||
return &DomainItem{
|
||||
domain.NewMatcher(domains, domainSuffixes, false),
|
||||
description,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func NewRawDomainItem(matcher *domain.Matcher) *DomainItem {
|
||||
|
||||
463
service/derp/derp.go
Normal file
463
service/derp/derp.go
Normal file
@@ -0,0 +1,463 @@
|
||||
package derp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
boxScale "github.com/sagernet/sing-box/protocol/tailscale"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/filemanager"
|
||||
"github.com/sagernet/tailscale/client/tailscale"
|
||||
"github.com/sagernet/tailscale/derp"
|
||||
"github.com/sagernet/tailscale/derp/derphttp"
|
||||
"github.com/sagernet/tailscale/net/netmon"
|
||||
"github.com/sagernet/tailscale/net/wsconn"
|
||||
"github.com/sagernet/tailscale/tsweb"
|
||||
"github.com/sagernet/tailscale/types/key"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"github.com/go-chi/render"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
)
|
||||
|
||||
func Register(registry *boxService.Registry) {
|
||||
boxService.Register[option.DERPServiceOptions](registry, C.TypeDERP, NewService)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
boxService.Adapter
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
listener *listener.Listener
|
||||
tlsConfig tls.ServerConfig
|
||||
server *derp.Server
|
||||
configPath string
|
||||
verifyClientEndpoint []string
|
||||
verifyClientURL []*option.DERPVerifyClientURLOptions
|
||||
home string
|
||||
domainResolveOptions *option.DomainResolveOptions
|
||||
domainResolver *adapter.DNSQueryOptions
|
||||
meshKey string
|
||||
meshKeyPath string
|
||||
meshWith []*option.DERPMeshOptions
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPServiceOptions) (adapter.Service, error) {
|
||||
if options.TLS == nil || !options.TLS.Enabled {
|
||||
return nil, E.New("TLS is required for DERP server")
|
||||
}
|
||||
tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var configPath string
|
||||
if options.ConfigPath != "" {
|
||||
configPath = filemanager.BasePath(ctx, os.ExpandEnv(options.ConfigPath))
|
||||
} else if os.Getuid() == 0 {
|
||||
configPath = "/var/lib/derper/derper.key"
|
||||
} else {
|
||||
return nil, E.New("missing config_path")
|
||||
}
|
||||
|
||||
if options.MeshPSK != "" {
|
||||
err = checkMeshKey(options.MeshPSK)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "invalid mesh_psk")
|
||||
}
|
||||
}
|
||||
|
||||
return &Service{
|
||||
Adapter: boxService.NewAdapter(C.TypeDERP, tag),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
listener: listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Network: []string{N.NetworkTCP},
|
||||
Listen: options.ListenOptions,
|
||||
}),
|
||||
tlsConfig: tlsConfig,
|
||||
configPath: configPath,
|
||||
verifyClientEndpoint: options.VerifyClientEndpoint,
|
||||
verifyClientURL: options.VerifyClientURL,
|
||||
meshKey: options.MeshPSK,
|
||||
meshKeyPath: options.MeshPSKFile,
|
||||
meshWith: options.MeshWith,
|
||||
domainResolveOptions: options.DomainResolver,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Service) Start(stage adapter.StartStage) error {
|
||||
switch stage {
|
||||
case adapter.StartStateInitialize:
|
||||
domainResolver, err := adapter.DNSQueryOptionsFrom(d.ctx, d.domainResolveOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.domainResolver = domainResolver
|
||||
case adapter.StartStateStart:
|
||||
config, err := readDERPConfig(d.configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
server := derp.NewServer(config.PrivateKey, func(format string, args ...any) {
|
||||
d.logger.Debug(fmt.Sprintf(format, args...))
|
||||
})
|
||||
|
||||
if len(d.verifyClientURL) > 0 {
|
||||
var httpClients []*http.Client
|
||||
var urls []string
|
||||
for index, options := range d.verifyClientURL {
|
||||
verifyDialer, createErr := dialer.NewWithOptions(dialer.Options{
|
||||
Context: d.ctx,
|
||||
Options: options.DialerOptions,
|
||||
RemoteIsDomain: options.ServerIsDomain(),
|
||||
})
|
||||
if createErr != nil {
|
||||
return E.Cause(createErr, "verify_client_url[", index, "]")
|
||||
}
|
||||
httpClients = append(httpClients, &http.Client{
|
||||
Transport: &http.Transport{
|
||||
ForceAttemptHTTP2: true,
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return verifyDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
},
|
||||
},
|
||||
})
|
||||
urls = append(urls, options.URL)
|
||||
}
|
||||
server.SetVerifyClientHTTPClient(httpClients)
|
||||
server.SetVerifyClientURL(urls)
|
||||
}
|
||||
|
||||
if d.meshKey != "" {
|
||||
server.SetMeshKey(d.meshKey)
|
||||
} else if d.meshKeyPath != "" {
|
||||
var meshKeyContent []byte
|
||||
meshKeyContent, err = os.ReadFile(d.meshKeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = checkMeshKey(string(meshKeyContent))
|
||||
if err != nil {
|
||||
return E.Cause(err, "invalid mesh_psk_path file")
|
||||
}
|
||||
server.SetMeshKey(string(meshKeyContent))
|
||||
}
|
||||
d.server = server
|
||||
|
||||
derpMux := http.NewServeMux()
|
||||
derpHandler := derphttp.Handler(server)
|
||||
derpHandler = addWebSocketSupport(server, derpHandler)
|
||||
derpMux.Handle("/derp", derpHandler)
|
||||
|
||||
homeHandler, ok := getHomeHandler(d.home)
|
||||
if !ok {
|
||||
return E.New("invalid home value: ", d.home)
|
||||
}
|
||||
|
||||
derpMux.HandleFunc("/derp/probe", derphttp.ProbeHandler)
|
||||
derpMux.HandleFunc("/derp/latency-check", derphttp.ProbeHandler)
|
||||
derpMux.HandleFunc("/bootstrap-dns", tsweb.BrowserHeaderHandlerFunc(handleBootstrapDNS(d.ctx, d.domainResolver)))
|
||||
derpMux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tsweb.AddBrowserHeaders(w)
|
||||
homeHandler.ServeHTTP(w, r)
|
||||
}))
|
||||
derpMux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tsweb.AddBrowserHeaders(w)
|
||||
io.WriteString(w, "User-agent: *\nDisallow: /\n")
|
||||
}))
|
||||
derpMux.Handle("/generate_204", http.HandlerFunc(derphttp.ServeNoContent))
|
||||
|
||||
err = d.tlsConfig.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tcpListener, err := d.listener.ListenTCP()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(d.tlsConfig.NextProtos()) == 0 {
|
||||
d.tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"})
|
||||
} else if !common.Contains(d.tlsConfig.NextProtos(), http2.NextProtoTLS) {
|
||||
d.tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, d.tlsConfig.NextProtos()...))
|
||||
}
|
||||
tcpListener = aTLS.NewListener(tcpListener, d.tlsConfig)
|
||||
httpServer := &http.Server{
|
||||
Handler: h2c.NewHandler(derpMux, &http2.Server{}),
|
||||
}
|
||||
go httpServer.Serve(tcpListener)
|
||||
case adapter.StartStatePostStart:
|
||||
if len(d.verifyClientEndpoint) > 0 {
|
||||
var endpoints []*tailscale.LocalClient
|
||||
endpointManager := service.FromContext[adapter.EndpointManager](d.ctx)
|
||||
for _, endpointTag := range d.verifyClientEndpoint {
|
||||
endpoint, loaded := endpointManager.Get(endpointTag)
|
||||
if !loaded {
|
||||
return E.New("verify_client_endpoint: endpoint not found: ", endpointTag)
|
||||
}
|
||||
tsEndpoint, isTailscale := endpoint.(*boxScale.Endpoint)
|
||||
if !isTailscale {
|
||||
return E.New("verify_client_endpoint: endpoint is not Tailscale: ", endpointTag)
|
||||
}
|
||||
localClient, err := tsEndpoint.Server().LocalClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpoints = append(endpoints, localClient)
|
||||
}
|
||||
d.server.SetVerifyClientLocalClient(endpoints)
|
||||
}
|
||||
if len(d.meshWith) > 0 {
|
||||
if !d.server.HasMeshKey() {
|
||||
return E.New("missing mesh psk")
|
||||
}
|
||||
for _, options := range d.meshWith {
|
||||
err := d.startMeshWithHost(d.server, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkMeshKey(meshKey string) error {
|
||||
checkRegex, err := regexp.Compile(`^[0-9a-f]{64}$`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !checkRegex.MatchString(meshKey) {
|
||||
return E.New("key must contain exactly 64 hex digits")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Service) startMeshWithHost(derpServer *derp.Server, server *option.DERPMeshOptions) error {
|
||||
meshDialer, err := dialer.NewWithOptions(dialer.Options{
|
||||
Context: d.ctx,
|
||||
Options: server.DialerOptions,
|
||||
RemoteIsDomain: server.ServerIsDomain(),
|
||||
NewDialer: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var hostname string
|
||||
if server.Host != "" {
|
||||
hostname = server.Host
|
||||
} else {
|
||||
hostname = server.Server
|
||||
}
|
||||
var stdConfig *tls.STDConfig
|
||||
if server.TLS != nil && server.TLS.Enabled {
|
||||
tlsConfig, err := tls.NewClient(d.ctx, hostname, common.PtrValueOrDefault(server.TLS))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stdConfig, err = tlsConfig.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
logf := func(format string, args ...any) {
|
||||
d.logger.Debug(F.ToString("mesh(", hostname, "): ", fmt.Sprintf(format, args...)))
|
||||
}
|
||||
var meshHost string
|
||||
if server.ServerPort == 0 || server.ServerPort == 443 {
|
||||
meshHost = hostname
|
||||
} else {
|
||||
meshHost = M.ParseSocksaddrHostPort(hostname, server.ServerPort).String()
|
||||
}
|
||||
meshClient, err := derphttp.NewClient(derpServer.PrivateKey(), "https://"+meshHost+"/derp", logf, netmon.NewStatic())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
meshClient.TLSConfig = stdConfig
|
||||
meshClient.MeshKey = derpServer.MeshKey()
|
||||
meshClient.WatchConnectionChanges = true
|
||||
meshClient.SetURLDialer(func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return meshDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
})
|
||||
add := func(m derp.PeerPresentMessage) { derpServer.AddPacketForwarder(m.Key, meshClient) }
|
||||
remove := func(m derp.PeerGoneMessage) { derpServer.RemovePacketForwarder(m.Peer, meshClient) }
|
||||
go meshClient.RunWatchConnectionLoop(context.Background(), derpServer.PublicKey(), logf, add, remove)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Service) Close() error {
|
||||
return common.Close(
|
||||
common.PtrOrNil(d.listener),
|
||||
d.tlsConfig,
|
||||
)
|
||||
}
|
||||
|
||||
var homePage = `
|
||||
<h1>DERP</h1>
|
||||
<p>
|
||||
This is a <a href="https://tailscale.com/">Tailscale</a> DERP server.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
It provides STUN, interactive connectivity establishment, and relaying of end-to-end encrypted traffic
|
||||
for Tailscale clients.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Documentation:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="https://tailscale.com/kb/1232/derp-servers">About DERP</a></li>
|
||||
<li><a href="https://pkg.go.dev/tailscale.com/derp">Protocol & Go docs</a></li>
|
||||
<li><a href="https://github.com/tailscale/tailscale/tree/main/cmd/derper#derp">How to run a DERP server</a></li>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
func getHomeHandler(val string) (_ http.Handler, ok bool) {
|
||||
if val == "" {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(homePage))
|
||||
}), true
|
||||
}
|
||||
if val == "blank" {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
}), true
|
||||
}
|
||||
if strings.HasPrefix(val, "http://") || strings.HasPrefix(val, "https://") {
|
||||
return http.RedirectHandler(val, http.StatusFound), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
up := strings.ToLower(r.Header.Get("Upgrade"))
|
||||
|
||||
// Very early versions of Tailscale set "Upgrade: WebSocket" but didn't actually
|
||||
// speak WebSockets (they still assumed DERP's binary framing). So to distinguish
|
||||
// clients that actually want WebSockets, look for an explicit "derp" subprotocol.
|
||||
if up != "websocket" || !strings.Contains(r.Header.Get("Sec-Websocket-Protocol"), "derp") {
|
||||
base.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
||||
Subprotocols: []string{"derp"},
|
||||
OriginPatterns: []string{"*"},
|
||||
// Disable compression because we transmit WireGuard messages that
|
||||
// are not compressible.
|
||||
// Additionally, Safari has a broken implementation of compression
|
||||
// (see https://github.com/nhooyr/websocket/issues/218) that makes
|
||||
// enabling it actively harmful.
|
||||
CompressionMode: websocket.CompressionDisabled,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer c.Close(websocket.StatusInternalError, "closing")
|
||||
if c.Subprotocol() != "derp" {
|
||||
c.Close(websocket.StatusPolicyViolation, "client must speak the derp subprotocol")
|
||||
return
|
||||
}
|
||||
wc := wsconn.NetConn(r.Context(), c, websocket.MessageBinary, r.RemoteAddr)
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc))
|
||||
s.Accept(r.Context(), wc, brw, r.RemoteAddr)
|
||||
})
|
||||
}
|
||||
|
||||
func handleBootstrapDNS(ctx context.Context, queryOptions *adapter.DNSQueryOptions) http.HandlerFunc {
|
||||
dnsRouter := service.FromContext[adapter.DNSRouter](ctx)
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Connection", "close")
|
||||
if queryDomain := r.URL.Query().Get("q"); queryDomain != "" {
|
||||
addresses, err := dnsRouter.Lookup(ctx, queryDomain, *queryOptions)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
render.JSON(w, r, render.M{
|
||||
queryDomain: addresses,
|
||||
})
|
||||
return
|
||||
}
|
||||
w.Write([]byte("{}"))
|
||||
}
|
||||
}
|
||||
|
||||
type derpConfig struct {
|
||||
PrivateKey key.NodePrivate
|
||||
}
|
||||
|
||||
func readDERPConfig(path string) (*derpConfig, error) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return writeNewDERPConfig(path)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var config derpConfig
|
||||
err = json.Unmarshal(content, &config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func writeNewDERPConfig(path string) (*derpConfig, error) {
|
||||
newKey := key.NewNode()
|
||||
err := os.MkdirAll(filepath.Dir(path), 0o777)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config := derpConfig{
|
||||
PrivateKey: newKey,
|
||||
}
|
||||
content, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = os.WriteFile(path, content, 0o644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
89
service/derp/stun.go
Normal file
89
service/derp/stun.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package derp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"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"
|
||||
"github.com/sagernet/tailscale/net/stun"
|
||||
)
|
||||
|
||||
func RegisterSTUN(registry *boxService.Registry) {
|
||||
boxService.Register[option.DERPSTUNServiceOptions](registry, C.TypeDERPSTUN, NewSTUNService)
|
||||
}
|
||||
|
||||
type STUNService struct {
|
||||
boxService.Adapter
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
listener *listener.Listener
|
||||
}
|
||||
|
||||
func NewSTUNService(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPSTUNServiceOptions) (adapter.Service, error) {
|
||||
return &STUNService{
|
||||
Adapter: boxService.NewAdapter(C.TypeDERPSTUN, tag),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
listener: listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Network: []string{N.NetworkUDP},
|
||||
Listen: options.ListenOptions,
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *STUNService) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
packetConn, err := d.listener.ListenUDP()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go d.loopPacket(packetConn.(*net.UDPConn))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *STUNService) Close() error {
|
||||
return d.listener.Close()
|
||||
}
|
||||
|
||||
func (d *STUNService) loopPacket(packetConn *net.UDPConn) {
|
||||
buffer := make([]byte, 65535)
|
||||
oob := make([]byte, 1024)
|
||||
var (
|
||||
n int
|
||||
oobN int
|
||||
addrPort netip.AddrPort
|
||||
err error
|
||||
)
|
||||
for {
|
||||
n, oobN, _, addrPort, err = packetConn.ReadMsgUDPAddrPort(buffer, oob)
|
||||
if err != nil {
|
||||
if E.IsClosedOrCanceled(err) {
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
if !stun.Is(buffer[:n]) {
|
||||
continue
|
||||
}
|
||||
txid, err := stun.ParseBindingRequest(buffer[:n])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
packetConn.WriteMsgUDPAddrPort(stun.Response(txid, addrPort), oob[:oobN], addrPort)
|
||||
}
|
||||
}
|
||||
648
service/resolved/resolve1.go
Normal file
648
service/resolved/resolve1.go
Normal file
@@ -0,0 +1,648 @@
|
||||
//go:build linux
|
||||
|
||||
package resolved
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/process"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type resolve1Manager Service
|
||||
|
||||
type Address struct {
|
||||
IfIndex int32
|
||||
Family int32
|
||||
Address []byte
|
||||
}
|
||||
|
||||
type Name struct {
|
||||
IfIndex int32
|
||||
Hostname string
|
||||
}
|
||||
|
||||
type ResourceRecord struct {
|
||||
IfIndex int32
|
||||
Type uint16
|
||||
Class uint16
|
||||
Data []byte
|
||||
}
|
||||
|
||||
type SRVRecord struct {
|
||||
Priority uint16
|
||||
Weight uint16
|
||||
Port uint16
|
||||
Hostname string
|
||||
Addresses []Address
|
||||
CNAME string
|
||||
}
|
||||
|
||||
type TXTRecord []byte
|
||||
|
||||
type LinkDNS struct {
|
||||
Family int32
|
||||
Address []byte
|
||||
}
|
||||
|
||||
type LinkDNSEx struct {
|
||||
Family int32
|
||||
Address []byte
|
||||
Port uint16
|
||||
Name string
|
||||
}
|
||||
|
||||
type LinkDomain struct {
|
||||
Domain string
|
||||
RoutingOnly bool
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) getLink(ifIndex int32) (*TransportLink, *dbus.Error) {
|
||||
link, loaded := t.links[ifIndex]
|
||||
if !loaded {
|
||||
link = &TransportLink{}
|
||||
t.links[ifIndex] = link
|
||||
iif, err := t.network.InterfaceFinder().ByIndex(int(ifIndex))
|
||||
if err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
link.iif = iif
|
||||
}
|
||||
return link, nil
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) getSenderProcess(sender dbus.Sender) (int32, error) {
|
||||
var senderPid int32
|
||||
dbusObject := t.systemBus.Object("org.freedesktop.DBus", "/org/freedesktop/DBus")
|
||||
if dbusObject == nil {
|
||||
return 0, E.New("missing dbus object")
|
||||
}
|
||||
err := dbusObject.Call("org.freedesktop.DBus.GetConnectionUnixProcessID", 0, string(sender)).Store(&senderPid)
|
||||
if err != nil {
|
||||
return 0, E.Cause(err, "GetConnectionUnixProcessID")
|
||||
}
|
||||
return senderPid, nil
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) createMetadata(sender dbus.Sender) adapter.InboundContext {
|
||||
var metadata adapter.InboundContext
|
||||
metadata.Inbound = t.Tag()
|
||||
metadata.InboundType = C.TypeResolved
|
||||
senderPid, err := t.getSenderProcess(sender)
|
||||
if err != nil {
|
||||
return metadata
|
||||
}
|
||||
var processInfo process.Info
|
||||
metadata.ProcessInfo = &processInfo
|
||||
processInfo.ProcessID = uint32(senderPid)
|
||||
|
||||
processPath, err := os.Readlink(F.ToString("/proc/", senderPid, "/exe"))
|
||||
if err == nil {
|
||||
processInfo.ProcessPath = processPath
|
||||
} else {
|
||||
processPath, err = os.Readlink(F.ToString("/proc/", senderPid, "/comm"))
|
||||
if err == nil {
|
||||
processInfo.ProcessPath = processPath
|
||||
}
|
||||
}
|
||||
|
||||
var uidFound bool
|
||||
statusContent, err := os.ReadFile(F.ToString("/proc/", senderPid, "/status"))
|
||||
if err == nil {
|
||||
for _, line := range strings.Split(string(statusContent), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "Uid:") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
uid, parseErr := strconv.ParseUint(fields[1], 10, 32)
|
||||
if parseErr != nil {
|
||||
break
|
||||
}
|
||||
processInfo.UserId = int32(uid)
|
||||
uidFound = true
|
||||
if osUser, _ := user.LookupId(F.ToString(uid)); osUser != nil {
|
||||
processInfo.User = osUser.Username
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !uidFound {
|
||||
metadata.ProcessInfo.UserId = -1
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) log(sender dbus.Sender, message ...any) {
|
||||
metadata := t.createMetadata(sender)
|
||||
if metadata.ProcessInfo != nil {
|
||||
var prefix string
|
||||
if metadata.ProcessInfo.ProcessPath != "" {
|
||||
prefix = filepath.Base(metadata.ProcessInfo.ProcessPath)
|
||||
} else if metadata.ProcessInfo.User != "" {
|
||||
prefix = F.ToString("user:", metadata.ProcessInfo.User)
|
||||
} else if metadata.ProcessInfo.UserId != 0 {
|
||||
prefix = F.ToString("uid:", metadata.ProcessInfo.UserId)
|
||||
}
|
||||
t.logger.Info("(", prefix, ") ", F.ToString(message...))
|
||||
} else {
|
||||
t.logger.Info(F.ToString(message...))
|
||||
}
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) logRequest(sender dbus.Sender, message ...any) context.Context {
|
||||
ctx := log.ContextWithNewID(t.ctx)
|
||||
metadata := t.createMetadata(sender)
|
||||
if metadata.ProcessInfo != nil {
|
||||
var prefix string
|
||||
if metadata.ProcessInfo.ProcessPath != "" {
|
||||
prefix = filepath.Base(metadata.ProcessInfo.ProcessPath)
|
||||
} else if metadata.ProcessInfo.User != "" {
|
||||
prefix = F.ToString("user:", metadata.ProcessInfo.User)
|
||||
} else if metadata.ProcessInfo.UserId != 0 {
|
||||
prefix = F.ToString("uid:", metadata.ProcessInfo.UserId)
|
||||
}
|
||||
t.logger.InfoContext(ctx, "(", prefix, ") ", F.ToString(message...))
|
||||
} else {
|
||||
t.logger.InfoContext(ctx, F.ToString(message...))
|
||||
}
|
||||
return adapter.WithContext(ctx, &metadata)
|
||||
}
|
||||
|
||||
func familyToString(family int32) string {
|
||||
switch family {
|
||||
case syscall.AF_UNSPEC:
|
||||
return "AF_UNSPEC"
|
||||
case syscall.AF_INET:
|
||||
return "AF_INET"
|
||||
case syscall.AF_INET6:
|
||||
return "AF_INET6"
|
||||
default:
|
||||
return F.ToString(family)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) ResolveHostname(sender dbus.Sender, ifIndex int32, hostname string, family int32, flags uint64) (addresses []Address, canonical string, outflags uint64, err *dbus.Error) {
|
||||
t.linkAccess.Lock()
|
||||
link, err := t.getLink(ifIndex)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.linkAccess.Unlock()
|
||||
var strategy C.DomainStrategy
|
||||
switch family {
|
||||
case syscall.AF_UNSPEC:
|
||||
strategy = C.DomainStrategyAsIS
|
||||
case syscall.AF_INET:
|
||||
strategy = C.DomainStrategyIPv4Only
|
||||
case syscall.AF_INET6:
|
||||
strategy = C.DomainStrategyIPv6Only
|
||||
}
|
||||
ctx := t.logRequest(sender, "ResolveHostname ", link.iif.Name, " ", hostname, " ", familyToString(family), " ", flags)
|
||||
responseAddresses, lookupErr := t.dnsRouter.Lookup(ctx, hostname, adapter.DNSQueryOptions{
|
||||
LookupStrategy: strategy,
|
||||
})
|
||||
if lookupErr != nil {
|
||||
err = wrapError(err)
|
||||
return
|
||||
}
|
||||
addresses = common.Map(responseAddresses, func(it netip.Addr) Address {
|
||||
var addrFamily int32
|
||||
if it.Is4() {
|
||||
addrFamily = syscall.AF_INET
|
||||
} else {
|
||||
addrFamily = syscall.AF_INET6
|
||||
}
|
||||
return Address{
|
||||
IfIndex: ifIndex,
|
||||
Family: addrFamily,
|
||||
Address: it.AsSlice(),
|
||||
}
|
||||
})
|
||||
canonical = mDNS.CanonicalName(hostname)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) ResolveAddress(sender dbus.Sender, ifIndex int32, family int32, address []byte, flags uint64) (names []Name, outflags uint64, err *dbus.Error) {
|
||||
t.linkAccess.Lock()
|
||||
link, err := t.getLink(ifIndex)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.linkAccess.Unlock()
|
||||
addr, ok := netip.AddrFromSlice(address)
|
||||
if !ok {
|
||||
err = wrapError(E.New("invalid address"))
|
||||
return
|
||||
}
|
||||
var nibbles []string
|
||||
for i := len(address) - 1; i >= 0; i-- {
|
||||
b := address[i]
|
||||
nibbles = append(nibbles, fmt.Sprintf("%x", b&0x0F))
|
||||
nibbles = append(nibbles, fmt.Sprintf("%x", b>>4))
|
||||
}
|
||||
var ptrDomain string
|
||||
if addr.Is4() {
|
||||
ptrDomain = strings.Join(nibbles, ".") + ".in-addr.arpa."
|
||||
} else {
|
||||
ptrDomain = strings.Join(nibbles, ".") + ".ip6.arpa."
|
||||
}
|
||||
request := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
RecursionDesired: true,
|
||||
},
|
||||
Question: []mDNS.Question{
|
||||
{
|
||||
Name: mDNS.Fqdn(ptrDomain),
|
||||
Qtype: mDNS.TypePTR,
|
||||
Qclass: mDNS.ClassINET,
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx := t.logRequest(sender, "ResolveAddress ", link.iif.Name, familyToString(family), addr, flags)
|
||||
response, lookupErr := t.dnsRouter.Exchange(ctx, request, adapter.DNSQueryOptions{})
|
||||
if lookupErr != nil {
|
||||
err = wrapError(err)
|
||||
return
|
||||
}
|
||||
if response.Rcode != mDNS.RcodeSuccess {
|
||||
err = rcodeError(response.Rcode)
|
||||
return
|
||||
}
|
||||
for _, rawRR := range response.Answer {
|
||||
switch rr := rawRR.(type) {
|
||||
case *mDNS.PTR:
|
||||
names = append(names, Name{
|
||||
IfIndex: ifIndex,
|
||||
Hostname: rr.Ptr,
|
||||
})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) ResolveRecord(sender dbus.Sender, ifIndex int32, family int32, hostname string, qClass uint16, qType uint16, flags uint64) (records []ResourceRecord, outflags uint64, err *dbus.Error) {
|
||||
t.linkAccess.Lock()
|
||||
link, err := t.getLink(ifIndex)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.linkAccess.Unlock()
|
||||
request := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
RecursionDesired: true,
|
||||
},
|
||||
Question: []mDNS.Question{
|
||||
{
|
||||
Name: mDNS.Fqdn(hostname),
|
||||
Qtype: qType,
|
||||
Qclass: qClass,
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx := t.logRequest(sender, "ResolveRecord ", link.iif.Name, familyToString(family), hostname, mDNS.Class(qClass), mDNS.Type(qType), flags)
|
||||
response, exchangeErr := t.dnsRouter.Exchange(ctx, request, adapter.DNSQueryOptions{})
|
||||
if exchangeErr != nil {
|
||||
err = wrapError(exchangeErr)
|
||||
return
|
||||
}
|
||||
if response.Rcode != mDNS.RcodeSuccess {
|
||||
err = rcodeError(response.Rcode)
|
||||
return
|
||||
}
|
||||
for _, rr := range response.Answer {
|
||||
var record ResourceRecord
|
||||
record.IfIndex = ifIndex
|
||||
record.Type = rr.Header().Rrtype
|
||||
record.Class = rr.Header().Class
|
||||
data := make([]byte, mDNS.Len(rr))
|
||||
_, unpackErr := mDNS.PackRR(rr, data, 0, nil, false)
|
||||
if unpackErr != nil {
|
||||
err = wrapError(unpackErr)
|
||||
}
|
||||
record.Data = data
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) ResolveService(sender dbus.Sender, ifIndex int32, hostname string, sType string, domain string, family int32, flags uint64) (srvData []SRVRecord, txtData []TXTRecord, canonicalName string, canonicalType string, canonicalDomain string, outflags uint64, err *dbus.Error) {
|
||||
t.linkAccess.Lock()
|
||||
link, err := t.getLink(ifIndex)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.linkAccess.Unlock()
|
||||
|
||||
serviceName := hostname
|
||||
if hostname != "" && !strings.HasSuffix(hostname, ".") {
|
||||
serviceName += "."
|
||||
}
|
||||
serviceName += sType
|
||||
if !strings.HasSuffix(serviceName, ".") {
|
||||
serviceName += "."
|
||||
}
|
||||
serviceName += domain
|
||||
if !strings.HasSuffix(serviceName, ".") {
|
||||
serviceName += "."
|
||||
}
|
||||
|
||||
ctx := t.logRequest(sender, "ResolveService ", link.iif.Name, " ", hostname, " ", sType, " ", domain, " ", familyToString(family), " ", flags)
|
||||
|
||||
srvRequest := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
RecursionDesired: true,
|
||||
},
|
||||
Question: []mDNS.Question{
|
||||
{
|
||||
Name: serviceName,
|
||||
Qtype: mDNS.TypeSRV,
|
||||
Qclass: mDNS.ClassINET,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
srvResponse, exchangeErr := t.dnsRouter.Exchange(ctx, srvRequest, adapter.DNSQueryOptions{})
|
||||
if exchangeErr != nil {
|
||||
err = wrapError(exchangeErr)
|
||||
return
|
||||
}
|
||||
if srvResponse.Rcode != mDNS.RcodeSuccess {
|
||||
err = rcodeError(srvResponse.Rcode)
|
||||
return
|
||||
}
|
||||
|
||||
txtRequest := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
RecursionDesired: true,
|
||||
},
|
||||
Question: []mDNS.Question{
|
||||
{
|
||||
Name: serviceName,
|
||||
Qtype: mDNS.TypeTXT,
|
||||
Qclass: mDNS.ClassINET,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
txtResponse, exchangeErr := t.dnsRouter.Exchange(ctx, txtRequest, adapter.DNSQueryOptions{})
|
||||
if exchangeErr != nil {
|
||||
err = wrapError(exchangeErr)
|
||||
return
|
||||
}
|
||||
|
||||
for _, rawRR := range srvResponse.Answer {
|
||||
switch rr := rawRR.(type) {
|
||||
case *mDNS.SRV:
|
||||
var srvRecord SRVRecord
|
||||
srvRecord.Priority = rr.Priority
|
||||
srvRecord.Weight = rr.Weight
|
||||
srvRecord.Port = rr.Port
|
||||
srvRecord.Hostname = rr.Target
|
||||
|
||||
var strategy C.DomainStrategy
|
||||
switch family {
|
||||
case syscall.AF_UNSPEC:
|
||||
strategy = C.DomainStrategyAsIS
|
||||
case syscall.AF_INET:
|
||||
strategy = C.DomainStrategyIPv4Only
|
||||
case syscall.AF_INET6:
|
||||
strategy = C.DomainStrategyIPv6Only
|
||||
}
|
||||
|
||||
addrs, lookupErr := t.dnsRouter.Lookup(ctx, rr.Target, adapter.DNSQueryOptions{
|
||||
LookupStrategy: strategy,
|
||||
})
|
||||
if lookupErr == nil {
|
||||
srvRecord.Addresses = common.Map(addrs, func(it netip.Addr) Address {
|
||||
var addrFamily int32
|
||||
if it.Is4() {
|
||||
addrFamily = syscall.AF_INET
|
||||
} else {
|
||||
addrFamily = syscall.AF_INET6
|
||||
}
|
||||
return Address{
|
||||
IfIndex: ifIndex,
|
||||
Family: addrFamily,
|
||||
Address: it.AsSlice(),
|
||||
}
|
||||
})
|
||||
}
|
||||
for _, a := range srvResponse.Answer {
|
||||
if cname, ok := a.(*mDNS.CNAME); ok && cname.Header().Name == rr.Target {
|
||||
srvRecord.CNAME = cname.Target
|
||||
break
|
||||
}
|
||||
}
|
||||
srvData = append(srvData, srvRecord)
|
||||
}
|
||||
}
|
||||
for _, rawRR := range txtResponse.Answer {
|
||||
switch rr := rawRR.(type) {
|
||||
case *mDNS.TXT:
|
||||
data := make([]byte, mDNS.Len(rr))
|
||||
_, packErr := mDNS.PackRR(rr, data, 0, nil, false)
|
||||
if packErr == nil {
|
||||
txtData = append(txtData, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
canonicalName = mDNS.CanonicalName(hostname)
|
||||
canonicalType = mDNS.CanonicalName(sType)
|
||||
canonicalDomain = mDNS.CanonicalName(domain)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) SetLinkDNS(sender dbus.Sender, ifIndex int32, addresses []LinkDNS) *dbus.Error {
|
||||
t.linkAccess.Lock()
|
||||
defer t.linkAccess.Unlock()
|
||||
link, err := t.getLink(ifIndex)
|
||||
if err != nil {
|
||||
return wrapError(err)
|
||||
}
|
||||
link.address = addresses
|
||||
if len(addresses) > 0 {
|
||||
t.log(sender, "SetLinkDNS ", link.iif.Name, " ", strings.Join(common.Map(addresses, func(it LinkDNS) string {
|
||||
return M.AddrFromIP(it.Address).String()
|
||||
}), ", "))
|
||||
} else {
|
||||
t.log(sender, "SetLinkDNS ", link.iif.Name, " (empty)")
|
||||
}
|
||||
return t.postUpdate(link)
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) SetLinkDNSEx(sender dbus.Sender, ifIndex int32, addresses []LinkDNSEx) *dbus.Error {
|
||||
t.linkAccess.Lock()
|
||||
defer t.linkAccess.Unlock()
|
||||
link, err := t.getLink(ifIndex)
|
||||
if err != nil {
|
||||
return wrapError(err)
|
||||
}
|
||||
link.addressEx = addresses
|
||||
if len(addresses) > 0 {
|
||||
t.log(sender, "SetLinkDNSEx ", link.iif.Name, " ", strings.Join(common.Map(addresses, func(it LinkDNSEx) string {
|
||||
return M.SocksaddrFrom(M.AddrFromIP(it.Address), it.Port).String()
|
||||
}), ", "))
|
||||
} else {
|
||||
t.log(sender, "SetLinkDNSEx ", link.iif.Name, " (empty)")
|
||||
}
|
||||
return t.postUpdate(link)
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) SetLinkDomains(sender dbus.Sender, ifIndex int32, domains []LinkDomain) *dbus.Error {
|
||||
t.linkAccess.Lock()
|
||||
defer t.linkAccess.Unlock()
|
||||
link, err := t.getLink(ifIndex)
|
||||
if err != nil {
|
||||
return wrapError(err)
|
||||
}
|
||||
link.domain = domains
|
||||
if len(domains) > 0 {
|
||||
t.log(sender, "SetLinkDomains ", link.iif.Name, " ", strings.Join(common.Map(domains, func(domain LinkDomain) string {
|
||||
if !domain.RoutingOnly {
|
||||
return domain.Domain
|
||||
} else {
|
||||
return "~" + domain.Domain
|
||||
}
|
||||
}), ", "))
|
||||
} else {
|
||||
t.log(sender, "SetLinkDomains ", link.iif.Name, " (empty)")
|
||||
}
|
||||
return t.postUpdate(link)
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) SetLinkDefaultRoute(sender dbus.Sender, ifIndex int32, defaultRoute bool) *dbus.Error {
|
||||
t.linkAccess.Lock()
|
||||
defer t.linkAccess.Unlock()
|
||||
link, err := t.getLink(ifIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
link.defaultRoute = defaultRoute
|
||||
if defaultRoute {
|
||||
t.defaultRouteSequence = append(common.Filter(t.defaultRouteSequence, func(it int32) bool { return it != ifIndex }), ifIndex)
|
||||
} else {
|
||||
t.defaultRouteSequence = common.Filter(t.defaultRouteSequence, func(it int32) bool { return it != ifIndex })
|
||||
}
|
||||
var defaultRouteString string
|
||||
if defaultRoute {
|
||||
defaultRouteString = "yes"
|
||||
} else {
|
||||
defaultRouteString = "no"
|
||||
}
|
||||
t.log(sender, "SetLinkDefaultRoute ", link.iif.Name, " ", defaultRouteString)
|
||||
return t.postUpdate(link)
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) SetLinkLLMNR(ifIndex int32, llmnrMode string) *dbus.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) SetLinkMulticastDNS(ifIndex int32, mdnsMode string) *dbus.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) SetLinkDNSOverTLS(sender dbus.Sender, ifIndex int32, dotMode string) *dbus.Error {
|
||||
t.linkAccess.Lock()
|
||||
defer t.linkAccess.Unlock()
|
||||
link, err := t.getLink(ifIndex)
|
||||
if err != nil {
|
||||
return wrapError(err)
|
||||
}
|
||||
switch dotMode {
|
||||
case "yes":
|
||||
link.dnsOverTLS = true
|
||||
case "":
|
||||
dotMode = "no"
|
||||
fallthrough
|
||||
case "opportunistic", "no":
|
||||
link.dnsOverTLS = false
|
||||
}
|
||||
t.log(sender, "SetLinkDNSOverTLS ", link.iif.Name, " ", dotMode)
|
||||
return t.postUpdate(link)
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) SetLinkDNSSEC(ifIndex int32, dnssecMode string) *dbus.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) SetLinkDNSSECNegativeTrustAnchors(ifIndex int32, domains []string) *dbus.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) RevertLink(sender dbus.Sender, ifIndex int32) *dbus.Error {
|
||||
t.linkAccess.Lock()
|
||||
defer t.linkAccess.Unlock()
|
||||
link, err := t.getLink(ifIndex)
|
||||
if err != nil {
|
||||
return wrapError(err)
|
||||
}
|
||||
delete(t.links, ifIndex)
|
||||
t.log(sender, "RevertLink ", link.iif.Name)
|
||||
return t.postUpdate(link)
|
||||
}
|
||||
|
||||
// TODO: implement RegisterService, UnregisterService
|
||||
|
||||
func (t *resolve1Manager) RegisterService(sender dbus.Sender, identifier string, nameTemplate string, serviceType string, port uint16, priority uint16, weight uint16, txtRecords []TXTRecord) (objectPath dbus.ObjectPath, dbusErr *dbus.Error) {
|
||||
return "", wrapError(E.New("not implemented"))
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) UnregisterService(sender dbus.Sender, servicePath dbus.ObjectPath) error {
|
||||
return wrapError(E.New("not implemented"))
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) ResetStatistics() *dbus.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) FlushCaches(sender dbus.Sender) *dbus.Error {
|
||||
t.dnsRouter.ClearCache()
|
||||
t.log(sender, "FlushCaches")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) ResetServerFeatures() *dbus.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *resolve1Manager) postUpdate(link *TransportLink) *dbus.Error {
|
||||
if t.updateCallback != nil {
|
||||
return wrapError(t.updateCallback(link))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func rcodeError(rcode int) *dbus.Error {
|
||||
return dbus.NewError("org.freedesktop.resolve1.DnsError."+mDNS.RcodeToString[rcode], []any{mDNS.RcodeToString[rcode]})
|
||||
}
|
||||
|
||||
func wrapError(err error) *dbus.Error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var rcode dns.RcodeError
|
||||
if errors.As(err, &rcode) {
|
||||
return rcodeError(int(rcode))
|
||||
}
|
||||
return dbus.MakeFailedError(err)
|
||||
}
|
||||
252
service/resolved/service.go
Normal file
252
service/resolved/service.go
Normal file
@@ -0,0 +1,252 @@
|
||||
//go:build linux
|
||||
|
||||
package resolved
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
dnsOutbound "github.com/sagernet/sing-box/protocol/dns"
|
||||
tun "github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func RegisterService(registry *boxService.Registry) {
|
||||
boxService.Register[option.ResolvedServiceOptions](registry, C.TypeResolved, NewService)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
boxService.Adapter
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
network adapter.NetworkManager
|
||||
dnsRouter adapter.DNSRouter
|
||||
listener *listener.Listener
|
||||
systemBus *dbus.Conn
|
||||
linkAccess sync.RWMutex
|
||||
links map[int32]*TransportLink
|
||||
defaultRouteSequence []int32
|
||||
networkUpdateCallback *list.Element[tun.NetworkUpdateCallback]
|
||||
updateCallback func(*TransportLink) error
|
||||
deleteCallback func(*TransportLink)
|
||||
}
|
||||
|
||||
type TransportLink struct {
|
||||
iif *control.Interface
|
||||
address []LinkDNS
|
||||
addressEx []LinkDNSEx
|
||||
domain []LinkDomain
|
||||
defaultRoute bool
|
||||
dnsOverTLS bool
|
||||
// dnsOverTLSFallback bool
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedServiceOptions) (adapter.Service, error) {
|
||||
inbound := &Service{
|
||||
Adapter: boxService.NewAdapter(C.TypeResolved, tag),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
network: service.FromContext[adapter.NetworkManager](ctx),
|
||||
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
|
||||
links: make(map[int32]*TransportLink),
|
||||
}
|
||||
inbound.listener = listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Network: []string{N.NetworkTCP, N.NetworkUDP},
|
||||
Listen: options.ListenOptions,
|
||||
ConnectionHandler: inbound,
|
||||
OOBPacketHandler: inbound,
|
||||
ThreadUnsafePacketWriter: true,
|
||||
})
|
||||
return inbound, nil
|
||||
}
|
||||
|
||||
func (i *Service) Start(stage adapter.StartStage) error {
|
||||
switch stage {
|
||||
case adapter.StartStateInitialize:
|
||||
inboundManager := service.FromContext[adapter.ServiceManager](i.ctx)
|
||||
for _, transport := range inboundManager.Services() {
|
||||
if transport.Type() == C.TypeResolved && transport != i {
|
||||
return E.New("multiple resolved service are not supported")
|
||||
}
|
||||
}
|
||||
case adapter.StartStateStart:
|
||||
err := i.listener.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
systemBus, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.systemBus = systemBus
|
||||
err = systemBus.Export((*resolve1Manager)(i), "/org/freedesktop/resolve1", "org.freedesktop.resolve1.Manager")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply, err := systemBus.RequestName("org.freedesktop.resolve1", dbus.NameFlagDoNotQueue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch reply {
|
||||
case dbus.RequestNameReplyPrimaryOwner:
|
||||
case dbus.RequestNameReplyExists:
|
||||
return E.New("D-Bus object already exists, maybe real resolved is running")
|
||||
default:
|
||||
return E.New("unknown request name reply: ", reply)
|
||||
}
|
||||
i.networkUpdateCallback = i.network.NetworkMonitor().RegisterCallback(i.onNetworkUpdate)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Service) Close() error {
|
||||
if i.networkUpdateCallback != nil {
|
||||
i.network.NetworkMonitor().UnregisterCallback(i.networkUpdateCallback)
|
||||
}
|
||||
if i.systemBus != nil {
|
||||
i.systemBus.ReleaseName("org.freedesktop.resolve1")
|
||||
i.systemBus.Close()
|
||||
}
|
||||
return i.listener.Close()
|
||||
}
|
||||
|
||||
func (i *Service) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
metadata.Inbound = i.Tag()
|
||||
metadata.InboundType = i.Type()
|
||||
metadata.Destination = M.Socksaddr{}
|
||||
for {
|
||||
conn.SetReadDeadline(time.Now().Add(C.DNSTimeout))
|
||||
err := dnsOutbound.HandleStreamDNSRequest(ctx, i.dnsRouter, conn, metadata)
|
||||
if err != nil {
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Service) NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr) {
|
||||
go i.exchangePacket(buffer, oob, source)
|
||||
}
|
||||
|
||||
func (i *Service) exchangePacket(buffer *buf.Buffer, oob []byte, source M.Socksaddr) {
|
||||
ctx := log.ContextWithNewID(i.ctx)
|
||||
err := i.exchangePacket0(ctx, buffer, oob, source)
|
||||
if err != nil {
|
||||
i.logger.ErrorContext(ctx, "process DNS packet: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Service) exchangePacket0(ctx context.Context, buffer *buf.Buffer, oob []byte, source M.Socksaddr) error {
|
||||
var message mDNS.Msg
|
||||
err := message.Unpack(buffer.Bytes())
|
||||
buffer.Release()
|
||||
if err != nil {
|
||||
return E.Cause(err, "unpack request")
|
||||
}
|
||||
var metadata adapter.InboundContext
|
||||
metadata.Source = source
|
||||
response, err := i.dnsRouter.Exchange(adapter.WithContext(ctx, &metadata), &message, adapter.DNSQueryOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
responseBuffer, err := dns.TruncateDNSMessage(&message, response, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer responseBuffer.Release()
|
||||
_, _, err = i.listener.UDPConn().WriteMsgUDPAddrPort(responseBuffer.Bytes(), oob, source.AddrPort())
|
||||
return err
|
||||
}
|
||||
|
||||
func (i *Service) onNetworkUpdate() {
|
||||
i.linkAccess.Lock()
|
||||
defer i.linkAccess.Unlock()
|
||||
var deleteIfIndex []int
|
||||
for ifIndex, link := range i.links {
|
||||
iif, err := i.network.InterfaceFinder().ByIndex(int(ifIndex))
|
||||
if err != nil || iif != link.iif {
|
||||
deleteIfIndex = append(deleteIfIndex, int(ifIndex))
|
||||
}
|
||||
i.defaultRouteSequence = common.Filter(i.defaultRouteSequence, func(it int32) bool {
|
||||
return it != ifIndex
|
||||
})
|
||||
if i.deleteCallback != nil {
|
||||
i.deleteCallback(link)
|
||||
}
|
||||
}
|
||||
for _, ifIndex := range deleteIfIndex {
|
||||
delete(i.links, int32(ifIndex))
|
||||
}
|
||||
}
|
||||
|
||||
func (conf *TransportLink) nameList(ndots int, name string) []string {
|
||||
search := common.Map(common.Filter(conf.domain, func(it LinkDomain) bool {
|
||||
return !it.RoutingOnly
|
||||
}), func(it LinkDomain) string {
|
||||
return it.Domain
|
||||
})
|
||||
|
||||
l := len(name)
|
||||
rooted := l > 0 && name[l-1] == '.'
|
||||
if l > 254 || l == 254 && !rooted {
|
||||
return nil
|
||||
}
|
||||
|
||||
if rooted {
|
||||
if avoidDNS(name) {
|
||||
return nil
|
||||
}
|
||||
return []string{name}
|
||||
}
|
||||
|
||||
hasNdots := strings.Count(name, ".") >= ndots
|
||||
name += "."
|
||||
// l++
|
||||
|
||||
names := make([]string, 0, 1+len(search))
|
||||
if hasNdots && !avoidDNS(name) {
|
||||
names = append(names, name)
|
||||
}
|
||||
for _, suffix := range search {
|
||||
fqdn := name + suffix
|
||||
if !avoidDNS(fqdn) && len(fqdn) <= 254 {
|
||||
names = append(names, fqdn)
|
||||
}
|
||||
}
|
||||
if !hasNdots && !avoidDNS(name) {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func avoidDNS(name string) bool {
|
||||
if name == "" {
|
||||
return true
|
||||
}
|
||||
if name[len(name)-1] == '.' {
|
||||
name = name[:len(name)-1]
|
||||
}
|
||||
return strings.HasSuffix(name, ".onion")
|
||||
}
|
||||
27
service/resolved/stub.go
Normal file
27
service/resolved/stub.go
Normal file
@@ -0,0 +1,27 @@
|
||||
//go:build !linux
|
||||
|
||||
package resolved
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func RegisterService(registry *boxService.Registry) {
|
||||
boxService.Register[option.ResolvedServiceOptions](registry, C.TypeResolved, func(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedServiceOptions) (adapter.Service, error) {
|
||||
return nil, E.New("resolved service is only supported on Linux")
|
||||
})
|
||||
}
|
||||
|
||||
func RegisterTransport(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.ResolvedDNSServerOptions](registry, C.TypeResolved, func(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
return nil, E.New("resolved DNS server is only supported on Linux")
|
||||
})
|
||||
}
|
||||
294
service/resolved/transport.go
Normal file
294
service/resolved/transport.go
Normal file
@@ -0,0 +1,294 @@
|
||||
//go:build linux
|
||||
|
||||
package resolved
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func RegisterTransport(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.ResolvedDNSServerOptions](registry, C.TypeResolved, NewTransport)
|
||||
}
|
||||
|
||||
var _ adapter.DNSTransport = (*Transport)(nil)
|
||||
|
||||
type Transport struct {
|
||||
dns.TransportAdapter
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
serviceTag string
|
||||
acceptDefaultResolvers bool
|
||||
ndots int
|
||||
timeout time.Duration
|
||||
attempts int
|
||||
rotate bool
|
||||
service *Service
|
||||
linkAccess sync.RWMutex
|
||||
linkServers map[*TransportLink]*LinkServers
|
||||
}
|
||||
|
||||
type LinkServers struct {
|
||||
Link *TransportLink
|
||||
Servers []adapter.DNSTransport
|
||||
serverOffset uint32
|
||||
}
|
||||
|
||||
func (c *LinkServers) ServerOffset(rotate bool) uint32 {
|
||||
if rotate {
|
||||
return atomic.AddUint32(&c.serverOffset, 1) - 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
return &Transport{
|
||||
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeDHCP, tag, nil),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
serviceTag: options.Service,
|
||||
acceptDefaultResolvers: options.AcceptDefaultResolvers,
|
||||
// ndots: options.NDots,
|
||||
// timeout: time.Duration(options.Timeout),
|
||||
// attempts: options.Attempts,
|
||||
// rotate: options.Rotate,
|
||||
ndots: 1,
|
||||
timeout: 5 * time.Second,
|
||||
attempts: 2,
|
||||
linkServers: make(map[*TransportLink]*LinkServers),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Transport) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateInitialize {
|
||||
return nil
|
||||
}
|
||||
serviceManager := service.FromContext[adapter.ServiceManager](t.ctx)
|
||||
service, loaded := serviceManager.Get(t.serviceTag)
|
||||
if !loaded {
|
||||
return E.New("service not found: ", t.serviceTag)
|
||||
}
|
||||
resolvedInbound, isResolved := service.(*Service)
|
||||
if !isResolved {
|
||||
return E.New("service is not resolved: ", t.serviceTag)
|
||||
}
|
||||
resolvedInbound.updateCallback = t.updateTransports
|
||||
resolvedInbound.deleteCallback = t.deleteTransport
|
||||
t.service = resolvedInbound
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) Close() error {
|
||||
t.linkAccess.RLock()
|
||||
defer t.linkAccess.RUnlock()
|
||||
for _, servers := range t.linkServers {
|
||||
for _, server := range servers.Servers {
|
||||
server.Close()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) updateTransports(link *TransportLink) error {
|
||||
t.linkAccess.Lock()
|
||||
defer t.linkAccess.Unlock()
|
||||
if servers, loaded := t.linkServers[link]; loaded {
|
||||
for _, server := range servers.Servers {
|
||||
server.Close()
|
||||
}
|
||||
}
|
||||
serverDialer := common.Must1(dialer.NewDefault(t.ctx, option.DialerOptions{
|
||||
BindInterface: link.iif.Name,
|
||||
UDPFragmentDefault: true,
|
||||
}))
|
||||
var transports []adapter.DNSTransport
|
||||
for _, address := range link.address {
|
||||
serverAddr, ok := netip.AddrFromSlice(address.Address)
|
||||
if !ok {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
if link.dnsOverTLS {
|
||||
tlsConfig := common.Must1(tls.NewClient(t.ctx, serverAddr.String(), option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: serverAddr.String(),
|
||||
}))
|
||||
transports = append(transports, transport.NewTLSRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, 53), tlsConfig))
|
||||
|
||||
} else {
|
||||
transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, 53)))
|
||||
}
|
||||
}
|
||||
for _, address := range link.addressEx {
|
||||
serverAddr, ok := netip.AddrFromSlice(address.Address)
|
||||
if !ok {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
if link.dnsOverTLS {
|
||||
var serverName string
|
||||
if address.Name != "" {
|
||||
serverName = address.Name
|
||||
} else {
|
||||
serverName = serverAddr.String()
|
||||
}
|
||||
tlsConfig := common.Must1(tls.NewClient(t.ctx, serverAddr.String(), option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: serverName,
|
||||
}))
|
||||
transports = append(transports, transport.NewTLSRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, address.Port), tlsConfig))
|
||||
|
||||
} else {
|
||||
transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, address.Port)))
|
||||
}
|
||||
}
|
||||
t.linkServers[link] = &LinkServers{
|
||||
Link: link,
|
||||
Servers: transports,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) deleteTransport(link *TransportLink) {
|
||||
t.linkAccess.Lock()
|
||||
defer t.linkAccess.Unlock()
|
||||
servers, loaded := t.linkServers[link]
|
||||
if !loaded {
|
||||
return
|
||||
}
|
||||
for _, server := range servers.Servers {
|
||||
server.Close()
|
||||
}
|
||||
delete(t.linkServers, link)
|
||||
}
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
question := message.Question[0]
|
||||
var selectedLink *TransportLink
|
||||
t.service.linkAccess.RLock()
|
||||
for _, link := range t.service.links {
|
||||
for _, domain := range link.domain {
|
||||
if strings.HasSuffix(question.Name, domain.Domain) {
|
||||
selectedLink = link
|
||||
}
|
||||
}
|
||||
}
|
||||
if selectedLink == nil && t.acceptDefaultResolvers {
|
||||
for l := len(t.service.defaultRouteSequence); l > 0; l-- {
|
||||
selectedLink = t.service.links[t.service.defaultRouteSequence[l-1]]
|
||||
if len(selectedLink.address) > 0 || len(selectedLink.addressEx) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
t.service.linkAccess.RUnlock()
|
||||
if selectedLink == nil {
|
||||
return dns.FixedResponseStatus(message, mDNS.RcodeNameError), nil
|
||||
}
|
||||
t.linkAccess.RLock()
|
||||
servers := t.linkServers[selectedLink]
|
||||
t.linkAccess.RUnlock()
|
||||
if len(servers.Servers) == 0 {
|
||||
return dns.FixedResponseStatus(message, mDNS.RcodeNameError), nil
|
||||
}
|
||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||
return t.exchangeParallel(ctx, servers, message)
|
||||
} else {
|
||||
return t.exchangeSingleRequest(ctx, servers, message)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeSingleRequest(ctx context.Context, servers *LinkServers, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
var lastErr error
|
||||
for _, fqdn := range servers.Link.nameList(t.ndots, message.Question[0].Name) {
|
||||
response, err := t.tryOneName(ctx, servers, message, fqdn)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
func (t *Transport) tryOneName(ctx context.Context, servers *LinkServers, message *mDNS.Msg, fqdn string) (*mDNS.Msg, error) {
|
||||
serverOffset := servers.ServerOffset(t.rotate)
|
||||
sLen := uint32(len(servers.Servers))
|
||||
var lastErr error
|
||||
for i := 0; i < t.attempts; i++ {
|
||||
for j := uint32(0); j < sLen; j++ {
|
||||
server := servers.Servers[(serverOffset+j)%sLen]
|
||||
question := message.Question[0]
|
||||
question.Name = fqdn
|
||||
exchangeMessage := *message
|
||||
exchangeMessage.Question = []mDNS.Question{question}
|
||||
exchangeCtx, cancel := context.WithTimeout(ctx, t.timeout)
|
||||
response, err := server.Exchange(exchangeCtx, &exchangeMessage)
|
||||
cancel()
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
return nil, E.Cause(lastErr, fqdn)
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeParallel(ctx context.Context, servers *LinkServers, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
returned := make(chan struct{})
|
||||
defer close(returned)
|
||||
type queryResult struct {
|
||||
response *mDNS.Msg
|
||||
err error
|
||||
}
|
||||
results := make(chan queryResult)
|
||||
startRacer := func(ctx context.Context, fqdn string) {
|
||||
response, err := t.tryOneName(ctx, servers, message, fqdn)
|
||||
select {
|
||||
case results <- queryResult{response, err}:
|
||||
case <-returned:
|
||||
}
|
||||
}
|
||||
queryCtx, queryCancel := context.WithCancel(ctx)
|
||||
defer queryCancel()
|
||||
var nameCount int
|
||||
for _, fqdn := range servers.Link.nameList(t.ndots, message.Question[0].Name) {
|
||||
nameCount++
|
||||
go startRacer(queryCtx, fqdn)
|
||||
}
|
||||
var errors []error
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case result := <-results:
|
||||
if result.err == nil {
|
||||
return result.response, nil
|
||||
}
|
||||
errors = append(errors, result.err)
|
||||
if len(errors) == nameCount {
|
||||
return nil, E.Errors(errors...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
177
service/ssmapi/api.go
Normal file
177
service/ssmapi/api.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package ssmapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
sHTTP "github.com/sagernet/sing/protocol/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
type APIServer struct {
|
||||
logger logger.Logger
|
||||
traffic *TrafficManager
|
||||
user *UserManager
|
||||
}
|
||||
|
||||
func NewAPIServer(logger logger.Logger, traffic *TrafficManager, user *UserManager) *APIServer {
|
||||
return &APIServer{
|
||||
logger: logger,
|
||||
traffic: traffic,
|
||||
user: user,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *APIServer) Route(r chi.Router) {
|
||||
r.Route("/server/v1", func(r chi.Router) {
|
||||
r.Use(func(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
s.logger.Debug(request.Method, " ", request.RequestURI, " ", sHTTP.SourceAddress(request))
|
||||
handler.ServeHTTP(writer, request)
|
||||
})
|
||||
})
|
||||
r.Get("/", s.getServerInfo)
|
||||
r.Get("/users", s.listUser)
|
||||
r.Post("/users", s.addUser)
|
||||
r.Get("/users/{username}", s.getUser)
|
||||
r.Put("/users/{username}", s.updateUser)
|
||||
r.Delete("/users/{username}", s.deleteUser)
|
||||
r.Get("/stats", s.getStats)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *APIServer) getServerInfo(writer http.ResponseWriter, request *http.Request) {
|
||||
render.JSON(writer, request, render.M{
|
||||
"server": "sing-box " + C.Version,
|
||||
"apiVersion": "v1",
|
||||
})
|
||||
}
|
||||
|
||||
type UserObject struct {
|
||||
UserName string `json:"username"`
|
||||
Password string `json:"uPSK,omitempty"`
|
||||
DownlinkBytes int64 `json:"downlinkBytes"`
|
||||
UplinkBytes int64 `json:"uplinkBytes"`
|
||||
DownlinkPackets int64 `json:"downlinkPackets"`
|
||||
UplinkPackets int64 `json:"uplinkPackets"`
|
||||
TCPSessions int64 `json:"tcpSessions"`
|
||||
UDPSessions int64 `json:"udpSessions"`
|
||||
}
|
||||
|
||||
func (s *APIServer) listUser(writer http.ResponseWriter, request *http.Request) {
|
||||
render.JSON(writer, request, render.M{
|
||||
"users": s.user.List(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *APIServer) addUser(writer http.ResponseWriter, request *http.Request) {
|
||||
var addRequest struct {
|
||||
UserName string `json:"username"`
|
||||
Password string `json:"uPSK"`
|
||||
}
|
||||
err := render.DecodeJSON(request.Body, &addRequest)
|
||||
if err != nil {
|
||||
render.Status(request, http.StatusBadRequest)
|
||||
render.PlainText(writer, request, err.Error())
|
||||
return
|
||||
}
|
||||
err = s.user.Add(addRequest.UserName, addRequest.Password)
|
||||
if err != nil {
|
||||
render.Status(request, http.StatusBadRequest)
|
||||
render.PlainText(writer, request, err.Error())
|
||||
return
|
||||
}
|
||||
writer.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
func (s *APIServer) getUser(writer http.ResponseWriter, request *http.Request) {
|
||||
userName := chi.URLParam(request, "username")
|
||||
if userName == "" {
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
uPSK, loaded := s.user.Get(userName)
|
||||
if !loaded {
|
||||
writer.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
user := UserObject{
|
||||
UserName: userName,
|
||||
Password: uPSK,
|
||||
}
|
||||
s.traffic.ReadUser(&user)
|
||||
render.JSON(writer, request, user)
|
||||
}
|
||||
|
||||
func (s *APIServer) updateUser(writer http.ResponseWriter, request *http.Request) {
|
||||
userName := chi.URLParam(request, "username")
|
||||
if userName == "" {
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var updateRequest struct {
|
||||
Password string `json:"uPSK"`
|
||||
}
|
||||
err := render.DecodeJSON(request.Body, &updateRequest)
|
||||
if err != nil {
|
||||
render.Status(request, http.StatusBadRequest)
|
||||
render.PlainText(writer, request, err.Error())
|
||||
return
|
||||
}
|
||||
_, loaded := s.user.Get(userName)
|
||||
if !loaded {
|
||||
writer.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
err = s.user.Update(userName, updateRequest.Password)
|
||||
if err != nil {
|
||||
render.Status(request, http.StatusBadRequest)
|
||||
render.PlainText(writer, request, err.Error())
|
||||
return
|
||||
}
|
||||
writer.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *APIServer) deleteUser(writer http.ResponseWriter, request *http.Request) {
|
||||
userName := chi.URLParam(request, "username")
|
||||
if userName == "" {
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, loaded := s.user.Get(userName)
|
||||
if !loaded {
|
||||
writer.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
err := s.user.Delete(userName)
|
||||
if err != nil {
|
||||
render.Status(request, http.StatusBadRequest)
|
||||
render.PlainText(writer, request, err.Error())
|
||||
return
|
||||
}
|
||||
writer.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *APIServer) getStats(writer http.ResponseWriter, request *http.Request) {
|
||||
requireClear := chi.URLParam(request, "clear") == "true"
|
||||
|
||||
users := s.user.List()
|
||||
s.traffic.ReadUsers(users, requireClear)
|
||||
for i := range users {
|
||||
users[i].Password = ""
|
||||
}
|
||||
uplinkBytes, downlinkBytes, uplinkPackets, downlinkPackets, tcpSessions, udpSessions := s.traffic.ReadGlobal(requireClear)
|
||||
|
||||
render.JSON(writer, request, render.M{
|
||||
"uplinkBytes": uplinkBytes,
|
||||
"downlinkBytes": downlinkBytes,
|
||||
"uplinkPackets": uplinkPackets,
|
||||
"downlinkPackets": downlinkPackets,
|
||||
"tcpSessions": tcpSessions,
|
||||
"udpSessions": udpSessions,
|
||||
"users": users,
|
||||
})
|
||||
}
|
||||
117
service/ssmapi/server.go
Normal file
117
service/ssmapi/server.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package ssmapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
func RegisterService(registry *boxService.Registry) {
|
||||
boxService.Register[option.SSMAPIServiceOptions](registry, C.TypeSSMAPI, NewService)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
boxService.Adapter
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
listener *listener.Listener
|
||||
tlsConfig tls.ServerConfig
|
||||
httpServer *http.Server
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.SSMAPIServiceOptions) (adapter.Service, error) {
|
||||
chiRouter := chi.NewRouter()
|
||||
s := &Service{
|
||||
Adapter: boxService.NewAdapter(C.TypeSSMAPI, tag),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
listener: listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Network: []string{N.NetworkTCP},
|
||||
Listen: options.ListenOptions,
|
||||
}),
|
||||
httpServer: &http.Server{
|
||||
Handler: chiRouter,
|
||||
},
|
||||
}
|
||||
inboundManager := service.FromContext[adapter.InboundManager](ctx)
|
||||
if options.Servers.Size() == 0 {
|
||||
return nil, E.New("missing servers")
|
||||
}
|
||||
for i, entry := range options.Servers.Entries() {
|
||||
inbound, loaded := inboundManager.Get(entry.Value)
|
||||
if !loaded {
|
||||
return nil, E.New("parse SSM server[", i, "]: inbound ", entry.Value, "not found")
|
||||
}
|
||||
managedServer, isManaged := inbound.(adapter.ManagedSSMServer)
|
||||
if !isManaged {
|
||||
return nil, E.New("parse SSM server[", i, "]: inbound/", inbound.Type(), "[", inbound.Tag(), "] is not a SSM server")
|
||||
}
|
||||
traffic := NewTrafficManager()
|
||||
managedServer.SetTracker(traffic)
|
||||
user := NewUserManager(managedServer, traffic)
|
||||
chiRouter.Route(entry.Key, NewAPIServer(logger, traffic, user).Route)
|
||||
}
|
||||
if options.TLS != nil {
|
||||
tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.tlsConfig = tlsConfig
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Service) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
if s.tlsConfig != nil {
|
||||
err := s.tlsConfig.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "create TLS config")
|
||||
}
|
||||
}
|
||||
tcpListener, err := s.listener.ListenTCP()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.tlsConfig != nil {
|
||||
if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) {
|
||||
s.tlsConfig.SetNextProtos(append([]string{"h2"}, s.tlsConfig.NextProtos()...))
|
||||
}
|
||||
tcpListener = aTLS.NewListener(tcpListener, s.tlsConfig)
|
||||
}
|
||||
go func() {
|
||||
err = s.httpServer.Serve(tcpListener)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
s.logger.Error("serve error: ", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) Close() error {
|
||||
return common.Close(
|
||||
common.PtrOrNil(s.httpServer),
|
||||
common.PtrOrNil(s.listener),
|
||||
s.tlsConfig,
|
||||
)
|
||||
}
|
||||
212
service/ssmapi/traffic.go
Normal file
212
service/ssmapi/traffic.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package ssmapi
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing/common/atomic"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
var _ adapter.SSMTracker = (*TrafficManager)(nil)
|
||||
|
||||
type TrafficManager struct {
|
||||
globalUplink atomic.Int64
|
||||
globalDownlink atomic.Int64
|
||||
globalUplinkPackets atomic.Int64
|
||||
globalDownlinkPackets atomic.Int64
|
||||
globalTCPSessions atomic.Int64
|
||||
globalUDPSessions atomic.Int64
|
||||
userAccess sync.Mutex
|
||||
userUplink map[string]*atomic.Int64
|
||||
userDownlink map[string]*atomic.Int64
|
||||
userUplinkPackets map[string]*atomic.Int64
|
||||
userDownlinkPackets map[string]*atomic.Int64
|
||||
userTCPSessions map[string]*atomic.Int64
|
||||
userUDPSessions map[string]*atomic.Int64
|
||||
}
|
||||
|
||||
func NewTrafficManager() *TrafficManager {
|
||||
manager := &TrafficManager{
|
||||
userUplink: make(map[string]*atomic.Int64),
|
||||
userDownlink: make(map[string]*atomic.Int64),
|
||||
userUplinkPackets: make(map[string]*atomic.Int64),
|
||||
userDownlinkPackets: make(map[string]*atomic.Int64),
|
||||
userTCPSessions: make(map[string]*atomic.Int64),
|
||||
userUDPSessions: make(map[string]*atomic.Int64),
|
||||
}
|
||||
return manager
|
||||
}
|
||||
|
||||
func (s *TrafficManager) UpdateUsers(users []string) {
|
||||
s.userAccess.Lock()
|
||||
defer s.userAccess.Unlock()
|
||||
newUserUplink := make(map[string]*atomic.Int64)
|
||||
newUserDownlink := make(map[string]*atomic.Int64)
|
||||
newUserUplinkPackets := make(map[string]*atomic.Int64)
|
||||
newUserDownlinkPackets := make(map[string]*atomic.Int64)
|
||||
newUserTCPSessions := make(map[string]*atomic.Int64)
|
||||
newUserUDPSessions := make(map[string]*atomic.Int64)
|
||||
for _, user := range users {
|
||||
newUserUplink[user] = s.userUplinkPackets[user]
|
||||
newUserDownlink[user] = s.userDownlinkPackets[user]
|
||||
newUserUplinkPackets[user] = s.userUplinkPackets[user]
|
||||
newUserDownlinkPackets[user] = s.userDownlinkPackets[user]
|
||||
newUserTCPSessions[user] = s.userTCPSessions[user]
|
||||
newUserUDPSessions[user] = s.userUDPSessions[user]
|
||||
}
|
||||
s.userUplink = newUserUplink
|
||||
s.userDownlink = newUserDownlink
|
||||
s.userUplinkPackets = newUserUplinkPackets
|
||||
s.userDownlinkPackets = newUserDownlinkPackets
|
||||
s.userTCPSessions = newUserTCPSessions
|
||||
s.userUDPSessions = newUserUDPSessions
|
||||
}
|
||||
|
||||
func (s *TrafficManager) userCounter(user string) (*atomic.Int64, *atomic.Int64, *atomic.Int64, *atomic.Int64, *atomic.Int64, *atomic.Int64) {
|
||||
s.userAccess.Lock()
|
||||
defer s.userAccess.Unlock()
|
||||
upCounter, loaded := s.userUplink[user]
|
||||
if !loaded {
|
||||
upCounter = new(atomic.Int64)
|
||||
s.userUplink[user] = upCounter
|
||||
}
|
||||
downCounter, loaded := s.userDownlink[user]
|
||||
if !loaded {
|
||||
downCounter = new(atomic.Int64)
|
||||
s.userDownlink[user] = downCounter
|
||||
}
|
||||
upPacketsCounter, loaded := s.userUplinkPackets[user]
|
||||
if !loaded {
|
||||
upPacketsCounter = new(atomic.Int64)
|
||||
s.userUplinkPackets[user] = upPacketsCounter
|
||||
}
|
||||
downPacketsCounter, loaded := s.userDownlinkPackets[user]
|
||||
if !loaded {
|
||||
downPacketsCounter = new(atomic.Int64)
|
||||
s.userDownlinkPackets[user] = downPacketsCounter
|
||||
}
|
||||
tcpSessionsCounter, loaded := s.userTCPSessions[user]
|
||||
if !loaded {
|
||||
tcpSessionsCounter = new(atomic.Int64)
|
||||
s.userTCPSessions[user] = tcpSessionsCounter
|
||||
}
|
||||
udpSessionsCounter, loaded := s.userUDPSessions[user]
|
||||
if !loaded {
|
||||
udpSessionsCounter = new(atomic.Int64)
|
||||
s.userUDPSessions[user] = udpSessionsCounter
|
||||
}
|
||||
return upCounter, downCounter, upPacketsCounter, downPacketsCounter, tcpSessionsCounter, udpSessionsCounter
|
||||
}
|
||||
|
||||
func (s *TrafficManager) TrackConnection(conn net.Conn, metadata adapter.InboundContext) net.Conn {
|
||||
s.globalTCPSessions.Add(1)
|
||||
var readCounter []*atomic.Int64
|
||||
var writeCounter []*atomic.Int64
|
||||
readCounter = append(readCounter, &s.globalUplink)
|
||||
writeCounter = append(writeCounter, &s.globalDownlink)
|
||||
upCounter, downCounter, _, _, tcpSessionCounter, _ := s.userCounter(metadata.User)
|
||||
readCounter = append(readCounter, upCounter)
|
||||
writeCounter = append(writeCounter, downCounter)
|
||||
tcpSessionCounter.Add(1)
|
||||
return bufio.NewInt64CounterConn(conn, readCounter, writeCounter)
|
||||
}
|
||||
|
||||
func (s *TrafficManager) TrackPacketConnection(conn N.PacketConn, metadata adapter.InboundContext) N.PacketConn {
|
||||
s.globalUDPSessions.Add(1)
|
||||
var readCounter []*atomic.Int64
|
||||
var readPacketCounter []*atomic.Int64
|
||||
var writeCounter []*atomic.Int64
|
||||
var writePacketCounter []*atomic.Int64
|
||||
readCounter = append(readCounter, &s.globalUplink)
|
||||
writeCounter = append(writeCounter, &s.globalDownlink)
|
||||
readPacketCounter = append(readPacketCounter, &s.globalUplinkPackets)
|
||||
writePacketCounter = append(writePacketCounter, &s.globalDownlinkPackets)
|
||||
upCounter, downCounter, upPacketsCounter, downPacketsCounter, _, udpSessionCounter := s.userCounter(metadata.User)
|
||||
readCounter = append(readCounter, upCounter)
|
||||
writeCounter = append(writeCounter, downCounter)
|
||||
readPacketCounter = append(readPacketCounter, upPacketsCounter)
|
||||
writePacketCounter = append(writePacketCounter, downPacketsCounter)
|
||||
udpSessionCounter.Add(1)
|
||||
return bufio.NewInt64CounterPacketConn(conn, append(readCounter, readPacketCounter...), append(writeCounter, writePacketCounter...))
|
||||
}
|
||||
|
||||
func (s *TrafficManager) ReadUser(user *UserObject) {
|
||||
s.userAccess.Lock()
|
||||
defer s.userAccess.Unlock()
|
||||
s.readUser(user, false)
|
||||
}
|
||||
|
||||
func (s *TrafficManager) readUser(user *UserObject, swap bool) {
|
||||
if counter, loaded := s.userUplink[user.UserName]; loaded {
|
||||
if swap {
|
||||
user.UplinkBytes = counter.Swap(0)
|
||||
} else {
|
||||
user.UplinkBytes = counter.Load()
|
||||
}
|
||||
}
|
||||
if counter, loaded := s.userDownlink[user.UserName]; loaded {
|
||||
if swap {
|
||||
user.DownlinkBytes = counter.Swap(0)
|
||||
} else {
|
||||
user.DownlinkBytes = counter.Load()
|
||||
}
|
||||
}
|
||||
if counter, loaded := s.userUplinkPackets[user.UserName]; loaded {
|
||||
if swap {
|
||||
user.UplinkPackets = counter.Swap(0)
|
||||
} else {
|
||||
user.UplinkPackets = counter.Load()
|
||||
}
|
||||
}
|
||||
if counter, loaded := s.userDownlinkPackets[user.UserName]; loaded {
|
||||
if swap {
|
||||
user.DownlinkPackets = counter.Swap(0)
|
||||
} else {
|
||||
user.DownlinkPackets = counter.Load()
|
||||
}
|
||||
}
|
||||
if counter, loaded := s.userTCPSessions[user.UserName]; loaded {
|
||||
if swap {
|
||||
user.TCPSessions = counter.Swap(0)
|
||||
} else {
|
||||
user.TCPSessions = counter.Load()
|
||||
}
|
||||
}
|
||||
if counter, loaded := s.userUDPSessions[user.UserName]; loaded {
|
||||
if swap {
|
||||
user.UDPSessions = counter.Swap(0)
|
||||
} else {
|
||||
user.UDPSessions = counter.Load()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TrafficManager) ReadUsers(users []*UserObject, swap bool) {
|
||||
s.userAccess.Lock()
|
||||
defer s.userAccess.Unlock()
|
||||
for _, user := range users {
|
||||
s.readUser(user, swap)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *TrafficManager) ReadGlobal(swap bool) (uplinkBytes int64, downlinkBytes int64, uplinkPackets int64, downlinkPackets int64, tcpSessions int64, udpSessions int64) {
|
||||
if swap {
|
||||
return s.globalUplink.Swap(0),
|
||||
s.globalDownlink.Swap(0),
|
||||
s.globalUplinkPackets.Swap(0),
|
||||
s.globalDownlinkPackets.Swap(0),
|
||||
s.globalTCPSessions.Swap(0),
|
||||
s.globalUDPSessions.Swap(0)
|
||||
} else {
|
||||
return s.globalUplink.Load(),
|
||||
s.globalDownlink.Load(),
|
||||
s.globalUplinkPackets.Load(),
|
||||
s.globalDownlinkPackets.Load(),
|
||||
s.globalTCPSessions.Load(),
|
||||
s.globalUDPSessions.Load()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user