Compare commits
91 Commits
Author | SHA1 | Date | |
---|---|---|---|
65071ea7d1 | |||
fa94403629 | |||
1beb2919e7 | |||
75c5d0482e | |||
55bcabdf46 | |||
abf80601e1 | |||
26f97b45d6 | |||
29315ce8e5 | |||
65f84d21ea | |||
e3ac58bc51 | |||
c66438d794 | |||
411e587460 | |||
6cc9c68458 | |||
d1c858d7ff | |||
3eef1ee064 | |||
514d374b8c | |||
a2334430c1 | |||
c8a3d6edd9 | |||
bda2ca3c13 | |||
f4b734c74c | |||
c2cdf43239 | |||
b939c81d3e | |||
0e92496eeb | |||
ea482598e0 | |||
16f3567ddc | |||
73f8da091e | |||
6bdaadc581 | |||
73a2cf593e | |||
665bfcab2d | |||
8be860472a | |||
1ec74f13f7 | |||
564b834e00 | |||
da04e00767 | |||
e0faffbfbd | |||
a0c7641ad5 | |||
1f592c43de | |||
4d7350923c | |||
76a7945994 | |||
a2bbd1cc8d | |||
4ec66d299a | |||
4e46cbfbde | |||
1a44dcee55 | |||
6c7d1657a5 | |||
38e210a851 | |||
359ee70daa | |||
8d1251f128 | |||
fb6a032872 | |||
47ad8e08be | |||
e1af4ddda3 | |||
58e05c42c9 | |||
880cc90e10 | |||
a4334e1d52 | |||
3ba94842cc | |||
a266589faf | |||
d9319ec09a | |||
070f8f8949 | |||
bf3c6a044c | |||
d6d2d90502 | |||
a1d0f4c6ee | |||
d569d8186d | |||
9b7aab1fc7 | |||
3c717097cb | |||
8293b7fdae | |||
0ba415866e | |||
53b41ca166 | |||
8a75f78e63 | |||
d9692c6366 | |||
f4b0062dfc | |||
b9ffc82e53 | |||
78aaea6a45 | |||
3645fbf161 | |||
a1d0f22132 | |||
fa73b0f4bf | |||
3b76a8b839 | |||
667f42dcdc | |||
dfbe09860f | |||
9e20f9c26a | |||
f968d0cb82 | |||
2ad84f4379 | |||
c7aa16426f | |||
5987f8e3b5 | |||
3a8eb72de2 | |||
33abbdfd24 | |||
0703d6cbff | |||
10d2d14938 | |||
691cf1d8d6 | |||
d1decb8e58 | |||
7d04904109 | |||
a5acd3aa97 | |||
eea9a12560 | |||
0a4570b55c |
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,7 +1,6 @@
|
||||
name: Bug report
|
||||
description: Create a report to help us improve
|
||||
title: "[Bug] "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: ensure
|
||||
@ -80,4 +79,4 @@ Paste the Clash core log below with the log level set to `DEBUG`.
|
||||
attributes:
|
||||
label: Description
|
||||
validations:
|
||||
required: true
|
||||
required: true
|
||||
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
5
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,5 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Clash.Meta Community Support
|
||||
url: https://github.com/MetaCubeX/Clash.Meta/discussions
|
||||
about: Please ask and answer questions about Clash.Meta here.
|
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -1,7 +1,6 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea for this project
|
||||
title: "[Feature] "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: ensure
|
||||
|
32
.github/genReleaseNote.sh
vendored
Executable file
32
.github/genReleaseNote.sh
vendored
Executable file
@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
while getopts "v:" opt; do
|
||||
case $opt in
|
||||
v)
|
||||
version_range=$OPTARG
|
||||
;;
|
||||
\?)
|
||||
echo "Invalid option: -$OPTARG" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$version_range" ]; then
|
||||
echo "Please provide the version range using -v option. Example: ./genReleashNote.sh -v v1.14.1...v1.14.2"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "## What's Changed" > release.md
|
||||
git log --pretty=format:"* %s by @%an" --grep="^feat" -i $version_range | sort -f | uniq >> release.md
|
||||
echo "" >> release.md
|
||||
|
||||
echo "## BUG & Fix" >> release.md
|
||||
git log --pretty=format:"* %s by @%an" --grep="^fix" -i $version_range | sort -f | uniq >> release.md
|
||||
echo "" >> release.md
|
||||
|
||||
echo "## Maintenance" >> release.md
|
||||
git log --pretty=format:"* %s by @%an" --grep="^chore\|^docs\|^refactor" -i $version_range | sort -f | uniq >> release.md
|
||||
echo "" >> release.md
|
||||
|
||||
echo "**Full Changelog**: https://github.com/MetaCubeX/Clash.Meta/compare/$version_range" >> release.md
|
12
.github/rename-go120.sh
vendored
12
.github/rename-go120.sh
vendored
@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
FILENAMES=$(ls)
|
||||
for FILENAME in $FILENAMES
|
||||
do
|
||||
if [[ ! ($FILENAME =~ ".exe" || $FILENAME =~ ".sh")]];then
|
||||
mv $FILENAME ${FILENAME}-go120
|
||||
elif [[ $FILENAME =~ ".exe" ]];then
|
||||
mv $FILENAME ${FILENAME%.*}-go120.exe
|
||||
else echo "skip $FILENAME"
|
||||
fi
|
||||
done
|
69
.github/workflows/android-branch-auto-sync.yml
vendored
69
.github/workflows/android-branch-auto-sync.yml
vendored
@ -1,69 +0,0 @@
|
||||
name: Android Branch Auto Sync
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "README.md"
|
||||
- ".github/ISSUE_TEMPLATE/**"
|
||||
branches:
|
||||
- Alpha
|
||||
- android-open
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request_target:
|
||||
branches:
|
||||
- Alpha
|
||||
- android-open
|
||||
|
||||
jobs:
|
||||
update-dependencies:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config --global user.name 'GitHub Action'
|
||||
git config --global user.email 'action@github.com'
|
||||
|
||||
- name: Sync android-real with Alpha rebase android-open
|
||||
run: |
|
||||
git fetch origin
|
||||
git checkout origin/Alpha -b android-real
|
||||
git merge --squash origin/android-open
|
||||
git commit -m "Android: patch"
|
||||
|
||||
- name: Check for conflicts
|
||||
run: |
|
||||
CONFLICTS=$(git diff --name-only --diff-filter=U)
|
||||
if [ ! -z "$CONFLICTS" ]; then
|
||||
echo "There are conflicts in the following files:"
|
||||
echo $CONFLICTS
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Push changes
|
||||
run: |
|
||||
git push origin android-real --force
|
||||
|
||||
# Send "core-updated" to MetaCubeX/ClashMetaForAndroid to trigger update-dependencies
|
||||
trigger-CMFA-update:
|
||||
needs: update-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: tibdex/github-app-token@v1
|
||||
id: generate-token
|
||||
with:
|
||||
app_id: ${{ secrets.MAINTAINER_APPID }}
|
||||
private_key: ${{ secrets.MAINTAINER_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Trigger update-dependencies
|
||||
run: |
|
||||
curl -X POST https://api.github.com/repos/MetaCubeX/ClashMetaForAndroid/dispatches \
|
||||
-H "Accept: application/vnd.github.everest-preview+json" \
|
||||
-H "Authorization: token ${{ steps.generate-token.outputs.token }}" \
|
||||
-d '{"event_type": "core-updated"}'
|
56
.github/workflows/build.yml
vendored
56
.github/workflows/build.yml
vendored
@ -69,12 +69,6 @@ jobs:
|
||||
target: "darwin-amd64 darwin-arm64 android-arm64",
|
||||
id: "9",
|
||||
}
|
||||
# only for test
|
||||
- { type: "WithoutCGO-GO120", target: "linux-amd64 linux-amd64-compatible",id: "1" }
|
||||
# Go 1.20 is the last release that will run on any release of Windows 7, 8, Server 2008 and Server 2012. Go 1.21 will require at least Windows 10 or Server 2016.
|
||||
- { type: "WithoutCGO-GO120", target: "windows-amd64-compatible windows-amd64 windows-386",id: "2" }
|
||||
# Go 1.20 is the last release that will run on macOS 10.13 High Sierra or 10.14 Mojave. Go 1.21 will require macOS 10.15 Catalina or later.
|
||||
- { type: "WithoutCGO-GO120", target: "darwin-amd64 darwin-arm64 android-arm64",id: "3" }
|
||||
- { type: "WithCGO", target: "windows/*", id: "1" }
|
||||
- { type: "WithCGO", target: "linux/386", id: "2" }
|
||||
- { type: "WithCGO", target: "linux/amd64", id: "3" }
|
||||
@ -100,11 +94,6 @@ jobs:
|
||||
run: echo "VERSION=alpha-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Set variables
|
||||
if: ${{github.ref_name=='Beta'}}
|
||||
run: echo "VERSION=beta-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Set variables
|
||||
if: ${{github.ref_name=='Meta'}}
|
||||
run: echo "VERSION=meta-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
@ -132,26 +121,18 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Setup Go
|
||||
if: ${{ matrix.job.type!='WithoutCGO-GO120' }}
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.21"
|
||||
check-latest: true
|
||||
|
||||
- name: Setup Go
|
||||
if: ${{ matrix.job.type=='WithoutCGO-GO120' }}
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.20"
|
||||
check-latest: true
|
||||
|
||||
- name: Test
|
||||
if: ${{ matrix.job.id=='1' && matrix.job.type!='WithCGO' }}
|
||||
if: ${{ matrix.job.id=='1' && matrix.job.type=='WithoutCGO' }}
|
||||
run: |
|
||||
go test ./...
|
||||
|
||||
- name: Build WithoutCGO
|
||||
if: ${{ matrix.job.type!='WithCGO' }}
|
||||
if: ${{ matrix.job.type=='WithoutCGO' }}
|
||||
env:
|
||||
NAME: Clash.Meta
|
||||
BINDIR: bin
|
||||
@ -161,7 +142,7 @@ jobs:
|
||||
if: ${{ matrix.job.type=='WithCGO' && matrix.job.target=='android' }}
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: r26
|
||||
ndk-version: r25b
|
||||
add-to-path: false
|
||||
local-cache: true
|
||||
|
||||
@ -199,17 +180,6 @@ jobs:
|
||||
ls -la
|
||||
cd ..
|
||||
|
||||
- name: Rename
|
||||
if: ${{ matrix.job.type=='WithoutCGO-GO120' }}
|
||||
run: |
|
||||
cd bin
|
||||
ls -la
|
||||
cp ../.github/rename-go120.sh ./
|
||||
bash ./rename-go120.sh
|
||||
rm ./rename-go120.sh
|
||||
ls -la
|
||||
cd ..
|
||||
|
||||
- name: Zip
|
||||
if: ${{ success() }}
|
||||
run: |
|
||||
@ -234,7 +204,7 @@ jobs:
|
||||
|
||||
Upload-Prerelease:
|
||||
permissions: write-all
|
||||
if: ${{ github.ref_type=='branch' && github.event_name != 'pull_request' }}
|
||||
if: ${{ github.ref_type=='branch' }}
|
||||
needs: [Build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -292,6 +262,23 @@ jobs:
|
||||
needs: [Build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get tags
|
||||
run: |
|
||||
echo "CURRENTVERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
git fetch --tags
|
||||
echo "PREVERSION=$(git describe --tags --abbrev=0 HEAD^)" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate release notes
|
||||
run: |
|
||||
cp ./.github/genReleaseNote.sh ./
|
||||
bash ./genReleaseNote.sh -v ${PREVERSION}...${CURRENTVERSION}
|
||||
rm ./genReleaseNote.sh
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: artifact
|
||||
@ -308,6 +295,7 @@ jobs:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
files: bin/*
|
||||
generate_release_notes: true
|
||||
body_path: release.md
|
||||
|
||||
Docker:
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
|
15
.github/workflows/delete.yml
vendored
Normal file
15
.github/workflows/delete.yml
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
name: Delete old workflow runs
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * SUN"
|
||||
|
||||
jobs:
|
||||
del_runs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete workflow runs
|
||||
uses: GitRML/delete-workflow-runs@main
|
||||
with:
|
||||
token: ${{ secrets.AUTH_PAT }}
|
||||
repository: ${{ github.repository }}
|
||||
retain_days: 30
|
320
README.md
320
README.md
@ -21,52 +21,261 @@
|
||||
## Features
|
||||
|
||||
- Local HTTP/HTTPS/SOCKS server with authentication support
|
||||
- VMess, VLESS, Shadowsocks, Trojan, Snell, TUIC, Hysteria protocol support
|
||||
- VMess, Shadowsocks, Trojan, Snell protocol support for remote connections
|
||||
- Built-in DNS server that aims to minimize DNS pollution attack impact, supports DoH/DoT upstream and fake IP.
|
||||
- Rules based off domains, GEOIP, IPCIDR or Process to forward packets to different nodes
|
||||
- Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node
|
||||
based off latency
|
||||
- Remote providers, allowing users to get node lists remotely instead of hard-coding in config
|
||||
- Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node based off latency
|
||||
- Remote providers, allowing users to get node lists remotely instead of hardcoding in config
|
||||
- Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`.
|
||||
- Comprehensive HTTP RESTful API controller
|
||||
|
||||
## Dashboard
|
||||
## Wiki
|
||||
Configuration examples can be found at [/docs/config.yaml](https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml), while documentation can be found [Clash.Meta Wiki](https://clash-meta.wiki).
|
||||
|
||||
A web dashboard with first-class support for this project has been created; it can be checked out at [metacubexd](https://github.com/MetaCubeX/metacubexd).
|
||||
## Build
|
||||
|
||||
## Configration example
|
||||
You should install [golang](https://go.dev) first.
|
||||
|
||||
Configuration example is located at [/docs/config.yaml](https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml).
|
||||
|
||||
## Docs
|
||||
|
||||
Documentation can be found in [Clash.Meta Docs](https://clash-meta.wiki).
|
||||
|
||||
## For development
|
||||
|
||||
Requirements:
|
||||
[Go 1.20 or newer](https://go.dev/dl/)
|
||||
|
||||
Build Clash.Meta:
|
||||
Then get the source code of Clash.Meta:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/MetaCubeX/Clash.Meta.git
|
||||
cd Clash.Meta && go mod download
|
||||
go build
|
||||
```
|
||||
|
||||
Set go proxy if a connection to GitHub is not possible:
|
||||
If you can't visit github,you should set proxy first:
|
||||
|
||||
```shell
|
||||
go env -w GOPROXY=https://goproxy.io,direct
|
||||
```
|
||||
|
||||
Build with gvisor tun stack:
|
||||
Now you can build it:
|
||||
|
||||
```shell
|
||||
go build
|
||||
```
|
||||
|
||||
If you need gvisor for tun stack, build with:
|
||||
|
||||
```shell
|
||||
go build -tags with_gvisor
|
||||
```
|
||||
|
||||
<!-- ## Advanced usage of this fork -->
|
||||
|
||||
<!-- ### DNS configuration
|
||||
|
||||
Support `geosite` with `fallback-filter`.
|
||||
|
||||
Restore `Redir remote resolution`.
|
||||
|
||||
Support resolve ip with a `Proxy Tunnel`.
|
||||
|
||||
```yaml
|
||||
proxy-groups:
|
||||
- name: DNS
|
||||
type: url-test
|
||||
use:
|
||||
- HK
|
||||
url: http://cp.cloudflare.com
|
||||
interval: 180
|
||||
lazy: true
|
||||
```
|
||||
|
||||
```yaml
|
||||
dns:
|
||||
enable: true
|
||||
use-hosts: true
|
||||
ipv6: false
|
||||
enhanced-mode: redir-host
|
||||
fake-ip-range: 198.18.0.1/16
|
||||
listen: 127.0.0.1:6868
|
||||
default-nameserver:
|
||||
- 119.29.29.29
|
||||
- 114.114.114.114
|
||||
nameserver:
|
||||
- https://doh.pub/dns-query
|
||||
- tls://223.5.5.5:853
|
||||
fallback:
|
||||
- "https://1.0.0.1/dns-query#DNS" # append the proxy adapter name or group name to the end of DNS URL with '#' prefix.
|
||||
- "tls://8.8.4.4:853#DNS"
|
||||
fallback-filter:
|
||||
geoip: false
|
||||
geosite:
|
||||
- gfw # `geosite` filter only use fallback server to resolve ip, prevent DNS leaks to unsafe DNS providers.
|
||||
domain:
|
||||
- +.example.com
|
||||
ipcidr:
|
||||
- 0.0.0.0/32
|
||||
```
|
||||
|
||||
### TUN configuration
|
||||
|
||||
Supports macOS, Linux and Windows.
|
||||
|
||||
Built-in [Wintun](https://www.wintun.net) driver.
|
||||
|
||||
```yaml
|
||||
# Enable the TUN listener
|
||||
tun:
|
||||
enable: true
|
||||
stack: system # system/gvisor
|
||||
dns-hijack:
|
||||
- 0.0.0.0:53 # additional dns server listen on TUN
|
||||
auto-route: true # auto set global route
|
||||
```
|
||||
|
||||
### Rules configuration
|
||||
|
||||
- Support rule `GEOSITE`.
|
||||
- Support rule-providers `RULE-SET`.
|
||||
- Support `multiport` condition for rule `SRC-PORT` and `DST-PORT`.
|
||||
- Support `network` condition for all rules.
|
||||
- Support source IPCIDR condition for all rules, just append to the end.
|
||||
- The `GEOSITE` databases via https://github.com/Loyalsoldier/v2ray-rules-dat.
|
||||
|
||||
```yaml
|
||||
rules:
|
||||
# network(tcp/udp) condition for all rules
|
||||
- DOMAIN-SUFFIX,bilibili.com,DIRECT,tcp
|
||||
- DOMAIN-SUFFIX,bilibili.com,REJECT,udp
|
||||
|
||||
# multiport condition for rules SRC-PORT and DST-PORT
|
||||
- DST-PORT,123/136/137-139,DIRECT,udp
|
||||
|
||||
# rule GEOSITE
|
||||
- GEOSITE,category-ads-all,REJECT
|
||||
- GEOSITE,icloud@cn,DIRECT
|
||||
- GEOSITE,apple@cn,DIRECT
|
||||
- GEOSITE,apple-cn,DIRECT
|
||||
- GEOSITE,microsoft@cn,DIRECT
|
||||
- GEOSITE,facebook,PROXY
|
||||
- GEOSITE,youtube,PROXY
|
||||
- GEOSITE,geolocation-cn,DIRECT
|
||||
- GEOSITE,geolocation-!cn,PROXY
|
||||
|
||||
# source IPCIDR condition for all rules in gateway proxy
|
||||
#- GEOSITE,geolocation-!cn,REJECT,192.168.1.88/32,192.168.1.99/32
|
||||
|
||||
- GEOIP,telegram,PROXY,no-resolve
|
||||
- GEOIP,private,DIRECT,no-resolve
|
||||
- GEOIP,cn,DIRECT
|
||||
|
||||
- MATCH,PROXY
|
||||
```
|
||||
|
||||
### Proxies configuration
|
||||
|
||||
Active health detection `urltest / fallback` (based on tcp handshake, multiple failures within a limited time will actively trigger health detection to use the node)
|
||||
|
||||
Support `Policy Group Filter`
|
||||
|
||||
```yaml
|
||||
proxy-groups:
|
||||
- name: 🚀 HK Group
|
||||
type: select
|
||||
use:
|
||||
- ALL
|
||||
filter: "HK"
|
||||
|
||||
- name: 🚀 US Group
|
||||
type: select
|
||||
use:
|
||||
- ALL
|
||||
filter: "US"
|
||||
|
||||
proxy-providers:
|
||||
ALL:
|
||||
type: http
|
||||
url: "xxxxx"
|
||||
interval: 3600
|
||||
path: "xxxxx"
|
||||
health-check:
|
||||
enable: true
|
||||
interval: 600
|
||||
url: http://www.gstatic.com/generate_204
|
||||
```
|
||||
|
||||
Support outbound transport protocol `VLESS`.
|
||||
|
||||
The XTLS support (TCP/UDP) transport by the XRAY-CORE.
|
||||
|
||||
```yaml
|
||||
proxies:
|
||||
- name: "vless"
|
||||
type: vless
|
||||
server: server
|
||||
port: 443
|
||||
uuid: uuid
|
||||
servername: example.com # AKA SNI
|
||||
# flow: xtls-rprx-direct # xtls-rprx-origin # enable XTLS
|
||||
# skip-cert-verify: true
|
||||
|
||||
- name: "vless-ws"
|
||||
type: vless
|
||||
server: server
|
||||
port: 443
|
||||
uuid: uuid
|
||||
tls: true
|
||||
udp: true
|
||||
network: ws
|
||||
servername: example.com # priority over wss host
|
||||
# skip-cert-verify: true
|
||||
ws-opts:
|
||||
path: /path
|
||||
headers: { Host: example.com, Edge: "12a00c4.fm.huawei.com:82897" }
|
||||
|
||||
- name: "vless-grpc"
|
||||
type: vless
|
||||
server: server
|
||||
port: 443
|
||||
uuid: uuid
|
||||
tls: true
|
||||
udp: true
|
||||
network: grpc
|
||||
servername: example.com # priority over wss host
|
||||
# skip-cert-verify: true
|
||||
grpc-opts:
|
||||
grpc-service-name: grpcname
|
||||
```
|
||||
|
||||
Support outbound transport protocol `Wireguard`
|
||||
|
||||
```yaml
|
||||
proxies:
|
||||
- name: "wg"
|
||||
type: wireguard
|
||||
server: 162.159.192.1
|
||||
port: 2480
|
||||
ip: 172.16.0.2
|
||||
ipv6: fd01:5ca1:ab1e:80fa:ab85:6eea:213f:f4a5
|
||||
private-key: eCtXsJZ27+4PbhDkHnB923tkUn2Gj59wZw5wFA75MnU=
|
||||
public-key: Cr8hWlKvtDt7nrvf+f0brNQQzabAqrjfBvas9pmowjo=
|
||||
udp: true
|
||||
```
|
||||
|
||||
Support outbound transport protocol `Tuic`
|
||||
|
||||
```yaml
|
||||
proxies:
|
||||
- name: "tuic"
|
||||
server: www.example.com
|
||||
port: 10443
|
||||
type: tuic
|
||||
token: TOKEN
|
||||
# ip: 127.0.0.1 # for overwriting the DNS lookup result of the server address set in option 'server'
|
||||
# heartbeat-interval: 10000
|
||||
# alpn: [h3]
|
||||
# disable-sni: true
|
||||
reduce-rtt: true
|
||||
# request-timeout: 8000
|
||||
udp-relay-mode: native # Available: "native", "quic". Default: "native"
|
||||
# congestion-controller: bbr # Available: "cubic", "new_reno", "bbr". Default: "cubic"
|
||||
# max-udp-relay-packet-size: 1500
|
||||
# fast-open: true
|
||||
# skip-cert-verify: true
|
||||
``` -->
|
||||
|
||||
### IPTABLES configuration
|
||||
|
||||
Work on Linux OS which supported `iptables`
|
||||
@ -80,10 +289,71 @@ iptables:
|
||||
inbound-interface: eth0 # detect the inbound interface, default is 'lo'
|
||||
```
|
||||
|
||||
## Debugging
|
||||
### General installation guide for Linux
|
||||
|
||||
- Create user given name `clash-meta`
|
||||
|
||||
- Download and decompress pre-built binaries from [releases](https://github.com/MetaCubeX/Clash.Meta/releases)
|
||||
|
||||
- Rename executable file to `Clash-Meta` and move to `/usr/local/bin/`
|
||||
|
||||
- Create folder `/etc/Clash-Meta/` as working directory
|
||||
|
||||
Run Meta Kernel by user `clash-meta` as a daemon.
|
||||
|
||||
Create the systemd configuration file at `/etc/systemd/system/Clash-Meta.service`:
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Clash-Meta Daemon, Another Clash Kernel.
|
||||
After=network.target NetworkManager.service systemd-networkd.service iwd.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=clash-meta
|
||||
Group=clash-meta
|
||||
LimitNPROC=500
|
||||
LimitNOFILE=1000000
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
|
||||
Restart=always
|
||||
ExecStartPre=/usr/bin/sleep 1s
|
||||
ExecStart=/usr/local/bin/Clash-Meta -d /etc/Clash-Meta
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Launch clashd on system startup with:
|
||||
|
||||
```shell
|
||||
$ systemctl enable Clash-Meta
|
||||
```
|
||||
|
||||
Launch clashd immediately with:
|
||||
|
||||
```shell
|
||||
$ systemctl start Clash-Meta
|
||||
```
|
||||
|
||||
### Display Process name
|
||||
|
||||
Clash add field `Process` to `Metadata` and prepare to get process name for Restful API `GET /connections`.
|
||||
|
||||
To display process name in GUI please use [Razord-meta](https://github.com/MetaCubeX/Razord-meta).
|
||||
|
||||
### Dashboard
|
||||
|
||||
We also made a custom fork of yacd provide better support for this project, check it out at [Yacd-meta](https://github.com/MetaCubeX/Yacd-meta)
|
||||
|
||||
## Development
|
||||
|
||||
If you want to build an application that uses clash as a library, check out the
|
||||
the [GitHub Wiki](https://github.com/Dreamacro/clash/wiki/use-clash-as-a-library)
|
||||
|
||||
## Debugging
|
||||
Check [wiki](https://github.com/MetaCubeX/Clash.Meta/wiki/How-to-use-debug-api) to get an instruction on using debug API.
|
||||
|
||||
Check [wiki](https://wiki.metacubex.one/api/#debug) to get an instruction on using debug
|
||||
API.
|
||||
|
||||
## Credits
|
||||
|
||||
|
@ -18,8 +18,6 @@ import (
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v2"
|
||||
)
|
||||
|
||||
var UnifiedDelay = atomic.NewBool(false)
|
||||
@ -30,15 +28,15 @@ const (
|
||||
|
||||
type extraProxyState struct {
|
||||
history *queue.Queue[C.DelayHistory]
|
||||
alive atomic.Bool
|
||||
alive *atomic.Bool
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
C.ProxyAdapter
|
||||
history *queue.Queue[C.DelayHistory]
|
||||
alive atomic.Bool
|
||||
alive *atomic.Bool
|
||||
url string
|
||||
extra *xsync.MapOf[string, *extraProxyState]
|
||||
extra map[string]*extraProxyState
|
||||
}
|
||||
|
||||
// Alive implements C.Proxy
|
||||
@ -48,8 +46,10 @@ func (p *Proxy) Alive() bool {
|
||||
|
||||
// AliveForTestUrl implements C.Proxy
|
||||
func (p *Proxy) AliveForTestUrl(url string) bool {
|
||||
if state, ok := p.extra.Load(url); ok {
|
||||
return state.alive.Load()
|
||||
if p.extra != nil {
|
||||
if state, ok := p.extra[url]; ok {
|
||||
return state.alive.Load()
|
||||
}
|
||||
}
|
||||
|
||||
return p.alive.Load()
|
||||
@ -88,16 +88,16 @@ func (p *Proxy) DelayHistory() []C.DelayHistory {
|
||||
for _, item := range queueM {
|
||||
histories = append(histories, item)
|
||||
}
|
||||
|
||||
return histories
|
||||
}
|
||||
|
||||
// DelayHistoryForTestUrl implements C.Proxy
|
||||
func (p *Proxy) DelayHistoryForTestUrl(url string) []C.DelayHistory {
|
||||
var queueM []C.DelayHistory
|
||||
|
||||
if state, ok := p.extra.Load(url); ok {
|
||||
queueM = state.history.Copy()
|
||||
if p.extra != nil {
|
||||
if state, ok := p.extra[url]; ok {
|
||||
queueM = state.history.Copy()
|
||||
}
|
||||
}
|
||||
|
||||
if queueM == nil {
|
||||
@ -112,25 +112,19 @@ func (p *Proxy) DelayHistoryForTestUrl(url string) []C.DelayHistory {
|
||||
}
|
||||
|
||||
func (p *Proxy) ExtraDelayHistory() map[string][]C.DelayHistory {
|
||||
extraHistory := map[string][]C.DelayHistory{}
|
||||
extra := map[string][]C.DelayHistory{}
|
||||
if p.extra != nil && len(p.extra) != 0 {
|
||||
for testUrl, option := range p.extra {
|
||||
histories := []C.DelayHistory{}
|
||||
queueM := option.history.Copy()
|
||||
for _, item := range queueM {
|
||||
histories = append(histories, item)
|
||||
}
|
||||
|
||||
p.extra.Range(func(k string, v *extraProxyState) bool {
|
||||
|
||||
testUrl := k
|
||||
state := v
|
||||
|
||||
histories := []C.DelayHistory{}
|
||||
queueM := state.history.Copy()
|
||||
|
||||
for _, item := range queueM {
|
||||
histories = append(histories, item)
|
||||
extra[testUrl] = histories
|
||||
}
|
||||
|
||||
extraHistory[testUrl] = histories
|
||||
|
||||
return true
|
||||
})
|
||||
return extraHistory
|
||||
}
|
||||
return extra
|
||||
}
|
||||
|
||||
// LastDelay return last history record. if proxy is not alive, return the max value of uint16.
|
||||
@ -155,9 +149,11 @@ func (p *Proxy) LastDelayForTestUrl(url string) (delay uint16) {
|
||||
alive := p.alive.Load()
|
||||
history := p.history.Last()
|
||||
|
||||
if state, ok := p.extra.Load(url); ok {
|
||||
alive = state.alive.Load()
|
||||
history = state.history.Last()
|
||||
if p.extra != nil {
|
||||
if state, ok := p.extra[url]; ok {
|
||||
alive = state.alive.Load()
|
||||
history = state.history.Last()
|
||||
}
|
||||
}
|
||||
|
||||
if !alive {
|
||||
@ -217,18 +213,18 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In
|
||||
if alive {
|
||||
record.Delay = t
|
||||
}
|
||||
p.history.Put(record)
|
||||
if p.history.Len() > defaultHistoriesNum {
|
||||
p.history.Pop()
|
||||
|
||||
if p.extra == nil {
|
||||
p.extra = map[string]*extraProxyState{}
|
||||
}
|
||||
|
||||
state, ok := p.extra.Load(url)
|
||||
state, ok := p.extra[url]
|
||||
if !ok {
|
||||
state = &extraProxyState{
|
||||
history: queue.New[C.DelayHistory](defaultHistoriesNum),
|
||||
alive: atomic.NewBool(true),
|
||||
}
|
||||
p.extra.Store(url, state)
|
||||
p.extra[url] = state
|
||||
}
|
||||
|
||||
state.alive.Store(alive)
|
||||
@ -311,12 +307,7 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In
|
||||
}
|
||||
|
||||
func NewProxy(adapter C.ProxyAdapter) *Proxy {
|
||||
return &Proxy{
|
||||
ProxyAdapter: adapter,
|
||||
history: queue.New[C.DelayHistory](defaultHistoriesNum),
|
||||
alive: atomic.NewBool(true),
|
||||
url: "",
|
||||
extra: xsync.NewMapOf[*extraProxyState]()}
|
||||
return &Proxy{adapter, queue.New[C.DelayHistory](defaultHistoriesNum), atomic.NewBool(true), "", map[string]*extraProxyState{}}
|
||||
}
|
||||
|
||||
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
|
||||
@ -359,14 +350,14 @@ func (p *Proxy) determineFinalStoreType(store C.DelayHistoryStoreType, url strin
|
||||
return C.OriginalHistory
|
||||
}
|
||||
|
||||
if p.extra.Size() < 2*C.DefaultMaxHealthCheckUrlNum {
|
||||
return C.ExtraHistory
|
||||
if p.extra == nil {
|
||||
store = C.ExtraHistory
|
||||
} else {
|
||||
if _, ok := p.extra[url]; ok {
|
||||
store = C.ExtraHistory
|
||||
} else if len(p.extra) < 2*C.DefaultMaxHealthCheckUrlNum {
|
||||
store = C.ExtraHistory
|
||||
}
|
||||
}
|
||||
|
||||
_, ok := p.extra.Load(url)
|
||||
if ok {
|
||||
return C.ExtraHistory
|
||||
}
|
||||
|
||||
return store
|
||||
}
|
||||
|
@ -1,17 +1,13 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type Addition func(metadata *C.Metadata)
|
||||
|
||||
func ApplyAdditions(metadata *C.Metadata, additions ...Addition) {
|
||||
for _, addition := range additions {
|
||||
addition(metadata)
|
||||
}
|
||||
func (a Addition) Apply(metadata *C.Metadata) {
|
||||
a(metadata)
|
||||
}
|
||||
|
||||
func WithInName(name string) Addition {
|
||||
@ -37,29 +33,3 @@ func WithSpecialProxy(specialProxy string) Addition {
|
||||
metadata.SpecialProxy = specialProxy
|
||||
}
|
||||
}
|
||||
|
||||
func WithDstAddr(addr net.Addr) Addition {
|
||||
return func(metadata *C.Metadata) {
|
||||
_ = metadata.SetRemoteAddr(addr)
|
||||
}
|
||||
}
|
||||
|
||||
func WithSrcAddr(addr net.Addr) Addition {
|
||||
return func(metadata *C.Metadata) {
|
||||
m := C.Metadata{}
|
||||
if err := m.SetRemoteAddr(addr);err ==nil{
|
||||
metadata.SrcIP = m.DstIP
|
||||
metadata.SrcPort = m.DstPort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithInAddr(addr net.Addr) Addition {
|
||||
return func(metadata *C.Metadata) {
|
||||
m := C.Metadata{}
|
||||
if err := m.SetRemoteAddr(addr);err ==nil{
|
||||
metadata.InIP = m.DstIP
|
||||
metadata.InPort = m.DstPort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,45 +0,0 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
var skipAuthPrefixes []netip.Prefix
|
||||
|
||||
func SetSkipAuthPrefixes(prefixes []netip.Prefix) {
|
||||
skipAuthPrefixes = prefixes
|
||||
}
|
||||
|
||||
func SkipAuthPrefixes() []netip.Prefix {
|
||||
return skipAuthPrefixes
|
||||
}
|
||||
|
||||
func SkipAuthRemoteAddr(addr net.Addr) bool {
|
||||
m := C.Metadata{}
|
||||
if err := m.SetRemoteAddr(addr); err != nil {
|
||||
return false
|
||||
}
|
||||
return skipAuth(m.AddrPort().Addr())
|
||||
}
|
||||
|
||||
func SkipAuthRemoteAddress(addr string) bool {
|
||||
m := C.Metadata{}
|
||||
if err := m.SetRemoteAddress(addr); err != nil {
|
||||
return false
|
||||
}
|
||||
return skipAuth(m.AddrPort().Addr())
|
||||
}
|
||||
|
||||
func skipAuth(addr netip.Addr) bool {
|
||||
if addr.IsValid() {
|
||||
for _, prefix := range skipAuthPrefixes {
|
||||
if prefix.Contains(addr.Unmap()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
@ -4,15 +4,25 @@ import (
|
||||
"net"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/context"
|
||||
"github.com/Dreamacro/clash/transport/socks5"
|
||||
)
|
||||
|
||||
// NewHTTP receive normal http request and return HTTPContext
|
||||
func NewHTTP(target socks5.Addr, srcConn net.Conn, conn net.Conn, additions ...Addition) (net.Conn, *C.Metadata) {
|
||||
func NewHTTP(target socks5.Addr, source net.Addr, conn net.Conn, additions ...Addition) *context.ConnContext {
|
||||
metadata := parseSocksAddr(target)
|
||||
metadata.NetWork = C.TCP
|
||||
metadata.Type = C.HTTP
|
||||
ApplyAdditions(metadata, WithSrcAddr(srcConn.RemoteAddr()), WithInAddr(conn.LocalAddr()))
|
||||
ApplyAdditions(metadata, additions...)
|
||||
return conn, metadata
|
||||
for _, addition := range additions {
|
||||
addition.Apply(metadata)
|
||||
}
|
||||
if ip, port, err := parseAddr(source); err == nil {
|
||||
metadata.SrcIP = ip
|
||||
metadata.SrcPort = port
|
||||
}
|
||||
if ip, port, err := parseAddr(conn.LocalAddr()); err == nil {
|
||||
metadata.InIP = ip
|
||||
metadata.InPort = port
|
||||
}
|
||||
return context.NewConnContext(conn, metadata)
|
||||
}
|
||||
|
@ -5,13 +5,23 @@ import (
|
||||
"net/http"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/context"
|
||||
)
|
||||
|
||||
// NewHTTPS receive CONNECT request and return ConnContext
|
||||
func NewHTTPS(request *http.Request, conn net.Conn, additions ...Addition) (net.Conn, *C.Metadata) {
|
||||
func NewHTTPS(request *http.Request, conn net.Conn, additions ...Addition) *context.ConnContext {
|
||||
metadata := parseHTTPAddr(request)
|
||||
metadata.Type = C.HTTPS
|
||||
ApplyAdditions(metadata, WithSrcAddr(conn.RemoteAddr()), WithInAddr(conn.LocalAddr()))
|
||||
ApplyAdditions(metadata, additions...)
|
||||
return conn, metadata
|
||||
for _, addition := range additions {
|
||||
addition.Apply(metadata)
|
||||
}
|
||||
if ip, port, err := parseAddr(conn.RemoteAddr()); err == nil {
|
||||
metadata.SrcIP = ip
|
||||
metadata.SrcPort = port
|
||||
}
|
||||
if ip, port, err := parseAddr(conn.LocalAddr()); err == nil {
|
||||
metadata.InIP = ip
|
||||
metadata.InPort = port
|
||||
}
|
||||
return context.NewConnContext(conn, metadata)
|
||||
}
|
||||
|
@ -5,16 +5,38 @@ import (
|
||||
"github.com/Dreamacro/clash/transport/socks5"
|
||||
)
|
||||
|
||||
// PacketAdapter is a UDP Packet adapter for socks/redir/tun
|
||||
type PacketAdapter struct {
|
||||
C.UDPPacket
|
||||
metadata *C.Metadata
|
||||
}
|
||||
|
||||
// Metadata returns destination metadata
|
||||
func (s *PacketAdapter) Metadata() *C.Metadata {
|
||||
return s.metadata
|
||||
}
|
||||
|
||||
// NewPacket is PacketAdapter generator
|
||||
func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type, additions ...Addition) (C.UDPPacket, *C.Metadata) {
|
||||
func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type, additions ...Addition) C.PacketAdapter {
|
||||
metadata := parseSocksAddr(target)
|
||||
metadata.NetWork = C.UDP
|
||||
metadata.Type = source
|
||||
ApplyAdditions(metadata, WithSrcAddr(packet.LocalAddr()))
|
||||
if p, ok := packet.(C.UDPPacketInAddr); ok {
|
||||
ApplyAdditions(metadata, WithInAddr(p.InAddr()))
|
||||
for _, addition := range additions {
|
||||
addition.Apply(metadata)
|
||||
}
|
||||
if ip, port, err := parseAddr(packet.LocalAddr()); err == nil {
|
||||
metadata.SrcIP = ip
|
||||
metadata.SrcPort = port
|
||||
}
|
||||
if p, ok := packet.(C.UDPPacketInAddr); ok {
|
||||
if ip, port, err := parseAddr(p.InAddr()); err == nil {
|
||||
metadata.InIP = ip
|
||||
metadata.InPort = port
|
||||
}
|
||||
}
|
||||
ApplyAdditions(metadata, additions...)
|
||||
|
||||
return packet, metadata
|
||||
return &PacketAdapter{
|
||||
packet,
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
|
@ -2,17 +2,51 @@ package inbound
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/context"
|
||||
"github.com/Dreamacro/clash/transport/socks5"
|
||||
)
|
||||
|
||||
// NewSocket receive TCP inbound and return ConnContext
|
||||
func NewSocket(target socks5.Addr, conn net.Conn, source C.Type, additions ...Addition) (net.Conn, *C.Metadata) {
|
||||
func NewSocket(target socks5.Addr, conn net.Conn, source C.Type, additions ...Addition) *context.ConnContext {
|
||||
metadata := parseSocksAddr(target)
|
||||
metadata.NetWork = C.TCP
|
||||
metadata.Type = source
|
||||
ApplyAdditions(metadata, WithSrcAddr(conn.RemoteAddr()), WithInAddr(conn.LocalAddr()))
|
||||
ApplyAdditions(metadata, additions...)
|
||||
return conn, metadata
|
||||
for _, addition := range additions {
|
||||
addition.Apply(metadata)
|
||||
}
|
||||
|
||||
if ip, port, err := parseAddr(conn.RemoteAddr()); err == nil {
|
||||
metadata.SrcIP = ip
|
||||
metadata.SrcPort = port
|
||||
}
|
||||
if ip, port, err := parseAddr(conn.LocalAddr()); err == nil {
|
||||
metadata.InIP = ip
|
||||
metadata.InPort = port
|
||||
}
|
||||
|
||||
return context.NewConnContext(conn, metadata)
|
||||
}
|
||||
|
||||
func NewInner(conn net.Conn, address string) *context.ConnContext {
|
||||
metadata := &C.Metadata{}
|
||||
metadata.NetWork = C.TCP
|
||||
metadata.Type = C.INNER
|
||||
metadata.DNSMode = C.DNSNormal
|
||||
metadata.Process = C.ClashName
|
||||
if h, port, err := net.SplitHostPort(address); err == nil {
|
||||
if port, err := strconv.ParseUint(port, 10, 16); err == nil {
|
||||
metadata.DstPort = uint16(port)
|
||||
}
|
||||
if ip, err := netip.ParseAddr(h); err == nil {
|
||||
metadata.DstIP = ip
|
||||
} else {
|
||||
metadata.Host = h
|
||||
}
|
||||
}
|
||||
|
||||
return context.NewConnContext(conn, metadata)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
@ -61,3 +62,29 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
func parseAddr(addr net.Addr) (netip.Addr, uint16, error) {
|
||||
// Filter when net.Addr interface is nil
|
||||
if addr == nil {
|
||||
return netip.Addr{}, 0, errors.New("nil addr")
|
||||
}
|
||||
if rawAddr, ok := addr.(interface{ RawAddr() net.Addr }); ok {
|
||||
ip, port, err := parseAddr(rawAddr.RawAddr())
|
||||
if err == nil {
|
||||
return ip, port, err
|
||||
}
|
||||
}
|
||||
addrStr := addr.String()
|
||||
host, port, err := net.SplitHostPort(addrStr)
|
||||
if err != nil {
|
||||
return netip.Addr{}, 0, err
|
||||
}
|
||||
|
||||
var uint16Port uint16
|
||||
if port, err := strconv.ParseUint(port, 10, 16); err == nil {
|
||||
uint16Port = uint16(port)
|
||||
}
|
||||
|
||||
ip, err := netip.ParseAddr(host)
|
||||
return ip, uint16Port, err
|
||||
}
|
||||
|
@ -3,9 +3,6 @@ package outbound
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/netip"
|
||||
|
||||
N "github.com/Dreamacro/clash/common/net"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
"github.com/Dreamacro/clash/component/resolver"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
@ -15,11 +12,6 @@ type Direct struct {
|
||||
*Base
|
||||
}
|
||||
|
||||
type DirectOption struct {
|
||||
BasicOption
|
||||
Name string `proxy:"name"`
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||
opts = append(opts, dialer.WithResolver(resolver.DefaultResolver))
|
||||
@ -27,7 +19,7 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
N.TCPKeepAlive(c)
|
||||
tcpKeepAlive(c)
|
||||
return NewConn(c, d), nil
|
||||
}
|
||||
|
||||
@ -41,28 +33,13 @@ func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
|
||||
}
|
||||
metadata.DstIP = ip
|
||||
}
|
||||
pc, err := dialer.NewDialer(d.Base.DialOptions(opts...)...).ListenPacket(ctx, "udp", "", netip.AddrPortFrom(metadata.DstIP, metadata.DstPort))
|
||||
pc, err := dialer.ListenPacket(ctx, dialer.ParseNetwork("udp", metadata.DstIP), "", d.Base.DialOptions(opts...)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newPacketConn(pc, d), nil
|
||||
}
|
||||
|
||||
func NewDirectWithOption(option DirectOption) *Direct {
|
||||
return &Direct{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
tp: C.Direct,
|
||||
udp: true,
|
||||
tfo: option.TFO,
|
||||
mpTcp: option.MPTCP,
|
||||
iface: option.Interface,
|
||||
rmark: option.RoutingMark,
|
||||
prefer: C.NewDNSPrefer(option.IPVersion),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewDirect() *Direct {
|
||||
return &Direct{
|
||||
Base: &Base{
|
||||
|
@ -7,16 +7,14 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
N "github.com/Dreamacro/clash/common/net"
|
||||
"github.com/Dreamacro/clash/component/ca"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
"github.com/Dreamacro/clash/component/proxydialer"
|
||||
tlsC "github.com/Dreamacro/clash/component/tls"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
@ -76,7 +74,7 @@ func (h *Http) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metad
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
|
||||
}
|
||||
N.TCPKeepAlive(c)
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
@ -157,13 +155,19 @@ func NewHttp(option HttpOption) (*Http, error) {
|
||||
if option.SNI != "" {
|
||||
sni = option.SNI
|
||||
}
|
||||
var err error
|
||||
tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{
|
||||
InsecureSkipVerify: option.SkipCertVerify,
|
||||
ServerName: sni,
|
||||
}, option.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if len(option.Fingerprint) == 0 {
|
||||
tlsConfig = tlsC.GetGlobalTLSConfig(&tls.Config{
|
||||
InsecureSkipVerify: option.SkipCertVerify,
|
||||
ServerName: sni,
|
||||
})
|
||||
} else {
|
||||
var err error
|
||||
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(&tls.Config{
|
||||
InsecureSkipVerify: option.SkipCertVerify,
|
||||
ServerName: sni,
|
||||
}, option.Fingerprint); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,11 +2,16 @@ package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@ -14,9 +19,9 @@ import (
|
||||
"github.com/metacubex/quic-go/congestion"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
|
||||
"github.com/Dreamacro/clash/component/ca"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
"github.com/Dreamacro/clash/component/proxydialer"
|
||||
tlsC "github.com/Dreamacro/clash/component/tls"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
hyCongestion "github.com/Dreamacro/clash/transport/hysteria/congestion"
|
||||
@ -38,6 +43,8 @@ const (
|
||||
DefaultHopInterval = 10
|
||||
)
|
||||
|
||||
var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`)
|
||||
|
||||
type Hysteria struct {
|
||||
*Base
|
||||
|
||||
@ -46,7 +53,7 @@ type Hysteria struct {
|
||||
}
|
||||
|
||||
func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||
tcpConn, err := h.client.DialTCP(metadata.String(), metadata.DstPort, h.genHdc(ctx, opts...))
|
||||
tcpConn, err := h.client.DialTCP(metadata.RemoteAddress(), h.genHdc(ctx, opts...))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -113,12 +120,12 @@ type HysteriaOption struct {
|
||||
|
||||
func (c *HysteriaOption) Speed() (uint64, uint64, error) {
|
||||
var up, down uint64
|
||||
up = StringToBps(c.Up)
|
||||
up = stringToBps(c.Up)
|
||||
if up == 0 {
|
||||
return 0, 0, fmt.Errorf("invaild upload speed: %s", c.Up)
|
||||
}
|
||||
|
||||
down = StringToBps(c.Down)
|
||||
down = stringToBps(c.Down)
|
||||
if down == 0 {
|
||||
return 0, 0, fmt.Errorf("invaild download speed: %s", c.Down)
|
||||
}
|
||||
@ -146,10 +153,37 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
|
||||
var bs []byte
|
||||
var err error
|
||||
tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if len(option.CustomCA) > 0 {
|
||||
bs, err = os.ReadFile(option.CustomCA)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hysteria %s load ca error: %w", addr, err)
|
||||
}
|
||||
} else if option.CustomCAString != "" {
|
||||
bs = []byte(option.CustomCAString)
|
||||
}
|
||||
|
||||
if len(bs) > 0 {
|
||||
block, _ := pem.Decode(bs)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("CA cert is not PEM")
|
||||
}
|
||||
|
||||
fpBytes := sha256.Sum256(block.Bytes)
|
||||
if len(option.Fingerprint) == 0 {
|
||||
option.Fingerprint = hex.EncodeToString(fpBytes[:])
|
||||
}
|
||||
}
|
||||
|
||||
if len(option.Fingerprint) != 0 {
|
||||
var err error
|
||||
tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
|
||||
}
|
||||
|
||||
if len(option.ALPN) > 0 {
|
||||
@ -234,6 +268,42 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func stringToBps(s string) uint64 {
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
// when have not unit, use Mbps
|
||||
if v, err := strconv.Atoi(s); err == nil {
|
||||
return stringToBps(fmt.Sprintf("%d Mbps", v))
|
||||
}
|
||||
|
||||
m := rateStringRegexp.FindStringSubmatch(s)
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
var n uint64
|
||||
switch m[2] {
|
||||
case "K":
|
||||
n = 1 << 10
|
||||
case "M":
|
||||
n = 1 << 20
|
||||
case "G":
|
||||
n = 1 << 30
|
||||
case "T":
|
||||
n = 1 << 40
|
||||
default:
|
||||
n = 1
|
||||
}
|
||||
v, _ := strconv.ParseUint(m[1], 10, 64)
|
||||
n = v * n
|
||||
if m[3] == "b" {
|
||||
// Bits, need to convert to bytes
|
||||
n = n >> 3
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
type hyPacketConn struct {
|
||||
core.UDPConn
|
||||
}
|
||||
|
@ -1,157 +0,0 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
CN "github.com/Dreamacro/clash/common/net"
|
||||
"github.com/Dreamacro/clash/component/ca"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
"github.com/Dreamacro/clash/component/proxydialer"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
tuicCommon "github.com/Dreamacro/clash/transport/tuic/common"
|
||||
|
||||
"github.com/metacubex/sing-quic/hysteria2"
|
||||
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
)
|
||||
|
||||
func init() {
|
||||
hysteria2.SetCongestionController = tuicCommon.SetCongestionController
|
||||
}
|
||||
|
||||
type Hysteria2 struct {
|
||||
*Base
|
||||
|
||||
option *Hysteria2Option
|
||||
client *hysteria2.Client
|
||||
dialer proxydialer.SingDialer
|
||||
}
|
||||
|
||||
type Hysteria2Option struct {
|
||||
BasicOption
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
Up string `proxy:"up,omitempty"`
|
||||
Down string `proxy:"down,omitempty"`
|
||||
Password string `proxy:"password,omitempty"`
|
||||
Obfs string `proxy:"obfs,omitempty"`
|
||||
ObfsPassword string `proxy:"obfs-password,omitempty"`
|
||||
SNI string `proxy:"sni,omitempty"`
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
Fingerprint string `proxy:"fingerprint,omitempty"`
|
||||
ALPN []string `proxy:"alpn,omitempty"`
|
||||
CustomCA string `proxy:"ca,omitempty"`
|
||||
CustomCAString string `proxy:"ca-str,omitempty"`
|
||||
CWND int `proxy:"cwnd,omitempty"`
|
||||
}
|
||||
|
||||
func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ 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))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewConn(CN.NewRefConn(c, h), h), nil
|
||||
}
|
||||
|
||||
func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
||||
options := h.Base.DialOptions(opts...)
|
||||
h.dialer.SetDialer(dialer.NewDialer(options...))
|
||||
pc, err := h.client.ListenPacket(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pc == nil {
|
||||
return nil, errors.New("packetConn is nil")
|
||||
}
|
||||
return newPacketConn(CN.NewRefPacketConn(CN.NewThreadSafePacketConn(pc), h), h), nil
|
||||
}
|
||||
|
||||
func closeHysteria2(h *Hysteria2) {
|
||||
if h.client != nil {
|
||||
_ = h.client.CloseWithError(errors.New("proxy removed"))
|
||||
}
|
||||
}
|
||||
|
||||
func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
|
||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||
var salamanderPassword string
|
||||
if len(option.Obfs) > 0 {
|
||||
if option.ObfsPassword == "" {
|
||||
return nil, errors.New("missing obfs password")
|
||||
}
|
||||
switch option.Obfs {
|
||||
case hysteria2.ObfsTypeSalamander:
|
||||
salamanderPassword = option.ObfsPassword
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown obfs type: %s", option.Obfs)
|
||||
}
|
||||
}
|
||||
|
||||
serverName := option.Server
|
||||
if option.SNI != "" {
|
||||
serverName = option.SNI
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: serverName,
|
||||
InsecureSkipVerify: option.SkipCertVerify,
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
|
||||
var err error
|
||||
tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(option.ALPN) > 0 {
|
||||
tlsConfig.NextProtos = option.ALPN
|
||||
}
|
||||
|
||||
singDialer := proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer())
|
||||
|
||||
clientOptions := hysteria2.ClientOptions{
|
||||
Context: context.TODO(),
|
||||
Dialer: singDialer,
|
||||
ServerAddress: M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)),
|
||||
SendBPS: StringToBps(option.Up),
|
||||
ReceiveBPS: StringToBps(option.Down),
|
||||
SalamanderPassword: salamanderPassword,
|
||||
Password: option.Password,
|
||||
TLSConfig: tlsConfig,
|
||||
UDPDisabled: false,
|
||||
CWND: option.CWND,
|
||||
}
|
||||
|
||||
client, err := hysteria2.NewClient(clientOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
runtime.SetFinalizer(outbound, closeHysteria2)
|
||||
|
||||
return outbound, nil
|
||||
}
|
@ -15,10 +15,6 @@ type Reject struct {
|
||||
*Base
|
||||
}
|
||||
|
||||
type RejectOption struct {
|
||||
Name string `proxy:"name"`
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||
return NewConn(nopConn{}, r), nil
|
||||
@ -29,16 +25,6 @@ func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
|
||||
return newPacketConn(nopPacketConn{}, r), nil
|
||||
}
|
||||
|
||||
func NewRejectWithOption(option RejectOption) *Reject {
|
||||
return &Reject{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
tp: C.Direct,
|
||||
udp: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewReject() *Reject {
|
||||
return &Reject{
|
||||
Base: &Base{
|
||||
|
@ -19,7 +19,7 @@ import (
|
||||
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
|
||||
|
||||
restlsC "github.com/3andne/restls-client-go"
|
||||
shadowsocks "github.com/metacubex/sing-shadowsocks2"
|
||||
"github.com/metacubex/sing-shadowsocks2"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/common/uot"
|
||||
)
|
||||
@ -58,15 +58,14 @@ type simpleObfsOption struct {
|
||||
}
|
||||
|
||||
type v2rayObfsOption struct {
|
||||
Mode string `obfs:"mode"`
|
||||
Host string `obfs:"host,omitempty"`
|
||||
Path string `obfs:"path,omitempty"`
|
||||
TLS bool `obfs:"tls,omitempty"`
|
||||
Fingerprint string `obfs:"fingerprint,omitempty"`
|
||||
Headers map[string]string `obfs:"headers,omitempty"`
|
||||
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
||||
Mux bool `obfs:"mux,omitempty"`
|
||||
V2rayHttpUpgrade bool `obfs:"v2ray-http-upgrade,omitempty"`
|
||||
Mode string `obfs:"mode"`
|
||||
Host string `obfs:"host,omitempty"`
|
||||
Path string `obfs:"path,omitempty"`
|
||||
TLS bool `obfs:"tls,omitempty"`
|
||||
Fingerprint string `obfs:"fingerprint,omitempty"`
|
||||
Headers map[string]string `obfs:"headers,omitempty"`
|
||||
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
||||
Mux bool `obfs:"mux,omitempty"`
|
||||
}
|
||||
|
||||
type shadowTLSOption struct {
|
||||
@ -124,9 +123,9 @@ func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metada
|
||||
}
|
||||
}
|
||||
if useEarly {
|
||||
return ss.method.DialEarlyConn(c, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)), nil
|
||||
return ss.method.DialEarlyConn(c, M.ParseSocksaddr(metadata.RemoteAddress())), nil
|
||||
} else {
|
||||
return ss.method.DialConn(c, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
|
||||
return ss.method.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,7 +146,7 @@ func (ss *ShadowSocks) DialContextWithDialer(ctx context.Context, dialer C.Diale
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||
}
|
||||
N.TCPKeepAlive(c)
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
@ -260,11 +259,10 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||
}
|
||||
obfsMode = opts.Mode
|
||||
v2rayOption = &v2rayObfs.Option{
|
||||
Host: opts.Host,
|
||||
Path: opts.Path,
|
||||
Headers: opts.Headers,
|
||||
Mux: opts.Mux,
|
||||
V2rayHttpUpgrade: opts.V2rayHttpUpgrade,
|
||||
Host: opts.Host,
|
||||
Path: opts.Path,
|
||||
Headers: opts.Headers,
|
||||
Mux: opts.Mux,
|
||||
}
|
||||
|
||||
if opts.TLS {
|
||||
@ -296,6 +294,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||
}
|
||||
|
||||
restlsConfig, err = restlsC.NewRestlsConfig(restlsOpt.Host, restlsOpt.Password, restlsOpt.VersionHint, restlsOpt.RestlsScript, option.ClientFingerprint)
|
||||
restlsConfig.SessionTicketsDisabled = true
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ss %s initialize restls-plugin error: %w", addr, err)
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ func (ssr *ShadowSocksR) DialContextWithDialer(ctx context.Context, dialer C.Dia
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err)
|
||||
}
|
||||
N.TCPKeepAlive(c)
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
|
@ -3,6 +3,7 @@ package outbound
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"runtime"
|
||||
|
||||
CN "github.com/Dreamacro/clash/common/net"
|
||||
@ -14,13 +15,14 @@ import (
|
||||
mux "github.com/sagernet/sing-mux"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type SingMux struct {
|
||||
C.ProxyAdapter
|
||||
base ProxyBase
|
||||
client *mux.Client
|
||||
dialer proxydialer.SingDialer
|
||||
dialer *muxSingDialer
|
||||
onlyTcp bool
|
||||
}
|
||||
|
||||
@ -39,10 +41,28 @@ type ProxyBase interface {
|
||||
DialOptions(opts ...dialer.Option) []dialer.Option
|
||||
}
|
||||
|
||||
type muxSingDialer struct {
|
||||
dialer dialer.Dialer
|
||||
proxy C.ProxyAdapter
|
||||
statistic bool
|
||||
}
|
||||
|
||||
var _ N.Dialer = (*muxSingDialer)(nil)
|
||||
|
||||
func (d *muxSingDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
var cDialer C.Dialer = proxydialer.New(d.proxy, d.dialer, d.statistic)
|
||||
return cDialer.DialContext(ctx, network, destination.String())
|
||||
}
|
||||
|
||||
func (d *muxSingDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
var cDialer C.Dialer = proxydialer.New(d.proxy, d.dialer, d.statistic)
|
||||
return cDialer.ListenPacket(ctx, "udp", "", destination.AddrPort())
|
||||
}
|
||||
|
||||
func (s *SingMux) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
||||
options := s.base.DialOptions(opts...)
|
||||
s.dialer.SetDialer(dialer.NewDialer(options...))
|
||||
c, err := s.client.DialContext(ctx, "tcp", M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
|
||||
s.dialer.dialer = dialer.NewDialer(options...)
|
||||
c, err := s.client.DialContext(ctx, "tcp", M.ParseSocksaddr(metadata.RemoteAddress()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -54,7 +74,7 @@ func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
|
||||
return s.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
|
||||
}
|
||||
options := s.base.DialOptions(opts...)
|
||||
s.dialer.SetDialer(dialer.NewDialer(options...))
|
||||
s.dialer.dialer = dialer.NewDialer(options...)
|
||||
|
||||
// sing-mux use stream-oriented udp with a special address, so we need a net.UDPAddr
|
||||
if !metadata.Resolved() {
|
||||
@ -94,7 +114,7 @@ func closeSingMux(s *SingMux) {
|
||||
}
|
||||
|
||||
func NewSingMux(option SingMuxOption, proxy C.ProxyAdapter, base ProxyBase) (C.ProxyAdapter, error) {
|
||||
singDialer := proxydialer.NewSingDialer(proxy, dialer.NewDialer(), option.Statistic)
|
||||
singDialer := &muxSingDialer{dialer: dialer.NewDialer(), proxy: proxy, statistic: option.Statistic}
|
||||
client, err := mux.NewClient(mux.Options{
|
||||
Dialer: singDialer,
|
||||
Protocol: option.Protocol,
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
N "github.com/Dreamacro/clash/common/net"
|
||||
"github.com/Dreamacro/clash/common/structure"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
"github.com/Dreamacro/clash/component/proxydialer"
|
||||
@ -94,7 +93,7 @@ func (s *Snell) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
|
||||
}
|
||||
N.TCPKeepAlive(c)
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
@ -122,7 +121,7 @@ func (s *Snell) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
N.TCPKeepAlive(c)
|
||||
tcpKeepAlive(c)
|
||||
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
|
||||
|
||||
err = snell.WriteUDPHeader(c, s.version)
|
||||
@ -208,7 +207,7 @@ func NewSnell(option SnellOption) (*Snell, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
N.TCPKeepAlive(c)
|
||||
tcpKeepAlive(c)
|
||||
return streamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil
|
||||
})
|
||||
}
|
||||
|
@ -7,13 +7,11 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
|
||||
N "github.com/Dreamacro/clash/common/net"
|
||||
"github.com/Dreamacro/clash/component/ca"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
"github.com/Dreamacro/clash/component/proxydialer"
|
||||
tlsC "github.com/Dreamacro/clash/component/tls"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/transport/socks5"
|
||||
)
|
||||
@ -82,7 +80,7 @@ func (ss *Socks5) DialContextWithDialer(ctx context.Context, dialer C.Dialer, me
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||
}
|
||||
N.TCPKeepAlive(c)
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
@ -128,7 +126,7 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
|
||||
N.TCPKeepAlive(c)
|
||||
tcpKeepAlive(c)
|
||||
var user *socks5.User
|
||||
if ss.user != "" {
|
||||
user = &socks5.User{
|
||||
@ -137,8 +135,7 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
|
||||
}
|
||||
}
|
||||
|
||||
udpAssocateAddr := socks5.AddrFromStdAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), 0))
|
||||
bindAddr, err := socks5.ClientHandshake(c, udpAssocateAddr, socks5.CmdUDPAssociate, user)
|
||||
bindAddr, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdUDPAssociate, user)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("client hanshake error: %w", err)
|
||||
return
|
||||
@ -158,7 +155,7 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
|
||||
bindUDPAddr.IP = serverAddr.IP
|
||||
}
|
||||
|
||||
pc, err := cDialer.ListenPacket(ctx, "udp", "", bindUDPAddr.AddrPort())
|
||||
pc, err := dialer.ListenPacket(ctx, dialer.ParseNetwork("udp", bindUDPAddr.AddrPort().Addr()), "", ss.Base.DialOptions(opts...)...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -182,10 +179,13 @@ func NewSocks5(option Socks5Option) (*Socks5, error) {
|
||||
ServerName: option.Server,
|
||||
}
|
||||
|
||||
var err error
|
||||
tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if len(option.Fingerprint) == 0 {
|
||||
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
|
||||
} else {
|
||||
var err error
|
||||
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,8 +8,6 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
N "github.com/Dreamacro/clash/common/net"
|
||||
"github.com/Dreamacro/clash/component/ca"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
"github.com/Dreamacro/clash/component/proxydialer"
|
||||
tlsC "github.com/Dreamacro/clash/component/tls"
|
||||
@ -53,10 +51,9 @@ func (t *Trojan) plainStream(ctx context.Context, c net.Conn) (net.Conn, error)
|
||||
if t.option.Network == "ws" {
|
||||
host, port, _ := net.SplitHostPort(t.addr)
|
||||
wsOpts := &trojan.WebsocketOption{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Path: t.option.WSOpts.Path,
|
||||
V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade,
|
||||
Host: host,
|
||||
Port: port,
|
||||
Path: t.option.WSOpts.Path,
|
||||
}
|
||||
|
||||
if t.option.SNI != "" {
|
||||
@ -134,7 +131,7 @@ func (t *Trojan) DialContextWithDialer(ctx context.Context, dialer C.Dialer, met
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||
}
|
||||
N.TCPKeepAlive(c)
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
@ -187,7 +184,7 @@ func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, me
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
N.TCPKeepAlive(c)
|
||||
tcpKeepAlive(c)
|
||||
c, err = t.plainStream(ctx, c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||
@ -271,7 +268,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error())
|
||||
}
|
||||
N.TCPKeepAlive(c)
|
||||
tcpKeepAlive(c)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@ -282,10 +279,13 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
|
||||
ServerName: tOption.ServerName,
|
||||
}
|
||||
|
||||
var err error
|
||||
tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if len(option.Fingerprint) == 0 {
|
||||
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
|
||||
} else {
|
||||
var err error
|
||||
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, tOption.ClientFingerprint, t.realityConfig)
|
||||
|
@ -2,25 +2,25 @@ package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/component/ca"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
"github.com/Dreamacro/clash/component/proxydialer"
|
||||
"github.com/Dreamacro/clash/component/resolver"
|
||||
tlsC "github.com/Dreamacro/clash/component/tls"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/transport/tuic"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"github.com/metacubex/quic-go"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/common/uot"
|
||||
)
|
||||
|
||||
type Tuic struct {
|
||||
@ -59,9 +59,6 @@ type TuicOption struct {
|
||||
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
|
||||
MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"`
|
||||
SNI string `proxy:"sni,omitempty"`
|
||||
|
||||
UDPOverStream bool `proxy:"udp-over-stream,omitempty"`
|
||||
UDPOverStreamVersion int `proxy:"udp-over-stream-version,omitempty"`
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
@ -85,32 +82,6 @@ func (t *Tuic) ListenPacketContext(ctx context.Context, metadata *C.Metadata, op
|
||||
|
||||
// ListenPacketWithDialer implements C.ProxyAdapter
|
||||
func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
if t.option.UDPOverStream {
|
||||
uotDestination := uot.RequestDestination(uint8(t.option.UDPOverStreamVersion))
|
||||
uotMetadata := *metadata
|
||||
uotMetadata.Host = uotDestination.Fqdn
|
||||
uotMetadata.DstPort = uotDestination.Port
|
||||
c, err := t.DialContextWithDialer(ctx, dialer, &uotMetadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// tuic uos use stream-oriented udp with a special address, so we need a net.UDPAddr
|
||||
if !metadata.Resolved() {
|
||||
ip, err := resolver.ResolveIP(ctx, metadata.Host)
|
||||
if err != nil {
|
||||
return nil, errors.New("can't resolve ip")
|
||||
}
|
||||
metadata.DstIP = ip
|
||||
}
|
||||
|
||||
destination := M.SocksaddrFromNet(metadata.UDPAddr())
|
||||
if t.option.UDPOverStreamVersion == uot.LegacyVersion {
|
||||
return newPacketConn(uot.NewConn(c, uot.Request{Destination: destination}), t), nil
|
||||
} else {
|
||||
return newPacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination}), t), nil
|
||||
}
|
||||
}
|
||||
pc, err := t.client.ListenPacketWithDialer(ctx, metadata, dialer, t.dialWithDialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -158,10 +129,37 @@ func NewTuic(option TuicOption) (*Tuic, error) {
|
||||
tlsConfig.ServerName = option.SNI
|
||||
}
|
||||
|
||||
var bs []byte
|
||||
var err error
|
||||
tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if len(option.CustomCA) > 0 {
|
||||
bs, err = os.ReadFile(option.CustomCA)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tuic %s load ca error: %w", addr, err)
|
||||
}
|
||||
} else if option.CustomCAString != "" {
|
||||
bs = []byte(option.CustomCAString)
|
||||
}
|
||||
|
||||
if len(bs) > 0 {
|
||||
block, _ := pem.Decode(bs)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("CA cert is not PEM")
|
||||
}
|
||||
|
||||
fpBytes := sha256.Sum256(block.Bytes)
|
||||
if len(option.Fingerprint) == 0 {
|
||||
option.Fingerprint = hex.EncodeToString(fpBytes[:])
|
||||
}
|
||||
}
|
||||
|
||||
if len(option.Fingerprint) != 0 {
|
||||
var err error
|
||||
tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
|
||||
}
|
||||
|
||||
if option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array
|
||||
@ -241,14 +239,6 @@ func NewTuic(option TuicOption) (*Tuic, error) {
|
||||
tlsConfig.InsecureSkipVerify = true // tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config
|
||||
}
|
||||
|
||||
switch option.UDPOverStreamVersion {
|
||||
case uot.Version, uot.LegacyVersion:
|
||||
case 0:
|
||||
option.UDPOverStreamVersion = uot.LegacyVersion
|
||||
default:
|
||||
return nil, fmt.Errorf("tuic %s unknown udp over stream protocol version: %d", addr, option.UDPOverStreamVersion)
|
||||
}
|
||||
|
||||
t := &Tuic{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
@ -292,10 +282,6 @@ func NewTuic(option TuicOption) (*Tuic, error) {
|
||||
|
||||
t.client = tuic.NewPoolClientV4(clientOption)
|
||||
} else {
|
||||
maxUdpRelayPacketSize := option.MaxUdpRelayPacketSize
|
||||
if maxUdpRelayPacketSize > tuic.MaxFragSizeV5 {
|
||||
maxUdpRelayPacketSize = tuic.MaxFragSizeV5
|
||||
}
|
||||
clientOption := &tuic.ClientOptionV5{
|
||||
TlsConfig: tlsConfig,
|
||||
QuicConfig: quicConfig,
|
||||
@ -304,7 +290,7 @@ func NewTuic(option TuicOption) (*Tuic, error) {
|
||||
UdpRelayMode: udpRelayMode,
|
||||
CongestionController: option.CongestionController,
|
||||
ReduceRtt: option.ReduceRtt,
|
||||
MaxUdpRelayPacketSize: maxUdpRelayPacketSize,
|
||||
MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize,
|
||||
MaxOpenStreams: clientMaxOpenStreams,
|
||||
CWND: option.CWND,
|
||||
}
|
||||
|
@ -4,12 +4,10 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/component/resolver"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
@ -21,6 +19,13 @@ var (
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func tcpKeepAlive(c net.Conn) {
|
||||
if tcp, ok := c.(*net.TCPConn); ok {
|
||||
_ = tcp.SetKeepAlive(true)
|
||||
_ = tcp.SetKeepAlivePeriod(30 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func getClientSessionCache() tls.ClientSessionCache {
|
||||
once.Do(func() {
|
||||
globalClientSessionCache = tls.NewLRUClientSessionCache(128)
|
||||
@ -123,41 +128,3 @@ func safeConnClose(c net.Conn, err error) {
|
||||
_ = c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`)
|
||||
|
||||
func StringToBps(s string) uint64 {
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
// when have not unit, use Mbps
|
||||
if v, err := strconv.Atoi(s); err == nil {
|
||||
return StringToBps(fmt.Sprintf("%d Mbps", v))
|
||||
}
|
||||
|
||||
m := rateStringRegexp.FindStringSubmatch(s)
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
var n uint64
|
||||
switch m[2] {
|
||||
case "K":
|
||||
n = 1 << 10
|
||||
case "M":
|
||||
n = 1 << 20
|
||||
case "G":
|
||||
n = 1 << 30
|
||||
case "T":
|
||||
n = 1 << 40
|
||||
default:
|
||||
n = 1
|
||||
}
|
||||
v, _ := strconv.ParseUint(m[1], 10, 64)
|
||||
n = v * n
|
||||
if m[3] == "b" {
|
||||
// Bits, need to convert to bytes
|
||||
n = n >> 3
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ import (
|
||||
"github.com/Dreamacro/clash/common/convert"
|
||||
N "github.com/Dreamacro/clash/common/net"
|
||||
"github.com/Dreamacro/clash/common/utils"
|
||||
"github.com/Dreamacro/clash/component/ca"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
"github.com/Dreamacro/clash/component/proxydialer"
|
||||
"github.com/Dreamacro/clash/component/resolver"
|
||||
@ -58,7 +57,6 @@ type VlessOption struct {
|
||||
UUID string `proxy:"uuid"`
|
||||
Flow string `proxy:"flow,omitempty"`
|
||||
TLS bool `proxy:"tls,omitempty"`
|
||||
ALPN []string `proxy:"alpn,omitempty"`
|
||||
UDP bool `proxy:"udp,omitempty"`
|
||||
PacketAddr bool `proxy:"packet-addr,omitempty"`
|
||||
XUDP bool `proxy:"xudp,omitempty"`
|
||||
@ -93,7 +91,6 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
|
||||
Path: v.option.WSOpts.Path,
|
||||
MaxEarlyData: v.option.WSOpts.MaxEarlyData,
|
||||
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
|
||||
V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
|
||||
ClientFingerprint: v.option.ClientFingerprint,
|
||||
Headers: http.Header{},
|
||||
}
|
||||
@ -112,9 +109,13 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
|
||||
NextProtos: []string{"http/1.1"},
|
||||
}
|
||||
|
||||
wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if len(v.option.Fingerprint) == 0 {
|
||||
wsOpts.TLSConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
|
||||
} else {
|
||||
wsOpts.TLSConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if v.option.ServerName != "" {
|
||||
@ -210,7 +211,6 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne
|
||||
FingerPrint: v.option.Fingerprint,
|
||||
ClientFingerprint: v.option.ClientFingerprint,
|
||||
Reality: v.realityConfig,
|
||||
NextProtos: v.option.ALPN,
|
||||
}
|
||||
|
||||
if isH2 {
|
||||
@ -261,7 +261,7 @@ func (v *Vless) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||
}
|
||||
N.TCPKeepAlive(c)
|
||||
tcpKeepAlive(c)
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
@ -326,7 +326,7 @@ func (v *Vless) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||
}
|
||||
N.TCPKeepAlive(c)
|
||||
tcpKeepAlive(c)
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
@ -576,7 +576,7 @@ func NewVless(option VlessOption) (*Vless, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||
}
|
||||
N.TCPKeepAlive(c)
|
||||
tcpKeepAlive(c)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@ -590,7 +590,7 @@ func NewVless(option VlessOption) (*Vless, error) {
|
||||
}
|
||||
var tlsConfig *tls.Config
|
||||
if option.TLS {
|
||||
tlsConfig = ca.GetGlobalTLSConfig(&tls.Config{
|
||||
tlsConfig = tlsC.GetGlobalTLSConfig(&tls.Config{
|
||||
InsecureSkipVerify: v.option.SkipCertVerify,
|
||||
ServerName: v.option.ServerName,
|
||||
})
|
||||
|
@ -13,13 +13,11 @@ import (
|
||||
|
||||
N "github.com/Dreamacro/clash/common/net"
|
||||
"github.com/Dreamacro/clash/common/utils"
|
||||
"github.com/Dreamacro/clash/component/ca"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
"github.com/Dreamacro/clash/component/proxydialer"
|
||||
"github.com/Dreamacro/clash/component/resolver"
|
||||
tlsC "github.com/Dreamacro/clash/component/tls"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/ntp"
|
||||
"github.com/Dreamacro/clash/transport/gun"
|
||||
clashVMess "github.com/Dreamacro/clash/transport/vmess"
|
||||
|
||||
@ -54,7 +52,6 @@ type VmessOption struct {
|
||||
UDP bool `proxy:"udp,omitempty"`
|
||||
Network string `proxy:"network,omitempty"`
|
||||
TLS bool `proxy:"tls,omitempty"`
|
||||
ALPN []string `proxy:"alpn,omitempty"`
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
Fingerprint string `proxy:"fingerprint,omitempty"`
|
||||
ServerName string `proxy:"servername,omitempty"`
|
||||
@ -91,7 +88,6 @@ type WSOptions struct {
|
||||
Headers map[string]string `proxy:"headers,omitempty"`
|
||||
MaxEarlyData int `proxy:"max-early-data,omitempty"`
|
||||
EarlyDataHeaderName string `proxy:"early-data-header-name,omitempty"`
|
||||
V2rayHttpUpgrade bool `proxy:"v2ray-http-upgrade,omitempty"`
|
||||
}
|
||||
|
||||
// StreamConnContext implements C.ProxyAdapter
|
||||
@ -111,7 +107,6 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
|
||||
Path: v.option.WSOpts.Path,
|
||||
MaxEarlyData: v.option.WSOpts.MaxEarlyData,
|
||||
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
|
||||
V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
|
||||
ClientFingerprint: v.option.ClientFingerprint,
|
||||
Headers: http.Header{},
|
||||
}
|
||||
@ -130,9 +125,12 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
|
||||
NextProtos: []string{"http/1.1"},
|
||||
}
|
||||
|
||||
wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if len(v.option.Fingerprint) == 0 {
|
||||
wsOpts.TLSConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
|
||||
} else {
|
||||
if wsOpts.TLSConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if v.option.ServerName != "" {
|
||||
@ -151,7 +149,6 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
|
||||
SkipCertVerify: v.option.SkipCertVerify,
|
||||
ClientFingerprint: v.option.ClientFingerprint,
|
||||
Reality: v.realityConfig,
|
||||
NextProtos: v.option.ALPN,
|
||||
}
|
||||
|
||||
if v.option.ServerName != "" {
|
||||
@ -208,7 +205,6 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
|
||||
SkipCertVerify: v.option.SkipCertVerify,
|
||||
ClientFingerprint: v.option.ClientFingerprint,
|
||||
Reality: v.realityConfig,
|
||||
NextProtos: v.option.ALPN,
|
||||
}
|
||||
|
||||
if v.option.ServerName != "" {
|
||||
@ -262,10 +258,10 @@ func (v *Vmess) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err
|
||||
} else {
|
||||
if N.NeedHandshake(c) {
|
||||
conn = v.client.DialEarlyConn(c,
|
||||
M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
|
||||
M.ParseSocksaddr(metadata.RemoteAddress()))
|
||||
} else {
|
||||
conn, err = v.client.DialConn(c,
|
||||
M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
|
||||
M.ParseSocksaddr(metadata.RemoteAddress()))
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
@ -286,7 +282,7 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
|
||||
c, err = v.client.DialConn(c, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
|
||||
c, err = v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -308,7 +304,7 @@ func (v *Vmess) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||
}
|
||||
N.TCPKeepAlive(c)
|
||||
tcpKeepAlive(c)
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
@ -369,7 +365,7 @@ func (v *Vmess) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||
}
|
||||
N.TCPKeepAlive(c)
|
||||
tcpKeepAlive(c)
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
@ -417,7 +413,6 @@ func NewVmess(option VmessOption) (*Vmess, error) {
|
||||
if option.AuthenticatedLength {
|
||||
options = append(options, vmess.ClientWithAuthenticatedLength())
|
||||
}
|
||||
options = append(options, vmess.ClientWithTimeFunc(ntp.Now))
|
||||
client, err := vmess.NewClient(option.UUID, security, option.AlterID, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -469,7 +464,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||
}
|
||||
N.TCPKeepAlive(c)
|
||||
tcpKeepAlive(c)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@ -483,7 +478,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
|
||||
}
|
||||
var tlsConfig *tls.Config
|
||||
if option.TLS {
|
||||
tlsConfig = ca.GetGlobalTLSConfig(&tls.Config{
|
||||
tlsConfig = tlsC.GetGlobalTLSConfig(&tls.Config{
|
||||
InsecureSkipVerify: v.option.SkipCertVerify,
|
||||
ServerName: v.option.ServerName,
|
||||
})
|
||||
|
@ -27,6 +27,7 @@ import (
|
||||
"github.com/sagernet/sing/common/debug"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/wireguard-go/device"
|
||||
)
|
||||
|
||||
@ -35,7 +36,7 @@ type WireGuard struct {
|
||||
bind *wireguard.ClientBind
|
||||
device *device.Device
|
||||
tunDevice wireguard.Device
|
||||
dialer proxydialer.SingDialer
|
||||
dialer *wgSingDialer
|
||||
startOnce sync.Once
|
||||
startErr error
|
||||
resolver *dns.Resolver
|
||||
@ -69,6 +70,37 @@ type WireGuardPeerOption struct {
|
||||
AllowedIPs []string `proxy:"allowed-ips,omitempty"`
|
||||
}
|
||||
|
||||
type wgSingDialer struct {
|
||||
dialer dialer.Dialer
|
||||
proxyName string
|
||||
}
|
||||
|
||||
var _ N.Dialer = (*wgSingDialer)(nil)
|
||||
|
||||
func (d *wgSingDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
var cDialer C.Dialer = d.dialer
|
||||
if len(d.proxyName) > 0 {
|
||||
pd, err := proxydialer.NewByName(d.proxyName, d.dialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cDialer = pd
|
||||
}
|
||||
return cDialer.DialContext(ctx, network, destination.String())
|
||||
}
|
||||
|
||||
func (d *wgSingDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
var cDialer C.Dialer = d.dialer
|
||||
if len(d.proxyName) > 0 {
|
||||
pd, err := proxydialer.NewByName(d.proxyName, d.dialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cDialer = pd
|
||||
}
|
||||
return cDialer.ListenPacket(ctx, "udp", "", destination.AddrPort())
|
||||
}
|
||||
|
||||
type wgSingErrorHandler struct {
|
||||
name string
|
||||
}
|
||||
@ -136,7 +168,7 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) {
|
||||
rmark: option.RoutingMark,
|
||||
prefer: C.NewDNSPrefer(option.IPVersion),
|
||||
},
|
||||
dialer: proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer()),
|
||||
dialer: &wgSingDialer{dialer: dialer.NewDialer(), proxyName: option.DialerProxy},
|
||||
}
|
||||
runtime.SetFinalizer(outbound, closeWireGuard)
|
||||
|
||||
@ -270,7 +302,7 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) {
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create WireGuard device")
|
||||
}
|
||||
outbound.device = device.NewDevice(context.Background(), outbound.tunDevice, outbound.bind, &device.Logger{
|
||||
outbound.device = device.NewDevice(outbound.tunDevice, outbound.bind, &device.Logger{
|
||||
Verbosef: func(format string, args ...interface{}) {
|
||||
log.SingLogger.Debug(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...)))
|
||||
},
|
||||
@ -323,7 +355,7 @@ func closeWireGuard(w *WireGuard) {
|
||||
|
||||
func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
||||
options := w.Base.DialOptions(opts...)
|
||||
w.dialer.SetDialer(dialer.NewDialer(options...))
|
||||
w.dialer.dialer = dialer.NewDialer(options...)
|
||||
var conn net.Conn
|
||||
w.startOnce.Do(func() {
|
||||
w.startErr = w.tunDevice.Start()
|
||||
@ -355,7 +387,7 @@ func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts
|
||||
|
||||
func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
||||
options := w.Base.DialOptions(opts...)
|
||||
w.dialer.SetDialer(dialer.NewDialer(options...))
|
||||
w.dialer.dialer = dialer.NewDialer(options...)
|
||||
var pc net.PacketConn
|
||||
w.startOnce.Do(func() {
|
||||
w.startErr = w.tunDevice.Start()
|
||||
|
@ -28,7 +28,7 @@ type GroupBase struct {
|
||||
failedTestMux sync.Mutex
|
||||
failedTimes int
|
||||
failedTime time.Time
|
||||
failedTesting atomic.Bool
|
||||
failedTesting *atomic.Bool
|
||||
proxies [][]C.Proxy
|
||||
versions []atomic.Uint32
|
||||
}
|
||||
|
@ -1,5 +1,17 @@
|
||||
package outboundgroup
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
func tcpKeepAlive(c net.Conn) {
|
||||
if tcp, ok := c.(*net.TCPConn); ok {
|
||||
_ = tcp.SetKeepAlive(true)
|
||||
_ = tcp.SetKeepAlivePeriod(30 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
type SelectAble interface {
|
||||
Set(string) error
|
||||
ForceSet(name string)
|
||||
|
@ -92,13 +92,6 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
|
||||
break
|
||||
}
|
||||
proxy, err = outbound.NewHysteria(*hyOption)
|
||||
case "hysteria2":
|
||||
hyOption := &outbound.Hysteria2Option{}
|
||||
err = decoder.Decode(mapping, hyOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = outbound.NewHysteria2(*hyOption)
|
||||
case "wireguard":
|
||||
wgOption := &outbound.WireGuardOption{}
|
||||
err = decoder.Decode(mapping, wgOption)
|
||||
@ -113,20 +106,6 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
|
||||
break
|
||||
}
|
||||
proxy, err = outbound.NewTuic(*tuicOption)
|
||||
case "direct":
|
||||
directOption := &outbound.DirectOption{}
|
||||
err = decoder.Decode(mapping, directOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy = outbound.NewDirectWithOption(*directOption)
|
||||
case "reject":
|
||||
rejectOption := &outbound.RejectOption{}
|
||||
err = decoder.Decode(mapping, rejectOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy = outbound.NewRejectWithOption(*rejectOption)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
|
||||
}
|
||||
|
@ -34,12 +34,12 @@ type HealthCheck struct {
|
||||
url string
|
||||
extra map[string]*extraOption
|
||||
mu sync.Mutex
|
||||
started atomic.Bool
|
||||
started *atomic.Bool
|
||||
proxies []C.Proxy
|
||||
interval time.Duration
|
||||
interval uint
|
||||
lazy bool
|
||||
expectedStatus utils.IntRanges[uint16]
|
||||
lastTouch atomic.TypedValue[time.Time]
|
||||
lastTouch *atomic.Int64
|
||||
done chan struct{}
|
||||
singleDo *singledo.Single[struct{}]
|
||||
}
|
||||
@ -50,14 +50,13 @@ func (hc *HealthCheck) process() {
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(hc.interval)
|
||||
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
|
||||
hc.start()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
lastTouch := hc.lastTouch.Load()
|
||||
since := time.Since(lastTouch)
|
||||
if !hc.lazy || since < hc.interval {
|
||||
now := time.Now().Unix()
|
||||
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
|
||||
hc.check()
|
||||
} else {
|
||||
log.Debugln("Skip once health check because we are lazy")
|
||||
@ -86,7 +85,7 @@ func (hc *HealthCheck) registerHealthCheckTask(url string, expectedStatus utils.
|
||||
|
||||
// if the provider has not set up health checks, then modify it to be the same as the group's interval
|
||||
if hc.interval == 0 {
|
||||
hc.interval = time.Duration(interval) * time.Second
|
||||
hc.interval = interval
|
||||
}
|
||||
|
||||
if hc.extra == nil {
|
||||
@ -136,7 +135,7 @@ func (hc *HealthCheck) auto() bool {
|
||||
}
|
||||
|
||||
func (hc *HealthCheck) touch() {
|
||||
hc.lastTouch.Store(time.Now())
|
||||
hc.lastTouch.Store(time.Now().Unix())
|
||||
}
|
||||
|
||||
func (hc *HealthCheck) start() {
|
||||
@ -229,9 +228,11 @@ func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool, exp
|
||||
proxies: proxies,
|
||||
url: url,
|
||||
extra: map[string]*extraOption{},
|
||||
interval: time.Duration(interval) * time.Second,
|
||||
started: atomic.NewBool(false),
|
||||
interval: interval,
|
||||
lazy: lazy,
|
||||
expectedStatus: expectedStatus,
|
||||
lastTouch: atomic.NewInt64(0),
|
||||
done: make(chan struct{}, 1),
|
||||
singleDo: singledo.NewSingle[struct{}](time.Second),
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/dlclark/regexp2"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@ -12,24 +13,45 @@ type SubscriptionInfo struct {
|
||||
Expire int64
|
||||
}
|
||||
|
||||
func NewSubscriptionInfo(userinfo string) (si *SubscriptionInfo, err error) {
|
||||
userinfo = strings.ToLower(userinfo)
|
||||
userinfo = strings.ReplaceAll(userinfo, " ", "")
|
||||
si = new(SubscriptionInfo)
|
||||
for _, field := range strings.Split(userinfo, ";") {
|
||||
switch name, value, _ := strings.Cut(field, "="); name {
|
||||
case "upload":
|
||||
si.Upload, err = strconv.ParseInt(value, 10, 64)
|
||||
case "download":
|
||||
si.Download, err = strconv.ParseInt(value, 10, 64)
|
||||
case "total":
|
||||
si.Total, err = strconv.ParseInt(value, 10, 64)
|
||||
case "expire":
|
||||
si.Expire, err = strconv.ParseInt(value, 10, 64)
|
||||
}
|
||||
func NewSubscriptionInfo(str string) (si *SubscriptionInfo, err error) {
|
||||
si = &SubscriptionInfo{}
|
||||
str = strings.ToLower(str)
|
||||
reTraffic := regexp2.MustCompile("upload=(\\d+); download=(\\d+); total=(\\d+)", 0)
|
||||
reExpire := regexp2.MustCompile("expire=(\\d+)", 0)
|
||||
|
||||
match, err := reTraffic.FindStringMatch(str)
|
||||
if err != nil || match == nil {
|
||||
return nil, err
|
||||
}
|
||||
group := match.Groups()
|
||||
si.Upload, err = str2uint64(group[1].String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
si.Download, err = str2uint64(group[2].String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
si.Total, err = str2uint64(group[3].String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
match, _ = reExpire.FindStringMatch(str)
|
||||
if match != nil {
|
||||
group = match.Groups()
|
||||
si.Expire, err = str2uint64(group[1].String())
|
||||
if err != nil {
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func str2uint64(str string) (int64, error) {
|
||||
i, err := strconv.ParseInt(str, 10, 64)
|
||||
return i, err
|
||||
}
|
||||
|
@ -11,9 +11,10 @@ type Bool struct {
|
||||
atomic.Bool
|
||||
}
|
||||
|
||||
func NewBool(val bool) (i Bool) {
|
||||
func NewBool(val bool) *Bool {
|
||||
i := &Bool{}
|
||||
i.Store(val)
|
||||
return
|
||||
return i
|
||||
}
|
||||
|
||||
func (i *Bool) MarshalJSON() ([]byte, error) {
|
||||
@ -38,11 +39,12 @@ type Pointer[T any] struct {
|
||||
atomic.Pointer[T]
|
||||
}
|
||||
|
||||
func NewPointer[T any](v *T) (p Pointer[T]) {
|
||||
func NewPointer[T any](v *T) *Pointer[T] {
|
||||
var p Pointer[T]
|
||||
if v != nil {
|
||||
p.Store(v)
|
||||
}
|
||||
return
|
||||
return &p
|
||||
}
|
||||
|
||||
func (p *Pointer[T]) MarshalJSON() ([]byte, error) {
|
||||
@ -66,9 +68,10 @@ type Int32 struct {
|
||||
atomic.Int32
|
||||
}
|
||||
|
||||
func NewInt32(val int32) (i Int32) {
|
||||
func NewInt32(val int32) *Int32 {
|
||||
i := &Int32{}
|
||||
i.Store(val)
|
||||
return
|
||||
return i
|
||||
}
|
||||
|
||||
func (i *Int32) MarshalJSON() ([]byte, error) {
|
||||
@ -93,9 +96,10 @@ type Int64 struct {
|
||||
atomic.Int64
|
||||
}
|
||||
|
||||
func NewInt64(val int64) (i Int64) {
|
||||
func NewInt64(val int64) *Int64 {
|
||||
i := &Int64{}
|
||||
i.Store(val)
|
||||
return
|
||||
return i
|
||||
}
|
||||
|
||||
func (i *Int64) MarshalJSON() ([]byte, error) {
|
||||
@ -120,9 +124,10 @@ type Uint32 struct {
|
||||
atomic.Uint32
|
||||
}
|
||||
|
||||
func NewUint32(val uint32) (i Uint32) {
|
||||
func NewUint32(val uint32) *Uint32 {
|
||||
i := &Uint32{}
|
||||
i.Store(val)
|
||||
return
|
||||
return i
|
||||
}
|
||||
|
||||
func (i *Uint32) MarshalJSON() ([]byte, error) {
|
||||
@ -147,9 +152,10 @@ type Uint64 struct {
|
||||
atomic.Uint64
|
||||
}
|
||||
|
||||
func NewUint64(val uint64) (i Uint64) {
|
||||
func NewUint64(val uint64) *Uint64 {
|
||||
i := &Uint64{}
|
||||
i.Store(val)
|
||||
return
|
||||
return i
|
||||
}
|
||||
|
||||
func (i *Uint64) MarshalJSON() ([]byte, error) {
|
||||
@ -174,9 +180,10 @@ type Uintptr struct {
|
||||
atomic.Uintptr
|
||||
}
|
||||
|
||||
func NewUintptr(val uintptr) (i Uintptr) {
|
||||
func NewUintptr(val uintptr) *Uintptr {
|
||||
i := &Uintptr{}
|
||||
i.Store(val)
|
||||
return
|
||||
return i
|
||||
}
|
||||
|
||||
func (i *Uintptr) MarshalJSON() ([]byte, error) {
|
||||
|
@ -12,7 +12,6 @@ func DefaultValue[T any]() T {
|
||||
|
||||
type TypedValue[T any] struct {
|
||||
value atomic.Value
|
||||
_ noCopy
|
||||
}
|
||||
|
||||
func (t *TypedValue[T]) Load() T {
|
||||
@ -52,13 +51,8 @@ func (t *TypedValue[T]) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewTypedValue[T any](t T) (v TypedValue[T]) {
|
||||
func NewTypedValue[T any](t T) *TypedValue[T] {
|
||||
v := &TypedValue[T]{}
|
||||
v.Store(t)
|
||||
return
|
||||
return v
|
||||
}
|
||||
|
||||
type noCopy struct{}
|
||||
|
||||
// Lock is a no-op used by -copylocks checker from `go vet`.
|
||||
func (*noCopy) Lock() {}
|
||||
func (*noCopy) Unlock() {}
|
||||
|
@ -10,11 +10,17 @@ const BufferSize = buf.BufferSize
|
||||
type Buffer = buf.Buffer
|
||||
|
||||
var New = buf.New
|
||||
var NewPacket = buf.NewPacket
|
||||
var NewSize = buf.NewSize
|
||||
var With = buf.With
|
||||
var As = buf.As
|
||||
|
||||
var KeepAlive = common.KeepAlive
|
||||
|
||||
//go:norace
|
||||
func Dup[T any](obj T) T {
|
||||
return common.Dup(obj)
|
||||
}
|
||||
|
||||
var (
|
||||
Must = common.Must
|
||||
Error = common.Error
|
||||
|
11
common/cache/lrucache.go
vendored
11
common/cache/lrucache.go
vendored
@ -7,8 +7,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/generics/list"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// Option is part of Functional Options Pattern
|
||||
@ -89,7 +87,7 @@ func (c *LruCache[K, V]) Get(key K) (V, bool) {
|
||||
|
||||
el := c.get(key)
|
||||
if el == nil {
|
||||
return lo.Empty[V](), false
|
||||
return getZero[V](), false
|
||||
}
|
||||
value := el.value
|
||||
|
||||
@ -121,7 +119,7 @@ func (c *LruCache[K, V]) GetWithExpire(key K) (V, time.Time, bool) {
|
||||
|
||||
el := c.get(key)
|
||||
if el == nil {
|
||||
return lo.Empty[V](), time.Time{}, false
|
||||
return getZero[V](), time.Time{}, false
|
||||
}
|
||||
|
||||
return el.value, time.Unix(el.expires, 0), true
|
||||
@ -261,3 +259,8 @@ type entry[K comparable, V any] struct {
|
||||
value V
|
||||
expires int64
|
||||
}
|
||||
|
||||
func getZero[T any]() T {
|
||||
var result T
|
||||
return result
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ func TestSplitArgs(t *testing.T) {
|
||||
|
||||
func TestExecCmd(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
_, err := ExecCmd("cmd -c 'dir'")
|
||||
_, err := ExecCmd("dir")
|
||||
assert.Nil(t, err)
|
||||
return
|
||||
}
|
||||
|
@ -50,9 +50,7 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
|
||||
hysteria["port"] = urlHysteria.Port()
|
||||
hysteria["sni"] = query.Get("peer")
|
||||
hysteria["obfs"] = query.Get("obfs")
|
||||
if alpn := query.Get("alpn"); alpn != "" {
|
||||
hysteria["alpn"] = strings.Split(alpn, ",")
|
||||
}
|
||||
hysteria["alpn"] = []string{query.Get("alpn")}
|
||||
hysteria["auth_str"] = query.Get("auth")
|
||||
hysteria["protocol"] = query.Get("protocol")
|
||||
up := query.Get("up")
|
||||
@ -68,79 +66,6 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
|
||||
hysteria["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure"))
|
||||
|
||||
proxies = append(proxies, hysteria)
|
||||
case "hysteria2":
|
||||
urlHysteria2, err := url.Parse(line)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
query := urlHysteria2.Query()
|
||||
name := uniqueName(names, urlHysteria2.Fragment)
|
||||
hysteria2 := make(map[string]any, 20)
|
||||
|
||||
hysteria2["name"] = name
|
||||
hysteria2["type"] = scheme
|
||||
hysteria2["server"] = urlHysteria2.Hostname()
|
||||
if port := urlHysteria2.Port(); port != "" {
|
||||
hysteria2["port"] = port
|
||||
} else {
|
||||
hysteria2["port"] = "443"
|
||||
}
|
||||
hysteria2["obfs"] = query.Get("obfs")
|
||||
hysteria2["obfs-password"] = query.Get("obfs-password")
|
||||
hysteria2["sni"] = query.Get("sni")
|
||||
hysteria2["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure"))
|
||||
if alpn := query.Get("alpn"); alpn != "" {
|
||||
hysteria2["alpn"] = strings.Split(alpn, ",")
|
||||
}
|
||||
if auth := urlHysteria2.User.String(); auth != "" {
|
||||
hysteria2["password"] = auth
|
||||
}
|
||||
hysteria2["fingerprint"] = query.Get("pinSHA256")
|
||||
hysteria2["down"] = query.Get("down")
|
||||
hysteria2["up"] = query.Get("up")
|
||||
|
||||
proxies = append(proxies, hysteria2)
|
||||
case "tuic":
|
||||
// A temporary unofficial TUIC share link standard
|
||||
// Modified from https://github.com/daeuniverse/dae/discussions/182
|
||||
// Changes:
|
||||
// 1. Support TUICv4, just replace uuid:password with token
|
||||
// 2. Remove `allow_insecure` field
|
||||
urlTUIC, err := url.Parse(line)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
query := urlTUIC.Query()
|
||||
|
||||
tuic := make(map[string]any, 20)
|
||||
tuic["name"] = uniqueName(names, urlTUIC.Fragment)
|
||||
tuic["type"] = scheme
|
||||
tuic["server"] = urlTUIC.Hostname()
|
||||
tuic["port"] = urlTUIC.Port()
|
||||
tuic["udp"] = true
|
||||
password, v5 := urlTUIC.User.Password()
|
||||
if v5 {
|
||||
tuic["uuid"] = urlTUIC.User.Username()
|
||||
tuic["password"] = password
|
||||
} else {
|
||||
tuic["token"] = urlTUIC.User.Username()
|
||||
}
|
||||
if cc := query.Get("congestion_control"); cc != "" {
|
||||
tuic["congestion-controller"] = cc
|
||||
}
|
||||
if alpn := query.Get("alpn"); alpn != "" {
|
||||
tuic["alpn"] = strings.Split(alpn, ",")
|
||||
}
|
||||
if sni := query.Get("sni"); sni != "" {
|
||||
tuic["sni"] = sni
|
||||
}
|
||||
if query.Get("disable_sni") == "1" {
|
||||
tuic["disable-sni"] = true
|
||||
}
|
||||
if udpRelayMode := query.Get("udp_relay_mode"); udpRelayMode != "" {
|
||||
tuic["udp-relay-mode"] = udpRelayMode
|
||||
}
|
||||
|
||||
case "trojan":
|
||||
urlTrojan, err := url.Parse(line)
|
||||
@ -161,12 +86,10 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
|
||||
trojan["udp"] = true
|
||||
trojan["skip-cert-verify"], _ = strconv.ParseBool(query.Get("allowInsecure"))
|
||||
|
||||
if sni := query.Get("sni"); sni != "" {
|
||||
sni := query.Get("sni")
|
||||
if sni != "" {
|
||||
trojan["sni"] = sni
|
||||
}
|
||||
if alpn := query.Get("alpn"); alpn != "" {
|
||||
trojan["alpn"] = strings.Split(alpn, ",")
|
||||
}
|
||||
|
||||
network := strings.ToLower(query.Get("type"))
|
||||
if network != "" {
|
||||
@ -294,9 +217,6 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
|
||||
if strings.HasSuffix(tls, "tls") {
|
||||
vmess["tls"] = true
|
||||
}
|
||||
if alpn, ok := values["alpn"].(string); ok {
|
||||
vmess["alpn"] = strings.Split(alpn, ",")
|
||||
}
|
||||
}
|
||||
|
||||
switch network {
|
||||
@ -412,7 +332,6 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
|
||||
}
|
||||
}
|
||||
proxies = append(proxies, ss)
|
||||
|
||||
case "ssr":
|
||||
dcBuf, err := encRaw.DecodeString(body)
|
||||
if err != nil {
|
||||
|
@ -1,35 +0,0 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// https://v2.hysteria.network/zh/docs/developers/URI-Scheme/
|
||||
func TestConvertsV2Ray_normal(t *testing.T) {
|
||||
hy2test := "hysteria2://letmein@example.com:8443/?insecure=1&obfs=salamander&obfs-password=gawrgura&pinSHA256=deadbeef&sni=real.example.com&up=114&down=514&alpn=h3,h4#hy2test"
|
||||
|
||||
expected := []map[string]interface{}{
|
||||
{
|
||||
"name": "hy2test",
|
||||
"type": "hysteria2",
|
||||
"server": "example.com",
|
||||
"port": "8443",
|
||||
"sni": "real.example.com",
|
||||
"obfs": "salamander",
|
||||
"obfs-password": "gawrgura",
|
||||
"alpn": []string{"h3", "h4"},
|
||||
"password": "letmein",
|
||||
"up": "114",
|
||||
"down": "514",
|
||||
"skip-cert-verify": true,
|
||||
"fingerprint": "deadbeef",
|
||||
},
|
||||
}
|
||||
|
||||
proxies, err := ConvertsV2Ray([]byte(hy2test))
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, proxies)
|
||||
}
|
@ -24,6 +24,8 @@ func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy m
|
||||
proxy["port"] = url.Port()
|
||||
proxy["uuid"] = url.User.Username()
|
||||
proxy["udp"] = true
|
||||
proxy["skip-cert-verify"] = false
|
||||
proxy["tls"] = false
|
||||
tls := strings.ToLower(query.Get("security"))
|
||||
if strings.HasSuffix(tls, "tls") || tls == "reality" {
|
||||
proxy["tls"] = true
|
||||
@ -32,9 +34,6 @@ func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy m
|
||||
} else {
|
||||
proxy["client-fingerprint"] = fingerprint
|
||||
}
|
||||
if alpn := query.Get("alpn"); alpn != "" {
|
||||
proxy["alpn"] = strings.Split(alpn, ",")
|
||||
}
|
||||
}
|
||||
if sni := query.Get("sni"); sni != "" {
|
||||
proxy["servername"] = sni
|
||||
|
@ -22,16 +22,6 @@ func NewBufferedConn(c net.Conn) *BufferedConn {
|
||||
return &BufferedConn{bufio.NewReader(c), NewExtendedConn(c), false}
|
||||
}
|
||||
|
||||
func WarpConnWithBioReader(c net.Conn, br *bufio.Reader) net.Conn {
|
||||
if br != nil && br.Buffered() > 0 {
|
||||
if bc, ok := c.(*BufferedConn); ok && bc.r == br {
|
||||
return bc
|
||||
}
|
||||
return &BufferedConn{br, NewExtendedConn(c), true}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Reader returns the internal bufio.Reader.
|
||||
func (c *BufferedConn) Reader() *bufio.Reader {
|
||||
return c.r
|
||||
|
@ -1,49 +0,0 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/Dreamacro/clash/common/buf"
|
||||
)
|
||||
|
||||
var _ ExtendedConn = (*CachedConn)(nil)
|
||||
|
||||
type CachedConn struct {
|
||||
ExtendedConn
|
||||
data []byte
|
||||
}
|
||||
|
||||
func NewCachedConn(c net.Conn, data []byte) *CachedConn {
|
||||
return &CachedConn{NewExtendedConn(c), data}
|
||||
}
|
||||
|
||||
func (c *CachedConn) Read(b []byte) (n int, err error) {
|
||||
if len(c.data) > 0 {
|
||||
n = copy(b, c.data)
|
||||
c.data = c.data[n:]
|
||||
return
|
||||
}
|
||||
return c.ExtendedConn.Read(b)
|
||||
}
|
||||
|
||||
func (c *CachedConn) ReadCached() *buf.Buffer { // call in sing/common/bufio.Copy
|
||||
if len(c.data) > 0 {
|
||||
return buf.As(c.data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CachedConn) Upstream() any {
|
||||
return c.ExtendedConn
|
||||
}
|
||||
|
||||
func (c *CachedConn) ReaderReplaceable() bool {
|
||||
if len(c.data) > 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *CachedConn) WriterReplaceable() bool {
|
||||
return true
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
)
|
||||
|
||||
// SetupContextForConn is a helper function that starts connection I/O interrupter goroutine.
|
||||
func SetupContextForConn(ctx context.Context, conn net.Conn) (done func(*error)) {
|
||||
var (
|
||||
quit = make(chan struct{})
|
||||
interrupt = make(chan error, 1)
|
||||
)
|
||||
go func() {
|
||||
select {
|
||||
case <-quit:
|
||||
interrupt <- nil
|
||||
case <-ctx.Done():
|
||||
// Close the connection, discarding the error
|
||||
_ = conn.Close()
|
||||
interrupt <- ctx.Err()
|
||||
}
|
||||
}()
|
||||
return func(inputErr *error) {
|
||||
close(quit)
|
||||
if ctxErr := <-interrupt; ctxErr != nil && inputErr != nil {
|
||||
// Return context error to user.
|
||||
inputErr = &ctxErr
|
||||
}
|
||||
}
|
||||
}
|
@ -4,11 +4,8 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var KeepAliveInterval = 15 * time.Second
|
||||
|
||||
func SplitNetworkType(s string) (string, string, error) {
|
||||
var (
|
||||
shecme string
|
||||
@ -47,10 +44,3 @@ func SplitHostPort(s string) (host, port string, hasPort bool, err error) {
|
||||
host, port, err = net.SplitHostPort(temp)
|
||||
return
|
||||
}
|
||||
|
||||
func TCPKeepAlive(c net.Conn) {
|
||||
if tcp, ok := c.(*net.TCPConn); ok {
|
||||
_ = tcp.SetKeepAlive(true)
|
||||
_ = tcp.SetKeepAlivePeriod(KeepAliveInterval)
|
||||
}
|
||||
}
|
||||
|
@ -10,11 +10,7 @@ import (
|
||||
"math/big"
|
||||
)
|
||||
|
||||
type Path interface {
|
||||
Resolve(path string) string
|
||||
}
|
||||
|
||||
func ParseCert(certificate, privateKey string, path Path) (tls.Certificate, error) {
|
||||
func ParseCert(certificate, privateKey string) (tls.Certificate, error) {
|
||||
if certificate == "" && privateKey == "" {
|
||||
return newRandomTLSKeyPair()
|
||||
}
|
||||
@ -23,8 +19,6 @@ func ParseCert(certificate, privateKey string, path Path) (tls.Certificate, erro
|
||||
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())
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -16,7 +15,7 @@ func sleepAndSend[T any](ctx context.Context, delay int, input T) func() (T, err
|
||||
case <-timer.C:
|
||||
return input, nil
|
||||
case <-ctx.Done():
|
||||
return lo.Empty[T](), ctx.Err()
|
||||
return getZero[T](), ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -36,6 +35,11 @@ func TestPicker_Timeout(t *testing.T) {
|
||||
picker.Go(sleepAndSend(ctx, 20, 1))
|
||||
|
||||
number := picker.Wait()
|
||||
assert.Equal(t, number, lo.Empty[int]())
|
||||
assert.Equal(t, number, getZero[int]())
|
||||
assert.NotNil(t, picker.Error())
|
||||
}
|
||||
|
||||
func getZero[T any]() T {
|
||||
var result T
|
||||
return result
|
||||
}
|
||||
|
@ -2,8 +2,6 @@ package queue
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// Queue is a simple concurrent safe queue
|
||||
@ -26,7 +24,7 @@ func (q *Queue[T]) Put(items ...T) {
|
||||
// Pop returns the head of items.
|
||||
func (q *Queue[T]) Pop() T {
|
||||
if len(q.items) == 0 {
|
||||
return lo.Empty[T]()
|
||||
return GetZero[T]()
|
||||
}
|
||||
|
||||
q.lock.Lock()
|
||||
@ -39,7 +37,7 @@ func (q *Queue[T]) Pop() T {
|
||||
// Last returns the last of item.
|
||||
func (q *Queue[T]) Last() T {
|
||||
if len(q.items) == 0 {
|
||||
return lo.Empty[T]()
|
||||
return GetZero[T]()
|
||||
}
|
||||
|
||||
q.lock.RLock()
|
||||
@ -71,3 +69,8 @@ func New[T any](hint int64) *Queue[T] {
|
||||
items: make([]T, 0, hint),
|
||||
}
|
||||
}
|
||||
|
||||
func GetZero[T any]() T {
|
||||
var result T
|
||||
return result
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
package util
|
||||
|
||||
import "github.com/samber/lo"
|
||||
|
||||
func EmptyOr[T comparable](v T, def T) T {
|
||||
ret, _ := lo.Coalesce(v, def)
|
||||
return ret
|
||||
}
|
@ -1,5 +1,9 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Authenticator interface {
|
||||
Verify(user string, pass string) bool
|
||||
Users() []string
|
||||
@ -11,12 +15,12 @@ type AuthUser struct {
|
||||
}
|
||||
|
||||
type inMemoryAuthenticator struct {
|
||||
storage map[string]string
|
||||
storage *sync.Map
|
||||
usernames []string
|
||||
}
|
||||
|
||||
func (au *inMemoryAuthenticator) Verify(user string, pass string) bool {
|
||||
realPass, ok := au.storage[user]
|
||||
realPass, ok := au.storage.Load(user)
|
||||
return ok && realPass == pass
|
||||
}
|
||||
|
||||
@ -26,13 +30,17 @@ func NewAuthenticator(users []AuthUser) Authenticator {
|
||||
if len(users) == 0 {
|
||||
return nil
|
||||
}
|
||||
au := &inMemoryAuthenticator{
|
||||
storage: make(map[string]string),
|
||||
usernames: make([]string, 0, len(users)),
|
||||
}
|
||||
|
||||
au := &inMemoryAuthenticator{storage: &sync.Map{}}
|
||||
for _, user := range users {
|
||||
au.storage[user.User] = user.Pass
|
||||
au.usernames = append(au.usernames, user.User)
|
||||
au.storage.Store(user.User, user.Pass)
|
||||
}
|
||||
usernames := make([]string, 0, len(users))
|
||||
au.storage.Range(func(key, value any) bool {
|
||||
usernames = append(usernames, key.(string))
|
||||
return true
|
||||
})
|
||||
au.usernames = usernames
|
||||
|
||||
return au
|
||||
}
|
||||
|
@ -14,15 +14,5 @@ func ListenDHCPClient(ctx context.Context, ifaceName string) (net.PacketConn, er
|
||||
listenAddr = "255.255.255.255:68"
|
||||
}
|
||||
|
||||
options := []dialer.Option{
|
||||
dialer.WithInterface(ifaceName),
|
||||
dialer.WithAddrReuse(true),
|
||||
}
|
||||
|
||||
// fallback bind on windows, because syscall bind can not receive broadcast
|
||||
if runtime.GOOS == "windows" {
|
||||
options = append(options, dialer.WithFallbackBind(true))
|
||||
}
|
||||
|
||||
return dialer.ListenPacket(ctx, "udp4", listenAddr, options...)
|
||||
return dialer.ListenPacket(ctx, "udp4", listenAddr, dialer.WithInterface(ifaceName), dialer.WithAddrReuse(true))
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package dialer
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Dreamacro/clash/component/iface"
|
||||
@ -15,7 +14,7 @@ func LookupLocalAddrFromIfaceName(ifaceName string, network string, destination
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var addr netip.Prefix
|
||||
var addr *netip.Prefix
|
||||
switch network {
|
||||
case "udp4", "tcp4":
|
||||
addr, err = ifaceObj.PickIPv4Addr(destination)
|
||||
@ -50,52 +49,3 @@ func LookupLocalAddrFromIfaceName(ifaceName string, network string, destination
|
||||
|
||||
return nil, iface.ErrAddrNotFound
|
||||
}
|
||||
|
||||
func fallbackBindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination netip.Addr) error {
|
||||
if !destination.IsGlobalUnicast() {
|
||||
return nil
|
||||
}
|
||||
|
||||
local := uint64(0)
|
||||
if dialer.LocalAddr != nil {
|
||||
_, port, err := net.SplitHostPort(dialer.LocalAddr.String())
|
||||
if err == nil {
|
||||
local, _ = strconv.ParseUint(port, 10, 16)
|
||||
}
|
||||
}
|
||||
|
||||
addr, err := LookupLocalAddrFromIfaceName(ifaceName, network, destination, int(local))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dialer.LocalAddr = addr
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fallbackBindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, address string) (string, error) {
|
||||
_, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
port = "0"
|
||||
}
|
||||
|
||||
local, _ := strconv.ParseUint(port, 10, 16)
|
||||
|
||||
addr, err := LookupLocalAddrFromIfaceName(ifaceName, network, netip.Addr{}, int(local))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return addr.String(), nil
|
||||
}
|
||||
|
||||
func fallbackParseNetwork(network string, addr netip.Addr) string {
|
||||
// fix fallbackBindIfaceToListenConfig() force bind to an ipv4 address
|
||||
if !strings.HasSuffix(network, "4") &&
|
||||
!strings.HasSuffix(network, "6") &&
|
||||
addr.Unmap().Is6() {
|
||||
network += "6"
|
||||
}
|
||||
return network
|
||||
}
|
||||
|
@ -5,16 +5,55 @@ package dialer
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination netip.Addr) error {
|
||||
return fallbackBindIfaceToDialer(ifaceName, dialer, network, destination)
|
||||
if !destination.IsGlobalUnicast() {
|
||||
return nil
|
||||
}
|
||||
|
||||
local := uint64(0)
|
||||
if dialer.LocalAddr != nil {
|
||||
_, port, err := net.SplitHostPort(dialer.LocalAddr.String())
|
||||
if err == nil {
|
||||
local, _ = strconv.ParseUint(port, 10, 16)
|
||||
}
|
||||
}
|
||||
|
||||
addr, err := LookupLocalAddrFromIfaceName(ifaceName, network, destination, int(local))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dialer.LocalAddr = addr
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, network, address string) (string, error) {
|
||||
return fallbackBindIfaceToListenConfig(ifaceName, lc, network, address)
|
||||
func bindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, address string) (string, error) {
|
||||
_, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
port = "0"
|
||||
}
|
||||
|
||||
local, _ := strconv.ParseUint(port, 10, 16)
|
||||
|
||||
addr, err := LookupLocalAddrFromIfaceName(ifaceName, network, netip.Addr{}, int(local))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return addr.String(), nil
|
||||
}
|
||||
|
||||
func ParseNetwork(network string, addr netip.Addr) string {
|
||||
return fallbackParseNetwork(network, addr)
|
||||
// fix bindIfaceToListenConfig() force bind to an ipv4 address
|
||||
if !strings.HasSuffix(network, "4") &&
|
||||
!strings.HasSuffix(network, "6") &&
|
||||
addr.Unmap().Is6() {
|
||||
network += "6"
|
||||
}
|
||||
return network
|
||||
}
|
||||
|
@ -74,11 +74,7 @@ func ListenPacket(ctx context.Context, network, address string, options ...Optio
|
||||
|
||||
lc := &net.ListenConfig{}
|
||||
if cfg.interfaceName != "" {
|
||||
bind := bindIfaceToListenConfig
|
||||
if cfg.fallbackBind {
|
||||
bind = fallbackBindIfaceToListenConfig
|
||||
}
|
||||
addr, err := bind(cfg.interfaceName, lc, network, address)
|
||||
addr, err := bindIfaceToListenConfig(cfg.interfaceName, lc, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -129,11 +125,7 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
|
||||
|
||||
dialer := netDialer.(*net.Dialer)
|
||||
if opt.interfaceName != "" {
|
||||
bind := bindIfaceToDialer
|
||||
if opt.fallbackBind {
|
||||
bind = fallbackBindIfaceToDialer
|
||||
}
|
||||
if err := bind(opt.interfaceName, dialer, network, destination); err != nil {
|
||||
if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@ -170,22 +162,14 @@ func concurrentDualStackDialContext(ctx context.Context, network string, ips []n
|
||||
|
||||
func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) {
|
||||
ipv4s, ipv6s := resolver.SortationAddr(ips)
|
||||
if len(ipv4s) == 0 && len(ipv6s) == 0 {
|
||||
return nil, ErrorNoIpAddress
|
||||
}
|
||||
|
||||
preferIPVersion := opt.prefer
|
||||
|
||||
fallbackTicker := time.NewTicker(fallbackTimeout)
|
||||
defer fallbackTicker.Stop()
|
||||
|
||||
results := make(chan dialResult)
|
||||
returned := make(chan struct{})
|
||||
defer close(returned)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
racer := func(ips []netip.Addr, isPrimary bool) {
|
||||
defer wg.Done()
|
||||
result := dialResult{isPrimary: isPrimary}
|
||||
defer func() {
|
||||
select {
|
||||
@ -198,36 +182,18 @@ func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string,
|
||||
}()
|
||||
result.Conn, result.error = dialFn(ctx, network, ips, port, opt)
|
||||
}
|
||||
|
||||
if len(ipv4s) != 0 {
|
||||
wg.Add(1)
|
||||
go racer(ipv4s, preferIPVersion != 6)
|
||||
}
|
||||
|
||||
if len(ipv6s) != 0 {
|
||||
wg.Add(1)
|
||||
go racer(ipv6s, preferIPVersion != 4)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(results)
|
||||
}()
|
||||
|
||||
go racer(ipv4s, preferIPVersion != 6)
|
||||
go racer(ipv6s, preferIPVersion != 4)
|
||||
var fallback dialResult
|
||||
var errs []error
|
||||
|
||||
loop:
|
||||
for {
|
||||
for i := 0; i < 2; {
|
||||
select {
|
||||
case <-fallbackTicker.C:
|
||||
if fallback.error == nil && fallback.Conn != nil {
|
||||
return fallback.Conn, nil
|
||||
}
|
||||
case res, ok := <-results:
|
||||
if !ok {
|
||||
break loop
|
||||
}
|
||||
case res := <-results:
|
||||
i++
|
||||
if res.error == nil {
|
||||
if res.isPrimary {
|
||||
return res.Conn, nil
|
||||
@ -242,7 +208,6 @@ loop:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fallback.error == nil && fallback.Conn != nil {
|
||||
return fallback.Conn, nil
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ type NetDialer interface {
|
||||
|
||||
type option struct {
|
||||
interfaceName string
|
||||
fallbackBind bool
|
||||
addrReuse bool
|
||||
routingMark int
|
||||
network int
|
||||
@ -39,12 +38,6 @@ func WithInterface(name string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithFallbackBind(fallback bool) Option {
|
||||
return func(opt *option) {
|
||||
opt.fallbackBind = fallback
|
||||
}
|
||||
}
|
||||
|
||||
func WithAddrReuse(reuse bool) Option {
|
||||
return func(opt *option) {
|
||||
opt.addrReuse = reuse
|
||||
|
@ -36,7 +36,7 @@ type Pool struct {
|
||||
cycle bool
|
||||
mux sync.Mutex
|
||||
host *trie.DomainTrie[struct{}]
|
||||
ipnet netip.Prefix
|
||||
ipnet *netip.Prefix
|
||||
store store
|
||||
}
|
||||
|
||||
@ -91,7 +91,7 @@ func (p *Pool) Broadcast() netip.Addr {
|
||||
}
|
||||
|
||||
// IPNet return raw ipnet
|
||||
func (p *Pool) IPNet() netip.Prefix {
|
||||
func (p *Pool) IPNet() *netip.Prefix {
|
||||
return p.ipnet
|
||||
}
|
||||
|
||||
@ -153,7 +153,7 @@ func (p *Pool) restoreState() {
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
IPNet netip.Prefix
|
||||
IPNet *netip.Prefix
|
||||
Host *trie.DomainTrie[struct{}]
|
||||
|
||||
// Size sets the maximum number of entries in memory
|
||||
@ -171,7 +171,7 @@ func New(options Options) (*Pool, error) {
|
||||
hostAddr = options.IPNet.Masked().Addr()
|
||||
gateway = hostAddr.Next()
|
||||
first = gateway.Next().Next().Next() // default start with 198.18.0.4
|
||||
last = nnip.UnMasked(options.IPNet)
|
||||
last = nnip.UnMasked(*options.IPNet)
|
||||
)
|
||||
|
||||
if !options.IPNet.IsValid() || !first.IsValid() || !first.Less(last) {
|
||||
|
@ -51,7 +51,7 @@ func createCachefileStore(options Options) (*Pool, string, error) {
|
||||
func TestPool_Basic(t *testing.T) {
|
||||
ipnet := netip.MustParsePrefix("192.168.0.0/28")
|
||||
pools, tempfile, err := createPools(Options{
|
||||
IPNet: ipnet,
|
||||
IPNet: &ipnet,
|
||||
Size: 10,
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
@ -79,7 +79,7 @@ func TestPool_Basic(t *testing.T) {
|
||||
func TestPool_BasicV6(t *testing.T) {
|
||||
ipnet := netip.MustParsePrefix("2001:4860:4860::8888/118")
|
||||
pools, tempfile, err := createPools(Options{
|
||||
IPNet: ipnet,
|
||||
IPNet: &ipnet,
|
||||
Size: 10,
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
@ -107,7 +107,7 @@ func TestPool_BasicV6(t *testing.T) {
|
||||
func TestPool_Case_Insensitive(t *testing.T) {
|
||||
ipnet := netip.MustParsePrefix("192.168.0.1/29")
|
||||
pools, tempfile, err := createPools(Options{
|
||||
IPNet: ipnet,
|
||||
IPNet: &ipnet,
|
||||
Size: 10,
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
@ -128,7 +128,7 @@ func TestPool_Case_Insensitive(t *testing.T) {
|
||||
func TestPool_CycleUsed(t *testing.T) {
|
||||
ipnet := netip.MustParsePrefix("192.168.0.16/28")
|
||||
pools, tempfile, err := createPools(Options{
|
||||
IPNet: ipnet,
|
||||
IPNet: &ipnet,
|
||||
Size: 10,
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
@ -152,7 +152,7 @@ func TestPool_Skip(t *testing.T) {
|
||||
tree := trie.New[struct{}]()
|
||||
tree.Insert("example.com", struct{}{})
|
||||
pools, tempfile, err := createPools(Options{
|
||||
IPNet: ipnet,
|
||||
IPNet: &ipnet,
|
||||
Size: 10,
|
||||
Host: tree,
|
||||
})
|
||||
@ -168,7 +168,7 @@ func TestPool_Skip(t *testing.T) {
|
||||
func TestPool_MaxCacheSize(t *testing.T) {
|
||||
ipnet := netip.MustParsePrefix("192.168.0.1/24")
|
||||
pool, _ := New(Options{
|
||||
IPNet: ipnet,
|
||||
IPNet: &ipnet,
|
||||
Size: 2,
|
||||
})
|
||||
|
||||
@ -183,7 +183,7 @@ func TestPool_MaxCacheSize(t *testing.T) {
|
||||
func TestPool_DoubleMapping(t *testing.T) {
|
||||
ipnet := netip.MustParsePrefix("192.168.0.1/24")
|
||||
pool, _ := New(Options{
|
||||
IPNet: ipnet,
|
||||
IPNet: &ipnet,
|
||||
Size: 2,
|
||||
})
|
||||
|
||||
@ -213,7 +213,7 @@ func TestPool_DoubleMapping(t *testing.T) {
|
||||
func TestPool_Clone(t *testing.T) {
|
||||
ipnet := netip.MustParsePrefix("192.168.0.1/24")
|
||||
pool, _ := New(Options{
|
||||
IPNet: ipnet,
|
||||
IPNet: &ipnet,
|
||||
Size: 2,
|
||||
})
|
||||
|
||||
@ -223,7 +223,7 @@ func TestPool_Clone(t *testing.T) {
|
||||
assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 5}))
|
||||
|
||||
newPool, _ := New(Options{
|
||||
IPNet: ipnet,
|
||||
IPNet: &ipnet,
|
||||
Size: 2,
|
||||
})
|
||||
newPool.CloneFrom(pool)
|
||||
@ -236,7 +236,7 @@ func TestPool_Clone(t *testing.T) {
|
||||
func TestPool_Error(t *testing.T) {
|
||||
ipnet := netip.MustParsePrefix("192.168.0.1/31")
|
||||
_, err := New(Options{
|
||||
IPNet: ipnet,
|
||||
IPNet: &ipnet,
|
||||
Size: 10,
|
||||
})
|
||||
|
||||
@ -246,7 +246,7 @@ func TestPool_Error(t *testing.T) {
|
||||
func TestPool_FlushFileCache(t *testing.T) {
|
||||
ipnet := netip.MustParsePrefix("192.168.0.1/28")
|
||||
pools, tempfile, err := createPools(Options{
|
||||
IPNet: ipnet,
|
||||
IPNet: &ipnet,
|
||||
Size: 10,
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
@ -278,7 +278,7 @@ func TestPool_FlushFileCache(t *testing.T) {
|
||||
func TestPool_FlushMemoryCache(t *testing.T) {
|
||||
ipnet := netip.MustParsePrefix("192.168.0.1/28")
|
||||
pool, _ := New(Options{
|
||||
IPNet: ipnet,
|
||||
IPNet: &ipnet,
|
||||
Size: 10,
|
||||
})
|
||||
|
||||
|
@ -2,22 +2,22 @@ package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
URL "net/url"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/component/ca"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/component/tls"
|
||||
"github.com/Dreamacro/clash/listener/inner"
|
||||
)
|
||||
|
||||
const (
|
||||
UA = "clash.meta"
|
||||
)
|
||||
|
||||
func HttpRequest(ctx context.Context, url, method string, header map[string][]string, body io.Reader) (*http.Response, error) {
|
||||
UA := C.UA
|
||||
method = strings.ToUpper(method)
|
||||
urlRes, err := URL.Parse(url)
|
||||
if err != nil {
|
||||
@ -48,7 +48,6 @@ func HttpRequest(ctx context.Context, url, method string, header map[string][]st
|
||||
|
||||
transport := &http.Transport{
|
||||
// from http.DefaultTransport
|
||||
DisableKeepAlives: runtime.GOOS == "android",
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
@ -61,7 +60,7 @@ func HttpRequest(ctx context.Context, url, method string, header map[string][]st
|
||||
return d.DialContext(ctx, network, address)
|
||||
}
|
||||
},
|
||||
TLSClientConfig: ca.GetGlobalTLSConfig(&tls.Config{}),
|
||||
TLSClientConfig: tls.GetDefaultTLSConfig(),
|
||||
}
|
||||
|
||||
client := http.Client{Transport: transport}
|
||||
|
@ -13,7 +13,7 @@ import (
|
||||
type Interface struct {
|
||||
Index int
|
||||
Name string
|
||||
Addrs []netip.Prefix
|
||||
Addrs []*netip.Prefix
|
||||
HardwareAddr net.HardwareAddr
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@ func ResolveInterface(name string) (*Interface, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
ipNets := make([]netip.Prefix, 0, len(addrs))
|
||||
ipNets := make([]*netip.Prefix, 0, len(addrs))
|
||||
for _, addr := range addrs {
|
||||
ipNet := addr.(*net.IPNet)
|
||||
ip, _ := netip.AddrFromSlice(ipNet.IP)
|
||||
@ -59,7 +59,7 @@ func ResolveInterface(name string) (*Interface, error) {
|
||||
}
|
||||
|
||||
pf := netip.PrefixFrom(ip, ones)
|
||||
ipNets = append(ipNets, pf)
|
||||
ipNets = append(ipNets, &pf)
|
||||
}
|
||||
|
||||
r[iface.Name] = &Interface{
|
||||
@ -89,27 +89,27 @@ func FlushCache() {
|
||||
interfaces.Reset()
|
||||
}
|
||||
|
||||
func (iface *Interface) PickIPv4Addr(destination netip.Addr) (netip.Prefix, error) {
|
||||
return iface.pickIPAddr(destination, func(addr netip.Prefix) bool {
|
||||
func (iface *Interface) PickIPv4Addr(destination netip.Addr) (*netip.Prefix, error) {
|
||||
return iface.pickIPAddr(destination, func(addr *netip.Prefix) bool {
|
||||
return addr.Addr().Is4()
|
||||
})
|
||||
}
|
||||
|
||||
func (iface *Interface) PickIPv6Addr(destination netip.Addr) (netip.Prefix, error) {
|
||||
return iface.pickIPAddr(destination, func(addr netip.Prefix) bool {
|
||||
func (iface *Interface) PickIPv6Addr(destination netip.Addr) (*netip.Prefix, error) {
|
||||
return iface.pickIPAddr(destination, func(addr *netip.Prefix) bool {
|
||||
return addr.Addr().Is6()
|
||||
})
|
||||
}
|
||||
|
||||
func (iface *Interface) pickIPAddr(destination netip.Addr, accept func(addr netip.Prefix) bool) (netip.Prefix, error) {
|
||||
var fallback netip.Prefix
|
||||
func (iface *Interface) pickIPAddr(destination netip.Addr, accept func(addr *netip.Prefix) bool) (*netip.Prefix, error) {
|
||||
var fallback *netip.Prefix
|
||||
|
||||
for _, addr := range iface.Addrs {
|
||||
if !accept(addr) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !fallback.IsValid() && !addr.Addr().IsLinkLocalUnicast() {
|
||||
if fallback == nil && !addr.Addr().IsLinkLocalUnicast() {
|
||||
fallback = addr
|
||||
|
||||
if !destination.IsValid() {
|
||||
@ -122,8 +122,8 @@ func (iface *Interface) pickIPAddr(destination netip.Addr, accept func(addr neti
|
||||
}
|
||||
}
|
||||
|
||||
if !fallback.IsValid() {
|
||||
return netip.Prefix{}, ErrAddrNotFound
|
||||
if fallback == nil {
|
||||
return nil, ErrAddrNotFound
|
||||
}
|
||||
|
||||
return fallback, nil
|
||||
|
@ -5,28 +5,23 @@ import (
|
||||
"sync"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v2"
|
||||
)
|
||||
|
||||
type Table struct {
|
||||
mapping *xsync.MapOf[string, *Entry]
|
||||
lockMap *xsync.MapOf[string, *sync.Cond]
|
||||
mapping sync.Map
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
PacketConn C.PacketConn
|
||||
WriteBackProxy C.WriteBackProxy
|
||||
LocalUDPConnMap *xsync.MapOf[string, *net.UDPConn]
|
||||
LocalLockMap *xsync.MapOf[string, *sync.Cond]
|
||||
LocalUDPConnMap sync.Map
|
||||
}
|
||||
|
||||
func (t *Table) Set(key string, e C.PacketConn, w C.WriteBackProxy) {
|
||||
t.mapping.Store(key, &Entry{
|
||||
PacketConn: e,
|
||||
WriteBackProxy: w,
|
||||
LocalUDPConnMap: xsync.NewMapOf[*net.UDPConn](),
|
||||
LocalLockMap: xsync.NewMapOf[*sync.Cond](),
|
||||
LocalUDPConnMap: sync.Map{},
|
||||
})
|
||||
}
|
||||
|
||||
@ -39,19 +34,15 @@ func (t *Table) Get(key string) (C.PacketConn, C.WriteBackProxy) {
|
||||
}
|
||||
|
||||
func (t *Table) GetOrCreateLock(key string) (*sync.Cond, bool) {
|
||||
item, loaded := t.lockMap.LoadOrCompute(key, makeLock)
|
||||
return item, loaded
|
||||
item, loaded := t.mapping.LoadOrStore(key, sync.NewCond(&sync.Mutex{}))
|
||||
return item.(*sync.Cond), loaded
|
||||
}
|
||||
|
||||
func (t *Table) Delete(key string) {
|
||||
t.mapping.Delete(key)
|
||||
}
|
||||
|
||||
func (t *Table) DeleteLock(lockKey string) {
|
||||
t.lockMap.Delete(lockKey)
|
||||
}
|
||||
|
||||
func (t *Table) GetForLocalConn(lAddr, rAddr string) *net.UDPConn {
|
||||
func (t *Table) GetLocalConn(lAddr, rAddr string) *net.UDPConn {
|
||||
entry, exist := t.getEntry(lAddr)
|
||||
if !exist {
|
||||
return nil
|
||||
@ -60,10 +51,10 @@ func (t *Table) GetForLocalConn(lAddr, rAddr string) *net.UDPConn {
|
||||
if !exist {
|
||||
return nil
|
||||
}
|
||||
return item
|
||||
return item.(*net.UDPConn)
|
||||
}
|
||||
|
||||
func (t *Table) AddForLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool {
|
||||
func (t *Table) AddLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool {
|
||||
entry, exist := t.getEntry(lAddr)
|
||||
if !exist {
|
||||
return false
|
||||
@ -72,7 +63,7 @@ func (t *Table) AddForLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *Table) RangeForLocalConn(lAddr string, f func(key string, value *net.UDPConn) bool) {
|
||||
func (t *Table) RangeLocalConn(lAddr string, f func(key, value any) bool) {
|
||||
entry, exist := t.getEntry(lAddr)
|
||||
if !exist {
|
||||
return
|
||||
@ -85,11 +76,11 @@ func (t *Table) GetOrCreateLockForLocalConn(lAddr, key string) (*sync.Cond, bool
|
||||
if !loaded {
|
||||
return nil, false
|
||||
}
|
||||
item, loaded := entry.LocalLockMap.LoadOrCompute(key, makeLock)
|
||||
return item, loaded
|
||||
item, loaded := entry.LocalUDPConnMap.LoadOrStore(key, sync.NewCond(&sync.Mutex{}))
|
||||
return item.(*sync.Cond), loaded
|
||||
}
|
||||
|
||||
func (t *Table) DeleteForLocalConn(lAddr, key string) {
|
||||
func (t *Table) DeleteLocalConnMap(lAddr, key string) {
|
||||
entry, loaded := t.getEntry(lAddr)
|
||||
if !loaded {
|
||||
return
|
||||
@ -97,26 +88,17 @@ func (t *Table) DeleteForLocalConn(lAddr, key string) {
|
||||
entry.LocalUDPConnMap.Delete(key)
|
||||
}
|
||||
|
||||
func (t *Table) DeleteLockForLocalConn(lAddr, key string) {
|
||||
entry, loaded := t.getEntry(lAddr)
|
||||
if !loaded {
|
||||
return
|
||||
}
|
||||
entry.LocalLockMap.Delete(key)
|
||||
}
|
||||
|
||||
func (t *Table) getEntry(key string) (*Entry, bool) {
|
||||
return t.mapping.Load(key)
|
||||
}
|
||||
|
||||
func makeLock() *sync.Cond {
|
||||
return sync.NewCond(&sync.Mutex{})
|
||||
item, ok := t.mapping.Load(key)
|
||||
// This should not happen usually since this function called after PacketConn created
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
entry, ok := item.(*Entry)
|
||||
return entry, ok
|
||||
}
|
||||
|
||||
// New return *Cache
|
||||
func New() *Table {
|
||||
return &Table{
|
||||
mapping: xsync.NewMapOf[*Entry](),
|
||||
lockMap: xsync.NewMapOf[*sync.Cond](),
|
||||
}
|
||||
return &Table{}
|
||||
}
|
||||
|
@ -70,7 +70,10 @@ func (p proxyDialer) DialContext(ctx context.Context, network, address string) (
|
||||
}
|
||||
|
||||
func (p proxyDialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) {
|
||||
currentMeta := &C.Metadata{Type: C.INNER, DstIP: rAddrPort.Addr(), DstPort: rAddrPort.Port()}
|
||||
currentMeta := &C.Metadata{Type: C.INNER}
|
||||
if err := currentMeta.SetRemoteAddress(rAddrPort.String()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.listenPacket(ctx, currentMeta)
|
||||
}
|
||||
|
||||
|
@ -1,82 +0,0 @@
|
||||
package proxydialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type SingDialer interface {
|
||||
N.Dialer
|
||||
SetDialer(dialer C.Dialer)
|
||||
}
|
||||
|
||||
type singDialer proxyDialer
|
||||
|
||||
var _ N.Dialer = (*singDialer)(nil)
|
||||
|
||||
func (d *singDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
return (*proxyDialer)(d).DialContext(ctx, network, destination.String())
|
||||
}
|
||||
|
||||
func (d *singDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
return (*proxyDialer)(d).ListenPacket(ctx, "udp", "", destination.AddrPort())
|
||||
}
|
||||
|
||||
func (d *singDialer) SetDialer(dialer C.Dialer) {
|
||||
(*proxyDialer)(d).dialer = dialer
|
||||
}
|
||||
|
||||
func NewSingDialer(proxy C.ProxyAdapter, dialer C.Dialer, statistic bool) SingDialer {
|
||||
return (*singDialer)(&proxyDialer{
|
||||
proxy: proxy,
|
||||
dialer: dialer,
|
||||
statistic: statistic,
|
||||
})
|
||||
}
|
||||
|
||||
type byNameSingDialer struct {
|
||||
dialer C.Dialer
|
||||
proxyName string
|
||||
}
|
||||
|
||||
var _ N.Dialer = (*byNameSingDialer)(nil)
|
||||
|
||||
func (d *byNameSingDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
var cDialer C.Dialer = d.dialer
|
||||
if len(d.proxyName) > 0 {
|
||||
pd, err := NewByName(d.proxyName, d.dialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cDialer = pd
|
||||
}
|
||||
return cDialer.DialContext(ctx, network, destination.String())
|
||||
}
|
||||
|
||||
func (d *byNameSingDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
var cDialer C.Dialer = d.dialer
|
||||
if len(d.proxyName) > 0 {
|
||||
pd, err := NewByName(d.proxyName, d.dialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cDialer = pd
|
||||
}
|
||||
return cDialer.ListenPacket(ctx, "udp", "", destination.AddrPort())
|
||||
}
|
||||
|
||||
func (d *byNameSingDialer) SetDialer(dialer C.Dialer) {
|
||||
d.dialer = dialer
|
||||
}
|
||||
|
||||
func NewByNameSingDialer(proxyName string, dialer C.Dialer) SingDialer {
|
||||
return &byNameSingDialer{
|
||||
dialer: dialer,
|
||||
proxyName: proxyName,
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
"strings"
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/Dreamacro/clash/common/utils"
|
||||
"github.com/Dreamacro/clash/component/trie"
|
||||
@ -21,39 +20,28 @@ func NewHosts(hosts *trie.DomainTrie[HostValue]) Hosts {
|
||||
}
|
||||
}
|
||||
|
||||
// lookupStaticHost looks up the addresses and the canonical name for the given host from /etc/hosts.
|
||||
//
|
||||
//go:linkname lookupStaticHost net.lookupStaticHost
|
||||
func lookupStaticHost(host string) ([]string, string)
|
||||
|
||||
// Return the search result and whether to match the parameter `isDomain`
|
||||
func (h *Hosts) Search(domain string, isDomain bool) (*HostValue, bool) {
|
||||
if value := h.DomainTrie.Search(domain); value != nil {
|
||||
hostValue := value.Data()
|
||||
for {
|
||||
if isDomain && hostValue.IsDomain {
|
||||
return &hostValue, true
|
||||
value := h.DomainTrie.Search(domain)
|
||||
if value == nil {
|
||||
return nil, false
|
||||
}
|
||||
hostValue := value.Data()
|
||||
for {
|
||||
if isDomain && hostValue.IsDomain {
|
||||
return &hostValue, true
|
||||
} else {
|
||||
if node := h.DomainTrie.Search(hostValue.Domain); node != nil {
|
||||
hostValue = node.Data()
|
||||
} else {
|
||||
if node := h.DomainTrie.Search(hostValue.Domain); node != nil {
|
||||
hostValue = node.Data()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if isDomain == hostValue.IsDomain {
|
||||
return &hostValue, true
|
||||
}
|
||||
|
||||
return &hostValue, false
|
||||
}
|
||||
if !isDomain {
|
||||
addr, _ := lookupStaticHost(domain)
|
||||
if hostValue, err := NewHostValue(addr); err == nil {
|
||||
return &hostValue, true
|
||||
}
|
||||
if isDomain == hostValue.IsDomain {
|
||||
return &hostValue, true
|
||||
}
|
||||
return nil, false
|
||||
return &hostValue, false
|
||||
}
|
||||
|
||||
type HostValue struct {
|
||||
|
@ -1,19 +0,0 @@
|
||||
//go:build !go1.22
|
||||
|
||||
// a simple standard lib fix from: https://github.com/golang/go/commit/33d4a5105cf2b2d549922e909e9239a48b8cefcc
|
||||
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/windows"
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
//go:linkname testHookHostsPath net.testHookHostsPath
|
||||
var testHookHostsPath string
|
||||
|
||||
func init() {
|
||||
if dir, err := windows.GetSystemDirectory(); err == nil {
|
||||
testHookHostsPath = dir + "/Drivers/etc/hosts"
|
||||
}
|
||||
}
|
@ -9,8 +9,6 @@ import (
|
||||
|
||||
types "github.com/Dreamacro/clash/constant/provider"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -67,7 +65,7 @@ func (f *Fetcher[V]) Initial() (V, error) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return lo.Empty[V](), err
|
||||
return getZero[V](), err
|
||||
}
|
||||
|
||||
var contents V
|
||||
@ -87,18 +85,18 @@ func (f *Fetcher[V]) Initial() (V, error) {
|
||||
|
||||
if err != nil {
|
||||
if !isLocal {
|
||||
return lo.Empty[V](), err
|
||||
return getZero[V](), err
|
||||
}
|
||||
|
||||
// parse local file error, fallback to remote
|
||||
buf, err = f.vehicle.Read()
|
||||
if err != nil {
|
||||
return lo.Empty[V](), err
|
||||
return getZero[V](), err
|
||||
}
|
||||
|
||||
contents, err = f.parser(buf)
|
||||
if err != nil {
|
||||
return lo.Empty[V](), err
|
||||
return getZero[V](), err
|
||||
}
|
||||
|
||||
isLocal = false
|
||||
@ -106,7 +104,7 @@ func (f *Fetcher[V]) Initial() (V, error) {
|
||||
|
||||
if f.vehicle.Type() != types.File && !isLocal {
|
||||
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
|
||||
return lo.Empty[V](), err
|
||||
return getZero[V](), err
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,7 +121,7 @@ func (f *Fetcher[V]) Initial() (V, error) {
|
||||
func (f *Fetcher[V]) Update() (V, bool, error) {
|
||||
buf, err := f.vehicle.Read()
|
||||
if err != nil {
|
||||
return lo.Empty[V](), false, err
|
||||
return getZero[V](), false, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
@ -131,17 +129,17 @@ func (f *Fetcher[V]) Update() (V, bool, error) {
|
||||
if bytes.Equal(f.hash[:], hash[:]) {
|
||||
f.UpdatedAt = &now
|
||||
_ = os.Chtimes(f.vehicle.Path(), now, now)
|
||||
return lo.Empty[V](), true, nil
|
||||
return getZero[V](), true, nil
|
||||
}
|
||||
|
||||
contents, err := f.parser(buf)
|
||||
if err != nil {
|
||||
return lo.Empty[V](), false, err
|
||||
return getZero[V](), false, err
|
||||
}
|
||||
|
||||
if f.vehicle.Type() != types.File {
|
||||
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
|
||||
return lo.Empty[V](), false, err
|
||||
return getZero[V](), false, err
|
||||
}
|
||||
}
|
||||
|
||||
@ -212,3 +210,8 @@ func NewFetcher[V any](name string, interval time.Duration, vehicle types.Vehicl
|
||||
interval: interval,
|
||||
}
|
||||
}
|
||||
|
||||
func getZero[V any]() V {
|
||||
var result V
|
||||
return result
|
||||
}
|
||||
|
@ -23,8 +23,8 @@ func (*BaseSniffer) Protocol() string {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// SniffData implements sniffer.Sniffer
|
||||
func (*BaseSniffer) SniffData(bytes []byte) (string, error) {
|
||||
// SniffTCP implements sniffer.Sniffer
|
||||
func (*BaseSniffer) SniffTCP(bytes []byte) (string, error) {
|
||||
return "", errors.New("TODO")
|
||||
}
|
||||
|
||||
|
@ -35,40 +35,8 @@ type SnifferDispatcher struct {
|
||||
parsePureIp bool
|
||||
}
|
||||
|
||||
func (sd *SnifferDispatcher) shouldOverride(metadata *C.Metadata) bool {
|
||||
return (metadata.Host == "" && sd.parsePureIp) ||
|
||||
sd.forceDomain.Has(metadata.Host) ||
|
||||
(metadata.DNSMode == C.DNSMapping && sd.forceDnsMapping)
|
||||
}
|
||||
|
||||
func (sd *SnifferDispatcher) UDPSniff(packet C.PacketAdapter) bool {
|
||||
metadata := packet.Metadata()
|
||||
|
||||
if sd.shouldOverride(packet.Metadata()) {
|
||||
for sniffer, config := range sd.sniffers {
|
||||
if sniffer.SupportNetwork() == C.UDP || sniffer.SupportNetwork() == C.ALLNet {
|
||||
inWhitelist := sniffer.SupportPort(metadata.DstPort)
|
||||
overrideDest := config.OverrideDest
|
||||
|
||||
if inWhitelist {
|
||||
host, err := sniffer.SniffData(packet.Data())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
sd.replaceDomain(metadata, host, overrideDest)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// TCPSniff returns true if the connection is sniffed to have a domain
|
||||
func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) bool {
|
||||
if sd.shouldOverride(metadata) {
|
||||
func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) {
|
||||
if (metadata.Host == "" && sd.parsePureIp) || sd.forceDomain.Has(metadata.Host) || (metadata.DNSMode == C.DNSMapping && sd.forceDnsMapping) {
|
||||
inWhitelist := false
|
||||
overrideDest := false
|
||||
for sniffer, config := range sd.sniffers {
|
||||
@ -82,7 +50,7 @@ func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata
|
||||
}
|
||||
|
||||
if !inWhitelist {
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
sd.rwMux.RLock()
|
||||
@ -90,18 +58,18 @@ func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata
|
||||
if count, ok := sd.skipList.Get(dst); ok && count > 5 {
|
||||
log.Debugln("[Sniffer] Skip sniffing[%s] due to multiple failures", dst)
|
||||
defer sd.rwMux.RUnlock()
|
||||
return false
|
||||
return
|
||||
}
|
||||
sd.rwMux.RUnlock()
|
||||
|
||||
if host, err := sd.sniffDomain(conn, metadata); err != nil {
|
||||
sd.cacheSniffFailed(metadata)
|
||||
log.Debugln("[Sniffer] All sniffing sniff failed with from [%s:%d] to [%s:%d]", metadata.SrcIP, metadata.SrcPort, metadata.String(), metadata.DstPort)
|
||||
return false
|
||||
return
|
||||
} else {
|
||||
if sd.skipSNI.Has(host) {
|
||||
log.Debugln("[Sniffer] Skip sni[%s]", host)
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
sd.rwMux.RLock()
|
||||
@ -109,24 +77,20 @@ func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata
|
||||
sd.rwMux.RUnlock()
|
||||
|
||||
sd.replaceDomain(metadata, host, overrideDest)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (sd *SnifferDispatcher) replaceDomain(metadata *C.Metadata, host string, overrideDest bool) {
|
||||
// show log early, since the following code may mutate `metadata.Host`
|
||||
log.Debugln("[Sniffer] Sniff %s [%s]-->[%s] success, replace domain [%s]-->[%s]",
|
||||
metadata.NetWork,
|
||||
metadata.SourceDetail(),
|
||||
metadata.RemoteAddress(),
|
||||
metadata.Host, host)
|
||||
metadata.SniffHost = host
|
||||
if overrideDest {
|
||||
metadata.Host = host
|
||||
}
|
||||
metadata.DNSMode = C.DNSNormal
|
||||
log.Debugln("[Sniffer] Sniff TCP [%s]-->[%s] success, replace domain [%s]-->[%s]",
|
||||
metadata.SourceDetail(),
|
||||
metadata.RemoteAddress(),
|
||||
metadata.Host, host)
|
||||
}
|
||||
|
||||
func (sd *SnifferDispatcher) Enable() bool {
|
||||
@ -157,7 +121,7 @@ func (sd *SnifferDispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metad
|
||||
continue
|
||||
}
|
||||
|
||||
host, err := s.SniffData(bytes)
|
||||
host, err := s.SniffTCP(bytes)
|
||||
if err != nil {
|
||||
//log.Debugln("[Sniffer] [%s] Sniff data failed %s", s.Protocol(), metadata.DstIP)
|
||||
continue
|
||||
@ -226,8 +190,6 @@ func NewSniffer(name sniffer.Type, snifferConfig SnifferConfig) (sniffer.Sniffer
|
||||
return NewTLSSniffer(snifferConfig)
|
||||
case sniffer.HTTP:
|
||||
return NewHTTPSniffer(snifferConfig)
|
||||
case sniffer.QUIC:
|
||||
return NewQuicSniffer(snifferConfig)
|
||||
default:
|
||||
return nil, ErrorUnsupportedSniffer
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ func (http *HTTPSniffer) SupportNetwork() C.NetWork {
|
||||
return C.TCP
|
||||
}
|
||||
|
||||
func (http *HTTPSniffer) SniffData(bytes []byte) (string, error) {
|
||||
func (http *HTTPSniffer) SniffTCP(bytes []byte) (string, error) {
|
||||
domain, err := SniffHTTP(bytes)
|
||||
if err == nil {
|
||||
return *domain, nil
|
||||
|
@ -1,287 +1,3 @@
|
||||
package sniffer
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/Dreamacro/clash/common/buf"
|
||||
"github.com/Dreamacro/clash/common/utils"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
"github.com/metacubex/quic-go/quicvarint"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
// Modified from https://github.com/v2fly/v2ray-core/blob/master/common/protocol/quic/sniff.go
|
||||
|
||||
const (
|
||||
versionDraft29 uint32 = 0xff00001d
|
||||
version1 uint32 = 0x1
|
||||
)
|
||||
|
||||
var (
|
||||
quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99}
|
||||
quicSalt = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a}
|
||||
errNotQuic = errors.New("not QUIC")
|
||||
errNotQuicInitial = errors.New("not QUIC initial packet")
|
||||
)
|
||||
|
||||
type QuicSniffer struct {
|
||||
*BaseSniffer
|
||||
}
|
||||
|
||||
func NewQuicSniffer(snifferConfig SnifferConfig) (*QuicSniffer, error) {
|
||||
ports := snifferConfig.Ports
|
||||
if len(ports) == 0 {
|
||||
ports = utils.IntRanges[uint16]{utils.NewRange[uint16](443, 443)}
|
||||
}
|
||||
return &QuicSniffer{
|
||||
BaseSniffer: NewBaseSniffer(ports, C.UDP),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (quic QuicSniffer) Protocol() string {
|
||||
return "quic"
|
||||
}
|
||||
|
||||
func (quic QuicSniffer) SupportNetwork() C.NetWork {
|
||||
return C.UDP
|
||||
}
|
||||
|
||||
func (quic QuicSniffer) SniffData(b []byte) (string, error) {
|
||||
buffer := buf.As(b)
|
||||
typeByte, err := buffer.ReadByte()
|
||||
if err != nil {
|
||||
return "", errNotQuic
|
||||
}
|
||||
isLongHeader := typeByte&0x80 > 0
|
||||
if !isLongHeader || typeByte&0x40 == 0 {
|
||||
return "", errNotQuicInitial
|
||||
}
|
||||
|
||||
vb, err := buffer.ReadBytes(4)
|
||||
if err != nil {
|
||||
return "", errNotQuic
|
||||
}
|
||||
|
||||
versionNumber := binary.BigEndian.Uint32(vb)
|
||||
|
||||
if versionNumber != 0 && typeByte&0x40 == 0 {
|
||||
return "", errNotQuic
|
||||
} else if versionNumber != versionDraft29 && versionNumber != version1 {
|
||||
return "", errNotQuic
|
||||
}
|
||||
|
||||
if (typeByte&0x30)>>4 != 0x0 {
|
||||
return "", errNotQuicInitial
|
||||
}
|
||||
|
||||
var destConnID []byte
|
||||
if l, err := buffer.ReadByte(); err != nil {
|
||||
return "", errNotQuic
|
||||
} else if destConnID, err = buffer.ReadBytes(int(l)); err != nil {
|
||||
return "", errNotQuic
|
||||
}
|
||||
|
||||
if l, err := buffer.ReadByte(); err != nil {
|
||||
return "", errNotQuic
|
||||
} else if _, err := buffer.ReadBytes(int(l)); err != nil {
|
||||
return "", errNotQuic
|
||||
}
|
||||
|
||||
tokenLen, err := quicvarint.Read(buffer)
|
||||
if err != nil || tokenLen > uint64(len(b)) {
|
||||
return "", errNotQuic
|
||||
}
|
||||
|
||||
if _, err = buffer.ReadBytes(int(tokenLen)); err != nil {
|
||||
return "", errNotQuic
|
||||
}
|
||||
|
||||
packetLen, err := quicvarint.Read(buffer)
|
||||
if err != nil {
|
||||
return "", errNotQuic
|
||||
}
|
||||
|
||||
hdrLen := len(b) - buffer.Len()
|
||||
|
||||
var salt []byte
|
||||
if versionNumber == version1 {
|
||||
salt = quicSalt
|
||||
} else {
|
||||
salt = quicSaltOld
|
||||
}
|
||||
initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt)
|
||||
secret := hkdfExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size())
|
||||
hpKey := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic hp", 16)
|
||||
block, err := aes.NewCipher(hpKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cache := buf.NewPacket()
|
||||
defer cache.Release()
|
||||
|
||||
mask := cache.Extend(block.BlockSize())
|
||||
block.Encrypt(mask, b[hdrLen+4:hdrLen+4+16])
|
||||
firstByte := b[0]
|
||||
// Encrypt/decrypt first byte.
|
||||
if isLongHeader {
|
||||
// Long header: 4 bits masked
|
||||
// High 4 bits are not protected.
|
||||
firstByte ^= mask[0] & 0x0f
|
||||
} else {
|
||||
// Short header: 5 bits masked
|
||||
// High 3 bits are not protected.
|
||||
firstByte ^= mask[0] & 0x1f
|
||||
}
|
||||
packetNumberLength := int(firstByte&0x3 + 1) // max = 4 (64-bit sequence number)
|
||||
extHdrLen := hdrLen + packetNumberLength
|
||||
|
||||
// copy to avoid modify origin data
|
||||
extHdr := cache.Extend(extHdrLen)
|
||||
copy(extHdr, b)
|
||||
extHdr[0] = firstByte
|
||||
|
||||
packetNumber := extHdr[hdrLen:extHdrLen]
|
||||
// Encrypt/decrypt packet number.
|
||||
for i := range packetNumber {
|
||||
packetNumber[i] ^= mask[1+i]
|
||||
}
|
||||
|
||||
if packetNumber[0] != 0 && packetNumber[0] != 1 {
|
||||
return "", errNotQuicInitial
|
||||
}
|
||||
|
||||
data := b[extHdrLen : int(packetLen)+hdrLen]
|
||||
|
||||
key := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16)
|
||||
iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12)
|
||||
aesCipher, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
aead, err := cipher.NewGCM(aesCipher)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// We only decrypt once, so we do not need to XOR it back.
|
||||
// https://github.com/quic-go/qtls-go1-20/blob/e132a0e6cb45e20ac0b705454849a11d09ba5a54/cipher_suites.go#L496
|
||||
for i, b := range packetNumber {
|
||||
iv[len(iv)-len(packetNumber)+i] ^= b
|
||||
}
|
||||
dst := cache.Extend(len(data))
|
||||
decrypted, err := aead.Open(dst[:0], iv, data, extHdr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
buffer = buf.As(decrypted)
|
||||
|
||||
cryptoLen := uint(0)
|
||||
cryptoData := cache.Extend(buffer.Len())
|
||||
for i := 0; !buffer.IsEmpty(); i++ {
|
||||
frameType := byte(0x0) // Default to PADDING frame
|
||||
for frameType == 0x0 && !buffer.IsEmpty() {
|
||||
frameType, _ = buffer.ReadByte()
|
||||
}
|
||||
switch frameType {
|
||||
case 0x00: // PADDING frame
|
||||
case 0x01: // PING frame
|
||||
case 0x02, 0x03: // ACK frame
|
||||
if _, err = quicvarint.Read(buffer); err != nil { // Field: Largest Acknowledged
|
||||
return "", io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Delay
|
||||
return "", io.ErrUnexpectedEOF
|
||||
}
|
||||
ackRangeCount, err := quicvarint.Read(buffer) // Field: ACK Range Count
|
||||
if err != nil {
|
||||
return "", io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err = quicvarint.Read(buffer); err != nil { // Field: First ACK Range
|
||||
return "", io.ErrUnexpectedEOF
|
||||
}
|
||||
for i := 0; i < int(ackRangeCount); i++ { // Field: ACK Range
|
||||
if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Range -> Gap
|
||||
return "", io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Range -> ACK Range Length
|
||||
return "", io.ErrUnexpectedEOF
|
||||
}
|
||||
}
|
||||
if frameType == 0x03 {
|
||||
if _, err = quicvarint.Read(buffer); err != nil { // Field: ECN Counts -> ECT0 Count
|
||||
return "", io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err = quicvarint.Read(buffer); err != nil { // Field: ECN Counts -> ECT1 Count
|
||||
return "", io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err = quicvarint.Read(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count
|
||||
return "", io.ErrUnexpectedEOF
|
||||
}
|
||||
}
|
||||
case 0x06: // CRYPTO frame, we will use this frame
|
||||
offset, err := quicvarint.Read(buffer) // Field: Offset
|
||||
if err != nil {
|
||||
return "", io.ErrUnexpectedEOF
|
||||
}
|
||||
length, err := quicvarint.Read(buffer) // Field: Length
|
||||
if err != nil || length > uint64(buffer.Len()) {
|
||||
return "", io.ErrUnexpectedEOF
|
||||
}
|
||||
if cryptoLen < uint(offset+length) {
|
||||
cryptoLen = uint(offset + length)
|
||||
}
|
||||
if _, err := buffer.Read(cryptoData[offset : offset+length]); err != nil { // Field: Crypto Data
|
||||
return "", io.ErrUnexpectedEOF
|
||||
}
|
||||
case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet
|
||||
if _, err = quicvarint.Read(buffer); err != nil { // Field: Error Code
|
||||
return "", io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err = quicvarint.Read(buffer); err != nil { // Field: Frame Type
|
||||
return "", io.ErrUnexpectedEOF
|
||||
}
|
||||
length, err := quicvarint.Read(buffer) // Field: Reason Phrase Length
|
||||
if err != nil {
|
||||
return "", io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err := buffer.ReadBytes(int(length)); err != nil { // Field: Reason Phrase
|
||||
return "", io.ErrUnexpectedEOF
|
||||
}
|
||||
default:
|
||||
// Only above frame types are permitted in initial packet.
|
||||
// See https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2.2-8
|
||||
return "", errNotQuicInitial
|
||||
}
|
||||
}
|
||||
|
||||
domain, err := ReadClientHello(cryptoData[:cryptoLen])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return *domain, nil
|
||||
}
|
||||
|
||||
func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte {
|
||||
b := make([]byte, 3, 3+6+len(label)+1+len(context))
|
||||
binary.BigEndian.PutUint16(b, uint16(length))
|
||||
b[2] = uint8(6 + len(label))
|
||||
b = append(b, []byte("tls13 ")...)
|
||||
b = append(b, []byte(label)...)
|
||||
b = b[:3+6+len(label)+1]
|
||||
b[3+6+len(label)] = uint8(len(context))
|
||||
b = append(b, context...)
|
||||
|
||||
out := make([]byte, length)
|
||||
n, err := hkdf.Expand(hash.New, secret, b).Read(out)
|
||||
if err != nil || n != length {
|
||||
panic("quic: HKDF-Expand-Label invocation failed unexpectedly")
|
||||
}
|
||||
return out
|
||||
}
|
||||
//TODO
|
||||
|
@ -1,40 +1,9 @@
|
||||
package sniffer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestQuicHeaders(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
domain string
|
||||
}{
|
||||
{
|
||||
input: "cd0000000108f1fb7bcc78aa5e7203a8f86400421531fe825b19541876db6c55c38890cd73149d267a084afee6087304095417a3033df6a81bbb71d8512e7a3e16df1e277cae5df3182cb214b8fe982ba3fdffbaa9ffec474547d55945f0fddbeadfb0b5243890b2fa3da45169e2bd34ec04b2e29382f48d612b28432a559757504d158e9e505407a77dd34f4b60b8d3b555ee85aacd6648686802f4de25e7216b19e54c5f78e8a5963380c742d861306db4c16e4f7fc94957aa50b9578a0b61f1e406b2ad5f0cd3cd271c4d99476409797b0c3cb3efec256118912d4b7e4fd79d9cb9016b6e5eaa4f5e57b637b217755daf8968a4092bed0ed5413f5d04904b3a61e4064f9211b2629e5b52a89c7b19f37a713e41e27743ea6dfa736dfa1bb0a4b2bc8c8dc632c6ce963493a20c550e6fdb2475213665e9a85cfc394da9cec0cf41f0c8abed3fc83be5245b2b5aa5e825d29349f721d30774ef5bf965b540f3d8d98febe20956b1fc8fa047e10e7d2f921c9c6622389e02322e80621a1cf5264e245b7276966eb02932584e3f7038bd36aa908766ad3fb98344025dec18670d6db43a1c5daac00937fce7b7c7d61ff4e6efd01a2bdee0ee183108b926393df4f3d74bbcbb015f240e7e346b7d01c41111a401225ce3b095ab4623a5836169bf9599eeca79d1d2e9b2202b5960a09211e978058d6fc0484eff3e91ce4649a5e3ba15b906d334cf66e28d9ff575406e1ae1ac2febafd72870b6f5d58fc5fb949cb1f40feb7c1d9ce5e71b",
|
||||
domain: "www.google.com",
|
||||
},
|
||||
{
|
||||
input: "c3000000011266f50524e8d0fe88cbf51e3ad71a13198235000044c82dc5d943fb34cc6d5c5e433610dc7a44f5951935c2c1d14ac641b02472340a892c4492dbfe3f8262109108fc36d96bdc1e9e46b5f1f6ef6104add2aafbfd8e79246eb3b4637541aaed7d195571724e642ab4d31c909f1db86e7d8516117ce8716bd1e3acb664c499086b0f3bc7258595420e7bb969f934457d195e832ffff4ffddf11123eeadacc48190e356c8f0f6abc381deb7e285e3b0613a795b19bddb9f002ffdf6fd70f0ff2072302b33d2421aac6540bb9f0e85c7237af0dd56225b2264d769160febab952e64bd5155f23e58c6113891143f946591032b41816aed3ac54f521f60605f86791de24c5765b664c1348cc53d5d631b4bbefe1915f2b21fefafb47badeb72d8ba1fd5c3cfeb0ba9d0112396f170e94cd33952c4fa87997b870931bf1a300e8e127f530815ff087815b4f9d004cbcd17013ac143847572a1655a5b36e054e8b9951d747c2c6ff25d7b2edb13a2a6b8074062332f2191f6830cf435a4ed9db5d9c4eb43a143bf3edf0c48f6f9435dafad4afb743a5a33990379df953ecd388e848aff0ebba9ccc052b8303c0bd1fee7e7553af1894e81b7772818bb69249540ccb8cfb47b1517abaf71c81c3bd271f1a5f1b66465f850f377c9db682b8e543c3d0c10fcd2dee263630889b7d1d521d1d27e866ea4ab5f43790d6a7f76ceefd5783678ca92cc131fa42fc4a01e2a81cad734ddf17a53e1bda8e0a21afc9e8c1118c9459b13519f5b3c3d9692c92234f01129d47ae8ec70625170847472801190b46d36f73b868f55f5a18a3cb05af6d38610e0829e4fbf13ddcc202341702e43dcf33be76ff4afe327e5783287c137aad075752940b41e7d9f5146e36d908897c6d7a9fdc343fde2d9c9d6e6a6b237669bd3e6abe0a732861a679eadfa29a876c6a646953c9361830811b012b26b31c9e7158f8de9c9a108346ddee3dd3886da6258364c1281bff8e055f6384e3a23e198b5e6b726fa7f811b3338072019d4b5fd05891770d11e3ed6ab5f7ed33db1c6220c5aa8fa1909949ac55d5435b75982e17aa80940fa574f0aba4dc340129cad491fdf1f5e05c4e83e36ad29ff38f15e1c9436c792024442f57f07583d671dd05446c84ea20b471303f6ae4e5e13f244d671e0ebe94d3d5c17d3f3f378cdd51fa8a6d2c977c78a2397dd1e251cd979803d617d45f575e5d9db0a28b3c4c25fe2af24af5bddac09786b6d6d8aa19cfbd5409bdbfed7d518ef5c863f3ee757bd9d37cddc546cc57d2e52b6ae58789f297a300f1d76c3842603eae4b1224de31a939a68875c86e697aeebf7ebc65568f43fc681bacab830ac4a2164d324e90067125bad702192d01cb3cb3d2689ae681967e86fd7ac93a25cf2e905c88ca5ad7d11962f021754cf3f61224517bd3411d5b5a83955bcea79d702466d073a6eaadc1202b3693e555b051a5b19457023a01e7f943742bb7f5f8aeba8d4e363973aebdccfb12479619cfb93e833be702a307e796dc7431a48abd9b755b392c510b98cd20ef778e2ac88d6a04f23ba8a253d7eb7c13e0c88c3a21f7e23857c58704d139703a47e0965bf2dc8810dc36894ac1f3da73c155e271c106a718b2d184e4e5637c820fe909984642960edfc9e62ac50af5dd3feee6bc560ced7bda676d4e290c9c5916fad52180bbc83d3483e95c79bac15c209936f21042dc2b6253eefdac06e7f4745044eaa0acedabf1d1c8cd9402738",
|
||||
domain: "cloudflare-dns.com",
|
||||
},
|
||||
}
|
||||
q, err := NewQuicSniffer(SnifferConfig{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, test := range cases {
|
||||
pkt, err := hex.DecodeString(test.input)
|
||||
assert.NoError(t, err)
|
||||
oriPkt := bytes.Clone(pkt)
|
||||
domain, err := q.SniffData(pkt)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.domain, domain)
|
||||
assert.Equal(t, oriPkt, pkt) // ensure input data not changed
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSHeaders(t *testing.T) {
|
||||
cases := []struct {
|
||||
input []byte
|
||||
@ -173,7 +142,6 @@ func TestTLSHeaders(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
input := bytes.Clone(test.input)
|
||||
domain, err := SniffTLS(test.input)
|
||||
if test.err {
|
||||
if err == nil {
|
||||
@ -187,6 +155,5 @@ func TestTLSHeaders(t *testing.T) {
|
||||
t.Error("expect domain ", test.domain, " but got ", domain)
|
||||
}
|
||||
}
|
||||
assert.Equal(t, input, test.input)
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ func (tls *TLSSniffer) SupportNetwork() C.NetWork {
|
||||
return C.TCP
|
||||
}
|
||||
|
||||
func (tls *TLSSniffer) SniffData(bytes []byte) (string, error) {
|
||||
func (tls *TLSSniffer) SniffTCP(bytes []byte) (string, error) {
|
||||
domain, err := SniffTLS(bytes)
|
||||
if err == nil {
|
||||
return *domain, nil
|
||||
|
@ -1,4 +1,4 @@
|
||||
package ca
|
||||
package tls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -8,13 +8,12 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var trustCerts []*x509.Certificate
|
||||
var globalCertPool *x509.CertPool
|
||||
var certPool *x509.CertPool
|
||||
var mutex sync.RWMutex
|
||||
var errNotMatch = errors.New("certificate fingerprints do not match")
|
||||
|
||||
@ -34,12 +33,12 @@ func AddCertificate(certificate string) error {
|
||||
|
||||
func initializeCertPool() {
|
||||
var err error
|
||||
globalCertPool, err = x509.SystemCertPool()
|
||||
certPool, err = x509.SystemCertPool()
|
||||
if err != nil {
|
||||
globalCertPool = x509.NewCertPool()
|
||||
certPool = x509.NewCertPool()
|
||||
}
|
||||
for _, cert := range trustCerts {
|
||||
globalCertPool.AddCert(cert)
|
||||
certPool.AddCert(cert)
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,15 +53,15 @@ func getCertPool() *x509.CertPool {
|
||||
if len(trustCerts) == 0 {
|
||||
return nil
|
||||
}
|
||||
if globalCertPool == nil {
|
||||
if certPool == nil {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
if globalCertPool != nil {
|
||||
return globalCertPool
|
||||
if certPool != nil {
|
||||
return certPool
|
||||
}
|
||||
initializeCertPool()
|
||||
}
|
||||
return globalCertPool
|
||||
return certPool
|
||||
}
|
||||
|
||||
func verifyFingerprint(fingerprint *[32]byte) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
||||
@ -95,49 +94,29 @@ func convertFingerprint(fingerprint string) (*[32]byte, error) {
|
||||
return (*[32]byte)(fpByte), nil
|
||||
}
|
||||
|
||||
// GetTLSConfig specified fingerprint, customCA and customCAString
|
||||
func GetTLSConfig(tlsConfig *tls.Config, fingerprint string, customCA string, customCAString string) (*tls.Config, error) {
|
||||
if tlsConfig == nil {
|
||||
tlsConfig = &tls.Config{}
|
||||
}
|
||||
var certificate []byte
|
||||
var err error
|
||||
if len(customCA) > 0 {
|
||||
certificate, err = os.ReadFile(customCA)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load ca error: %w", err)
|
||||
}
|
||||
} else if customCAString != "" {
|
||||
certificate = []byte(customCAString)
|
||||
}
|
||||
if len(certificate) > 0 {
|
||||
certPool := x509.NewCertPool()
|
||||
if !certPool.AppendCertsFromPEM(certificate) {
|
||||
return nil, fmt.Errorf("failed to parse certificate:\n\n %s", certificate)
|
||||
}
|
||||
tlsConfig.RootCAs = certPool
|
||||
} else {
|
||||
tlsConfig.RootCAs = getCertPool()
|
||||
}
|
||||
if len(fingerprint) > 0 {
|
||||
var fingerprintBytes *[32]byte
|
||||
fingerprintBytes, err = convertFingerprint(fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig = GetGlobalTLSConfig(tlsConfig)
|
||||
tlsConfig.VerifyPeerCertificate = verifyFingerprint(fingerprintBytes)
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
}
|
||||
return tlsConfig, nil
|
||||
func GetDefaultTLSConfig() *tls.Config {
|
||||
return GetGlobalTLSConfig(nil)
|
||||
}
|
||||
|
||||
// GetSpecifiedFingerprintTLSConfig specified fingerprint
|
||||
func GetSpecifiedFingerprintTLSConfig(tlsConfig *tls.Config, fingerprint string) (*tls.Config, error) {
|
||||
return GetTLSConfig(tlsConfig, fingerprint, "", "")
|
||||
if fingerprintBytes, err := convertFingerprint(fingerprint); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
tlsConfig = GetGlobalTLSConfig(tlsConfig)
|
||||
tlsConfig.VerifyPeerCertificate = verifyFingerprint(fingerprintBytes)
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
return tlsConfig, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetGlobalTLSConfig(tlsConfig *tls.Config) *tls.Config {
|
||||
tlsConfig, _ = GetTLSConfig(tlsConfig, "", "", "")
|
||||
certPool := getCertPool()
|
||||
if tlsConfig == nil {
|
||||
return &tls.Config{
|
||||
RootCAs: certPool,
|
||||
}
|
||||
}
|
||||
tlsConfig.RootCAs = certPool
|
||||
return tlsConfig
|
||||
}
|
@ -22,7 +22,6 @@ import (
|
||||
|
||||
"github.com/Dreamacro/clash/common/utils"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
"github.com/Dreamacro/clash/ntp"
|
||||
|
||||
utls "github.com/sagernet/utls"
|
||||
"github.com/zhangyunhao116/fastrand"
|
||||
@ -43,8 +42,7 @@ type RealityConfig struct {
|
||||
func aesgcmPreferred(ciphers []uint16) bool
|
||||
|
||||
func GetRealityConn(ctx context.Context, conn net.Conn, ClientFingerprint string, tlsConfig *tls.Config, realityConfig *RealityConfig) (net.Conn, error) {
|
||||
retry := 0
|
||||
for fingerprint, exists := GetFingerprint(ClientFingerprint); exists; retry++ {
|
||||
if fingerprint, exists := GetFingerprint(ClientFingerprint); exists {
|
||||
verifier := &realityVerifier{
|
||||
serverName: tlsConfig.ServerName,
|
||||
}
|
||||
@ -72,7 +70,7 @@ func GetRealityConn(ctx context.Context, conn net.Conn, ClientFingerprint string
|
||||
rawSessionID[i] = 0
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint64(hello.SessionId, uint64(ntp.Now().Unix()))
|
||||
binary.BigEndian.PutUint64(hello.SessionId, uint64(time.Now().Unix()))
|
||||
|
||||
copy(hello.SessionId[8:], realityConfig.ShortID[:])
|
||||
hello.SessionId[0] = 1
|
||||
@ -81,15 +79,7 @@ func GetRealityConn(ctx context.Context, conn net.Conn, ClientFingerprint string
|
||||
|
||||
//log.Debugln("REALITY hello.sessionId[:16]: %v", hello.SessionId[:16])
|
||||
|
||||
ecdheParams := uConn.HandshakeState.State13.EcdheParams
|
||||
if ecdheParams == nil {
|
||||
// WTF???
|
||||
if retry > 2 {
|
||||
return nil, errors.New("nil ecdheParams")
|
||||
}
|
||||
continue // retry
|
||||
}
|
||||
authKey := ecdheParams.SharedKey(realityConfig.PublicKey[:])
|
||||
authKey := uConn.HandshakeState.State13.EcdheParams.SharedKey(realityConfig.PublicKey[:])
|
||||
if authKey == nil {
|
||||
return nil, errors.New("nil auth_key")
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ type UClientHelloID struct {
|
||||
var initRandomFingerprint UClientHelloID
|
||||
var initUtlsClient string
|
||||
|
||||
func UClient(c net.Conn, config *tls.Config, fingerprint UClientHelloID) *UConn {
|
||||
func UClient(c net.Conn, config *tls.Config, fingerprint UClientHelloID) net.Conn {
|
||||
utlsConn := utls.UClient(c, copyConfig(config), utls.ClientHelloID{
|
||||
Client: fingerprint.Client,
|
||||
Version: fingerprint.Version,
|
||||
@ -99,9 +99,10 @@ func copyConfig(c *tls.Config) *utls.Config {
|
||||
}
|
||||
}
|
||||
|
||||
// BuildWebsocketHandshakeState it will only send http/1.1 in its ALPN.
|
||||
// WebsocketHandshake basically calls UConn.Handshake inside it but it will only send
|
||||
// http/1.1 in its ALPN.
|
||||
// Copy from https://github.com/XTLS/Xray-core/blob/main/transport/internet/tls/tls.go
|
||||
func (c *UConn) BuildWebsocketHandshakeState() error {
|
||||
func (c *UConn) WebsocketHandshake() error {
|
||||
// Build the handshake state. This will apply every variable of the TLS of the
|
||||
// fingerprint in the UConn
|
||||
if err := c.BuildHandshakeState(); err != nil {
|
||||
@ -119,11 +120,11 @@ func (c *UConn) BuildWebsocketHandshakeState() error {
|
||||
if !hasALPNExtension { // Append extension if doesn't exists
|
||||
c.Extensions = append(c.Extensions, &utls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}})
|
||||
}
|
||||
// Rebuild the client hello
|
||||
// Rebuild the client hello and do the handshake
|
||||
if err := c.BuildHandshakeState(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return c.Handshake()
|
||||
}
|
||||
|
||||
func SetGlobalUtlsClient(Client string) {
|
||||
|
208
config/config.go
208
config/config.go
@ -8,7 +8,6 @@ import (
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
@ -17,9 +16,9 @@ import (
|
||||
"github.com/Dreamacro/clash/adapter/outbound"
|
||||
"github.com/Dreamacro/clash/adapter/outboundgroup"
|
||||
"github.com/Dreamacro/clash/adapter/provider"
|
||||
N "github.com/Dreamacro/clash/common/net"
|
||||
"github.com/Dreamacro/clash/common/utils"
|
||||
"github.com/Dreamacro/clash/component/auth"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
"github.com/Dreamacro/clash/component/fakeip"
|
||||
"github.com/Dreamacro/clash/component/geodata"
|
||||
"github.com/Dreamacro/clash/component/geodata/router"
|
||||
@ -60,26 +59,24 @@ type General struct {
|
||||
Sniffing bool `json:"sniffing"`
|
||||
EBpf EBpf `json:"-"`
|
||||
GlobalClientFingerprint string `json:"global-client-fingerprint"`
|
||||
GlobalUA string `json:"global-ua"`
|
||||
}
|
||||
|
||||
// Inbound config
|
||||
type Inbound struct {
|
||||
Port int `json:"port"`
|
||||
SocksPort int `json:"socks-port"`
|
||||
RedirPort int `json:"redir-port"`
|
||||
TProxyPort int `json:"tproxy-port"`
|
||||
MixedPort int `json:"mixed-port"`
|
||||
Tun LC.Tun `json:"tun"`
|
||||
TuicServer LC.TuicServer `json:"tuic-server"`
|
||||
ShadowSocksConfig string `json:"ss-config"`
|
||||
VmessConfig string `json:"vmess-config"`
|
||||
Authentication []string `json:"authentication"`
|
||||
SkipAuthPrefixes []netip.Prefix `json:"skip-auth-prefixes"`
|
||||
AllowLan bool `json:"allow-lan"`
|
||||
BindAddress string `json:"bind-address"`
|
||||
InboundTfo bool `json:"inbound-tfo"`
|
||||
InboundMPTCP bool `json:"inbound-mptcp"`
|
||||
Port int `json:"port"`
|
||||
SocksPort int `json:"socks-port"`
|
||||
RedirPort int `json:"redir-port"`
|
||||
TProxyPort int `json:"tproxy-port"`
|
||||
MixedPort int `json:"mixed-port"`
|
||||
Tun LC.Tun `json:"tun"`
|
||||
TuicServer LC.TuicServer `json:"tuic-server"`
|
||||
ShadowSocksConfig string `json:"ss-config"`
|
||||
VmessConfig string `json:"vmess-config"`
|
||||
Authentication []string `json:"authentication"`
|
||||
AllowLan bool `json:"allow-lan"`
|
||||
BindAddress string `json:"bind-address"`
|
||||
InboundTfo bool `json:"inbound-tfo"`
|
||||
InboundMPTCP bool `json:"inbound-mptcp"`
|
||||
}
|
||||
|
||||
// Controller config
|
||||
@ -90,16 +87,6 @@ type Controller struct {
|
||||
Secret string `json:"-"`
|
||||
}
|
||||
|
||||
// NTP config
|
||||
type NTP struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
Server string `yaml:"server"`
|
||||
Port int `yaml:"port"`
|
||||
Interval int `yaml:"interval"`
|
||||
DialerProxy string `yaml:"dialer-proxy"`
|
||||
WriteToSystem bool `yaml:"write-to-system"`
|
||||
}
|
||||
|
||||
// DNS config
|
||||
type DNS struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
@ -122,7 +109,7 @@ type DNS struct {
|
||||
type FallbackFilter struct {
|
||||
GeoIP bool `yaml:"geoip"`
|
||||
GeoIPCode string `yaml:"geoip-code"`
|
||||
IPCIDR []netip.Prefix `yaml:"ipcidr"`
|
||||
IPCIDR []*netip.Prefix `yaml:"ipcidr"`
|
||||
Domain []string `yaml:"domain"`
|
||||
GeoSite []*router.DomainMatcher `yaml:"geosite"`
|
||||
}
|
||||
@ -157,16 +144,13 @@ type Sniffer struct {
|
||||
|
||||
// Experimental config
|
||||
type Experimental struct {
|
||||
Fingerprints []string `yaml:"fingerprints"`
|
||||
QUICGoDisableGSO bool `yaml:"quic-go-disable-gso"`
|
||||
QUICGoDisableECN bool `yaml:"quic-go-disable-ecn"`
|
||||
Fingerprints []string `yaml:"fingerprints"`
|
||||
}
|
||||
|
||||
// Config is clash config manager
|
||||
type Config struct {
|
||||
General *General
|
||||
IPTables *IPTables
|
||||
NTP *NTP
|
||||
DNS *DNS
|
||||
Experimental *Experimental
|
||||
Hosts *trie.DomainTrie[resolver.HostValue]
|
||||
@ -183,15 +167,6 @@ type Config struct {
|
||||
TLS *TLS
|
||||
}
|
||||
|
||||
type RawNTP struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
Server string `yaml:"server"`
|
||||
ServerPort int `yaml:"server-port"`
|
||||
Interval int `yaml:"interval"`
|
||||
DialerProxy string `yaml:"dialer-proxy"`
|
||||
WriteToSystem bool `yaml:"write-to-system"`
|
||||
}
|
||||
|
||||
type RawDNS struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
PreferH3 bool `yaml:"prefer-h3"`
|
||||
@ -228,23 +203,21 @@ type RawTun struct {
|
||||
RedirectToTun []string `yaml:"-" json:"-"`
|
||||
|
||||
MTU uint32 `yaml:"mtu" json:"mtu,omitempty"`
|
||||
//Inet4Address []netip.Prefix `yaml:"inet4-address" json:"inet4_address,omitempty"`
|
||||
Inet6Address []netip.Prefix `yaml:"inet6-address" json:"inet6_address,omitempty"`
|
||||
StrictRoute bool `yaml:"strict-route" json:"strict_route,omitempty"`
|
||||
Inet4RouteAddress []netip.Prefix `yaml:"inet4-route-address" json:"inet4_route_address,omitempty"`
|
||||
Inet6RouteAddress []netip.Prefix `yaml:"inet6-route-address" json:"inet6_route_address,omitempty"`
|
||||
Inet4RouteExcludeAddress []netip.Prefix `yaml:"inet4-route-exclude-address" json:"inet4_route_exclude_address,omitempty"`
|
||||
Inet6RouteExcludeAddress []netip.Prefix `yaml:"inet6-route-exclude-address" json:"inet6_route_exclude_address,omitempty"`
|
||||
IncludeUID []uint32 `yaml:"include-uid" json:"include_uid,omitempty"`
|
||||
IncludeUIDRange []string `yaml:"include-uid-range" json:"include_uid_range,omitempty"`
|
||||
ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude_uid,omitempty"`
|
||||
ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude_uid_range,omitempty"`
|
||||
IncludeAndroidUser []int `yaml:"include-android-user" json:"include_android_user,omitempty"`
|
||||
IncludePackage []string `yaml:"include-package" json:"include_package,omitempty"`
|
||||
ExcludePackage []string `yaml:"exclude-package" json:"exclude_package,omitempty"`
|
||||
EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint_independent_nat,omitempty"`
|
||||
UDPTimeout int64 `yaml:"udp-timeout" json:"udp_timeout,omitempty"`
|
||||
FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"`
|
||||
//Inet4Address []LC.ListenPrefix `yaml:"inet4-address" json:"inet4_address,omitempty"`
|
||||
Inet6Address []LC.ListenPrefix `yaml:"inet6-address" json:"inet6_address,omitempty"`
|
||||
StrictRoute bool `yaml:"strict-route" json:"strict_route,omitempty"`
|
||||
Inet4RouteAddress []LC.ListenPrefix `yaml:"inet4_route_address" json:"inet4_route_address,omitempty"`
|
||||
Inet6RouteAddress []LC.ListenPrefix `yaml:"inet6_route_address" json:"inet6_route_address,omitempty"`
|
||||
IncludeUID []uint32 `yaml:"include-uid" json:"include_uid,omitempty"`
|
||||
IncludeUIDRange []string `yaml:"include-uid-range" json:"include_uid_range,omitempty"`
|
||||
ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude_uid,omitempty"`
|
||||
ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude_uid_range,omitempty"`
|
||||
IncludeAndroidUser []int `yaml:"include-android-user" json:"include_android_user,omitempty"`
|
||||
IncludePackage []string `yaml:"include-package" json:"include_package,omitempty"`
|
||||
ExcludePackage []string `yaml:"exclude-package" json:"exclude_package,omitempty"`
|
||||
EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint_independent_nat,omitempty"`
|
||||
UDPTimeout int64 `yaml:"udp-timeout" json:"udp_timeout,omitempty"`
|
||||
FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"`
|
||||
}
|
||||
|
||||
type RawTuicServer struct {
|
||||
@ -273,7 +246,6 @@ type RawConfig struct {
|
||||
InboundTfo bool `yaml:"inbound-tfo"`
|
||||
InboundMPTCP bool `yaml:"inbound-mptcp"`
|
||||
Authentication []string `yaml:"authentication"`
|
||||
SkipAuthPrefixes []netip.Prefix `yaml:"skip-auth-prefixes"`
|
||||
AllowLan bool `yaml:"allow-lan"`
|
||||
BindAddress string `yaml:"bind-address"`
|
||||
Mode T.TunnelMode `yaml:"mode"`
|
||||
@ -283,8 +255,6 @@ type RawConfig struct {
|
||||
ExternalController string `yaml:"external-controller"`
|
||||
ExternalControllerTLS string `yaml:"external-controller-tls"`
|
||||
ExternalUI string `yaml:"external-ui"`
|
||||
ExternalUIURL string `yaml:"external-ui-url" json:"external-ui-url"`
|
||||
ExternalUIName string `yaml:"external-ui-name" json:"external-ui-name"`
|
||||
Secret string `yaml:"secret"`
|
||||
Interface string `yaml:"interface-name"`
|
||||
RoutingMark int `yaml:"routing-mark"`
|
||||
@ -294,14 +264,11 @@ type RawConfig struct {
|
||||
TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"`
|
||||
FindProcessMode P.FindProcessMode `yaml:"find-process-mode" json:"find-process-mode"`
|
||||
GlobalClientFingerprint string `yaml:"global-client-fingerprint"`
|
||||
GlobalUA string `yaml:"global-ua"`
|
||||
KeepAliveInterval int `yaml:"keep-alive-interval"`
|
||||
|
||||
Sniffer RawSniffer `yaml:"sniffer"`
|
||||
ProxyProvider map[string]map[string]any `yaml:"proxy-providers"`
|
||||
RuleProvider map[string]map[string]any `yaml:"rule-providers"`
|
||||
Hosts map[string]any `yaml:"hosts"`
|
||||
NTP RawNTP `yaml:"ntp"`
|
||||
DNS RawDNS `yaml:"dns"`
|
||||
Tun RawTun `yaml:"tun"`
|
||||
TuicServer RawTuicServer `yaml:"tuic-server"`
|
||||
@ -381,7 +348,6 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
|
||||
ProxyGroup: []map[string]any{},
|
||||
TCPConcurrent: false,
|
||||
FindProcessMode: P.FindProcessStrict,
|
||||
GlobalUA: "clash.meta",
|
||||
Tun: RawTun{
|
||||
Enable: false,
|
||||
Device: "",
|
||||
@ -389,7 +355,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
|
||||
DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query
|
||||
AutoRoute: true,
|
||||
AutoDetectInterface: true,
|
||||
Inet6Address: []netip.Prefix{netip.MustParsePrefix("fdfe:dcba:9876::1/126")},
|
||||
Inet6Address: []LC.ListenPrefix{LC.ListenPrefix(netip.MustParsePrefix("fdfe:dcba:9876::1/126"))},
|
||||
},
|
||||
TuicServer: RawTuicServer{
|
||||
Enable: false,
|
||||
@ -413,13 +379,6 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
|
||||
InboundInterface: "lo",
|
||||
Bypass: []string{},
|
||||
},
|
||||
NTP: RawNTP{
|
||||
Enable: false,
|
||||
WriteToSystem: false,
|
||||
Server: "time.apple.com",
|
||||
ServerPort: 123,
|
||||
Interval: 30,
|
||||
},
|
||||
DNS: RawDNS{
|
||||
Enable: false,
|
||||
IPv6: false,
|
||||
@ -463,11 +422,10 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
|
||||
StoreSelected: true,
|
||||
},
|
||||
GeoXUrl: GeoXUrl{
|
||||
Mmdb: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
|
||||
GeoIp: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat",
|
||||
GeoSite: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat",
|
||||
Mmdb: "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb",
|
||||
GeoIp: "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat",
|
||||
GeoSite: "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat",
|
||||
},
|
||||
ExternalUIURL: "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip",
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(buf, rawCfg); err != nil {
|
||||
@ -535,9 +493,6 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
|
||||
}
|
||||
config.Hosts = hosts
|
||||
|
||||
ntpCfg := paresNTP(rawCfg)
|
||||
config.NTP = ntpCfg
|
||||
|
||||
dnsCfg, err := parseDNS(rawCfg, hosts, rules, ruleProviders)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -578,40 +533,19 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
|
||||
}
|
||||
|
||||
func parseGeneral(cfg *RawConfig) (*General, error) {
|
||||
externalUI := cfg.ExternalUI
|
||||
geodata.SetLoader(cfg.GeodataLoader)
|
||||
C.GeoIpUrl = cfg.GeoXUrl.GeoIp
|
||||
C.GeoSiteUrl = cfg.GeoXUrl.GeoSite
|
||||
C.MmdbUrl = cfg.GeoXUrl.Mmdb
|
||||
C.GeodataMode = cfg.GeodataMode
|
||||
C.UA = cfg.GlobalUA
|
||||
if cfg.KeepAliveInterval != 0 {
|
||||
N.KeepAliveInterval = time.Duration(cfg.KeepAliveInterval) * time.Second
|
||||
}
|
||||
|
||||
ExternalUIPath = cfg.ExternalUI
|
||||
// checkout externalUI exist
|
||||
if ExternalUIPath != "" {
|
||||
ExternalUIPath = C.Path.Resolve(ExternalUIPath)
|
||||
if _, err := os.Stat(ExternalUIPath); os.IsNotExist(err) {
|
||||
defaultUIpath := path.Join(C.Path.HomeDir(), "ui")
|
||||
log.Warnln("external-ui: %s does not exist, creating folder in %s", ExternalUIPath, defaultUIpath)
|
||||
if err := os.MkdirAll(defaultUIpath, os.ModePerm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ExternalUIPath = defaultUIpath
|
||||
cfg.ExternalUI = defaultUIpath
|
||||
if externalUI != "" {
|
||||
externalUI = C.Path.Resolve(externalUI)
|
||||
if _, err := os.Stat(externalUI); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("external-ui: %s not exist", externalUI)
|
||||
}
|
||||
}
|
||||
// checkout UIpath/name exist
|
||||
if cfg.ExternalUIName != "" {
|
||||
ExternalUIName = cfg.ExternalUIName
|
||||
} else {
|
||||
ExternalUIFolder = ExternalUIPath
|
||||
}
|
||||
if cfg.ExternalUIURL != "" {
|
||||
ExternalUIURL = cfg.ExternalUIURL
|
||||
}
|
||||
|
||||
cfg.Tun.RedirectToTun = cfg.EBpf.RedirectToTun
|
||||
return &General{
|
||||
Inbound: Inbound{
|
||||
@ -623,7 +557,6 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
|
||||
ShadowSocksConfig: cfg.ShadowSocksConfig,
|
||||
VmessConfig: cfg.VmessConfig,
|
||||
AllowLan: cfg.AllowLan,
|
||||
SkipAuthPrefixes: cfg.SkipAuthPrefixes,
|
||||
BindAddress: cfg.BindAddress,
|
||||
InboundTfo: cfg.InboundTfo,
|
||||
InboundMPTCP: cfg.InboundMPTCP,
|
||||
@ -647,7 +580,6 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
|
||||
FindProcessMode: cfg.FindProcessMode,
|
||||
EBpf: cfg.EBpf,
|
||||
GlobalClientFingerprint: cfg.GlobalClientFingerprint,
|
||||
GlobalUA: cfg.GlobalUA,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -1049,6 +981,7 @@ func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error)
|
||||
Net: dnsNetType,
|
||||
Addr: addr,
|
||||
ProxyName: proxyName,
|
||||
Interface: dialer.DefaultInterface,
|
||||
Params: params,
|
||||
PreferH3: preferH3,
|
||||
},
|
||||
@ -1150,15 +1083,15 @@ func parseNameServerPolicy(nsPolicy map[string]any, ruleProviders map[string]pro
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
func parseFallbackIPCIDR(ips []string) ([]netip.Prefix, error) {
|
||||
var ipNets []netip.Prefix
|
||||
func parseFallbackIPCIDR(ips []string) ([]*netip.Prefix, error) {
|
||||
var ipNets []*netip.Prefix
|
||||
|
||||
for idx, ip := range ips {
|
||||
ipnet, err := netip.ParsePrefix(ip)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("DNS FallbackIP[%d] format error: %s", idx, err.Error())
|
||||
}
|
||||
ipNets = append(ipNets, ipnet)
|
||||
ipNets = append(ipNets, &ipnet)
|
||||
}
|
||||
|
||||
return ipNets, nil
|
||||
@ -1199,19 +1132,6 @@ func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainM
|
||||
return sites, nil
|
||||
}
|
||||
|
||||
func paresNTP(rawCfg *RawConfig) *NTP {
|
||||
cfg := rawCfg.NTP
|
||||
ntpCfg := &NTP{
|
||||
Enable: cfg.Enable,
|
||||
Server: cfg.Server,
|
||||
Port: cfg.ServerPort,
|
||||
Interval: cfg.Interval,
|
||||
DialerProxy: cfg.DialerProxy,
|
||||
WriteToSystem: cfg.WriteToSystem,
|
||||
}
|
||||
return ntpCfg
|
||||
}
|
||||
|
||||
func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rules []C.Rule, ruleProviders map[string]providerTypes.RuleProvider) (*DNS, error) {
|
||||
cfg := rawCfg.DNS
|
||||
if cfg.Enable && len(cfg.NameServer) == 0 {
|
||||
@ -1226,7 +1146,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rul
|
||||
IPv6: cfg.IPv6,
|
||||
EnhancedMode: cfg.EnhancedMode,
|
||||
FallbackFilter: FallbackFilter{
|
||||
IPCIDR: []netip.Prefix{},
|
||||
IPCIDR: []*netip.Prefix{},
|
||||
GeoSite: []*router.DomainMatcher{},
|
||||
},
|
||||
}
|
||||
@ -1300,7 +1220,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rul
|
||||
}
|
||||
|
||||
pool, err := fakeip.New(fakeip.Options{
|
||||
IPNet: fakeIPRange,
|
||||
IPNet: &fakeIPRange,
|
||||
Size: 1000,
|
||||
Host: host,
|
||||
Persistence: rawCfg.Profile.StoreFakeIP,
|
||||
@ -1363,24 +1283,22 @@ func parseTun(rawTun RawTun, general *General) error {
|
||||
AutoDetectInterface: rawTun.AutoDetectInterface,
|
||||
RedirectToTun: rawTun.RedirectToTun,
|
||||
|
||||
MTU: rawTun.MTU,
|
||||
Inet4Address: []netip.Prefix{tunAddressPrefix},
|
||||
Inet6Address: rawTun.Inet6Address,
|
||||
StrictRoute: rawTun.StrictRoute,
|
||||
Inet4RouteAddress: rawTun.Inet4RouteAddress,
|
||||
Inet6RouteAddress: rawTun.Inet6RouteAddress,
|
||||
Inet4RouteExcludeAddress: rawTun.Inet4RouteExcludeAddress,
|
||||
Inet6RouteExcludeAddress: rawTun.Inet6RouteExcludeAddress,
|
||||
IncludeUID: rawTun.IncludeUID,
|
||||
IncludeUIDRange: rawTun.IncludeUIDRange,
|
||||
ExcludeUID: rawTun.ExcludeUID,
|
||||
ExcludeUIDRange: rawTun.ExcludeUIDRange,
|
||||
IncludeAndroidUser: rawTun.IncludeAndroidUser,
|
||||
IncludePackage: rawTun.IncludePackage,
|
||||
ExcludePackage: rawTun.ExcludePackage,
|
||||
EndpointIndependentNat: rawTun.EndpointIndependentNat,
|
||||
UDPTimeout: rawTun.UDPTimeout,
|
||||
FileDescriptor: rawTun.FileDescriptor,
|
||||
MTU: rawTun.MTU,
|
||||
Inet4Address: []LC.ListenPrefix{LC.ListenPrefix(tunAddressPrefix)},
|
||||
Inet6Address: rawTun.Inet6Address,
|
||||
StrictRoute: rawTun.StrictRoute,
|
||||
Inet4RouteAddress: rawTun.Inet4RouteAddress,
|
||||
Inet6RouteAddress: rawTun.Inet6RouteAddress,
|
||||
IncludeUID: rawTun.IncludeUID,
|
||||
IncludeUIDRange: rawTun.IncludeUIDRange,
|
||||
ExcludeUID: rawTun.ExcludeUID,
|
||||
ExcludeUIDRange: rawTun.ExcludeUIDRange,
|
||||
IncludeAndroidUser: rawTun.IncludeAndroidUser,
|
||||
IncludePackage: rawTun.IncludePackage,
|
||||
ExcludePackage: rawTun.ExcludePackage,
|
||||
EndpointIndependentNat: rawTun.EndpointIndependentNat,
|
||||
UDPTimeout: rawTun.UDPTimeout,
|
||||
FileDescriptor: rawTun.FileDescriptor,
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -1,11 +1,17 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/component/geodata"
|
||||
_ "github.com/Dreamacro/clash/component/geodata/standard"
|
||||
clashHttp "github.com/Dreamacro/clash/component/http"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
"github.com/oschwald/maxminddb-golang"
|
||||
@ -66,3 +72,19 @@ func UpdateGeoDatabases() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func downloadForBytes(url string) ([]byte, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
|
||||
defer cancel()
|
||||
resp, err := clashHttp.HttpRequest(ctx, url, http.MethodGet, http.Header{"User-Agent": {"clash"}}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func saveFile(bytes []byte, path string) error {
|
||||
return os.WriteFile(path, bytes, 0o644)
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
var (
|
||||
ExternalUIURL string
|
||||
ExternalUIPath string
|
||||
ExternalUIFolder string
|
||||
ExternalUIName string
|
||||
)
|
||||
var (
|
||||
ErrIncompleteConf = errors.New("ExternalUI configure incomplete")
|
||||
)
|
||||
var xdMutex sync.Mutex
|
||||
|
||||
func UpdateUI() error {
|
||||
xdMutex.Lock()
|
||||
defer xdMutex.Unlock()
|
||||
|
||||
err := prepare()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := downloadForBytes(ExternalUIURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't download file: %w", err)
|
||||
}
|
||||
|
||||
saved := path.Join(C.Path.HomeDir(), "download.zip")
|
||||
if saveFile(data, saved) != nil {
|
||||
return fmt.Errorf("can't save zip file: %w", err)
|
||||
}
|
||||
defer os.Remove(saved)
|
||||
|
||||
err = cleanup(ExternalUIFolder)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("cleanup exist file error: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
unzipFolder, err := unzip(saved, C.Path.HomeDir())
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't extract zip file: %w", err)
|
||||
}
|
||||
|
||||
err = os.Rename(unzipFolder, ExternalUIFolder)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't rename folder: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepare() error {
|
||||
if ExternalUIPath == "" || ExternalUIURL == "" {
|
||||
return ErrIncompleteConf
|
||||
}
|
||||
|
||||
if ExternalUIName != "" {
|
||||
ExternalUIFolder = filepath.Clean(path.Join(ExternalUIPath, ExternalUIName))
|
||||
if _, err := os.Stat(ExternalUIPath); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(ExternalUIPath, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ExternalUIFolder = ExternalUIPath
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func unzip(src, dest string) (string, error) {
|
||||
r, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer r.Close()
|
||||
var extractedFolder string
|
||||
for _, f := range r.File {
|
||||
fpath := filepath.Join(dest, f.Name)
|
||||
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
|
||||
return "", fmt.Errorf("invalid file path: %s", fpath)
|
||||
}
|
||||
if f.FileInfo().IsDir() {
|
||||
os.MkdirAll(fpath, os.ModePerm)
|
||||
continue
|
||||
}
|
||||
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = io.Copy(outFile, rc)
|
||||
outFile.Close()
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if extractedFolder == "" {
|
||||
extractedFolder = filepath.Dir(fpath)
|
||||
}
|
||||
}
|
||||
return extractedFolder, nil
|
||||
}
|
||||
|
||||
func cleanup(root string) error {
|
||||
if _, err := os.Stat(root); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
if err := os.RemoveAll(path); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := os.Remove(path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
@ -1,37 +1,15 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter/outboundgroup"
|
||||
"github.com/Dreamacro/clash/common/structure"
|
||||
clashHttp "github.com/Dreamacro/clash/component/http"
|
||||
)
|
||||
|
||||
func downloadForBytes(url string) ([]byte, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
|
||||
defer cancel()
|
||||
resp, err := clashHttp.HttpRequest(ctx, url, http.MethodGet, http.Header{"User-Agent": {"clash"}}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func saveFile(bytes []byte, path string) error {
|
||||
return os.WriteFile(path, bytes, 0o644)
|
||||
}
|
||||
|
||||
func trimArr(arr []string) (r []string) {
|
||||
for _, e := range arr {
|
||||
r = append(r, strings.Trim(e, " "))
|
||||
|
@ -36,7 +36,6 @@ const (
|
||||
Vless
|
||||
Trojan
|
||||
Hysteria
|
||||
Hysteria2
|
||||
WireGuard
|
||||
Tuic
|
||||
)
|
||||
@ -201,8 +200,6 @@ func (at AdapterType) String() string {
|
||||
return "Trojan"
|
||||
case Hysteria:
|
||||
return "Hysteria"
|
||||
case Hysteria2:
|
||||
return "Hysteria2"
|
||||
case WireGuard:
|
||||
return "WireGuard"
|
||||
case Tuic:
|
||||
@ -252,23 +249,6 @@ type PacketAdapter interface {
|
||||
Metadata() *Metadata
|
||||
}
|
||||
|
||||
type packetAdapter struct {
|
||||
UDPPacket
|
||||
metadata *Metadata
|
||||
}
|
||||
|
||||
// Metadata returns destination metadata
|
||||
func (s *packetAdapter) Metadata() *Metadata {
|
||||
return s.metadata
|
||||
}
|
||||
|
||||
func NewPacketAdapter(packet UDPPacket, metadata *Metadata) PacketAdapter {
|
||||
return &packetAdapter{
|
||||
packet,
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
|
||||
type WriteBack interface {
|
||||
WriteBack(b []byte, addr net.Addr) (n int, err error)
|
||||
}
|
||||
@ -287,17 +267,13 @@ type NatTable interface {
|
||||
|
||||
Delete(key string)
|
||||
|
||||
DeleteLock(key string)
|
||||
GetLocalConn(lAddr, rAddr string) *net.UDPConn
|
||||
|
||||
GetForLocalConn(lAddr, rAddr string) *net.UDPConn
|
||||
AddLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool
|
||||
|
||||
AddForLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool
|
||||
RangeLocalConn(lAddr string, f func(key, value any) bool)
|
||||
|
||||
RangeForLocalConn(lAddr string, f func(key string, value *net.UDPConn) bool)
|
||||
GetOrCreateLockForLocalConn(lAddr, key string) (*sync.Cond, bool)
|
||||
|
||||
GetOrCreateLockForLocalConn(lAddr string, key string) (*sync.Cond, bool)
|
||||
|
||||
DeleteForLocalConn(lAddr, key string)
|
||||
|
||||
DeleteLockForLocalConn(lAddr, key string)
|
||||
DeleteLocalConnMap(lAddr, key string)
|
||||
}
|
||||
|
@ -1,5 +0,0 @@
|
||||
package constant
|
||||
|
||||
var (
|
||||
UA string
|
||||
)
|
@ -16,7 +16,7 @@ type MultiAddrListener interface {
|
||||
|
||||
type InboundListener interface {
|
||||
Name() string
|
||||
Listen(tunnel Tunnel) error
|
||||
Listen(tcpIn chan<- ConnContext, udpIn chan<- PacketAdapter, natTable NatTable) error
|
||||
Close() error
|
||||
Address() string
|
||||
RawAddress() string
|
||||
|
@ -30,7 +30,6 @@ const (
|
||||
TUNNEL
|
||||
TUN
|
||||
TUIC
|
||||
HYSTERIA2
|
||||
INNER
|
||||
)
|
||||
|
||||
@ -79,8 +78,6 @@ func (t Type) String() string {
|
||||
return "Tun"
|
||||
case TUIC:
|
||||
return "Tuic"
|
||||
case HYSTERIA2:
|
||||
return "Hysteria2"
|
||||
case INNER:
|
||||
return "Inner"
|
||||
default:
|
||||
@ -113,8 +110,6 @@ func ParseType(t string) (*Type, error) {
|
||||
res = TUN
|
||||
case "TUIC":
|
||||
res = TUIC
|
||||
case "HYSTERIA2":
|
||||
res = HYSTERIA2
|
||||
case "INNER":
|
||||
res = INNER
|
||||
default:
|
||||
@ -240,34 +235,6 @@ func (m *Metadata) Valid() bool {
|
||||
return m.Host != "" || m.DstIP.IsValid()
|
||||
}
|
||||
|
||||
func (m *Metadata) SetRemoteAddr(addr net.Addr) error {
|
||||
if addr == nil {
|
||||
return nil
|
||||
}
|
||||
if rawAddr, ok := addr.(interface{ RawAddr() net.Addr }); ok {
|
||||
if rawAddr := rawAddr.RawAddr(); rawAddr != nil {
|
||||
if err := m.SetRemoteAddr(rawAddr); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if addr, ok := addr.(interface{ AddrPort() netip.AddrPort }); ok { // *net.TCPAddr, *net.UDPAddr, M.Socksaddr
|
||||
if addrPort := addr.AddrPort(); addrPort.Port() != 0 {
|
||||
m.DstPort = addrPort.Port()
|
||||
if addrPort.IsValid() { // sing's M.Socksaddr maybe return an invalid AddrPort if it's a DomainName
|
||||
m.DstIP = addrPort.Addr().Unmap()
|
||||
return nil
|
||||
} else {
|
||||
if addr, ok := addr.(interface{ AddrString() string }); ok { // must be sing's M.Socksaddr
|
||||
m.Host = addr.AddrString() // actually is M.Socksaddr.Fqdn
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return m.SetRemoteAddress(addr.String())
|
||||
}
|
||||
|
||||
func (m *Metadata) SetRemoteAddress(rawAddress string) error {
|
||||
host, port, err := net.SplitHostPort(rawAddress)
|
||||
if err != nil {
|
||||
|
@ -18,9 +18,6 @@ var (
|
||||
)
|
||||
|
||||
// Path is used to get the configuration path
|
||||
//
|
||||
// on Unix systems, `$HOME/.config/clash`.
|
||||
// on Windows, `%USERPROFILE%/.config/clash`.
|
||||
var Path = func() *path {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
@ -28,13 +25,6 @@ var Path = func() *path {
|
||||
}
|
||||
allowUnsafePath, _ := strconv.ParseBool(os.Getenv("SKIP_SAFE_PATH_CHECK"))
|
||||
homeDir = P.Join(homeDir, ".config", Name)
|
||||
|
||||
if _, err = os.Stat(homeDir); err != nil {
|
||||
if configHome, ok := os.LookupEnv("XDG_CONFIG_HOME"); ok {
|
||||
homeDir = P.Join(configHome, Name)
|
||||
}
|
||||
}
|
||||
|
||||
return &path{homeDir: homeDir, configFile: "config.yaml", allowUnsafePath: allowUnsafePath}
|
||||
}()
|
||||
|
||||
|
@ -4,8 +4,7 @@ import "github.com/Dreamacro/clash/constant"
|
||||
|
||||
type Sniffer interface {
|
||||
SupportNetwork() constant.NetWork
|
||||
// SniffData must not change input bytes
|
||||
SniffData(bytes []byte) (string, error)
|
||||
SniffTCP(bytes []byte) (string, error)
|
||||
Protocol() string
|
||||
SupportPort(port uint16) bool
|
||||
}
|
||||
@ -13,11 +12,10 @@ type Sniffer interface {
|
||||
const (
|
||||
TLS Type = iota
|
||||
HTTP
|
||||
QUIC
|
||||
)
|
||||
|
||||
var (
|
||||
List = []Type{TLS, HTTP, QUIC}
|
||||
List = []Type{TLS, HTTP}
|
||||
)
|
||||
|
||||
type Type int
|
||||
@ -28,8 +26,6 @@ func (rt Type) String() string {
|
||||
return "TLS"
|
||||
case HTTP:
|
||||
return "HTTP"
|
||||
case QUIC:
|
||||
return "QUIC"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
package constant
|
||||
|
||||
import "net"
|
||||
|
||||
type Tunnel interface {
|
||||
// HandleTCPConn will handle a tcp connection blocking
|
||||
HandleTCPConn(conn net.Conn, metadata *Metadata)
|
||||
// HandleUDPPacket will handle a udp packet nonblocking
|
||||
HandleUDPPacket(packet UDPPacket, metadata *Metadata)
|
||||
// NatTable return nat table
|
||||
NatTable() NatTable
|
||||
}
|
@ -8,9 +8,10 @@ import (
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/Dreamacro/clash/component/ca"
|
||||
"github.com/Dreamacro/clash/common/atomic"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
"github.com/Dreamacro/clash/component/resolver"
|
||||
tlsC "github.com/Dreamacro/clash/component/tls"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
D "github.com/miekg/dns"
|
||||
@ -22,7 +23,7 @@ type client struct {
|
||||
r *Resolver
|
||||
port string
|
||||
host string
|
||||
iface string
|
||||
iface *atomic.TypedValue[string]
|
||||
proxyAdapter C.ProxyAdapter
|
||||
proxyName string
|
||||
addr string
|
||||
@ -47,6 +48,10 @@ func (c *client) Address() string {
|
||||
return c.addr
|
||||
}
|
||||
|
||||
func (c *client) Exchange(m *D.Msg) (*D.Msg, error) {
|
||||
return c.ExchangeContext(context.Background(), m)
|
||||
}
|
||||
|
||||
func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) {
|
||||
var (
|
||||
ip netip.Addr
|
||||
@ -72,9 +77,9 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error)
|
||||
network = "tcp"
|
||||
}
|
||||
|
||||
var options []dialer.Option
|
||||
if c.iface != "" {
|
||||
options = append(options, dialer.WithInterface(c.iface))
|
||||
options := []dialer.Option{}
|
||||
if c.iface != nil && c.iface.Load() != "" {
|
||||
options = append(options, dialer.WithInterface(c.iface.Load()))
|
||||
}
|
||||
|
||||
conn, err := getDialHandler(c.r, c.proxyAdapter, c.proxyName, options...)(ctx, network, net.JoinHostPort(ip.String(), c.port))
|
||||
@ -94,7 +99,7 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error)
|
||||
ch := make(chan result, 1)
|
||||
go func() {
|
||||
if strings.HasSuffix(c.Client.Net, "tls") {
|
||||
conn = tls.Client(conn, ca.GetGlobalTLSConfig(c.Client.TLSConfig))
|
||||
conn = tls.Client(conn, tlsC.GetGlobalTLSConfig(c.Client.TLSConfig))
|
||||
}
|
||||
|
||||
msg, _, err := c.Client.ExchangeWithConn(m, &D.Conn{
|
||||
|
14
dns/dhcp.go
14
dns/dhcp.go
@ -8,8 +8,11 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/atomic"
|
||||
"github.com/Dreamacro/clash/component/dhcp"
|
||||
"github.com/Dreamacro/clash/component/iface"
|
||||
"github.com/Dreamacro/clash/component/resolver"
|
||||
|
||||
D "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
@ -26,7 +29,7 @@ type dhcpClient struct {
|
||||
ifaceInvalidate time.Time
|
||||
dnsInvalidate time.Time
|
||||
|
||||
ifaceAddr netip.Prefix
|
||||
ifaceAddr *netip.Prefix
|
||||
done chan struct{}
|
||||
clients []dnsClient
|
||||
err error
|
||||
@ -43,6 +46,13 @@ func (d *dhcpClient) Address() string {
|
||||
return strings.Join(addrs, ",")
|
||||
}
|
||||
|
||||
func (d *dhcpClient) Exchange(m *D.Msg) (msg *D.Msg, err error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout)
|
||||
defer cancel()
|
||||
|
||||
return d.ExchangeContext(ctx, m)
|
||||
}
|
||||
|
||||
func (d *dhcpClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
|
||||
clients, err := d.resolve(ctx)
|
||||
if err != nil {
|
||||
@ -76,7 +86,7 @@ func (d *dhcpClient) resolve(ctx context.Context) ([]dnsClient, error) {
|
||||
for _, item := range dns {
|
||||
nameserver = append(nameserver, NameServer{
|
||||
Addr: net.JoinHostPort(item.String(), "53"),
|
||||
Interface: d.ifaceName,
|
||||
Interface: atomic.NewTypedValue(d.ifaceName),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/component/ca"
|
||||
tlsC "github.com/Dreamacro/clash/component/tls"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
"github.com/metacubex/quic-go"
|
||||
@ -157,6 +157,11 @@ func (doh *dnsOverHTTPS) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.
|
||||
return msg, err
|
||||
}
|
||||
|
||||
// Exchange implements the Upstream interface for *dnsOverHTTPS.
|
||||
func (doh *dnsOverHTTPS) Exchange(m *D.Msg) (*D.Msg, error) {
|
||||
return doh.ExchangeContext(context.Background(), m)
|
||||
}
|
||||
|
||||
// Close implements the Upstream interface for *dnsOverHTTPS.
|
||||
func (doh *dnsOverHTTPS) Close() (err error) {
|
||||
doh.clientMu.Lock()
|
||||
@ -377,7 +382,7 @@ func (doh *dnsOverHTTPS) createClient(ctx context.Context) (*http.Client, error)
|
||||
// HTTP3 is enabled in the upstream options). If this attempt is successful,
|
||||
// it returns an HTTP3 transport, otherwise it returns the H1/H2 transport.
|
||||
func (doh *dnsOverHTTPS) createTransport(ctx context.Context) (t http.RoundTripper, err error) {
|
||||
tlsConfig := ca.GetGlobalTLSConfig(
|
||||
tlsConfig := tlsC.GetGlobalTLSConfig(
|
||||
&tls.Config{
|
||||
InsecureSkipVerify: false,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/component/ca"
|
||||
tlsC "github.com/Dreamacro/clash/component/tls"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
"github.com/metacubex/quic-go"
|
||||
@ -134,6 +134,11 @@ func (doq *dnsOverQUIC) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.M
|
||||
return msg, err
|
||||
}
|
||||
|
||||
// Exchange implements the Upstream interface for *dnsOverQUIC.
|
||||
func (doq *dnsOverQUIC) Exchange(m *D.Msg) (msg *D.Msg, err error) {
|
||||
return doq.ExchangeContext(context.Background(), m)
|
||||
}
|
||||
|
||||
// Close implements the Upstream interface for *dnsOverQUIC.
|
||||
func (doq *dnsOverQUIC) Close() (err error) {
|
||||
doq.connMu.Lock()
|
||||
@ -325,7 +330,7 @@ func (doq *dnsOverQUIC) openConnection(ctx context.Context) (conn quic.Connectio
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tlsConfig := ca.GetGlobalTLSConfig(
|
||||
tlsConfig := tlsC.GetGlobalTLSConfig(
|
||||
&tls.Config{
|
||||
ServerName: host,
|
||||
InsecureSkipVerify: false,
|
||||
|
@ -45,7 +45,7 @@ func (gf *geoipFilter) Match(ip netip.Addr) bool {
|
||||
}
|
||||
|
||||
type ipnetFilter struct {
|
||||
ipnet netip.Prefix
|
||||
ipnet *netip.Prefix
|
||||
}
|
||||
|
||||
func (inf *ipnetFilter) Match(ip netip.Addr) bool {
|
||||
|
@ -39,12 +39,16 @@ type rcodeClient struct {
|
||||
|
||||
var _ dnsClient = rcodeClient{}
|
||||
|
||||
func (r rcodeClient) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) {
|
||||
func (r rcodeClient) Exchange(m *D.Msg) (*D.Msg, error) {
|
||||
m.Response = true
|
||||
m.Rcode = r.rcode
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (r rcodeClient) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) {
|
||||
return r.Exchange(m)
|
||||
}
|
||||
|
||||
func (r rcodeClient) Address() string {
|
||||
return r.addr
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/atomic"
|
||||
"github.com/Dreamacro/clash/common/cache"
|
||||
"github.com/Dreamacro/clash/component/fakeip"
|
||||
"github.com/Dreamacro/clash/component/geodata/router"
|
||||
@ -17,11 +18,11 @@ import (
|
||||
"github.com/Dreamacro/clash/log"
|
||||
|
||||
D "github.com/miekg/dns"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
type dnsClient interface {
|
||||
Exchange(m *D.Msg) (msg *D.Msg, err error)
|
||||
ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error)
|
||||
Address() string
|
||||
}
|
||||
@ -134,6 +135,11 @@ func (r *Resolver) shouldIPFallback(ip netip.Addr) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Exchange a batch of dns request, and it use cache
|
||||
func (r *Resolver) Exchange(m *D.Msg) (msg *D.Msg, err error) {
|
||||
return r.ExchangeContext(context.Background(), m)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if len(m.Question) == 0 {
|
||||
@ -188,25 +194,20 @@ func (r *Resolver) exchangeWithoutCache(ctx context.Context, m *D.Msg) (msg *D.M
|
||||
msg := result.(*D.Msg)
|
||||
|
||||
if cache {
|
||||
// OPT RRs MUST NOT be cached, forwarded, or stored in or loaded from master files.
|
||||
msg.Extra = lo.Filter(msg.Extra, func(rr D.RR, index int) bool {
|
||||
return rr.Header().Rrtype != D.TypeOPT
|
||||
})
|
||||
putMsgToCache(r.lruCache, q.String(), q, msg)
|
||||
putMsgToCache(r.lruCache, q.String(), msg)
|
||||
}
|
||||
}()
|
||||
|
||||
isIPReq := isIPRequest(q)
|
||||
if isIPReq {
|
||||
cache = true
|
||||
return r.ipExchange(ctx, m)
|
||||
}
|
||||
|
||||
if matched := r.matchPolicy(m); len(matched) != 0 {
|
||||
result, cache, err = batchExchange(ctx, matched, m)
|
||||
result, cache, err = r.batchExchange(ctx, matched, m)
|
||||
return
|
||||
}
|
||||
result, cache, err = batchExchange(ctx, r.main, m)
|
||||
result, cache, err = r.batchExchange(ctx, r.main, m)
|
||||
return
|
||||
}
|
||||
|
||||
@ -248,6 +249,13 @@ func (r *Resolver) exchangeWithoutCache(ctx context.Context, m *D.Msg) (msg *D.M
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Resolver) batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.Msg, cache bool, err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, resolver.DefaultDNSTimeout)
|
||||
defer cancel()
|
||||
|
||||
return batchExchange(ctx, clients, m)
|
||||
}
|
||||
|
||||
func (r *Resolver) matchPolicy(m *D.Msg) []dnsClient {
|
||||
if r.policy == nil {
|
||||
return nil
|
||||
@ -323,10 +331,7 @@ func (r *Resolver) ipExchange(ctx context.Context, m *D.Msg) (msg *D.Msg, err er
|
||||
res := <-msgCh
|
||||
if res.Error == nil {
|
||||
if ips := msgToIP(res.Msg); len(ips) != 0 {
|
||||
shouldNotFallback := lo.EveryBy(ips, func(ip netip.Addr) bool {
|
||||
return !r.shouldIPFallback(ip)
|
||||
})
|
||||
if shouldNotFallback {
|
||||
if !r.shouldIPFallback(ips[0]) {
|
||||
msg, err = res.Msg, res.Error // no need to wait for fallback result
|
||||
return
|
||||
}
|
||||
@ -371,7 +376,7 @@ func (r *Resolver) lookupIP(ctx context.Context, host string, dnsType uint16) (i
|
||||
func (r *Resolver) asyncExchange(ctx context.Context, client []dnsClient, msg *D.Msg) <-chan *result {
|
||||
ch := make(chan *result, 1)
|
||||
go func() {
|
||||
res, _, err := batchExchange(ctx, client, msg)
|
||||
res, _, err := r.batchExchange(ctx, client, msg)
|
||||
ch <- &result{Msg: res, Error: err}
|
||||
}()
|
||||
return ch
|
||||
@ -388,7 +393,7 @@ func (r *Resolver) Invalid() bool {
|
||||
type NameServer struct {
|
||||
Net string
|
||||
Addr string
|
||||
Interface string
|
||||
Interface *atomic.TypedValue[string]
|
||||
ProxyAdapter C.ProxyAdapter
|
||||
ProxyName string
|
||||
Params map[string]string
|
||||
@ -398,7 +403,7 @@ type NameServer struct {
|
||||
type FallbackFilter struct {
|
||||
GeoIP bool
|
||||
GeoIPCode string
|
||||
IPCIDR []netip.Prefix
|
||||
IPCIDR []*netip.Prefix
|
||||
Domain []string
|
||||
GeoSite []*router.DomainMatcher
|
||||
}
|
||||
|
112
dns/system.go
112
dns/system.go
@ -1,113 +1,23 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/log"
|
||||
|
||||
D "github.com/miekg/dns"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
SystemDnsFlushTime = 5 * time.Minute
|
||||
SystemDnsDeleteTimes = 12 // 12*5 = 60min
|
||||
)
|
||||
|
||||
type systemDnsClient struct {
|
||||
disableTimes uint32
|
||||
dnsClient
|
||||
}
|
||||
|
||||
type systemClient struct {
|
||||
mu sync.Mutex
|
||||
dnsClients map[string]*systemDnsClient
|
||||
lastFlush time.Time
|
||||
}
|
||||
|
||||
func (c *systemClient) getDnsClients() ([]dnsClient, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
var err error
|
||||
if time.Since(c.lastFlush) > SystemDnsFlushTime {
|
||||
var nameservers []string
|
||||
if nameservers, err = dnsReadConfig(); err == nil {
|
||||
log.Debugln("[DNS] system dns update to %s", nameservers)
|
||||
for _, addr := range nameservers {
|
||||
if _, ok := c.dnsClients[addr]; !ok {
|
||||
clients := transform(
|
||||
[]NameServer{{
|
||||
Addr: net.JoinHostPort(addr, "53"),
|
||||
Net: "udp",
|
||||
}},
|
||||
nil,
|
||||
)
|
||||
if len(clients) > 0 {
|
||||
c.dnsClients[addr] = &systemDnsClient{
|
||||
disableTimes: 0,
|
||||
dnsClient: clients[0],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
available := 0
|
||||
for nameserver, sdc := range c.dnsClients {
|
||||
if slices.Contains(nameservers, nameserver) {
|
||||
sdc.disableTimes = 0 // enable
|
||||
available++
|
||||
} else {
|
||||
if sdc.disableTimes > SystemDnsDeleteTimes {
|
||||
delete(c.dnsClients, nameserver) // drop too old dnsClient
|
||||
} else {
|
||||
sdc.disableTimes++
|
||||
}
|
||||
}
|
||||
}
|
||||
if available > 0 {
|
||||
c.lastFlush = time.Now()
|
||||
}
|
||||
}
|
||||
}
|
||||
dnsClients := make([]dnsClient, 0, len(c.dnsClients))
|
||||
for _, sdc := range c.dnsClients {
|
||||
if sdc.disableTimes == 0 {
|
||||
dnsClients = append(dnsClients, sdc.dnsClient)
|
||||
}
|
||||
}
|
||||
if len(dnsClients) > 0 {
|
||||
return dnsClients, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (c *systemClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
|
||||
dnsClients, err := c.getDnsClients()
|
||||
func loadSystemResolver() (clients []dnsClient, err error) {
|
||||
nameservers, err := dnsReadConfig()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
msg, _, err = batchExchange(ctx, dnsClients, m)
|
||||
return
|
||||
}
|
||||
|
||||
// Address implements dnsClient
|
||||
func (c *systemClient) Address() string {
|
||||
dnsClients, _ := c.getDnsClients()
|
||||
addrs := make([]string, 0, len(dnsClients))
|
||||
for _, c := range dnsClients {
|
||||
addrs = append(addrs, c.Address())
|
||||
if len(nameservers) == 0 {
|
||||
return
|
||||
}
|
||||
return fmt.Sprintf("system(%s)", strings.Join(addrs, ","))
|
||||
}
|
||||
|
||||
var _ dnsClient = (*systemClient)(nil)
|
||||
|
||||
func newSystemClient() *systemClient {
|
||||
return &systemClient{
|
||||
dnsClients: map[string]*systemDnsClient{},
|
||||
servers := make([]NameServer, 0, len(nameservers))
|
||||
for _, addr := range nameservers {
|
||||
servers = append(servers, NameServer{
|
||||
Addr: net.JoinHostPort(addr, "53"),
|
||||
Net: "udp",
|
||||
})
|
||||
}
|
||||
return transform(servers, nil), nil
|
||||
}
|
||||
|
81
dns/util.go
81
dns/util.go
@ -29,16 +29,10 @@ const (
|
||||
MaxMsgSize = 65535
|
||||
)
|
||||
|
||||
const serverFailureCacheTTL uint32 = 5
|
||||
|
||||
func minimalTTL(records []D.RR) uint32 {
|
||||
rr := lo.MinBy(records, func(r1 D.RR, r2 D.RR) bool {
|
||||
return lo.MinBy(records, func(r1 D.RR, r2 D.RR) bool {
|
||||
return r1.Header().Ttl < r2.Header().Ttl
|
||||
})
|
||||
if rr == nil {
|
||||
return 0
|
||||
}
|
||||
return rr.Header().Ttl
|
||||
}).Header().Ttl
|
||||
}
|
||||
|
||||
func updateTTL(records []D.RR, ttl uint32) {
|
||||
@ -51,25 +45,28 @@ func updateTTL(records []D.RR, ttl uint32) {
|
||||
}
|
||||
}
|
||||
|
||||
func putMsgToCache(c *cache.LruCache[string, *D.Msg], key string, q D.Question, msg *D.Msg) {
|
||||
func putMsgToCache(c *cache.LruCache[string, *D.Msg], key string, msg *D.Msg) {
|
||||
// skip dns cache for acme challenge
|
||||
if q.Qtype == D.TypeTXT && strings.HasPrefix(q.Name, "_acme-challenge.") {
|
||||
log.Debugln("[DNS] dns cache ignored because of acme challenge for: %s", q.Name)
|
||||
if len(msg.Question) != 0 {
|
||||
if q := msg.Question[0]; q.Qtype == D.TypeTXT && strings.HasPrefix(q.Name, "_acme-challenge") {
|
||||
log.Debugln("[DNS] dns cache ignored because of acme challenge for: %s", q.Name)
|
||||
return
|
||||
}
|
||||
}
|
||||
var ttl uint32
|
||||
switch {
|
||||
case len(msg.Answer) != 0:
|
||||
ttl = minimalTTL(msg.Answer)
|
||||
case len(msg.Ns) != 0:
|
||||
ttl = minimalTTL(msg.Ns)
|
||||
case len(msg.Extra) != 0:
|
||||
ttl = minimalTTL(msg.Extra)
|
||||
default:
|
||||
log.Debugln("[DNS] response msg empty: %#v", msg)
|
||||
return
|
||||
}
|
||||
|
||||
var ttl uint32
|
||||
if msg.Rcode == D.RcodeServerFailure {
|
||||
// [...] a resolver MAY cache a server failure response.
|
||||
// If it does so it MUST NOT cache it for longer than five (5) minutes [...]
|
||||
ttl = serverFailureCacheTTL
|
||||
} else {
|
||||
ttl = minimalTTL(append(append(msg.Answer, msg.Ns...), msg.Extra...))
|
||||
}
|
||||
if ttl == 0 {
|
||||
return
|
||||
}
|
||||
c.SetWithExpire(key, msg.Copy(), time.Now().Add(time.Duration(ttl)*time.Second))
|
||||
c.SetWithExpire(key, msg.Copy(), time.Now().Add(time.Second*time.Duration(ttl)))
|
||||
}
|
||||
|
||||
func setMsgTTL(msg *D.Msg, ttl uint32) {
|
||||
@ -107,7 +104,16 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient {
|
||||
ret = append(ret, newDHCPClient(s.Addr))
|
||||
continue
|
||||
case "system":
|
||||
ret = append(ret, newSystemClient())
|
||||
clients, err := loadSystemResolver()
|
||||
if err != nil {
|
||||
log.Errorln("[DNS:system] load system resolver failed: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
if len(clients) == 0 {
|
||||
log.Errorln("[DNS:system] no nameserver found in system")
|
||||
continue
|
||||
}
|
||||
ret = append(ret, clients...)
|
||||
continue
|
||||
case "rcode":
|
||||
ret = append(ret, newRCodeClient(s.Addr))
|
||||
@ -280,7 +286,7 @@ func listenPacket(ctx context.Context, proxyAdapter C.ProxyAdapter, proxyName st
|
||||
DstPort: uint16(uintPort),
|
||||
}
|
||||
if proxyAdapter == nil {
|
||||
return dialer.NewDialer(opts...).ListenPacket(ctx, network, "", netip.AddrPortFrom(metadata.DstIP, metadata.DstPort))
|
||||
return dialer.ListenPacket(ctx, dialer.ParseNetwork(network, dstIP), "", opts...)
|
||||
}
|
||||
|
||||
if !proxyAdapter.SupportUDP() {
|
||||
@ -290,17 +296,14 @@ func listenPacket(ctx context.Context, proxyAdapter C.ProxyAdapter, proxyName st
|
||||
return proxyAdapter.ListenPacketContext(ctx, metadata, opts...)
|
||||
}
|
||||
|
||||
var errIPNotFound = errors.New("couldn't find ip")
|
||||
|
||||
func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.Msg, cache bool, err error) {
|
||||
cache = true
|
||||
fast, ctx := picker.WithTimeout[*D.Msg](ctx, resolver.DefaultDNSTimeout)
|
||||
defer fast.Close()
|
||||
domain := msgToDomain(m)
|
||||
var noIpMsg *D.Msg
|
||||
for _, client := range clients {
|
||||
if _, isRCodeClient := client.(rcodeClient); isRCodeClient {
|
||||
msg, err = client.ExchangeContext(ctx, m)
|
||||
msg, err = client.Exchange(m)
|
||||
return msg, false, err
|
||||
}
|
||||
client := client // shadow define client to ensure the value captured by the closure will not be changed in the next loop
|
||||
@ -314,31 +317,13 @@ func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.M
|
||||
// so we would ignore RCode errors from RCode clients.
|
||||
return nil, errors.New("server failure: " + D.RcodeToString[m.Rcode])
|
||||
}
|
||||
if ips := msgToIP(m); len(m.Question) > 0 {
|
||||
qType := m.Question[0].Qtype
|
||||
log.Debugln("[DNS] %s --> %s %s from %s", domain, ips, D.Type(qType), client.Address())
|
||||
switch qType {
|
||||
case D.TypeAAAA:
|
||||
if len(ips) == 0 {
|
||||
noIpMsg = m
|
||||
return nil, errIPNotFound
|
||||
}
|
||||
case D.TypeA:
|
||||
if len(ips) == 0 {
|
||||
noIpMsg = m
|
||||
return nil, errIPNotFound
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Debugln("[DNS] %s --> %s, from %s", domain, msgToIP(m), client.Address())
|
||||
return m, nil
|
||||
})
|
||||
}
|
||||
|
||||
msg = fast.Wait()
|
||||
if msg == nil {
|
||||
if noIpMsg != nil {
|
||||
return noIpMsg, false, nil
|
||||
}
|
||||
err = errors.New("all DNS requests failed")
|
||||
if fErr := fast.Error(); fErr != nil {
|
||||
err = fmt.Errorf("%w, first error: %w", err, fErr)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user