Compare commits

..

62 Commits

Author SHA1 Message Date
wwqgtxx
fd959feff2 chore: update dependencies 2025-05-21 21:37:20 +08:00
wwqgtxx
d5a03901d2 fix: race in close grpc transport 2025-05-20 16:15:04 +08:00
wwqgtxx
257fead538 docs: update config.yaml follow 5cf0f18c 2025-05-20 11:08:42 +08:00
wwqgtxx
c489c5260b fix: hysteria2 hop ports init
https://github.com/MetaCubeX/mihomo/issues/2056
2025-05-20 10:56:14 +08:00
wwqgtxx
8f92b1de13 chore: simplify the single root decompression process 2025-05-20 09:48:05 +08:00
wwqgtxx
9f7a2a36c1 chore: unpack externalUI in a separate temporary directory to avoid malicious compressed packages from polluting workdir 2025-05-20 01:58:25 +08:00
wwqgtxx
a93479124c chore: stricter path checking when unpacking zip/tgz 2025-05-20 00:00:30 +08:00
wwqgtxx
ed42c4feb8 chore: disallow symlink in unzip 2025-05-19 23:42:39 +08:00
wwqgtxx
608ddb1b44 fix: external-ui-name must in local 2025-05-19 23:11:52 +08:00
wwqgtxx
d036d98128 fix: http server does not handle http2 logic correctly 2025-05-18 23:05:00 +08:00
wwqgtxx
d900c71214 fix: shadowtls v2 not work with X25519MLKEM768 2025-05-18 23:03:07 +08:00
wwqgtxx
1672750c47 chore: simplifying the old fingerprint processing method 2025-05-18 23:03:07 +08:00
wwqgtxx
41b57afb3f fix: grpc deadline implement 2025-05-18 23:03:07 +08:00
wwqgtxx
188372cb04 feat: add tls.ech-key for external-controller-tls 2025-05-17 21:21:02 +08:00
wwqgtxx
a1350d4985 feat: add ech-key for listeners 2025-05-17 20:50:21 +08:00
wwqgtxx
dc958e6a39 feat: add ech-opts for hysteria/hysteria2/tuic outbound 2025-05-17 18:41:39 +08:00
wwqgtxx
8a5f3b8909 chore: simplify port hop costs 2025-05-17 17:06:38 +08:00
wwqgtxx
c6d7ef8cb8 feat: add ech-opts for anytls/shadowsocks/trojan/vmess/vless outbound 2025-05-17 13:53:21 +08:00
wwqgtxx
bb8c47d83d fix: error typo 2025-05-15 18:07:55 +08:00
wwqgtxx
5cf0f18c29 feat: reality add support-x25519mlkem768, it only works with new version server 2025-05-15 14:54:43 +08:00
wwqgtxx
83213d493e chore: adjust min backoff from 1s to 10s 2025-05-14 21:51:18 +08:00
wwqgtxx
90ed01ed53 fix: backoff not reset when the file unchanged 2025-05-14 21:45:12 +08:00
wwqgtxx
f91a586da8 fix: inline proxy provider's healthcheck not work 2025-05-13 19:00:32 +08:00
wwqgtxx
266fb03838 chore: update dependencies 2025-05-13 12:09:38 +08:00
wwqgtxx
76e9607fd7 chore: move start healthcheck.process() from New to Initial in provider
avoid panic cause by build-in proxy have not set to tunnel
2025-05-13 01:12:06 +08:00
wwqgtxx
23e2d3a132 chore: rebuild provider load 2025-05-12 22:19:49 +08:00
wwqgtxx
6e35cf9399 fix: truncated UDP response in system dns
https://github.com/MetaCubeX/mihomo/issues/2031
2025-05-12 12:34:22 +08:00
wwqgtxx
2116640886 chore: the updateConfigs api also adds a check for SAFE_PATHS 2025-05-12 11:28:15 +08:00
wwqgtxx
a4fcd3af07 chore: rollback incompatible changes to updateConfigs api 2025-05-12 10:00:01 +08:00
wwqgtxx
d22a893060 fix: hysteria server port hopping compatibility issues 2025-05-11 11:44:42 +08:00
Anya Lin
00cceba890 docs: update config.yaml follow 7e7016b (#2022) 2025-05-10 13:12:45 +08:00
wwqgtxx
2b4726b9ad fix: build on go1.24.3
https://github.com/golang/go/issues/73617
2025-05-10 12:32:47 +08:00
xishang0128
26e6d83f8b chore: make select display the specified testUrl
for https://github.com/MetaCubeX/mihomo/issues/2013
2025-05-07 18:21:21 +08:00
wwqgtxx
50d7834e09 chore: change the separator of the SAFE_PATHS environment variable to the default separator of the operating system platform (i.e., ; in Windows and : in other systems) 2025-05-05 01:32:25 +08:00
wwqgtxx
86c127db8b fix: missing read waiter for cancelers 2025-05-04 11:18:42 +08:00
wwqgtxx
febb6021aa fix: hysteria2 inbound not set UDPTimeout 2025-05-04 11:18:42 +08:00
wwqgtxx
9e57b298bf chore: update dependencies 2025-05-03 15:06:13 +08:00
wwqgtxx
791ea5e568 chore: allow setting addition safePaths by environment variable SAFE_PATHS
package managers can allow for pre-defined safe paths without disabling the entire security check feature
for https://github.com/MetaCubeX/mihomo/issues/2004
2025-05-01 12:33:21 +08:00
wwqgtxx
7e7016b567 chore: removed routing-mark and interface-name of the group, please set it directly on the proxy instead 2025-05-01 02:13:35 +08:00
wwqgtxx
b4fe669848 chore: better path checks 2025-05-01 02:13:35 +08:00
wwqgtxx
cad26ac6a8 chore: fetcher will change duration to achieve fast retry when the update failed with a 2x factor step from 1s to interval 2025-04-30 17:28:06 +08:00
wwqgtxx
f328203bc1 feat: not inline proxy-provider can also set payload as fallback proxies when file/http parsing fails 2025-04-30 16:03:02 +08:00
wwqgtxx
5c40a6340c feat: not inline rule-provider can also set payload as fallback rules when file/http parsing fails 2025-04-30 14:09:15 +08:00
wwqgtxx
61d6a9abd6 fix: fetcher does not start the pull loop when local file parsing errors occur and the first remote update fails 2025-04-30 13:29:19 +08:00
wwqgtxx
a013ac32a3 chore: give better error messages for some stupid config files 2025-04-29 21:52:44 +08:00
wwqgtxx
ee5d77cfd1 chore: cleanup tls clientFingerprint code 2025-04-29 21:15:48 +08:00
wwqgtxx
936df90ace chore: update dependencies 2025-04-29 09:01:54 +08:00
Larvan2
f774276896 fix: ensure wait group completes 2025-04-28 03:07:21 +00:00
wwqgtxx
aa51b9faba chore: replace using internal batch package to x/sync/errgroup
In the original batch implementation, the Go() method will always start a new goroutine and then wait for the concurrency limit, which is unnecessary for the current code. x/sync/errgroup will block Go() until the concurrency limit is met, which can effectively reduce memory usage.
In addition, the original batch always saves the return value of Go(), but it is not used in the current code, which will also waste a lot of memory space in high concurrency scenarios.
2025-04-28 10:28:45 +08:00
wwqgtxx
d55b047125 chore: ignore interfaces not with FlagUp in local interface finding 2025-04-27 09:40:17 +08:00
xishang0128
efc7abc6e0 actions: fix pacman build 2025-04-25 12:36:28 +08:00
wwqgtxx
c2301f66a4 chore: rebuild fingerprint and keypair handle 2025-04-25 10:34:34 +08:00
WeidiDeng
468cfc3cc4 fix: set sni to servername if not specified for trojan outbound (#1991) 2025-04-24 19:50:16 +08:00
xishang0128
5dce957755 actions: improve build process 2025-04-24 19:17:32 +08:00
wwqgtxx
4ecb49b3b9 chore: dynamic fetch remoteAddr in hysteria2 service 2025-04-23 12:25:42 +08:00
wwqgtxx
7de4af28d2 fix: shadowtls test 2025-04-23 12:10:37 +08:00
wwqgtxx
48d8efb3e9 fix: do NOT reset the quic-go internal state when only port is different 2025-04-23 12:00:10 +08:00
wwqgtxx
e6e7aa5ae2 fix: alpn apply on shadowtls 2025-04-22 23:44:55 +08:00
wwqgtxx
99aa1b0de1 feat: inbound support shadow-tls 2025-04-22 21:16:56 +08:00
wwqgtxx
52ad793d11 fix: shadowtls v1 not work 2025-04-22 20:52:34 +08:00
wwqgtxx
2fb9331211 fix: some resources are not released in listener 2025-04-22 20:52:33 +08:00
wwqgtxx
793ce45db0 chore: update quic-go to 0.51.0 2025-04-21 22:58:08 +08:00
149 changed files with 2913 additions and 1467 deletions

18
.github/release/.fpm_systemd vendored Normal file
View 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
View 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

View File

@@ -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,16 @@ 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)"
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}" >> $GITHUB_ENV
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 +216,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 +234,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 +243,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 +258,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 +311,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

View File

@@ -17,7 +17,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 +62,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 +75,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
} }

View File

@@ -15,8 +15,8 @@ import (
"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 +28,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,10 +52,8 @@ 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) {
// 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
@@ -93,29 +90,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 +102,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
} }

View File

@@ -18,7 +18,7 @@ import (
type ProxyAdapter interface { type ProxyAdapter interface {
C.ProxyAdapter C.ProxyAdapter
DialOptions(opts ...dialer.Option) []dialer.Option DialOptions() []dialer.Option
} }
type Base struct { type Base struct {
@@ -59,7 +59,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 +69,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 +128,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))
} }
@@ -167,8 +167,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
} }
@@ -317,8 +317,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 +339,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
} }

View File

@@ -20,12 +20,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,7 +34,7 @@ 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
} }
@@ -45,7 +46,7 @@ func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
} }
metadata.DstIP = 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
} }

View File

@@ -7,7 +7,6 @@ import (
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,14 +22,14 @@ 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())
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())

28
adapter/outbound/ech.go Normal file
View File

@@ -0,0 +1,28 @@
package outbound
import (
"encoding/base64"
"fmt"
"github.com/metacubex/mihomo/component/ech"
)
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.EncryptedClientHelloConfigList = list
}
return echConfig, nil
}

View File

@@ -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

View File

@@ -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,20 @@ 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...)) 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 +83,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 +105,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 +174,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 +235,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 +253,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

View File

@@ -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,7 @@ 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...)
h.dialer.SetDialer(dialer.NewDialer(options...))
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 +104,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 +154,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 +173,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 +181,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 +228,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
} }

View File

@@ -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,8 @@ 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.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 +76,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 +85,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)

View File

@@ -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)

View File

@@ -7,7 +7,6 @@ import (
"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 +20,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,7 +28,7 @@ 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) {
return newPacketConn(&nopPacketConn{}, r), nil return newPacketConn(&nopPacketConn{}, r), nil
} }

View File

@@ -20,9 +20,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 +64,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 +78,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 +86,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 +157,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 +183,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
@@ -302,6 +305,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 +333,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 +357,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{}

View File

@@ -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

View File

@@ -11,9 +11,9 @@ import (
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 +41,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,12 +49,10 @@ 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...)
s.dialer.SetDialer(dialer.NewDialer(options...))
// sing-mux use stream-oriented udp with a special address, so we need a net.UDPAddr // sing-mux use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() { if !metadata.Resolved() {
@@ -109,7 +105,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,

View File

@@ -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
@@ -207,7 +207,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 {

View File

@@ -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,8 +101,8 @@ 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 {

View File

@@ -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
} }

View File

@@ -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,11 @@ 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) {
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 +239,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
@@ -295,6 +300,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 +326,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 +348,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 +375,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{

View File

@@ -12,21 +12,26 @@ 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" "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 +52,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,8 +85,8 @@ 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
@@ -134,6 +140,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 +258,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 +283,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 +302,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 +322,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,

View File

@@ -17,6 +17,7 @@ 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" "github.com/metacubex/mihomo/component/resolver"
tlsC "github.com/metacubex/mihomo/component/tls" tlsC "github.com/metacubex/mihomo/component/tls"
@@ -27,7 +28,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 +47,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 +64,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 +91,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 +155,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 +210,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 +230,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 +249,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,7 +276,7 @@ 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 // vless use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() { if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host) ip, err := resolver.ResolveIP(ctx, metadata.Host)
@@ -282,7 +287,7 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
} }
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 +303,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
@@ -563,6 +568,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 +581,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 +621,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

View File

@@ -15,6 +15,7 @@ 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" "github.com/metacubex/mihomo/component/resolver"
tlsC "github.com/metacubex/mihomo/component/tls" tlsC "github.com/metacubex/mihomo/component/tls"
@@ -25,7 +26,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 +42,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 +60,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 +112,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 +150,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 +200,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 +210,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 +286,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 +305,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,7 +329,7 @@ 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 // vmess use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() { if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host) ip, err := resolver.ResolveIP(ctx, metadata.Host)
@@ -334,7 +340,7 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
} }
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 +355,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
@@ -474,6 +480,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 +493,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 +533,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

View File

@@ -26,9 +26,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 +166,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 +489,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 +499,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,9 +515,7 @@ 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

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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{}

View File

@@ -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,

View File

@@ -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,
} }

View File

@@ -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,

View File

@@ -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
}) })
} }
} }

View File

@@ -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)
} }

View File

@@ -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,

View File

@@ -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

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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{}

90
common/net/listener.go Normal file
View 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 c, err := l.handle(l.ctx, c); err == nil {
l.conns <- c
} 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,
}
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
View 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
}
}

View File

@@ -1,6 +1,6 @@
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

View File

@@ -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 {

View 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
View 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
}

35
component/ech/ech.go Normal file
View File

@@ -0,0 +1,35 @@
package ech
import (
"context"
"fmt"
"github.com/metacubex/mihomo/component/resolver"
tlsC "github.com/metacubex/mihomo/component/tls"
)
type Config struct {
EncryptedClientHelloConfigList []byte
}
func (cfg *Config) ClientHandle(ctx context.Context, tlsConfig *tlsC.Config) (err error) {
if cfg == nil {
return nil
}
echConfigList := cfg.EncryptedClientHelloConfigList
if len(echConfigList) == 0 {
echConfigList, err = resolver.ResolveECH(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
View 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
}

View File

@@ -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)
} }
} }

View File

@@ -80,6 +80,9 @@ func getCache() (*ifaceCache, error) {
} }
cache.ifMap[iface.Name] = ifaceObj cache.ifMap[iface.Name] = ifaceObj
if iface.Flags&net.FlagUp == 0 {
continue // interface down
}
for _, prefix := range ipNets { for _, prefix := range ipNets {
cache.ifTable.Insert(prefix, ifaceObj) cache.ifTable.Insert(prefix, ifaceObj)
} }

View File

@@ -55,8 +55,8 @@ func (p proxyDialer) DialContext(ctx context.Context, network, address string) (
} }
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 +78,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)
} }

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -49,6 +49,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()
@@ -216,6 +217,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()

View File

@@ -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,
},
} }
} }

View File

@@ -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{

View File

@@ -0,0 +1,68 @@
package tls
import (
"context"
"net"
"net/http"
"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) {
log.Errorln("https server panic: %s", a)
})
}

View File

@@ -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) {

View File

@@ -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
} }

View File

@@ -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")

View File

@@ -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
} }

View File

@@ -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
} }
@@ -360,6 +362,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 +757,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 +820,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
} }

View File

@@ -134,8 +134,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

View File

@@ -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
View 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"))
}

View File

@@ -77,7 +77,7 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error)
msg, _, err := c.Client.ExchangeWithConn(m, dConn) msg, _, err := c.Client.ExchangeWithConn(m, dConn)
// Resolvers MUST resend queries over TCP if they receive a truncated UDP response (with TC=1 set)! // Resolvers MUST resend queries over TCP if they receive a truncated UDP response (with TC=1 set)!
if msg != nil && msg.Truncated && c.Client.Net == "" { if msg != nil && msg.Truncated && network == "udp" {
tcpClient := *c.Client // copy a client tcpClient := *c.Client // copy a client
tcpClient.Net = "tcp" tcpClient.Net = "tcp"
network = "tcp" network = "tcp"

View File

@@ -82,7 +82,7 @@ func (d *dhcpClient) resolve(ctx context.Context) ([]dnsClient, error) {
for _, item := range dns { for _, item := range dns {
nameserver = append(nameserver, NameServer{ nameserver = append(nameserver, NameServer{
Addr: net.JoinHostPort(item.String(), "53"), Addr: net.JoinHostPort(item.String(), "53"),
Interface: d.ifaceName, ProxyName: d.ifaceName,
}) })
} }

View File

@@ -17,8 +17,10 @@ import (
"time" "time"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
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"
"github.com/metacubex/quic-go" "github.com/metacubex/quic-go"
"github.com/metacubex/quic-go/http3" "github.com/metacubex/quic-go/http3"
D "github.com/miekg/dns" D "github.com/miekg/dns"
@@ -550,23 +552,23 @@ func (doh *dnsOverHTTPS) createTransportH3(
Dial: func( Dial: func(
ctx context.Context, ctx context.Context,
// Ignore the address and always connect to the one that we got // Ignore the address and always connect to the one that we got
// from the bootstrapper. // from the bootstrapper.
_ string, _ string,
tlsCfg *tls.Config, tlsCfg *tlsC.Config,
cfg *quic.Config, cfg *quic.Config,
) (c quic.EarlyConnection, err error) { ) (c quic.EarlyConnection, err error) {
return doh.dialQuic(ctx, addr, tlsCfg, cfg) return doh.dialQuic(ctx, addr, tlsCfg, cfg)
}, },
DisableCompression: true, DisableCompression: true,
TLSClientConfig: tlsConfig, TLSClientConfig: tlsC.UConfig(tlsConfig),
QUICConfig: doh.getQUICConfig(), QUICConfig: doh.getQUICConfig(),
} }
return &http3Transport{baseTransport: rt}, nil return &http3Transport{baseTransport: rt}, nil
} }
func (doh *dnsOverHTTPS) dialQuic(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { func (doh *dnsOverHTTPS) dialQuic(ctx context.Context, addr string, tlsCfg *tlsC.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
ip, port, err := net.SplitHostPort(addr) ip, port, err := net.SplitHostPort(addr)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -635,7 +637,7 @@ func (doh *dnsOverHTTPS) probeH3(
// Run probeQUIC and probeTLS in parallel and see which one is faster. // Run probeQUIC and probeTLS in parallel and see which one is faster.
chQuic := make(chan error, 1) chQuic := make(chan error, 1)
chTLS := make(chan error, 1) chTLS := make(chan error, 1)
go doh.probeQUIC(ctx, addr, probeTLSCfg, chQuic) go doh.probeQUIC(ctx, addr, tlsC.UConfig(probeTLSCfg), chQuic)
go doh.probeTLS(ctx, probeTLSCfg, chTLS) go doh.probeTLS(ctx, probeTLSCfg, chTLS)
select { select {
@@ -660,7 +662,7 @@ func (doh *dnsOverHTTPS) probeH3(
// probeQUIC attempts to establish a QUIC connection to the specified address. // probeQUIC attempts to establish a QUIC connection to the specified address.
// We run probeQUIC and probeTLS in parallel and see which one is faster. // We run probeQUIC and probeTLS in parallel and see which one is faster.
func (doh *dnsOverHTTPS) probeQUIC(ctx context.Context, addr string, tlsConfig *tls.Config, ch chan error) { func (doh *dnsOverHTTPS) probeQUIC(ctx context.Context, addr string, tlsConfig *tlsC.Config, ch chan error) {
startTime := time.Now() startTime := time.Now()
conn, err := doh.dialQuic(ctx, addr, tlsConfig, doh.getQUICConfig()) conn, err := doh.dialQuic(ctx, addr, tlsConfig, doh.getQUICConfig())
if err != nil { if err != nil {

View File

@@ -13,10 +13,11 @@ import (
"time" "time"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
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"
"github.com/metacubex/quic-go"
"github.com/metacubex/quic-go"
D "github.com/miekg/dns" D "github.com/miekg/dns"
) )
@@ -338,7 +339,7 @@ func (doq *dnsOverQUIC) openConnection(ctx context.Context) (conn quic.Connectio
transport := quic.Transport{Conn: udp} transport := quic.Transport{Conn: udp}
transport.SetCreatedConn(true) // auto close conn transport.SetCreatedConn(true) // auto close conn
transport.SetSingleUse(true) // auto close transport transport.SetSingleUse(true) // auto close transport
conn, err = transport.Dial(ctx, &udpAddr, tlsConfig, doq.getQUICConfig()) conn, err = transport.Dial(ctx, &udpAddr, tlsC.UConfig(tlsConfig), doq.getQUICConfig())
if err != nil { if err != nil {
return nil, fmt.Errorf("opening quic connection to %s: %w", doq.addr, err) return nil, fmt.Errorf("opening quic connection to %s: %w", doq.addr, err)
} }

View File

@@ -127,6 +127,28 @@ func (r *Resolver) shouldIPFallback(ip netip.Addr) bool {
return false return false
} }
func (r *Resolver) ResolveECH(ctx context.Context, host string) ([]byte, error) {
query := &D.Msg{}
query.SetQuestion(D.Fqdn(host), D.TypeHTTPS)
msg, err := r.ExchangeContext(ctx, query)
if err != nil {
return nil, err
}
for _, rr := range msg.Answer {
switch resource := rr.(type) {
case *D.HTTPS:
for _, value := range resource.Value {
if echConfig, ok := value.(*D.SVCBECHConfig); ok {
return echConfig.ECH, nil
}
}
}
}
return nil, errors.New("no ECH config found in DNS records")
}
// ExchangeContext a batch of dns request with context.Context, and it use cache // ExchangeContext a batch of dns request with context.Context, and it use cache
func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
if len(m.Question) == 0 { if len(m.Question) == 0 {
@@ -393,7 +415,6 @@ func (r *Resolver) ResetConnection() {
type NameServer struct { type NameServer struct {
Net string Net string
Addr string Addr string
Interface string
ProxyAdapter C.ProxyAdapter ProxyAdapter C.ProxyAdapter
ProxyName string ProxyName string
Params map[string]string Params map[string]string
@@ -407,7 +428,6 @@ func (ns NameServer) Equal(ns2 NameServer) bool {
}() }()
if ns.Net == ns2.Net && if ns.Net == ns2.Net &&
ns.Addr == ns2.Addr && ns.Addr == ns2.Addr &&
ns.Interface == ns2.Interface &&
ns.ProxyAdapter == ns2.ProxyAdapter && ns.ProxyAdapter == ns2.ProxyAdapter &&
ns.ProxyName == ns2.ProxyName && ns.ProxyName == ns2.ProxyName &&
maps.Equal(ns.Params, ns2.Params) && maps.Equal(ns.Params, ns2.Params) &&

View File

@@ -11,7 +11,6 @@ import (
"time" "time"
"github.com/metacubex/mihomo/common/picker" "github.com/metacubex/mihomo/common/picker"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
@@ -115,11 +114,6 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient {
continue continue
} }
var options []dialer.Option
if s.Interface != "" {
options = append(options, dialer.WithInterface(s.Interface))
}
host, port, _ := net.SplitHostPort(s.Addr) host, port, _ := net.SplitHostPort(s.Addr)
ret = append(ret, &client{ ret = append(ret, &client{
Client: &D.Client{ Client: &D.Client{
@@ -132,7 +126,7 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient {
}, },
port: port, port: port,
host: host, host: host,
dialer: newDNSDialer(resolver, s.ProxyAdapter, s.ProxyName, options...), dialer: newDNSDialer(resolver, s.ProxyAdapter, s.ProxyName),
}) })
} }
return ret return ret

View File

@@ -48,6 +48,13 @@ ipv6: true # 开启 IPv6 总开关,关闭阻断所有 IPv6 链接和屏蔽 DNS
tls: tls:
certificate: string # 证书 PEM 格式,或者 证书的路径 certificate: string # 证书 PEM 格式,或者 证书的路径
private-key: string # 证书对应的私钥 PEM 格式,或者私钥路径 private-key: string # 证书对应的私钥 PEM 格式,或者私钥路径
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
custom-certifactes: custom-certifactes:
- | - |
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
@@ -427,6 +434,10 @@ proxies: # socks5
# 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取
# 配置指纹将实现 SSL Pining 效果 # 配置指纹将实现 SSL Pining 效果
# fingerprint: xxxx # fingerprint: xxxx
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
# skip-cert-verify: true # skip-cert-verify: true
# host: bing.com # host: bing.com
# path: "/" # path: "/"
@@ -448,6 +459,7 @@ proxies: # socks5
host: "cloud.tencent.com" host: "cloud.tencent.com"
password: "shadow_tls_password" password: "shadow_tls_password"
version: 2 # support 1/2/3 version: 2 # support 1/2/3
# alpn: ["h2","http/1.1"]
- name: "ss5" - name: "ss5"
type: ss type: ss
@@ -526,6 +538,10 @@ proxies: # socks5
# skip-cert-verify: true # skip-cert-verify: true
# servername: example.com # priority over wss host # servername: example.com # priority over wss host
# network: ws # network: ws
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
# ws-opts: # ws-opts:
# path: /path # path: /path
# headers: # headers:
@@ -598,6 +614,10 @@ proxies: # socks5
# skip-cert-verify: true # skip-cert-verify: true
# fingerprint: xxxx # fingerprint: xxxx
# client-fingerprint: random # Available: "chrome","firefox","safari","random","none" # client-fingerprint: random # Available: "chrome","firefox","safari","random","none"
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
- name: "vless-vision" - name: "vless-vision"
type: vless type: vless
@@ -625,6 +645,7 @@ proxies: # socks5
reality-opts: reality-opts:
public-key: xxx public-key: xxx
short-id: xxx # optional short-id: xxx # optional
support-x25519mlkem768: false # 如果服务端支持可手动设置为true
client-fingerprint: chrome # cannot be empty client-fingerprint: chrome # cannot be empty
- name: "vless-reality-grpc" - name: "vless-reality-grpc"
@@ -644,6 +665,7 @@ proxies: # socks5
reality-opts: reality-opts:
public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE
short-id: 10f897e26c4b9478 short-id: 10f897e26c4b9478
support-x25519mlkem768: false # 如果服务端支持可手动设置为true
- name: "vless-ws" - name: "vless-ws"
type: vless type: vless
@@ -682,6 +704,10 @@ proxies: # socks5
# enabled: false # enabled: false
# method: aes-128-gcm # aes-128-gcm/aes-256-gcm/chacha20-ietf-poly1305 # method: aes-128-gcm # aes-128-gcm/aes-256-gcm/chacha20-ietf-poly1305
# password: "example" # password: "example"
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
- name: trojan-grpc - name: trojan-grpc
server: server server: server
@@ -739,6 +765,10 @@ proxies: # socks5
up: "30 Mbps" # 若不写单位,默认为 Mbps up: "30 Mbps" # 若不写单位,默认为 Mbps
down: "200 Mbps" # 若不写单位,默认为 Mbps down: "200 Mbps" # 若不写单位,默认为 Mbps
# sni: server.com # sni: server.com
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
# skip-cert-verify: false # skip-cert-verify: false
# recv-window-conn: 12582912 # recv-window-conn: 12582912
# recv-window: 52428800 # recv-window: 52428800
@@ -762,6 +792,10 @@ proxies: # socks5
# obfs: salamander # 默认为空,如果填写则开启 obfs目前仅支持 salamander # obfs: salamander # 默认为空,如果填写则开启 obfs目前仅支持 salamander
# obfs-password: yourpassword # obfs-password: yourpassword
# sni: server.com # sni: server.com
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
# skip-cert-verify: false # skip-cert-verify: false
# fingerprint: xxxx # fingerprint: xxxx
# alpn: # alpn:
@@ -837,6 +871,10 @@ proxies: # socks5
# skip-cert-verify: true # skip-cert-verify: true
# max-open-streams: 20 # default 100, too many open streams may hurt performance # max-open-streams: 20 # default 100, too many open streams may hurt performance
# sni: example.com # sni: example.com
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
# #
# meta 和 sing-box 私有扩展,将 ss-uot 用于 udp 中继,开启此选项后 udp-relay-mode 将失效 # meta 和 sing-box 私有扩展,将 ss-uot 用于 udp 中继,开启此选项后 udp-relay-mode 将失效
# 警告,与原版 tuic 不兼容!!! # 警告,与原版 tuic 不兼容!!!
@@ -905,6 +943,12 @@ proxies: # socks5
# dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理 # dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理
- name: "dns-out" - name: "dns-out"
type: dns type: dns
# 配置指定 interface-name 和 fwmark 的 DIRECT
- name: en1-direct
type: direct
interface-name: en1
routing-mark: 6667
proxy-groups: proxy-groups:
# 代理链,目前 relay 可以支持 udp 的只有 vmess/vless/trojan/ss/ssr/tuic # 代理链,目前 relay 可以支持 udp 的只有 vmess/vless/trojan/ss/ssr/tuic
# wireguard 目前不支持在 relay 中使用,请使用 proxy 中的 dialer-proxy 配置项 # wireguard 目前不支持在 relay 中使用,请使用 proxy 中的 dialer-proxy 配置项
@@ -961,14 +1005,6 @@ proxy-groups:
- vmess1 - vmess1
- auto - auto
# 配置指定 interface-name 和 fwmark 的 DIRECT
- name: en1
type: select
interface-name: en1
routing-mark: 6667
proxies:
- DIRECT
- name: UseProvider - name: UseProvider
type: select type: select
filter: "HK|TW" # 正则表达式,过滤 provider1 中节点名包含 HK 或 TW filter: "HK|TW" # 正则表达式,过滤 provider1 中节点名包含 HK 或 TW
@@ -1128,6 +1164,13 @@ listeners:
# 下面两项如果填写则开启 tls需要同时填写 # 下面两项如果填写则开启 tls需要同时填写
# certificate: ./server.crt # certificate: ./server.crt
# private-key: ./server.key # private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
- name: http-in-1 - name: http-in-1
type: http type: http
@@ -1141,6 +1184,13 @@ listeners:
# 下面两项如果填写则开启 tls需要同时填写 # 下面两项如果填写则开启 tls需要同时填写
# certificate: ./server.crt # certificate: ./server.crt
# private-key: ./server.key # private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
- name: mixed-in-1 - name: mixed-in-1
type: mixed # HTTP(S) 和 SOCKS 代理混合 type: mixed # HTTP(S) 和 SOCKS 代理混合
@@ -1155,6 +1205,13 @@ listeners:
# 下面两项如果填写则开启 tls需要同时填写 # 下面两项如果填写则开启 tls需要同时填写
# certificate: ./server.crt # certificate: ./server.crt
# private-key: ./server.key # private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
- name: reidr-in-1 - name: reidr-in-1
type: redir type: redir
@@ -1179,6 +1236,15 @@ listeners:
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错)
password: vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg= password: vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg=
cipher: 2022-blake3-aes-256-gcm cipher: 2022-blake3-aes-256-gcm
# shadow-tls:
# enable: false # 设置为true时开启
# version: 3 # 支持v1/v2/v3
# password: password # v2设置项
# users: # v3设置项
# - name: 1
# password: password
# handshake:
# dest: test.com:443
- name: vmess-in-1 - name: vmess-in-1
type: vmess type: vmess
@@ -1195,6 +1261,13 @@ listeners:
# 下面两项如果填写则开启 tls需要同时填写 # 下面两项如果填写则开启 tls需要同时填写
# certificate: ./server.crt # certificate: ./server.crt
# private-key: ./server.key # private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
# 如果填写reality-config则开启reality注意不可与certificate和private-key同时填写 # 如果填写reality-config则开启reality注意不可与certificate和private-key同时填写
# reality-config: # reality-config:
# dest: test.com:443 # dest: test.com:443
@@ -1217,6 +1290,13 @@ listeners:
# 00000000-0000-0000-0000-000000000001: PASSWORD_1 # 00000000-0000-0000-0000-000000000001: PASSWORD_1
# certificate: ./server.crt # certificate: ./server.crt
# private-key: ./server.key # private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
# congestion-controller: bbr # congestion-controller: bbr
# max-idle-time: 15000 # max-idle-time: 15000
# authentication-timeout: 1000 # authentication-timeout: 1000
@@ -1248,6 +1328,13 @@ listeners:
# 下面两项如果填写则开启 tls需要同时填写 # 下面两项如果填写则开启 tls需要同时填写
# certificate: ./server.crt # certificate: ./server.crt
# private-key: ./server.key # private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
# 如果填写reality-config则开启reality注意不可与certificate和private-key同时填写 # 如果填写reality-config则开启reality注意不可与certificate和private-key同时填写
reality-config: reality-config:
dest: test.com:443 dest: test.com:443
@@ -1268,6 +1355,13 @@ listeners:
# "certificate" and "private-key" are required # "certificate" and "private-key" are required
certificate: ./server.crt certificate: ./server.crt
private-key: ./server.key private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
# padding-scheme: "" # https://github.com/anytls/anytls-go/blob/main/docs/protocol.md#cmdupdatepaddingscheme # padding-scheme: "" # https://github.com/anytls/anytls-go/blob/main/docs/protocol.md#cmdupdatepaddingscheme
- name: trojan-in-1 - name: trojan-in-1
@@ -1284,6 +1378,13 @@ listeners:
# 下面两项如果填写则开启 tls需要同时填写 # 下面两项如果填写则开启 tls需要同时填写
certificate: ./server.crt certificate: ./server.crt
private-key: ./server.key private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
# 如果填写reality-config则开启reality注意不可与certificate和private-key同时填写 # 如果填写reality-config则开启reality注意不可与certificate和private-key同时填写
# reality-config: # reality-config:
# dest: test.com:443 # dest: test.com:443
@@ -1309,6 +1410,13 @@ listeners:
00000000-0000-0000-0000-000000000001: PASSWORD_1 00000000-0000-0000-0000-000000000001: PASSWORD_1
# certificate: ./server.crt # certificate: ./server.crt
# private-key: ./server.key # private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
## up 和 down 均不写或为 0 则使用 BBR 流控 ## up 和 down 均不写或为 0 则使用 BBR 流控
# up: "30 Mbps" # 若不写单位,默认为 Mbps # up: "30 Mbps" # 若不写单位,默认为 Mbps
# down: "200 Mbps" # 若不写单位,默认为 Mbps # down: "200 Mbps" # 若不写单位,默认为 Mbps

51
go.mod
View File

@@ -11,41 +11,41 @@ require (
github.com/go-chi/chi/v5 v5.2.1 github.com/go-chi/chi/v5 v5.2.1
github.com/go-chi/render v1.0.3 github.com/go-chi/render v1.0.3
github.com/gobwas/ws v1.4.0 github.com/gobwas/ws v1.4.0
github.com/gofrs/uuid/v5 v5.3.1 github.com/gofrs/uuid/v5 v5.3.2
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905
github.com/klauspost/compress v1.17.9 // lastest version compatible with golang1.20 github.com/klauspost/compress v1.17.9 // lastest version compatible with golang1.20
github.com/klauspost/cpuid/v2 v2.2.9 // lastest version compatible with golang1.20 github.com/klauspost/cpuid/v2 v2.2.9 // lastest version compatible with golang1.20
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
github.com/mdlayher/netlink v1.7.2 github.com/mdlayher/netlink v1.7.2
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab
github.com/metacubex/bart v0.19.0 github.com/metacubex/bart v0.20.5
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399
github.com/metacubex/chacha v0.1.2 github.com/metacubex/chacha v0.1.2
github.com/metacubex/fswatch v0.1.1
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 github.com/metacubex/quic-go v0.51.1-0.20250511032541-4e34341cf18b
github.com/metacubex/randv2 v0.2.0 github.com/metacubex/randv2 v0.2.0
github.com/metacubex/sing-quic v0.0.0-20250404030904-b2cc8aab562c github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7
github.com/metacubex/sing-shadowsocks v0.2.8 github.com/metacubex/sing-mux v0.3.2
github.com/metacubex/sing-shadowsocks2 v0.2.2 github.com/metacubex/sing-quic v0.0.0-20250520025433-6e556a6bef7a
github.com/metacubex/sing-shadowtls v0.0.0-20250412122235-0e9005731a63 github.com/metacubex/sing-shadowsocks v0.2.9
github.com/metacubex/sing-tun v0.4.6-0.20250412144348-c426cb167db5 github.com/metacubex/sing-shadowsocks2 v0.2.3
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 github.com/metacubex/sing-tun v0.4.6-0.20250503065609-efb9f0beb6f6
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 github.com/metacubex/sing-vmess v0.2.1
github.com/metacubex/utls v1.7.0-alpha.1 github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4
github.com/metacubex/utls v1.7.3
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181
github.com/miekg/dns v1.1.63 github.com/miekg/dns v1.1.63 // lastest version compatible with golang1.20
github.com/mroth/weightedrand/v2 v2.1.0 github.com/mroth/weightedrand/v2 v2.1.0
github.com/openacid/low v0.1.21 github.com/openacid/low v0.1.21
github.com/oschwald/maxminddb-golang v1.12.0 // lastest version compatible with golang1.20 github.com/oschwald/maxminddb-golang v1.12.0 // lastest version compatible with golang1.20
github.com/puzpuzpuz/xsync/v3 v3.5.1 github.com/puzpuzpuz/xsync/v3 v3.5.1
github.com/sagernet/cors v1.2.1 github.com/sagernet/cors v1.2.1
github.com/sagernet/fswatch v0.1.1
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a
github.com/sagernet/sing v0.5.2 github.com/samber/lo v1.50.0
github.com/sagernet/sing-mux v0.2.1
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
github.com/samber/lo v1.49.1
github.com/shirou/gopsutil/v4 v4.25.1 // lastest version compatible with golang1.20 github.com/shirou/gopsutil/v4 v4.25.1 // lastest version compatible with golang1.20
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
@@ -57,6 +57,7 @@ require (
golang.org/x/crypto v0.33.0 // lastest version compatible with golang1.20 golang.org/x/crypto v0.33.0 // lastest version compatible with golang1.20
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // lastest version compatible with golang1.20 golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // lastest version compatible with golang1.20
golang.org/x/net v0.35.0 // lastest version compatible with golang1.20 golang.org/x/net v0.35.0 // lastest version compatible with golang1.20
golang.org/x/sync v0.11.0 // lastest version compatible with golang1.20
golang.org/x/sys v0.30.0 // lastest version compatible with golang1.20 golang.org/x/sys v0.30.0 // lastest version compatible with golang1.20
google.golang.org/protobuf v1.34.2 // lastest version compatible with golang1.20 google.golang.org/protobuf v1.34.2 // lastest version compatible with golang1.20
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
@@ -64,19 +65,19 @@ require (
) )
require ( require (
github.com/RyuaNerin/go-krypto v1.2.4 // indirect github.com/RyuaNerin/go-krypto v1.3.0 // indirect
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
github.com/ajg/form v1.5.1 // indirect github.com/ajg/form v1.5.1 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect github.com/andybalholm/brotli v1.0.6 // indirect
github.com/buger/jsonparser v1.1.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect
github.com/cloudflare/circl v1.3.7 // indirect github.com/cloudflare/circl v1.3.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/ebitengine/purego v0.8.2 // indirect github.com/ebitengine/purego v0.8.3 // indirect
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect github.com/gaukas/godicttls v0.0.4 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
@@ -91,14 +92,13 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mdlayher/socket v0.4.1 // indirect github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 // indirect
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
@@ -111,10 +111,7 @@ require (
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
go.uber.org/mock v0.4.0 // indirect go.uber.org/mock v0.4.0 // indirect
golang.org/x/mod v0.20.0 // indirect golang.org/x/mod v0.20.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.7.0 // indirect golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.24.0 // indirect golang.org/x/tools v0.24.0 // indirect
) )
replace github.com/sagernet/sing => github.com/metacubex/sing v0.0.0-20250228041610-d94509dc612a

93
go.sum
View File

@@ -1,8 +1,8 @@
github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08= github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08=
github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY= github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY=
github.com/RyuaNerin/elliptic2 v1.0.0/go.mod h1:wWB8fWrJI/6EPJkyV/r1Rj0hxUgrusmqSj8JN6yNf/A= github.com/RyuaNerin/go-krypto v1.3.0 h1:smavTzSMAx8iuVlGb4pEwl9MD2qicqMzuXR2QWp2/Pg=
github.com/RyuaNerin/go-krypto v1.2.4 h1:mXuNdK6M317aPV0llW6Xpjbo4moOlPF7Yxz4tb4b4Go= github.com/RyuaNerin/go-krypto v1.3.0/go.mod h1:9R9TU936laAIqAmjcHo/LsaXYOZlymudOAxjaBf62UM=
github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8= github.com/RyuaNerin/testingutil v0.1.0 h1:IYT6JL57RV3U2ml3dLHZsVtPOP6yNK7WUVdzzlpNrss=
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok=
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
@@ -26,12 +26,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98= github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM= github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po=
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg= github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg=
@@ -39,8 +39,8 @@ github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBE
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4= github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA= github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok= github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
@@ -59,8 +59,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/gofrs/uuid/v5 v5.3.1 h1:aPx49MwJbekCzOyhZDjJVb0hx3A0KLjlbLx6p2gY0p0= github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
github.com/gofrs/uuid/v5 v5.3.1/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
@@ -97,40 +97,49 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4= github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI= github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY= github.com/metacubex/bart v0.20.5 h1:XkgLZ17QxfxkqKdGsojoM2Zu01mmHyyQSFzt2/calTM=
github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI= github.com/metacubex/bart v0.20.5/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig= github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro= github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
github.com/metacubex/chacha v0.1.2 h1:QulCq3eVm3TO6+4nVIWJtmSe7BT2GMrgVHuAoqRQnlc= github.com/metacubex/chacha v0.1.2 h1:QulCq3eVm3TO6+4nVIWJtmSe7BT2GMrgVHuAoqRQnlc=
github.com/metacubex/chacha v0.1.2/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8= github.com/metacubex/chacha v0.1.2/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU=
github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI= github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU= github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 h1:B+AP/Pj2/jBDS/kCYjz/x+0BCOKfd2VODYevyeIt+Ds= github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo=
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996/go.mod h1:ExVjGyEwTUjCFqx+5uxgV7MOoA3fZI+th4D40H35xmY= github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
github.com/metacubex/quic-go v0.51.1-0.20250511032541-4e34341cf18b h1:8oDU32eJ+RRhl1FodGgPfxQxtoBAiD9D40XG2XtU/SE=
github.com/metacubex/quic-go v0.51.1-0.20250511032541-4e34341cf18b/go.mod h1:9R1NOzCgTcWsdWvOMlmtMuF0uKzuOpsfvEf7U3I8zM0=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
github.com/metacubex/sing v0.0.0-20250228041610-d94509dc612a h1:xjPXdDTlIKq4U/KnKpoCtkxD03T8GimtQrvHy/3dN00= github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing v0.0.0-20250228041610-d94509dc612a/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7 h1:m4nSxvw46JEgxMzzmnXams+ebwabcry4Ydep/zNiesQ=
github.com/metacubex/sing-quic v0.0.0-20250404030904-b2cc8aab562c h1:OB3WmMA8YPJjE36RjD9X8xlrWGJ4orxbf2R/KAE28b0= github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing-quic v0.0.0-20250404030904-b2cc8aab562c/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8= github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw=
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4= github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw=
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0= github.com/metacubex/sing-quic v0.0.0-20250520025433-6e556a6bef7a h1:Ho73vGiB94LmtK5T+tKVwtCNEi/YiHmPjlqpHSAmAVs=
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo= github.com/metacubex/sing-quic v0.0.0-20250520025433-6e556a6bef7a/go.mod h1:JPTpf7fpnojsSuwRJExhSZSy63pVbp3VM39+zj+sAJM=
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q= github.com/metacubex/sing-shadowsocks v0.2.9 h1:2e++13WNN7EGjGtvrGLUzW1xrCdQbW2gIFpgw5GEw00=
github.com/metacubex/sing-shadowtls v0.0.0-20250412122235-0e9005731a63 h1:vy/8ZYYtWUXYnOnw/NF8ThG1W/RqM/h5rkun+OXZMH0= github.com/metacubex/sing-shadowsocks v0.2.9/go.mod h1:CJSEGO4FWQAWe+ZiLZxCweGdjRR60A61SIoVjdjQeBA=
github.com/metacubex/sing-shadowtls v0.0.0-20250412122235-0e9005731a63/go.mod h1:eDZ2JpkSkewGmUlCoLSn2MRFn1D0jKPIys/6aogFx7U= github.com/metacubex/sing-shadowsocks2 v0.2.3 h1:v3rNS/5Ywh0NIZ6VU/NmdERQIN5RePzyxCFeQsU4Cx0=
github.com/metacubex/sing-tun v0.4.6-0.20250412144348-c426cb167db5 h1:hcsz5e5lqhBxn3iQQDIF60FLZ8PQT542GTQZ+1wcIGo= github.com/metacubex/sing-shadowsocks2 v0.2.3/go.mod h1:/WNy/Q8ahLCoPRriWuFZFD0Jy+JNp1MEQl28Zw6SaF8=
github.com/metacubex/sing-tun v0.4.6-0.20250412144348-c426cb167db5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ= github.com/metacubex/sing-tun v0.4.6-0.20250503065609-efb9f0beb6f6 h1:TAwL91XPa6x1QK55CRm+VTzPvLPUfEr/uFDnOZArqEU=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg= github.com/metacubex/sing-tun v0.4.6-0.20250503065609-efb9f0beb6f6/go.mod h1:HDaHDL6onAX2ZGbAGUXKp++PohRdNb7Nzt6zxzhox+U=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc= github.com/metacubex/sing-vmess v0.2.1 h1:I6gM3VUjtvJ15D805EUbNH+SRBuqzJeFnuIbKYUsWZ0=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY= github.com/metacubex/sing-vmess v0.2.1/go.mod h1:DsODWItJtOMZNna8Qhheg8r3tUivrcO3vWgaTYKnfTo=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=
github.com/metacubex/utls v1.7.0-alpha.1 h1:oMFsPh2oTlALJ7vKXPJuqgy0YeiZ+q/LLw+ZdxZ80l4= github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80=
github.com/metacubex/utls v1.7.0-alpha.1/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU= github.com/metacubex/smux v0.0.0-20250503055512-501391591dee h1:lp6hJ+4wCLZu113awp7P6odM2okB5s60HUyF0FMqKmo=
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE=
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 h1:j1VRTiC9JLR4nUbSikx9OGdu/3AgFDqgcLj4GoqyQkc=
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.7.3 h1:yDcMEWojFh+t8rU9X0HPcZDPAoFze/rIIyssqivzj8A=
github.com/metacubex/utls v1.7.3/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ= github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y= github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
@@ -162,22 +171,12 @@ github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs= github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=

View File

@@ -113,12 +113,11 @@ func ApplyConfig(cfg *config.Config, force bool) {
tunnel.OnInnerLoading() tunnel.OnInnerLoading()
initInnerTcp() initInnerTcp()
loadProxyProvider(cfg.Providers) loadProvider(cfg.Providers)
updateProfile(cfg) updateProfile(cfg)
loadRuleProvider(cfg.RuleProviders) loadProvider(cfg.RuleProviders)
runtime.GC() runtime.GC()
tunnel.OnRunning() tunnel.OnRunning()
hcCompatibleProvider(cfg.Providers)
updateUpdater(cfg) updateUpdater(cfg)
resolver.ResetConnection() resolver.ResetConnection()
@@ -303,81 +302,41 @@ func updateRules(rules []C.Rule, subRules map[string][]C.Rule, ruleProviders map
tunnel.UpdateRules(rules, subRules, ruleProviders) tunnel.UpdateRules(rules, subRules, ruleProviders)
} }
func loadProvider(pv provider.Provider) { func loadProvider[P provider.Provider](providers map[string]P) {
if pv.VehicleType() == provider.Compatible { load := func(pv P) {
return name := pv.Name()
} else { if pv.VehicleType() == provider.Compatible {
log.Infoln("Start initial provider %s", (pv).Name()) log.Infoln("Start initial compatible provider %s", name)
} } else {
log.Infoln("Start initial provider %s", name)
if err := pv.Initial(); err != nil {
switch pv.Type() {
case provider.Proxy:
{
log.Errorln("initial proxy provider %s error: %v", (pv).Name(), err)
}
case provider.Rule:
{
log.Errorln("initial rule provider %s error: %v", (pv).Name(), err)
}
} }
}
}
func loadRuleProvider(ruleProviders map[string]provider.RuleProvider) { if err := pv.Initial(); err != nil {
wg := sync.WaitGroup{} switch pv.Type() {
ch := make(chan struct{}, concurrentCount) case provider.Proxy:
for _, ruleProvider := range ruleProviders { {
ruleProvider := ruleProvider log.Errorln("initial proxy provider %s error: %v", name, err)
wg.Add(1)
ch <- struct{}{}
go func() {
defer func() { <-ch; wg.Done() }()
loadProvider(ruleProvider)
}()
}
wg.Wait()
}
func loadProxyProvider(proxyProviders map[string]provider.ProxyProvider) {
// limit concurrent size
wg := sync.WaitGroup{}
ch := make(chan struct{}, concurrentCount)
for _, proxyProvider := range proxyProviders {
proxyProvider := proxyProvider
wg.Add(1)
ch <- struct{}{}
go func() {
defer func() { <-ch; wg.Done() }()
loadProvider(proxyProvider)
}()
}
wg.Wait()
}
func hcCompatibleProvider(proxyProviders map[string]provider.ProxyProvider) {
// limit concurrent size
wg := sync.WaitGroup{}
ch := make(chan struct{}, concurrentCount)
for _, proxyProvider := range proxyProviders {
proxyProvider := proxyProvider
if proxyProvider.VehicleType() == provider.Compatible {
log.Infoln("Start initial Compatible provider %s", proxyProvider.Name())
wg.Add(1)
ch <- struct{}{}
go func() {
defer func() { <-ch; wg.Done() }()
if err := proxyProvider.Initial(); err != nil {
log.Errorln("initial Compatible provider %s error: %v", proxyProvider.Name(), err)
} }
}() case provider.Rule:
{
log.Errorln("initial rule provider %s error: %v", name, err)
}
}
} }
} }
wg := sync.WaitGroup{}
ch := make(chan struct{}, concurrentCount)
for _, pv := range providers {
pv := pv
wg.Add(1)
ch <- struct{}{}
go func() {
defer func() { <-ch; wg.Done() }()
load(pv)
}()
}
wg.Wait()
} }
func updateSniffer(snifferConfig *sniffer.Config) { func updateSniffer(snifferConfig *sniffer.Config) {
@@ -455,7 +414,7 @@ func updateGeneral(general *config.General, logging bool) {
mihomoHttp.SetUA(general.GlobalUA) mihomoHttp.SetUA(general.GlobalUA)
resource.SetETag(general.ETagSupport) resource.SetETag(general.ETagSupport)
tlsC.SetGlobalUtlsClient(general.GlobalClientFingerprint) tlsC.SetGlobalFingerprint(general.GlobalClientFingerprint)
} }
func updateUsers(users []auth.AuthUser) { func updateUsers(users []auth.AuthUser) {

View File

@@ -57,6 +57,7 @@ func applyRoute(cfg *config.Config) {
Secret: cfg.Controller.Secret, Secret: cfg.Controller.Secret,
Certificate: cfg.TLS.Certificate, Certificate: cfg.TLS.Certificate,
PrivateKey: cfg.TLS.PrivateKey, PrivateKey: cfg.TLS.PrivateKey,
EchKey: cfg.TLS.EchKey,
DohServer: cfg.Controller.ExternalDohServer, DohServer: cfg.Controller.ExternalDohServer,
IsDebug: cfg.General.LogLevel == log.DEBUG, IsDebug: cfg.General.LogLevel == log.DEBUG,
Cors: route.Cors{ Cors: route.Cors{

View File

@@ -371,13 +371,20 @@ func updateConfigs(w http.ResponseWriter, r *http.Request) {
return return
} }
} else { } else {
if req.Path == "" { if req.Path == "" { // default path unneeded any safe check
req.Path = C.Path.Config() req.Path = C.Path.Config()
} } else {
if !filepath.IsAbs(req.Path) { if !filepath.IsAbs(req.Path) {
render.Status(r, http.StatusBadRequest) render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError("path is not a absolute path")) render.JSON(w, r, newError("path is not a absolute path"))
return return
}
if !C.Path.IsSafePath(req.Path) {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError(C.Path.ErrNotSafePath(req.Path).Error()))
return
}
} }
cfg, err = executor.ParseWithPath(req.Path) cfg, err = executor.ParseWithPath(req.Path)

View File

@@ -3,7 +3,6 @@ package route
import ( import (
"bytes" "bytes"
"crypto/subtle" "crypto/subtle"
"crypto/tls"
"encoding/json" "encoding/json"
"net" "net"
"net/http" "net/http"
@@ -15,8 +14,10 @@ import (
"time" "time"
"github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/inbound"
CN "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
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"
"github.com/metacubex/mihomo/tunnel/statistic" "github.com/metacubex/mihomo/tunnel/statistic"
@@ -62,6 +63,7 @@ type Config struct {
Secret string Secret string
Certificate string Certificate string
PrivateKey string PrivateKey string
EchKey string
DohServer string DohServer string
IsDebug bool IsDebug bool
Cors Cors Cors Cors
@@ -186,7 +188,7 @@ func startTLS(cfg *Config) {
// handle tlsAddr // handle tlsAddr
if len(cfg.TLSAddr) > 0 { if len(cfg.TLSAddr) > 0 {
c, err := CN.ParseCert(cfg.Certificate, cfg.PrivateKey, C.Path) cert, err := ca.LoadTLSKeyPair(cfg.Certificate, cfg.PrivateKey, C.Path)
if err != nil { if err != nil {
log.Errorln("External controller tls listen error: %s", err) log.Errorln("External controller tls listen error: %s", err)
return return
@@ -199,14 +201,22 @@ func startTLS(cfg *Config) {
} }
log.Infoln("RESTful API tls listening at: %s", l.Addr().String()) log.Infoln("RESTful API tls listening at: %s", l.Addr().String())
tlsConfig := &tlsC.Config{}
tlsConfig.NextProtos = []string{"h2", "http/1.1"}
tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)}
if cfg.EchKey != "" {
err = ech.LoadECHKey(cfg.EchKey, tlsConfig, C.Path)
if err != nil {
log.Errorln("External controller tls serve error: %s", err)
return
}
}
server := &http.Server{ server := &http.Server{
Handler: router(cfg.IsDebug, cfg.Secret, cfg.DohServer, cfg.Cors), Handler: router(cfg.IsDebug, cfg.Secret, cfg.DohServer, cfg.Cors),
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{c},
},
} }
tlsServer = server tlsServer = server
if err = server.ServeTLS(l, "", ""); err != nil { if err = server.Serve(tlsC.NewListenerForHttps(l, server, tlsConfig)); err != nil {
log.Errorln("External controller tls serve error: %s", err) log.Errorln("External controller tls serve error: %s", err)
} }
} }

View File

@@ -3,7 +3,6 @@ package anytls
import ( import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"crypto/tls"
"encoding/binary" "encoding/binary"
"errors" "errors"
"net" "net"
@@ -12,23 +11,25 @@ import (
"github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/atomic"
"github.com/metacubex/mihomo/common/buf" "github.com/metacubex/mihomo/common/buf"
N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
LC "github.com/metacubex/mihomo/listener/config" LC "github.com/metacubex/mihomo/listener/config"
"github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/listener/sing"
"github.com/metacubex/mihomo/transport/anytls/padding" "github.com/metacubex/mihomo/transport/anytls/padding"
"github.com/metacubex/mihomo/transport/anytls/session" "github.com/metacubex/mihomo/transport/anytls/session"
"github.com/sagernet/sing/common/auth" "github.com/metacubex/sing/common/auth"
"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"
) )
type Listener struct { type Listener struct {
closed bool closed bool
config LC.AnyTLSServer config LC.AnyTLSServer
listeners []net.Listener listeners []net.Listener
tlsConfig *tls.Config tlsConfig *tlsC.Config
userMap map[[32]byte]string userMap map[[32]byte]string
padding atomic.TypedValue[*padding.PaddingFactory] padding atomic.TypedValue[*padding.PaddingFactory]
} }
@@ -41,13 +42,20 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition)
} }
} }
tlsConfig := &tls.Config{} tlsConfig := &tlsC.Config{}
if config.Certificate != "" && config.PrivateKey != "" { if config.Certificate != "" && config.PrivateKey != "" {
cert, err := N.ParseCert(config.Certificate, config.PrivateKey, C.Path) cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tlsConfig.Certificates = []tls.Certificate{cert} tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
if err != nil {
return nil, err
}
}
} }
sl = &Listener{ sl = &Listener{
@@ -87,7 +95,7 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition)
return nil, err return nil, err
} }
if len(tlsConfig.Certificates) > 0 { if len(tlsConfig.Certificates) > 0 {
l = tls.NewListener(l, tlsConfig) l = tlsC.NewListener(l, tlsConfig)
} else { } else {
return nil, errors.New("disallow using AnyTLS without certificates config") return nil, errors.New("disallow using AnyTLS without certificates config")
} }

View File

@@ -10,6 +10,7 @@ type AnyTLSServer struct {
Users map[string]string `yaml:"users" json:"users,omitempty"` Users map[string]string `yaml:"users" json:"users,omitempty"`
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"`
PaddingScheme string `yaml:"padding-scheme" json:"padding-scheme,omitempty"` PaddingScheme string `yaml:"padding-scheme" json:"padding-scheme,omitempty"`
} }

View File

@@ -12,5 +12,6 @@ type AuthServer struct {
AuthStore auth.AuthStore AuthStore auth.AuthStore
Certificate string Certificate string
PrivateKey string PrivateKey string
EchKey string
RealityConfig reality.Config RealityConfig reality.Config
} }

View File

@@ -14,6 +14,7 @@ type Hysteria2Server struct {
ObfsPassword string `yaml:"obfs-password" json:"obfs-password,omitempty"` ObfsPassword string `yaml:"obfs-password" json:"obfs-password,omitempty"`
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,omitempty"`
MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"` MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"`
ALPN []string `yaml:"alpn" json:"alpn,omitempty"` ALPN []string `yaml:"alpn" json:"alpn,omitempty"`
Up string `yaml:"up" json:"up,omitempty"` Up string `yaml:"up" json:"up,omitempty"`

View File

@@ -13,6 +13,7 @@ type ShadowsocksServer struct {
Cipher string Cipher string
Udp bool Udp bool
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
ShadowTLS ShadowTLS `yaml:"shadow-tls" json:"shadow-tls,omitempty"`
} }
func (t ShadowsocksServer) String() string { func (t ShadowsocksServer) String() string {

View File

@@ -0,0 +1,22 @@
package config
type ShadowTLS struct {
Enable bool
Version int
Password string
Users []ShadowTLSUser
Handshake ShadowTLSHandshakeOptions
HandshakeForServerName map[string]ShadowTLSHandshakeOptions
StrictMode bool
WildcardSNI string
}
type ShadowTLSUser struct {
Name string
Password string
}
type ShadowTLSHandshakeOptions struct {
Dest string
Proxy string
}

View File

@@ -20,6 +20,7 @@ type TrojanServer struct {
GrpcServiceName string GrpcServiceName string
Certificate string Certificate string
PrivateKey string PrivateKey string
EchKey string
RealityConfig reality.Config RealityConfig reality.Config
MuxOption sing.MuxOption MuxOption sing.MuxOption
TrojanSSOption TrojanSSOption TrojanSSOption TrojanSSOption

View File

@@ -13,6 +13,7 @@ type TuicServer struct {
Users map[string]string `yaml:"users" json:"users,omitempty"` Users map[string]string `yaml:"users" json:"users,omitempty"`
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"`
CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"` CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"`
MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"` MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"`
AuthenticationTimeout int `yaml:"authentication-timeout" json:"authentication-timeout,omitempty"` AuthenticationTimeout int `yaml:"authentication-timeout" json:"authentication-timeout,omitempty"`

View File

@@ -21,6 +21,7 @@ type VlessServer struct {
GrpcServiceName string GrpcServiceName string
Certificate string Certificate string
PrivateKey string PrivateKey string
EchKey string
RealityConfig reality.Config RealityConfig reality.Config
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
} }

View File

@@ -21,6 +21,7 @@ type VmessServer struct {
GrpcServiceName string GrpcServiceName string
Certificate string Certificate string
PrivateKey string PrivateKey string
EchKey string
RealityConfig reality.Config RealityConfig reality.Config
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
} }

View File

@@ -1,12 +1,13 @@
package http package http
import ( import (
"crypto/tls"
"errors" "errors"
"net" "net"
"github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/inbound"
N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
authStore "github.com/metacubex/mihomo/listener/auth" authStore "github.com/metacubex/mihomo/listener/auth"
LC "github.com/metacubex/mihomo/listener/config" LC "github.com/metacubex/mihomo/listener/config"
@@ -64,15 +65,22 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
return nil, err return nil, err
} }
tlsConfig := &tls.Config{} tlsConfig := &tlsC.Config{}
var realityBuilder *reality.Builder var realityBuilder *reality.Builder
if config.Certificate != "" && config.PrivateKey != "" { if config.Certificate != "" && config.PrivateKey != "" {
cert, err := N.ParseCert(config.Certificate, config.PrivateKey, C.Path) cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tlsConfig.Certificates = []tls.Certificate{cert} tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
if err != nil {
return nil, err
}
}
} }
if config.RealityConfig.PrivateKey != "" { if config.RealityConfig.PrivateKey != "" {
if tlsConfig.Certificates != nil { if tlsConfig.Certificates != nil {
@@ -87,7 +95,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
if realityBuilder != nil { if realityBuilder != nil {
l = realityBuilder.NewListener(l) l = realityBuilder.NewListener(l)
} else if len(tlsConfig.Certificates) > 0 { } else if len(tlsConfig.Certificates) > 0 {
l = tls.NewListener(l, tlsConfig) l = tlsC.NewListener(l, tlsConfig)
} }
hl := &Listener{ hl := &Listener{

View File

@@ -14,6 +14,7 @@ type AnyTLSOption struct {
Users map[string]string `inbound:"users,omitempty"` Users map[string]string `inbound:"users,omitempty"`
Certificate string `inbound:"certificate"` Certificate string `inbound:"certificate"`
PrivateKey string `inbound:"private-key"` PrivateKey string `inbound:"private-key"`
EchKey string `inbound:"ech-key,omitempty"`
PaddingScheme string `inbound:"padding-scheme,omitempty"` PaddingScheme string `inbound:"padding-scheme,omitempty"`
} }
@@ -42,6 +43,7 @@ func NewAnyTLS(options *AnyTLSOption) (*AnyTLS, error) {
Users: options.Users, Users: options.Users,
Certificate: options.Certificate, Certificate: options.Certificate,
PrivateKey: options.PrivateKey, PrivateKey: options.PrivateKey,
EchKey: options.EchKey,
PaddingScheme: options.PaddingScheme, PaddingScheme: options.PaddingScheme,
}, },
}, nil }, nil

View File

@@ -60,4 +60,14 @@ func TestInboundAnyTLS_TLS(t *testing.T) {
Fingerprint: tlsFingerprint, Fingerprint: tlsFingerprint,
} }
testInboundAnyTLS(t, inboundOptions, outboundOptions) testInboundAnyTLS(t, inboundOptions, outboundOptions)
t.Run("ECH", func(t *testing.T) {
inboundOptions := inboundOptions
outboundOptions := outboundOptions
inboundOptions.EchKey = echKeyPem
outboundOptions.ECHOpts = outbound.ECHOptions{
Enable: true,
Config: echConfigBase64,
}
testInboundAnyTLS(t, inboundOptions, outboundOptions)
})
} }

View File

@@ -17,25 +17,32 @@ import (
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/ca" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/generater" "github.com/metacubex/mihomo/component/generater"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"golang.org/x/net/http2"
) )
var httpPath = "/inbound_test" var httpPath = "/inbound_test"
var httpData = make([]byte, 10240) var httpData = make([]byte, 10240)
var remoteAddr = netip.MustParseAddr("1.2.3.4") var remoteAddr = netip.MustParseAddr("1.2.3.4")
var userUUID = utils.NewUUIDV4().String() var userUUID = utils.NewUUIDV4().String()
var tlsCertificate, tlsPrivateKey, tlsFingerprint, _ = N.NewRandomTLSKeyPair() var tlsCertificate, tlsPrivateKey, tlsFingerprint, _ = ca.NewRandomTLSKeyPair(ca.KeyPairTypeP256)
var tlsConfigCert, _ = tls.X509KeyPair([]byte(tlsCertificate), []byte(tlsPrivateKey)) var tlsConfigCert, _ = tls.X509KeyPair([]byte(tlsCertificate), []byte(tlsPrivateKey))
var tlsConfig = &tls.Config{Certificates: []tls.Certificate{tlsConfigCert}, NextProtos: []string{"h2", "http/1.1"}} var tlsConfig = &tls.Config{Certificates: []tls.Certificate{tlsConfigCert}, NextProtos: []string{"h2", "http/1.1"}}
var tlsClientConfig, _ = ca.GetTLSConfig(nil, tlsFingerprint, "", "") var tlsClientConfig, _ = ca.GetTLSConfig(nil, tlsFingerprint, "", "")
var realityPrivateKey, realityPublickey string var realityPrivateKey, realityPublickey string
var realityDest = "itunes.apple.com" var realityDest = "itunes.apple.com"
var realityShortid = "10f897e26c4b9478" var realityShortid = "10f897e26c4b9478"
var realityRealDial = false
var echPublicSni = "public.sni"
var echConfigBase64, echKeyPem, _ = ech.GenECHConfig(echPublicSni)
func init() { func init() {
rand.Read(httpData) rand.Read(httpData)
@@ -129,7 +136,10 @@ func NewHttpTestTunnel() *TestTunnel {
r.Get(httpPath, func(w http.ResponseWriter, r *http.Request) { r.Get(httpPath, func(w http.ResponseWriter, r *http.Request) {
render.Data(w, r, httpData) render.Data(w, r, httpData)
}) })
go http.Serve(ln, r) h2Server := &http2.Server{}
server := http.Server{Handler: r}
_ = http2.ConfigureServer(&server, h2Server)
go server.Serve(ln)
testFn := func(t *testing.T, proxy C.ProxyAdapter, proto string) { testFn := func(t *testing.T, proxy C.ProxyAdapter, proto string) {
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s://%s%s", proto, remoteAddr, httpPath), nil) req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s://%s%s", proto, remoteAddr, httpPath), nil)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
@@ -203,15 +213,27 @@ func NewHttpTestTunnel() *TestTunnel {
ch: make(chan struct{}), ch: make(chan struct{}),
} }
if metadata.DstPort == 443 { if metadata.DstPort == 443 {
tlsConn := tls.Server(c, tlsConfig.Clone()) tlsConn := tlsC.Server(c, tlsC.UConfig(tlsConfig))
if metadata.Host == realityDest { // ignore the tls handshake error for realityDest if metadata.Host == realityDest { // ignore the tls handshake error for realityDest
ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout) if realityRealDial {
defer cancel() rconn, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress())
if err := tlsConn.HandshakeContext(ctx); err != nil { if err != nil {
panic(err)
}
N.Relay(rconn, conn)
return return
} }
} }
ln.ch <- tlsConn ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout)
defer cancel()
if err := tlsConn.HandshakeContext(ctx); err != nil {
return
}
if tlsConn.ConnectionState().NegotiatedProtocol == http2.NextProtoTLS {
h2Server.ServeConn(tlsConn, &http2.ServeConnOpts{BaseConfig: &server})
} else {
ln.ch <- tlsConn
}
} else { } else {
ln.ch <- c ln.ch <- c
} }

View File

@@ -16,6 +16,7 @@ type HTTPOption struct {
Users AuthUsers `inbound:"users,omitempty"` Users AuthUsers `inbound:"users,omitempty"`
Certificate string `inbound:"certificate,omitempty"` Certificate string `inbound:"certificate,omitempty"`
PrivateKey string `inbound:"private-key,omitempty"` PrivateKey string `inbound:"private-key,omitempty"`
EchKey string `inbound:"ech-key,omitempty"`
RealityConfig RealityConfig `inbound:"reality-config,omitempty"` RealityConfig RealityConfig `inbound:"reality-config,omitempty"`
} }
@@ -64,6 +65,7 @@ func (h *HTTP) Listen(tunnel C.Tunnel) error {
AuthStore: h.config.Users.GetAuthStore(), AuthStore: h.config.Users.GetAuthStore(),
Certificate: h.config.Certificate, Certificate: h.config.Certificate,
PrivateKey: h.config.PrivateKey, PrivateKey: h.config.PrivateKey,
EchKey: h.config.EchKey,
RealityConfig: h.config.RealityConfig.Build(), RealityConfig: h.config.RealityConfig.Build(),
}, },
tunnel, tunnel,

Some files were not shown because too many files have changed in this diff Show More