mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2026-03-04 04:47:30 +00:00
Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b975275f5 | ||
|
|
166392fe17 | ||
|
|
5c6aa433ca | ||
|
|
2c55dc2557 | ||
|
|
56c0b088e8 | ||
|
|
5344e869a8 | ||
|
|
6cfaf15cbf | ||
|
|
31f0060b30 | ||
|
|
c60750d549 | ||
|
|
ebf5918e94 | ||
|
|
93ca18517c | ||
|
|
32d447ce99 | ||
|
|
617fef84ae | ||
|
|
d19199322d | ||
|
|
87795e3a07 | ||
|
|
85bb40aaf8 | ||
|
|
082bcec281 | ||
|
|
9283cb0f5f | ||
|
|
ae7967f662 | ||
|
|
01f8f2db2f | ||
|
|
255ff5e977 | ||
|
|
939e4109d7 | ||
|
|
40587b62b8 | ||
|
|
85e6d25de5 | ||
|
|
29a37f4f4b | ||
|
|
2f9a3b3469 | ||
|
|
40ea0ba098 | ||
|
|
8d7f947a80 | ||
|
|
71a8705636 | ||
|
|
c0f452b540 | ||
|
|
6c9abe16cc | ||
|
|
213d80c1e2 | ||
|
|
1db89da122 | ||
|
|
689c58f661 | ||
|
|
33590c4066 | ||
|
|
60ae9dce56 | ||
|
|
4741ac6702 | ||
|
|
ef3d7e4dd7 | ||
|
|
a1c7881229 | ||
|
|
12e3952b74 | ||
|
|
88419cbd12 | ||
|
|
4ed830330e | ||
|
|
3ed6ff9402 | ||
|
|
34de62d21d | ||
|
|
d2e255f257 | ||
|
|
a0c46bb4b7 | ||
|
|
9e3bf14b1a | ||
|
|
28c387a9b6 | ||
|
|
15eda703b4 | ||
|
|
b1d12a15db | ||
|
|
5a21bf3642 | ||
|
|
199fb8fd5d | ||
|
|
fd959feff2 | ||
|
|
d5a03901d2 | ||
|
|
257fead538 | ||
|
|
c489c5260b | ||
|
|
8f92b1de13 | ||
|
|
9f7a2a36c1 | ||
|
|
a93479124c | ||
|
|
ed42c4feb8 | ||
|
|
608ddb1b44 | ||
|
|
d036d98128 | ||
|
|
d900c71214 | ||
|
|
1672750c47 | ||
|
|
41b57afb3f | ||
|
|
188372cb04 | ||
|
|
a1350d4985 | ||
|
|
dc958e6a39 | ||
|
|
8a5f3b8909 | ||
|
|
c6d7ef8cb8 | ||
|
|
bb8c47d83d | ||
|
|
5cf0f18c29 | ||
|
|
83213d493e | ||
|
|
90ed01ed53 | ||
|
|
f91a586da8 | ||
|
|
266fb03838 | ||
|
|
76e9607fd7 | ||
|
|
23e2d3a132 | ||
|
|
6e35cf9399 | ||
|
|
2116640886 | ||
|
|
a4fcd3af07 | ||
|
|
d22a893060 | ||
|
|
00cceba890 | ||
|
|
2b4726b9ad | ||
|
|
26e6d83f8b | ||
|
|
50d7834e09 | ||
|
|
86c127db8b | ||
|
|
febb6021aa | ||
|
|
9e57b298bf | ||
|
|
791ea5e568 | ||
|
|
7e7016b567 | ||
|
|
b4fe669848 | ||
|
|
cad26ac6a8 | ||
|
|
f328203bc1 | ||
|
|
5c40a6340c | ||
|
|
61d6a9abd6 | ||
|
|
a013ac32a3 | ||
|
|
ee5d77cfd1 | ||
|
|
936df90ace | ||
|
|
f774276896 | ||
|
|
aa51b9faba | ||
|
|
d55b047125 | ||
|
|
efc7abc6e0 | ||
|
|
c2301f66a4 | ||
|
|
468cfc3cc4 | ||
|
|
5dce957755 | ||
|
|
4ecb49b3b9 | ||
|
|
7de4af28d2 | ||
|
|
48d8efb3e9 | ||
|
|
e6e7aa5ae2 | ||
|
|
99aa1b0de1 | ||
|
|
52ad793d11 | ||
|
|
2fb9331211 | ||
|
|
793ce45db0 |
18
.github/release/.fpm_systemd
vendored
Normal file
18
.github/release/.fpm_systemd
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
-s dir
|
||||||
|
--name mihomo
|
||||||
|
--category net
|
||||||
|
--license GPL-3.0-or-later
|
||||||
|
--description "The universal proxy platform."
|
||||||
|
--url "https://wiki.metacubex.one/"
|
||||||
|
--maintainer "MetaCubeX <none@example.com>"
|
||||||
|
--deb-field "Bug: https://github.com/MetaCubeX/mihomo/issues"
|
||||||
|
--no-deb-generate-changes
|
||||||
|
--config-files /etc/mihomo/config.yaml
|
||||||
|
|
||||||
|
.github/release/config.yaml=/etc/mihomo/config.yaml
|
||||||
|
|
||||||
|
.github/release/mihomo.service=/usr/lib/systemd/system/mihomo.service
|
||||||
|
.github/release/mihomo@.service=/usr/lib/systemd/system/mihomo@.service
|
||||||
|
|
||||||
|
|
||||||
|
LICENSE=/usr/share/licenses/mihomo/LICENSE
|
||||||
15
.github/release/config.yaml
vendored
Normal file
15
.github/release/config.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
mixed-port: 7890
|
||||||
|
|
||||||
|
dns:
|
||||||
|
enable: true
|
||||||
|
ipv6: true
|
||||||
|
enhanced-mode: fake-ip
|
||||||
|
fake-ip-filter:
|
||||||
|
- "*"
|
||||||
|
- "+.lan"
|
||||||
|
- "+.local"
|
||||||
|
nameserver:
|
||||||
|
- system
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- MATCH,DIRECT
|
||||||
152
.github/workflows/build.yml
vendored
152
.github/workflows/build.yml
vendored
@@ -33,23 +33,25 @@ jobs:
|
|||||||
- { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible }
|
- { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible }
|
||||||
- { goos: darwin, goarch: amd64, goamd64: v3, output: amd64 }
|
- { goos: darwin, goarch: amd64, goamd64: v3, output: amd64 }
|
||||||
|
|
||||||
- { goos: linux, goarch: '386', output: '386' }
|
- { goos: linux, goarch: '386', go386: sse2, output: '386', debian: i386, rpm: i386}
|
||||||
|
- { goos: linux, goarch: '386', go386: softfloat, output: '386-softfloat' }
|
||||||
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible, test: test }
|
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible, test: test }
|
||||||
- { goos: linux, goarch: amd64, goamd64: v3, output: amd64 }
|
- { goos: linux, goarch: amd64, goamd64: v3, output: amd64, debian: amd64, rpm: x86_64, pacman: x86_64}
|
||||||
- { goos: linux, goarch: arm64, output: arm64 }
|
- { goos: linux, goarch: arm64, output: arm64, debian: arm64, rpm: aarch64, pacman: aarch64}
|
||||||
- { goos: linux, goarch: arm, goarm: '5', output: armv5 }
|
- { goos: linux, goarch: arm, goarm: '5', output: armv5 }
|
||||||
- { goos: linux, goarch: arm, goarm: '6', output: armv6 }
|
- { goos: linux, goarch: arm, goarm: '6', output: armv6, debian: armel, rpm: armv6hl}
|
||||||
- { goos: linux, goarch: arm, goarm: '7', output: armv7 }
|
- { goos: linux, goarch: arm, goarm: '7', output: armv7, debian: armhf, rpm: armv7hl, pacman: armv7hl}
|
||||||
- { goos: linux, goarch: mips, gomips: hardfloat, output: mips-hardfloat }
|
- { goos: linux, goarch: mips, gomips: hardfloat, output: mips-hardfloat }
|
||||||
- { goos: linux, goarch: mips, gomips: softfloat, output: mips-softfloat }
|
- { goos: linux, goarch: mips, gomips: softfloat, output: mips-softfloat }
|
||||||
- { goos: linux, goarch: mipsle, gomips: hardfloat, output: mipsle-hardfloat }
|
- { goos: linux, goarch: mipsle, gomips: hardfloat, output: mipsle-hardfloat }
|
||||||
- { goos: linux, goarch: mipsle, gomips: softfloat, output: mipsle-softfloat }
|
- { goos: linux, goarch: mipsle, gomips: softfloat, output: mipsle-softfloat }
|
||||||
- { goos: linux, goarch: mips64, output: mips64 }
|
- { goos: linux, goarch: mips64, output: mips64 }
|
||||||
- { goos: linux, goarch: mips64le, output: mips64le }
|
- { goos: linux, goarch: mips64le, output: mips64le, debian: mips64el, rpm: mips64el }
|
||||||
- { goos: linux, goarch: loong64, output: loong64-abi1, abi: '1' }
|
- { goos: linux, goarch: loong64, output: loong64-abi1, abi: '1', debian: loongarch64, rpm: loongarch64 }
|
||||||
- { goos: linux, goarch: loong64, output: loong64-abi2, abi: '2' }
|
- { goos: linux, goarch: loong64, output: loong64-abi2, abi: '2', debian: loong64, rpm: loong64 }
|
||||||
- { goos: linux, goarch: riscv64, output: riscv64 }
|
- { goos: linux, goarch: riscv64, output: riscv64, debian: riscv64, rpm: riscv64 }
|
||||||
- { goos: linux, goarch: s390x, output: s390x }
|
- { goos: linux, goarch: s390x, output: s390x, debian: s390x, rpm: s390x }
|
||||||
|
- { goos: linux, goarch: ppc64le, output: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||||
|
|
||||||
- { goos: windows, goarch: '386', output: '386' }
|
- { goos: windows, goarch: '386', output: '386' }
|
||||||
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible }
|
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible }
|
||||||
@@ -125,11 +127,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.jobs.goversion }}
|
go-version: ${{ matrix.jobs.goversion }}
|
||||||
|
|
||||||
- name: Set up Go1.23 loongarch abi1
|
- name: Set up Go1.24 loongarch abi1
|
||||||
if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }}
|
if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }}
|
||||||
run: |
|
run: |
|
||||||
wget -q https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.23.0/go1.23.0.linux-amd64-abi1.tar.gz
|
wget -q https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.24.0/go1.24.0.linux-amd64-abi1.tar.gz
|
||||||
sudo tar zxf go1.23.0.linux-amd64-abi1.tar.gz -C /usr/local
|
sudo tar zxf go1.24.0.linux-amd64-abi1.tar.gz -C /usr/local
|
||||||
echo "/usr/local/go/bin" >> $GITHUB_PATH
|
echo "/usr/local/go/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||||
@@ -194,17 +196,17 @@ jobs:
|
|||||||
curl https://github.com/golang/go/commit/9e43850a3298a9b8b1162ba0033d4c53f8637571.diff | patch --verbose -R -p 1
|
curl https://github.com/golang/go/commit/9e43850a3298a9b8b1162ba0033d4c53f8637571.diff | patch --verbose -R -p 1
|
||||||
|
|
||||||
- name: Set variables
|
- name: Set variables
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }}
|
|
||||||
run: echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Set variables
|
|
||||||
if: ${{ github.event_name != 'workflow_dispatch' && github.ref_name == 'Alpha' }}
|
|
||||||
run: echo "VERSION=alpha-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Set Time Variable
|
|
||||||
run: |
|
run: |
|
||||||
|
VERSION="${GITHUB_REF_NAME,,}-$(git rev-parse --short HEAD)"
|
||||||
|
VERSION="${VERSION//\//-}"
|
||||||
|
PackageVersion="$(curl -s "https://api.github.com/repos/MetaCubeX/mihomo/releases/latest" | jq -r '.tag_name' | sed 's/v//g' | awk -F '.' '{$NF = $NF + 1; print}' OFS='.').${VERSION/-/.}"
|
||||||
|
if [ -n "${{ github.event.inputs.version }}" ]; then
|
||||||
|
VERSION=${{ github.event.inputs.version }}
|
||||||
|
PackageVersion="${VERSION#v}"
|
||||||
|
fi
|
||||||
|
echo "VERSION=${VERSION}" >> $GITHUB_ENV
|
||||||
|
echo "PackageVersion=${PackageVersion}" >> $GITHUB_ENV
|
||||||
|
|
||||||
echo "BUILDTIME=$(date)" >> $GITHUB_ENV
|
echo "BUILDTIME=$(date)" >> $GITHUB_ENV
|
||||||
echo "CGO_ENABLED=0" >> $GITHUB_ENV
|
echo "CGO_ENABLED=0" >> $GITHUB_ENV
|
||||||
echo "BUILDTAG=-extldflags --static" >> $GITHUB_ENV
|
echo "BUILDTAG=-extldflags --static" >> $GITHUB_ENV
|
||||||
@@ -215,7 +217,7 @@ jobs:
|
|||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
with:
|
with:
|
||||||
ndk-version: r28-beta1
|
ndk-version: r29-beta1
|
||||||
|
|
||||||
- name: Set NDK path
|
- name: Set NDK path
|
||||||
if: ${{ matrix.jobs.goos == 'android' }}
|
if: ${{ matrix.jobs.goos == 'android' }}
|
||||||
@@ -233,7 +235,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Update CA
|
- name: Update CA
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install ca-certificates
|
sudo apt-get update && sudo apt-get install ca-certificates
|
||||||
sudo update-ca-certificates
|
sudo update-ca-certificates
|
||||||
cp -f /etc/ssl/certs/ca-certificates.crt component/ca/ca-certificates.crt
|
cp -f /etc/ssl/certs/ca-certificates.crt component/ca/ca-certificates.crt
|
||||||
|
|
||||||
@@ -242,6 +244,7 @@ jobs:
|
|||||||
GOOS: ${{matrix.jobs.goos}}
|
GOOS: ${{matrix.jobs.goos}}
|
||||||
GOARCH: ${{matrix.jobs.goarch}}
|
GOARCH: ${{matrix.jobs.goarch}}
|
||||||
GOAMD64: ${{matrix.jobs.goamd64}}
|
GOAMD64: ${{matrix.jobs.goamd64}}
|
||||||
|
GO386: ${{matrix.jobs.go386}}
|
||||||
GOARM: ${{matrix.jobs.goarm}}
|
GOARM: ${{matrix.jobs.goarm}}
|
||||||
GOMIPS: ${{matrix.jobs.gomips}}
|
GOMIPS: ${{matrix.jobs.gomips}}
|
||||||
run: |
|
run: |
|
||||||
@@ -256,79 +259,51 @@ jobs:
|
|||||||
rm mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}
|
rm mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Create DEB package
|
- name: Package DEB
|
||||||
if: ${{ matrix.jobs.goos == 'linux' && !contains(matrix.jobs.goarch, 'mips') }}
|
if: matrix.jobs.debian != ''
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install dpkg
|
set -xeuo pipefail
|
||||||
if [ "${{matrix.jobs.abi}}" = "1" ]; then
|
sudo gem install fpm
|
||||||
ARCH=loongarch64
|
cp .github/release/.fpm_systemd .fpm
|
||||||
elif [ "${{matrix.jobs.goarm}}" = "7" ]; then
|
|
||||||
ARCH=armhf
|
|
||||||
elif [ "${{matrix.jobs.goarch}}" = "arm" ]; then
|
|
||||||
ARCH=armel
|
|
||||||
else
|
|
||||||
ARCH=${{matrix.jobs.goarch}}
|
|
||||||
fi
|
|
||||||
PackageVersion=$(curl -s "https://api.github.com/repos/MetaCubeX/mihomo/releases/latest" | grep -o '"tag_name": "[^"]*' | grep -o '[^"]*$' | sed 's/v//g' )
|
|
||||||
if [ $(git branch | awk -F ' ' '{print $2}') = "Alpha" ]; then
|
|
||||||
PackageVersion="$(echo "${PackageVersion}" | awk -F '.' '{$NF = $NF + 1; print}' OFS='.')-${VERSION}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/DEBIAN
|
fpm -t deb \
|
||||||
mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/bin
|
-v "${PackageVersion}" \
|
||||||
mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/share/licenses/mihomo
|
-p "mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.deb" \
|
||||||
mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/etc/mihomo
|
--architecture ${{ matrix.jobs.debian }} \
|
||||||
mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/lib/systemd/system
|
mihomo=/usr/bin/mihomo
|
||||||
|
|
||||||
cp mihomo mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/bin/
|
- name: Package RPM
|
||||||
cp LICENSE mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/share/licenses/mihomo/
|
if: matrix.jobs.rpm != ''
|
||||||
cp .github/{mihomo.service,mihomo@.service} mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/lib/systemd/system/
|
|
||||||
|
|
||||||
cat > mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/etc/mihomo/config.yaml <<EOF
|
|
||||||
mixed-port: 7890
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/DEBIAN/conffiles <<EOF
|
|
||||||
/etc/mihomo/config.yaml
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/DEBIAN/control <<EOF
|
|
||||||
Package: mihomo
|
|
||||||
Version: ${PackageVersion}
|
|
||||||
Section:
|
|
||||||
Priority: extra
|
|
||||||
Architecture: ${ARCH}
|
|
||||||
Maintainer: MetaCubeX <none@example.com>
|
|
||||||
Homepage: https://wiki.metacubex.one/
|
|
||||||
Description: The universal proxy platform.
|
|
||||||
EOF
|
|
||||||
|
|
||||||
dpkg-deb -Z gzip --build mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}
|
|
||||||
|
|
||||||
- name: Convert DEB to RPM
|
|
||||||
if: ${{ matrix.jobs.goos == 'linux' && !contains(matrix.jobs.goarch, 'mips') }}
|
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y alien
|
set -xeuo pipefail
|
||||||
alien --to-rpm --scripts mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.deb
|
sudo gem install fpm
|
||||||
mv mihomo*.rpm mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.rpm
|
cp .github/release/.fpm_systemd .fpm
|
||||||
|
|
||||||
# - name: Convert DEB to PKG
|
fpm -t rpm \
|
||||||
# if: ${{ matrix.jobs.goos == 'linux' && !contains(matrix.jobs.goarch, 'mips') && !contains(matrix.jobs.goarch, 'loong64') }}
|
-v "${PackageVersion}" \
|
||||||
# run: |
|
-p "mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.rpm" \
|
||||||
# docker pull archlinux
|
--architecture ${{ matrix.jobs.rpm }} \
|
||||||
# docker run --rm -v ./:/mnt archlinux bash -c "
|
mihomo=/usr/bin/mihomo
|
||||||
# pacman -Syu pkgfile base-devel --noconfirm
|
|
||||||
# curl -L https://github.com/helixarch/debtap/raw/master/debtap > /usr/bin/debtap
|
- name: Package Pacman
|
||||||
# chmod 755 /usr/bin/debtap
|
if: matrix.jobs.pacman != ''
|
||||||
# debtap -u
|
run: |
|
||||||
# debtap -Q /mnt/mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.deb
|
set -xeuo pipefail
|
||||||
# "
|
sudo gem install fpm
|
||||||
# mv mihomo*.pkg.tar.zst mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.pkg.tar.zst
|
sudo apt-get update && sudo apt-get install -y libarchive-tools
|
||||||
|
cp .github/release/.fpm_systemd .fpm
|
||||||
|
|
||||||
|
fpm -t pacman \
|
||||||
|
-v "${PackageVersion}" \
|
||||||
|
-p "mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.pkg.tar.zst" \
|
||||||
|
--architecture ${{ matrix.jobs.pacman }} \
|
||||||
|
mihomo=/usr/bin/mihomo
|
||||||
|
|
||||||
- name: Save version
|
- name: Save version
|
||||||
run: |
|
run: |
|
||||||
echo ${VERSION} > version.txt
|
echo ${VERSION} > version.txt
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Archive production artifacts
|
- name: Archive production artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -337,6 +312,7 @@ jobs:
|
|||||||
mihomo*.gz
|
mihomo*.gz
|
||||||
mihomo*.deb
|
mihomo*.deb
|
||||||
mihomo*.rpm
|
mihomo*.rpm
|
||||||
|
mihomo*.pkg.tar.zst
|
||||||
mihomo*.zip
|
mihomo*.zip
|
||||||
version.txt
|
version.txt
|
||||||
checksums.txt
|
checksums.txt
|
||||||
|
|||||||
3
.github/workflows/trigger-cmfa-update.yml
vendored
3
.github/workflows/trigger-cmfa-update.yml
vendored
@@ -10,9 +10,6 @@ on:
|
|||||||
- Alpha
|
- Alpha
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "v*"
|
||||||
pull_request_target:
|
|
||||||
branches:
|
|
||||||
- Alpha
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Send "core-updated" to MetaCubeX/ClashMetaForAndroid to trigger update-dependencies
|
# Send "core-updated" to MetaCubeX/ClashMetaForAndroid to trigger update-dependencies
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,7 +15,6 @@ import (
|
|||||||
"github.com/metacubex/mihomo/common/queue"
|
"github.com/metacubex/mihomo/common/queue"
|
||||||
"github.com/metacubex/mihomo/common/utils"
|
"github.com/metacubex/mihomo/common/utils"
|
||||||
"github.com/metacubex/mihomo/component/ca"
|
"github.com/metacubex/mihomo/component/ca"
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
"github.com/metacubex/mihomo/log"
|
"github.com/metacubex/mihomo/log"
|
||||||
"github.com/puzpuzpuz/xsync/v3"
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
@@ -63,8 +60,8 @@ func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
|
conn, err := p.ProxyAdapter.DialContext(ctx, metadata)
|
||||||
return conn, err
|
return conn, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,8 +73,8 @@ func (p *Proxy) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
|
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata)
|
||||||
return pc, err
|
return pc, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,15 +314,7 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uintPort, err := strconv.ParseUint(port, 10, 16)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
addr = C.Metadata{
|
err = addr.SetRemoteAddress(net.JoinHostPort(u.Hostname(), port))
|
||||||
Host: u.Hostname(),
|
|
||||||
DstIP: netip.Addr{},
|
|
||||||
DstPort: uint16(uintPort),
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
@@ -41,23 +40,8 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
|
|||||||
// trim FQDN (#737)
|
// trim FQDN (#737)
|
||||||
host = strings.TrimRight(host, ".")
|
host = strings.TrimRight(host, ".")
|
||||||
|
|
||||||
var uint16Port uint16
|
metadata := &C.Metadata{}
|
||||||
if port, err := strconv.ParseUint(port, 10, 16); err == nil {
|
_ = metadata.SetRemoteAddress(net.JoinHostPort(host, port))
|
||||||
uint16Port = uint16(port)
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata := &C.Metadata{
|
|
||||||
NetWork: C.TCP,
|
|
||||||
Host: host,
|
|
||||||
DstIP: netip.Addr{},
|
|
||||||
DstPort: uint16Port,
|
|
||||||
}
|
|
||||||
|
|
||||||
ip, err := netip.ParseAddr(host)
|
|
||||||
if err == nil {
|
|
||||||
metadata.DstIP = ip
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata
|
return metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package outbound
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@@ -10,13 +9,12 @@ import (
|
|||||||
CN "github.com/metacubex/mihomo/common/net"
|
CN "github.com/metacubex/mihomo/common/net"
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
"github.com/metacubex/mihomo/component/dialer"
|
||||||
"github.com/metacubex/mihomo/component/proxydialer"
|
"github.com/metacubex/mihomo/component/proxydialer"
|
||||||
"github.com/metacubex/mihomo/component/resolver"
|
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
"github.com/metacubex/mihomo/transport/anytls"
|
"github.com/metacubex/mihomo/transport/anytls"
|
||||||
"github.com/metacubex/mihomo/transport/vmess"
|
"github.com/metacubex/mihomo/transport/vmess"
|
||||||
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/metacubex/sing/common/metadata"
|
||||||
"github.com/sagernet/sing/common/uot"
|
"github.com/metacubex/sing/common/uot"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AnyTLS struct {
|
type AnyTLS struct {
|
||||||
@@ -28,24 +26,23 @@ type AnyTLS struct {
|
|||||||
|
|
||||||
type AnyTLSOption struct {
|
type AnyTLSOption struct {
|
||||||
BasicOption
|
BasicOption
|
||||||
Name string `proxy:"name"`
|
Name string `proxy:"name"`
|
||||||
Server string `proxy:"server"`
|
Server string `proxy:"server"`
|
||||||
Port int `proxy:"port"`
|
Port int `proxy:"port"`
|
||||||
Password string `proxy:"password"`
|
Password string `proxy:"password"`
|
||||||
ALPN []string `proxy:"alpn,omitempty"`
|
ALPN []string `proxy:"alpn,omitempty"`
|
||||||
SNI string `proxy:"sni,omitempty"`
|
SNI string `proxy:"sni,omitempty"`
|
||||||
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
|
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
|
||||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
|
||||||
Fingerprint string `proxy:"fingerprint,omitempty"`
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
UDP bool `proxy:"udp,omitempty"`
|
Fingerprint string `proxy:"fingerprint,omitempty"`
|
||||||
IdleSessionCheckInterval int `proxy:"idle-session-check-interval,omitempty"`
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
IdleSessionTimeout int `proxy:"idle-session-timeout,omitempty"`
|
IdleSessionCheckInterval int `proxy:"idle-session-check-interval,omitempty"`
|
||||||
MinIdleSession int `proxy:"min-idle-session,omitempty"`
|
IdleSessionTimeout int `proxy:"idle-session-timeout,omitempty"`
|
||||||
|
MinIdleSession int `proxy:"min-idle-session,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *AnyTLS) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
func (t *AnyTLS) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
options := t.Base.DialOptions(opts...)
|
|
||||||
t.dialer.SetDialer(dialer.NewDialer(options...))
|
|
||||||
c, err := t.client.CreateProxy(ctx, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
|
c, err := t.client.CreateProxy(ctx, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -53,23 +50,18 @@ func (t *AnyTLS) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
|
|||||||
return NewConn(c, t), nil
|
return NewConn(c, t), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *AnyTLS) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
func (t *AnyTLS) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
|
if err = t.ResolveUDP(ctx, metadata); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// create tcp
|
// create tcp
|
||||||
options := t.Base.DialOptions(opts...)
|
|
||||||
t.dialer.SetDialer(dialer.NewDialer(options...))
|
|
||||||
c, err := t.client.CreateProxy(ctx, uot.RequestDestination(2))
|
c, err := t.client.CreateProxy(ctx, uot.RequestDestination(2))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// create uot on tcp
|
// create uot on tcp
|
||||||
if !metadata.Resolved() {
|
|
||||||
ip, err := resolver.ResolveIP(ctx, metadata.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("can't resolve ip")
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
|
||||||
}
|
|
||||||
destination := M.SocksaddrFromNet(metadata.UDPAddr())
|
destination := M.SocksaddrFromNet(metadata.UDPAddr())
|
||||||
return newPacketConn(CN.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), t), nil
|
return newPacketConn(CN.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), t), nil
|
||||||
}
|
}
|
||||||
@@ -93,29 +85,6 @@ func (t *AnyTLS) Close() error {
|
|||||||
|
|
||||||
func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) {
|
func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) {
|
||||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||||
|
|
||||||
singDialer := proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer())
|
|
||||||
|
|
||||||
tOption := anytls.ClientConfig{
|
|
||||||
Password: option.Password,
|
|
||||||
Server: M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)),
|
|
||||||
Dialer: singDialer,
|
|
||||||
IdleSessionCheckInterval: time.Duration(option.IdleSessionCheckInterval) * time.Second,
|
|
||||||
IdleSessionTimeout: time.Duration(option.IdleSessionTimeout) * time.Second,
|
|
||||||
MinIdleSession: option.MinIdleSession,
|
|
||||||
}
|
|
||||||
tlsConfig := &vmess.TLSConfig{
|
|
||||||
Host: option.SNI,
|
|
||||||
SkipCertVerify: option.SkipCertVerify,
|
|
||||||
NextProtos: option.ALPN,
|
|
||||||
FingerPrint: option.Fingerprint,
|
|
||||||
ClientFingerprint: option.ClientFingerprint,
|
|
||||||
}
|
|
||||||
if tlsConfig.Host == "" {
|
|
||||||
tlsConfig.Host = option.Server
|
|
||||||
}
|
|
||||||
tOption.TLSConfig = tlsConfig
|
|
||||||
|
|
||||||
outbound := &AnyTLS{
|
outbound := &AnyTLS{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
@@ -128,10 +97,39 @@ func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) {
|
|||||||
rmark: option.RoutingMark,
|
rmark: option.RoutingMark,
|
||||||
prefer: C.NewDNSPrefer(option.IPVersion),
|
prefer: C.NewDNSPrefer(option.IPVersion),
|
||||||
},
|
},
|
||||||
client: anytls.NewClient(context.TODO(), tOption),
|
|
||||||
option: &option,
|
option: &option,
|
||||||
dialer: singDialer,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
singDialer := proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer(outbound.DialOptions()...))
|
||||||
|
outbound.dialer = singDialer
|
||||||
|
|
||||||
|
tOption := anytls.ClientConfig{
|
||||||
|
Password: option.Password,
|
||||||
|
Server: M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)),
|
||||||
|
Dialer: singDialer,
|
||||||
|
IdleSessionCheckInterval: time.Duration(option.IdleSessionCheckInterval) * time.Second,
|
||||||
|
IdleSessionTimeout: time.Duration(option.IdleSessionTimeout) * time.Second,
|
||||||
|
MinIdleSession: option.MinIdleSession,
|
||||||
|
}
|
||||||
|
echConfig, err := option.ECHOpts.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsConfig := &vmess.TLSConfig{
|
||||||
|
Host: option.SNI,
|
||||||
|
SkipCertVerify: option.SkipCertVerify,
|
||||||
|
NextProtos: option.ALPN,
|
||||||
|
FingerPrint: option.Fingerprint,
|
||||||
|
ClientFingerprint: option.ClientFingerprint,
|
||||||
|
ECH: echConfig,
|
||||||
|
}
|
||||||
|
if tlsConfig.Host == "" {
|
||||||
|
tlsConfig.Host = option.Server
|
||||||
|
}
|
||||||
|
tOption.TLSConfig = tlsConfig
|
||||||
|
|
||||||
|
client := anytls.NewClient(context.TODO(), tOption)
|
||||||
|
outbound.client = client
|
||||||
|
|
||||||
return outbound, nil
|
return outbound, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,22 +3,24 @@ package outbound
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
N "github.com/metacubex/mihomo/common/net"
|
N "github.com/metacubex/mihomo/common/net"
|
||||||
"github.com/metacubex/mihomo/common/utils"
|
"github.com/metacubex/mihomo/common/utils"
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
"github.com/metacubex/mihomo/component/dialer"
|
||||||
|
"github.com/metacubex/mihomo/component/resolver"
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
"github.com/metacubex/mihomo/log"
|
"github.com/metacubex/mihomo/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProxyAdapter interface {
|
type ProxyAdapter interface {
|
||||||
C.ProxyAdapter
|
C.ProxyAdapter
|
||||||
DialOptions(opts ...dialer.Option) []dialer.Option
|
DialOptions() []dialer.Option
|
||||||
|
ResolveUDP(ctx context.Context, metadata *C.Metadata) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Base struct {
|
type Base struct {
|
||||||
@@ -59,7 +61,7 @@ func (b *Base) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Me
|
|||||||
return c, C.ErrNotSupport
|
return c, C.ErrNotSupport
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Base) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
func (b *Base) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
return nil, C.ErrNotSupport
|
return nil, C.ErrNotSupport
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +71,7 @@ func (b *Base) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metad
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
return nil, C.ErrNotSupport
|
return nil, C.ErrNotSupport
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +130,7 @@ func (b *Base) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialOptions return []dialer.Option from struct
|
// DialOptions return []dialer.Option from struct
|
||||||
func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option {
|
func (b *Base) DialOptions() (opts []dialer.Option) {
|
||||||
if b.iface != "" {
|
if b.iface != "" {
|
||||||
opts = append(opts, dialer.WithInterface(b.iface))
|
opts = append(opts, dialer.WithInterface(b.iface))
|
||||||
}
|
}
|
||||||
@@ -160,6 +162,17 @@ func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option {
|
|||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Base) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
|
||||||
|
if !metadata.Resolved() {
|
||||||
|
ip, err := resolver.ResolveIP(ctx, metadata.Host)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't resolve ip: %w", err)
|
||||||
|
}
|
||||||
|
metadata.DstIP = ip
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Base) Close() error {
|
func (b *Base) Close() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -167,8 +180,8 @@ func (b *Base) Close() error {
|
|||||||
type BasicOption struct {
|
type BasicOption struct {
|
||||||
TFO bool `proxy:"tfo,omitempty"`
|
TFO bool `proxy:"tfo,omitempty"`
|
||||||
MPTCP bool `proxy:"mptcp,omitempty"`
|
MPTCP bool `proxy:"mptcp,omitempty"`
|
||||||
Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"`
|
Interface string `proxy:"interface-name,omitempty"`
|
||||||
RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"`
|
RoutingMark int `proxy:"routing-mark,omitempty"`
|
||||||
IPVersion string `proxy:"ip-version,omitempty"`
|
IPVersion string `proxy:"ip-version,omitempty"`
|
||||||
DialerProxy string `proxy:"dialer-proxy,omitempty"` // don't apply this option into groups, but can set a group name in a proxy
|
DialerProxy string `proxy:"dialer-proxy,omitempty"` // don't apply this option into groups, but can set a group name in a proxy
|
||||||
}
|
}
|
||||||
@@ -203,12 +216,21 @@ func NewBase(opt BaseOption) *Base {
|
|||||||
|
|
||||||
type conn struct {
|
type conn struct {
|
||||||
N.ExtendedConn
|
N.ExtendedConn
|
||||||
chain C.Chain
|
chain C.Chain
|
||||||
actualRemoteDestination string
|
adapterAddr string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *conn) RemoteDestination() string {
|
func (c *conn) RemoteDestination() string {
|
||||||
return c.actualRemoteDestination
|
if remoteAddr := c.RemoteAddr(); remoteAddr != nil {
|
||||||
|
m := C.Metadata{}
|
||||||
|
if err := m.SetRemoteAddr(remoteAddr); err == nil {
|
||||||
|
if m.Valid() {
|
||||||
|
return m.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
host, _, _ := net.SplitHostPort(c.adapterAddr)
|
||||||
|
return host
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chains implements C.Connection
|
// Chains implements C.Connection
|
||||||
@@ -241,19 +263,25 @@ func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
|
|||||||
if _, ok := c.(syscall.Conn); !ok { // exclusion system conn like *net.TCPConn
|
if _, ok := c.(syscall.Conn); !ok { // exclusion system conn like *net.TCPConn
|
||||||
c = N.NewDeadlineConn(c) // most conn from outbound can't handle readDeadline correctly
|
c = N.NewDeadlineConn(c) // most conn from outbound can't handle readDeadline correctly
|
||||||
}
|
}
|
||||||
return &conn{N.NewExtendedConn(c), []string{a.Name()}, parseRemoteDestination(a.Addr())}
|
return &conn{N.NewExtendedConn(c), []string{a.Name()}, a.Addr()}
|
||||||
}
|
}
|
||||||
|
|
||||||
type packetConn struct {
|
type packetConn struct {
|
||||||
N.EnhancePacketConn
|
N.EnhancePacketConn
|
||||||
chain C.Chain
|
chain C.Chain
|
||||||
adapterName string
|
adapterName string
|
||||||
connID string
|
connID string
|
||||||
actualRemoteDestination string
|
adapterAddr string
|
||||||
|
resolveUDP func(ctx context.Context, metadata *C.Metadata) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *packetConn) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
|
||||||
|
return c.resolveUDP(ctx, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *packetConn) RemoteDestination() string {
|
func (c *packetConn) RemoteDestination() string {
|
||||||
return c.actualRemoteDestination
|
host, _, _ := net.SplitHostPort(c.adapterAddr)
|
||||||
|
return host
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chains implements C.Connection
|
// Chains implements C.Connection
|
||||||
@@ -287,24 +315,12 @@ func (c *packetConn) AddRef(ref any) {
|
|||||||
c.EnhancePacketConn = N.NewRefPacketConn(c.EnhancePacketConn, ref) // add ref for autoCloseProxyAdapter
|
c.EnhancePacketConn = N.NewRefPacketConn(c.EnhancePacketConn, ref) // add ref for autoCloseProxyAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
|
func newPacketConn(pc net.PacketConn, a ProxyAdapter) C.PacketConn {
|
||||||
epc := N.NewEnhancePacketConn(pc)
|
epc := N.NewEnhancePacketConn(pc)
|
||||||
if _, ok := pc.(syscall.Conn); !ok { // exclusion system conn like *net.UDPConn
|
if _, ok := pc.(syscall.Conn); !ok { // exclusion system conn like *net.UDPConn
|
||||||
epc = N.NewDeadlineEnhancePacketConn(epc) // most conn from outbound can't handle readDeadline correctly
|
epc = N.NewDeadlineEnhancePacketConn(epc) // most conn from outbound can't handle readDeadline correctly
|
||||||
}
|
}
|
||||||
return &packetConn{epc, []string{a.Name()}, a.Name(), utils.NewUUIDV4().String(), parseRemoteDestination(a.Addr())}
|
return &packetConn{epc, []string{a.Name()}, a.Name(), utils.NewUUIDV4().String(), a.Addr(), a.ResolveUDP}
|
||||||
}
|
|
||||||
|
|
||||||
func parseRemoteDestination(addr string) string {
|
|
||||||
if dst, _, err := net.SplitHostPort(addr); err == nil {
|
|
||||||
return dst
|
|
||||||
} else {
|
|
||||||
if addrError, ok := err.(*net.AddrError); ok && strings.Contains(addrError.Err, "missing port") {
|
|
||||||
return dst
|
|
||||||
} else {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddRef interface {
|
type AddRef interface {
|
||||||
@@ -317,8 +333,8 @@ type autoCloseProxyAdapter struct {
|
|||||||
closeErr error
|
closeErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *autoCloseProxyAdapter) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
func (p *autoCloseProxyAdapter) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
c, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
|
c, err := p.ProxyAdapter.DialContext(ctx, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -339,8 +355,8 @@ func (p *autoCloseProxyAdapter) DialContextWithDialer(ctx context.Context, diale
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *autoCloseProxyAdapter) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
func (p *autoCloseProxyAdapter) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
|
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ package outbound
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"fmt"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
"github.com/metacubex/mihomo/component/dialer"
|
||||||
"github.com/metacubex/mihomo/component/loopback"
|
"github.com/metacubex/mihomo/component/loopback"
|
||||||
"github.com/metacubex/mihomo/component/resolver"
|
"github.com/metacubex/mihomo/component/resolver"
|
||||||
@@ -20,12 +21,13 @@ type DirectOption struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
if err := d.loopBack.CheckConn(metadata); err != nil {
|
if err := d.loopBack.CheckConn(metadata); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
opts := d.DialOptions()
|
||||||
opts = append(opts, dialer.WithResolver(resolver.DirectHostResolver))
|
opts = append(opts, dialer.WithResolver(resolver.DirectHostResolver))
|
||||||
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...)
|
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -33,25 +35,31 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
if err := d.loopBack.CheckPacketConn(metadata); err != nil {
|
if err := d.loopBack.CheckPacketConn(metadata); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// net.UDPConn.WriteTo only working with *net.UDPAddr, so we need a net.UDPAddr
|
if err := d.ResolveUDP(ctx, metadata); err != nil {
|
||||||
if !metadata.Resolved() {
|
return nil, err
|
||||||
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, resolver.DirectHostResolver)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("can't resolve ip")
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
|
||||||
}
|
}
|
||||||
pc, err := dialer.NewDialer(d.Base.DialOptions(opts...)...).ListenPacket(ctx, "udp", "", metadata.AddrPort())
|
pc, err := dialer.NewDialer(d.DialOptions()...).ListenPacket(ctx, "udp", "", metadata.AddrPort())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return d.loopBack.NewPacketConn(newPacketConn(pc, d)), nil
|
return d.loopBack.NewPacketConn(newPacketConn(pc, d)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Direct) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
|
||||||
|
if (!metadata.Resolved() || resolver.DirectHostResolver != resolver.DefaultResolver) && metadata.Host != "" {
|
||||||
|
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, resolver.DirectHostResolver)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't resolve ip: %w", err)
|
||||||
|
}
|
||||||
|
metadata.DstIP = ip
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Direct) IsL3Protocol(metadata *C.Metadata) bool {
|
func (d *Direct) IsL3Protocol(metadata *C.Metadata) bool {
|
||||||
return true // tell DNSDialer don't send domain to DialContext, avoid lookback to DefaultResolver
|
return true // tell DNSDialer don't send domain to DialContext, avoid lookback to DefaultResolver
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ package outbound
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
N "github.com/metacubex/mihomo/common/net"
|
N "github.com/metacubex/mihomo/common/net"
|
||||||
"github.com/metacubex/mihomo/common/pool"
|
"github.com/metacubex/mihomo/common/pool"
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
|
||||||
"github.com/metacubex/mihomo/component/resolver"
|
"github.com/metacubex/mihomo/component/resolver"
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
"github.com/metacubex/mihomo/log"
|
"github.com/metacubex/mihomo/log"
|
||||||
@@ -23,15 +23,18 @@ type DnsOption struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (d *Dns) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
func (d *Dns) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
left, right := N.Pipe()
|
left, right := N.Pipe()
|
||||||
go resolver.RelayDnsConn(context.Background(), right, 0)
|
go resolver.RelayDnsConn(context.Background(), right, 0)
|
||||||
return NewConn(left, d), nil
|
return NewConn(left, d), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (d *Dns) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
func (d *Dns) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
log.Debugln("[DNS] hijack udp:%s from %s", metadata.RemoteAddress(), metadata.SourceAddrPort())
|
log.Debugln("[DNS] hijack udp:%s from %s", metadata.RemoteAddress(), metadata.SourceAddrPort())
|
||||||
|
if err := d.ResolveUDP(ctx, metadata); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
@@ -42,6 +45,13 @@ func (d *Dns) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opt
|
|||||||
}, d), nil
|
}, d), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Dns) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
|
||||||
|
if !metadata.Resolved() {
|
||||||
|
metadata.DstIP = netip.AddrFrom4([4]byte{127, 0, 0, 2})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type dnsPacket struct {
|
type dnsPacket struct {
|
||||||
data []byte
|
data []byte
|
||||||
put func()
|
put func()
|
||||||
|
|||||||
36
adapter/outbound/ech.go
Normal file
36
adapter/outbound/ech.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/metacubex/mihomo/component/ech"
|
||||||
|
"github.com/metacubex/mihomo/component/resolver"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ECHOptions struct {
|
||||||
|
Enable bool `proxy:"enable,omitempty" obfs:"enable,omitempty"`
|
||||||
|
Config string `proxy:"config,omitempty" obfs:"config,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o ECHOptions) Parse() (*ech.Config, error) {
|
||||||
|
if !o.Enable {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
echConfig := &ech.Config{}
|
||||||
|
if o.Config != "" {
|
||||||
|
list, err := base64.StdEncoding.DecodeString(o.Config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("base64 decode ech config string failed: %v", err)
|
||||||
|
}
|
||||||
|
echConfig.GetEncryptedClientHelloConfigList = func(ctx context.Context, serverName string) ([]byte, error) {
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echConfig.GetEncryptedClientHelloConfigList = func(ctx context.Context, serverName string) ([]byte, error) {
|
||||||
|
return resolver.ResolveECHWithResolver(ctx, serverName, resolver.ProxyServerHostResolver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return echConfig, nil
|
||||||
|
}
|
||||||
@@ -58,8 +58,8 @@ func (h *Http) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Me
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
return h.DialContextWithDialer(ctx, dialer.NewDialer(h.Base.DialOptions(opts...)...), metadata)
|
return h.DialContextWithDialer(ctx, dialer.NewDialer(h.DialOptions()...), metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContextWithDialer implements C.ProxyAdapter
|
// DialContextWithDialer implements C.ProxyAdapter
|
||||||
|
|||||||
@@ -10,13 +10,11 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/metacubex/quic-go"
|
|
||||||
"github.com/metacubex/quic-go/congestion"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/component/ca"
|
"github.com/metacubex/mihomo/component/ca"
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
"github.com/metacubex/mihomo/component/dialer"
|
||||||
|
"github.com/metacubex/mihomo/component/ech"
|
||||||
"github.com/metacubex/mihomo/component/proxydialer"
|
"github.com/metacubex/mihomo/component/proxydialer"
|
||||||
|
tlsC "github.com/metacubex/mihomo/component/tls"
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
"github.com/metacubex/mihomo/log"
|
"github.com/metacubex/mihomo/log"
|
||||||
hyCongestion "github.com/metacubex/mihomo/transport/hysteria/congestion"
|
hyCongestion "github.com/metacubex/mihomo/transport/hysteria/congestion"
|
||||||
@@ -25,6 +23,10 @@ import (
|
|||||||
"github.com/metacubex/mihomo/transport/hysteria/pmtud_fix"
|
"github.com/metacubex/mihomo/transport/hysteria/pmtud_fix"
|
||||||
"github.com/metacubex/mihomo/transport/hysteria/transport"
|
"github.com/metacubex/mihomo/transport/hysteria/transport"
|
||||||
"github.com/metacubex/mihomo/transport/hysteria/utils"
|
"github.com/metacubex/mihomo/transport/hysteria/utils"
|
||||||
|
|
||||||
|
"github.com/metacubex/quic-go"
|
||||||
|
"github.com/metacubex/quic-go/congestion"
|
||||||
|
M "github.com/metacubex/sing/common/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -43,10 +45,13 @@ type Hysteria struct {
|
|||||||
|
|
||||||
option *HysteriaOption
|
option *HysteriaOption
|
||||||
client *core.Client
|
client *core.Client
|
||||||
|
|
||||||
|
tlsConfig *tlsC.Config
|
||||||
|
echConfig *ech.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
tcpConn, err := h.client.DialTCP(metadata.String(), metadata.DstPort, h.genHdc(ctx, opts...))
|
tcpConn, err := h.client.DialTCP(metadata.String(), metadata.DstPort, h.genHdc(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -54,20 +59,23 @@ func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts .
|
|||||||
return NewConn(tcpConn, h), nil
|
return NewConn(tcpConn, h), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
udpConn, err := h.client.DialUDP(h.genHdc(ctx, opts...))
|
if err := h.ResolveUDP(ctx, metadata); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
udpConn, err := h.client.DialUDP(h.genHdc(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return newPacketConn(&hyPacketConn{udpConn}, h), nil
|
return newPacketConn(&hyPacketConn{udpConn}, h), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Hysteria) genHdc(ctx context.Context, opts ...dialer.Option) utils.PacketDialer {
|
func (h *Hysteria) genHdc(ctx context.Context) utils.PacketDialer {
|
||||||
return &hyDialerWithContext{
|
return &hyDialerWithContext{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
hyDialer: func(network string, rAddr net.Addr) (net.PacketConn, error) {
|
hyDialer: func(network string, rAddr net.Addr) (net.PacketConn, error) {
|
||||||
var err error
|
var err error
|
||||||
var cDialer C.Dialer = dialer.NewDialer(h.Base.DialOptions(opts...)...)
|
var cDialer C.Dialer = dialer.NewDialer(h.DialOptions()...)
|
||||||
if len(h.option.DialerProxy) > 0 {
|
if len(h.option.DialerProxy) > 0 {
|
||||||
cDialer, err = proxydialer.NewByName(h.option.DialerProxy, cDialer)
|
cDialer, err = proxydialer.NewByName(h.option.DialerProxy, cDialer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -78,7 +86,15 @@ func (h *Hysteria) genHdc(ctx context.Context, opts ...dialer.Option) utils.Pack
|
|||||||
return cDialer.ListenPacket(ctx, network, "", rAddrPort)
|
return cDialer.ListenPacket(ctx, network, "", rAddrPort)
|
||||||
},
|
},
|
||||||
remoteAddr: func(addr string) (net.Addr, error) {
|
remoteAddr: func(addr string) (net.Addr, error) {
|
||||||
return resolveUDPAddr(ctx, "udp", addr, h.prefer)
|
udpAddr, err := resolveUDPAddr(ctx, "udp", addr, h.prefer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = h.echConfig.ClientHandle(ctx, h.tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return udpAddr, nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,30 +108,31 @@ func (h *Hysteria) ProxyInfo() C.ProxyInfo {
|
|||||||
|
|
||||||
type HysteriaOption struct {
|
type HysteriaOption struct {
|
||||||
BasicOption
|
BasicOption
|
||||||
Name string `proxy:"name"`
|
Name string `proxy:"name"`
|
||||||
Server string `proxy:"server"`
|
Server string `proxy:"server"`
|
||||||
Port int `proxy:"port,omitempty"`
|
Port int `proxy:"port,omitempty"`
|
||||||
Ports string `proxy:"ports,omitempty"`
|
Ports string `proxy:"ports,omitempty"`
|
||||||
Protocol string `proxy:"protocol,omitempty"`
|
Protocol string `proxy:"protocol,omitempty"`
|
||||||
ObfsProtocol string `proxy:"obfs-protocol,omitempty"` // compatible with Stash
|
ObfsProtocol string `proxy:"obfs-protocol,omitempty"` // compatible with Stash
|
||||||
Up string `proxy:"up"`
|
Up string `proxy:"up"`
|
||||||
UpSpeed int `proxy:"up-speed,omitempty"` // compatible with Stash
|
UpSpeed int `proxy:"up-speed,omitempty"` // compatible with Stash
|
||||||
Down string `proxy:"down"`
|
Down string `proxy:"down"`
|
||||||
DownSpeed int `proxy:"down-speed,omitempty"` // compatible with Stash
|
DownSpeed int `proxy:"down-speed,omitempty"` // compatible with Stash
|
||||||
Auth string `proxy:"auth,omitempty"`
|
Auth string `proxy:"auth,omitempty"`
|
||||||
AuthString string `proxy:"auth-str,omitempty"`
|
AuthString string `proxy:"auth-str,omitempty"`
|
||||||
Obfs string `proxy:"obfs,omitempty"`
|
Obfs string `proxy:"obfs,omitempty"`
|
||||||
SNI string `proxy:"sni,omitempty"`
|
SNI string `proxy:"sni,omitempty"`
|
||||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
|
||||||
Fingerprint string `proxy:"fingerprint,omitempty"`
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
ALPN []string `proxy:"alpn,omitempty"`
|
Fingerprint string `proxy:"fingerprint,omitempty"`
|
||||||
CustomCA string `proxy:"ca,omitempty"`
|
ALPN []string `proxy:"alpn,omitempty"`
|
||||||
CustomCAString string `proxy:"ca-str,omitempty"`
|
CustomCA string `proxy:"ca,omitempty"`
|
||||||
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
|
CustomCAString string `proxy:"ca-str,omitempty"`
|
||||||
ReceiveWindow int `proxy:"recv-window,omitempty"`
|
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
|
||||||
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
|
ReceiveWindow int `proxy:"recv-window,omitempty"`
|
||||||
FastOpen bool `proxy:"fast-open,omitempty"`
|
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
|
||||||
HopInterval int `proxy:"hop-interval,omitempty"`
|
FastOpen bool `proxy:"fast-open,omitempty"`
|
||||||
|
HopInterval int `proxy:"hop-interval,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *HysteriaOption) Speed() (uint64, uint64, error) {
|
func (c *HysteriaOption) Speed() (uint64, uint64, error) {
|
||||||
@@ -160,6 +177,13 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
|
|||||||
} else {
|
} else {
|
||||||
tlsConfig.NextProtos = []string{DefaultALPN}
|
tlsConfig.NextProtos = []string{DefaultALPN}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
echConfig, err := option.ECHOpts.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsClientConfig := tlsC.UConfig(tlsConfig)
|
||||||
|
|
||||||
quicConfig := &quic.Config{
|
quicConfig := &quic.Config{
|
||||||
InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn),
|
InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn),
|
||||||
MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn),
|
MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn),
|
||||||
@@ -214,7 +238,7 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
|
|||||||
down = uint64(option.DownSpeed * mbpsToBps)
|
down = uint64(option.DownSpeed * mbpsToBps)
|
||||||
}
|
}
|
||||||
client, err := core.NewClient(
|
client, err := core.NewClient(
|
||||||
addr, ports, option.Protocol, auth, tlsConfig, quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl {
|
addr, ports, option.Protocol, auth, tlsClientConfig, quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl {
|
||||||
return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS))
|
return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS))
|
||||||
}, obfuscator, hopInterval, option.FastOpen,
|
}, obfuscator, hopInterval, option.FastOpen,
|
||||||
)
|
)
|
||||||
@@ -232,8 +256,10 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
|
|||||||
rmark: option.RoutingMark,
|
rmark: option.RoutingMark,
|
||||||
prefer: C.NewDNSPrefer(option.IPVersion),
|
prefer: C.NewDNSPrefer(option.IPVersion),
|
||||||
},
|
},
|
||||||
option: &option,
|
option: &option,
|
||||||
client: client,
|
client: client,
|
||||||
|
tlsConfig: tlsClientConfig,
|
||||||
|
echConfig: echConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
return outbound, nil
|
return outbound, nil
|
||||||
|
|||||||
@@ -14,15 +14,14 @@ import (
|
|||||||
"github.com/metacubex/mihomo/component/ca"
|
"github.com/metacubex/mihomo/component/ca"
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
"github.com/metacubex/mihomo/component/dialer"
|
||||||
"github.com/metacubex/mihomo/component/proxydialer"
|
"github.com/metacubex/mihomo/component/proxydialer"
|
||||||
|
tlsC "github.com/metacubex/mihomo/component/tls"
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
"github.com/metacubex/mihomo/log"
|
"github.com/metacubex/mihomo/log"
|
||||||
tuicCommon "github.com/metacubex/mihomo/transport/tuic/common"
|
tuicCommon "github.com/metacubex/mihomo/transport/tuic/common"
|
||||||
|
|
||||||
"github.com/metacubex/sing-quic/hysteria2"
|
|
||||||
|
|
||||||
"github.com/metacubex/quic-go"
|
"github.com/metacubex/quic-go"
|
||||||
"github.com/metacubex/randv2"
|
"github.com/metacubex/sing-quic/hysteria2"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/metacubex/sing/common/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -42,24 +41,25 @@ type Hysteria2 struct {
|
|||||||
|
|
||||||
type Hysteria2Option struct {
|
type Hysteria2Option struct {
|
||||||
BasicOption
|
BasicOption
|
||||||
Name string `proxy:"name"`
|
Name string `proxy:"name"`
|
||||||
Server string `proxy:"server"`
|
Server string `proxy:"server"`
|
||||||
Port int `proxy:"port,omitempty"`
|
Port int `proxy:"port,omitempty"`
|
||||||
Ports string `proxy:"ports,omitempty"`
|
Ports string `proxy:"ports,omitempty"`
|
||||||
HopInterval int `proxy:"hop-interval,omitempty"`
|
HopInterval int `proxy:"hop-interval,omitempty"`
|
||||||
Up string `proxy:"up,omitempty"`
|
Up string `proxy:"up,omitempty"`
|
||||||
Down string `proxy:"down,omitempty"`
|
Down string `proxy:"down,omitempty"`
|
||||||
Password string `proxy:"password,omitempty"`
|
Password string `proxy:"password,omitempty"`
|
||||||
Obfs string `proxy:"obfs,omitempty"`
|
Obfs string `proxy:"obfs,omitempty"`
|
||||||
ObfsPassword string `proxy:"obfs-password,omitempty"`
|
ObfsPassword string `proxy:"obfs-password,omitempty"`
|
||||||
SNI string `proxy:"sni,omitempty"`
|
SNI string `proxy:"sni,omitempty"`
|
||||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
|
||||||
Fingerprint string `proxy:"fingerprint,omitempty"`
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
ALPN []string `proxy:"alpn,omitempty"`
|
Fingerprint string `proxy:"fingerprint,omitempty"`
|
||||||
CustomCA string `proxy:"ca,omitempty"`
|
ALPN []string `proxy:"alpn,omitempty"`
|
||||||
CustomCAString string `proxy:"ca-str,omitempty"`
|
CustomCA string `proxy:"ca,omitempty"`
|
||||||
CWND int `proxy:"cwnd,omitempty"`
|
CustomCAString string `proxy:"ca-str,omitempty"`
|
||||||
UdpMTU int `proxy:"udp-mtu,omitempty"`
|
CWND int `proxy:"cwnd,omitempty"`
|
||||||
|
UdpMTU int `proxy:"udp-mtu,omitempty"`
|
||||||
|
|
||||||
// quic-go special config
|
// quic-go special config
|
||||||
InitialStreamReceiveWindow uint64 `proxy:"initial-stream-receive-window,omitempty"`
|
InitialStreamReceiveWindow uint64 `proxy:"initial-stream-receive-window,omitempty"`
|
||||||
@@ -68,9 +68,7 @@ type Hysteria2Option struct {
|
|||||||
MaxConnectionReceiveWindow uint64 `proxy:"max-connection-receive-window,omitempty"`
|
MaxConnectionReceiveWindow uint64 `proxy:"max-connection-receive-window,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
options := h.Base.DialOptions(opts...)
|
|
||||||
h.dialer.SetDialer(dialer.NewDialer(options...))
|
|
||||||
c, err := h.client.DialConn(ctx, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
|
c, err := h.client.DialConn(ctx, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -78,9 +76,10 @@ func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata, opts
|
|||||||
return NewConn(c, h), nil
|
return NewConn(c, h), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
options := h.Base.DialOptions(opts...)
|
if err = h.ResolveUDP(ctx, metadata); err != nil {
|
||||||
h.dialer.SetDialer(dialer.NewDialer(options...))
|
return nil, err
|
||||||
|
}
|
||||||
pc, err := h.client.ListenPacket(ctx)
|
pc, err := h.client.ListenPacket(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -108,6 +107,22 @@ func (h *Hysteria2) ProxyInfo() C.ProxyInfo {
|
|||||||
|
|
||||||
func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
|
func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
|
||||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||||
|
outbound := &Hysteria2{
|
||||||
|
Base: &Base{
|
||||||
|
name: option.Name,
|
||||||
|
addr: addr,
|
||||||
|
tp: C.Hysteria2,
|
||||||
|
udp: true,
|
||||||
|
iface: option.Interface,
|
||||||
|
rmark: option.RoutingMark,
|
||||||
|
prefer: C.NewDNSPrefer(option.IPVersion),
|
||||||
|
},
|
||||||
|
option: &option,
|
||||||
|
}
|
||||||
|
|
||||||
|
singDialer := proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer(outbound.DialOptions()...))
|
||||||
|
outbound.dialer = singDialer
|
||||||
|
|
||||||
var salamanderPassword string
|
var salamanderPassword string
|
||||||
if len(option.Obfs) > 0 {
|
if len(option.Obfs) > 0 {
|
||||||
if option.ObfsPassword == "" {
|
if option.ObfsPassword == "" {
|
||||||
@@ -142,6 +157,12 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
|
|||||||
tlsConfig.NextProtos = option.ALPN
|
tlsConfig.NextProtos = option.ALPN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tlsClientConfig := tlsC.UConfig(tlsConfig)
|
||||||
|
echConfig, err := option.ECHOpts.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if option.UdpMTU == 0 {
|
if option.UdpMTU == 0 {
|
||||||
// "1200" from quic-go's MaxDatagramSize
|
// "1200" from quic-go's MaxDatagramSize
|
||||||
// "-3" from quic-go's DatagramFrame.MaxDataLen
|
// "-3" from quic-go's DatagramFrame.MaxDataLen
|
||||||
@@ -155,8 +176,6 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
|
|||||||
MaxConnectionReceiveWindow: option.MaxConnectionReceiveWindow,
|
MaxConnectionReceiveWindow: option.MaxConnectionReceiveWindow,
|
||||||
}
|
}
|
||||||
|
|
||||||
singDialer := proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer())
|
|
||||||
|
|
||||||
clientOptions := hysteria2.ClientOptions{
|
clientOptions := hysteria2.ClientOptions{
|
||||||
Context: context.TODO(),
|
Context: context.TODO(),
|
||||||
Dialer: singDialer,
|
Dialer: singDialer,
|
||||||
@@ -165,41 +184,46 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
|
|||||||
ReceiveBPS: StringToBps(option.Down),
|
ReceiveBPS: StringToBps(option.Down),
|
||||||
SalamanderPassword: salamanderPassword,
|
SalamanderPassword: salamanderPassword,
|
||||||
Password: option.Password,
|
Password: option.Password,
|
||||||
TLSConfig: tlsConfig,
|
TLSConfig: tlsClientConfig,
|
||||||
QUICConfig: quicConfig,
|
QUICConfig: quicConfig,
|
||||||
UDPDisabled: false,
|
UDPDisabled: false,
|
||||||
CWND: option.CWND,
|
CWND: option.CWND,
|
||||||
UdpMTU: option.UdpMTU,
|
UdpMTU: option.UdpMTU,
|
||||||
ServerAddress: func(ctx context.Context) (*net.UDPAddr, error) {
|
ServerAddress: func(ctx context.Context) (*net.UDPAddr, error) {
|
||||||
return resolveUDPAddr(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion))
|
udpAddr, err := resolveUDPAddr(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = echConfig.ClientHandle(ctx, tlsClientConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return udpAddr, nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var ranges utils.IntRanges[uint16]
|
var ranges utils.IntRanges[uint16]
|
||||||
var serverAddress []string
|
var serverPorts []uint16
|
||||||
if option.Ports != "" {
|
if option.Ports != "" {
|
||||||
ranges, err = utils.NewUnsignedRanges[uint16](option.Ports)
|
ranges, err = utils.NewUnsignedRanges[uint16](option.Ports)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ranges.Range(func(port uint16) bool {
|
ranges.Range(func(port uint16) bool {
|
||||||
serverAddress = append(serverAddress, net.JoinHostPort(option.Server, strconv.Itoa(int(port))))
|
serverPorts = append(serverPorts, port)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
if len(serverAddress) > 0 {
|
if len(serverPorts) > 0 {
|
||||||
clientOptions.ServerAddress = func(ctx context.Context) (*net.UDPAddr, error) {
|
|
||||||
return resolveUDPAddr(ctx, "udp", serverAddress[randv2.IntN(len(serverAddress))], C.NewDNSPrefer(option.IPVersion))
|
|
||||||
}
|
|
||||||
|
|
||||||
if option.HopInterval == 0 {
|
if option.HopInterval == 0 {
|
||||||
option.HopInterval = defaultHopInterval
|
option.HopInterval = defaultHopInterval
|
||||||
} else if option.HopInterval < minHopInterval {
|
} else if option.HopInterval < minHopInterval {
|
||||||
option.HopInterval = minHopInterval
|
option.HopInterval = minHopInterval
|
||||||
}
|
}
|
||||||
clientOptions.HopInterval = time.Duration(option.HopInterval) * time.Second
|
clientOptions.HopInterval = time.Duration(option.HopInterval) * time.Second
|
||||||
|
clientOptions.ServerPorts = serverPorts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if option.Port == 0 && len(serverAddress) == 0 {
|
if option.Port == 0 && len(serverPorts) == 0 {
|
||||||
return nil, errors.New("invalid port")
|
return nil, errors.New("invalid port")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,21 +231,7 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
outbound.client = client
|
||||||
outbound := &Hysteria2{
|
|
||||||
Base: &Base{
|
|
||||||
name: option.Name,
|
|
||||||
addr: addr,
|
|
||||||
tp: C.Hysteria2,
|
|
||||||
udp: true,
|
|
||||||
iface: option.Interface,
|
|
||||||
rmark: option.RoutingMark,
|
|
||||||
prefer: C.NewDNSPrefer(option.IPVersion),
|
|
||||||
},
|
|
||||||
option: &option,
|
|
||||||
client: client,
|
|
||||||
dialer: singDialer,
|
|
||||||
}
|
|
||||||
|
|
||||||
return outbound, nil
|
return outbound, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ type MieruOption struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (m *Mieru) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
func (m *Mieru) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
if err := m.ensureClientIsRunning(opts...); err != nil {
|
if err := m.ensureClientIsRunning(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
addr := metadataToMieruNetAddrSpec(metadata)
|
addr := metadataToMieruNetAddrSpec(metadata)
|
||||||
@@ -53,8 +53,11 @@ func (m *Mieru) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (m *Mieru) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
func (m *Mieru) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
if err := m.ensureClientIsRunning(opts...); err != nil {
|
if err = m.ResolveUDP(ctx, metadata); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := m.ensureClientIsRunning(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
c, err := m.client.DialContext(ctx, metadata.UDPAddr())
|
c, err := m.client.DialContext(ctx, metadata.UDPAddr())
|
||||||
@@ -76,7 +79,7 @@ func (m *Mieru) ProxyInfo() C.ProxyInfo {
|
|||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Mieru) ensureClientIsRunning(opts ...dialer.Option) error {
|
func (m *Mieru) ensureClientIsRunning() error {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
@@ -85,7 +88,7 @@ func (m *Mieru) ensureClientIsRunning(opts ...dialer.Option) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a dialer and add it to the client config, before starting the client.
|
// Create a dialer and add it to the client config, before starting the client.
|
||||||
var dialer C.Dialer = dialer.NewDialer(m.Base.DialOptions(opts...)...)
|
var dialer C.Dialer = dialer.NewDialer(m.DialOptions()...)
|
||||||
var err error
|
var err error
|
||||||
if len(m.option.DialerProxy) > 0 {
|
if len(m.option.DialerProxy) > 0 {
|
||||||
dialer, err = proxydialer.NewByName(m.option.DialerProxy, dialer)
|
dialer, err = proxydialer.NewByName(m.option.DialerProxy, dialer)
|
||||||
|
|||||||
@@ -13,11 +13,14 @@ import (
|
|||||||
type RealityOptions struct {
|
type RealityOptions struct {
|
||||||
PublicKey string `proxy:"public-key"`
|
PublicKey string `proxy:"public-key"`
|
||||||
ShortID string `proxy:"short-id"`
|
ShortID string `proxy:"short-id"`
|
||||||
|
|
||||||
|
SupportX25519MLKEM768 bool `proxy:"support-x25519mlkem768"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) {
|
func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) {
|
||||||
if o.PublicKey != "" {
|
if o.PublicKey != "" {
|
||||||
config := new(tlsC.RealityConfig)
|
config := new(tlsC.RealityConfig)
|
||||||
|
config.SupportX25519MLKEM768 = o.SupportX25519MLKEM768
|
||||||
|
|
||||||
const x25519ScalarSize = 32
|
const x25519ScalarSize = 32
|
||||||
publicKey, err := base64.RawURLEncoding.DecodeString(o.PublicKey)
|
publicKey, err := base64.RawURLEncoding.DecodeString(o.PublicKey)
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/common/buf"
|
"github.com/metacubex/mihomo/common/buf"
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ type RejectOption struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
if r.drop {
|
if r.drop {
|
||||||
return NewConn(dropConn{}, r), nil
|
return NewConn(dropConn{}, r), nil
|
||||||
}
|
}
|
||||||
@@ -29,10 +29,20 @@ func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
|
if err := r.ResolveUDP(ctx, metadata); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return newPacketConn(&nopPacketConn{}, r), nil
|
return newPacketConn(&nopPacketConn{}, r), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Reject) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
|
||||||
|
if !metadata.Resolved() {
|
||||||
|
metadata.DstIP = netip.IPv4Unspecified()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewRejectWithOption(option RejectOption) *Reject {
|
func NewRejectWithOption(option RejectOption) *Reject {
|
||||||
return &Reject{
|
return &Reject{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package outbound
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -11,7 +10,6 @@ import (
|
|||||||
"github.com/metacubex/mihomo/common/structure"
|
"github.com/metacubex/mihomo/common/structure"
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
"github.com/metacubex/mihomo/component/dialer"
|
||||||
"github.com/metacubex/mihomo/component/proxydialer"
|
"github.com/metacubex/mihomo/component/proxydialer"
|
||||||
"github.com/metacubex/mihomo/component/resolver"
|
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
gost "github.com/metacubex/mihomo/transport/gost-plugin"
|
gost "github.com/metacubex/mihomo/transport/gost-plugin"
|
||||||
"github.com/metacubex/mihomo/transport/restls"
|
"github.com/metacubex/mihomo/transport/restls"
|
||||||
@@ -20,9 +18,9 @@ import (
|
|||||||
v2rayObfs "github.com/metacubex/mihomo/transport/v2ray-plugin"
|
v2rayObfs "github.com/metacubex/mihomo/transport/v2ray-plugin"
|
||||||
|
|
||||||
shadowsocks "github.com/metacubex/sing-shadowsocks2"
|
shadowsocks "github.com/metacubex/sing-shadowsocks2"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/metacubex/sing/common/bufio"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/metacubex/sing/common/metadata"
|
||||||
"github.com/sagernet/sing/common/uot"
|
"github.com/metacubex/sing/common/uot"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ShadowSocks struct {
|
type ShadowSocks struct {
|
||||||
@@ -64,6 +62,7 @@ type v2rayObfsOption struct {
|
|||||||
Host string `obfs:"host,omitempty"`
|
Host string `obfs:"host,omitempty"`
|
||||||
Path string `obfs:"path,omitempty"`
|
Path string `obfs:"path,omitempty"`
|
||||||
TLS bool `obfs:"tls,omitempty"`
|
TLS bool `obfs:"tls,omitempty"`
|
||||||
|
ECHOpts ECHOptions `obfs:"ech-opts,omitempty"`
|
||||||
Fingerprint string `obfs:"fingerprint,omitempty"`
|
Fingerprint string `obfs:"fingerprint,omitempty"`
|
||||||
Headers map[string]string `obfs:"headers,omitempty"`
|
Headers map[string]string `obfs:"headers,omitempty"`
|
||||||
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
||||||
@@ -77,6 +76,7 @@ type gostObfsOption struct {
|
|||||||
Host string `obfs:"host,omitempty"`
|
Host string `obfs:"host,omitempty"`
|
||||||
Path string `obfs:"path,omitempty"`
|
Path string `obfs:"path,omitempty"`
|
||||||
TLS bool `obfs:"tls,omitempty"`
|
TLS bool `obfs:"tls,omitempty"`
|
||||||
|
ECHOpts ECHOptions `obfs:"ech-opts,omitempty"`
|
||||||
Fingerprint string `obfs:"fingerprint,omitempty"`
|
Fingerprint string `obfs:"fingerprint,omitempty"`
|
||||||
Headers map[string]string `obfs:"headers,omitempty"`
|
Headers map[string]string `obfs:"headers,omitempty"`
|
||||||
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
||||||
@@ -84,11 +84,12 @@ type gostObfsOption struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type shadowTLSOption struct {
|
type shadowTLSOption struct {
|
||||||
Password string `obfs:"password"`
|
Password string `obfs:"password,omitempty"`
|
||||||
Host string `obfs:"host"`
|
Host string `obfs:"host"`
|
||||||
Fingerprint string `obfs:"fingerprint,omitempty"`
|
Fingerprint string `obfs:"fingerprint,omitempty"`
|
||||||
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
||||||
Version int `obfs:"version,omitempty"`
|
Version int `obfs:"version,omitempty"`
|
||||||
|
ALPN []string `obfs:"alpn,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type restlsOption struct {
|
type restlsOption struct {
|
||||||
@@ -154,8 +155,8 @@ func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metada
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
return ss.DialContextWithDialer(ctx, dialer.NewDialer(ss.Base.DialOptions(opts...)...), metadata)
|
return ss.DialContextWithDialer(ctx, dialer.NewDialer(ss.DialOptions()...), metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContextWithDialer implements C.ProxyAdapter
|
// DialContextWithDialer implements C.ProxyAdapter
|
||||||
@@ -180,8 +181,8 @@ func (ss *ShadowSocks) DialContextWithDialer(ctx context.Context, dialer C.Diale
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
return ss.ListenPacketWithDialer(ctx, dialer.NewDialer(ss.Base.DialOptions(opts...)...), metadata)
|
return ss.ListenPacketWithDialer(ctx, dialer.NewDialer(ss.DialOptions()...), metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketWithDialer implements C.ProxyAdapter
|
// ListenPacketWithDialer implements C.ProxyAdapter
|
||||||
@@ -199,6 +200,9 @@ func (ss *ShadowSocks) ListenPacketWithDialer(ctx context.Context, dialer C.Dial
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err = ss.ResolveUDP(ctx, metadata); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
addr, err := resolveUDPAddr(ctx, "udp", ss.addr, ss.prefer)
|
addr, err := resolveUDPAddr(ctx, "udp", ss.addr, ss.prefer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -227,15 +231,9 @@ func (ss *ShadowSocks) ProxyInfo() C.ProxyInfo {
|
|||||||
// ListenPacketOnStreamConn implements C.ProxyAdapter
|
// ListenPacketOnStreamConn implements C.ProxyAdapter
|
||||||
func (ss *ShadowSocks) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
func (ss *ShadowSocks) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
if ss.option.UDPOverTCP {
|
if ss.option.UDPOverTCP {
|
||||||
// ss uot use stream-oriented udp with a special address, so we need a net.UDPAddr
|
if err = ss.ResolveUDP(ctx, metadata); err != nil {
|
||||||
if !metadata.Resolved() {
|
return nil, err
|
||||||
ip, err := resolver.ResolveIP(ctx, metadata.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("can't resolve ip")
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destination := M.SocksaddrFromNet(metadata.UDPAddr())
|
destination := M.SocksaddrFromNet(metadata.UDPAddr())
|
||||||
if ss.option.UDPOverTCPVersion == uot.LegacyVersion {
|
if ss.option.UDPOverTCPVersion == uot.LegacyVersion {
|
||||||
return newPacketConn(N.NewThreadSafePacketConn(uot.NewConn(c, uot.Request{Destination: destination})), ss), nil
|
return newPacketConn(N.NewThreadSafePacketConn(uot.NewConn(c, uot.Request{Destination: destination})), ss), nil
|
||||||
@@ -302,6 +300,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
|||||||
v2rayOption.TLS = true
|
v2rayOption.TLS = true
|
||||||
v2rayOption.SkipCertVerify = opts.SkipCertVerify
|
v2rayOption.SkipCertVerify = opts.SkipCertVerify
|
||||||
v2rayOption.Fingerprint = opts.Fingerprint
|
v2rayOption.Fingerprint = opts.Fingerprint
|
||||||
|
|
||||||
|
echConfig, err := opts.ECHOpts.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %w", addr, err)
|
||||||
|
}
|
||||||
|
v2rayOption.ECHConfig = echConfig
|
||||||
}
|
}
|
||||||
} else if option.Plugin == "gost-plugin" {
|
} else if option.Plugin == "gost-plugin" {
|
||||||
opts := gostObfsOption{Host: "bing.com", Mux: true}
|
opts := gostObfsOption{Host: "bing.com", Mux: true}
|
||||||
@@ -324,6 +328,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
|||||||
gostOption.TLS = true
|
gostOption.TLS = true
|
||||||
gostOption.SkipCertVerify = opts.SkipCertVerify
|
gostOption.SkipCertVerify = opts.SkipCertVerify
|
||||||
gostOption.Fingerprint = opts.Fingerprint
|
gostOption.Fingerprint = opts.Fingerprint
|
||||||
|
|
||||||
|
echConfig, err := opts.ECHOpts.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ss %s initialize gost-plugin error: %w", addr, err)
|
||||||
|
}
|
||||||
|
gostOption.ECHConfig = echConfig
|
||||||
}
|
}
|
||||||
} else if option.Plugin == shadowtls.Mode {
|
} else if option.Plugin == shadowtls.Mode {
|
||||||
obfsMode = shadowtls.Mode
|
obfsMode = shadowtls.Mode
|
||||||
@@ -342,6 +352,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
|||||||
SkipCertVerify: opt.SkipCertVerify,
|
SkipCertVerify: opt.SkipCertVerify,
|
||||||
Version: opt.Version,
|
Version: opt.Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opt.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array
|
||||||
|
shadowTLSOpt.ALPN = opt.ALPN
|
||||||
|
} else {
|
||||||
|
shadowTLSOpt.ALPN = shadowtls.DefaultALPN
|
||||||
|
}
|
||||||
} else if option.Plugin == restls.Mode {
|
} else if option.Plugin == restls.Mode {
|
||||||
obfsMode = restls.Mode
|
obfsMode = restls.Mode
|
||||||
restlsOpt := &restlsOption{}
|
restlsOpt := &restlsOption{}
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ func (ssr *ShadowSocksR) StreamConnContext(ctx context.Context, c net.Conn, meta
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
return ssr.DialContextWithDialer(ctx, dialer.NewDialer(ssr.Base.DialOptions(opts...)...), metadata)
|
return ssr.DialContextWithDialer(ctx, dialer.NewDialer(ssr.DialOptions()...), metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContextWithDialer implements C.ProxyAdapter
|
// DialContextWithDialer implements C.ProxyAdapter
|
||||||
@@ -93,8 +93,8 @@ func (ssr *ShadowSocksR) DialContextWithDialer(ctx context.Context, dialer C.Dia
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
return ssr.ListenPacketWithDialer(ctx, dialer.NewDialer(ssr.Base.DialOptions(opts...)...), metadata)
|
return ssr.ListenPacketWithDialer(ctx, dialer.NewDialer(ssr.DialOptions()...), metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketWithDialer implements C.ProxyAdapter
|
// ListenPacketWithDialer implements C.ProxyAdapter
|
||||||
@@ -105,6 +105,9 @@ func (ssr *ShadowSocksR) ListenPacketWithDialer(ctx context.Context, dialer C.Di
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err = ssr.ResolveUDP(ctx, metadata); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
addr, err := resolveUDPAddr(ctx, "udp", ssr.addr, ssr.prefer)
|
addr, err := resolveUDPAddr(ctx, "udp", ssr.addr, ssr.prefer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -2,18 +2,16 @@ package outbound
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
|
|
||||||
CN "github.com/metacubex/mihomo/common/net"
|
CN "github.com/metacubex/mihomo/common/net"
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
"github.com/metacubex/mihomo/component/dialer"
|
||||||
"github.com/metacubex/mihomo/component/proxydialer"
|
"github.com/metacubex/mihomo/component/proxydialer"
|
||||||
"github.com/metacubex/mihomo/component/resolver"
|
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
"github.com/metacubex/mihomo/log"
|
"github.com/metacubex/mihomo/log"
|
||||||
|
|
||||||
mux "github.com/sagernet/sing-mux"
|
mux "github.com/metacubex/sing-mux"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/metacubex/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/metacubex/sing/common/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SingMux struct {
|
type SingMux struct {
|
||||||
@@ -41,9 +39,7 @@ type BrutalOption struct {
|
|||||||
Down string `proxy:"down,omitempty"`
|
Down string `proxy:"down,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SingMux) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
func (s *SingMux) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
options := s.ProxyAdapter.DialOptions(opts...)
|
|
||||||
s.dialer.SetDialer(dialer.NewDialer(options...))
|
|
||||||
c, err := s.client.DialContext(ctx, "tcp", M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
|
c, err := s.client.DialContext(ctx, "tcp", M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -51,22 +47,13 @@ func (s *SingMux) DialContext(ctx context.Context, metadata *C.Metadata, opts ..
|
|||||||
return NewConn(c, s), err
|
return NewConn(c, s), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
if s.onlyTcp {
|
if s.onlyTcp {
|
||||||
return s.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
|
return s.ProxyAdapter.ListenPacketContext(ctx, metadata)
|
||||||
}
|
}
|
||||||
options := s.ProxyAdapter.DialOptions(opts...)
|
if err = s.ProxyAdapter.ResolveUDP(ctx, metadata); err != nil {
|
||||||
s.dialer.SetDialer(dialer.NewDialer(options...))
|
return nil, err
|
||||||
|
|
||||||
// sing-mux use stream-oriented udp with a special address, so we need a net.UDPAddr
|
|
||||||
if !metadata.Resolved() {
|
|
||||||
ip, err := resolver.ResolveIP(ctx, metadata.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("can't resolve ip")
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pc, err := s.client.ListenPacket(ctx, M.SocksaddrFromNet(metadata.UDPAddr()))
|
pc, err := s.client.ListenPacket(ctx, M.SocksaddrFromNet(metadata.UDPAddr()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -109,7 +96,7 @@ func NewSingMux(option SingMuxOption, proxy ProxyAdapter) (ProxyAdapter, error)
|
|||||||
// TODO
|
// TODO
|
||||||
// "TCP Brutal is only supported on Linux-based systems"
|
// "TCP Brutal is only supported on Linux-based systems"
|
||||||
|
|
||||||
singDialer := proxydialer.NewSingDialer(proxy, dialer.NewDialer(), option.Statistic)
|
singDialer := proxydialer.NewSingDialer(proxy, dialer.NewDialer(proxy.DialOptions()...), option.Statistic)
|
||||||
client, err := mux.NewClient(mux.Options{
|
client, err := mux.NewClient(mux.Options{
|
||||||
Dialer: singDialer,
|
Dialer: singDialer,
|
||||||
Logger: log.SingLogger,
|
Logger: log.SingLogger,
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ func (s *Snell) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
if s.version == snell.Version2 && dialer.IsZeroOptions(opts) {
|
if s.version == snell.Version2 {
|
||||||
c, err := s.pool.Get()
|
c, err := s.pool.Get()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -89,7 +89,7 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
|
|||||||
return NewConn(c, s), err
|
return NewConn(c, s), err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.DialContextWithDialer(ctx, dialer.NewDialer(s.Base.DialOptions(opts...)...), metadata)
|
return s.DialContextWithDialer(ctx, dialer.NewDialer(s.DialOptions()...), metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContextWithDialer implements C.ProxyAdapter
|
// DialContextWithDialer implements C.ProxyAdapter
|
||||||
@@ -114,8 +114,8 @@ func (s *Snell) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
return s.ListenPacketWithDialer(ctx, dialer.NewDialer(s.Base.DialOptions(opts...)...), metadata)
|
return s.ListenPacketWithDialer(ctx, dialer.NewDialer(s.DialOptions()...), metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketWithDialer implements C.ProxyAdapter
|
// ListenPacketWithDialer implements C.ProxyAdapter
|
||||||
@@ -127,6 +127,9 @@ func (s *Snell) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err = s.ResolveUDP(ctx, metadata); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
c, err := dialer.DialContext(ctx, "tcp", s.addr)
|
c, err := dialer.DialContext(ctx, "tcp", s.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -207,7 +210,7 @@ func NewSnell(option SnellOption) (*Snell, error) {
|
|||||||
if option.Version == snell.Version2 {
|
if option.Version == snell.Version2 {
|
||||||
s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) {
|
s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) {
|
||||||
var err error
|
var err error
|
||||||
var cDialer C.Dialer = dialer.NewDialer(s.Base.DialOptions()...)
|
var cDialer C.Dialer = dialer.NewDialer(s.DialOptions()...)
|
||||||
if len(s.option.DialerProxy) > 0 {
|
if len(s.option.DialerProxy) > 0 {
|
||||||
cDialer, err = proxydialer.NewByName(s.option.DialerProxy, cDialer)
|
cDialer, err = proxydialer.NewByName(s.option.DialerProxy, cDialer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ func (ss *Socks5) StreamConnContext(ctx context.Context, c net.Conn, metadata *C
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
return ss.DialContextWithDialer(ctx, dialer.NewDialer(ss.Base.DialOptions(opts...)...), metadata)
|
return ss.DialContextWithDialer(ctx, dialer.NewDialer(ss.DialOptions()...), metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContextWithDialer implements C.ProxyAdapter
|
// DialContextWithDialer implements C.ProxyAdapter
|
||||||
@@ -101,14 +101,17 @@ func (ss *Socks5) SupportWithDialer() C.NetWork {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
var cDialer C.Dialer = dialer.NewDialer(ss.Base.DialOptions(opts...)...)
|
var cDialer C.Dialer = dialer.NewDialer(ss.DialOptions()...)
|
||||||
if len(ss.option.DialerProxy) > 0 {
|
if len(ss.option.DialerProxy) > 0 {
|
||||||
cDialer, err = proxydialer.NewByName(ss.option.DialerProxy, cDialer)
|
cDialer, err = proxydialer.NewByName(ss.option.DialerProxy, cDialer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err = ss.ResolveUDP(ctx, metadata); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
c, err := cDialer.DialContext(ctx, "tcp", ss.addr)
|
c, err := cDialer.DialContext(ctx, "tcp", ss.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("%s connect error: %w", ss.addr, err)
|
err = fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ type SshOption struct {
|
|||||||
HostKeyAlgorithms []string `proxy:"host-key-algorithms,omitempty"`
|
HostKeyAlgorithms []string `proxy:"host-key-algorithms,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Ssh) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
func (s *Ssh) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
var cDialer C.Dialer = dialer.NewDialer(s.Base.DialOptions(opts...)...)
|
var cDialer C.Dialer = dialer.NewDialer(s.DialOptions()...)
|
||||||
if len(s.option.DialerProxy) > 0 {
|
if len(s.option.DialerProxy) > 0 {
|
||||||
cDialer, err = proxydialer.NewByName(s.option.DialerProxy, cDialer)
|
cDialer, err = proxydialer.NewByName(s.option.DialerProxy, cDialer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -136,7 +136,11 @@ func NewSsh(option SshOption) (*Ssh, error) {
|
|||||||
if strings.Contains(option.PrivateKey, "PRIVATE KEY") {
|
if strings.Contains(option.PrivateKey, "PRIVATE KEY") {
|
||||||
b = []byte(option.PrivateKey)
|
b = []byte(option.PrivateKey)
|
||||||
} else {
|
} else {
|
||||||
b, err = os.ReadFile(C.Path.Resolve(option.PrivateKey))
|
path := C.Path.Resolve(option.PrivateKey)
|
||||||
|
if !C.Path.IsSafePath(path) {
|
||||||
|
return nil, C.Path.ErrNotSafePath(path)
|
||||||
|
}
|
||||||
|
b, err = os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
N "github.com/metacubex/mihomo/common/net"
|
N "github.com/metacubex/mihomo/common/net"
|
||||||
"github.com/metacubex/mihomo/component/ca"
|
"github.com/metacubex/mihomo/component/ca"
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
"github.com/metacubex/mihomo/component/dialer"
|
||||||
|
"github.com/metacubex/mihomo/component/ech"
|
||||||
"github.com/metacubex/mihomo/component/proxydialer"
|
"github.com/metacubex/mihomo/component/proxydialer"
|
||||||
tlsC "github.com/metacubex/mihomo/component/tls"
|
tlsC "github.com/metacubex/mihomo/component/tls"
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
@@ -32,6 +33,7 @@ type Trojan struct {
|
|||||||
transport *gun.TransportWrap
|
transport *gun.TransportWrap
|
||||||
|
|
||||||
realityConfig *tlsC.RealityConfig
|
realityConfig *tlsC.RealityConfig
|
||||||
|
echConfig *ech.Config
|
||||||
|
|
||||||
ssCipher core.Cipher
|
ssCipher core.Cipher
|
||||||
}
|
}
|
||||||
@@ -48,6 +50,7 @@ type TrojanOption struct {
|
|||||||
Fingerprint string `proxy:"fingerprint,omitempty"`
|
Fingerprint string `proxy:"fingerprint,omitempty"`
|
||||||
UDP bool `proxy:"udp,omitempty"`
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
Network string `proxy:"network,omitempty"`
|
Network string `proxy:"network,omitempty"`
|
||||||
|
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
|
||||||
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
|
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
|
||||||
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
|
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
|
||||||
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
|
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
|
||||||
@@ -77,6 +80,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
|
|||||||
V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade,
|
V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade,
|
||||||
V2rayHttpUpgradeFastOpen: t.option.WSOpts.V2rayHttpUpgradeFastOpen,
|
V2rayHttpUpgradeFastOpen: t.option.WSOpts.V2rayHttpUpgradeFastOpen,
|
||||||
ClientFingerprint: t.option.ClientFingerprint,
|
ClientFingerprint: t.option.ClientFingerprint,
|
||||||
|
ECHConfig: t.echConfig,
|
||||||
Headers: http.Header{},
|
Headers: http.Header{},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +114,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
|
|||||||
|
|
||||||
c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts)
|
c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts)
|
||||||
case "grpc":
|
case "grpc":
|
||||||
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.realityConfig)
|
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.echConfig, t.realityConfig)
|
||||||
default:
|
default:
|
||||||
// default tcp network
|
// default tcp network
|
||||||
// handle TLS
|
// handle TLS
|
||||||
@@ -124,6 +128,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
|
|||||||
FingerPrint: t.option.Fingerprint,
|
FingerPrint: t.option.Fingerprint,
|
||||||
ClientFingerprint: t.option.ClientFingerprint,
|
ClientFingerprint: t.option.ClientFingerprint,
|
||||||
NextProtos: alpn,
|
NextProtos: alpn,
|
||||||
|
ECH: t.echConfig,
|
||||||
Reality: t.realityConfig,
|
Reality: t.realityConfig,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -165,10 +170,10 @@ func (t *Trojan) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
var c net.Conn
|
var c net.Conn
|
||||||
// gun transport
|
// gun transport
|
||||||
if t.transport != nil && dialer.IsZeroOptions(opts) {
|
if t.transport != nil {
|
||||||
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
|
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -184,7 +189,7 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
|
|||||||
|
|
||||||
return NewConn(c, t), nil
|
return NewConn(c, t), nil
|
||||||
}
|
}
|
||||||
return t.DialContextWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
|
return t.DialContextWithDialer(ctx, dialer.NewDialer(t.DialOptions()...), metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContextWithDialer implements C.ProxyAdapter
|
// DialContextWithDialer implements C.ProxyAdapter
|
||||||
@@ -213,11 +218,15 @@ func (t *Trojan) DialContextWithDialer(ctx context.Context, dialer C.Dialer, met
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
|
if err = t.ResolveUDP(ctx, metadata); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var c net.Conn
|
var c net.Conn
|
||||||
|
|
||||||
// grpc transport
|
// grpc transport
|
||||||
if t.transport != nil && dialer.IsZeroOptions(opts) {
|
if t.transport != nil {
|
||||||
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
|
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
@@ -234,7 +243,7 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
|
|||||||
pc := trojan.NewPacketConn(c)
|
pc := trojan.NewPacketConn(c)
|
||||||
return newPacketConn(pc, t), err
|
return newPacketConn(pc, t), err
|
||||||
}
|
}
|
||||||
return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
|
return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.DialOptions()...), metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketWithDialer implements C.ProxyAdapter
|
// ListenPacketWithDialer implements C.ProxyAdapter
|
||||||
@@ -245,6 +254,9 @@ func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, me
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err = t.ResolveUDP(ctx, metadata); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
c, err := dialer.DialContext(ctx, "tcp", t.addr)
|
c, err := dialer.DialContext(ctx, "tcp", t.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
@@ -266,12 +278,6 @@ func (t *Trojan) SupportWithDialer() C.NetWork {
|
|||||||
return C.ALLNet
|
return C.ALLNet
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketOnStreamConn implements C.ProxyAdapter
|
|
||||||
func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
|
||||||
pc := trojan.NewPacketConn(c)
|
|
||||||
return newPacketConn(pc, t), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SupportUOT implements C.ProxyAdapter
|
// SupportUOT implements C.ProxyAdapter
|
||||||
func (t *Trojan) SupportUOT() bool {
|
func (t *Trojan) SupportUOT() bool {
|
||||||
return true
|
return true
|
||||||
@@ -295,6 +301,10 @@ func (t *Trojan) Close() error {
|
|||||||
func NewTrojan(option TrojanOption) (*Trojan, error) {
|
func NewTrojan(option TrojanOption) (*Trojan, error) {
|
||||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||||
|
|
||||||
|
if option.SNI == "" {
|
||||||
|
option.SNI = option.Server
|
||||||
|
}
|
||||||
|
|
||||||
t := &Trojan{
|
t := &Trojan{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
@@ -317,6 +327,11 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.echConfig, err = option.ECHOpts.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if option.SSOpts.Enabled {
|
if option.SSOpts.Enabled {
|
||||||
if option.SSOpts.Password == "" {
|
if option.SSOpts.Password == "" {
|
||||||
return nil, errors.New("empty password")
|
return nil, errors.New("empty password")
|
||||||
@@ -334,7 +349,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
|
|||||||
if option.Network == "grpc" {
|
if option.Network == "grpc" {
|
||||||
dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
var err error
|
var err error
|
||||||
var cDialer C.Dialer = dialer.NewDialer(t.Base.DialOptions()...)
|
var cDialer C.Dialer = dialer.NewDialer(t.DialOptions()...)
|
||||||
if len(t.option.DialerProxy) > 0 {
|
if len(t.option.DialerProxy) > 0 {
|
||||||
cDialer, err = proxydialer.NewByName(t.option.DialerProxy, cDialer)
|
cDialer, err = proxydialer.NewByName(t.option.DialerProxy, cDialer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -361,7 +376,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, option.ClientFingerprint, t.realityConfig)
|
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, option.ClientFingerprint, t.echConfig, t.realityConfig)
|
||||||
|
|
||||||
t.gunTLSConfig = tlsConfig
|
t.gunTLSConfig = tlsConfig
|
||||||
t.gunConfig = &gun.Config{
|
t.gunConfig = &gun.Config{
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package outbound
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
@@ -12,21 +11,25 @@ import (
|
|||||||
|
|
||||||
"github.com/metacubex/mihomo/component/ca"
|
"github.com/metacubex/mihomo/component/ca"
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
"github.com/metacubex/mihomo/component/dialer"
|
||||||
|
"github.com/metacubex/mihomo/component/ech"
|
||||||
"github.com/metacubex/mihomo/component/proxydialer"
|
"github.com/metacubex/mihomo/component/proxydialer"
|
||||||
"github.com/metacubex/mihomo/component/resolver"
|
tlsC "github.com/metacubex/mihomo/component/tls"
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
"github.com/metacubex/mihomo/transport/tuic"
|
"github.com/metacubex/mihomo/transport/tuic"
|
||||||
|
|
||||||
"github.com/gofrs/uuid/v5"
|
"github.com/gofrs/uuid/v5"
|
||||||
"github.com/metacubex/quic-go"
|
"github.com/metacubex/quic-go"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/metacubex/sing/common/metadata"
|
||||||
"github.com/sagernet/sing/common/uot"
|
"github.com/metacubex/sing/common/uot"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Tuic struct {
|
type Tuic struct {
|
||||||
*Base
|
*Base
|
||||||
option *TuicOption
|
option *TuicOption
|
||||||
client *tuic.PoolClient
|
client *tuic.PoolClient
|
||||||
|
|
||||||
|
tlsConfig *tlsC.Config
|
||||||
|
echConfig *ech.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
type TuicOption struct {
|
type TuicOption struct {
|
||||||
@@ -47,26 +50,27 @@ type TuicOption struct {
|
|||||||
DisableSni bool `proxy:"disable-sni,omitempty"`
|
DisableSni bool `proxy:"disable-sni,omitempty"`
|
||||||
MaxUdpRelayPacketSize int `proxy:"max-udp-relay-packet-size,omitempty"`
|
MaxUdpRelayPacketSize int `proxy:"max-udp-relay-packet-size,omitempty"`
|
||||||
|
|
||||||
FastOpen bool `proxy:"fast-open,omitempty"`
|
FastOpen bool `proxy:"fast-open,omitempty"`
|
||||||
MaxOpenStreams int `proxy:"max-open-streams,omitempty"`
|
MaxOpenStreams int `proxy:"max-open-streams,omitempty"`
|
||||||
CWND int `proxy:"cwnd,omitempty"`
|
CWND int `proxy:"cwnd,omitempty"`
|
||||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
Fingerprint string `proxy:"fingerprint,omitempty"`
|
Fingerprint string `proxy:"fingerprint,omitempty"`
|
||||||
CustomCA string `proxy:"ca,omitempty"`
|
CustomCA string `proxy:"ca,omitempty"`
|
||||||
CustomCAString string `proxy:"ca-str,omitempty"`
|
CustomCAString string `proxy:"ca-str,omitempty"`
|
||||||
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
|
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
|
||||||
ReceiveWindow int `proxy:"recv-window,omitempty"`
|
ReceiveWindow int `proxy:"recv-window,omitempty"`
|
||||||
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
|
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
|
||||||
MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"`
|
MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"`
|
||||||
SNI string `proxy:"sni,omitempty"`
|
SNI string `proxy:"sni,omitempty"`
|
||||||
|
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
|
||||||
|
|
||||||
UDPOverStream bool `proxy:"udp-over-stream,omitempty"`
|
UDPOverStream bool `proxy:"udp-over-stream,omitempty"`
|
||||||
UDPOverStreamVersion int `proxy:"udp-over-stream-version,omitempty"`
|
UDPOverStreamVersion int `proxy:"udp-over-stream-version,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (t *Tuic) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
func (t *Tuic) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
return t.DialContextWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
|
return t.DialContextWithDialer(ctx, dialer.NewDialer(t.DialOptions()...), metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContextWithDialer implements C.ProxyAdapter
|
// DialContextWithDialer implements C.ProxyAdapter
|
||||||
@@ -79,12 +83,16 @@ func (t *Tuic) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metad
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (t *Tuic) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
func (t *Tuic) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
|
return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.DialOptions()...), metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketWithDialer implements C.ProxyAdapter
|
// ListenPacketWithDialer implements C.ProxyAdapter
|
||||||
func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
|
if err = t.ResolveUDP(ctx, metadata); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if t.option.UDPOverStream {
|
if t.option.UDPOverStream {
|
||||||
uotDestination := uot.RequestDestination(uint8(t.option.UDPOverStreamVersion))
|
uotDestination := uot.RequestDestination(uint8(t.option.UDPOverStreamVersion))
|
||||||
uotMetadata := *metadata
|
uotMetadata := *metadata
|
||||||
@@ -96,13 +104,6 @@ func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, meta
|
|||||||
}
|
}
|
||||||
|
|
||||||
// tuic uos use stream-oriented udp with a special address, so we need a net.UDPAddr
|
// tuic uos use stream-oriented udp with a special address, so we need a net.UDPAddr
|
||||||
if !metadata.Resolved() {
|
|
||||||
ip, err := resolver.ResolveIP(ctx, metadata.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("can't resolve ip")
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
|
||||||
}
|
|
||||||
|
|
||||||
destination := M.SocksaddrFromNet(metadata.UDPAddr())
|
destination := M.SocksaddrFromNet(metadata.UDPAddr())
|
||||||
if t.option.UDPOverStreamVersion == uot.LegacyVersion {
|
if t.option.UDPOverStreamVersion == uot.LegacyVersion {
|
||||||
@@ -134,6 +135,10 @@ func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (transport *
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
err = t.echConfig.ClientHandle(ctx, t.tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
addr = udpAddr
|
addr = udpAddr
|
||||||
var pc net.PacketConn
|
var pc net.PacketConn
|
||||||
pc, err = dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
|
pc, err = dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
|
||||||
@@ -248,6 +253,12 @@ func NewTuic(option TuicOption) (*Tuic, error) {
|
|||||||
tlsConfig.InsecureSkipVerify = true // tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config
|
tlsConfig.InsecureSkipVerify = true // tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tlsClientConfig := tlsC.UConfig(tlsConfig)
|
||||||
|
echConfig, err := option.ECHOpts.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
switch option.UDPOverStreamVersion {
|
switch option.UDPOverStreamVersion {
|
||||||
case uot.Version, uot.LegacyVersion:
|
case uot.Version, uot.LegacyVersion:
|
||||||
case 0:
|
case 0:
|
||||||
@@ -267,7 +278,9 @@ func NewTuic(option TuicOption) (*Tuic, error) {
|
|||||||
rmark: option.RoutingMark,
|
rmark: option.RoutingMark,
|
||||||
prefer: C.NewDNSPrefer(option.IPVersion),
|
prefer: C.NewDNSPrefer(option.IPVersion),
|
||||||
},
|
},
|
||||||
option: &option,
|
option: &option,
|
||||||
|
tlsConfig: tlsClientConfig,
|
||||||
|
echConfig: echConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
clientMaxOpenStreams := int64(option.MaxOpenStreams)
|
clientMaxOpenStreams := int64(option.MaxOpenStreams)
|
||||||
@@ -284,7 +297,7 @@ func NewTuic(option TuicOption) (*Tuic, error) {
|
|||||||
if len(option.Token) > 0 {
|
if len(option.Token) > 0 {
|
||||||
tkn := tuic.GenTKN(option.Token)
|
tkn := tuic.GenTKN(option.Token)
|
||||||
clientOption := &tuic.ClientOptionV4{
|
clientOption := &tuic.ClientOptionV4{
|
||||||
TlsConfig: tlsConfig,
|
TlsConfig: tlsClientConfig,
|
||||||
QuicConfig: quicConfig,
|
QuicConfig: quicConfig,
|
||||||
Token: tkn,
|
Token: tkn,
|
||||||
UdpRelayMode: udpRelayMode,
|
UdpRelayMode: udpRelayMode,
|
||||||
@@ -304,7 +317,7 @@ func NewTuic(option TuicOption) (*Tuic, error) {
|
|||||||
maxUdpRelayPacketSize = tuic.MaxFragSizeV5
|
maxUdpRelayPacketSize = tuic.MaxFragSizeV5
|
||||||
}
|
}
|
||||||
clientOption := &tuic.ClientOptionV5{
|
clientOption := &tuic.ClientOptionV5{
|
||||||
TlsConfig: tlsConfig,
|
TlsConfig: tlsClientConfig,
|
||||||
QuicConfig: quicConfig,
|
QuicConfig: quicConfig,
|
||||||
Uuid: uuid.FromStringOrNil(option.UUID),
|
Uuid: uuid.FromStringOrNil(option.UUID),
|
||||||
Password: option.Password,
|
Password: option.Password,
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import (
|
|||||||
"github.com/metacubex/mihomo/common/utils"
|
"github.com/metacubex/mihomo/common/utils"
|
||||||
"github.com/metacubex/mihomo/component/ca"
|
"github.com/metacubex/mihomo/component/ca"
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
"github.com/metacubex/mihomo/component/dialer"
|
||||||
|
"github.com/metacubex/mihomo/component/ech"
|
||||||
"github.com/metacubex/mihomo/component/proxydialer"
|
"github.com/metacubex/mihomo/component/proxydialer"
|
||||||
"github.com/metacubex/mihomo/component/resolver"
|
|
||||||
tlsC "github.com/metacubex/mihomo/component/tls"
|
tlsC "github.com/metacubex/mihomo/component/tls"
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
"github.com/metacubex/mihomo/transport/gun"
|
"github.com/metacubex/mihomo/transport/gun"
|
||||||
@@ -27,7 +27,7 @@ import (
|
|||||||
|
|
||||||
vmessSing "github.com/metacubex/sing-vmess"
|
vmessSing "github.com/metacubex/sing-vmess"
|
||||||
"github.com/metacubex/sing-vmess/packetaddr"
|
"github.com/metacubex/sing-vmess/packetaddr"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/metacubex/sing/common/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -46,6 +46,7 @@ type Vless struct {
|
|||||||
transport *gun.TransportWrap
|
transport *gun.TransportWrap
|
||||||
|
|
||||||
realityConfig *tlsC.RealityConfig
|
realityConfig *tlsC.RealityConfig
|
||||||
|
echConfig *ech.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
type VlessOption struct {
|
type VlessOption struct {
|
||||||
@@ -62,6 +63,7 @@ type VlessOption struct {
|
|||||||
XUDP bool `proxy:"xudp,omitempty"`
|
XUDP bool `proxy:"xudp,omitempty"`
|
||||||
PacketEncoding string `proxy:"packet-encoding,omitempty"`
|
PacketEncoding string `proxy:"packet-encoding,omitempty"`
|
||||||
Network string `proxy:"network,omitempty"`
|
Network string `proxy:"network,omitempty"`
|
||||||
|
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
|
||||||
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
|
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
|
||||||
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
|
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
|
||||||
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
|
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
|
||||||
@@ -88,6 +90,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
|
|||||||
V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
|
V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
|
||||||
V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen,
|
V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen,
|
||||||
ClientFingerprint: v.option.ClientFingerprint,
|
ClientFingerprint: v.option.ClientFingerprint,
|
||||||
|
ECHConfig: v.echConfig,
|
||||||
Headers: http.Header{},
|
Headers: http.Header{},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +154,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
|
|||||||
|
|
||||||
c, err = vmess.StreamH2Conn(ctx, c, h2Opts)
|
c, err = vmess.StreamH2Conn(ctx, c, h2Opts)
|
||||||
case "grpc":
|
case "grpc":
|
||||||
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig)
|
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.echConfig, v.realityConfig)
|
||||||
default:
|
default:
|
||||||
// default tcp network
|
// default tcp network
|
||||||
// handle TLS
|
// handle TLS
|
||||||
@@ -206,6 +209,7 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne
|
|||||||
SkipCertVerify: v.option.SkipCertVerify,
|
SkipCertVerify: v.option.SkipCertVerify,
|
||||||
FingerPrint: v.option.Fingerprint,
|
FingerPrint: v.option.Fingerprint,
|
||||||
ClientFingerprint: v.option.ClientFingerprint,
|
ClientFingerprint: v.option.ClientFingerprint,
|
||||||
|
ECH: v.echConfig,
|
||||||
Reality: v.realityConfig,
|
Reality: v.realityConfig,
|
||||||
NextProtos: v.option.ALPN,
|
NextProtos: v.option.ALPN,
|
||||||
}
|
}
|
||||||
@@ -225,10 +229,10 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
var c net.Conn
|
var c net.Conn
|
||||||
// gun transport
|
// gun transport
|
||||||
if v.transport != nil && dialer.IsZeroOptions(opts) {
|
if v.transport != nil {
|
||||||
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -244,7 +248,7 @@ func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
|
|||||||
|
|
||||||
return NewConn(c, v), nil
|
return NewConn(c, v), nil
|
||||||
}
|
}
|
||||||
return v.DialContextWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata)
|
return v.DialContextWithDialer(ctx, dialer.NewDialer(v.DialOptions()...), metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContextWithDialer implements C.ProxyAdapter
|
// DialContextWithDialer implements C.ProxyAdapter
|
||||||
@@ -271,18 +275,13 @@ func (v *Vless) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
// vless use stream-oriented udp with a special address, so we need a net.UDPAddr
|
if err = v.ResolveUDP(ctx, metadata); err != nil {
|
||||||
if !metadata.Resolved() {
|
return nil, err
|
||||||
ip, err := resolver.ResolveIP(ctx, metadata.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("can't resolve ip")
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
|
||||||
}
|
}
|
||||||
var c net.Conn
|
var c net.Conn
|
||||||
// gun transport
|
// gun transport
|
||||||
if v.transport != nil && dialer.IsZeroOptions(opts) {
|
if v.transport != nil {
|
||||||
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -298,7 +297,7 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
|
|||||||
|
|
||||||
return v.ListenPacketOnStreamConn(ctx, c, metadata)
|
return v.ListenPacketOnStreamConn(ctx, c, metadata)
|
||||||
}
|
}
|
||||||
return v.ListenPacketWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata)
|
return v.ListenPacketWithDialer(ctx, dialer.NewDialer(v.DialOptions()...), metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketWithDialer implements C.ProxyAdapter
|
// ListenPacketWithDialer implements C.ProxyAdapter
|
||||||
@@ -310,13 +309,8 @@ func (v *Vless) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// vless use stream-oriented udp with a special address, so we need a net.UDPAddr
|
if err = v.ResolveUDP(ctx, metadata); err != nil {
|
||||||
if !metadata.Resolved() {
|
return nil, err
|
||||||
ip, err := resolver.ResolveIP(ctx, metadata.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("can't resolve ip")
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := dialer.DialContext(ctx, "tcp", v.addr)
|
c, err := dialer.DialContext(ctx, "tcp", v.addr)
|
||||||
@@ -342,13 +336,8 @@ func (v *Vless) SupportWithDialer() C.NetWork {
|
|||||||
|
|
||||||
// ListenPacketOnStreamConn implements C.ProxyAdapter
|
// ListenPacketOnStreamConn implements C.ProxyAdapter
|
||||||
func (v *Vless) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
func (v *Vless) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
// vless use stream-oriented udp with a special address, so we need a net.UDPAddr
|
if err = v.ResolveUDP(ctx, metadata); err != nil {
|
||||||
if !metadata.Resolved() {
|
return nil, err
|
||||||
ip, err := resolver.ResolveIP(ctx, metadata.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("can't resolve ip")
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.option.XUDP {
|
if v.option.XUDP {
|
||||||
@@ -563,6 +552,11 @@ func NewVless(option VlessOption) (*Vless, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v.echConfig, err = v.option.ECHOpts.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
switch option.Network {
|
switch option.Network {
|
||||||
case "h2":
|
case "h2":
|
||||||
if len(option.HTTP2Opts.Host) == 0 {
|
if len(option.HTTP2Opts.Host) == 0 {
|
||||||
@@ -571,7 +565,7 @@ func NewVless(option VlessOption) (*Vless, error) {
|
|||||||
case "grpc":
|
case "grpc":
|
||||||
dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
var err error
|
var err error
|
||||||
var cDialer C.Dialer = dialer.NewDialer(v.Base.DialOptions()...)
|
var cDialer C.Dialer = dialer.NewDialer(v.DialOptions()...)
|
||||||
if len(v.option.DialerProxy) > 0 {
|
if len(v.option.DialerProxy) > 0 {
|
||||||
cDialer, err = proxydialer.NewByName(v.option.DialerProxy, cDialer)
|
cDialer, err = proxydialer.NewByName(v.option.DialerProxy, cDialer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -611,7 +605,7 @@ func NewVless(option VlessOption) (*Vless, error) {
|
|||||||
v.gunTLSConfig = tlsConfig
|
v.gunTLSConfig = tlsConfig
|
||||||
v.gunConfig = gunConfig
|
v.gunConfig = gunConfig
|
||||||
|
|
||||||
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig)
|
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.echConfig, v.realityConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
return v, nil
|
return v, nil
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import (
|
|||||||
"github.com/metacubex/mihomo/common/utils"
|
"github.com/metacubex/mihomo/common/utils"
|
||||||
"github.com/metacubex/mihomo/component/ca"
|
"github.com/metacubex/mihomo/component/ca"
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
"github.com/metacubex/mihomo/component/dialer"
|
||||||
|
"github.com/metacubex/mihomo/component/ech"
|
||||||
"github.com/metacubex/mihomo/component/proxydialer"
|
"github.com/metacubex/mihomo/component/proxydialer"
|
||||||
"github.com/metacubex/mihomo/component/resolver"
|
|
||||||
tlsC "github.com/metacubex/mihomo/component/tls"
|
tlsC "github.com/metacubex/mihomo/component/tls"
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
"github.com/metacubex/mihomo/ntp"
|
"github.com/metacubex/mihomo/ntp"
|
||||||
@@ -25,7 +25,7 @@ import (
|
|||||||
|
|
||||||
vmess "github.com/metacubex/sing-vmess"
|
vmess "github.com/metacubex/sing-vmess"
|
||||||
"github.com/metacubex/sing-vmess/packetaddr"
|
"github.com/metacubex/sing-vmess/packetaddr"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/metacubex/sing/common/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrUDPRemoteAddrMismatch = errors.New("udp packet dropped due to mismatched remote address")
|
var ErrUDPRemoteAddrMismatch = errors.New("udp packet dropped due to mismatched remote address")
|
||||||
@@ -41,6 +41,7 @@ type Vmess struct {
|
|||||||
transport *gun.TransportWrap
|
transport *gun.TransportWrap
|
||||||
|
|
||||||
realityConfig *tlsC.RealityConfig
|
realityConfig *tlsC.RealityConfig
|
||||||
|
echConfig *ech.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
type VmessOption struct {
|
type VmessOption struct {
|
||||||
@@ -58,6 +59,7 @@ type VmessOption struct {
|
|||||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
Fingerprint string `proxy:"fingerprint,omitempty"`
|
Fingerprint string `proxy:"fingerprint,omitempty"`
|
||||||
ServerName string `proxy:"servername,omitempty"`
|
ServerName string `proxy:"servername,omitempty"`
|
||||||
|
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
|
||||||
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
|
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
|
||||||
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
|
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
|
||||||
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
|
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
|
||||||
@@ -109,6 +111,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
|
|||||||
V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
|
V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
|
||||||
V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen,
|
V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen,
|
||||||
ClientFingerprint: v.option.ClientFingerprint,
|
ClientFingerprint: v.option.ClientFingerprint,
|
||||||
|
ECHConfig: v.echConfig,
|
||||||
Headers: http.Header{},
|
Headers: http.Header{},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +149,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
|
|||||||
Host: host,
|
Host: host,
|
||||||
SkipCertVerify: v.option.SkipCertVerify,
|
SkipCertVerify: v.option.SkipCertVerify,
|
||||||
ClientFingerprint: v.option.ClientFingerprint,
|
ClientFingerprint: v.option.ClientFingerprint,
|
||||||
|
ECH: v.echConfig,
|
||||||
Reality: v.realityConfig,
|
Reality: v.realityConfig,
|
||||||
NextProtos: v.option.ALPN,
|
NextProtos: v.option.ALPN,
|
||||||
}
|
}
|
||||||
@@ -195,7 +199,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
|
|||||||
|
|
||||||
c, err = mihomoVMess.StreamH2Conn(ctx, c, h2Opts)
|
c, err = mihomoVMess.StreamH2Conn(ctx, c, h2Opts)
|
||||||
case "grpc":
|
case "grpc":
|
||||||
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig)
|
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.echConfig, v.realityConfig)
|
||||||
default:
|
default:
|
||||||
// handle TLS
|
// handle TLS
|
||||||
if v.option.TLS {
|
if v.option.TLS {
|
||||||
@@ -205,6 +209,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
|
|||||||
SkipCertVerify: v.option.SkipCertVerify,
|
SkipCertVerify: v.option.SkipCertVerify,
|
||||||
FingerPrint: v.option.Fingerprint,
|
FingerPrint: v.option.Fingerprint,
|
||||||
ClientFingerprint: v.option.ClientFingerprint,
|
ClientFingerprint: v.option.ClientFingerprint,
|
||||||
|
ECH: v.echConfig,
|
||||||
Reality: v.realityConfig,
|
Reality: v.realityConfig,
|
||||||
NextProtos: v.option.ALPN,
|
NextProtos: v.option.ALPN,
|
||||||
}
|
}
|
||||||
@@ -280,10 +285,10 @@ func (v *Vmess) streamConnContext(ctx context.Context, c net.Conn, metadata *C.M
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
var c net.Conn
|
var c net.Conn
|
||||||
// gun transport
|
// gun transport
|
||||||
if v.transport != nil && dialer.IsZeroOptions(opts) {
|
if v.transport != nil {
|
||||||
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -299,7 +304,7 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
|
|||||||
|
|
||||||
return NewConn(c, v), nil
|
return NewConn(c, v), nil
|
||||||
}
|
}
|
||||||
return v.DialContextWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata)
|
return v.DialContextWithDialer(ctx, dialer.NewDialer(v.DialOptions()...), metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContextWithDialer implements C.ProxyAdapter
|
// DialContextWithDialer implements C.ProxyAdapter
|
||||||
@@ -323,18 +328,13 @@ func (v *Vmess) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
// vmess use stream-oriented udp with a special address, so we need a net.UDPAddr
|
if err = v.ResolveUDP(ctx, metadata); err != nil {
|
||||||
if !metadata.Resolved() {
|
return nil, err
|
||||||
ip, err := resolver.ResolveIP(ctx, metadata.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("can't resolve ip")
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
|
||||||
}
|
}
|
||||||
var c net.Conn
|
var c net.Conn
|
||||||
// gun transport
|
// gun transport
|
||||||
if v.transport != nil && dialer.IsZeroOptions(opts) {
|
if v.transport != nil {
|
||||||
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -349,7 +349,7 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
|
|||||||
}
|
}
|
||||||
return v.ListenPacketOnStreamConn(ctx, c, metadata)
|
return v.ListenPacketOnStreamConn(ctx, c, metadata)
|
||||||
}
|
}
|
||||||
return v.ListenPacketWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata)
|
return v.ListenPacketWithDialer(ctx, dialer.NewDialer(v.DialOptions()...), metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketWithDialer implements C.ProxyAdapter
|
// ListenPacketWithDialer implements C.ProxyAdapter
|
||||||
@@ -361,13 +361,8 @@ func (v *Vmess) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// vmess use stream-oriented udp with a special address, so we need a net.UDPAddr
|
if err = v.ResolveUDP(ctx, metadata); err != nil {
|
||||||
if !metadata.Resolved() {
|
return nil, err
|
||||||
ip, err := resolver.ResolveIP(ctx, metadata.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("can't resolve ip")
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := dialer.DialContext(ctx, "tcp", v.addr)
|
c, err := dialer.DialContext(ctx, "tcp", v.addr)
|
||||||
@@ -407,13 +402,8 @@ func (v *Vmess) Close() error {
|
|||||||
|
|
||||||
// ListenPacketOnStreamConn implements C.ProxyAdapter
|
// ListenPacketOnStreamConn implements C.ProxyAdapter
|
||||||
func (v *Vmess) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
func (v *Vmess) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
// vmess use stream-oriented udp with a special address, so we need a net.UDPAddr
|
if err = v.ResolveUDP(ctx, metadata); err != nil {
|
||||||
if !metadata.Resolved() {
|
return nil, err
|
||||||
ip, err := resolver.ResolveIP(ctx, metadata.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("can't resolve ip")
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if pc, ok := c.(net.PacketConn); ok {
|
if pc, ok := c.(net.PacketConn); ok {
|
||||||
@@ -474,6 +464,11 @@ func NewVmess(option VmessOption) (*Vmess, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v.echConfig, err = v.option.ECHOpts.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
switch option.Network {
|
switch option.Network {
|
||||||
case "h2":
|
case "h2":
|
||||||
if len(option.HTTP2Opts.Host) == 0 {
|
if len(option.HTTP2Opts.Host) == 0 {
|
||||||
@@ -482,7 +477,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
|
|||||||
case "grpc":
|
case "grpc":
|
||||||
dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
var err error
|
var err error
|
||||||
var cDialer C.Dialer = dialer.NewDialer(v.Base.DialOptions()...)
|
var cDialer C.Dialer = dialer.NewDialer(v.DialOptions()...)
|
||||||
if len(v.option.DialerProxy) > 0 {
|
if len(v.option.DialerProxy) > 0 {
|
||||||
cDialer, err = proxydialer.NewByName(v.option.DialerProxy, cDialer)
|
cDialer, err = proxydialer.NewByName(v.option.DialerProxy, cDialer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -522,7 +517,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
|
|||||||
v.gunTLSConfig = tlsConfig
|
v.gunTLSConfig = tlsConfig
|
||||||
v.gunConfig = gunConfig
|
v.gunConfig = gunConfig
|
||||||
|
|
||||||
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig)
|
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.echConfig, v.realityConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
return v, nil
|
return v, nil
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -26,9 +25,9 @@ import (
|
|||||||
wireguard "github.com/metacubex/sing-wireguard"
|
wireguard "github.com/metacubex/sing-wireguard"
|
||||||
"github.com/metacubex/wireguard-go/device"
|
"github.com/metacubex/wireguard-go/device"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/debug"
|
"github.com/metacubex/sing/common/debug"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/metacubex/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/metacubex/sing/common/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
type wireguardGoDevice interface {
|
type wireguardGoDevice interface {
|
||||||
@@ -166,8 +165,9 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) {
|
|||||||
rmark: option.RoutingMark,
|
rmark: option.RoutingMark,
|
||||||
prefer: C.NewDNSPrefer(option.IPVersion),
|
prefer: C.NewDNSPrefer(option.IPVersion),
|
||||||
},
|
},
|
||||||
dialer: proxydialer.NewSlowDownSingDialer(proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer()), slowdown.New()),
|
|
||||||
}
|
}
|
||||||
|
singDialer := proxydialer.NewSlowDownSingDialer(proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer(outbound.DialOptions()...)), slowdown.New())
|
||||||
|
outbound.dialer = singDialer
|
||||||
|
|
||||||
var reserved [3]uint8
|
var reserved [3]uint8
|
||||||
if len(option.Reserved) > 0 {
|
if len(option.Reserved) > 0 {
|
||||||
@@ -488,9 +488,7 @@ func (w *WireGuard) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
options := w.Base.DialOptions(opts...)
|
|
||||||
w.dialer.SetDialer(dialer.NewDialer(options...))
|
|
||||||
var conn net.Conn
|
var conn net.Conn
|
||||||
if err = w.init(ctx); err != nil {
|
if err = w.init(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -500,6 +498,7 @@ func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts
|
|||||||
if w.resolver != nil {
|
if w.resolver != nil {
|
||||||
r = w.resolver
|
r = w.resolver
|
||||||
}
|
}
|
||||||
|
options := w.DialOptions()
|
||||||
options = append(options, dialer.WithResolver(r))
|
options = append(options, dialer.WithResolver(r))
|
||||||
options = append(options, dialer.WithNetDialer(wgNetDialer{tunDevice: w.tunDevice}))
|
options = append(options, dialer.WithNetDialer(wgNetDialer{tunDevice: w.tunDevice}))
|
||||||
conn, err = dialer.NewDialer(options...).DialContext(ctx, "tcp", metadata.RemoteAddress())
|
conn, err = dialer.NewDialer(options...).DialContext(ctx, "tcp", metadata.RemoteAddress())
|
||||||
@@ -515,23 +514,13 @@ func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts
|
|||||||
return NewConn(conn, w), nil
|
return NewConn(conn, w), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
options := w.Base.DialOptions(opts...)
|
|
||||||
w.dialer.SetDialer(dialer.NewDialer(options...))
|
|
||||||
var pc net.PacketConn
|
var pc net.PacketConn
|
||||||
if err = w.init(ctx); err != nil {
|
if err = w.init(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" {
|
if err = w.ResolveUDP(ctx, metadata); err != nil {
|
||||||
r := resolver.DefaultResolver
|
return nil, err
|
||||||
if w.resolver != nil {
|
|
||||||
r = w.resolver
|
|
||||||
}
|
|
||||||
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("can't resolve ip")
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
|
||||||
}
|
}
|
||||||
pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap())
|
pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -543,6 +532,21 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat
|
|||||||
return newPacketConn(pc, w), nil
|
return newPacketConn(pc, w), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *WireGuard) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
|
||||||
|
if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" {
|
||||||
|
r := resolver.DefaultResolver
|
||||||
|
if w.resolver != nil {
|
||||||
|
r = w.resolver
|
||||||
|
}
|
||||||
|
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't resolve ip: %w", err)
|
||||||
|
}
|
||||||
|
metadata.DstIP = ip
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// IsL3Protocol implements C.ProxyAdapter
|
// IsL3Protocol implements C.ProxyAdapter
|
||||||
func (w *WireGuard) IsL3Protocol(metadata *C.Metadata) bool {
|
func (w *WireGuard) IsL3Protocol(metadata *C.Metadata) bool {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -6,11 +6,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/adapter/outbound"
|
|
||||||
"github.com/metacubex/mihomo/common/callback"
|
"github.com/metacubex/mihomo/common/callback"
|
||||||
N "github.com/metacubex/mihomo/common/net"
|
N "github.com/metacubex/mihomo/common/net"
|
||||||
"github.com/metacubex/mihomo/common/utils"
|
"github.com/metacubex/mihomo/common/utils"
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
"github.com/metacubex/mihomo/constant/provider"
|
"github.com/metacubex/mihomo/constant/provider"
|
||||||
)
|
)
|
||||||
@@ -31,9 +29,9 @@ func (f *Fallback) Now() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
proxy := f.findAliveProxy(true)
|
proxy := f.findAliveProxy(true)
|
||||||
c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...)
|
c, err := proxy.DialContext(ctx, metadata)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.AppendToChains(f)
|
c.AppendToChains(f)
|
||||||
} else {
|
} else {
|
||||||
@@ -54,9 +52,9 @@ func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts .
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
proxy := f.findAliveProxy(true)
|
proxy := f.findAliveProxy(true)
|
||||||
pc, err := proxy.ListenPacketContext(ctx, metadata, f.Base.DialOptions(opts...)...)
|
pc, err := proxy.ListenPacketContext(ctx, metadata)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
pc.AppendToChains(f)
|
pc.AppendToChains(f)
|
||||||
}
|
}
|
||||||
@@ -155,18 +153,14 @@ func (f *Fallback) ForceSet(name string) {
|
|||||||
func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
|
func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
|
||||||
return &Fallback{
|
return &Fallback{
|
||||||
GroupBase: NewGroupBase(GroupBaseOption{
|
GroupBase: NewGroupBase(GroupBaseOption{
|
||||||
outbound.BaseOption{
|
Name: option.Name,
|
||||||
Name: option.Name,
|
Type: C.Fallback,
|
||||||
Type: C.Fallback,
|
Filter: option.Filter,
|
||||||
Interface: option.Interface,
|
ExcludeFilter: option.ExcludeFilter,
|
||||||
RoutingMark: option.RoutingMark,
|
ExcludeType: option.ExcludeType,
|
||||||
},
|
TestTimeout: option.TestTimeout,
|
||||||
option.Filter,
|
MaxFailedTimes: option.MaxFailedTimes,
|
||||||
option.ExcludeFilter,
|
Providers: providers,
|
||||||
option.ExcludeType,
|
|
||||||
option.TestTimeout,
|
|
||||||
option.MaxFailedTimes,
|
|
||||||
providers,
|
|
||||||
}),
|
}),
|
||||||
disableUDP: option.DisableUDP,
|
disableUDP: option.DisableUDP,
|
||||||
testUrl: option.URL,
|
testUrl: option.URL,
|
||||||
|
|||||||
@@ -41,53 +41,47 @@ type GroupBase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GroupBaseOption struct {
|
type GroupBaseOption struct {
|
||||||
outbound.BaseOption
|
Name string
|
||||||
filter string
|
Type C.AdapterType
|
||||||
excludeFilter string
|
Filter string
|
||||||
excludeType string
|
ExcludeFilter string
|
||||||
|
ExcludeType string
|
||||||
TestTimeout int
|
TestTimeout int
|
||||||
maxFailedTimes int
|
MaxFailedTimes int
|
||||||
providers []provider.ProxyProvider
|
Providers []provider.ProxyProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGroupBase(opt GroupBaseOption) *GroupBase {
|
func NewGroupBase(opt GroupBaseOption) *GroupBase {
|
||||||
if opt.RoutingMark != 0 {
|
|
||||||
log.Warnln("The group [%s] with routing-mark configuration is deprecated, please set it directly on the proxy instead", opt.Name)
|
|
||||||
}
|
|
||||||
if opt.Interface != "" {
|
|
||||||
log.Warnln("The group [%s] with interface-name configuration is deprecated, please set it directly on the proxy instead", opt.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
var excludeTypeArray []string
|
var excludeTypeArray []string
|
||||||
if opt.excludeType != "" {
|
if opt.ExcludeType != "" {
|
||||||
excludeTypeArray = strings.Split(opt.excludeType, "|")
|
excludeTypeArray = strings.Split(opt.ExcludeType, "|")
|
||||||
}
|
}
|
||||||
|
|
||||||
var excludeFilterRegs []*regexp2.Regexp
|
var excludeFilterRegs []*regexp2.Regexp
|
||||||
if opt.excludeFilter != "" {
|
if opt.ExcludeFilter != "" {
|
||||||
for _, excludeFilter := range strings.Split(opt.excludeFilter, "`") {
|
for _, excludeFilter := range strings.Split(opt.ExcludeFilter, "`") {
|
||||||
excludeFilterReg := regexp2.MustCompile(excludeFilter, regexp2.None)
|
excludeFilterReg := regexp2.MustCompile(excludeFilter, regexp2.None)
|
||||||
excludeFilterRegs = append(excludeFilterRegs, excludeFilterReg)
|
excludeFilterRegs = append(excludeFilterRegs, excludeFilterReg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var filterRegs []*regexp2.Regexp
|
var filterRegs []*regexp2.Regexp
|
||||||
if opt.filter != "" {
|
if opt.Filter != "" {
|
||||||
for _, filter := range strings.Split(opt.filter, "`") {
|
for _, filter := range strings.Split(opt.Filter, "`") {
|
||||||
filterReg := regexp2.MustCompile(filter, regexp2.None)
|
filterReg := regexp2.MustCompile(filter, regexp2.None)
|
||||||
filterRegs = append(filterRegs, filterReg)
|
filterRegs = append(filterRegs, filterReg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gb := &GroupBase{
|
gb := &GroupBase{
|
||||||
Base: outbound.NewBase(opt.BaseOption),
|
Base: outbound.NewBase(outbound.BaseOption{Name: opt.Name, Type: opt.Type}),
|
||||||
filterRegs: filterRegs,
|
filterRegs: filterRegs,
|
||||||
excludeFilterRegs: excludeFilterRegs,
|
excludeFilterRegs: excludeFilterRegs,
|
||||||
excludeTypeArray: excludeTypeArray,
|
excludeTypeArray: excludeTypeArray,
|
||||||
providers: opt.providers,
|
providers: opt.Providers,
|
||||||
failedTesting: atomic.NewBool(false),
|
failedTesting: atomic.NewBool(false),
|
||||||
TestTimeout: opt.TestTimeout,
|
TestTimeout: opt.TestTimeout,
|
||||||
maxFailedTimes: opt.maxFailedTimes,
|
maxFailedTimes: opt.MaxFailedTimes,
|
||||||
}
|
}
|
||||||
|
|
||||||
if gb.TestTimeout == 0 {
|
if gb.TestTimeout == 0 {
|
||||||
|
|||||||
@@ -9,12 +9,10 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/adapter/outbound"
|
|
||||||
"github.com/metacubex/mihomo/common/callback"
|
"github.com/metacubex/mihomo/common/callback"
|
||||||
"github.com/metacubex/mihomo/common/lru"
|
"github.com/metacubex/mihomo/common/lru"
|
||||||
N "github.com/metacubex/mihomo/common/net"
|
N "github.com/metacubex/mihomo/common/net"
|
||||||
"github.com/metacubex/mihomo/common/utils"
|
"github.com/metacubex/mihomo/common/utils"
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
"github.com/metacubex/mihomo/constant/provider"
|
"github.com/metacubex/mihomo/constant/provider"
|
||||||
|
|
||||||
@@ -88,9 +86,9 @@ func jumpHash(key uint64, buckets int32) int32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
|
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
|
||||||
proxy := lb.Unwrap(metadata, true)
|
proxy := lb.Unwrap(metadata, true)
|
||||||
c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
|
c, err = proxy.DialContext(ctx, metadata)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.AppendToChains(lb)
|
c.AppendToChains(lb)
|
||||||
@@ -112,7 +110,7 @@ func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, op
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (pc C.PacketConn, err error) {
|
func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (pc C.PacketConn, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
pc.AppendToChains(lb)
|
pc.AppendToChains(lb)
|
||||||
@@ -120,7 +118,7 @@ func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Meta
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
proxy := lb.Unwrap(metadata, true)
|
proxy := lb.Unwrap(metadata, true)
|
||||||
return proxy.ListenPacketContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
|
return proxy.ListenPacketContext(ctx, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SupportUDP implements C.ProxyAdapter
|
// SupportUDP implements C.ProxyAdapter
|
||||||
@@ -255,18 +253,14 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide
|
|||||||
}
|
}
|
||||||
return &LoadBalance{
|
return &LoadBalance{
|
||||||
GroupBase: NewGroupBase(GroupBaseOption{
|
GroupBase: NewGroupBase(GroupBaseOption{
|
||||||
outbound.BaseOption{
|
Name: option.Name,
|
||||||
Name: option.Name,
|
Type: C.LoadBalance,
|
||||||
Type: C.LoadBalance,
|
Filter: option.Filter,
|
||||||
Interface: option.Interface,
|
ExcludeFilter: option.ExcludeFilter,
|
||||||
RoutingMark: option.RoutingMark,
|
ExcludeType: option.ExcludeType,
|
||||||
},
|
TestTimeout: option.TestTimeout,
|
||||||
option.Filter,
|
MaxFailedTimes: option.MaxFailedTimes,
|
||||||
option.ExcludeFilter,
|
Providers: providers,
|
||||||
option.ExcludeType,
|
|
||||||
option.TestTimeout,
|
|
||||||
option.MaxFailedTimes,
|
|
||||||
providers,
|
|
||||||
}),
|
}),
|
||||||
strategyFn: strategyFn,
|
strategyFn: strategyFn,
|
||||||
disableUDP: option.DisableUDP,
|
disableUDP: option.DisableUDP,
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import (
|
|||||||
|
|
||||||
"github.com/dlclark/regexp2"
|
"github.com/dlclark/regexp2"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/adapter/outbound"
|
|
||||||
"github.com/metacubex/mihomo/adapter/provider"
|
"github.com/metacubex/mihomo/adapter/provider"
|
||||||
"github.com/metacubex/mihomo/common/structure"
|
"github.com/metacubex/mihomo/common/structure"
|
||||||
"github.com/metacubex/mihomo/common/utils"
|
"github.com/metacubex/mihomo/common/utils"
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
types "github.com/metacubex/mihomo/constant/provider"
|
types "github.com/metacubex/mihomo/constant/provider"
|
||||||
|
"github.com/metacubex/mihomo/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -23,7 +23,6 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type GroupCommonOption struct {
|
type GroupCommonOption struct {
|
||||||
outbound.BasicOption
|
|
||||||
Name string `group:"name"`
|
Name string `group:"name"`
|
||||||
Type string `group:"type"`
|
Type string `group:"type"`
|
||||||
Proxies []string `group:"proxies,omitempty"`
|
Proxies []string `group:"proxies,omitempty"`
|
||||||
@@ -43,6 +42,10 @@ type GroupCommonOption struct {
|
|||||||
IncludeAllProviders bool `group:"include-all-providers,omitempty"`
|
IncludeAllProviders bool `group:"include-all-providers,omitempty"`
|
||||||
Hidden bool `group:"hidden,omitempty"`
|
Hidden bool `group:"hidden,omitempty"`
|
||||||
Icon string `group:"icon,omitempty"`
|
Icon string `group:"icon,omitempty"`
|
||||||
|
|
||||||
|
// removed configs, only for error logging
|
||||||
|
Interface string `group:"interface-name,omitempty"`
|
||||||
|
RoutingMark int `group:"routing-mark,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider, AllProxies []string, AllProviders []string) (C.ProxyAdapter, error) {
|
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider, AllProxies []string, AllProviders []string) (C.ProxyAdapter, error) {
|
||||||
@@ -59,6 +62,13 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
|
|||||||
return nil, errFormat
|
return nil, errFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if groupOption.RoutingMark != 0 {
|
||||||
|
log.Errorln("The group [%s] with routing-mark configuration was removed, please set it directly on the proxy instead", groupOption.Name)
|
||||||
|
}
|
||||||
|
if groupOption.Interface != "" {
|
||||||
|
log.Errorln("The group [%s] with interface-name configuration was removed, please set it directly on the proxy instead", groupOption.Name)
|
||||||
|
}
|
||||||
|
|
||||||
groupName := groupOption.Name
|
groupName := groupOption.Name
|
||||||
|
|
||||||
providers := []types.ProxyProvider{}
|
providers := []types.ProxyProvider{}
|
||||||
|
|||||||
@@ -19,17 +19,17 @@ type Relay struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
proxies, chainProxies := r.proxies(metadata, true)
|
proxies, chainProxies := r.proxies(metadata, true)
|
||||||
|
|
||||||
switch len(proxies) {
|
switch len(proxies) {
|
||||||
case 0:
|
case 0:
|
||||||
return outbound.NewDirect().DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
|
return outbound.NewDirect().DialContext(ctx, metadata)
|
||||||
case 1:
|
case 1:
|
||||||
return proxies[0].DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
|
return proxies[0].DialContext(ctx, metadata)
|
||||||
}
|
}
|
||||||
var d C.Dialer
|
var d C.Dialer
|
||||||
d = dialer.NewDialer(r.Base.DialOptions(opts...)...)
|
d = dialer.NewDialer()
|
||||||
for _, proxy := range proxies[:len(proxies)-1] {
|
for _, proxy := range proxies[:len(proxies)-1] {
|
||||||
d = proxydialer.New(proxy, d, false)
|
d = proxydialer.New(proxy, d, false)
|
||||||
}
|
}
|
||||||
@@ -49,18 +49,18 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (r *Relay) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
func (r *Relay) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
proxies, chainProxies := r.proxies(metadata, true)
|
proxies, chainProxies := r.proxies(metadata, true)
|
||||||
|
|
||||||
switch len(proxies) {
|
switch len(proxies) {
|
||||||
case 0:
|
case 0:
|
||||||
return outbound.NewDirect().ListenPacketContext(ctx, metadata, r.Base.DialOptions(opts...)...)
|
return outbound.NewDirect().ListenPacketContext(ctx, metadata)
|
||||||
case 1:
|
case 1:
|
||||||
return proxies[0].ListenPacketContext(ctx, metadata, r.Base.DialOptions(opts...)...)
|
return proxies[0].ListenPacketContext(ctx, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
var d C.Dialer
|
var d C.Dialer
|
||||||
d = dialer.NewDialer(r.Base.DialOptions(opts...)...)
|
d = dialer.NewDialer()
|
||||||
for _, proxy := range proxies[:len(proxies)-1] {
|
for _, proxy := range proxies[:len(proxies)-1] {
|
||||||
d = proxydialer.New(proxy, d, false)
|
d = proxydialer.New(proxy, d, false)
|
||||||
}
|
}
|
||||||
@@ -153,18 +153,9 @@ func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Re
|
|||||||
log.Warnln("The group [%s] with relay type is deprecated, please using dialer-proxy instead", option.Name)
|
log.Warnln("The group [%s] with relay type is deprecated, please using dialer-proxy instead", option.Name)
|
||||||
return &Relay{
|
return &Relay{
|
||||||
GroupBase: NewGroupBase(GroupBaseOption{
|
GroupBase: NewGroupBase(GroupBaseOption{
|
||||||
outbound.BaseOption{
|
Name: option.Name,
|
||||||
Name: option.Name,
|
Type: C.Relay,
|
||||||
Type: C.Relay,
|
Providers: providers,
|
||||||
Interface: option.Interface,
|
|
||||||
RoutingMark: option.RoutingMark,
|
|
||||||
},
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
5000,
|
|
||||||
5,
|
|
||||||
providers,
|
|
||||||
}),
|
}),
|
||||||
Hidden: option.Hidden,
|
Hidden: option.Hidden,
|
||||||
Icon: option.Icon,
|
Icon: option.Icon,
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/adapter/outbound"
|
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
"github.com/metacubex/mihomo/constant/provider"
|
"github.com/metacubex/mihomo/constant/provider"
|
||||||
)
|
)
|
||||||
@@ -15,13 +13,14 @@ type Selector struct {
|
|||||||
*GroupBase
|
*GroupBase
|
||||||
disableUDP bool
|
disableUDP bool
|
||||||
selected string
|
selected string
|
||||||
|
testUrl string
|
||||||
Hidden bool
|
Hidden bool
|
||||||
Icon string
|
Icon string
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
c, err := s.selectedProxy(true).DialContext(ctx, metadata, s.Base.DialOptions(opts...)...)
|
c, err := s.selectedProxy(true).DialContext(ctx, metadata)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.AppendToChains(s)
|
c.AppendToChains(s)
|
||||||
}
|
}
|
||||||
@@ -29,8 +28,8 @@ func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata, opts .
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (s *Selector) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
func (s *Selector) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
pc, err := s.selectedProxy(true).ListenPacketContext(ctx, metadata, s.Base.DialOptions(opts...)...)
|
pc, err := s.selectedProxy(true).ListenPacketContext(ctx, metadata)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
pc.AppendToChains(s)
|
pc.AppendToChains(s)
|
||||||
}
|
}
|
||||||
@@ -57,13 +56,20 @@ func (s *Selector) MarshalJSON() ([]byte, error) {
|
|||||||
for _, proxy := range s.GetProxies(false) {
|
for _, proxy := range s.GetProxies(false) {
|
||||||
all = append(all, proxy.Name())
|
all = append(all, proxy.Name())
|
||||||
}
|
}
|
||||||
|
// When testurl is the default value
|
||||||
|
// do not append a value to ensure that the web dashboard follows the settings of the dashboard
|
||||||
|
var url string
|
||||||
|
if s.testUrl != C.DefaultTestURL {
|
||||||
|
url = s.testUrl
|
||||||
|
}
|
||||||
|
|
||||||
return json.Marshal(map[string]any{
|
return json.Marshal(map[string]any{
|
||||||
"type": s.Type().String(),
|
"type": s.Type().String(),
|
||||||
"now": s.Now(),
|
"now": s.Now(),
|
||||||
"all": all,
|
"all": all,
|
||||||
"hidden": s.Hidden,
|
"testUrl": url,
|
||||||
"icon": s.Icon,
|
"hidden": s.Hidden,
|
||||||
|
"icon": s.Icon,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,21 +111,18 @@ func (s *Selector) selectedProxy(touch bool) C.Proxy {
|
|||||||
func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
|
func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
|
||||||
return &Selector{
|
return &Selector{
|
||||||
GroupBase: NewGroupBase(GroupBaseOption{
|
GroupBase: NewGroupBase(GroupBaseOption{
|
||||||
outbound.BaseOption{
|
Name: option.Name,
|
||||||
Name: option.Name,
|
Type: C.Selector,
|
||||||
Type: C.Selector,
|
Filter: option.Filter,
|
||||||
Interface: option.Interface,
|
ExcludeFilter: option.ExcludeFilter,
|
||||||
RoutingMark: option.RoutingMark,
|
ExcludeType: option.ExcludeType,
|
||||||
},
|
TestTimeout: option.TestTimeout,
|
||||||
option.Filter,
|
MaxFailedTimes: option.MaxFailedTimes,
|
||||||
option.ExcludeFilter,
|
Providers: providers,
|
||||||
option.ExcludeType,
|
|
||||||
option.TestTimeout,
|
|
||||||
option.MaxFailedTimes,
|
|
||||||
providers,
|
|
||||||
}),
|
}),
|
||||||
selected: "COMPATIBLE",
|
selected: "COMPATIBLE",
|
||||||
disableUDP: option.DisableUDP,
|
disableUDP: option.DisableUDP,
|
||||||
|
testUrl: option.URL,
|
||||||
Hidden: option.Hidden,
|
Hidden: option.Hidden,
|
||||||
Icon: option.Icon,
|
Icon: option.Icon,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/adapter/outbound"
|
|
||||||
"github.com/metacubex/mihomo/common/callback"
|
"github.com/metacubex/mihomo/common/callback"
|
||||||
N "github.com/metacubex/mihomo/common/net"
|
N "github.com/metacubex/mihomo/common/net"
|
||||||
"github.com/metacubex/mihomo/common/singledo"
|
"github.com/metacubex/mihomo/common/singledo"
|
||||||
"github.com/metacubex/mihomo/common/utils"
|
"github.com/metacubex/mihomo/common/utils"
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
"github.com/metacubex/mihomo/constant/provider"
|
"github.com/metacubex/mihomo/constant/provider"
|
||||||
)
|
)
|
||||||
@@ -62,9 +60,9 @@ func (u *URLTest) ForceSet(name string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
|
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
|
||||||
proxy := u.fast(true)
|
proxy := u.fast(true)
|
||||||
c, err = proxy.DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
|
c, err = proxy.DialContext(ctx, metadata)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.AppendToChains(u)
|
c.AppendToChains(u)
|
||||||
} else {
|
} else {
|
||||||
@@ -85,9 +83,9 @@ func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ..
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
proxy := u.fast(true)
|
proxy := u.fast(true)
|
||||||
pc, err := proxy.ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...)
|
pc, err := proxy.ListenPacketContext(ctx, metadata)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
pc.AppendToChains(u)
|
pc.AppendToChains(u)
|
||||||
} else {
|
} else {
|
||||||
@@ -207,19 +205,14 @@ func parseURLTestOption(config map[string]any) []urlTestOption {
|
|||||||
func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
|
func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
|
||||||
urlTest := &URLTest{
|
urlTest := &URLTest{
|
||||||
GroupBase: NewGroupBase(GroupBaseOption{
|
GroupBase: NewGroupBase(GroupBaseOption{
|
||||||
outbound.BaseOption{
|
Name: option.Name,
|
||||||
Name: option.Name,
|
Type: C.URLTest,
|
||||||
Type: C.URLTest,
|
Filter: option.Filter,
|
||||||
Interface: option.Interface,
|
ExcludeFilter: option.ExcludeFilter,
|
||||||
RoutingMark: option.RoutingMark,
|
ExcludeType: option.ExcludeType,
|
||||||
},
|
TestTimeout: option.TestTimeout,
|
||||||
|
MaxFailedTimes: option.MaxFailedTimes,
|
||||||
option.Filter,
|
Providers: providers,
|
||||||
option.ExcludeFilter,
|
|
||||||
option.ExcludeType,
|
|
||||||
option.TestTimeout,
|
|
||||||
option.MaxFailedTimes,
|
|
||||||
providers,
|
|
||||||
}),
|
}),
|
||||||
fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10),
|
fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10),
|
||||||
disableUDP: option.DisableUDP,
|
disableUDP: option.DisableUDP,
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/common/atomic"
|
"github.com/metacubex/mihomo/common/atomic"
|
||||||
"github.com/metacubex/mihomo/common/batch"
|
|
||||||
"github.com/metacubex/mihomo/common/singledo"
|
"github.com/metacubex/mihomo/common/singledo"
|
||||||
"github.com/metacubex/mihomo/common/utils"
|
"github.com/metacubex/mihomo/common/utils"
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
"github.com/metacubex/mihomo/log"
|
"github.com/metacubex/mihomo/log"
|
||||||
|
|
||||||
"github.com/dlclark/regexp2"
|
"github.com/dlclark/regexp2"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HealthCheckOption struct {
|
type HealthCheckOption struct {
|
||||||
@@ -32,7 +32,6 @@ type HealthCheck struct {
|
|||||||
url string
|
url string
|
||||||
extra map[string]*extraOption
|
extra map[string]*extraOption
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
started atomic.Bool
|
|
||||||
proxies []C.Proxy
|
proxies []C.Proxy
|
||||||
interval time.Duration
|
interval time.Duration
|
||||||
lazy bool
|
lazy bool
|
||||||
@@ -43,13 +42,8 @@ type HealthCheck struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (hc *HealthCheck) process() {
|
func (hc *HealthCheck) process() {
|
||||||
if hc.started.Load() {
|
|
||||||
log.Warnln("Skip start health check timer due to it's started")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ticker := time.NewTicker(hc.interval)
|
ticker := time.NewTicker(hc.interval)
|
||||||
hc.start()
|
go hc.check()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
@@ -62,13 +56,12 @@ func (hc *HealthCheck) process() {
|
|||||||
}
|
}
|
||||||
case <-hc.ctx.Done():
|
case <-hc.ctx.Done():
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
hc.stop()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hc *HealthCheck) setProxy(proxies []C.Proxy) {
|
func (hc *HealthCheck) setProxies(proxies []C.Proxy) {
|
||||||
hc.proxies = proxies
|
hc.proxies = proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,10 +98,6 @@ func (hc *HealthCheck) registerHealthCheckTask(url string, expectedStatus utils.
|
|||||||
option := &extraOption{filters: map[string]struct{}{}, expectedStatus: expectedStatus}
|
option := &extraOption{filters: map[string]struct{}{}, expectedStatus: expectedStatus}
|
||||||
splitAndAddFiltersToExtra(filter, option)
|
splitAndAddFiltersToExtra(filter, option)
|
||||||
hc.extra[url] = option
|
hc.extra[url] = option
|
||||||
|
|
||||||
if hc.auto() && !hc.started.Load() {
|
|
||||||
go hc.process()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitAndAddFiltersToExtra(filter string, option *extraOption) {
|
func splitAndAddFiltersToExtra(filter string, option *extraOption) {
|
||||||
@@ -131,14 +120,6 @@ func (hc *HealthCheck) touch() {
|
|||||||
hc.lastTouch.Store(time.Now())
|
hc.lastTouch.Store(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hc *HealthCheck) start() {
|
|
||||||
hc.started.Store(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hc *HealthCheck) stop() {
|
|
||||||
hc.started.Store(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hc *HealthCheck) check() {
|
func (hc *HealthCheck) check() {
|
||||||
if len(hc.proxies) == 0 {
|
if len(hc.proxies) == 0 {
|
||||||
return
|
return
|
||||||
@@ -147,7 +128,8 @@ func (hc *HealthCheck) check() {
|
|||||||
_, _, _ = hc.singleDo.Do(func() (struct{}, error) {
|
_, _, _ = hc.singleDo.Do(func() (struct{}, error) {
|
||||||
id := utils.NewUUIDV4().String()
|
id := utils.NewUUIDV4().String()
|
||||||
log.Debugln("Start New Health Checking {%s}", id)
|
log.Debugln("Start New Health Checking {%s}", id)
|
||||||
b, _ := batch.New[bool](hc.ctx, batch.WithConcurrencyNum[bool](10))
|
b := new(errgroup.Group)
|
||||||
|
b.SetLimit(10)
|
||||||
|
|
||||||
// execute default health check
|
// execute default health check
|
||||||
option := &extraOption{filters: nil, expectedStatus: hc.expectedStatus}
|
option := &extraOption{filters: nil, expectedStatus: hc.expectedStatus}
|
||||||
@@ -159,13 +141,13 @@ func (hc *HealthCheck) check() {
|
|||||||
hc.execute(b, url, id, option)
|
hc.execute(b, url, id, option)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b.Wait()
|
_ = b.Wait()
|
||||||
log.Debugln("Finish A Health Checking {%s}", id)
|
log.Debugln("Finish A Health Checking {%s}", id)
|
||||||
return struct{}{}, nil
|
return struct{}{}, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hc *HealthCheck) execute(b *batch.Batch[bool], url, uid string, option *extraOption) {
|
func (hc *HealthCheck) execute(b *errgroup.Group, url, uid string, option *extraOption) {
|
||||||
url = strings.TrimSpace(url)
|
url = strings.TrimSpace(url)
|
||||||
if len(url) == 0 {
|
if len(url) == 0 {
|
||||||
log.Debugln("Health Check has been skipped due to testUrl is empty, {%s}", uid)
|
log.Debugln("Health Check has been skipped due to testUrl is empty, {%s}", uid)
|
||||||
@@ -195,13 +177,13 @@ func (hc *HealthCheck) execute(b *batch.Batch[bool], url, uid string, option *ex
|
|||||||
}
|
}
|
||||||
|
|
||||||
p := proxy
|
p := proxy
|
||||||
b.Go(p.Name(), func() (bool, error) {
|
b.Go(func() error {
|
||||||
ctx, cancel := context.WithTimeout(hc.ctx, hc.timeout)
|
ctx, cancel := context.WithTimeout(hc.ctx, hc.timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
log.Debugln("Health Checking, proxy: %s, url: %s, id: {%s}", p.Name(), url, uid)
|
log.Debugln("Health Checking, proxy: %s, url: %s, id: {%s}", p.Name(), url, uid)
|
||||||
_, _ = p.URLTest(ctx, url, expectedStatus)
|
_, _ = p.URLTest(ctx, url, expectedStatus)
|
||||||
log.Debugln("Health Checked, proxy: %s, url: %s, alive: %t, delay: %d ms uid: {%s}", p.Name(), url, p.AliveForTestUrl(url), p.LastDelayForTestUrl(url), uid)
|
log.Debugln("Health Checked, proxy: %s, url: %s, alive: %t, delay: %d ms uid: {%s}", p.Name(), url, p.AliveForTestUrl(url), p.LastDelayForTestUrl(url), uid)
|
||||||
return false, nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
errVehicleType = errors.New("unsupport vehicle type")
|
errVehicleType = errors.New("unsupport vehicle type")
|
||||||
errSubPath = errors.New("path is not subpath of home directory")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type healthCheckSchema struct {
|
type healthCheckSchema struct {
|
||||||
@@ -115,7 +114,7 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
|
|||||||
if schema.Path != "" {
|
if schema.Path != "" {
|
||||||
path = C.Path.Resolve(schema.Path)
|
path = C.Path.Resolve(schema.Path)
|
||||||
if !C.Path.IsSafePath(path) {
|
if !C.Path.IsSafePath(path) {
|
||||||
return nil, fmt.Errorf("%w: %s", errSubPath, path)
|
return nil, C.Path.ErrNotSafePath(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header, resource.DefaultHttpTimeout, schema.SizeLimit)
|
vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header, resource.DefaultHttpTimeout, schema.SizeLimit)
|
||||||
@@ -127,5 +126,5 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
|
|||||||
|
|
||||||
interval := time.Duration(uint(schema.Interval)) * time.Second
|
interval := time.Duration(uint(schema.Interval)) * time.Second
|
||||||
|
|
||||||
return NewProxySetProvider(name, interval, parser, vehicle, hc)
|
return NewProxySetProvider(name, interval, schema.Payload, parser, vehicle, hc)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,13 @@ func (bp *baseProvider) Version() uint32 {
|
|||||||
return bp.version
|
return bp.version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bp *baseProvider) Initial() error {
|
||||||
|
if bp.healthCheck.auto() {
|
||||||
|
go bp.healthCheck.process()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (bp *baseProvider) HealthCheck() {
|
func (bp *baseProvider) HealthCheck() {
|
||||||
bp.healthCheck.check()
|
bp.healthCheck.check()
|
||||||
}
|
}
|
||||||
@@ -88,7 +95,7 @@ func (bp *baseProvider) RegisterHealthCheckTask(url string, expectedStatus utils
|
|||||||
func (bp *baseProvider) setProxies(proxies []C.Proxy) {
|
func (bp *baseProvider) setProxies(proxies []C.Proxy) {
|
||||||
bp.proxies = proxies
|
bp.proxies = proxies
|
||||||
bp.version += 1
|
bp.version += 1
|
||||||
bp.healthCheck.setProxy(proxies)
|
bp.healthCheck.setProxies(proxies)
|
||||||
if bp.healthCheck.auto() {
|
if bp.healthCheck.auto() {
|
||||||
go bp.healthCheck.check()
|
go bp.healthCheck.check()
|
||||||
}
|
}
|
||||||
@@ -133,6 +140,9 @@ func (pp *proxySetProvider) Update() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pp *proxySetProvider) Initial() error {
|
func (pp *proxySetProvider) Initial() error {
|
||||||
|
if err := pp.baseProvider.Initial(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
_, err := pp.Fetcher.Initial()
|
_, err := pp.Fetcher.Initial()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -161,11 +171,7 @@ func (pp *proxySetProvider) Close() error {
|
|||||||
return pp.Fetcher.Close()
|
return pp.Fetcher.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProxySetProvider(name string, interval time.Duration, parser resource.Parser[[]C.Proxy], vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
|
func NewProxySetProvider(name string, interval time.Duration, payload []map[string]any, parser resource.Parser[[]C.Proxy], vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
|
||||||
if hc.auto() {
|
|
||||||
go hc.process()
|
|
||||||
}
|
|
||||||
|
|
||||||
pd := &proxySetProvider{
|
pd := &proxySetProvider{
|
||||||
baseProvider: baseProvider{
|
baseProvider: baseProvider{
|
||||||
name: name,
|
name: name,
|
||||||
@@ -174,6 +180,21 @@ func NewProxySetProvider(name string, interval time.Duration, parser resource.Pa
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(payload) > 0 { // using as fallback proxies
|
||||||
|
ps := ProxySchema{Proxies: payload}
|
||||||
|
buf, err := yaml.Marshal(ps)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
proxies, err := parser(buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pd.proxies = proxies
|
||||||
|
// direct call setProxies on hc to avoid starting a health check process immediately, it should be done by Initial()
|
||||||
|
hc.setProxies(proxies)
|
||||||
|
}
|
||||||
|
|
||||||
fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, parser, pd.setProxies)
|
fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, parser, pd.setProxies)
|
||||||
pd.Fetcher = fetcher
|
pd.Fetcher = fetcher
|
||||||
if httpVehicle, ok := vehicle.(*resource.HTTPVehicle); ok {
|
if httpVehicle, ok := vehicle.(*resource.HTTPVehicle); ok {
|
||||||
@@ -221,10 +242,6 @@ func (ip *inlineProvider) VehicleType() types.VehicleType {
|
|||||||
return types.Inline
|
return types.Inline
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ip *inlineProvider) Initial() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ip *inlineProvider) Update() error {
|
func (ip *inlineProvider) Update() error {
|
||||||
// make api update happy
|
// make api update happy
|
||||||
ip.updateAt = time.Now()
|
ip.updateAt = time.Now()
|
||||||
@@ -232,10 +249,6 @@ func (ip *inlineProvider) Update() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewInlineProvider(name string, payload []map[string]any, parser resource.Parser[[]C.Proxy], hc *HealthCheck) (*InlineProvider, error) {
|
func NewInlineProvider(name string, payload []map[string]any, parser resource.Parser[[]C.Proxy], hc *HealthCheck) (*InlineProvider, error) {
|
||||||
if hc.auto() {
|
|
||||||
go hc.process()
|
|
||||||
}
|
|
||||||
|
|
||||||
ps := ProxySchema{Proxies: payload}
|
ps := ProxySchema{Proxies: payload}
|
||||||
buf, err := yaml.Marshal(ps)
|
buf, err := yaml.Marshal(ps)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -245,6 +258,8 @@ func NewInlineProvider(name string, payload []map[string]any, parser resource.Pa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// direct call setProxies on hc to avoid starting a health check process immediately, it should be done by Initial()
|
||||||
|
hc.setProxies(proxies)
|
||||||
|
|
||||||
ip := &inlineProvider{
|
ip := &inlineProvider{
|
||||||
baseProvider: baseProvider{
|
baseProvider: baseProvider{
|
||||||
@@ -288,13 +303,6 @@ func (cp *compatibleProvider) Update() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *compatibleProvider) Initial() error {
|
|
||||||
if cp.healthCheck.interval != 0 && cp.healthCheck.url != "" {
|
|
||||||
cp.HealthCheck()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp *compatibleProvider) VehicleType() types.VehicleType {
|
func (cp *compatibleProvider) VehicleType() types.VehicleType {
|
||||||
return types.Compatible
|
return types.Compatible
|
||||||
}
|
}
|
||||||
@@ -304,10 +312,6 @@ func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*Co
|
|||||||
return nil, errors.New("provider need one proxy at least")
|
return nil, errors.New("provider need one proxy at least")
|
||||||
}
|
}
|
||||||
|
|
||||||
if hc.auto() {
|
|
||||||
go hc.process()
|
|
||||||
}
|
|
||||||
|
|
||||||
pd := &compatibleProvider{
|
pd := &compatibleProvider{
|
||||||
baseProvider: baseProvider{
|
baseProvider: baseProvider{
|
||||||
name: name,
|
name: name,
|
||||||
|
|||||||
63
common/atomic/enum.go
Normal file
63
common/atomic/enum.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package atomic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Int32Enum[T ~int32] struct {
|
||||||
|
value atomic.Int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Int32Enum[T]) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(i.Load())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Int32Enum[T]) UnmarshalJSON(b []byte) error {
|
||||||
|
var v T
|
||||||
|
if err := json.Unmarshal(b, &v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.Store(v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Int32Enum[T]) MarshalYAML() (any, error) {
|
||||||
|
return i.Load(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Int32Enum[T]) UnmarshalYAML(unmarshal func(any) error) error {
|
||||||
|
var v T
|
||||||
|
if err := unmarshal(&v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.Store(v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Int32Enum[T]) String() string {
|
||||||
|
return fmt.Sprint(i.Load())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Int32Enum[T]) Store(v T) {
|
||||||
|
i.value.Store(int32(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Int32Enum[T]) Load() T {
|
||||||
|
return T(i.value.Load())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Int32Enum[T]) Swap(new T) T {
|
||||||
|
return T(i.value.Swap(int32(new)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Int32Enum[T]) CompareAndSwap(old, new T) bool {
|
||||||
|
return i.value.CompareAndSwap(int32(old), int32(new))
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInt32Enum[T ~int32](v T) *Int32Enum[T] {
|
||||||
|
a := &Int32Enum[T]{}
|
||||||
|
a.Store(v)
|
||||||
|
return a
|
||||||
|
}
|
||||||
@@ -29,6 +29,19 @@ func (i *Bool) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Bool) MarshalYAML() (any, error) {
|
||||||
|
return i.Load(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Bool) UnmarshalYAML(unmarshal func(any) error) error {
|
||||||
|
var v bool
|
||||||
|
if err := unmarshal(&v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.Store(v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (i *Bool) String() string {
|
func (i *Bool) String() string {
|
||||||
v := i.Load()
|
v := i.Load()
|
||||||
return strconv.FormatBool(v)
|
return strconv.FormatBool(v)
|
||||||
@@ -58,6 +71,19 @@ func (p *Pointer[T]) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Pointer[T]) MarshalYAML() (any, error) {
|
||||||
|
return p.Load(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pointer[T]) UnmarshalYAML(unmarshal func(any) error) error {
|
||||||
|
var v *T
|
||||||
|
if err := unmarshal(&v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Store(v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Pointer[T]) String() string {
|
func (p *Pointer[T]) String() string {
|
||||||
return fmt.Sprint(p.Load())
|
return fmt.Sprint(p.Load())
|
||||||
}
|
}
|
||||||
@@ -84,6 +110,19 @@ func (i *Int32) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Int32) MarshalYAML() (any, error) {
|
||||||
|
return i.Load(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Int32) UnmarshalYAML(unmarshal func(any) error) error {
|
||||||
|
var v int32
|
||||||
|
if err := unmarshal(&v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.Store(v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (i *Int32) String() string {
|
func (i *Int32) String() string {
|
||||||
v := i.Load()
|
v := i.Load()
|
||||||
return strconv.FormatInt(int64(v), 10)
|
return strconv.FormatInt(int64(v), 10)
|
||||||
@@ -111,6 +150,19 @@ func (i *Int64) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Int64) MarshalYAML() (any, error) {
|
||||||
|
return i.Load(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Int64) UnmarshalYAML(unmarshal func(any) error) error {
|
||||||
|
var v int64
|
||||||
|
if err := unmarshal(&v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.Store(v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (i *Int64) String() string {
|
func (i *Int64) String() string {
|
||||||
v := i.Load()
|
v := i.Load()
|
||||||
return strconv.FormatInt(int64(v), 10)
|
return strconv.FormatInt(int64(v), 10)
|
||||||
@@ -138,6 +190,19 @@ func (i *Uint32) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Uint32) MarshalYAML() (any, error) {
|
||||||
|
return i.Load(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Uint32) UnmarshalYAML(unmarshal func(any) error) error {
|
||||||
|
var v uint32
|
||||||
|
if err := unmarshal(&v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.Store(v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (i *Uint32) String() string {
|
func (i *Uint32) String() string {
|
||||||
v := i.Load()
|
v := i.Load()
|
||||||
return strconv.FormatUint(uint64(v), 10)
|
return strconv.FormatUint(uint64(v), 10)
|
||||||
@@ -165,6 +230,19 @@ func (i *Uint64) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Uint64) MarshalYAML() (any, error) {
|
||||||
|
return i.Load(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Uint64) UnmarshalYAML(unmarshal func(any) error) error {
|
||||||
|
var v uint64
|
||||||
|
if err := unmarshal(&v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.Store(v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (i *Uint64) String() string {
|
func (i *Uint64) String() string {
|
||||||
v := i.Load()
|
v := i.Load()
|
||||||
return strconv.FormatUint(uint64(v), 10)
|
return strconv.FormatUint(uint64(v), 10)
|
||||||
@@ -192,6 +270,19 @@ func (i *Uintptr) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Uintptr) MarshalYAML() (any, error) {
|
||||||
|
return i.Load(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Uintptr) UnmarshalYAML(unmarshal func(any) error) error {
|
||||||
|
var v uintptr
|
||||||
|
if err := unmarshal(&v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.Store(v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (i *Uintptr) String() string {
|
func (i *Uintptr) String() string {
|
||||||
v := i.Load()
|
v := i.Load()
|
||||||
return strconv.FormatUint(uint64(v), 10)
|
return strconv.FormatUint(uint64(v), 10)
|
||||||
|
|||||||
@@ -27,11 +27,16 @@ type tValue[T any] struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *TypedValue[T]) Load() T {
|
func (t *TypedValue[T]) Load() T {
|
||||||
|
value, _ := t.LoadOk()
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypedValue[T]) LoadOk() (_ T, ok bool) {
|
||||||
value := t.value.Load()
|
value := t.value.Load()
|
||||||
if value == nil {
|
if value == nil {
|
||||||
return DefaultValue[T]()
|
return DefaultValue[T](), false
|
||||||
}
|
}
|
||||||
return value.(tValue[T]).value
|
return value.(tValue[T]).value, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TypedValue[T]) Store(value T) {
|
func (t *TypedValue[T]) Store(value T) {
|
||||||
@@ -47,7 +52,11 @@ func (t *TypedValue[T]) Swap(new T) T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *TypedValue[T]) CompareAndSwap(old, new T) bool {
|
func (t *TypedValue[T]) CompareAndSwap(old, new T) bool {
|
||||||
return t.value.CompareAndSwap(tValue[T]{old}, tValue[T]{new})
|
return t.value.CompareAndSwap(tValue[T]{old}, tValue[T]{new}) ||
|
||||||
|
// In the edge-case where [atomic.Value.Store] is uninitialized
|
||||||
|
// and trying to compare with the zero value of T,
|
||||||
|
// then compare-and-swap with the nil any value.
|
||||||
|
(any(old) == any(DefaultValue[T]()) && t.value.CompareAndSwap(any(nil), tValue[T]{new}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TypedValue[T]) MarshalJSON() ([]byte, error) {
|
func (t *TypedValue[T]) MarshalJSON() ([]byte, error) {
|
||||||
@@ -63,6 +72,19 @@ func (t *TypedValue[T]) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TypedValue[T]) MarshalYAML() (any, error) {
|
||||||
|
return t.Load(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypedValue[T]) UnmarshalYAML(unmarshal func(any) error) error {
|
||||||
|
var v T
|
||||||
|
if err := unmarshal(&v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.Store(v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewTypedValue[T any](t T) (v TypedValue[T]) {
|
func NewTypedValue[T any](t T) (v TypedValue[T]) {
|
||||||
v.Store(t)
|
v.Store(t)
|
||||||
return
|
return
|
||||||
|
|||||||
77
common/atomic/value_test.go
Normal file
77
common/atomic/value_test.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package atomic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTypedValue(t *testing.T) {
|
||||||
|
{
|
||||||
|
// Always wrapping should not allocate for simple values
|
||||||
|
// because tValue[T] has the same memory layout as T.
|
||||||
|
var v TypedValue[bool]
|
||||||
|
bools := []bool{true, false}
|
||||||
|
if n := int(testing.AllocsPerRun(1000, func() {
|
||||||
|
for _, b := range bools {
|
||||||
|
v.Store(b)
|
||||||
|
}
|
||||||
|
})); n != 0 {
|
||||||
|
t.Errorf("AllocsPerRun = %d, want 0", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var v TypedValue[int]
|
||||||
|
got, gotOk := v.LoadOk()
|
||||||
|
if got != 0 || gotOk {
|
||||||
|
t.Fatalf("LoadOk = (%v, %v), want (0, false)", got, gotOk)
|
||||||
|
}
|
||||||
|
v.Store(1)
|
||||||
|
got, gotOk = v.LoadOk()
|
||||||
|
if got != 1 || !gotOk {
|
||||||
|
t.Fatalf("LoadOk = (%v, %v), want (1, true)", got, gotOk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var v TypedValue[error]
|
||||||
|
got, gotOk := v.LoadOk()
|
||||||
|
if got != nil || gotOk {
|
||||||
|
t.Fatalf("LoadOk = (%v, %v), want (nil, false)", got, gotOk)
|
||||||
|
}
|
||||||
|
v.Store(io.EOF)
|
||||||
|
got, gotOk = v.LoadOk()
|
||||||
|
if got != io.EOF || !gotOk {
|
||||||
|
t.Fatalf("LoadOk = (%v, %v), want (EOF, true)", got, gotOk)
|
||||||
|
}
|
||||||
|
err := &os.PathError{}
|
||||||
|
v.Store(err)
|
||||||
|
got, gotOk = v.LoadOk()
|
||||||
|
if got != err || !gotOk {
|
||||||
|
t.Fatalf("LoadOk = (%v, %v), want (%v, true)", got, gotOk, err)
|
||||||
|
}
|
||||||
|
v.Store(nil)
|
||||||
|
got, gotOk = v.LoadOk()
|
||||||
|
if got != nil || !gotOk {
|
||||||
|
t.Fatalf("LoadOk = (%v, %v), want (nil, true)", got, gotOk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
c1, c2, c3 := make(chan struct{}), make(chan struct{}), make(chan struct{})
|
||||||
|
var v TypedValue[chan struct{}]
|
||||||
|
if v.CompareAndSwap(c1, c2) != false {
|
||||||
|
t.Fatalf("CompareAndSwap = true, want false")
|
||||||
|
}
|
||||||
|
if v.CompareAndSwap(nil, c1) != true {
|
||||||
|
t.Fatalf("CompareAndSwap = false, want true")
|
||||||
|
}
|
||||||
|
if v.CompareAndSwap(c2, c3) != false {
|
||||||
|
t.Fatalf("CompareAndSwap = true, want false")
|
||||||
|
}
|
||||||
|
if v.CompareAndSwap(c1, c2) != true {
|
||||||
|
t.Fatalf("CompareAndSwap = false, want true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package buf
|
package buf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/metacubex/sing/common"
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/metacubex/sing/common/buf"
|
||||||
)
|
)
|
||||||
|
|
||||||
const BufferSize = buf.BufferSize
|
const BufferSize = buf.BufferSize
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package convert
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -43,3 +44,22 @@ func decodeUrlSafe(data string) string {
|
|||||||
}
|
}
|
||||||
return string(dcBuf)
|
return string(dcBuf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TryDecodeBase64(s string) (decoded []byte, err error) {
|
||||||
|
if len(s)%4 == 0 {
|
||||||
|
if decoded, err = base64.StdEncoding.DecodeString(s); err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if decoded, err = base64.URLEncoding.DecodeString(s); err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if decoded, err = base64.RawStdEncoding.DecodeString(s); err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if decoded, err = base64.RawURLEncoding.DecodeString(s); err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid base64-encoded string")
|
||||||
|
}
|
||||||
|
|||||||
@@ -456,12 +456,12 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
|
|||||||
proxies = append(proxies, ss)
|
proxies = append(proxies, ss)
|
||||||
|
|
||||||
case "ssr":
|
case "ssr":
|
||||||
dcBuf, err := encRaw.DecodeString(body)
|
dcBuf, err := TryDecodeBase64(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// ssr://host:port:protocol:method:obfs:urlsafebase64pass/?obfsparam=urlsafebase64&protoparam=&remarks=urlsafebase64&group=urlsafebase64&udpport=0&uot=1
|
// ssr://host:port:protocol:method:obfs:urlsafebase64pass/?obfsparam=urlsafebase64param&protoparam=urlsafebase64param&remarks=urlsafebase64remarks&group=urlsafebase64group&udpport=0&uot=1
|
||||||
|
|
||||||
before, after, ok := strings.Cut(string(dcBuf), "/?")
|
before, after, ok := strings.Cut(string(dcBuf), "/?")
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -490,7 +490,7 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
|
|||||||
name := uniqueName(names, remarks)
|
name := uniqueName(names, remarks)
|
||||||
|
|
||||||
obfsParam := decodeUrlSafe(query.Get("obfsparam"))
|
obfsParam := decodeUrlSafe(query.Get("obfsparam"))
|
||||||
protocolParam := query.Get("protoparam")
|
protocolParam := decodeUrlSafe(query.Get("protoparam"))
|
||||||
|
|
||||||
ssr := make(map[string]any, 20)
|
ssr := make(map[string]any, 20)
|
||||||
|
|
||||||
@@ -513,6 +513,101 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
proxies = append(proxies, ssr)
|
proxies = append(proxies, ssr)
|
||||||
|
|
||||||
|
case "socks", "socks5", "socks5h", "http", "https":
|
||||||
|
link, err := url.Parse(line)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
server := link.Hostname()
|
||||||
|
if server == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
portStr := link.Port()
|
||||||
|
if portStr == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
remarks := link.Fragment
|
||||||
|
if remarks == "" {
|
||||||
|
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||||
|
}
|
||||||
|
name := uniqueName(names, remarks)
|
||||||
|
encodeStr := link.User.String()
|
||||||
|
var username, password string
|
||||||
|
if encodeStr != "" {
|
||||||
|
decodeStr := string(DecodeBase64([]byte(encodeStr)))
|
||||||
|
splitStr := strings.Split(decodeStr, ":")
|
||||||
|
|
||||||
|
// todo: should use url.QueryUnescape ?
|
||||||
|
username = splitStr[0]
|
||||||
|
if len(splitStr) == 2 {
|
||||||
|
password = splitStr[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
socks := make(map[string]any, 10)
|
||||||
|
socks["name"] = name
|
||||||
|
socks["type"] = func() string {
|
||||||
|
switch scheme {
|
||||||
|
case "socks", "socks5", "socks5h":
|
||||||
|
return "socks5"
|
||||||
|
case "http", "https":
|
||||||
|
return "http"
|
||||||
|
}
|
||||||
|
return scheme
|
||||||
|
}()
|
||||||
|
socks["server"] = server
|
||||||
|
socks["port"] = portStr
|
||||||
|
socks["username"] = username
|
||||||
|
socks["password"] = password
|
||||||
|
socks["skip-cert-verify"] = true
|
||||||
|
if scheme == "https" {
|
||||||
|
socks["tls"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
proxies = append(proxies, socks)
|
||||||
|
|
||||||
|
case "anytls":
|
||||||
|
// https://github.com/anytls/anytls-go/blob/main/docs/uri_scheme.md
|
||||||
|
link, err := url.Parse(line)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
username := link.User.Username()
|
||||||
|
password, exist := link.User.Password()
|
||||||
|
if !exist {
|
||||||
|
password = username
|
||||||
|
}
|
||||||
|
query := link.Query()
|
||||||
|
server := link.Hostname()
|
||||||
|
if server == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
portStr := link.Port()
|
||||||
|
if portStr == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
insecure, sni := query.Get("insecure"), query.Get("sni")
|
||||||
|
insecureBool := insecure == "1"
|
||||||
|
fingerprint := query.Get("hpkp")
|
||||||
|
|
||||||
|
remarks := link.Fragment
|
||||||
|
if remarks == "" {
|
||||||
|
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||||
|
}
|
||||||
|
name := uniqueName(names, remarks)
|
||||||
|
anytls := make(map[string]any, 10)
|
||||||
|
anytls["name"] = name
|
||||||
|
anytls["type"] = "anytls"
|
||||||
|
anytls["server"] = server
|
||||||
|
anytls["port"] = portStr
|
||||||
|
anytls["username"] = username
|
||||||
|
anytls["password"] = password
|
||||||
|
anytls["sni"] = sni
|
||||||
|
anytls["fingerprint"] = fingerprint
|
||||||
|
anytls["skip-cert-verify"] = insecureBool
|
||||||
|
anytls["udp"] = true
|
||||||
|
|
||||||
|
proxies = append(proxies, anytls)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
|
|
||||||
"github.com/metacubex/mihomo/common/atomic"
|
"github.com/metacubex/mihomo/common/atomic"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/metacubex/sing/common/buf"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/metacubex/sing/common/bufio"
|
||||||
"github.com/sagernet/sing/common/network"
|
"github.com/metacubex/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
type connReadResult struct {
|
type connReadResult struct {
|
||||||
@@ -20,7 +20,7 @@ type connReadResult struct {
|
|||||||
type Conn struct {
|
type Conn struct {
|
||||||
network.ExtendedConn
|
network.ExtendedConn
|
||||||
deadline atomic.TypedValue[time.Time]
|
deadline atomic.TypedValue[time.Time]
|
||||||
pipeDeadline pipeDeadline
|
pipeDeadline PipeDeadline
|
||||||
disablePipe atomic.Bool
|
disablePipe atomic.Bool
|
||||||
inRead atomic.Bool
|
inRead atomic.Bool
|
||||||
resultCh chan *connReadResult
|
resultCh chan *connReadResult
|
||||||
@@ -34,7 +34,7 @@ func IsConn(conn any) bool {
|
|||||||
func NewConn(conn net.Conn) *Conn {
|
func NewConn(conn net.Conn) *Conn {
|
||||||
c := &Conn{
|
c := &Conn{
|
||||||
ExtendedConn: bufio.NewExtendedConn(conn),
|
ExtendedConn: bufio.NewExtendedConn(conn),
|
||||||
pipeDeadline: makePipeDeadline(),
|
pipeDeadline: MakePipeDeadline(),
|
||||||
resultCh: make(chan *connReadResult, 1),
|
resultCh: make(chan *connReadResult, 1),
|
||||||
}
|
}
|
||||||
c.resultCh <- nil
|
c.resultCh <- nil
|
||||||
@@ -58,7 +58,7 @@ func (c *Conn) Read(p []byte) (n int, err error) {
|
|||||||
c.resultCh <- nil
|
c.resultCh <- nil
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case <-c.pipeDeadline.wait():
|
case <-c.pipeDeadline.Wait():
|
||||||
return 0, os.ErrDeadlineExceeded
|
return 0, os.ErrDeadlineExceeded
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ func (c *Conn) ReadBuffer(buffer *buf.Buffer) (err error) {
|
|||||||
c.resultCh <- nil
|
c.resultCh <- nil
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case <-c.pipeDeadline.wait():
|
case <-c.pipeDeadline.Wait():
|
||||||
return os.ErrDeadlineExceeded
|
return os.ErrDeadlineExceeded
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ func (c *Conn) SetReadDeadline(t time.Time) error {
|
|||||||
return c.ExtendedConn.SetReadDeadline(t)
|
return c.ExtendedConn.SetReadDeadline(t)
|
||||||
}
|
}
|
||||||
c.deadline.Store(t)
|
c.deadline.Store(t)
|
||||||
c.pipeDeadline.set(t)
|
c.pipeDeadline.Set(t)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type readResult struct {
|
|||||||
type NetPacketConn struct {
|
type NetPacketConn struct {
|
||||||
net.PacketConn
|
net.PacketConn
|
||||||
deadline atomic.TypedValue[time.Time]
|
deadline atomic.TypedValue[time.Time]
|
||||||
pipeDeadline pipeDeadline
|
pipeDeadline PipeDeadline
|
||||||
disablePipe atomic.Bool
|
disablePipe atomic.Bool
|
||||||
inRead atomic.Bool
|
inRead atomic.Bool
|
||||||
resultCh chan any
|
resultCh chan any
|
||||||
@@ -28,7 +28,7 @@ type NetPacketConn struct {
|
|||||||
func NewNetPacketConn(pc net.PacketConn) net.PacketConn {
|
func NewNetPacketConn(pc net.PacketConn) net.PacketConn {
|
||||||
npc := &NetPacketConn{
|
npc := &NetPacketConn{
|
||||||
PacketConn: pc,
|
PacketConn: pc,
|
||||||
pipeDeadline: makePipeDeadline(),
|
pipeDeadline: MakePipeDeadline(),
|
||||||
resultCh: make(chan any, 1),
|
resultCh: make(chan any, 1),
|
||||||
}
|
}
|
||||||
npc.resultCh <- nil
|
npc.resultCh <- nil
|
||||||
@@ -83,7 +83,7 @@ FOR:
|
|||||||
c.resultCh <- nil
|
c.resultCh <- nil
|
||||||
break FOR
|
break FOR
|
||||||
}
|
}
|
||||||
case <-c.pipeDeadline.wait():
|
case <-c.pipeDeadline.Wait():
|
||||||
return 0, nil, os.ErrDeadlineExceeded
|
return 0, nil, os.ErrDeadlineExceeded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ func (c *NetPacketConn) SetReadDeadline(t time.Time) error {
|
|||||||
return c.PacketConn.SetReadDeadline(t)
|
return c.PacketConn.SetReadDeadline(t)
|
||||||
}
|
}
|
||||||
c.deadline.Store(t)
|
c.deadline.Store(t)
|
||||||
c.pipeDeadline.set(t)
|
c.pipeDeadline.Set(t)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ FOR:
|
|||||||
c.netPacketConn.resultCh <- nil
|
c.netPacketConn.resultCh <- nil
|
||||||
break FOR
|
break FOR
|
||||||
}
|
}
|
||||||
case <-c.netPacketConn.pipeDeadline.wait():
|
case <-c.netPacketConn.pipeDeadline.Wait():
|
||||||
return nil, nil, nil, os.ErrDeadlineExceeded
|
return nil, nil, nil, os.ErrDeadlineExceeded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import (
|
|||||||
|
|
||||||
"github.com/metacubex/mihomo/common/net/packet"
|
"github.com/metacubex/mihomo/common/net/packet"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/metacubex/sing/common/buf"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/metacubex/sing/common/bufio"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/metacubex/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/metacubex/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SingPacketConn struct {
|
type SingPacketConn struct {
|
||||||
@@ -69,7 +69,7 @@ FOR:
|
|||||||
c.netPacketConn.resultCh <- nil
|
c.netPacketConn.resultCh <- nil
|
||||||
break FOR
|
break FOR
|
||||||
}
|
}
|
||||||
case <-c.netPacketConn.pipeDeadline.wait():
|
case <-c.netPacketConn.pipeDeadline.Wait():
|
||||||
return M.Socksaddr{}, os.ErrDeadlineExceeded
|
return M.Socksaddr{}, os.ErrDeadlineExceeded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ FOR:
|
|||||||
c.netPacketConn.resultCh <- nil
|
c.netPacketConn.resultCh <- nil
|
||||||
break FOR
|
break FOR
|
||||||
}
|
}
|
||||||
case <-c.netPacketConn.pipeDeadline.wait():
|
case <-c.netPacketConn.pipeDeadline.Wait():
|
||||||
return nil, M.Socksaddr{}, os.ErrDeadlineExceeded
|
return nil, M.Socksaddr{}, os.ErrDeadlineExceeded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,24 +9,24 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// pipeDeadline is an abstraction for handling timeouts.
|
// PipeDeadline is an abstraction for handling timeouts.
|
||||||
type pipeDeadline struct {
|
type PipeDeadline struct {
|
||||||
mu sync.Mutex // Guards timer and cancel
|
mu sync.Mutex // Guards timer and cancel
|
||||||
timer *time.Timer
|
timer *time.Timer
|
||||||
cancel chan struct{} // Must be non-nil
|
cancel chan struct{} // Must be non-nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func makePipeDeadline() pipeDeadline {
|
func MakePipeDeadline() PipeDeadline {
|
||||||
return pipeDeadline{cancel: make(chan struct{})}
|
return PipeDeadline{cancel: make(chan struct{})}
|
||||||
}
|
}
|
||||||
|
|
||||||
// set sets the point in time when the deadline will time out.
|
// Set sets the point in time when the deadline will time out.
|
||||||
// A timeout event is signaled by closing the channel returned by waiter.
|
// A timeout event is signaled by closing the channel returned by waiter.
|
||||||
// Once a timeout has occurred, the deadline can be refreshed by specifying a
|
// Once a timeout has occurred, the deadline can be refreshed by specifying a
|
||||||
// t value in the future.
|
// t value in the future.
|
||||||
//
|
//
|
||||||
// A zero value for t prevents timeout.
|
// A zero value for t prevents timeout.
|
||||||
func (d *pipeDeadline) set(t time.Time) {
|
func (d *PipeDeadline) Set(t time.Time) {
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
defer d.mu.Unlock()
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
@@ -61,8 +61,8 @@ func (d *pipeDeadline) set(t time.Time) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait returns a channel that is closed when the deadline is exceeded.
|
// Wait returns a channel that is closed when the deadline is exceeded.
|
||||||
func (d *pipeDeadline) wait() chan struct{} {
|
func (d *PipeDeadline) Wait() chan struct{} {
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
defer d.mu.Unlock()
|
defer d.mu.Unlock()
|
||||||
return d.cancel
|
return d.cancel
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/metacubex/sing/common/buf"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/metacubex/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
type pipeAddr struct{}
|
type pipeAddr struct{}
|
||||||
@@ -33,8 +33,8 @@ type pipe struct {
|
|||||||
localDone chan struct{}
|
localDone chan struct{}
|
||||||
remoteDone <-chan struct{}
|
remoteDone <-chan struct{}
|
||||||
|
|
||||||
readDeadline pipeDeadline
|
readDeadline PipeDeadline
|
||||||
writeDeadline pipeDeadline
|
writeDeadline PipeDeadline
|
||||||
|
|
||||||
readWaitOptions N.ReadWaitOptions
|
readWaitOptions N.ReadWaitOptions
|
||||||
}
|
}
|
||||||
@@ -56,15 +56,15 @@ func Pipe() (net.Conn, net.Conn) {
|
|||||||
rdRx: cb1, rdTx: cn1,
|
rdRx: cb1, rdTx: cn1,
|
||||||
wrTx: cb2, wrRx: cn2,
|
wrTx: cb2, wrRx: cn2,
|
||||||
localDone: done1, remoteDone: done2,
|
localDone: done1, remoteDone: done2,
|
||||||
readDeadline: makePipeDeadline(),
|
readDeadline: MakePipeDeadline(),
|
||||||
writeDeadline: makePipeDeadline(),
|
writeDeadline: MakePipeDeadline(),
|
||||||
}
|
}
|
||||||
p2 := &pipe{
|
p2 := &pipe{
|
||||||
rdRx: cb2, rdTx: cn2,
|
rdRx: cb2, rdTx: cn2,
|
||||||
wrTx: cb1, wrRx: cn1,
|
wrTx: cb1, wrRx: cn1,
|
||||||
localDone: done2, remoteDone: done1,
|
localDone: done2, remoteDone: done1,
|
||||||
readDeadline: makePipeDeadline(),
|
readDeadline: MakePipeDeadline(),
|
||||||
writeDeadline: makePipeDeadline(),
|
writeDeadline: MakePipeDeadline(),
|
||||||
}
|
}
|
||||||
return p1, p2
|
return p1, p2
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ func (p *pipe) read(b []byte) (n int, err error) {
|
|||||||
return 0, io.ErrClosedPipe
|
return 0, io.ErrClosedPipe
|
||||||
case isClosedChan(p.remoteDone):
|
case isClosedChan(p.remoteDone):
|
||||||
return 0, io.EOF
|
return 0, io.EOF
|
||||||
case isClosedChan(p.readDeadline.wait()):
|
case isClosedChan(p.readDeadline.Wait()):
|
||||||
return 0, os.ErrDeadlineExceeded
|
return 0, os.ErrDeadlineExceeded
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ func (p *pipe) read(b []byte) (n int, err error) {
|
|||||||
return 0, io.ErrClosedPipe
|
return 0, io.ErrClosedPipe
|
||||||
case <-p.remoteDone:
|
case <-p.remoteDone:
|
||||||
return 0, io.EOF
|
return 0, io.EOF
|
||||||
case <-p.readDeadline.wait():
|
case <-p.readDeadline.Wait():
|
||||||
return 0, os.ErrDeadlineExceeded
|
return 0, os.ErrDeadlineExceeded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,7 +118,7 @@ func (p *pipe) write(b []byte) (n int, err error) {
|
|||||||
return 0, io.ErrClosedPipe
|
return 0, io.ErrClosedPipe
|
||||||
case isClosedChan(p.remoteDone):
|
case isClosedChan(p.remoteDone):
|
||||||
return 0, io.ErrClosedPipe
|
return 0, io.ErrClosedPipe
|
||||||
case isClosedChan(p.writeDeadline.wait()):
|
case isClosedChan(p.writeDeadline.Wait()):
|
||||||
return 0, os.ErrDeadlineExceeded
|
return 0, os.ErrDeadlineExceeded
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ func (p *pipe) write(b []byte) (n int, err error) {
|
|||||||
return n, io.ErrClosedPipe
|
return n, io.ErrClosedPipe
|
||||||
case <-p.remoteDone:
|
case <-p.remoteDone:
|
||||||
return n, io.ErrClosedPipe
|
return n, io.ErrClosedPipe
|
||||||
case <-p.writeDeadline.wait():
|
case <-p.writeDeadline.Wait():
|
||||||
return n, os.ErrDeadlineExceeded
|
return n, os.ErrDeadlineExceeded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,8 +145,8 @@ func (p *pipe) SetDeadline(t time.Time) error {
|
|||||||
if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) {
|
if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) {
|
||||||
return io.ErrClosedPipe
|
return io.ErrClosedPipe
|
||||||
}
|
}
|
||||||
p.readDeadline.set(t)
|
p.readDeadline.Set(t)
|
||||||
p.writeDeadline.set(t)
|
p.writeDeadline.Set(t)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ func (p *pipe) SetReadDeadline(t time.Time) error {
|
|||||||
if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) {
|
if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) {
|
||||||
return io.ErrClosedPipe
|
return io.ErrClosedPipe
|
||||||
}
|
}
|
||||||
p.readDeadline.set(t)
|
p.readDeadline.Set(t)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ func (p *pipe) SetWriteDeadline(t time.Time) error {
|
|||||||
if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) {
|
if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) {
|
||||||
return io.ErrClosedPipe
|
return io.ErrClosedPipe
|
||||||
}
|
}
|
||||||
p.writeDeadline.set(t)
|
p.writeDeadline.Set(t)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ func (p *pipe) waitReadBuffer() (buffer *buf.Buffer, err error) {
|
|||||||
return nil, io.ErrClosedPipe
|
return nil, io.ErrClosedPipe
|
||||||
case isClosedChan(p.remoteDone):
|
case isClosedChan(p.remoteDone):
|
||||||
return nil, io.EOF
|
return nil, io.EOF
|
||||||
case isClosedChan(p.readDeadline.wait()):
|
case isClosedChan(p.readDeadline.Wait()):
|
||||||
return nil, os.ErrDeadlineExceeded
|
return nil, os.ErrDeadlineExceeded
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
@@ -211,7 +211,7 @@ func (p *pipe) waitReadBuffer() (buffer *buf.Buffer, err error) {
|
|||||||
return nil, io.ErrClosedPipe
|
return nil, io.ErrClosedPipe
|
||||||
case <-p.remoteDone:
|
case <-p.remoteDone:
|
||||||
return nil, io.EOF
|
return nil, io.EOF
|
||||||
case <-p.readDeadline.wait():
|
case <-p.readDeadline.Wait():
|
||||||
return nil, os.ErrDeadlineExceeded
|
return nil, os.ErrDeadlineExceeded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
90
common/net/listener.go
Normal file
90
common/net/listener.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type handleContextListener struct {
|
||||||
|
net.Listener
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
conns chan net.Conn
|
||||||
|
err error
|
||||||
|
once sync.Once
|
||||||
|
handle func(context.Context, net.Conn) (net.Conn, error)
|
||||||
|
panicLog func(any)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *handleContextListener) init() {
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
c, err := l.Listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
l.err = err
|
||||||
|
close(l.conns)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
if l.panicLog != nil {
|
||||||
|
l.panicLog(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if conn, err := l.handle(l.ctx, c); err == nil {
|
||||||
|
l.conns <- conn
|
||||||
|
} else {
|
||||||
|
// handle failed, close the underlying connection.
|
||||||
|
_ = c.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *handleContextListener) Accept() (net.Conn, error) {
|
||||||
|
l.once.Do(l.init)
|
||||||
|
if c, ok := <-l.conns; ok {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
return nil, l.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *handleContextListener) Close() error {
|
||||||
|
l.cancel()
|
||||||
|
l.once.Do(func() { // l.init has not been called yet, so close related resources directly.
|
||||||
|
l.err = net.ErrClosed
|
||||||
|
close(l.conns)
|
||||||
|
})
|
||||||
|
defer func() {
|
||||||
|
// at here, listener has been closed, so we should close all connections in the channel
|
||||||
|
for c := range l.conns {
|
||||||
|
go func(c net.Conn) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
if l.panicLog != nil {
|
||||||
|
l.panicLog(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_ = c.Close()
|
||||||
|
}(c)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return l.Listener.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandleContextListener(ctx context.Context, l net.Listener, handle func(context.Context, net.Conn) (net.Conn, error), panicLog func(any)) net.Listener {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
return &handleContextListener{
|
||||||
|
Listener: l,
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
conns: make(chan net.Conn),
|
||||||
|
handle: handle,
|
||||||
|
panicLog: panicLog,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,10 @@ package packet
|
|||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/metacubex/sing/common/buf"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/metacubex/sing/common/bufio"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/metacubex/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/metacubex/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SingPacketConn = N.NetPacketConn
|
type SingPacketConn = N.NetPacketConn
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package packet
|
|||||||
import (
|
import (
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/metacubex/sing/common/buf"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/metacubex/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/metacubex/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
type refSingPacketConn struct {
|
type refSingPacketConn struct {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package packet
|
package packet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/metacubex/sing/common/buf"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/metacubex/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/metacubex/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
type threadSafeSingPacketConn struct {
|
type threadSafeSingPacketConn struct {
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
|
|
||||||
"github.com/metacubex/mihomo/common/net/deadline"
|
"github.com/metacubex/mihomo/common/net/deadline"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/metacubex/sing/common"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/metacubex/sing/common/bufio"
|
||||||
"github.com/sagernet/sing/common/network"
|
"github.com/metacubex/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
var NewExtendedConn = bufio.NewExtendedConn
|
var NewExtendedConn = bufio.NewExtendedConn
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
package net
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Path interface {
|
|
||||||
Resolve(path string) string
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseCert(certificate, privateKey string, path Path) (tls.Certificate, error) {
|
|
||||||
if certificate == "" && privateKey == "" {
|
|
||||||
var err error
|
|
||||||
certificate, privateKey, _, err = NewRandomTLSKeyPair()
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey))
|
|
||||||
if painTextErr == nil {
|
|
||||||
return cert, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
certificate = path.Resolve(certificate)
|
|
||||||
privateKey = path.Resolve(privateKey)
|
|
||||||
cert, loadErr := tls.LoadX509KeyPair(certificate, privateKey)
|
|
||||||
if loadErr != nil {
|
|
||||||
return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())
|
|
||||||
}
|
|
||||||
return cert, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRandomTLSKeyPair() (certificate string, privateKey string, fingerprint string, err error) {
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
template := x509.Certificate{SerialNumber: big.NewInt(1)}
|
|
||||||
certDER, err := x509.CreateCertificate(
|
|
||||||
rand.Reader,
|
|
||||||
&template,
|
|
||||||
&template,
|
|
||||||
&key.PublicKey,
|
|
||||||
key)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cert, err := x509.ParseCertificate(certDER)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hash := sha256.Sum256(cert.Raw)
|
|
||||||
fingerprint = hex.EncodeToString(hash[:])
|
|
||||||
privateKey = string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}))
|
|
||||||
certificate = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
102
common/once/oncefunc.go
Normal file
102
common/once/oncefunc.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
// Copyright 2022 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package once
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
// OnceFunc returns a function that invokes f only once. The returned function
|
||||||
|
// may be called concurrently.
|
||||||
|
//
|
||||||
|
// If f panics, the returned function will panic with the same value on every call.
|
||||||
|
func OnceFunc(f func()) func() {
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
valid bool
|
||||||
|
p any
|
||||||
|
)
|
||||||
|
// Construct the inner closure just once to reduce costs on the fast path.
|
||||||
|
g := func() {
|
||||||
|
defer func() {
|
||||||
|
p = recover()
|
||||||
|
if !valid {
|
||||||
|
// Re-panic immediately so on the first call the user gets a
|
||||||
|
// complete stack trace into f.
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
f()
|
||||||
|
f = nil // Do not keep f alive after invoking it.
|
||||||
|
valid = true // Set only if f does not panic.
|
||||||
|
}
|
||||||
|
return func() {
|
||||||
|
once.Do(g)
|
||||||
|
if !valid {
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnceValue returns a function that invokes f only once and returns the value
|
||||||
|
// returned by f. The returned function may be called concurrently.
|
||||||
|
//
|
||||||
|
// If f panics, the returned function will panic with the same value on every call.
|
||||||
|
func OnceValue[T any](f func() T) func() T {
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
valid bool
|
||||||
|
p any
|
||||||
|
result T
|
||||||
|
)
|
||||||
|
g := func() {
|
||||||
|
defer func() {
|
||||||
|
p = recover()
|
||||||
|
if !valid {
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
result = f()
|
||||||
|
f = nil
|
||||||
|
valid = true
|
||||||
|
}
|
||||||
|
return func() T {
|
||||||
|
once.Do(g)
|
||||||
|
if !valid {
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnceValues returns a function that invokes f only once and returns the values
|
||||||
|
// returned by f. The returned function may be called concurrently.
|
||||||
|
//
|
||||||
|
// If f panics, the returned function will panic with the same value on every call.
|
||||||
|
func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
valid bool
|
||||||
|
p any
|
||||||
|
r1 T1
|
||||||
|
r2 T2
|
||||||
|
)
|
||||||
|
g := func() {
|
||||||
|
defer func() {
|
||||||
|
p = recover()
|
||||||
|
if !valid {
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
r1, r2 = f()
|
||||||
|
f = nil
|
||||||
|
valid = true
|
||||||
|
}
|
||||||
|
return func() (T1, T2) {
|
||||||
|
once.Do(g)
|
||||||
|
if !valid {
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
return r1, r2
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,18 +8,23 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultAllocator = NewAllocator()
|
var DefaultAllocator = NewAllocator()
|
||||||
|
|
||||||
// Allocator for incoming frames, optimized to prevent overwriting after zeroing
|
type Allocator interface {
|
||||||
type Allocator struct {
|
Get(size int) []byte
|
||||||
|
Put(buf []byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultAllocator for incoming frames, optimized to prevent overwriting after zeroing
|
||||||
|
type defaultAllocator struct {
|
||||||
buffers [11]sync.Pool
|
buffers [11]sync.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAllocator initiates a []byte allocator for frames less than 65536 bytes,
|
// NewAllocator initiates a []byte allocator for frames less than 65536 bytes,
|
||||||
// the waste(memory fragmentation) of space allocation is guaranteed to be
|
// the waste(memory fragmentation) of space allocation is guaranteed to be
|
||||||
// no more than 50%.
|
// no more than 50%.
|
||||||
func NewAllocator() *Allocator {
|
func NewAllocator() Allocator {
|
||||||
return &Allocator{
|
return &defaultAllocator{
|
||||||
buffers: [...]sync.Pool{ // 64B -> 64K
|
buffers: [...]sync.Pool{ // 64B -> 64K
|
||||||
{New: func() any { return new([1 << 6]byte) }},
|
{New: func() any { return new([1 << 6]byte) }},
|
||||||
{New: func() any { return new([1 << 7]byte) }},
|
{New: func() any { return new([1 << 7]byte) }},
|
||||||
@@ -37,7 +42,7 @@ func NewAllocator() *Allocator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get a []byte from pool with most appropriate cap
|
// Get a []byte from pool with most appropriate cap
|
||||||
func (alloc *Allocator) Get(size int) []byte {
|
func (alloc *defaultAllocator) Get(size int) []byte {
|
||||||
switch {
|
switch {
|
||||||
case size < 0:
|
case size < 0:
|
||||||
panic("alloc.Get: len out of range")
|
panic("alloc.Get: len out of range")
|
||||||
@@ -87,7 +92,7 @@ func (alloc *Allocator) Get(size int) []byte {
|
|||||||
|
|
||||||
// Put returns a []byte to pool for future use,
|
// Put returns a []byte to pool for future use,
|
||||||
// which the cap must be exactly 2^n
|
// which the cap must be exactly 2^n
|
||||||
func (alloc *Allocator) Put(buf []byte) error {
|
func (alloc *defaultAllocator) Put(buf []byte) error {
|
||||||
if cap(buf) == 0 || cap(buf) > 65536 {
|
if cap(buf) == 0 || cap(buf) > 65536 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,12 @@
|
|||||||
package pool
|
package pool
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// RelayBufferSize using for tcp
|
||||||
// io.Copy default buffer size is 32 KiB
|
// io.Copy default buffer size is 32 KiB
|
||||||
// but the maximum packet size of vmess/shadowsocks is about 16 KiB
|
|
||||||
// so define a buffer of 20 KiB to reduce the memory of each TCP relay
|
|
||||||
RelayBufferSize = 16 * 1024
|
RelayBufferSize = 16 * 1024
|
||||||
|
|
||||||
// RelayBufferSize uses 20KiB, but due to the allocator it will actually
|
// UDPBufferSize using for udp
|
||||||
// request 32Kib. Most UDPs are smaller than the MTU, and the TUN's MTU
|
// Most UDPs are smaller than the MTU, and the TUN's MTU
|
||||||
// set to 9000, so the UDP Buffer size set to 16Kib
|
// set to 9000, so the UDP Buffer size set to 16Kib
|
||||||
UDPBufferSize = 8 * 1024
|
UDPBufferSize = 8 * 1024
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,13 +3,12 @@
|
|||||||
package pool
|
package pool
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// RelayBufferSize using for tcp
|
||||||
// io.Copy default buffer size is 32 KiB
|
// io.Copy default buffer size is 32 KiB
|
||||||
// but the maximum packet size of vmess/shadowsocks is about 16 KiB
|
RelayBufferSize = 32 * 1024
|
||||||
// so define a buffer of 20 KiB to reduce the memory of each TCP relay
|
|
||||||
RelayBufferSize = 20 * 1024
|
|
||||||
|
|
||||||
// RelayBufferSize uses 20KiB, but due to the allocator it will actually
|
// UDPBufferSize using for udp
|
||||||
// request 32Kib. Most UDPs are smaller than the MTU, and the TUN's MTU
|
// Most UDPs are smaller than the MTU, and the TUN's MTU
|
||||||
// set to 9000, so the UDP Buffer size set to 16Kib
|
// set to 9000, so the UDP Buffer size set to 16Kib
|
||||||
UDPBufferSize = 16 * 1024
|
UDPBufferSize = 16 * 1024
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package pool
|
package pool
|
||||||
|
|
||||||
func Get(size int) []byte {
|
func Get(size int) []byte {
|
||||||
return defaultAllocator.Get(size)
|
return DefaultAllocator.Get(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Put(buf []byte) error {
|
func Put(buf []byte) error {
|
||||||
return defaultAllocator.Put(buf)
|
return DefaultAllocator.Put(buf)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package pool
|
package pool
|
||||||
|
|
||||||
import "github.com/sagernet/sing/common/buf"
|
import "github.com/metacubex/sing/common/buf"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
buf.DefaultAllocator = defaultAllocator
|
buf.DefaultAllocator = DefaultAllocator
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,13 +239,15 @@ func (n *num) UnmarshalText(text []byte) (err error) {
|
|||||||
|
|
||||||
func TestStructure_TextUnmarshaller(t *testing.T) {
|
func TestStructure_TextUnmarshaller(t *testing.T) {
|
||||||
rawMap := map[string]any{
|
rawMap := map[string]any{
|
||||||
"num": "255",
|
"num": "255",
|
||||||
"num_p": "127",
|
"num_p": "127",
|
||||||
|
"num_arr": []string{"1", "2", "3"},
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &struct {
|
s := &struct {
|
||||||
Num num `test:"num"`
|
Num num `test:"num"`
|
||||||
NumP *num `test:"num_p"`
|
NumP *num `test:"num_p"`
|
||||||
|
NumArr []num `test:"num_arr"`
|
||||||
}{}
|
}{}
|
||||||
|
|
||||||
err := decoder.Decode(rawMap, s)
|
err := decoder.Decode(rawMap, s)
|
||||||
@@ -253,6 +255,7 @@ func TestStructure_TextUnmarshaller(t *testing.T) {
|
|||||||
assert.Equal(t, 255, s.Num.a)
|
assert.Equal(t, 255, s.Num.a)
|
||||||
assert.NotNil(t, s.NumP)
|
assert.NotNil(t, s.NumP)
|
||||||
assert.Equal(t, s.NumP.a, 127)
|
assert.Equal(t, s.NumP.a, 127)
|
||||||
|
assert.Equal(t, s.NumArr, []num{{1}, {2}, {3}})
|
||||||
|
|
||||||
// test WeaklyTypedInput
|
// test WeaklyTypedInput
|
||||||
rawMap["num"] = 256
|
rawMap["num"] = 256
|
||||||
|
|||||||
@@ -41,3 +41,11 @@ func NewAuthenticator(users []AuthUser) Authenticator {
|
|||||||
}
|
}
|
||||||
return au
|
return au
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var AlwaysValid Authenticator = alwaysValid{}
|
||||||
|
|
||||||
|
type alwaysValid struct{}
|
||||||
|
|
||||||
|
func (alwaysValid) Verify(string, string) bool { return true }
|
||||||
|
|
||||||
|
func (alwaysValid) Users() []string { return nil }
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package ca
|
package ca
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
@@ -81,41 +77,15 @@ func getCertPool() *x509.CertPool {
|
|||||||
return globalCertPool
|
return globalCertPool
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyFingerprint(fingerprint *[32]byte) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
|
||||||
return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
|
||||||
// ssl pining
|
|
||||||
for i := range rawCerts {
|
|
||||||
rawCert := rawCerts[i]
|
|
||||||
cert, err := x509.ParseCertificate(rawCert)
|
|
||||||
if err == nil {
|
|
||||||
hash := sha256.Sum256(cert.Raw)
|
|
||||||
if bytes.Equal(fingerprint[:], hash[:]) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errNotMatch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertFingerprint(fingerprint string) (*[32]byte, error) {
|
|
||||||
fingerprint = strings.TrimSpace(strings.Replace(fingerprint, ":", "", -1))
|
|
||||||
fpByte, err := hex.DecodeString(fingerprint)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(fpByte) != 32 {
|
|
||||||
return nil, fmt.Errorf("fingerprint string length error,need sha256 fingerprint")
|
|
||||||
}
|
|
||||||
return (*[32]byte)(fpByte), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetCertPool(customCA string, customCAString string) (*x509.CertPool, error) {
|
func GetCertPool(customCA string, customCAString string) (*x509.CertPool, error) {
|
||||||
var certificate []byte
|
var certificate []byte
|
||||||
var err error
|
var err error
|
||||||
if len(customCA) > 0 {
|
if len(customCA) > 0 {
|
||||||
certificate, err = os.ReadFile(C.Path.Resolve(customCA))
|
path := C.Path.Resolve(customCA)
|
||||||
|
if !C.Path.IsSafePath(path) {
|
||||||
|
return nil, C.Path.ErrNotSafePath(path)
|
||||||
|
}
|
||||||
|
certificate, err = os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("load ca error: %w", err)
|
return nil, fmt.Errorf("load ca error: %w", err)
|
||||||
}
|
}
|
||||||
@@ -133,14 +103,6 @@ func GetCertPool(customCA string, customCAString string) (*x509.CertPool, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFingerprintVerifier(fingerprint string) (func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error, error) {
|
|
||||||
fingerprintBytes, err := convertFingerprint(fingerprint)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return verifyFingerprint(fingerprintBytes), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTLSConfig specified fingerprint, customCA and customCAString
|
// GetTLSConfig specified fingerprint, customCA and customCAString
|
||||||
func GetTLSConfig(tlsConfig *tls.Config, fingerprint string, customCA string, customCAString string) (_ *tls.Config, err error) {
|
func GetTLSConfig(tlsConfig *tls.Config, fingerprint string, customCA string, customCAString string) (_ *tls.Config, err error) {
|
||||||
if tlsConfig == nil {
|
if tlsConfig == nil {
|
||||||
|
|||||||
44
component/ca/fingerprint.go
Normal file
44
component/ca/fingerprint.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package ca
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewFingerprintVerifier returns a function that verifies whether a certificate's SHA-256 fingerprint matches the given one.
|
||||||
|
func NewFingerprintVerifier(fingerprint string) (func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error, error) {
|
||||||
|
switch fingerprint {
|
||||||
|
case "chrome", "firefox", "safari", "ios", "android", "edge", "360", "qq", "random", "randomized": // WTF???
|
||||||
|
return nil, fmt.Errorf("`fingerprint` is used for TLS certificate pinning. If you need to specify the browser fingerprint, use `client-fingerprint`")
|
||||||
|
}
|
||||||
|
fingerprint = strings.TrimSpace(strings.Replace(fingerprint, ":", "", -1))
|
||||||
|
fpByte, err := hex.DecodeString(fingerprint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fingerprint string decode error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fpByte) != 32 {
|
||||||
|
return nil, fmt.Errorf("fingerprint string length error,need sha256 fingerprint")
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
||||||
|
// ssl pining
|
||||||
|
for _, rawCert := range rawCerts {
|
||||||
|
hash := sha256.Sum256(rawCert)
|
||||||
|
if bytes.Equal(fpByte, hash[:]) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errNotMatch
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateFingerprint computes the SHA-256 fingerprint of the given DER-encoded certificate and returns it as a hex string.
|
||||||
|
func CalculateFingerprint(certDER []byte) string {
|
||||||
|
hash := sha256.Sum256(certDER)
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
101
component/ca/keypair.go
Normal file
101
component/ca/keypair.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package ca
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Path interface {
|
||||||
|
Resolve(path string) string
|
||||||
|
IsSafePath(path string) bool
|
||||||
|
ErrNotSafePath(path string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadTLSKeyPair loads a TLS key pair from the provided certificate and private key data or file paths, supporting fallback resolution.
|
||||||
|
// Returns a tls.Certificate and an error, where the error indicates issues during parsing or file loading.
|
||||||
|
// If both certificate and privateKey are empty, generates a random TLS RSA key pair.
|
||||||
|
// Accepts a Path interface for resolving file paths when necessary.
|
||||||
|
func LoadTLSKeyPair(certificate, privateKey string, path Path) (tls.Certificate, error) {
|
||||||
|
if certificate == "" && privateKey == "" {
|
||||||
|
var err error
|
||||||
|
certificate, privateKey, _, err = NewRandomTLSKeyPair(KeyPairTypeRSA)
|
||||||
|
if err != nil {
|
||||||
|
return tls.Certificate{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey))
|
||||||
|
if painTextErr == nil {
|
||||||
|
return cert, nil
|
||||||
|
}
|
||||||
|
if path == nil {
|
||||||
|
return tls.Certificate{}, painTextErr
|
||||||
|
}
|
||||||
|
|
||||||
|
certificate = path.Resolve(certificate)
|
||||||
|
privateKey = path.Resolve(privateKey)
|
||||||
|
var loadErr error
|
||||||
|
if !path.IsSafePath(certificate) {
|
||||||
|
loadErr = path.ErrNotSafePath(certificate)
|
||||||
|
} else if !path.IsSafePath(privateKey) {
|
||||||
|
loadErr = path.ErrNotSafePath(privateKey)
|
||||||
|
} else {
|
||||||
|
cert, loadErr = tls.LoadX509KeyPair(certificate, privateKey)
|
||||||
|
}
|
||||||
|
if loadErr != nil {
|
||||||
|
return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())
|
||||||
|
}
|
||||||
|
return cert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeyPairType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
KeyPairTypeRSA KeyPairType = "rsa"
|
||||||
|
KeyPairTypeP256 KeyPairType = "p256"
|
||||||
|
KeyPairTypeP384 KeyPairType = "p384"
|
||||||
|
KeyPairTypeEd25519 KeyPairType = "ed25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewRandomTLSKeyPair generates a random TLS key pair based on the specified KeyPairType and returns it with a SHA256 fingerprint.
|
||||||
|
// Note: Most browsers do not support KeyPairTypeEd25519 type of certificate, and utls.UConn will also reject this type of certificate.
|
||||||
|
func NewRandomTLSKeyPair(keyPairType KeyPairType) (certificate string, privateKey string, fingerprint string, err error) {
|
||||||
|
var key crypto.Signer
|
||||||
|
switch keyPairType {
|
||||||
|
case KeyPairTypeRSA:
|
||||||
|
key, err = rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
case KeyPairTypeP256:
|
||||||
|
key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
case KeyPairTypeP384:
|
||||||
|
key, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||||
|
case KeyPairTypeEd25519:
|
||||||
|
_, key, err = ed25519.GenerateKey(rand.Reader)
|
||||||
|
default: // fallback to KeyPairTypeRSA
|
||||||
|
key, err = rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
template := x509.Certificate{SerialNumber: big.NewInt(1)}
|
||||||
|
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, key.Public(), key)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
privBytes, err := x509.MarshalPKCS8PrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fingerprint = CalculateFingerprint(certDER)
|
||||||
|
privateKey = string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}))
|
||||||
|
certificate = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}))
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -73,7 +73,7 @@ func ListenPacket(ctx context.Context, network, address string, rAddrPort netip.
|
|||||||
}
|
}
|
||||||
if opt.interfaceName == "" {
|
if opt.interfaceName == "" {
|
||||||
if finder := DefaultInterfaceFinder.Load(); finder != nil {
|
if finder := DefaultInterfaceFinder.Load(); finder != nil {
|
||||||
opt.interfaceName = finder.FindInterfaceName(rAddrPort.Addr())
|
opt.interfaceName = finder.FindInterfaceName(rAddrPort.Addr().Unmap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if rAddrPort.Addr().Unmap().IsLoopback() {
|
if rAddrPort.Addr().Unmap().IsLoopback() {
|
||||||
|
|||||||
31
component/ech/ech.go
Normal file
31
component/ech/ech.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package ech
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
tlsC "github.com/metacubex/mihomo/component/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
GetEncryptedClientHelloConfigList func(ctx context.Context, serverName string) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) ClientHandle(ctx context.Context, tlsConfig *tlsC.Config) (err error) {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
echConfigList, err := cfg.GetEncryptedClientHelloConfigList(ctx, tlsConfig.ServerName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resolve ECH config error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConfig.EncryptedClientHelloConfigList = echConfigList
|
||||||
|
if tlsConfig.MinVersion != 0 && tlsConfig.MinVersion < tlsC.VersionTLS13 {
|
||||||
|
tlsConfig.MinVersion = tlsC.VersionTLS13
|
||||||
|
}
|
||||||
|
if tlsConfig.MaxVersion != 0 && tlsConfig.MaxVersion < tlsC.VersionTLS13 {
|
||||||
|
tlsConfig.MaxVersion = tlsC.VersionTLS13
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
143
component/ech/key.go
Normal file
143
component/ech/key.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package ech
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdh"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/metacubex/mihomo/component/ca"
|
||||||
|
tlsC "github.com/metacubex/mihomo/component/tls"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/cryptobyte"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AEAD_AES_128_GCM = 0x0001
|
||||||
|
AEAD_AES_256_GCM = 0x0002
|
||||||
|
AEAD_ChaCha20Poly1305 = 0x0003
|
||||||
|
)
|
||||||
|
|
||||||
|
const extensionEncryptedClientHello = 0xfe0d
|
||||||
|
const DHKEM_X25519_HKDF_SHA256 = 0x0020
|
||||||
|
const KDF_HKDF_SHA256 = 0x0001
|
||||||
|
|
||||||
|
// sortedSupportedAEADs is just a sorted version of hpke.SupportedAEADS.
|
||||||
|
// We need this so that when we insert them into ECHConfigs the ordering
|
||||||
|
// is stable.
|
||||||
|
var sortedSupportedAEADs = []uint16{AEAD_AES_128_GCM, AEAD_AES_256_GCM, AEAD_ChaCha20Poly1305}
|
||||||
|
|
||||||
|
func marshalECHConfig(id uint8, pubKey []byte, publicName string, maxNameLen uint8) []byte {
|
||||||
|
builder := cryptobyte.NewBuilder(nil)
|
||||||
|
|
||||||
|
builder.AddUint16(extensionEncryptedClientHello)
|
||||||
|
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
|
||||||
|
builder.AddUint8(id)
|
||||||
|
|
||||||
|
builder.AddUint16(DHKEM_X25519_HKDF_SHA256) // The only DHKEM we support
|
||||||
|
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
|
||||||
|
builder.AddBytes(pubKey)
|
||||||
|
})
|
||||||
|
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
|
||||||
|
for _, aeadID := range sortedSupportedAEADs {
|
||||||
|
builder.AddUint16(KDF_HKDF_SHA256) // The only KDF we support
|
||||||
|
builder.AddUint16(aeadID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
builder.AddUint8(maxNameLen)
|
||||||
|
builder.AddUint8LengthPrefixed(func(builder *cryptobyte.Builder) {
|
||||||
|
builder.AddBytes([]byte(publicName))
|
||||||
|
})
|
||||||
|
builder.AddUint16(0) // extensions
|
||||||
|
})
|
||||||
|
|
||||||
|
return builder.BytesOrPanic()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenECHConfig(publicName string) (configBase64 string, keyPem string, err error) {
|
||||||
|
echKey, err := ecdh.X25519().GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
echConfig := marshalECHConfig(0, echKey.PublicKey().Bytes(), publicName, 0)
|
||||||
|
|
||||||
|
builder := cryptobyte.NewBuilder(nil)
|
||||||
|
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
|
||||||
|
builder.AddBytes(echConfig)
|
||||||
|
})
|
||||||
|
echConfigList := builder.BytesOrPanic()
|
||||||
|
|
||||||
|
builder2 := cryptobyte.NewBuilder(nil)
|
||||||
|
builder2.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
|
||||||
|
builder.AddBytes(echKey.Bytes())
|
||||||
|
})
|
||||||
|
builder2.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
|
||||||
|
builder.AddBytes(echConfig)
|
||||||
|
})
|
||||||
|
echConfigKeys := builder2.BytesOrPanic()
|
||||||
|
|
||||||
|
configBase64 = base64.StdEncoding.EncodeToString(echConfigList)
|
||||||
|
keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: echConfigKeys}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnmarshalECHKeys(raw []byte) ([]tlsC.EncryptedClientHelloKey, error) {
|
||||||
|
var keys []tlsC.EncryptedClientHelloKey
|
||||||
|
rawString := cryptobyte.String(raw)
|
||||||
|
for !rawString.Empty() {
|
||||||
|
var key tlsC.EncryptedClientHelloKey
|
||||||
|
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.PrivateKey)) {
|
||||||
|
return nil, errors.New("error parsing private key")
|
||||||
|
}
|
||||||
|
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.Config)) {
|
||||||
|
return nil, errors.New("error parsing config")
|
||||||
|
}
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return nil, errors.New("empty ECH keys")
|
||||||
|
}
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadECHKey(key string, tlsConfig *tlsC.Config, path ca.Path) error {
|
||||||
|
if key == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
painTextErr := loadECHKey([]byte(key), tlsConfig)
|
||||||
|
if painTextErr == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
key = path.Resolve(key)
|
||||||
|
var loadErr error
|
||||||
|
if !path.IsSafePath(key) {
|
||||||
|
loadErr = path.ErrNotSafePath(key)
|
||||||
|
} else {
|
||||||
|
var echKey []byte
|
||||||
|
echKey, loadErr = os.ReadFile(key)
|
||||||
|
if loadErr == nil {
|
||||||
|
loadErr = loadECHKey(echKey, tlsConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if loadErr != nil {
|
||||||
|
return fmt.Errorf("parse ECH keys failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadECHKey(echKey []byte, tlsConfig *tlsC.Config) error {
|
||||||
|
block, rest := pem.Decode(echKey)
|
||||||
|
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
|
||||||
|
return errors.New("invalid ECH keys pem")
|
||||||
|
}
|
||||||
|
echKeys, err := UnmarshalECHKeys(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse ECH keys: %w", err)
|
||||||
|
}
|
||||||
|
tlsConfig.EncryptedClientHelloKeys = echKeys
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -4,12 +4,14 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/metacubex/mihomo/component/ech"
|
||||||
|
|
||||||
"github.com/gofrs/uuid/v5"
|
"github.com/gofrs/uuid/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Main(args []string) {
|
func Main(args []string) {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
panic("Using: generate uuid/reality-keypair/wg-keypair")
|
panic("Using: generate uuid/reality-keypair/wg-keypair/ech-keypair")
|
||||||
}
|
}
|
||||||
switch args[0] {
|
switch args[0] {
|
||||||
case "uuid":
|
case "uuid":
|
||||||
@@ -33,5 +35,15 @@ func Main(args []string) {
|
|||||||
}
|
}
|
||||||
fmt.Println("PrivateKey: " + privateKey.String())
|
fmt.Println("PrivateKey: " + privateKey.String())
|
||||||
fmt.Println("PublicKey: " + privateKey.PublicKey().String())
|
fmt.Println("PublicKey: " + privateKey.PublicKey().String())
|
||||||
|
case "ech-keypair":
|
||||||
|
if len(args) < 2 {
|
||||||
|
panic("Using: generate ech-keypair <plain_server_name>")
|
||||||
|
}
|
||||||
|
configBase64, keyPem, err := ech.GenECHConfig(args[1])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println("Config:", configBase64)
|
||||||
|
fmt.Println("Key:", keyPem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package geodata
|
package geodata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -76,13 +75,13 @@ func LoadGeoSiteMatcher(countryCode string) (router.DomainMatcher, error) {
|
|||||||
if countryCode[0] == '!' {
|
if countryCode[0] == '!' {
|
||||||
not = true
|
not = true
|
||||||
countryCode = countryCode[1:]
|
countryCode = countryCode[1:]
|
||||||
|
if countryCode == "" {
|
||||||
|
return nil, fmt.Errorf("country code could not be empty")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
countryCode = strings.ToLower(countryCode)
|
countryCode = strings.ToLower(countryCode)
|
||||||
|
|
||||||
parts := strings.Split(countryCode, "@")
|
parts := strings.Split(countryCode, "@")
|
||||||
if len(parts) == 0 {
|
|
||||||
return nil, errors.New("empty rule")
|
|
||||||
}
|
|
||||||
listName := strings.TrimSpace(parts[0])
|
listName := strings.TrimSpace(parts[0])
|
||||||
attrVal := parts[1:]
|
attrVal := parts[1:]
|
||||||
attrs := parseAttrs(attrVal)
|
attrs := parseAttrs(attrVal)
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ifaceCache struct {
|
type ifaceCache struct {
|
||||||
ifMap map[string]*Interface
|
ifMapByName map[string]*Interface
|
||||||
ifTable bart.Table[*Interface]
|
ifMapByAddr map[netip.Addr]*Interface
|
||||||
|
ifTable bart.Table[*Interface]
|
||||||
}
|
}
|
||||||
|
|
||||||
var caches = singledo.NewSingle[*ifaceCache](time.Second * 20)
|
var caches = singledo.NewSingle[*ifaceCache](time.Second * 20)
|
||||||
@@ -40,7 +41,8 @@ func getCache() (*ifaceCache, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cache := &ifaceCache{
|
cache := &ifaceCache{
|
||||||
ifMap: make(map[string]*Interface),
|
ifMapByName: make(map[string]*Interface),
|
||||||
|
ifMapByAddr: make(map[netip.Addr]*Interface),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, iface := range ifaces {
|
for _, iface := range ifaces {
|
||||||
@@ -78,9 +80,13 @@ func getCache() (*ifaceCache, error) {
|
|||||||
Flags: iface.Flags,
|
Flags: iface.Flags,
|
||||||
Addresses: ipNets,
|
Addresses: ipNets,
|
||||||
}
|
}
|
||||||
cache.ifMap[iface.Name] = ifaceObj
|
cache.ifMapByName[iface.Name] = ifaceObj
|
||||||
|
|
||||||
|
if iface.Flags&net.FlagUp == 0 {
|
||||||
|
continue // interface down
|
||||||
|
}
|
||||||
for _, prefix := range ipNets {
|
for _, prefix := range ipNets {
|
||||||
|
cache.ifMapByAddr[prefix.Addr()] = ifaceObj
|
||||||
cache.ifTable.Insert(prefix, ifaceObj)
|
cache.ifTable.Insert(prefix, ifaceObj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,7 +101,7 @@ func Interfaces() (map[string]*Interface, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return cache.ifMap, nil
|
return cache.ifMapByName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResolveInterface(name string) (*Interface, error) {
|
func ResolveInterface(name string) (*Interface, error) {
|
||||||
@@ -117,6 +123,11 @@ func ResolveInterfaceByAddr(addr netip.Addr) (*Interface, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// maybe two interfaces have the same prefix but different address
|
||||||
|
// so direct check address equal before do a route lookup (longest prefix match)
|
||||||
|
if iface, ok := cache.ifMapByAddr[addr]; ok {
|
||||||
|
return iface, nil
|
||||||
|
}
|
||||||
iface, ok := cache.ifTable.Lookup(addr)
|
iface, ok := cache.ifTable.Lookup(addr)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, ErrIfaceNotFound
|
return nil, ErrIfaceNotFound
|
||||||
@@ -130,7 +141,8 @@ func IsLocalIp(addr netip.Addr) (bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return cache.ifTable.Contains(addr), nil
|
_, ok := cache.ifMapByAddr[addr]
|
||||||
|
return ok, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FlushCache() {
|
func FlushCache() {
|
||||||
|
|||||||
@@ -1,57 +1,52 @@
|
|||||||
package process
|
package process
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FindProcessAlways = "always"
|
FindProcessStrict FindProcessMode = iota
|
||||||
FindProcessStrict = "strict"
|
FindProcessAlways
|
||||||
FindProcessOff = "off"
|
FindProcessOff
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
validModes = map[string]struct{}{
|
validModes = map[string]FindProcessMode{
|
||||||
FindProcessAlways: {},
|
FindProcessStrict.String(): FindProcessStrict,
|
||||||
FindProcessOff: {},
|
FindProcessAlways.String(): FindProcessAlways,
|
||||||
FindProcessStrict: {},
|
FindProcessOff.String(): FindProcessOff,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type FindProcessMode string
|
type FindProcessMode int32
|
||||||
|
|
||||||
func (m FindProcessMode) Always() bool {
|
// UnmarshalText unserialize FindProcessMode
|
||||||
return m == FindProcessAlways
|
func (m *FindProcessMode) UnmarshalText(data []byte) error {
|
||||||
}
|
return m.Set(string(data))
|
||||||
|
|
||||||
func (m FindProcessMode) Off() bool {
|
|
||||||
return m == FindProcessOff
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *FindProcessMode) UnmarshalYAML(unmarshal func(any) error) error {
|
|
||||||
var tp string
|
|
||||||
if err := unmarshal(&tp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return m.Set(tp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *FindProcessMode) UnmarshalJSON(data []byte) error {
|
|
||||||
var tp string
|
|
||||||
if err := json.Unmarshal(data, &tp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return m.Set(tp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *FindProcessMode) Set(value string) error {
|
func (m *FindProcessMode) Set(value string) error {
|
||||||
mode := strings.ToLower(value)
|
mode, exist := validModes[strings.ToLower(value)]
|
||||||
_, exist := validModes[mode]
|
|
||||||
if !exist {
|
if !exist {
|
||||||
return errors.New("invalid find process mode")
|
return errors.New("invalid find process mode")
|
||||||
}
|
}
|
||||||
*m = FindProcessMode(mode)
|
*m = mode
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalText serialize FindProcessMode
|
||||||
|
func (m FindProcessMode) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(m.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m FindProcessMode) String() string {
|
||||||
|
switch m {
|
||||||
|
case FindProcessAlways:
|
||||||
|
return "always"
|
||||||
|
case FindProcessOff:
|
||||||
|
return "off"
|
||||||
|
default:
|
||||||
|
return "strict"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package proxydialer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -10,7 +9,6 @@ import (
|
|||||||
|
|
||||||
N "github.com/metacubex/mihomo/common/net"
|
N "github.com/metacubex/mihomo/common/net"
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
"github.com/metacubex/mihomo/component/dialer"
|
||||||
"github.com/metacubex/mihomo/component/resolver"
|
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
"github.com/metacubex/mihomo/tunnel"
|
"github.com/metacubex/mihomo/tunnel"
|
||||||
"github.com/metacubex/mihomo/tunnel/statistic"
|
"github.com/metacubex/mihomo/tunnel/statistic"
|
||||||
@@ -40,23 +38,22 @@ func (p proxyDialer) DialContext(ctx context.Context, network, address string) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if strings.Contains(network, "udp") { // using in wireguard outbound
|
if strings.Contains(network, "udp") { // using in wireguard outbound
|
||||||
if !currentMeta.Resolved() {
|
|
||||||
ip, err := resolver.ResolveIP(ctx, currentMeta.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("can't resolve ip")
|
|
||||||
}
|
|
||||||
currentMeta.DstIP = ip
|
|
||||||
}
|
|
||||||
pc, err := p.listenPacket(ctx, currentMeta)
|
pc, err := p.listenPacket(ctx, currentMeta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if !currentMeta.Resolved() { // should not happen, maybe by a wrongly implemented proxy, but we can handle this (:
|
||||||
|
err = pc.ResolveUDP(ctx, currentMeta)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return N.NewBindPacketConn(pc, currentMeta.UDPAddr()), nil
|
return N.NewBindPacketConn(pc, currentMeta.UDPAddr()), nil
|
||||||
}
|
}
|
||||||
var conn C.Conn
|
var conn C.Conn
|
||||||
var err error
|
var err error
|
||||||
if d, ok := p.dialer.(dialer.Dialer); ok { // first using old function to let mux work
|
if _, ok := p.dialer.(dialer.Dialer); ok { // first using old function to let mux work
|
||||||
conn, err = p.proxy.DialContext(ctx, currentMeta, dialer.WithOption(d.Opt))
|
conn, err = p.proxy.DialContext(ctx, currentMeta)
|
||||||
} else {
|
} else {
|
||||||
conn, err = p.proxy.DialContextWithDialer(ctx, p.dialer, currentMeta)
|
conn, err = p.proxy.DialContextWithDialer(ctx, p.dialer, currentMeta)
|
||||||
}
|
}
|
||||||
@@ -78,8 +75,8 @@ func (p proxyDialer) listenPacket(ctx context.Context, currentMeta *C.Metadata)
|
|||||||
var pc C.PacketConn
|
var pc C.PacketConn
|
||||||
var err error
|
var err error
|
||||||
currentMeta.NetWork = C.UDP
|
currentMeta.NetWork = C.UDP
|
||||||
if d, ok := p.dialer.(dialer.Dialer); ok { // first using old function to let mux work
|
if _, ok := p.dialer.(dialer.Dialer); ok { // first using old function to let mux work
|
||||||
pc, err = p.proxy.ListenPacketContext(ctx, currentMeta, dialer.WithOption(d.Opt))
|
pc, err = p.proxy.ListenPacketContext(ctx, currentMeta)
|
||||||
} else {
|
} else {
|
||||||
pc, err = p.proxy.ListenPacketWithDialer(ctx, p.dialer, currentMeta)
|
pc, err = p.proxy.ListenPacketWithDialer(ctx, p.dialer, currentMeta)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
|
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/metacubex/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/metacubex/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SingDialer interface {
|
type SingDialer interface {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/component/slowdown"
|
"github.com/metacubex/mihomo/component/slowdown"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/metacubex/sing/common/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SlowDownSingDialer struct {
|
type SlowDownSingDialer struct {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func NewHostValue(value any) (HostValue, error) {
|
|||||||
isDomain = false
|
isDomain = false
|
||||||
for _, str := range valueArr {
|
for _, str := range valueArr {
|
||||||
if ip, err := netip.ParseAddr(str); err == nil {
|
if ip, err := netip.ParseAddr(str); err == nil {
|
||||||
ips = append(ips, ip)
|
ips = append(ips, ip.Unmap())
|
||||||
} else {
|
} else {
|
||||||
return HostValue{}, err
|
return HostValue{}, err
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ func NewHostValue(value any) (HostValue, error) {
|
|||||||
} else if len(valueArr) == 1 {
|
} else if len(valueArr) == 1 {
|
||||||
host := valueArr[0]
|
host := valueArr[0]
|
||||||
if ip, err := netip.ParseAddr(host); err == nil {
|
if ip, err := netip.ParseAddr(host); err == nil {
|
||||||
ips = append(ips, ip)
|
ips = append(ips, ip.Unmap())
|
||||||
isDomain = false
|
isDomain = false
|
||||||
} else {
|
} else {
|
||||||
domain = host
|
domain = host
|
||||||
|
|||||||
@@ -46,17 +46,24 @@ func RelayDnsConn(ctx context.Context, conn net.Conn, readTimeout time.Duration)
|
|||||||
ctx, cancel := context.WithTimeout(ctx, DefaultDnsRelayTimeout)
|
ctx, cancel := context.WithTimeout(ctx, DefaultDnsRelayTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
inData := buff[:n]
|
inData := buff[:n]
|
||||||
msg, err := relayDnsPacket(ctx, inData, buff, 0)
|
outBuff := buff[2:]
|
||||||
|
msg, err := relayDnsPacket(ctx, inData, outBuff, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = binary.Write(conn, binary.BigEndian, uint16(len(msg)))
|
if &msg[0] == &outBuff[0] { // msg is still in the buff
|
||||||
if err != nil {
|
binary.BigEndian.PutUint16(buff[:2], uint16(len(msg)))
|
||||||
return err
|
outBuff = buff[:2+len(msg)]
|
||||||
|
} else { // buff not big enough (WTF???)
|
||||||
|
newBuff := pool.Get(len(msg) + 2)
|
||||||
|
defer pool.Put(newBuff)
|
||||||
|
binary.BigEndian.PutUint16(newBuff[:2], uint16(len(msg)))
|
||||||
|
copy(newBuff[2:], msg)
|
||||||
|
outBuff = newBuff
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = conn.Write(msg)
|
_, err = conn.Write(outBuff)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/common/utils"
|
"github.com/metacubex/mihomo/common/utils"
|
||||||
@@ -49,6 +48,7 @@ type Resolver interface {
|
|||||||
LookupIP(ctx context.Context, host string) (ips []netip.Addr, err error)
|
LookupIP(ctx context.Context, host string) (ips []netip.Addr, err error)
|
||||||
LookupIPv4(ctx context.Context, host string) (ips []netip.Addr, err error)
|
LookupIPv4(ctx context.Context, host string) (ips []netip.Addr, err error)
|
||||||
LookupIPv6(ctx context.Context, host string) (ips []netip.Addr, err error)
|
LookupIPv6(ctx context.Context, host string) (ips []netip.Addr, err error)
|
||||||
|
ResolveECH(ctx context.Context, host string) ([]byte, error)
|
||||||
ExchangeContext(ctx context.Context, m *dns.Msg) (msg *dns.Msg, err error)
|
ExchangeContext(ctx context.Context, m *dns.Msg) (msg *dns.Msg, err error)
|
||||||
Invalid() bool
|
Invalid() bool
|
||||||
ClearCache()
|
ClearCache()
|
||||||
@@ -67,7 +67,8 @@ func LookupIPv4WithResolver(ctx context.Context, host string, r Resolver) ([]net
|
|||||||
|
|
||||||
ip, err := netip.ParseAddr(host)
|
ip, err := netip.ParseAddr(host)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if ip.Is4() || ip.Is4In6() {
|
ip = ip.Unmap()
|
||||||
|
if ip.Is4() {
|
||||||
return []netip.Addr{ip}, nil
|
return []netip.Addr{ip}, nil
|
||||||
}
|
}
|
||||||
return []netip.Addr{}, ErrIPVersion
|
return []netip.Addr{}, ErrIPVersion
|
||||||
@@ -116,7 +117,8 @@ func LookupIPv6WithResolver(ctx context.Context, host string, r Resolver) ([]net
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ip, err := netip.ParseAddr(host); err == nil {
|
if ip, err := netip.ParseAddr(host); err == nil {
|
||||||
if strings.Contains(host, ":") {
|
ip = ip.Unmap()
|
||||||
|
if ip.Is6() {
|
||||||
return []netip.Addr{ip}, nil
|
return []netip.Addr{ip}, nil
|
||||||
}
|
}
|
||||||
return nil, ErrIPVersion
|
return nil, ErrIPVersion
|
||||||
@@ -165,6 +167,7 @@ func LookupIPWithResolver(ctx context.Context, host string, r Resolver) ([]netip
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ip, err := netip.ParseAddr(host); err == nil {
|
if ip, err := netip.ParseAddr(host); err == nil {
|
||||||
|
ip = ip.Unmap()
|
||||||
return []netip.Addr{ip}, nil
|
return []netip.Addr{ip}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +219,17 @@ func ResolveIPPrefer6(ctx context.Context, host string) (netip.Addr, error) {
|
|||||||
return ResolveIPPrefer6WithResolver(ctx, host, DefaultResolver)
|
return ResolveIPPrefer6WithResolver(ctx, host, DefaultResolver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ResolveECHWithResolver(ctx context.Context, host string, r Resolver) ([]byte, error) {
|
||||||
|
if r != nil && r.Invalid() {
|
||||||
|
return r.ResolveECH(ctx, host)
|
||||||
|
}
|
||||||
|
return SystemResolver.ResolveECH(ctx, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveECH(ctx context.Context, host string) ([]byte, error) {
|
||||||
|
return ResolveECHWithResolver(ctx, host, DefaultResolver)
|
||||||
|
}
|
||||||
|
|
||||||
func ResetConnection() {
|
func ResetConnection() {
|
||||||
if DefaultResolver != nil {
|
if DefaultResolver != nil {
|
||||||
go DefaultResolver.ResetConnection()
|
go DefaultResolver.ResetConnection()
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ package resource
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/common/utils"
|
"github.com/metacubex/mihomo/common/utils"
|
||||||
|
"github.com/metacubex/mihomo/component/slowdown"
|
||||||
types "github.com/metacubex/mihomo/constant/provider"
|
types "github.com/metacubex/mihomo/constant/provider"
|
||||||
"github.com/metacubex/mihomo/log"
|
"github.com/metacubex/mihomo/log"
|
||||||
|
|
||||||
"github.com/sagernet/fswatch"
|
"github.com/metacubex/fswatch"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,6 +29,8 @@ type Fetcher[V any] struct {
|
|||||||
interval time.Duration
|
interval time.Duration
|
||||||
onUpdate func(V)
|
onUpdate func(V)
|
||||||
watcher *fswatch.Watcher
|
watcher *fswatch.Watcher
|
||||||
|
loadBufMutex sync.Mutex
|
||||||
|
backoff slowdown.Backoff
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher[V]) Name() string {
|
func (f *Fetcher[V]) Name() string {
|
||||||
@@ -46,17 +50,11 @@ func (f *Fetcher[V]) UpdatedAt() time.Time {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher[V]) Initial() (V, error) {
|
func (f *Fetcher[V]) Initial() (V, error) {
|
||||||
var (
|
|
||||||
buf []byte
|
|
||||||
contents V
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
|
if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
|
||||||
// local file exists, use it first
|
// local file exists, use it first
|
||||||
buf, err = os.ReadFile(f.vehicle.Path())
|
buf, err := os.ReadFile(f.vehicle.Path())
|
||||||
modTime := stat.ModTime()
|
modTime := stat.ModTime()
|
||||||
contents, _, err = f.loadBuf(buf, utils.MakeHash(buf), false)
|
contents, _, err := f.loadBuf(buf, utils.MakeHash(buf), false)
|
||||||
f.updatedAt = modTime // reset updatedAt to file's modTime
|
f.updatedAt = modTime // reset updatedAt to file's modTime
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -69,21 +67,25 @@ func (f *Fetcher[V]) Initial() (V, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parse local file error, fallback to remote
|
// parse local file error, fallback to remote
|
||||||
contents, _, err = f.Update()
|
contents, _, updateErr := f.Update()
|
||||||
|
|
||||||
|
// start the pull loop even if f.Update() failed
|
||||||
|
err := f.startPullLoop(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return lo.Empty[V](), err
|
return lo.Empty[V](), err
|
||||||
}
|
}
|
||||||
err = f.startPullLoop(false)
|
|
||||||
if err != nil {
|
if updateErr != nil {
|
||||||
return lo.Empty[V](), err
|
return lo.Empty[V](), updateErr
|
||||||
}
|
}
|
||||||
|
|
||||||
return contents, nil
|
return contents, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher[V]) Update() (V, bool, error) {
|
func (f *Fetcher[V]) Update() (V, bool, error) {
|
||||||
buf, hash, err := f.vehicle.Read(f.ctx, f.hash)
|
buf, hash, err := f.vehicle.Read(f.ctx, f.hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
f.backoff.AddAttempt() // add a failed attempt to backoff
|
||||||
return lo.Empty[V](), false, err
|
return lo.Empty[V](), false, err
|
||||||
}
|
}
|
||||||
return f.loadBuf(buf, hash, f.vehicle.Type() != types.File)
|
return f.loadBuf(buf, hash, f.vehicle.Type() != types.File)
|
||||||
@@ -94,12 +96,16 @@ func (f *Fetcher[V]) SideUpdate(buf []byte) (V, bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher[V]) loadBuf(buf []byte, hash utils.HashType, updateFile bool) (V, bool, error) {
|
func (f *Fetcher[V]) loadBuf(buf []byte, hash utils.HashType, updateFile bool) (V, bool, error) {
|
||||||
|
f.loadBufMutex.Lock()
|
||||||
|
defer f.loadBufMutex.Unlock()
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if f.hash.Equal(hash) {
|
if f.hash.Equal(hash) {
|
||||||
if updateFile {
|
if updateFile {
|
||||||
_ = os.Chtimes(f.vehicle.Path(), now, now)
|
_ = os.Chtimes(f.vehicle.Path(), now, now)
|
||||||
}
|
}
|
||||||
f.updatedAt = now
|
f.updatedAt = now
|
||||||
|
f.backoff.Reset() // no error, reset backoff
|
||||||
return lo.Empty[V](), true, nil
|
return lo.Empty[V](), true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,8 +115,10 @@ func (f *Fetcher[V]) loadBuf(buf []byte, hash utils.HashType, updateFile bool) (
|
|||||||
|
|
||||||
contents, err := f.parser(buf)
|
contents, err := f.parser(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
f.backoff.AddAttempt() // add a failed attempt to backoff
|
||||||
return lo.Empty[V](), false, err
|
return lo.Empty[V](), false, err
|
||||||
}
|
}
|
||||||
|
f.backoff.Reset() // no error, reset backoff
|
||||||
|
|
||||||
if updateFile {
|
if updateFile {
|
||||||
if err = f.vehicle.Write(buf); err != nil {
|
if err = f.vehicle.Write(buf); err != nil {
|
||||||
@@ -145,14 +153,25 @@ func (f *Fetcher[V]) pullLoop(forceUpdate bool) {
|
|||||||
log.Warnln("[Provider] %s not updated for a long time, force refresh", f.Name())
|
log.Warnln("[Provider] %s not updated for a long time, force refresh", f.Name())
|
||||||
f.updateWithLog()
|
f.updateWithLog()
|
||||||
}
|
}
|
||||||
|
if attempt := f.backoff.Attempt(); attempt > 0 { // f.Update() was failed, decrease the interval from backoff to achieve fast retry
|
||||||
|
if duration := f.backoff.ForAttempt(attempt); duration < initialInterval {
|
||||||
|
initialInterval = duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
timer := time.NewTimer(initialInterval)
|
timer := time.NewTimer(initialInterval)
|
||||||
defer timer.Stop()
|
defer timer.Stop()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
timer.Reset(f.interval)
|
|
||||||
f.updateWithLog()
|
f.updateWithLog()
|
||||||
|
interval := f.interval
|
||||||
|
if attempt := f.backoff.Attempt(); attempt > 0 { // f.Update() was failed, decrease the interval from backoff to achieve fast retry
|
||||||
|
if duration := f.backoff.ForAttempt(attempt); duration < interval {
|
||||||
|
interval = duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timer.Reset(interval)
|
||||||
case <-f.ctx.Done():
|
case <-f.ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -202,6 +221,10 @@ func (f *Fetcher[V]) updateWithLog() {
|
|||||||
|
|
||||||
func NewFetcher[V any](name string, interval time.Duration, vehicle types.Vehicle, parser Parser[V], onUpdate func(V)) *Fetcher[V] {
|
func NewFetcher[V any](name string, interval time.Duration, vehicle types.Vehicle, parser Parser[V], onUpdate func(V)) *Fetcher[V] {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
minBackoff := 10 * time.Second
|
||||||
|
if interval < minBackoff {
|
||||||
|
minBackoff = interval
|
||||||
|
}
|
||||||
return &Fetcher[V]{
|
return &Fetcher[V]{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
ctxCancel: cancel,
|
ctxCancel: cancel,
|
||||||
@@ -210,5 +233,11 @@ func NewFetcher[V any](name string, interval time.Duration, vehicle types.Vehicl
|
|||||||
parser: parser,
|
parser: parser,
|
||||||
onUpdate: onUpdate,
|
onUpdate: onUpdate,
|
||||||
interval: interval,
|
interval: interval,
|
||||||
|
backoff: slowdown.Backoff{
|
||||||
|
Factor: 2,
|
||||||
|
Jitter: false,
|
||||||
|
Min: minBackoff,
|
||||||
|
Max: interval,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ package slowdown
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
"math/rand"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/metacubex/randv2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Backoff is a time.Duration counter, starting at Min. After every call to
|
// Backoff is a time.Duration counter, starting at Min. After every call to
|
||||||
@@ -63,7 +64,7 @@ func (b *Backoff) ForAttempt(attempt float64) time.Duration {
|
|||||||
minf := float64(min)
|
minf := float64(min)
|
||||||
durf := minf * math.Pow(factor, attempt)
|
durf := minf * math.Pow(factor, attempt)
|
||||||
if b.Jitter {
|
if b.Jitter {
|
||||||
durf = rand.Float64()*(durf-minf) + minf
|
durf = randv2.Float64()*(durf-minf) + minf
|
||||||
}
|
}
|
||||||
//ensure float64 wont overflow int64
|
//ensure float64 wont overflow int64
|
||||||
if durf > maxInt64 {
|
if durf > maxInt64 {
|
||||||
@@ -90,6 +91,11 @@ func (b *Backoff) Attempt() float64 {
|
|||||||
return float64(b.attempt.Load())
|
return float64(b.attempt.Load())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddAttempt adds one to the attempt counter.
|
||||||
|
func (b *Backoff) AddAttempt() {
|
||||||
|
b.attempt.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
// Copy returns a backoff with equals constraints as the original
|
// Copy returns a backoff with equals constraints as the original
|
||||||
func (b *Backoff) Copy() *Backoff {
|
func (b *Backoff) Copy() *Backoff {
|
||||||
return &Backoff{
|
return &Backoff{
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/metacubex/sing/common/metadata"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/common/lru"
|
"github.com/metacubex/mihomo/common/lru"
|
||||||
N "github.com/metacubex/mihomo/common/net"
|
N "github.com/metacubex/mihomo/common/net"
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
@@ -72,8 +74,16 @@ func (sd *Dispatcher) UDPSniff(packet C.PacketAdapter, packetSender C.PacketSend
|
|||||||
overrideDest := config.OverrideDest
|
overrideDest := config.OverrideDest
|
||||||
|
|
||||||
if inWhitelist {
|
if inWhitelist {
|
||||||
|
replaceDomain := func(metadata *C.Metadata, host string) {
|
||||||
|
if sd.domainCanReplace(host) {
|
||||||
|
replaceDomain(metadata, host, overrideDest)
|
||||||
|
} else {
|
||||||
|
log.Debugln("[Sniffer] Skip sni[%s]", host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if wrapable, ok := current.(sniffer.MultiPacketSniffer); ok {
|
if wrapable, ok := current.(sniffer.MultiPacketSniffer); ok {
|
||||||
return wrapable.WrapperSender(packetSender, overrideDest)
|
return wrapable.WrapperSender(packetSender, replaceDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
host, err := current.SniffData(packet.Data())
|
host, err := current.SniffData(packet.Data())
|
||||||
@@ -81,7 +91,7 @@ func (sd *Dispatcher) UDPSniff(packet C.PacketAdapter, packetSender C.PacketSend
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceDomain(metadata, host, overrideDest)
|
replaceDomain(metadata, host)
|
||||||
return packetSender
|
return packetSender
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,11 +138,9 @@ func (sd *Dispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) bool
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, matcher := range sd.skipDomain {
|
if !sd.domainCanReplace(host) {
|
||||||
if matcher.MatchDomain(host) {
|
log.Debugln("[Sniffer] Skip sni[%s]", host)
|
||||||
log.Debugln("[Sniffer] Skip sni[%s]", host)
|
return false
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sd.skipList.Delete(dst)
|
sd.skipList.Delete(dst)
|
||||||
@@ -152,10 +160,23 @@ func replaceDomain(metadata *C.Metadata, host string, overrideDest bool) {
|
|||||||
metadata.RemoteAddress(),
|
metadata.RemoteAddress(),
|
||||||
metadata.Host, host)
|
metadata.Host, host)
|
||||||
metadata.Host = host
|
metadata.Host = host
|
||||||
|
metadata.DstIP = netip.Addr{}
|
||||||
}
|
}
|
||||||
metadata.DNSMode = C.DNSNormal
|
metadata.DNSMode = C.DNSNormal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sd *Dispatcher) domainCanReplace(host string) bool {
|
||||||
|
if host == "." || !metadata.IsDomainName(host) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, matcher := range sd.skipDomain {
|
||||||
|
if matcher.MatchDomain(host) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (sd *Dispatcher) Enable() bool {
|
func (sd *Dispatcher) Enable() bool {
|
||||||
return sd != nil && sd.enable
|
return sd != nil && sd.enable
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,24 +74,25 @@ func (sniffer *QuicSniffer) SniffData(b []byte) (string, error) {
|
|||||||
return "", ErrorUnsupportedSniffer
|
return "", ErrorUnsupportedSniffer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sniffer *QuicSniffer) WrapperSender(packetSender constant.PacketSender, override bool) constant.PacketSender {
|
func (sniffer *QuicSniffer) WrapperSender(packetSender constant.PacketSender, replaceDomain sniffer.ReplaceDomain) constant.PacketSender {
|
||||||
return &quicPacketSender{
|
return &quicPacketSender{
|
||||||
sender: packetSender,
|
PacketSender: packetSender,
|
||||||
chClose: make(chan struct{}),
|
replaceDomain: replaceDomain,
|
||||||
override: override,
|
chClose: make(chan struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ constant.PacketSender = (*quicPacketSender)(nil)
|
var _ constant.PacketSender = (*quicPacketSender)(nil)
|
||||||
|
|
||||||
type quicPacketSender struct {
|
type quicPacketSender struct {
|
||||||
lock sync.RWMutex
|
lock sync.RWMutex
|
||||||
ranges utils.IntRanges[uint64]
|
ranges utils.IntRanges[uint64]
|
||||||
buffer []byte
|
buffer []byte
|
||||||
result string
|
result *string
|
||||||
override bool
|
|
||||||
|
|
||||||
sender constant.PacketSender
|
replaceDomain sniffer.ReplaceDomain
|
||||||
|
|
||||||
|
constant.PacketSender
|
||||||
|
|
||||||
chClose chan struct{}
|
chClose chan struct{}
|
||||||
closed bool
|
closed bool
|
||||||
@@ -100,7 +101,7 @@ type quicPacketSender struct {
|
|||||||
// Send will send PacketAdapter nonblocking
|
// Send will send PacketAdapter nonblocking
|
||||||
// the implement must call UDPPacket.Drop() inside Send
|
// the implement must call UDPPacket.Drop() inside Send
|
||||||
func (q *quicPacketSender) Send(current constant.PacketAdapter) {
|
func (q *quicPacketSender) Send(current constant.PacketAdapter) {
|
||||||
defer q.sender.Send(current)
|
defer q.PacketSender.Send(current)
|
||||||
|
|
||||||
q.lock.RLock()
|
q.lock.RLock()
|
||||||
if q.closed {
|
if q.closed {
|
||||||
@@ -116,29 +117,27 @@ func (q *quicPacketSender) Send(current constant.PacketAdapter) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process is a blocking loop to send PacketAdapter to PacketConn and update the WriteBackProxy
|
// DoSniff wait sniffer recv all fragments and update the domain
|
||||||
func (q *quicPacketSender) Process(conn constant.PacketConn, proxy constant.WriteBackProxy) {
|
func (q *quicPacketSender) DoSniff(metadata *constant.Metadata) error {
|
||||||
q.sender.Process(conn, proxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveUDP wait sniffer recv all fragments and update the domain
|
|
||||||
func (q *quicPacketSender) ResolveUDP(data *constant.Metadata) error {
|
|
||||||
select {
|
select {
|
||||||
case <-q.chClose:
|
case <-q.chClose:
|
||||||
q.lock.RLock()
|
q.lock.RLock()
|
||||||
replaceDomain(data, q.result, q.override)
|
if q.result != nil {
|
||||||
|
host := *q.result
|
||||||
|
q.replaceDomain(metadata, host)
|
||||||
|
}
|
||||||
q.lock.RUnlock()
|
q.lock.RUnlock()
|
||||||
break
|
break
|
||||||
case <-time.After(quicWaitConn):
|
case <-time.After(quicWaitConn):
|
||||||
q.close()
|
q.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return q.sender.ResolveUDP(data)
|
return q.PacketSender.DoSniff(metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close stop the Process loop
|
// Close stop the Process loop
|
||||||
func (q *quicPacketSender) Close() {
|
func (q *quicPacketSender) Close() {
|
||||||
q.sender.Close()
|
q.PacketSender.Close()
|
||||||
q.close()
|
q.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,7 +432,7 @@ func (q *quicPacketSender) tryAssemble() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
q.lock.Lock()
|
q.lock.Lock()
|
||||||
q.result = *domain
|
q.result = domain
|
||||||
q.closeLocked()
|
q.closeLocked()
|
||||||
q.lock.Unlock()
|
q.lock.Unlock()
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type fakeSender struct {
|
type fakeSender struct {
|
||||||
resultCh chan *constant.Metadata
|
constant.PacketSender
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ constant.PacketSender = (*fakeSender)(nil)
|
var _ constant.PacketSender = (*fakeSender)(nil)
|
||||||
@@ -22,18 +22,7 @@ func (e *fakeSender) Send(packet constant.PacketAdapter) {
|
|||||||
packet.Drop()
|
packet.Drop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *fakeSender) Process(constant.PacketConn, constant.WriteBackProxy) {
|
func (e *fakeSender) DoSniff(metadata *constant.Metadata) error { return nil }
|
||||||
panic("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *fakeSender) ResolveUDP(metadata *constant.Metadata) error {
|
|
||||||
e.resultCh <- metadata
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *fakeSender) Close() {
|
|
||||||
panic("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeUDPPacket struct {
|
type fakeUDPPacket struct {
|
||||||
data []byte
|
data []byte
|
||||||
@@ -78,23 +67,28 @@ func asPacket(data string) constant.PacketAdapter {
|
|||||||
return pktAdp
|
return pktAdp
|
||||||
}
|
}
|
||||||
|
|
||||||
func testQuicSniffer(data []string, async bool) (string, error) {
|
const fakeHost = "fake.host.com"
|
||||||
|
|
||||||
|
func testQuicSniffer(data []string, async bool) (string, string, error) {
|
||||||
q, err := NewQuicSniffer(SnifferConfig{})
|
q, err := NewQuicSniffer(SnifferConfig{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
resultCh := make(chan *constant.Metadata, 1)
|
resultCh := make(chan *constant.Metadata, 1)
|
||||||
emptySender := &fakeSender{resultCh: resultCh}
|
emptySender := &fakeSender{}
|
||||||
|
|
||||||
sender := q.WrapperSender(emptySender, true)
|
sender := q.WrapperSender(emptySender, func(metadata *constant.Metadata, host string) {
|
||||||
|
replaceDomain(metadata, host, true)
|
||||||
|
})
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
meta := constant.Metadata{}
|
meta := constant.Metadata{Host: fakeHost}
|
||||||
err = sender.ResolveUDP(&meta)
|
err := sender.DoSniff(&meta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
resultCh <- &meta
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for _, d := range data {
|
for _, d := range data {
|
||||||
@@ -106,14 +100,15 @@ func testQuicSniffer(data []string, async bool) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
meta := <-resultCh
|
meta := <-resultCh
|
||||||
return meta.SniffHost, nil
|
return meta.SniffHost, meta.Host, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestQuicHeaders(t *testing.T) {
|
func TestQuicHeaders(t *testing.T) {
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
input []string
|
input []string
|
||||||
domain string
|
domain string
|
||||||
|
invalid bool
|
||||||
}{
|
}{
|
||||||
//Normal domain quic sniff
|
//Normal domain quic sniff
|
||||||
{
|
{
|
||||||
@@ -171,16 +166,31 @@ func TestQuicHeaders(t *testing.T) {
|
|||||||
},
|
},
|
||||||
domain: "www.google.com",
|
domain: "www.google.com",
|
||||||
},
|
},
|
||||||
|
// invalid packet
|
||||||
|
{
|
||||||
|
input: []string{"00000000000000000000"},
|
||||||
|
invalid: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range cases {
|
for _, test := range cases {
|
||||||
data, err := testQuicSniffer(test.input, true)
|
data, host, err := testQuicSniffer(test.input, true)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, test.domain, data)
|
assert.Equal(t, test.domain, data)
|
||||||
|
if test.invalid {
|
||||||
|
assert.Equal(t, fakeHost, host)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, test.domain, host)
|
||||||
|
}
|
||||||
|
|
||||||
data, err = testQuicSniffer(test.input, false)
|
data, host, err = testQuicSniffer(test.input, false)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, test.domain, data)
|
assert.Equal(t, test.domain, data)
|
||||||
|
if test.invalid {
|
||||||
|
assert.Equal(t, fakeHost, host)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, test.domain, host)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
70
component/tls/httpserver.go
Normal file
70
component/tls/httpserver.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"runtime/debug"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
N "github.com/metacubex/mihomo/common/net"
|
||||||
|
"github.com/metacubex/mihomo/log"
|
||||||
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func extractTlsHandshakeTimeoutFromServer(s *http.Server) time.Duration {
|
||||||
|
var ret time.Duration
|
||||||
|
for _, v := range [...]time.Duration{
|
||||||
|
s.ReadHeaderTimeout,
|
||||||
|
s.ReadTimeout,
|
||||||
|
s.WriteTimeout,
|
||||||
|
} {
|
||||||
|
if v <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ret == 0 || v < ret {
|
||||||
|
ret = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewListenerForHttps returns a net.Listener for (*http.Server).Serve()
|
||||||
|
// the "func (c *conn) serve(ctx context.Context)" in http\server.go
|
||||||
|
// only do tls handshake and check NegotiatedProtocol with std's *tls.Conn
|
||||||
|
// so we do the same logic to let http2 (not h2c) work fine
|
||||||
|
func NewListenerForHttps(l net.Listener, httpServer *http.Server, tlsConfig *Config) net.Listener {
|
||||||
|
http2Server := &http2.Server{}
|
||||||
|
_ = http2.ConfigureServer(httpServer, http2Server)
|
||||||
|
return N.NewHandleContextListener(context.Background(), l, func(ctx context.Context, conn net.Conn) (net.Conn, error) {
|
||||||
|
c := Server(conn, tlsConfig)
|
||||||
|
|
||||||
|
tlsTO := extractTlsHandshakeTimeoutFromServer(httpServer)
|
||||||
|
if tlsTO > 0 {
|
||||||
|
dl := time.Now().Add(tlsTO)
|
||||||
|
_ = conn.SetReadDeadline(dl)
|
||||||
|
_ = conn.SetWriteDeadline(dl)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.HandshakeContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore Conn-level deadlines.
|
||||||
|
if tlsTO > 0 {
|
||||||
|
_ = conn.SetReadDeadline(time.Time{})
|
||||||
|
_ = conn.SetWriteDeadline(time.Time{})
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ConnectionState().NegotiatedProtocol == http2.NextProtoTLS {
|
||||||
|
http2Server.ServeConn(c, &http2.ServeConnOpts{BaseConfig: httpServer})
|
||||||
|
return nil, net.ErrClosed
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}, func(a any) {
|
||||||
|
stack := debug.Stack()
|
||||||
|
log.Errorln("https server panic: %s\n%s", a, stack)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -26,7 +26,6 @@ import (
|
|||||||
utls "github.com/metacubex/utls"
|
utls "github.com/metacubex/utls"
|
||||||
"golang.org/x/crypto/chacha20poly1305"
|
"golang.org/x/crypto/chacha20poly1305"
|
||||||
"golang.org/x/crypto/hkdf"
|
"golang.org/x/crypto/hkdf"
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,11 +34,12 @@ const RealityMaxShortIDLen = 8
|
|||||||
type RealityConfig struct {
|
type RealityConfig struct {
|
||||||
PublicKey *ecdh.PublicKey
|
PublicKey *ecdh.PublicKey
|
||||||
ShortID [RealityMaxShortIDLen]byte
|
ShortID [RealityMaxShortIDLen]byte
|
||||||
|
|
||||||
|
SupportX25519MLKEM768 bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRealityConn(ctx context.Context, conn net.Conn, clientFingerprint string, tlsConfig *tls.Config, realityConfig *RealityConfig) (net.Conn, error) {
|
func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHelloID, tlsConfig *Config, realityConfig *RealityConfig) (net.Conn, error) {
|
||||||
retry := 0
|
for retry := 0; ; retry++ {
|
||||||
for fingerprint, exists := GetFingerprint(clientFingerprint); exists; retry++ {
|
|
||||||
verifier := &realityVerifier{
|
verifier := &realityVerifier{
|
||||||
serverName: tlsConfig.ServerName,
|
serverName: tlsConfig.ServerName,
|
||||||
}
|
}
|
||||||
@@ -49,39 +49,18 @@ func GetRealityConn(ctx context.Context, conn net.Conn, clientFingerprint string
|
|||||||
SessionTicketsDisabled: true,
|
SessionTicketsDisabled: true,
|
||||||
VerifyPeerCertificate: verifier.VerifyPeerCertificate,
|
VerifyPeerCertificate: verifier.VerifyPeerCertificate,
|
||||||
}
|
}
|
||||||
clientID := utls.ClientHelloID{
|
|
||||||
Client: fingerprint.Client,
|
if !realityConfig.SupportX25519MLKEM768 && fingerprint == HelloChrome_Auto {
|
||||||
Version: fingerprint.Version,
|
fingerprint = HelloChrome_120 // old reality server doesn't work with X25519MLKEM768
|
||||||
Seed: fingerprint.Seed,
|
|
||||||
}
|
}
|
||||||
uConn := utls.UClient(conn, uConfig, clientID)
|
|
||||||
|
uConn := utls.UClient(conn, uConfig, fingerprint)
|
||||||
verifier.UConn = uConn
|
verifier.UConn = uConn
|
||||||
err := uConn.BuildHandshakeState()
|
err := uConn.BuildHandshakeState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------for X25519MLKEM768 does not work properly with reality-------
|
|
||||||
// Iterate over extensions and check
|
|
||||||
for _, extension := range uConn.Extensions {
|
|
||||||
if ce, ok := extension.(*utls.SupportedCurvesExtension); ok {
|
|
||||||
ce.Curves = slices.DeleteFunc(ce.Curves, func(curveID utls.CurveID) bool {
|
|
||||||
return curveID == utls.X25519MLKEM768
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if ks, ok := extension.(*utls.KeyShareExtension); ok {
|
|
||||||
ks.KeyShares = slices.DeleteFunc(ks.KeyShares, func(share utls.KeyShare) bool {
|
|
||||||
return share.Group == utls.X25519MLKEM768
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Rebuild the client hello
|
|
||||||
err = uConn.BuildHandshakeState()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// --------------------------------------------------------------------
|
|
||||||
|
|
||||||
hello := uConn.HandshakeState.Hello
|
hello := uConn.HandshakeState.Hello
|
||||||
rawSessionID := hello.Raw[39 : 39+32] // the location of session ID
|
rawSessionID := hello.Raw[39 : 39+32] // the location of session ID
|
||||||
for i := range rawSessionID { // https://github.com/golang/go/issues/5373
|
for i := range rawSessionID { // https://github.com/golang/go/issues/5373
|
||||||
@@ -145,13 +124,12 @@ func GetRealityConn(ctx context.Context, conn net.Conn, clientFingerprint string
|
|||||||
log.Debugln("REALITY Authentication: %v, AEAD: %T", verifier.verified, aeadCipher)
|
log.Debugln("REALITY Authentication: %v, AEAD: %T", verifier.verified, aeadCipher)
|
||||||
|
|
||||||
if !verifier.verified {
|
if !verifier.verified {
|
||||||
go realityClientFallback(uConn, uConfig.ServerName, clientID)
|
go realityClientFallback(uConn, uConfig.ServerName, fingerprint)
|
||||||
return nil, errors.New("REALITY authentication failed")
|
return nil, errors.New("REALITY authentication failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return uConn, nil
|
return uConn, nil
|
||||||
}
|
}
|
||||||
return nil, errors.New("unknown uTLS fingerprint")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func realityClientFallback(uConn net.Conn, serverName string, fingerprint utls.ClientHelloID) {
|
func realityClientFallback(uConn net.Conn, serverName string, fingerprint utls.ClientHelloID) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
|
"github.com/metacubex/mihomo/common/once"
|
||||||
"github.com/metacubex/mihomo/common/utils"
|
"github.com/metacubex/mihomo/common/utils"
|
||||||
"github.com/metacubex/mihomo/log"
|
"github.com/metacubex/mihomo/log"
|
||||||
|
|
||||||
@@ -11,46 +12,53 @@ import (
|
|||||||
"github.com/mroth/weightedrand/v2"
|
"github.com/mroth/weightedrand/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Conn = utls.Conn
|
||||||
type UConn = utls.UConn
|
type UConn = utls.UConn
|
||||||
|
type UClientHelloID = utls.ClientHelloID
|
||||||
|
|
||||||
|
const VersionTLS12 = utls.VersionTLS12
|
||||||
const VersionTLS13 = utls.VersionTLS13
|
const VersionTLS13 = utls.VersionTLS13
|
||||||
|
|
||||||
type UClientHelloID struct {
|
func Client(c net.Conn, config *utls.Config) *Conn {
|
||||||
*utls.ClientHelloID
|
return utls.Client(c, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
var initRandomFingerprint UClientHelloID
|
|
||||||
var initUtlsClient string
|
|
||||||
|
|
||||||
func UClient(c net.Conn, config *utls.Config, fingerprint UClientHelloID) *UConn {
|
func UClient(c net.Conn, config *utls.Config, fingerprint UClientHelloID) *UConn {
|
||||||
return utls.UClient(c, config, *fingerprint.ClientHelloID)
|
return utls.UClient(c, config, fingerprint)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFingerprint(ClientFingerprint string) (UClientHelloID, bool) {
|
func Server(c net.Conn, config *utls.Config) *Conn {
|
||||||
if ClientFingerprint == "none" {
|
return utls.Server(c, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListener(inner net.Listener, config *Config) net.Listener {
|
||||||
|
return utls.NewListener(inner, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFingerprint(clientFingerprint string) (UClientHelloID, bool) {
|
||||||
|
if len(clientFingerprint) == 0 {
|
||||||
|
clientFingerprint = globalFingerprint
|
||||||
|
}
|
||||||
|
if len(clientFingerprint) == 0 || clientFingerprint == "none" {
|
||||||
return UClientHelloID{}, false
|
return UClientHelloID{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if initRandomFingerprint.ClientHelloID == nil {
|
if clientFingerprint == "random" {
|
||||||
initRandomFingerprint, _ = RollFingerprint()
|
fingerprint := randomFingerprint()
|
||||||
|
log.Debugln("use initial random HelloID:%s", fingerprint.Client)
|
||||||
|
return fingerprint, true
|
||||||
}
|
}
|
||||||
|
|
||||||
if ClientFingerprint == "random" {
|
if fingerprint, ok := fingerprints[clientFingerprint]; ok {
|
||||||
log.Debugln("use initial random HelloID:%s", initRandomFingerprint.Client)
|
|
||||||
return initRandomFingerprint, true
|
|
||||||
}
|
|
||||||
|
|
||||||
fingerprint, ok := Fingerprints[ClientFingerprint]
|
|
||||||
if ok {
|
|
||||||
log.Debugln("use specified fingerprint:%s", fingerprint.Client)
|
log.Debugln("use specified fingerprint:%s", fingerprint.Client)
|
||||||
return fingerprint, ok
|
return fingerprint, true
|
||||||
} else {
|
} else {
|
||||||
log.Warnln("wrong ClientFingerprint:%s", ClientFingerprint)
|
log.Warnln("wrong clientFingerprint:%s", clientFingerprint)
|
||||||
return UClientHelloID{}, false
|
return UClientHelloID{}, false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func RollFingerprint() (UClientHelloID, bool) {
|
var randomFingerprint = once.OnceValue(func() UClientHelloID {
|
||||||
chooser, _ := weightedrand.NewChooser(
|
chooser, _ := weightedrand.NewChooser(
|
||||||
weightedrand.NewChoice("chrome", 6),
|
weightedrand.NewChoice("chrome", 6),
|
||||||
weightedrand.NewChoice("safari", 3),
|
weightedrand.NewChoice("safari", 3),
|
||||||
@@ -59,26 +67,34 @@ func RollFingerprint() (UClientHelloID, bool) {
|
|||||||
)
|
)
|
||||||
initClient := chooser.Pick()
|
initClient := chooser.Pick()
|
||||||
log.Debugln("initial random HelloID:%s", initClient)
|
log.Debugln("initial random HelloID:%s", initClient)
|
||||||
fingerprint, ok := Fingerprints[initClient]
|
fingerprint, ok := fingerprints[initClient]
|
||||||
return fingerprint, ok
|
if !ok {
|
||||||
}
|
log.Warnln("error in initial random HelloID:%s", initClient)
|
||||||
|
}
|
||||||
|
return fingerprint
|
||||||
|
})
|
||||||
|
|
||||||
var Fingerprints = map[string]UClientHelloID{
|
var HelloChrome_Auto = utls.HelloChrome_Auto
|
||||||
"chrome": {&utls.HelloChrome_Auto},
|
var HelloChrome_120 = utls.HelloChrome_120 // special fingerprint for some old protocols doesn't work with HelloChrome_Auto
|
||||||
"chrome_psk": {&utls.HelloChrome_100_PSK},
|
|
||||||
"chrome_psk_shuffle": {&utls.HelloChrome_106_Shuffle},
|
var fingerprints = map[string]UClientHelloID{
|
||||||
"chrome_padding_psk_shuffle": {&utls.HelloChrome_114_Padding_PSK_Shuf},
|
"chrome": utls.HelloChrome_Auto,
|
||||||
"chrome_pq": {&utls.HelloChrome_115_PQ},
|
"firefox": utls.HelloFirefox_Auto,
|
||||||
"chrome_pq_psk": {&utls.HelloChrome_115_PQ_PSK},
|
"safari": utls.HelloSafari_Auto,
|
||||||
"firefox": {&utls.HelloFirefox_Auto},
|
"ios": utls.HelloIOS_Auto,
|
||||||
"safari": {&utls.HelloSafari_Auto},
|
"android": utls.HelloAndroid_11_OkHttp,
|
||||||
"ios": {&utls.HelloIOS_Auto},
|
"edge": utls.HelloEdge_Auto,
|
||||||
"android": {&utls.HelloAndroid_11_OkHttp},
|
"360": utls.Hello360_Auto,
|
||||||
"edge": {&utls.HelloEdge_Auto},
|
"qq": utls.HelloQQ_Auto,
|
||||||
"360": {&utls.Hello360_Auto},
|
"random": {},
|
||||||
"qq": {&utls.HelloQQ_Auto},
|
|
||||||
"random": {nil},
|
// deprecated fingerprints should not be used
|
||||||
"randomized": {nil},
|
"chrome_psk": utls.HelloChrome_100_PSK,
|
||||||
|
"chrome_psk_shuffle": utls.HelloChrome_106_Shuffle,
|
||||||
|
"chrome_padding_psk_shuffle": utls.HelloChrome_114_Padding_PSK_Shuf,
|
||||||
|
"chrome_pq": utls.HelloChrome_115_PQ,
|
||||||
|
"chrome_pq_psk": utls.HelloChrome_115_PQ_PSK,
|
||||||
|
"randomized": utls.HelloRandomized,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -88,10 +104,12 @@ func init() {
|
|||||||
randomized := utls.HelloRandomized
|
randomized := utls.HelloRandomized
|
||||||
randomized.Seed, _ = utls.NewPRNGSeed()
|
randomized.Seed, _ = utls.NewPRNGSeed()
|
||||||
randomized.Weights = &weights
|
randomized.Weights = &weights
|
||||||
Fingerprints["randomized"] = UClientHelloID{&randomized}
|
fingerprints["randomized"] = randomized
|
||||||
}
|
}
|
||||||
|
|
||||||
func UCertificates(it tls.Certificate) utls.Certificate {
|
type Certificate = utls.Certificate
|
||||||
|
|
||||||
|
func UCertificate(it tls.Certificate) utls.Certificate {
|
||||||
return utls.Certificate{
|
return utls.Certificate{
|
||||||
Certificate: it.Certificate,
|
Certificate: it.Certificate,
|
||||||
PrivateKey: it.PrivateKey,
|
PrivateKey: it.PrivateKey,
|
||||||
@@ -104,11 +122,15 @@ func UCertificates(it tls.Certificate) utls.Certificate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EncryptedClientHelloKey = utls.EncryptedClientHelloKey
|
||||||
|
|
||||||
|
type Config = utls.Config
|
||||||
|
|
||||||
func UConfig(config *tls.Config) *utls.Config {
|
func UConfig(config *tls.Config) *utls.Config {
|
||||||
return &utls.Config{
|
return &utls.Config{
|
||||||
Rand: config.Rand,
|
Rand: config.Rand,
|
||||||
Time: config.Time,
|
Time: config.Time,
|
||||||
Certificates: utils.Map(config.Certificates, UCertificates),
|
Certificates: utils.Map(config.Certificates, UCertificate),
|
||||||
VerifyPeerCertificate: config.VerifyPeerCertificate,
|
VerifyPeerCertificate: config.VerifyPeerCertificate,
|
||||||
RootCAs: config.RootCAs,
|
RootCAs: config.RootCAs,
|
||||||
NextProtos: config.NextProtos,
|
NextProtos: config.NextProtos,
|
||||||
@@ -152,14 +174,12 @@ func BuildWebsocketHandshakeState(c *UConn) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetGlobalUtlsClient(Client string) {
|
var globalFingerprint string
|
||||||
initUtlsClient = Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func HaveGlobalFingerprint() bool {
|
func SetGlobalFingerprint(fingerprint string) {
|
||||||
return len(initUtlsClient) != 0 && initUtlsClient != "none"
|
globalFingerprint = fingerprint
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetGlobalFingerprint() string {
|
func GetGlobalFingerprint() string {
|
||||||
return initUtlsClient
|
return globalFingerprint
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/common/atomic"
|
"github.com/metacubex/mihomo/common/atomic"
|
||||||
"github.com/metacubex/mihomo/common/batch"
|
|
||||||
"github.com/metacubex/mihomo/common/utils"
|
"github.com/metacubex/mihomo/common/utils"
|
||||||
"github.com/metacubex/mihomo/component/geodata"
|
"github.com/metacubex/mihomo/component/geodata"
|
||||||
_ "github.com/metacubex/mihomo/component/geodata/standard"
|
_ "github.com/metacubex/mihomo/component/geodata/standard"
|
||||||
@@ -19,6 +18,7 @@ import (
|
|||||||
"github.com/metacubex/mihomo/log"
|
"github.com/metacubex/mihomo/log"
|
||||||
|
|
||||||
"github.com/oschwald/maxminddb-golang"
|
"github.com/oschwald/maxminddb-golang"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -169,41 +169,25 @@ func UpdateGeoSite() (err error) {
|
|||||||
func updateGeoDatabases() error {
|
func updateGeoDatabases() error {
|
||||||
defer runtime.GC()
|
defer runtime.GC()
|
||||||
|
|
||||||
b, _ := batch.New[interface{}](context.Background())
|
b := errgroup.Group{}
|
||||||
|
|
||||||
if geodata.GeoIpEnable() {
|
if geodata.GeoIpEnable() {
|
||||||
if geodata.GeodataMode() {
|
if geodata.GeodataMode() {
|
||||||
b.Go("UpdateGeoIp", func() (_ interface{}, err error) {
|
b.Go(UpdateGeoIp)
|
||||||
err = UpdateGeoIp()
|
|
||||||
return
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
b.Go("UpdateMMDB", func() (_ interface{}, err error) {
|
b.Go(UpdateMMDB)
|
||||||
err = UpdateMMDB()
|
|
||||||
return
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if geodata.ASNEnable() {
|
if geodata.ASNEnable() {
|
||||||
b.Go("UpdateASN", func() (_ interface{}, err error) {
|
b.Go(UpdateASN)
|
||||||
err = UpdateASN()
|
|
||||||
return
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if geodata.GeoSiteEnable() {
|
if geodata.GeoSiteEnable() {
|
||||||
b.Go("UpdateGeoSite", func() (_ interface{}, err error) {
|
b.Go(UpdateGeoSite)
|
||||||
err = UpdateGeoSite()
|
|
||||||
return
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if e := b.Wait(); e != nil {
|
return b.Wait()
|
||||||
return e.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrGetDatabaseUpdateSkip = errors.New("GEO database is updating, skip")
|
var ErrGetDatabaseUpdateSkip = errors.New("GEO database is updating, skip")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package updater
|
|||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -32,6 +33,17 @@ const (
|
|||||||
typeTarGzip
|
typeTarGzip
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (t compressionType) String() string {
|
||||||
|
switch t {
|
||||||
|
case typeZip:
|
||||||
|
return "zip"
|
||||||
|
case typeTarGzip:
|
||||||
|
return "tar.gz"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var DefaultUiUpdater = &UIUpdater{}
|
var DefaultUiUpdater = &UIUpdater{}
|
||||||
|
|
||||||
func NewUiUpdater(externalUI, externalUIURL, externalUIName string) *UIUpdater {
|
func NewUiUpdater(externalUI, externalUIURL, externalUIName string) *UIUpdater {
|
||||||
@@ -99,48 +111,38 @@ func detectFileType(data []byte) compressionType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *UIUpdater) downloadUI() error {
|
func (u *UIUpdater) downloadUI() error {
|
||||||
err := u.prepareUIPath()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("prepare UI path failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := downloadForBytes(u.externalUIURL)
|
data, err := downloadForBytes(u.externalUIURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("can't download file: %w", err)
|
return fmt.Errorf("can't download file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileType := detectFileType(data)
|
tmpDir := C.Path.Resolve("downloadUI.tmp")
|
||||||
if fileType == typeUnknown {
|
defer os.RemoveAll(tmpDir)
|
||||||
return fmt.Errorf("unknown or unsupported file type")
|
|
||||||
|
os.RemoveAll(tmpDir) // cleanup tmp dir before extract
|
||||||
|
log.Debugln("extractedFolder: %s", tmpDir)
|
||||||
|
err = extract(data, tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't extract compressed file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ext := ".zip"
|
log.Debugln("cleanupFolder: %s", u.externalUIPath)
|
||||||
if fileType == typeTarGzip {
|
err = cleanup(u.externalUIPath) // cleanup files in dir don't remove dir itself
|
||||||
ext = ".tgz"
|
|
||||||
}
|
|
||||||
|
|
||||||
saved := path.Join(C.Path.HomeDir(), "download"+ext)
|
|
||||||
log.Debugln("compression Type: %s", ext)
|
|
||||||
if err = saveFile(data, saved); err != nil {
|
|
||||||
return fmt.Errorf("can't save compressed file: %w", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(saved)
|
|
||||||
|
|
||||||
err = cleanup(u.externalUIPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
return fmt.Errorf("cleanup exist file error: %w", err)
|
return fmt.Errorf("cleanup exist file error: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extractedFolder, err := extract(saved, C.Path.HomeDir())
|
err = u.prepareUIPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("can't extract compressed file: %w", err)
|
return fmt.Errorf("prepare UI path failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.Rename(extractedFolder, u.externalUIPath)
|
log.Debugln("moveFolder from %s to %s", tmpDir, u.externalUIPath)
|
||||||
|
err = moveDir(tmpDir, u.externalUIPath) // move files from tmp to target
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("rename UI folder failed: %w", err)
|
return fmt.Errorf("move UI folder failed: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -155,228 +157,109 @@ func (u *UIUpdater) prepareUIPath() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func unzip(src, dest string) (string, error) {
|
func unzip(data []byte, dest string) error {
|
||||||
r, err := zip.OpenReader(src)
|
r, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return err
|
||||||
}
|
}
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
// check whether or not only exists singleRoot dir
|
// check whether or not only exists singleRoot dir
|
||||||
rootDir := ""
|
|
||||||
isSingleRoot := true
|
|
||||||
rootItemCount := 0
|
|
||||||
for _, f := range r.File {
|
|
||||||
parts := strings.Split(strings.Trim(f.Name, "/"), "/")
|
|
||||||
if len(parts) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(parts) == 1 {
|
|
||||||
isDir := strings.HasSuffix(f.Name, "/")
|
|
||||||
if !isDir {
|
|
||||||
isSingleRoot = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if rootDir == "" {
|
|
||||||
rootDir = parts[0]
|
|
||||||
}
|
|
||||||
rootItemCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if rootItemCount != 1 {
|
|
||||||
isSingleRoot = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// build the dir of extraction
|
|
||||||
var extractedFolder string
|
|
||||||
if isSingleRoot && rootDir != "" {
|
|
||||||
// if the singleRoot, use it directly
|
|
||||||
log.Debugln("Match the singleRoot")
|
|
||||||
extractedFolder = filepath.Join(dest, rootDir)
|
|
||||||
log.Debugln("extractedFolder: %s", extractedFolder)
|
|
||||||
} else {
|
|
||||||
log.Debugln("Match the multiRoot")
|
|
||||||
// or put the files/dirs into new dir
|
|
||||||
baseName := filepath.Base(src)
|
|
||||||
baseName = strings.TrimSuffix(baseName, filepath.Ext(baseName))
|
|
||||||
extractedFolder = filepath.Join(dest, baseName)
|
|
||||||
|
|
||||||
for i := 1; ; i++ {
|
|
||||||
if _, err := os.Stat(extractedFolder); os.IsNotExist(err) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
extractedFolder = filepath.Join(dest, fmt.Sprintf("%s_%d", baseName, i))
|
|
||||||
}
|
|
||||||
log.Debugln("extractedFolder: %s", extractedFolder)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range r.File {
|
for _, f := range r.File {
|
||||||
var fpath string
|
fpath := filepath.Join(dest, f.Name)
|
||||||
if isSingleRoot && rootDir != "" {
|
|
||||||
fpath = filepath.Join(dest, f.Name)
|
|
||||||
} else {
|
|
||||||
fpath = filepath.Join(extractedFolder, f.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
|
if !inDest(fpath, dest) {
|
||||||
return "", fmt.Errorf("invalid file path: %s", fpath)
|
return fmt.Errorf("invalid file path: %s", fpath)
|
||||||
}
|
}
|
||||||
if f.FileInfo().IsDir() {
|
info := f.FileInfo()
|
||||||
|
if info.IsDir() {
|
||||||
os.MkdirAll(fpath, os.ModePerm)
|
os.MkdirAll(fpath, os.ModePerm)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
continue // disallow symlink
|
||||||
|
}
|
||||||
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
||||||
return "", err
|
return err
|
||||||
}
|
}
|
||||||
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return err
|
||||||
}
|
}
|
||||||
rc, err := f.Open()
|
rc, err := f.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return err
|
||||||
}
|
}
|
||||||
_, err = io.Copy(outFile, rc)
|
_, err = io.Copy(outFile, rc)
|
||||||
outFile.Close()
|
outFile.Close()
|
||||||
rc.Close()
|
rc.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return extractedFolder, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func untgz(src, dest string) (string, error) {
|
func untgz(data []byte, dest string) error {
|
||||||
file, err := os.Open(src)
|
gzr, err := gzip.NewReader(bytes.NewReader(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return err
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
gzr, err := gzip.NewReader(file)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
defer gzr.Close()
|
defer gzr.Close()
|
||||||
|
|
||||||
tr := tar.NewReader(gzr)
|
tr := tar.NewReader(gzr)
|
||||||
|
|
||||||
rootDir := ""
|
_ = gzr.Reset(bytes.NewReader(data))
|
||||||
isSingleRoot := true
|
|
||||||
rootItemCount := 0
|
|
||||||
for {
|
|
||||||
header, err := tr.Next()
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(cleanTarPath(header.Name), string(os.PathSeparator))
|
|
||||||
if len(parts) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(parts) == 1 {
|
|
||||||
isDir := header.Typeflag == tar.TypeDir
|
|
||||||
if !isDir {
|
|
||||||
isSingleRoot = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if rootDir == "" {
|
|
||||||
rootDir = parts[0]
|
|
||||||
}
|
|
||||||
rootItemCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if rootItemCount != 1 {
|
|
||||||
isSingleRoot = false
|
|
||||||
}
|
|
||||||
|
|
||||||
file.Seek(0, 0)
|
|
||||||
gzr, _ = gzip.NewReader(file)
|
|
||||||
tr = tar.NewReader(gzr)
|
tr = tar.NewReader(gzr)
|
||||||
|
|
||||||
var extractedFolder string
|
|
||||||
if isSingleRoot && rootDir != "" {
|
|
||||||
log.Debugln("Match the singleRoot")
|
|
||||||
extractedFolder = filepath.Join(dest, rootDir)
|
|
||||||
log.Debugln("extractedFolder: %s", extractedFolder)
|
|
||||||
} else {
|
|
||||||
log.Debugln("Match the multiRoot")
|
|
||||||
baseName := filepath.Base(src)
|
|
||||||
baseName = strings.TrimSuffix(baseName, filepath.Ext(baseName))
|
|
||||||
baseName = strings.TrimSuffix(baseName, ".tar")
|
|
||||||
extractedFolder = filepath.Join(dest, baseName)
|
|
||||||
|
|
||||||
for i := 1; ; i++ {
|
|
||||||
if _, err := os.Stat(extractedFolder); os.IsNotExist(err) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
extractedFolder = filepath.Join(dest, fmt.Sprintf("%s_%d", baseName, i))
|
|
||||||
}
|
|
||||||
log.Debugln("extractedFolder: %s", extractedFolder)
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
header, err := tr.Next()
|
header, err := tr.Next()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var fpath string
|
fpath := filepath.Join(dest, header.Name)
|
||||||
if isSingleRoot && rootDir != "" {
|
|
||||||
fpath = filepath.Join(dest, cleanTarPath(header.Name))
|
|
||||||
} else {
|
|
||||||
fpath = filepath.Join(extractedFolder, cleanTarPath(header.Name))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
|
if !inDest(fpath, dest) {
|
||||||
return "", fmt.Errorf("invalid file path: %s", fpath)
|
return fmt.Errorf("invalid file path: %s", fpath)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch header.Typeflag {
|
switch header.Typeflag {
|
||||||
case tar.TypeDir:
|
case tar.TypeDir:
|
||||||
if err = os.MkdirAll(fpath, os.FileMode(header.Mode)); err != nil {
|
if err = os.MkdirAll(fpath, os.FileMode(header.Mode)); err != nil {
|
||||||
return "", err
|
return err
|
||||||
}
|
}
|
||||||
case tar.TypeReg:
|
case tar.TypeReg:
|
||||||
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
||||||
return "", err
|
return err
|
||||||
}
|
}
|
||||||
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode))
|
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := io.Copy(outFile, tr); err != nil {
|
if _, err := io.Copy(outFile, tr); err != nil {
|
||||||
outFile.Close()
|
outFile.Close()
|
||||||
return "", err
|
return err
|
||||||
}
|
}
|
||||||
outFile.Close()
|
outFile.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return extractedFolder, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func extract(src, dest string) (string, error) {
|
func extract(data []byte, dest string) error {
|
||||||
srcLower := strings.ToLower(src)
|
fileType := detectFileType(data)
|
||||||
switch {
|
log.Debugln("compression Type: %s", fileType)
|
||||||
case strings.HasSuffix(srcLower, ".tar.gz") ||
|
switch fileType {
|
||||||
strings.HasSuffix(srcLower, ".tgz"):
|
case typeZip:
|
||||||
return untgz(src, dest)
|
return unzip(data, dest)
|
||||||
case strings.HasSuffix(srcLower, ".zip"):
|
case typeTarGzip:
|
||||||
return unzip(src, dest)
|
return untgz(data, dest)
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("unsupported file format: %s", src)
|
return fmt.Errorf("unknown or unsupported file type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,22 +281,49 @@ func cleanTarPath(path string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cleanup(root string) error {
|
func cleanup(root string) error {
|
||||||
if _, err := os.Stat(root); os.IsNotExist(err) {
|
dirEntryList, err := os.ReadDir(root)
|
||||||
return nil
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
||||||
|
for _, dirEntry := range dirEntryList {
|
||||||
|
err = os.RemoveAll(filepath.Join(root, dirEntry.Name()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if info.IsDir() {
|
}
|
||||||
if err := os.RemoveAll(path); err != nil {
|
return nil
|
||||||
return err
|
}
|
||||||
}
|
|
||||||
} else {
|
func moveDir(src string, dst string) error {
|
||||||
if err := os.Remove(path); err != nil {
|
dirEntryList, err := os.ReadDir(src)
|
||||||
return err
|
if err != nil {
|
||||||
}
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
})
|
if len(dirEntryList) == 1 && dirEntryList[0].IsDir() {
|
||||||
|
src = filepath.Join(src, dirEntryList[0].Name())
|
||||||
|
log.Debugln("match the singleRoot: %s", src)
|
||||||
|
dirEntryList, err = os.ReadDir(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dirEntry := range dirEntryList {
|
||||||
|
err = os.Rename(filepath.Join(src, dirEntry.Name()), filepath.Join(dst, dirEntry.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func inDest(fpath, dest string) bool {
|
||||||
|
if rel, err := filepath.Rel(dest, fpath); err == nil {
|
||||||
|
if filepath.IsLocal(rel) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
_ "unsafe"
|
_ "unsafe"
|
||||||
@@ -174,6 +175,7 @@ type Profile struct {
|
|||||||
type TLS struct {
|
type TLS struct {
|
||||||
Certificate string
|
Certificate string
|
||||||
PrivateKey string
|
PrivateKey string
|
||||||
|
EchKey string
|
||||||
CustomTrustCert []string
|
CustomTrustCert []string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,6 +270,7 @@ type RawTun struct {
|
|||||||
AutoRedirect bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"`
|
AutoRedirect bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"`
|
||||||
AutoRedirectInputMark uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"`
|
AutoRedirectInputMark uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"`
|
||||||
AutoRedirectOutputMark uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"`
|
AutoRedirectOutputMark uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"`
|
||||||
|
LoopbackAddress []netip.Addr `yaml:"loopback-address" json:"loopback-address,omitempty"`
|
||||||
StrictRoute bool `yaml:"strict-route" json:"strict-route,omitempty"`
|
StrictRoute bool `yaml:"strict-route" json:"strict-route,omitempty"`
|
||||||
RouteAddress []netip.Prefix `yaml:"route-address" json:"route-address,omitempty"`
|
RouteAddress []netip.Prefix `yaml:"route-address" json:"route-address,omitempty"`
|
||||||
RouteAddressSet []string `yaml:"route-address-set" json:"route-address-set,omitempty"`
|
RouteAddressSet []string `yaml:"route-address-set" json:"route-address-set,omitempty"`
|
||||||
@@ -360,6 +363,7 @@ type RawSniffingConfig struct {
|
|||||||
type RawTLS struct {
|
type RawTLS struct {
|
||||||
Certificate string `yaml:"certificate" json:"certificate"`
|
Certificate string `yaml:"certificate" json:"certificate"`
|
||||||
PrivateKey string `yaml:"private-key" json:"private-key"`
|
PrivateKey string `yaml:"private-key" json:"private-key"`
|
||||||
|
EchKey string `yaml:"ech-key" json:"ech-key"`
|
||||||
CustomTrustCert []string `yaml:"custom-certifactes" json:"custom-certifactes"`
|
CustomTrustCert []string `yaml:"custom-certifactes" json:"custom-certifactes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -754,6 +758,12 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseController(cfg *RawConfig) (*Controller, error) {
|
func parseController(cfg *RawConfig) (*Controller, error) {
|
||||||
|
if path := cfg.ExternalUI; path != "" && !C.Path.IsSafePath(path) {
|
||||||
|
return nil, C.Path.ErrNotSafePath(path)
|
||||||
|
}
|
||||||
|
if uiName := cfg.ExternalUIName; uiName != "" && !filepath.IsLocal(uiName) {
|
||||||
|
return nil, fmt.Errorf("external UI name is not local: %s", uiName)
|
||||||
|
}
|
||||||
return &Controller{
|
return &Controller{
|
||||||
ExternalController: cfg.ExternalController,
|
ExternalController: cfg.ExternalController,
|
||||||
ExternalUI: cfg.ExternalUI,
|
ExternalUI: cfg.ExternalUI,
|
||||||
@@ -811,6 +821,7 @@ func parseTLS(cfg *RawConfig) (*TLS, error) {
|
|||||||
return &TLS{
|
return &TLS{
|
||||||
Certificate: cfg.TLS.Certificate,
|
Certificate: cfg.TLS.Certificate,
|
||||||
PrivateKey: cfg.TLS.PrivateKey,
|
PrivateKey: cfg.TLS.PrivateKey,
|
||||||
|
EchKey: cfg.TLS.EchKey,
|
||||||
CustomTrustCert: cfg.TLS.CustomTrustCert,
|
CustomTrustCert: cfg.TLS.CustomTrustCert,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -1158,10 +1169,19 @@ func parseNameServer(servers []string, respectRules bool, preferH3 bool) ([]dns.
|
|||||||
return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error())
|
return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyName := u.Fragment
|
var proxyName string
|
||||||
|
params := map[string]string{}
|
||||||
|
for _, s := range strings.Split(u.Fragment, "&") {
|
||||||
|
arr := strings.SplitN(s, "=", 2)
|
||||||
|
switch len(arr) {
|
||||||
|
case 1:
|
||||||
|
proxyName = arr[0]
|
||||||
|
case 2:
|
||||||
|
params[arr[0]] = arr[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var addr, dnsNetType string
|
var addr, dnsNetType string
|
||||||
params := map[string]string{}
|
|
||||||
switch u.Scheme {
|
switch u.Scheme {
|
||||||
case "udp":
|
case "udp":
|
||||||
addr, err = hostWithDefaultPort(u.Host, "53")
|
addr, err = hostWithDefaultPort(u.Host, "53")
|
||||||
@@ -1179,23 +1199,8 @@ func parseNameServer(servers []string, respectRules bool, preferH3 bool) ([]dns.
|
|||||||
addr, err = hostWithDefaultPort(u.Host, "80")
|
addr, err = hostWithDefaultPort(u.Host, "80")
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
proxyName = ""
|
|
||||||
clearURL := url.URL{Scheme: u.Scheme, Host: addr, Path: u.Path, User: u.User}
|
clearURL := url.URL{Scheme: u.Scheme, Host: addr, Path: u.Path, User: u.User}
|
||||||
addr = clearURL.String()
|
addr = clearURL.String()
|
||||||
if len(u.Fragment) != 0 {
|
|
||||||
for _, s := range strings.Split(u.Fragment, "&") {
|
|
||||||
arr := strings.Split(s, "=")
|
|
||||||
if len(arr) == 0 {
|
|
||||||
continue
|
|
||||||
} else if len(arr) == 1 {
|
|
||||||
proxyName = arr[0]
|
|
||||||
} else if len(arr) == 2 {
|
|
||||||
params[arr[0]] = arr[1]
|
|
||||||
} else {
|
|
||||||
params[arr[0]] = strings.Join(arr[1:], "=")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case "quic":
|
case "quic":
|
||||||
addr, err = hostWithDefaultPort(u.Host, "853")
|
addr, err = hostWithDefaultPort(u.Host, "853")
|
||||||
@@ -1553,6 +1558,7 @@ func parseTun(rawTun RawTun, general *General) error {
|
|||||||
AutoRedirect: rawTun.AutoRedirect,
|
AutoRedirect: rawTun.AutoRedirect,
|
||||||
AutoRedirectInputMark: rawTun.AutoRedirectInputMark,
|
AutoRedirectInputMark: rawTun.AutoRedirectInputMark,
|
||||||
AutoRedirectOutputMark: rawTun.AutoRedirectOutputMark,
|
AutoRedirectOutputMark: rawTun.AutoRedirectOutputMark,
|
||||||
|
LoopbackAddress: rawTun.LoopbackAddress,
|
||||||
StrictRoute: rawTun.StrictRoute,
|
StrictRoute: rawTun.StrictRoute,
|
||||||
RouteAddress: rawTun.RouteAddress,
|
RouteAddress: rawTun.RouteAddress,
|
||||||
RouteAddressSet: rawTun.RouteAddressSet,
|
RouteAddressSet: rawTun.RouteAddressSet,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/adapter/outboundgroup"
|
"github.com/metacubex/mihomo/adapter/outboundgroup"
|
||||||
@@ -150,6 +152,9 @@ func proxyGroupsDagSort(groupsConfig []map[string]any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func verifyIP6() bool {
|
func verifyIP6() bool {
|
||||||
|
if skip, _ := strconv.ParseBool(os.Getenv("SKIP_SYSTEM_IPV6_CHECK")); skip {
|
||||||
|
return true
|
||||||
|
}
|
||||||
if iAddrs, err := net.InterfaceAddrs(); err == nil {
|
if iAddrs, err := net.InterfaceAddrs(); err == nil {
|
||||||
for _, addr := range iAddrs {
|
for _, addr := range iAddrs {
|
||||||
if prefix, err := netip.ParsePrefix(addr.String()); err == nil {
|
if prefix, err := netip.ParsePrefix(addr.String()); err == nil {
|
||||||
|
|||||||
@@ -92,8 +92,7 @@ type Conn interface {
|
|||||||
type PacketConn interface {
|
type PacketConn interface {
|
||||||
N.EnhancePacketConn
|
N.EnhancePacketConn
|
||||||
Connection
|
Connection
|
||||||
// Deprecate WriteWithMetadata because of remote resolve DNS cause TURN failed
|
ResolveUDP(ctx context.Context, metadata *Metadata) error
|
||||||
// WriteWithMetadata(p []byte, metadata *Metadata) (n int, err error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Dialer interface {
|
type Dialer interface {
|
||||||
@@ -134,8 +133,8 @@ type ProxyAdapter interface {
|
|||||||
|
|
||||||
// DialContext return a C.Conn with protocol which
|
// DialContext return a C.Conn with protocol which
|
||||||
// contains multiplexing-related reuse logic (if any)
|
// contains multiplexing-related reuse logic (if any)
|
||||||
DialContext(ctx context.Context, metadata *Metadata, opts ...dialer.Option) (Conn, error)
|
DialContext(ctx context.Context, metadata *Metadata) (Conn, error)
|
||||||
ListenPacketContext(ctx context.Context, metadata *Metadata, opts ...dialer.Option) (PacketConn, error)
|
ListenPacketContext(ctx context.Context, metadata *Metadata) (PacketConn, error)
|
||||||
|
|
||||||
// SupportUOT return UDP over TCP support
|
// SupportUOT return UDP over TCP support
|
||||||
SupportUOT() bool
|
SupportUOT() bool
|
||||||
@@ -319,10 +318,15 @@ type PacketSender interface {
|
|||||||
Send(PacketAdapter)
|
Send(PacketAdapter)
|
||||||
// Process is a blocking loop to send PacketAdapter to PacketConn and update the WriteBackProxy
|
// Process is a blocking loop to send PacketAdapter to PacketConn and update the WriteBackProxy
|
||||||
Process(PacketConn, WriteBackProxy)
|
Process(PacketConn, WriteBackProxy)
|
||||||
// ResolveUDP do a local resolve UDP dns blocking if metadata is not resolved
|
|
||||||
ResolveUDP(*Metadata) error
|
|
||||||
// Close stop the Process loop
|
// Close stop the Process loop
|
||||||
Close()
|
Close()
|
||||||
|
// DoSniff will blocking after sniffer work done
|
||||||
|
DoSniff(*Metadata) error
|
||||||
|
// AddMapping add a destination NAT record
|
||||||
|
AddMapping(originMetadata *Metadata, metadata *Metadata)
|
||||||
|
// RestoreReadFrom restore destination NAT for ReadFrom
|
||||||
|
// the implement must ensure returned netip.Add is valid (or just return input addr)
|
||||||
|
RestoreReadFrom(addr netip.Addr) netip.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
type NatTable interface {
|
type NatTable interface {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package constant
|
package constant
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -22,44 +21,6 @@ const (
|
|||||||
|
|
||||||
type DNSMode int
|
type DNSMode int
|
||||||
|
|
||||||
// UnmarshalYAML unserialize EnhancedMode with yaml
|
|
||||||
func (e *DNSMode) UnmarshalYAML(unmarshal func(any) error) error {
|
|
||||||
var tp string
|
|
||||||
if err := unmarshal(&tp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
mode, exist := DNSModeMapping[strings.ToLower(tp)]
|
|
||||||
if !exist {
|
|
||||||
return errors.New("invalid mode")
|
|
||||||
}
|
|
||||||
*e = mode
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalYAML serialize EnhancedMode with yaml
|
|
||||||
func (e DNSMode) MarshalYAML() (any, error) {
|
|
||||||
return e.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON unserialize EnhancedMode with json
|
|
||||||
func (e *DNSMode) UnmarshalJSON(data []byte) error {
|
|
||||||
var tp string
|
|
||||||
if err := json.Unmarshal(data, &tp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
mode, exist := DNSModeMapping[strings.ToLower(tp)]
|
|
||||||
if !exist {
|
|
||||||
return errors.New("invalid mode")
|
|
||||||
}
|
|
||||||
*e = mode
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON serialize EnhancedMode with json
|
|
||||||
func (e DNSMode) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(e.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalText unserialize EnhancedMode
|
// UnmarshalText unserialize EnhancedMode
|
||||||
func (e *DNSMode) UnmarshalText(data []byte) error {
|
func (e *DNSMode) UnmarshalText(data []byte) error {
|
||||||
mode, exist := DNSModeMapping[strings.ToLower(string(data))]
|
mode, exist := DNSModeMapping[strings.ToLower(string(data))]
|
||||||
@@ -157,40 +118,6 @@ func (e FilterMode) String() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e FilterMode) MarshalYAML() (interface{}, error) {
|
|
||||||
return e.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *FilterMode) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
var tp string
|
|
||||||
if err := unmarshal(&tp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
mode, exist := FilterModeMapping[strings.ToLower(tp)]
|
|
||||||
if !exist {
|
|
||||||
return errors.New("invalid mode")
|
|
||||||
}
|
|
||||||
*e = mode
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e FilterMode) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(e.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *FilterMode) UnmarshalJSON(data []byte) error {
|
|
||||||
var tp string
|
|
||||||
if err := json.Unmarshal(data, &tp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
mode, exist := FilterModeMapping[strings.ToLower(tp)]
|
|
||||||
if !exist {
|
|
||||||
return errors.New("invalid mode")
|
|
||||||
}
|
|
||||||
*e = mode
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e FilterMode) MarshalText() ([]byte, error) {
|
func (e FilterMode) MarshalText() ([]byte, error) {
|
||||||
return []byte(e.String()), nil
|
return []byte(e.String()), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -261,6 +261,11 @@ func (m *Metadata) Pure() *Metadata {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Metadata) Clone() *Metadata {
|
||||||
|
copyM := *m
|
||||||
|
return ©M
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Metadata) AddrPort() netip.AddrPort {
|
func (m *Metadata) AddrPort() netip.AddrPort {
|
||||||
return netip.AddrPortFrom(m.DstIP.Unmap(), m.DstPort)
|
return netip.AddrPortFrom(m.DstIP.Unmap(), m.DstPort)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package constant
|
package constant
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
P "path"
|
P "path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -37,13 +38,23 @@ var Path = func() *path {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &path{homeDir: homeDir, configFile: "config.yaml", allowUnsafePath: allowUnsafePath}
|
var safePaths []string
|
||||||
|
for _, safePath := range filepath.SplitList(os.Getenv("SAFE_PATHS")) {
|
||||||
|
safePath = strings.TrimSpace(safePath)
|
||||||
|
if len(safePath) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
safePaths = append(safePaths, safePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &path{homeDir: homeDir, configFile: "config.yaml", allowUnsafePath: allowUnsafePath, safePaths: safePaths}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
type path struct {
|
type path struct {
|
||||||
homeDir string
|
homeDir string
|
||||||
configFile string
|
configFile string
|
||||||
allowUnsafePath bool
|
allowUnsafePath bool
|
||||||
|
safePaths []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetHomeDir is used to set the configuration path
|
// SetHomeDir is used to set the configuration path
|
||||||
@@ -72,19 +83,37 @@ func (p *path) Resolve(path string) string {
|
|||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSafePath return true if path is a subpath of homedir
|
// IsSafePath return true if path is a subpath of homedir (or in the SAFE_PATHS environment variable)
|
||||||
func (p *path) IsSafePath(path string) bool {
|
func (p *path) IsSafePath(path string) bool {
|
||||||
if p.allowUnsafePath || features.CMFA {
|
if p.allowUnsafePath || features.CMFA {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
homedir := p.HomeDir()
|
|
||||||
path = p.Resolve(path)
|
path = p.Resolve(path)
|
||||||
rel, err := filepath.Rel(homedir, path)
|
for _, safePath := range p.SafePaths() {
|
||||||
if err != nil {
|
if rel, err := filepath.Rel(safePath, path); err == nil {
|
||||||
return false
|
if filepath.IsLocal(rel) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return !strings.Contains(rel, "..")
|
func (p *path) SafePaths() []string {
|
||||||
|
return append([]string{p.homeDir}, p.safePaths...) // add homedir to safePaths
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *path) ErrNotSafePath(path string) error {
|
||||||
|
return ErrNotSafePath{Path: path, SafePaths: p.SafePaths()}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrNotSafePath struct {
|
||||||
|
Path string
|
||||||
|
SafePaths []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrNotSafePath) Error() string {
|
||||||
|
return fmt.Sprintf("path is not subpath of home directory or SAFE_PATHS: %s \n allowed paths: %s", e.Path, e.SafePaths)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *path) GetPathByHash(prefix, name string) string {
|
func (p *path) GetPathByHash(prefix, name string) string {
|
||||||
|
|||||||
41
constant/path_test.go
Normal file
41
constant/path_test.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPath(t *testing.T) {
|
||||||
|
assert.False(t, (&path{}).IsSafePath("/usr/share/metacubexd/"))
|
||||||
|
assert.True(t, (&path{
|
||||||
|
safePaths: []string{"/usr/share/metacubexd"},
|
||||||
|
}).IsSafePath("/usr/share/metacubexd/"))
|
||||||
|
|
||||||
|
assert.False(t, (&path{}).IsSafePath("../metacubexd/"))
|
||||||
|
assert.True(t, (&path{
|
||||||
|
homeDir: "/usr/share/mihomo",
|
||||||
|
safePaths: []string{"/usr/share/metacubexd"},
|
||||||
|
}).IsSafePath("../metacubexd/"))
|
||||||
|
assert.False(t, (&path{
|
||||||
|
homeDir: "/usr/share/mihomo",
|
||||||
|
safePaths: []string{"/usr/share/ycad"},
|
||||||
|
}).IsSafePath("../metacubexd/"))
|
||||||
|
|
||||||
|
assert.False(t, (&path{}).IsSafePath("/opt/mykeys/key1.key"))
|
||||||
|
assert.True(t, (&path{
|
||||||
|
safePaths: []string{"/opt/mykeys"},
|
||||||
|
}).IsSafePath("/opt/mykeys/key1.key"))
|
||||||
|
assert.True(t, (&path{
|
||||||
|
safePaths: []string{"/opt/mykeys/"},
|
||||||
|
}).IsSafePath("/opt/mykeys/key1.key"))
|
||||||
|
assert.True(t, (&path{
|
||||||
|
safePaths: []string{"/opt/mykeys/key1.key"},
|
||||||
|
}).IsSafePath("/opt/mykeys/key1.key"))
|
||||||
|
|
||||||
|
assert.True(t, (&path{}).IsSafePath("key1.key"))
|
||||||
|
assert.True(t, (&path{}).IsSafePath("./key1.key"))
|
||||||
|
assert.True(t, (&path{}).IsSafePath("./mykey/key1.key"))
|
||||||
|
assert.True(t, (&path{}).IsSafePath("./mykey/../key1.key"))
|
||||||
|
assert.False(t, (&path{}).IsSafePath("./mykey/../../key1.key"))
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user