Compare commits

..

91 Commits

Author SHA1 Message Date
53f9e1ee71 Revert "chore: update quic-go to 0.36.0"
This reverts commit 2284acce94.
2023-06-29 14:25:25 +08:00
fa94403629 chore: better resolv.conf parsing 2023-06-29 14:25:25 +08:00
1beb2919e7 fix: panic when add 4in6 ipcidr 2023-06-29 14:25:25 +08:00
75c5d0482e chore: better close single connection in restful api 2023-06-29 14:25:25 +08:00
55bcabdf46 chore: statistic's Snapshot only contains TrackerInfo 2023-06-29 14:25:25 +08:00
abf80601e1 chore: avoid unneeded map copy when close connection in restful api 2023-06-29 14:25:25 +08:00
26f97b45d6 chore: update quic-go to 0.36.0 2023-06-29 14:25:25 +08:00
29315ce8e5 fix: tuic server cwnd parsing 2023-06-29 14:25:25 +08:00
65f84d21ea chore: tuic server can handle V4 and V5 in same port 2023-06-29 14:25:25 +08:00
e3ac58bc51 chore: fix TUIC cwnd parsing 2023-06-29 14:25:25 +08:00
c66438d794 Revert "chore: Refine adapter type name"
This reverts commit 61734e5cac.
2023-06-29 14:25:25 +08:00
411e587460 chore: function rename 2023-06-29 14:25:25 +08:00
6cc9c68458 chore: Update dependencies 2023-06-29 14:25:25 +08:00
d1c858d7ff update docs 2023-06-29 14:25:25 +08:00
3eef1ee064 chore: adjustable cwnd for cc in quic 2023-06-29 14:25:25 +08:00
514d374b8c chore: Refine adapter type name 2023-06-29 14:25:25 +08:00
a2334430c1 feat: optional provider path (#624) 2023-06-29 14:25:25 +08:00
c8a3d6edd9 Add REALITY ChaCha20-Poly1305 auth mode support 2023-06-29 14:25:25 +08:00
bda2ca3c13 fix: singmux return wrong supportUDP value 2023-06-29 14:25:25 +08:00
f4b734c74c fix: tuicV5's heartbeat should be a datagram packet 2023-06-29 14:25:25 +08:00
c2cdf43239 fix: dns concurrent not work 2023-06-29 14:25:25 +08:00
b939c81d3e feat: support tuicV5 2023-06-29 14:25:25 +08:00
0e92496eeb chore: Update dependencies 2023-06-29 14:25:25 +08:00
ea482598e0 chore: Disable cache for RCode client 2023-06-29 14:25:25 +08:00
16f3567ddc feat: Add RCode DNS client 2023-06-29 14:25:25 +08:00
73f8da091e chore: allow unsafe path for provider by environment variable 2023-06-29 14:25:25 +08:00
6bdaadc581 chore: Replace murmur3 with maphash 2023-06-29 14:25:24 +08:00
73a2cf593e chore: reduce process lookup attempts when process not exist #613 2023-06-29 14:25:24 +08:00
665bfcab2d fix: Disable XUDP global ID if source address invalid 2023-06-29 14:25:24 +08:00
8be860472a chore: init gopacket only when dial fake-tcp to decrease memory using 2023-06-29 14:25:24 +08:00
1ec74f13f7 feat: Add XUDP migration support 2023-06-29 14:25:24 +08:00
564b834e00 fix: Resolve delay omission in the presence of nested proxy-groups 2023-06-29 14:25:24 +08:00
da04e00767 When testing the delay through REST API, determine whether to store the delay data based on certain conditions instead of discarding it directly (#609) 2023-06-29 14:25:24 +08:00
e0faffbfbd fix: go1.19 compile 2023-06-29 14:25:24 +08:00
a0c7641ad5 chore: Something update from clash :) (#606) 2023-06-29 14:25:24 +08:00
1f592c43de fix: nil pointer in urltest (#603) 2023-06-29 14:25:24 +08:00
4d7350923c fix hysteria faketcp lookback in TUN mode (#601) 2023-06-29 14:25:24 +08:00
76a7945994 chore: Ignore PR in Docker build 2023-06-29 14:25:24 +08:00
a2bbd1cc8d ProxyProvider health check also supports specifying expected status (#600)
Co-authored-by: wwqgtxx <wwqgtxx@gmail.com>
2023-06-29 14:25:24 +08:00
4ec66d299a [Feature] Proxy stores delay data of different URLs. And supports specifying different test URLs and expected statue by group (#588)
Co-authored-by: Larvan2 <78135608+Larvan2@users.noreply.github.com>
Co-authored-by: wwqgtxx <wwqgtxx@gmail.com>
2023-06-29 14:25:24 +08:00
4e46cbfbde fix: hysteria faketcp loopback in tun mode 2023-06-29 14:25:24 +08:00
1a44dcee55 chore: update proxy's udpConn when received a new packet 2023-06-29 14:25:24 +08:00
6c7d1657a5 chore: update quic-go to 0.35.1 2023-06-29 14:25:24 +08:00
38e210a851 chore: Update dependencies 2023-06-29 14:25:24 +08:00
359ee70daa chore: update quic-go to 0.34.0 2023-06-29 14:25:24 +08:00
8d1251f128 chore: generate release note automatically 2023-06-09 12:59:59 +00:00
fb6a032872 chore: genReleaseNote support verrsion range
This commit updates the release script to handle the input version range provided via command line options. Now, you can specify the version range using the '-v' option, such as '-v v1.14.1...v1.14.2', and the script will generate the release notes based on the specified range.

The script parses the command line options, extracts the version range, and uses it in the 'git log' commands to capture the relevant commits. The output is then organized into different sections, including 'What's Changed', 'BUG & Fix', and 'Maintenance', along with the full changelog link.

This enhancement improves the flexibility and usability of the release script, allowing users to generate release notes for specific version ranges easily.

Co-authored-by: ChatGPT
2023-06-03 10:25:00 +00:00
47ad8e08be chore: Random only if the certificate and private-key are empty 2023-06-03 10:02:31 +00:00
e1af4ddda3 chore: Reject packet conn implement wait read 2023-06-03 10:01:50 +00:00
58e05c42c9 fix: handle manually select in url-test 2023-06-03 09:55:29 +00:00
880cc90e10 Merge branch 'Alpha' into Meta 2023-06-03 09:54:25 +00:00
a4334e1d52 fix: wildcard matching problem (#536) 2023-04-29 01:10:55 +08:00
3ba94842cc chore: update genReleaseNote.sh 2023-04-28 07:29:58 +00:00
a266589faf Merge branch 'Alpha' into Meta 2023-04-28 07:05:21 +00:00
d9319ec09a chore: update bug_report 2023-04-27 12:19:59 +00:00
070f8f8949 Merge branch 'Alpha' into Meta 2023-04-26 14:14:46 +00:00
bf3c6a044c chore: update templates 2023-04-14 05:26:44 +00:00
d6d2d90502 add issue templates 2023-04-04 06:45:21 +00:00
a1d0f4c6ee chore: rename delete.yml 2023-03-31 06:34:23 +00:00
d569d8186d chore: add genReleaseNote.sh 2023-03-30 16:43:05 +00:00
9b7aab1fc7 chore: rename delete.yml 2023-03-30 16:04:09 +00:00
3c717097cb Merge branch 'Alpha' into Meta 2023-03-30 16:03:47 +00:00
8293b7fdae Merge branch 'Beta' into Meta
# Conflicts:
#	.github/workflows/docker.yaml
#	.github/workflows/prerelease.yml
2023-02-19 01:25:34 +08:00
0ba415866e Merge branch 'Alpha' into Beta 2023-02-19 01:25:01 +08:00
53b41ca166 Chore: Add action for deleting old workflow 2023-01-30 18:17:22 +08:00
8a75f78e63 chore: adjust Dockerfile 2023-01-12 02:24:12 +08:00
d9692c6366 Merge branch 'Beta' into Meta 2023-01-12 02:15:14 +08:00
f4b0062dfc Merge branch 'Alpha' into Beta 2023-01-12 02:14:49 +08:00
b9ffc82e53 Merge branch 'Beta' into Meta 2023-01-12 01:33:56 +08:00
78aaea6a45 Merge branch 'Alpha' into Beta 2023-01-12 01:33:16 +08:00
3645fbf161 Merge pull request #327 from Rasphino/Meta
Update flake.nix hash
2023-01-08 00:29:29 +08:00
a1d0f22132 fix: update flake.nix hash 2023-01-07 23:38:32 +08:00
fa73b0f4bf Merge remote-tracking branch 'origin/Beta' into Meta 2023-01-01 19:41:36 +08:00
3b76a8b839 Merge remote-tracking branch 'origin/Alpha' into Beta 2023-01-01 19:40:36 +08:00
667f42dcdc Merge pull request #282 from tdjnodj/Meta
Update README.md
2022-12-03 17:23:51 +08:00
dfbe09860f Update README.md 2022-12-03 17:17:08 +08:00
9e20f9c26a chore: update dependencies 2022-11-28 20:33:10 +08:00
f968d0cb82 chore: update github action 2022-11-26 20:16:12 +08:00
2ad84f4379 Merge branch 'Beta' into Meta 2022-11-02 18:08:22 +08:00
c7aa16426f Merge branch 'Alpha' into Beta 2022-11-02 18:07:29 +08:00
5987f8e3b5 Merge branch 'Beta' into Meta 2022-08-29 13:08:29 +08:00
3a8eb72de2 Merge branch 'Alpha' into Beta 2022-08-29 13:08:22 +08:00
33abbdfd24 Merge pull request #174 from MetaCubeX/Alpha
Alpha
2022-08-29 11:24:07 +08:00
0703d6cbff Merge pull request #173 from MetaCubeX/Alpha
Alpha
2022-08-29 11:22:14 +08:00
10d2d14938 Merge branch 'Beta' into Meta
# Conflicts:
#	rules/provider/classical_strategy.go
2022-07-02 10:41:41 +08:00
691cf1d8d6 Merge pull request #94 from bash99/Meta
Update README.md
2022-06-15 19:15:51 +08:00
d1decb8e58 Update README.md
add permissions for systemctl services
clash-dashboard change to updated one
2022-06-15 14:00:05 +08:00
7d04904109 fix: leak dns when domain in hosts list 2022-06-11 18:51:26 +08:00
a5acd3aa97 refactor: clear linkname,reduce cycle dependencies,transport init geosite function 2022-06-11 18:51:22 +08:00
eea9a12560 fix: 规则匹配默认策略组返回错误 2022-06-09 14:18:35 +08:00
0a4570b55c fix: group filter touch provider 2022-06-09 14:18:29 +08:00
244 changed files with 3864 additions and 7580 deletions

View File

@ -1,7 +1,6 @@
name: Bug report name: Bug report
description: Create a report to help us improve description: Create a report to help us improve
title: "[Bug] " title: "[Bug] "
labels: ["bug"]
body: body:
- type: checkboxes - type: checkboxes
id: ensure id: ensure
@ -80,4 +79,4 @@ Paste the Clash core log below with the log level set to `DEBUG`.
attributes: attributes:
label: Description label: Description
validations: validations:
required: true required: true

View File

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

View File

@ -1,7 +1,6 @@
name: Feature request name: Feature request
description: Suggest an idea for this project description: Suggest an idea for this project
title: "[Feature] " title: "[Feature] "
labels: ["enhancement"]
body: body:
- type: checkboxes - type: checkboxes
id: ensure id: ensure

32
.github/genReleaseNote.sh vendored Executable file
View 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

View File

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

View File

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

View File

@ -69,12 +69,6 @@ jobs:
target: "darwin-amd64 darwin-arm64 android-arm64", target: "darwin-amd64 darwin-arm64 android-arm64",
id: "9", 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: "windows/*", id: "1" }
- { type: "WithCGO", target: "linux/386", id: "2" } - { type: "WithCGO", target: "linux/386", id: "2" }
- { type: "WithCGO", target: "linux/amd64", id: "3" } - { type: "WithCGO", target: "linux/amd64", id: "3" }
@ -100,11 +94,6 @@ jobs:
run: echo "VERSION=alpha-$(git rev-parse --short HEAD)" >> $GITHUB_ENV run: echo "VERSION=alpha-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
shell: bash 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 - name: Set variables
if: ${{github.ref_name=='Meta'}} if: ${{github.ref_name=='Meta'}}
run: echo "VERSION=meta-$(git rev-parse --short HEAD)" >> $GITHUB_ENV run: echo "VERSION=meta-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
@ -132,26 +121,18 @@ jobs:
shell: bash shell: bash
- name: Setup Go - 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 uses: actions/setup-go@v4
with: with:
go-version: "1.20" go-version: "1.20"
check-latest: true check-latest: true
- name: Test - name: Test
if: ${{ matrix.job.id=='1' && matrix.job.type!='WithCGO' }} if: ${{ matrix.job.id=='1' && matrix.job.type=='WithoutCGO' }}
run: | run: |
go test ./... go test ./...
- name: Build WithoutCGO - name: Build WithoutCGO
if: ${{ matrix.job.type!='WithCGO' }} if: ${{ matrix.job.type=='WithoutCGO' }}
env: env:
NAME: Clash.Meta NAME: Clash.Meta
BINDIR: bin BINDIR: bin
@ -161,7 +142,7 @@ jobs:
if: ${{ matrix.job.type=='WithCGO' && matrix.job.target=='android' }} if: ${{ matrix.job.type=='WithCGO' && matrix.job.target=='android' }}
id: setup-ndk id: setup-ndk
with: with:
ndk-version: r26 ndk-version: r25b
add-to-path: false add-to-path: false
local-cache: true local-cache: true
@ -199,17 +180,6 @@ jobs:
ls -la ls -la
cd .. 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 - name: Zip
if: ${{ success() }} if: ${{ success() }}
run: | run: |
@ -234,7 +204,7 @@ jobs:
Upload-Prerelease: Upload-Prerelease:
permissions: write-all permissions: write-all
if: ${{ github.ref_type=='branch' && github.event_name != 'pull_request' }} if: ${{ github.ref_type=='branch' }}
needs: [Build] needs: [Build]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -292,6 +262,23 @@ jobs:
needs: [Build] needs: [Build]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 - uses: actions/download-artifact@v3
with: with:
name: artifact name: artifact
@ -308,6 +295,7 @@ jobs:
tag_name: ${{ github.ref_name }} tag_name: ${{ github.ref_name }}
files: bin/* files: bin/*
generate_release_notes: true generate_release_notes: true
body_path: release.md
Docker: Docker:
if: ${{ github.event_name != 'pull_request' }} if: ${{ github.event_name != 'pull_request' }}

15
.github/workflows/delete.yml vendored Normal file
View 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

View File

@ -4,9 +4,9 @@ RUN echo "I'm building for $TARGETPLATFORM"
RUN apk add --no-cache gzip && \ RUN apk add --no-cache gzip && \
mkdir /clash-config && \ mkdir /clash-config && \
wget -O /clash-config/geoip.metadb https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb && \ wget -O /clash-config/Country.mmdb https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb && \
wget -O /clash-config/geosite.dat https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat && \ wget -O /clash-config/geosite.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat && \
wget -O /clash-config/geoip.dat https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat wget -O /clash-config/geoip.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
COPY docker/file-name.sh /clash/file-name.sh COPY docker/file-name.sh /clash/file-name.sh
WORKDIR /clash WORKDIR /clash

320
README.md
View File

@ -21,52 +21,261 @@
## Features ## Features
- Local HTTP/HTTPS/SOCKS server with authentication support - 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. - 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 - 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 - Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node based off latency
based off latency - Remote providers, allowing users to get node lists remotely instead of hardcoding in config
- Remote providers, allowing users to get node lists remotely instead of hard-coding in config
- Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`. - Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`.
- Comprehensive HTTP RESTful API controller - 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). Then get the source code of Clash.Meta:
## 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:
```shell ```shell
git clone https://github.com/MetaCubeX/Clash.Meta.git git clone https://github.com/MetaCubeX/Clash.Meta.git
cd Clash.Meta && go mod download 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 ```shell
go env -w GOPROXY=https://goproxy.io,direct 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 ```shell
go build -tags with_gvisor 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 ### IPTABLES configuration
Work on Linux OS which supported `iptables` Work on Linux OS which supported `iptables`
@ -80,10 +289,71 @@ iptables:
inbound-interface: eth0 # detect the inbound interface, default is 'lo' 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 ## Credits

View File

@ -9,7 +9,6 @@ import (
"net/http" "net/http"
"net/netip" "net/netip"
"net/url" "net/url"
"strconv"
"time" "time"
"github.com/Dreamacro/clash/common/atomic" "github.com/Dreamacro/clash/common/atomic"
@ -18,8 +17,6 @@ import (
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
"github.com/puzpuzpuz/xsync/v2"
) )
var UnifiedDelay = atomic.NewBool(false) var UnifiedDelay = atomic.NewBool(false)
@ -30,15 +27,15 @@ const (
type extraProxyState struct { type extraProxyState struct {
history *queue.Queue[C.DelayHistory] history *queue.Queue[C.DelayHistory]
alive atomic.Bool alive *atomic.Bool
} }
type Proxy struct { type Proxy struct {
C.ProxyAdapter C.ProxyAdapter
history *queue.Queue[C.DelayHistory] history *queue.Queue[C.DelayHistory]
alive atomic.Bool alive *atomic.Bool
url string url string
extra *xsync.MapOf[string, *extraProxyState] extra map[string]*extraProxyState
} }
// Alive implements C.Proxy // Alive implements C.Proxy
@ -48,8 +45,10 @@ func (p *Proxy) Alive() bool {
// AliveForTestUrl implements C.Proxy // AliveForTestUrl implements C.Proxy
func (p *Proxy) AliveForTestUrl(url string) bool { func (p *Proxy) AliveForTestUrl(url string) bool {
if state, ok := p.extra.Load(url); ok { if p.extra != nil {
return state.alive.Load() if state, ok := p.extra[url]; ok {
return state.alive.Load()
}
} }
return p.alive.Load() return p.alive.Load()
@ -88,16 +87,16 @@ func (p *Proxy) DelayHistory() []C.DelayHistory {
for _, item := range queueM { for _, item := range queueM {
histories = append(histories, item) histories = append(histories, item)
} }
return histories return histories
} }
// DelayHistoryForTestUrl implements C.Proxy // DelayHistoryForTestUrl implements C.Proxy
func (p *Proxy) DelayHistoryForTestUrl(url string) []C.DelayHistory { func (p *Proxy) DelayHistoryForTestUrl(url string) []C.DelayHistory {
var queueM []C.DelayHistory var queueM []C.DelayHistory
if p.extra != nil {
if state, ok := p.extra.Load(url); ok { if state, ok := p.extra[url]; ok {
queueM = state.history.Copy() queueM = state.history.Copy()
}
} }
if queueM == nil { if queueM == nil {
@ -112,25 +111,19 @@ func (p *Proxy) DelayHistoryForTestUrl(url string) []C.DelayHistory {
} }
func (p *Proxy) ExtraDelayHistory() map[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 { extra[testUrl] = histories
testUrl := k
state := v
histories := []C.DelayHistory{}
queueM := state.history.Copy()
for _, item := range queueM {
histories = append(histories, item)
} }
}
extraHistory[testUrl] = histories return extra
return true
})
return extraHistory
} }
// LastDelay return last history record. if proxy is not alive, return the max value of uint16. // LastDelay return last history record. if proxy is not alive, return the max value of uint16.
@ -155,9 +148,11 @@ func (p *Proxy) LastDelayForTestUrl(url string) (delay uint16) {
alive := p.alive.Load() alive := p.alive.Load()
history := p.history.Last() history := p.history.Last()
if state, ok := p.extra.Load(url); ok { if p.extra != nil {
alive = state.alive.Load() if state, ok := p.extra[url]; ok {
history = state.history.Last() alive = state.alive.Load()
history = state.history.Last()
}
} }
if !alive { if !alive {
@ -181,7 +176,6 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
_ = json.Unmarshal(inner, &mapping) _ = json.Unmarshal(inner, &mapping)
mapping["history"] = p.DelayHistory() mapping["history"] = p.DelayHistory()
mapping["extra"] = p.ExtraDelayHistory() mapping["extra"] = p.ExtraDelayHistory()
mapping["alive"] = p.Alive()
mapping["name"] = p.Name() mapping["name"] = p.Name()
mapping["udp"] = p.SupportUDP() mapping["udp"] = p.SupportUDP()
mapping["xudp"] = p.SupportXUDP() mapping["xudp"] = p.SupportXUDP()
@ -217,18 +211,18 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In
if alive { if alive {
record.Delay = t record.Delay = t
} }
p.history.Put(record)
if p.history.Len() > defaultHistoriesNum { if p.extra == nil {
p.history.Pop() p.extra = map[string]*extraProxyState{}
} }
state, ok := p.extra.Load(url) state, ok := p.extra[url]
if !ok { if !ok {
state = &extraProxyState{ state = &extraProxyState{
history: queue.New[C.DelayHistory](defaultHistoriesNum), history: queue.New[C.DelayHistory](defaultHistoriesNum),
alive: atomic.NewBool(true), alive: atomic.NewBool(true),
} }
p.extra.Store(url, state) p.extra[url] = state
} }
state.alive.Store(alive) state.alive.Store(alive)
@ -311,12 +305,7 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In
} }
func NewProxy(adapter C.ProxyAdapter) *Proxy { func NewProxy(adapter C.ProxyAdapter) *Proxy {
return &Proxy{ return &Proxy{adapter, queue.New[C.DelayHistory](defaultHistoriesNum), atomic.NewBool(true), "", map[string]*extraProxyState{}}
ProxyAdapter: adapter,
history: queue.New[C.DelayHistory](defaultHistoriesNum),
alive: atomic.NewBool(true),
url: "",
extra: xsync.NewMapOf[*extraProxyState]()}
} }
func urlToMetadata(rawURL string) (addr C.Metadata, err error) { func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
@ -337,15 +326,11 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
return return
} }
} }
uintPort, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return
}
addr = C.Metadata{ addr = C.Metadata{
Host: u.Hostname(), Host: u.Hostname(),
DstIP: netip.Addr{}, DstIP: netip.Addr{},
DstPort: uint16(uintPort), DstPort: port,
} }
return return
} }
@ -359,14 +344,14 @@ func (p *Proxy) determineFinalStoreType(store C.DelayHistoryStoreType, url strin
return C.OriginalHistory return C.OriginalHistory
} }
if p.extra.Size() < 2*C.DefaultMaxHealthCheckUrlNum { if p.extra == nil {
return C.ExtraHistory 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 return store
} }

View File

@ -1,17 +1,13 @@
package inbound package inbound
import ( import (
"net"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
type Addition func(metadata *C.Metadata) type Addition func(metadata *C.Metadata)
func ApplyAdditions(metadata *C.Metadata, additions ...Addition) { func (a Addition) Apply(metadata *C.Metadata) {
for _, addition := range additions { a(metadata)
addition(metadata)
}
} }
func WithInName(name string) Addition { func WithInName(name string) Addition {
@ -37,29 +33,3 @@ func WithSpecialProxy(specialProxy string) Addition {
metadata.SpecialProxy = specialProxy 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
}
}
}

View File

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

View File

@ -4,15 +4,25 @@ import (
"net" "net"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/socks5"
) )
// NewHTTP receive normal http request and return HTTPContext // 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 := parseSocksAddr(target)
metadata.NetWork = C.TCP metadata.NetWork = C.TCP
metadata.Type = C.HTTP metadata.Type = C.HTTP
ApplyAdditions(metadata, WithSrcAddr(srcConn.RemoteAddr()), WithInAddr(conn.LocalAddr())) for _, addition := range additions {
ApplyAdditions(metadata, additions...) addition.Apply(metadata)
return conn, 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)
} }

View File

@ -5,13 +5,23 @@ import (
"net/http" "net/http"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
) )
// NewHTTPS receive CONNECT request and return ConnContext // 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 := parseHTTPAddr(request)
metadata.Type = C.HTTPS metadata.Type = C.HTTPS
ApplyAdditions(metadata, WithSrcAddr(conn.RemoteAddr()), WithInAddr(conn.LocalAddr())) for _, addition := range additions {
ApplyAdditions(metadata, additions...) addition.Apply(metadata)
return conn, 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)
} }

View File

@ -17,10 +17,6 @@ func SetTfo(open bool) {
lc.DisableTFO = !open lc.DisableTFO = !open
} }
func SetMPTCP(open bool) {
setMultiPathTCP(&lc.ListenConfig, open)
}
func ListenContext(ctx context.Context, network, address string) (net.Listener, error) { func ListenContext(ctx context.Context, network, address string) (net.Listener, error) {
return lc.Listen(ctx, network, address) return lc.Listen(ctx, network, address)
} }

View File

@ -1,10 +0,0 @@
//go:build !go1.21
package inbound
import "net"
const multipathTCPAvailable = false
func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) {
}

View File

@ -1,11 +0,0 @@
//go:build go1.21
package inbound
import "net"
const multipathTCPAvailable = true
func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) {
listenConfig.SetMultipathTCP(open)
}

View File

@ -5,16 +5,38 @@ import (
"github.com/Dreamacro/clash/transport/socks5" "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 // 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 := parseSocksAddr(target)
metadata.NetWork = C.UDP metadata.NetWork = C.UDP
metadata.Type = source metadata.Type = source
ApplyAdditions(metadata, WithSrcAddr(packet.LocalAddr())) for _, addition := range additions {
if p, ok := packet.(C.UDPPacketInAddr); ok { addition.Apply(metadata)
ApplyAdditions(metadata, WithInAddr(p.InAddr())) }
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,
}
} }

View File

@ -2,17 +2,48 @@ package inbound
import ( import (
"net" "net"
"net/netip"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/socks5"
) )
// NewSocket receive TCP inbound and return ConnContext // 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 := parseSocksAddr(target)
metadata.NetWork = C.TCP metadata.NetWork = C.TCP
metadata.Type = source metadata.Type = source
ApplyAdditions(metadata, WithSrcAddr(conn.RemoteAddr()), WithInAddr(conn.LocalAddr())) for _, addition := range additions {
ApplyAdditions(metadata, additions...) addition.Apply(metadata)
return conn, 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 {
metadata.DstPort = port
if ip, err := netip.ParseAddr(h); err == nil {
metadata.DstIP = ip
} else {
metadata.Host = h
}
}
return context.NewConnContext(conn, metadata)
} }

View File

@ -1,6 +1,7 @@
package inbound package inbound
import ( import (
"errors"
"net" "net"
"net/http" "net/http"
"net/netip" "net/netip"
@ -19,14 +20,14 @@ func parseSocksAddr(target socks5.Addr) *C.Metadata {
case socks5.AtypDomainName: case socks5.AtypDomainName:
// trim for FQDN // trim for FQDN
metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".") metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".")
metadata.DstPort = uint16((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1])) metadata.DstPort = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
case socks5.AtypIPv4: case socks5.AtypIPv4:
metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv4len])) metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv4len]))
metadata.DstPort = uint16((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1])) metadata.DstPort = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
case socks5.AtypIPv6: case socks5.AtypIPv6:
ip6, _ := netip.AddrFromSlice(target[1 : 1+net.IPv6len]) ip6, _ := netip.AddrFromSlice(target[1 : 1+net.IPv6len])
metadata.DstIP = ip6.Unmap() metadata.DstIP = ip6.Unmap()
metadata.DstPort = uint16((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1])) metadata.DstPort = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
} }
return metadata return metadata
@ -42,16 +43,11 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
// trim FQDN (#737) // trim FQDN (#737)
host = strings.TrimRight(host, ".") host = strings.TrimRight(host, ".")
var uint16Port uint16
if port, err := strconv.ParseUint(port, 10, 16); err == nil {
uint16Port = uint16(port)
}
metadata := &C.Metadata{ metadata := &C.Metadata{
NetWork: C.TCP, NetWork: C.TCP,
Host: host, Host: host,
DstIP: netip.Addr{}, DstIP: netip.Addr{},
DstPort: uint16Port, DstPort: port,
} }
ip, err := netip.ParseAddr(host) ip, err := netip.ParseAddr(host)
@ -61,3 +57,24 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
return metadata return metadata
} }
func parseAddr(addr net.Addr) (netip.Addr, string, error) {
// Filter when net.Addr interface is nil
if addr == nil {
return netip.Addr{}, "", 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{}, "", err
}
ip, err := netip.ParseAddr(host)
return ip, port, err
}

View File

@ -21,7 +21,6 @@ type Base struct {
udp bool udp bool
xudp bool xudp bool
tfo bool tfo bool
mpTcp bool
rmark int rmark int
id string id string
prefer C.DNSPrefer prefer C.DNSPrefer
@ -144,16 +143,11 @@ func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option {
opts = append(opts, dialer.WithTFO(true)) opts = append(opts, dialer.WithTFO(true))
} }
if b.mpTcp {
opts = append(opts, dialer.WithMPTCP(true))
}
return opts return opts
} }
type BasicOption struct { type BasicOption struct {
TFO bool `proxy:"tfo,omitempty" group:"tfo,omitempty"` TFO bool `proxy:"tfo,omitempty" group:"tfo,omitempty"`
MPTCP bool `proxy:"mptcp,omitempty" group:"mptcp,omitempty"`
Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"` Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"`
RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"` RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"`
IPVersion string `proxy:"ip-version,omitempty" group:"ip-version,omitempty"` IPVersion string `proxy:"ip-version,omitempty" group:"ip-version,omitempty"`
@ -167,7 +161,6 @@ type BaseOption struct {
UDP bool UDP bool
XUDP bool XUDP bool
TFO bool TFO bool
MPTCP bool
Interface string Interface string
RoutingMark int RoutingMark int
Prefer C.DNSPrefer Prefer C.DNSPrefer
@ -181,7 +174,6 @@ func NewBase(opt BaseOption) *Base {
udp: opt.UDP, udp: opt.UDP,
xudp: opt.XUDP, xudp: opt.XUDP,
tfo: opt.TFO, tfo: opt.TFO,
mpTcp: opt.MPTCP,
iface: opt.Interface, iface: opt.Interface,
rmark: opt.RoutingMark, rmark: opt.RoutingMark,
prefer: opt.Prefer, prefer: opt.Prefer,

View File

@ -3,9 +3,6 @@ package outbound
import ( import (
"context" "context"
"errors" "errors"
"net/netip"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
@ -15,11 +12,6 @@ type Direct struct {
*Base *Base
} }
type DirectOption struct {
BasicOption
Name string `proxy:"name"`
}
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
opts = append(opts, dialer.WithResolver(resolver.DefaultResolver)) 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 { if err != nil {
return nil, err return nil, err
} }
N.TCPKeepAlive(c) tcpKeepAlive(c)
return NewConn(c, d), nil return NewConn(c, d), nil
} }
@ -41,28 +33,13 @@ func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
} }
metadata.DstIP = ip metadata.DstIP = ip
} }
pc, err := dialer.NewDialer(d.Base.DialOptions(opts...)...).ListenPacket(ctx, "udp", "", netip.AddrPortFrom(metadata.DstIP, metadata.DstPort)) pc, err := dialer.ListenPacket(ctx, dialer.ParseNetwork("udp", metadata.DstIP), "", d.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return newPacketConn(pc, d), nil 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 { func NewDirect() *Direct {
return &Direct{ return &Direct{
Base: &Base{ Base: &Base{

View File

@ -7,16 +7,14 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"net" "net"
"net/http" "net/http"
"strconv" "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/dialer"
"github.com/Dreamacro/clash/component/proxydialer" "github.com/Dreamacro/clash/component/proxydialer"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
@ -76,7 +74,7 @@ func (h *Http) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metad
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", h.addr, err) return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
} }
N.TCPKeepAlive(c) tcpKeepAlive(c)
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
@ -157,13 +155,19 @@ func NewHttp(option HttpOption) (*Http, error) {
if option.SNI != "" { if option.SNI != "" {
sni = option.SNI sni = option.SNI
} }
var err error if len(option.Fingerprint) == 0 {
tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{ tlsConfig = tlsC.GetGlobalTLSConfig(&tls.Config{
InsecureSkipVerify: option.SkipCertVerify, InsecureSkipVerify: option.SkipCertVerify,
ServerName: sni, ServerName: sni,
}, option.Fingerprint) })
if err != nil { } else {
return nil, err var err error
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(&tls.Config{
InsecureSkipVerify: option.SkipCertVerify,
ServerName: sni,
}, option.Fingerprint); err != nil {
return nil, err
}
} }
} }
@ -173,7 +177,6 @@ func NewHttp(option HttpOption) (*Http, error) {
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Http, tp: C.Http,
tfo: option.TFO, tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),

View File

@ -2,11 +2,16 @@ package outbound
import ( import (
"context" "context"
"crypto/sha256"
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"encoding/hex"
"encoding/pem"
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
"os"
"regexp"
"strconv" "strconv"
"time" "time"
@ -14,9 +19,9 @@ import (
"github.com/metacubex/quic-go/congestion" "github.com/metacubex/quic-go/congestion"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
"github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer" "github.com/Dreamacro/clash/component/proxydialer"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
hyCongestion "github.com/Dreamacro/clash/transport/hysteria/congestion" hyCongestion "github.com/Dreamacro/clash/transport/hysteria/congestion"
@ -38,6 +43,8 @@ const (
DefaultHopInterval = 10 DefaultHopInterval = 10
) )
var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`)
type Hysteria struct { type Hysteria struct {
*Base *Base
@ -46,7 +53,7 @@ type Hysteria struct {
} }
func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, 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 { if err != nil {
return nil, err return nil, err
} }
@ -113,12 +120,12 @@ type HysteriaOption struct {
func (c *HysteriaOption) Speed() (uint64, uint64, error) { func (c *HysteriaOption) Speed() (uint64, uint64, error) {
var up, down uint64 var up, down uint64
up = StringToBps(c.Up) up = stringToBps(c.Up)
if up == 0 { if up == 0 {
return 0, 0, fmt.Errorf("invaild upload speed: %s", c.Up) return 0, 0, fmt.Errorf("invaild upload speed: %s", c.Up)
} }
down = StringToBps(c.Down) down = stringToBps(c.Down)
if down == 0 { if down == 0 {
return 0, 0, fmt.Errorf("invaild download speed: %s", c.Down) return 0, 0, fmt.Errorf("invaild download speed: %s", c.Down)
} }
@ -146,10 +153,37 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
MinVersion: tls.VersionTLS13, MinVersion: tls.VersionTLS13,
} }
var bs []byte
var err error var err error
tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString) if len(option.CustomCA) > 0 {
if err != nil { bs, err = os.ReadFile(option.CustomCA)
return nil, err 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 { if len(option.ALPN) > 0 {
@ -234,6 +268,42 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
}, nil }, 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 { type hyPacketConn struct {
core.UDPConn core.UDPConn
} }

View File

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

View File

@ -15,10 +15,6 @@ type Reject struct {
*Base *Base
} }
type RejectOption struct {
Name string `proxy:"name"`
}
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
return NewConn(nopConn{}, r), nil return NewConn(nopConn{}, r), nil
@ -29,16 +25,6 @@ func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
return newPacketConn(nopPacketConn{}, r), nil 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 { func NewReject() *Reject {
return &Reject{ return &Reject{
Base: &Base{ Base: &Base{

View File

@ -19,7 +19,7 @@ import (
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin" v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
restlsC "github.com/3andne/restls-client-go" 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" M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/uot" "github.com/sagernet/sing/common/uot"
) )
@ -58,15 +58,14 @@ type simpleObfsOption struct {
} }
type v2rayObfsOption struct { type v2rayObfsOption struct {
Mode string `obfs:"mode"` Mode string `obfs:"mode"`
Host string `obfs:"host,omitempty"` Host string `obfs:"host,omitempty"`
Path string `obfs:"path,omitempty"` Path string `obfs:"path,omitempty"`
TLS bool `obfs:"tls,omitempty"` TLS bool `obfs:"tls,omitempty"`
Fingerprint string `obfs:"fingerprint,omitempty"` Fingerprint string `obfs:"fingerprint,omitempty"`
Headers map[string]string `obfs:"headers,omitempty"` Headers map[string]string `obfs:"headers,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
Mux bool `obfs:"mux,omitempty"` Mux bool `obfs:"mux,omitempty"`
V2rayHttpUpgrade bool `obfs:"v2ray-http-upgrade,omitempty"`
} }
type shadowTLSOption struct { type shadowTLSOption struct {
@ -124,9 +123,9 @@ func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metada
} }
} }
if useEarly { 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 { } 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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
N.TCPKeepAlive(c) tcpKeepAlive(c)
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
@ -260,11 +259,10 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
} }
obfsMode = opts.Mode obfsMode = opts.Mode
v2rayOption = &v2rayObfs.Option{ v2rayOption = &v2rayObfs.Option{
Host: opts.Host, Host: opts.Host,
Path: opts.Path, Path: opts.Path,
Headers: opts.Headers, Headers: opts.Headers,
Mux: opts.Mux, Mux: opts.Mux,
V2rayHttpUpgrade: opts.V2rayHttpUpgrade,
} }
if opts.TLS { 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, err = restlsC.NewRestlsConfig(restlsOpt.Host, restlsOpt.Password, restlsOpt.VersionHint, restlsOpt.RestlsScript, option.ClientFingerprint)
restlsConfig.SessionTicketsDisabled = true
if err != nil { if err != nil {
return nil, fmt.Errorf("ss %s initialize restls-plugin error: %w", addr, err) return nil, fmt.Errorf("ss %s initialize restls-plugin error: %w", addr, err)
} }
@ -316,7 +315,6 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
tp: C.Shadowsocks, tp: C.Shadowsocks,
udp: option.UDP, udp: option.UDP,
tfo: option.TFO, tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),

View File

@ -80,7 +80,7 @@ func (ssr *ShadowSocksR) DialContextWithDialer(ctx context.Context, dialer C.Dia
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err) return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err)
} }
N.TCPKeepAlive(c) tcpKeepAlive(c)
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
@ -181,7 +181,6 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
tp: C.ShadowsocksR, tp: C.ShadowsocksR,
udp: option.UDP, udp: option.UDP,
tfo: option.TFO, tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),

View File

@ -3,6 +3,7 @@ package outbound
import ( import (
"context" "context"
"errors" "errors"
"net"
"runtime" "runtime"
CN "github.com/Dreamacro/clash/common/net" CN "github.com/Dreamacro/clash/common/net"
@ -14,13 +15,14 @@ import (
mux "github.com/sagernet/sing-mux" mux "github.com/sagernet/sing-mux"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
) )
type SingMux struct { type SingMux struct {
C.ProxyAdapter C.ProxyAdapter
base ProxyBase base ProxyBase
client *mux.Client client *mux.Client
dialer proxydialer.SingDialer dialer *muxSingDialer
onlyTcp bool onlyTcp bool
} }
@ -39,10 +41,28 @@ type ProxyBase interface {
DialOptions(opts ...dialer.Option) []dialer.Option 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) { func (s *SingMux) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
options := s.base.DialOptions(opts...) options := s.base.DialOptions(opts...)
s.dialer.SetDialer(dialer.NewDialer(options...)) s.dialer.dialer = dialer.NewDialer(options...)
c, err := s.client.DialContext(ctx, "tcp", M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) c, err := s.client.DialContext(ctx, "tcp", M.ParseSocksaddr(metadata.RemoteAddress()))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -54,7 +74,7 @@ func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
return s.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...) return s.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
} }
options := s.base.DialOptions(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 // sing-mux use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() { if !metadata.Resolved() {
@ -94,7 +114,7 @@ func closeSingMux(s *SingMux) {
} }
func NewSingMux(option SingMuxOption, proxy C.ProxyAdapter, base ProxyBase) (C.ProxyAdapter, error) { 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{ client, err := mux.NewClient(mux.Options{
Dialer: singDialer, Dialer: singDialer,
Protocol: option.Protocol, Protocol: option.Protocol,

View File

@ -6,7 +6,6 @@ import (
"net" "net"
"strconv" "strconv"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer" "github.com/Dreamacro/clash/component/proxydialer"
@ -60,7 +59,8 @@ func (s *Snell) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
err := snell.WriteUDPHeader(c, s.version) err := snell.WriteUDPHeader(c, s.version)
return c, err return c, err
} }
err := snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version) port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
return c, err return c, err
} }
@ -72,7 +72,8 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
return nil, err return nil, err
} }
if err = snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version); err != nil { port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil {
c.Close() c.Close()
return nil, err return nil, err
} }
@ -94,7 +95,7 @@ func (s *Snell) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", s.addr, err) return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
} }
N.TCPKeepAlive(c) tcpKeepAlive(c)
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
@ -122,7 +123,7 @@ func (s *Snell) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
if err != nil { if err != nil {
return nil, err return nil, err
} }
N.TCPKeepAlive(c) tcpKeepAlive(c)
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption}) c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
err = snell.WriteUDPHeader(c, s.version) err = snell.WriteUDPHeader(c, s.version)
@ -182,7 +183,6 @@ func NewSnell(option SnellOption) (*Snell, error) {
tp: C.Snell, tp: C.Snell,
udp: option.UDP, udp: option.UDP,
tfo: option.TFO, tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),
@ -208,7 +208,7 @@ func NewSnell(option SnellOption) (*Snell, error) {
return nil, err return nil, err
} }
N.TCPKeepAlive(c) tcpKeepAlive(c)
return streamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil return streamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil
}) })
} }

View File

@ -7,13 +7,11 @@ import (
"fmt" "fmt"
"io" "io"
"net" "net"
"net/netip"
"strconv" "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/dialer"
"github.com/Dreamacro/clash/component/proxydialer" "github.com/Dreamacro/clash/component/proxydialer"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/socks5"
) )
@ -82,7 +80,7 @@ func (ss *Socks5) DialContextWithDialer(ctx context.Context, dialer C.Dialer, me
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
N.TCPKeepAlive(c) tcpKeepAlive(c)
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
@ -128,7 +126,7 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
N.TCPKeepAlive(c) tcpKeepAlive(c)
var user *socks5.User var user *socks5.User
if ss.user != "" { if ss.user != "" {
user = &socks5.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, serializesSocksAddr(metadata), socks5.CmdUDPAssociate, user)
bindAddr, err := socks5.ClientHandshake(c, udpAssocateAddr, socks5.CmdUDPAssociate, user)
if err != nil { if err != nil {
err = fmt.Errorf("client hanshake error: %w", err) err = fmt.Errorf("client hanshake error: %w", err)
return return
@ -158,7 +155,7 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
bindUDPAddr.IP = serverAddr.IP 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 { if err != nil {
return return
} }
@ -182,10 +179,13 @@ func NewSocks5(option Socks5Option) (*Socks5, error) {
ServerName: option.Server, ServerName: option.Server,
} }
var err error if len(option.Fingerprint) == 0 {
tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
if err != nil { } else {
return nil, err var err error
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint); err != nil {
return nil, err
}
} }
} }
@ -196,7 +196,6 @@ func NewSocks5(option Socks5Option) (*Socks5, error) {
tp: C.Socks5, tp: C.Socks5,
udp: option.UDP, udp: option.UDP,
tfo: option.TFO, tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),

View File

@ -8,14 +8,13 @@ import (
"net/http" "net/http"
"strconv" "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/dialer"
"github.com/Dreamacro/clash/component/proxydialer" "github.com/Dreamacro/clash/component/proxydialer"
tlsC "github.com/Dreamacro/clash/component/tls" tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun" "github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/trojan" "github.com/Dreamacro/clash/transport/trojan"
"github.com/Dreamacro/clash/transport/vless"
) )
type Trojan struct { type Trojan struct {
@ -46,6 +45,8 @@ type TrojanOption struct {
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"`
Flow string `proxy:"flow,omitempty"`
FlowShow bool `proxy:"flow-show,omitempty"`
ClientFingerprint string `proxy:"client-fingerprint,omitempty"` ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
} }
@ -53,10 +54,9 @@ func (t *Trojan) plainStream(ctx context.Context, c net.Conn) (net.Conn, error)
if t.option.Network == "ws" { if t.option.Network == "ws" {
host, port, _ := net.SplitHostPort(t.addr) host, port, _ := net.SplitHostPort(t.addr)
wsOpts := &trojan.WebsocketOption{ wsOpts := &trojan.WebsocketOption{
Host: host, Host: host,
Port: port, Port: port,
Path: t.option.WSOpts.Path, Path: t.option.WSOpts.Path,
V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade,
} }
if t.option.SNI != "" { if t.option.SNI != "" {
@ -95,6 +95,11 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
c, err = t.instance.PresetXTLSConn(c)
if err != nil {
return nil, err
}
if metadata.NetWork == C.UDP { if metadata.NetWork == C.UDP {
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata)) err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
return c, err return c, err
@ -112,6 +117,12 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
return nil, err return nil, err
} }
c, err = t.instance.PresetXTLSConn(c)
if err != nil {
c.Close()
return nil, err
}
if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil { if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil {
c.Close() c.Close()
return nil, err return nil, err
@ -134,7 +145,7 @@ func (t *Trojan) DialContextWithDialer(ctx context.Context, dialer C.Dialer, met
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
N.TCPKeepAlive(c) tcpKeepAlive(c)
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
@ -187,7 +198,7 @@ func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, me
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
N.TCPKeepAlive(c) tcpKeepAlive(c)
c, err = t.plainStream(ctx, c) c, err = t.plainStream(ctx, c)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
@ -226,10 +237,24 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
ALPN: option.ALPN, ALPN: option.ALPN,
ServerName: option.Server, ServerName: option.Server,
SkipCertVerify: option.SkipCertVerify, SkipCertVerify: option.SkipCertVerify,
FlowShow: option.FlowShow,
Fingerprint: option.Fingerprint, Fingerprint: option.Fingerprint,
ClientFingerprint: option.ClientFingerprint, ClientFingerprint: option.ClientFingerprint,
} }
switch option.Network {
case "", "tcp":
if len(option.Flow) >= 16 {
option.Flow = option.Flow[:16]
switch option.Flow {
case vless.XRO, vless.XRD, vless.XRS:
tOption.Flow = option.Flow
default:
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
}
}
}
if option.SNI != "" { if option.SNI != "" {
tOption.ServerName = option.SNI tOption.ServerName = option.SNI
} }
@ -241,7 +266,6 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
tp: C.Trojan, tp: C.Trojan,
udp: option.UDP, udp: option.UDP,
tfo: option.TFO, tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),
@ -271,7 +295,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error())
} }
N.TCPKeepAlive(c) tcpKeepAlive(c)
return c, nil return c, nil
} }
@ -282,10 +306,13 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
ServerName: tOption.ServerName, ServerName: tOption.ServerName,
} }
var err error if len(option.Fingerprint) == 0 {
tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
if err != nil { } else {
return nil, err 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) t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, tOption.ClientFingerprint, t.realityConfig)

View File

@ -2,25 +2,25 @@ package outbound
import ( import (
"context" "context"
"crypto/sha256"
"crypto/tls" "crypto/tls"
"errors" "encoding/hex"
"encoding/pem"
"fmt" "fmt"
"math" "math"
"net" "net"
"os"
"strconv" "strconv"
"time" "time"
"github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer" "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" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/tuic" "github.com/Dreamacro/clash/transport/tuic"
"github.com/gofrs/uuid/v5" "github.com/gofrs/uuid/v5"
"github.com/metacubex/quic-go" "github.com/metacubex/quic-go"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/uot"
) )
type Tuic struct { type Tuic struct {
@ -59,9 +59,6 @@ type TuicOption struct {
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"` DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"` MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"`
SNI string `proxy:"sni,omitempty"` SNI string `proxy:"sni,omitempty"`
UDPOverStream bool `proxy:"udp-over-stream,omitempty"`
UDPOverStreamVersion int `proxy:"udp-over-stream-version,omitempty"`
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
@ -85,32 +82,6 @@ func (t *Tuic) ListenPacketContext(ctx context.Context, metadata *C.Metadata, op
// ListenPacketWithDialer implements C.ProxyAdapter // ListenPacketWithDialer implements C.ProxyAdapter
func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
if 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) pc, err := t.client.ListenPacketWithDialer(ctx, metadata, dialer, t.dialWithDialer)
if err != nil { if err != nil {
return nil, err return nil, err
@ -158,13 +129,40 @@ func NewTuic(option TuicOption) (*Tuic, error) {
tlsConfig.ServerName = option.SNI tlsConfig.ServerName = option.SNI
} }
var bs []byte
var err error var err error
tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString) if len(option.CustomCA) > 0 {
if err != nil { bs, err = os.ReadFile(option.CustomCA)
return nil, err if err != nil {
return nil, fmt.Errorf("tuic %s load ca error: %w", addr, err)
}
} else if option.CustomCAString != "" {
bs = []byte(option.CustomCAString)
} }
if option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array 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 {
tlsConfig.NextProtos = option.ALPN tlsConfig.NextProtos = option.ALPN
} else { } else {
tlsConfig.NextProtos = []string{"h3"} tlsConfig.NextProtos = []string{"h3"}
@ -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 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{ t := &Tuic{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
@ -292,10 +282,6 @@ func NewTuic(option TuicOption) (*Tuic, error) {
t.client = tuic.NewPoolClientV4(clientOption) t.client = tuic.NewPoolClientV4(clientOption)
} else { } else {
maxUdpRelayPacketSize := option.MaxUdpRelayPacketSize
if maxUdpRelayPacketSize > tuic.MaxFragSizeV5 {
maxUdpRelayPacketSize = tuic.MaxFragSizeV5
}
clientOption := &tuic.ClientOptionV5{ clientOption := &tuic.ClientOptionV5{
TlsConfig: tlsConfig, TlsConfig: tlsConfig,
QuicConfig: quicConfig, QuicConfig: quicConfig,
@ -304,7 +290,7 @@ func NewTuic(option TuicOption) (*Tuic, error) {
UdpRelayMode: udpRelayMode, UdpRelayMode: udpRelayMode,
CongestionController: option.CongestionController, CongestionController: option.CongestionController,
ReduceRtt: option.ReduceRtt, ReduceRtt: option.ReduceRtt,
MaxUdpRelayPacketSize: maxUdpRelayPacketSize, MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize,
MaxOpenStreams: clientMaxOpenStreams, MaxOpenStreams: clientMaxOpenStreams,
CWND: option.CWND, CWND: option.CWND,
} }

View File

@ -4,12 +4,12 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/tls" "crypto/tls"
"fmt" xtls "github.com/xtls/go"
"net" "net"
"net/netip" "net/netip"
"regexp"
"strconv" "strconv"
"sync" "sync"
"time"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
@ -17,10 +17,18 @@ import (
) )
var ( var (
globalClientSessionCache tls.ClientSessionCache globalClientSessionCache tls.ClientSessionCache
once sync.Once globalClientXSessionCache xtls.ClientSessionCache
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 { func getClientSessionCache() tls.ClientSessionCache {
once.Do(func() { once.Do(func() {
globalClientSessionCache = tls.NewLRUClientSessionCache(128) globalClientSessionCache = tls.NewLRUClientSessionCache(128)
@ -28,11 +36,18 @@ func getClientSessionCache() tls.ClientSessionCache {
return globalClientSessionCache return globalClientSessionCache
} }
func getClientXSessionCache() xtls.ClientSessionCache {
once.Do(func() {
globalClientXSessionCache = xtls.NewLRUClientSessionCache(128)
})
return globalClientXSessionCache
}
func serializesSocksAddr(metadata *C.Metadata) []byte { func serializesSocksAddr(metadata *C.Metadata) []byte {
var buf [][]byte var buf [][]byte
addrType := metadata.AddrType() addrType := metadata.AddrType()
aType := uint8(addrType) aType := uint8(addrType)
p := uint(metadata.DstPort) p, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
port := []byte{uint8(p >> 8), uint8(p & 0xff)} port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch addrType { switch addrType {
case socks5.AtypDomainName: case socks5.AtypDomainName:
@ -123,41 +138,3 @@ func safeConnClose(c net.Conn, err error) {
_ = c.Close() _ = 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
}

View File

@ -15,7 +15,6 @@ import (
"github.com/Dreamacro/clash/common/convert" "github.com/Dreamacro/clash/common/convert"
N "github.com/Dreamacro/clash/common/net" N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/utils" "github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer" "github.com/Dreamacro/clash/component/proxydialer"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
@ -57,8 +56,8 @@ type VlessOption struct {
Port int `proxy:"port"` Port int `proxy:"port"`
UUID string `proxy:"uuid"` UUID string `proxy:"uuid"`
Flow string `proxy:"flow,omitempty"` Flow string `proxy:"flow,omitempty"`
FlowShow bool `proxy:"flow-show,omitempty"`
TLS bool `proxy:"tls,omitempty"` TLS bool `proxy:"tls,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
PacketAddr bool `proxy:"packet-addr,omitempty"` PacketAddr bool `proxy:"packet-addr,omitempty"`
XUDP bool `proxy:"xudp,omitempty"` XUDP bool `proxy:"xudp,omitempty"`
@ -93,7 +92,6 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
Path: v.option.WSOpts.Path, Path: v.option.WSOpts.Path,
MaxEarlyData: v.option.WSOpts.MaxEarlyData, MaxEarlyData: v.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
Headers: http.Header{}, Headers: http.Header{},
} }
@ -112,9 +110,13 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
NextProtos: []string{"http/1.1"}, NextProtos: []string{"http/1.1"},
} }
wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint) if len(v.option.Fingerprint) == 0 {
if err != nil { wsOpts.TLSConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
return nil, err } else {
wsOpts.TLSConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint)
if err != nil {
return nil, err
}
} }
if v.option.ServerName != "" { if v.option.ServerName != "" {
@ -131,7 +133,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts) c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts)
case "http": case "http":
// readability first, so just copy default TLS logic // readability first, so just copy default TLS logic
c, err = v.streamTLSConn(ctx, c, false) c, err = v.streamTLSOrXTLSConn(ctx, c, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -146,7 +148,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c = vmess.StreamHTTPConn(c, httpOpts) c = vmess.StreamHTTPConn(c, httpOpts)
case "h2": case "h2":
c, err = v.streamTLSConn(ctx, c, true) c, err = v.streamTLSOrXTLSConn(ctx, c, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -161,8 +163,8 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig) c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig)
default: default:
// default tcp network // default tcp network
// handle TLS // handle TLS And XTLS
c, err = v.streamTLSConn(ctx, c, false) c, err = v.streamTLSOrXTLSConn(ctx, c, false)
} }
if err != nil { if err != nil {
@ -178,7 +180,7 @@ func (v *Vless) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err
metadata = &C.Metadata{ metadata = &C.Metadata{
NetWork: C.UDP, NetWork: C.UDP,
Host: packetaddr.SeqPacketMagicAddress, Host: packetaddr.SeqPacketMagicAddress,
DstPort: 443, DstPort: "443",
} }
} else { } else {
metadata = &C.Metadata{ // a clear metadata only contains ip metadata = &C.Metadata{ // a clear metadata only contains ip
@ -200,17 +202,29 @@ func (v *Vless) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err
return return
} }
func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error) { func (v *Vless) streamTLSOrXTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error) {
if v.option.TLS { host, _, _ := net.SplitHostPort(v.addr)
host, _, _ := net.SplitHostPort(v.addr)
if v.isLegacyXTLSEnabled() && !isH2 {
xtlsOpts := vless.XTLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
Fingerprint: v.option.Fingerprint,
}
if v.option.ServerName != "" {
xtlsOpts.Host = v.option.ServerName
}
return vless.StreamXTLSConn(ctx, conn, &xtlsOpts)
} else if v.option.TLS {
tlsOpts := vmess.TLSConfig{ tlsOpts := vmess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint, FingerPrint: v.option.Fingerprint,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig, Reality: v.realityConfig,
NextProtos: v.option.ALPN,
} }
if isH2 { if isH2 {
@ -227,6 +241,10 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne
return conn, nil return conn, nil
} }
func (v *Vless) isLegacyXTLSEnabled() bool {
return v.client.Addons != nil && v.client.Addons.Flow != vless.XRV
}
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
// gun transport // gun transport
@ -261,7 +279,7 @@ func (v *Vless) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
N.TCPKeepAlive(c) tcpKeepAlive(c)
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
@ -326,7 +344,7 @@ func (v *Vless) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
N.TCPKeepAlive(c) tcpKeepAlive(c)
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
@ -399,11 +417,12 @@ func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr {
copy(addr[1:], metadata.Host) copy(addr[1:], metadata.Host)
} }
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
return &vless.DstAddr{ return &vless.DstAddr{
UDP: metadata.NetWork == C.UDP, UDP: metadata.NetWork == C.UDP,
AddrType: addrType, AddrType: addrType,
Addr: addr, Addr: addr,
Port: metadata.DstPort, Port: uint16(port),
Mux: metadata.NetWork == C.UDP && xudp, Mux: metadata.NetWork == C.UDP && xudp,
} }
} }
@ -507,11 +526,11 @@ func NewVless(option VlessOption) (*Vless, error) {
switch option.Flow { switch option.Flow {
case vless.XRV: case vless.XRV:
log.Warnln("To use %s, ensure your server is upgrade to Xray-core v1.8.0+", vless.XRV) log.Warnln("To use %s, ensure your server is upgrade to Xray-core v1.8.0+", vless.XRV)
fallthrough
case vless.XRO, vless.XRD, vless.XRS:
addons = &vless.Addons{ addons = &vless.Addons{
Flow: option.Flow, Flow: option.Flow,
} }
case vless.XRO, vless.XRD, vless.XRS:
log.Fatalln("Legacy XTLS protocol %s is deprecated and no longer supported", option.Flow)
default: default:
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow) return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
} }
@ -530,7 +549,7 @@ func NewVless(option VlessOption) (*Vless, error) {
option.PacketAddr = false option.PacketAddr = false
} }
client, err := vless.NewClient(option.UUID, addons) client, err := vless.NewClient(option.UUID, addons, option.FlowShow)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -543,7 +562,6 @@ func NewVless(option VlessOption) (*Vless, error) {
udp: option.UDP, udp: option.UDP,
xudp: option.XUDP, xudp: option.XUDP,
tfo: option.TFO, tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),
@ -576,7 +594,7 @@ func NewVless(option VlessOption) (*Vless, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
N.TCPKeepAlive(c) tcpKeepAlive(c)
return c, nil return c, nil
} }
@ -590,7 +608,7 @@ func NewVless(option VlessOption) (*Vless, error) {
} }
var tlsConfig *tls.Config var tlsConfig *tls.Config
if option.TLS { if option.TLS {
tlsConfig = ca.GetGlobalTLSConfig(&tls.Config{ tlsConfig = tlsC.GetGlobalTLSConfig(&tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify, InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName, ServerName: v.option.ServerName,
}) })

View File

@ -13,13 +13,11 @@ import (
N "github.com/Dreamacro/clash/common/net" N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/utils" "github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer" "github.com/Dreamacro/clash/component/proxydialer"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
tlsC "github.com/Dreamacro/clash/component/tls" tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/ntp"
"github.com/Dreamacro/clash/transport/gun" "github.com/Dreamacro/clash/transport/gun"
clashVMess "github.com/Dreamacro/clash/transport/vmess" clashVMess "github.com/Dreamacro/clash/transport/vmess"
@ -54,7 +52,6 @@ type VmessOption struct {
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"` Network string `proxy:"network,omitempty"`
TLS bool `proxy:"tls,omitempty"` TLS bool `proxy:"tls,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"`
ServerName string `proxy:"servername,omitempty"` ServerName string `proxy:"servername,omitempty"`
@ -91,7 +88,6 @@ type WSOptions struct {
Headers map[string]string `proxy:"headers,omitempty"` Headers map[string]string `proxy:"headers,omitempty"`
MaxEarlyData int `proxy:"max-early-data,omitempty"` MaxEarlyData int `proxy:"max-early-data,omitempty"`
EarlyDataHeaderName string `proxy:"early-data-header-name,omitempty"` EarlyDataHeaderName string `proxy:"early-data-header-name,omitempty"`
V2rayHttpUpgrade bool `proxy:"v2ray-http-upgrade,omitempty"`
} }
// StreamConnContext implements C.ProxyAdapter // 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, Path: v.option.WSOpts.Path,
MaxEarlyData: v.option.WSOpts.MaxEarlyData, MaxEarlyData: v.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
Headers: http.Header{}, 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"}, NextProtos: []string{"http/1.1"},
} }
wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint) if len(v.option.Fingerprint) == 0 {
if err != nil { wsOpts.TLSConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
return nil, err } else {
if wsOpts.TLSConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint); err != nil {
return nil, err
}
} }
if v.option.ServerName != "" { 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, SkipCertVerify: v.option.SkipCertVerify,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig, Reality: v.realityConfig,
NextProtos: v.option.ALPN,
} }
if v.option.ServerName != "" { 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, SkipCertVerify: v.option.SkipCertVerify,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig, Reality: v.realityConfig,
NextProtos: v.option.ALPN,
} }
if v.option.ServerName != "" { if v.option.ServerName != "" {
@ -262,10 +258,10 @@ func (v *Vmess) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err
} else { } else {
if N.NeedHandshake(c) { if N.NeedHandshake(c) {
conn = v.client.DialEarlyConn(c, conn = v.client.DialEarlyConn(c,
M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) M.ParseSocksaddr(metadata.RemoteAddress()))
} else { } else {
conn, err = v.client.DialConn(c, conn, err = v.client.DialConn(c,
M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) M.ParseSocksaddr(metadata.RemoteAddress()))
} }
} }
if err != nil { if err != nil {
@ -286,7 +282,7 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(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 { if err != nil {
return nil, err return nil, err
} }
@ -308,7 +304,7 @@ func (v *Vmess) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
N.TCPKeepAlive(c) tcpKeepAlive(c)
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
@ -369,7 +365,7 @@ func (v *Vmess) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
N.TCPKeepAlive(c) tcpKeepAlive(c)
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
@ -417,7 +413,6 @@ func NewVmess(option VmessOption) (*Vmess, error) {
if option.AuthenticatedLength { if option.AuthenticatedLength {
options = append(options, vmess.ClientWithAuthenticatedLength()) options = append(options, vmess.ClientWithAuthenticatedLength())
} }
options = append(options, vmess.ClientWithTimeFunc(ntp.Now))
client, err := vmess.NewClient(option.UUID, security, option.AlterID, options...) client, err := vmess.NewClient(option.UUID, security, option.AlterID, options...)
if err != nil { if err != nil {
return nil, err return nil, err
@ -441,7 +436,6 @@ func NewVmess(option VmessOption) (*Vmess, error) {
udp: option.UDP, udp: option.UDP,
xudp: option.XUDP, xudp: option.XUDP,
tfo: option.TFO, tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),
@ -469,7 +463,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
N.TCPKeepAlive(c) tcpKeepAlive(c)
return c, nil return c, nil
} }
@ -483,7 +477,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
} }
var tlsConfig *tls.Config var tlsConfig *tls.Config
if option.TLS { if option.TLS {
tlsConfig = ca.GetGlobalTLSConfig(&tls.Config{ tlsConfig = tlsC.GetGlobalTLSConfig(&tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify, InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName, ServerName: v.option.ServerName,
}) })

View File

@ -27,6 +27,7 @@ import (
"github.com/sagernet/sing/common/debug" "github.com/sagernet/sing/common/debug"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/wireguard-go/device" "github.com/sagernet/wireguard-go/device"
) )
@ -35,7 +36,7 @@ type WireGuard struct {
bind *wireguard.ClientBind bind *wireguard.ClientBind
device *device.Device device *device.Device
tunDevice wireguard.Device tunDevice wireguard.Device
dialer proxydialer.SingDialer dialer *wgSingDialer
startOnce sync.Once startOnce sync.Once
startErr error startErr error
resolver *dns.Resolver resolver *dns.Resolver
@ -69,6 +70,37 @@ type WireGuardPeerOption struct {
AllowedIPs []string `proxy:"allowed-ips,omitempty"` 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 { type wgSingErrorHandler struct {
name string name string
} }
@ -136,7 +168,7 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) {
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),
}, },
dialer: proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer()), dialer: &wgSingDialer{dialer: dialer.NewDialer(), proxyName: option.DialerProxy},
} }
runtime.SetFinalizer(outbound, closeWireGuard) runtime.SetFinalizer(outbound, closeWireGuard)
@ -270,7 +302,7 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) {
if err != nil { if err != nil {
return nil, E.Cause(err, "create WireGuard device") 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{}) { Verbosef: func(format string, args ...interface{}) {
log.SingLogger.Debug(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...))) 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) { func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
options := w.Base.DialOptions(opts...) options := w.Base.DialOptions(opts...)
w.dialer.SetDialer(dialer.NewDialer(options...)) w.dialer.dialer = dialer.NewDialer(options...)
var conn net.Conn var conn net.Conn
w.startOnce.Do(func() { w.startOnce.Do(func() {
w.startErr = w.tunDevice.Start() w.startErr = w.tunDevice.Start()
@ -342,7 +374,8 @@ func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts
options = append(options, dialer.WithNetDialer(wgNetDialer{tunDevice: w.tunDevice})) options = append(options, dialer.WithNetDialer(wgNetDialer{tunDevice: w.tunDevice}))
conn, err = dialer.NewDialer(options...).DialContext(ctx, "tcp", metadata.RemoteAddress()) conn, err = dialer.NewDialer(options...).DialContext(ctx, "tcp", metadata.RemoteAddress())
} else { } else {
conn, err = w.tunDevice.DialContext(ctx, "tcp", M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap()) port, _ := strconv.Atoi(metadata.DstPort)
conn, err = w.tunDevice.DialContext(ctx, "tcp", M.SocksaddrFrom(metadata.DstIP, uint16(port)).Unwrap())
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -355,7 +388,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) { func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
options := w.Base.DialOptions(opts...) options := w.Base.DialOptions(opts...)
w.dialer.SetDialer(dialer.NewDialer(options...)) w.dialer.dialer = dialer.NewDialer(options...)
var pc net.PacketConn var pc net.PacketConn
w.startOnce.Do(func() { w.startOnce.Do(func() {
w.startErr = w.tunDevice.Start() w.startErr = w.tunDevice.Start()
@ -379,7 +412,8 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat
} }
metadata.DstIP = ip metadata.DstIP = ip
} }
pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap()) port, _ := strconv.Atoi(metadata.DstPort)
pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, uint16(port)).Unwrap())
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -28,7 +28,7 @@ type GroupBase struct {
failedTestMux sync.Mutex failedTestMux sync.Mutex
failedTimes int failedTimes int
failedTime time.Time failedTime time.Time
failedTesting atomic.Bool failedTesting *atomic.Bool
proxies [][]C.Proxy proxies [][]C.Proxy
versions []atomic.Uint32 versions []atomic.Uint32
} }

View File

@ -1,5 +1,17 @@
package outboundgroup 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 { type SelectAble interface {
Set(string) error Set(string) error
ForceSet(name string) ForceSet(name string)

View File

@ -92,13 +92,6 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
break break
} }
proxy, err = outbound.NewHysteria(*hyOption) 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": case "wireguard":
wgOption := &outbound.WireGuardOption{} wgOption := &outbound.WireGuardOption{}
err = decoder.Decode(mapping, wgOption) err = decoder.Decode(mapping, wgOption)
@ -113,20 +106,6 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
break break
} }
proxy, err = outbound.NewTuic(*tuicOption) 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: default:
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
} }

View File

@ -34,12 +34,12 @@ type HealthCheck struct {
url string url string
extra map[string]*extraOption extra map[string]*extraOption
mu sync.Mutex mu sync.Mutex
started atomic.Bool started *atomic.Bool
proxies []C.Proxy proxies []C.Proxy
interval time.Duration interval uint
lazy bool lazy bool
expectedStatus utils.IntRanges[uint16] expectedStatus utils.IntRanges[uint16]
lastTouch atomic.TypedValue[time.Time] lastTouch *atomic.Int64
done chan struct{} done chan struct{}
singleDo *singledo.Single[struct{}] singleDo *singledo.Single[struct{}]
} }
@ -50,14 +50,13 @@ func (hc *HealthCheck) process() {
return return
} }
ticker := time.NewTicker(hc.interval) ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
hc.start() hc.start()
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
lastTouch := hc.lastTouch.Load() now := time.Now().Unix()
since := time.Since(lastTouch) if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
if !hc.lazy || since < hc.interval {
hc.check() hc.check()
} else { } else {
log.Debugln("Skip once health check because we are lazy") 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 the provider has not set up health checks, then modify it to be the same as the group's interval
if hc.interval == 0 { if hc.interval == 0 {
hc.interval = time.Duration(interval) * time.Second hc.interval = interval
} }
if hc.extra == nil { if hc.extra == nil {
@ -136,7 +135,7 @@ func (hc *HealthCheck) auto() bool {
} }
func (hc *HealthCheck) touch() { func (hc *HealthCheck) touch() {
hc.lastTouch.Store(time.Now()) hc.lastTouch.Store(time.Now().Unix())
} }
func (hc *HealthCheck) start() { func (hc *HealthCheck) start() {
@ -229,9 +228,11 @@ func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool, exp
proxies: proxies, proxies: proxies,
url: url, url: url,
extra: map[string]*extraOption{}, extra: map[string]*extraOption{},
interval: time.Duration(interval) * time.Second, started: atomic.NewBool(false),
interval: interval,
lazy: lazy, lazy: lazy,
expectedStatus: expectedStatus, expectedStatus: expectedStatus,
lastTouch: atomic.NewInt64(0),
done: make(chan struct{}, 1), done: make(chan struct{}, 1),
singleDo: singledo.NewSingle[struct{}](time.Second), singleDo: singledo.NewSingle[struct{}](time.Second),
} }

View File

@ -299,7 +299,7 @@ func proxiesParseAndFilter(filter string, excludeFilter string, excludeTypeArray
if err := yaml.Unmarshal(buf, schema); err != nil { if err := yaml.Unmarshal(buf, schema); err != nil {
proxies, err1 := convert.ConvertsV2Ray(buf) proxies, err1 := convert.ConvertsV2Ray(buf)
if err1 != nil { if err1 != nil {
return nil, fmt.Errorf("%w, %w", err, err1) return nil, fmt.Errorf("%s, %w", err.Error(), err1)
} }
schema.Proxies = proxies schema.Proxies = proxies
} }

View File

@ -1,6 +1,7 @@
package provider package provider
import ( import (
"github.com/dlclark/regexp2"
"strconv" "strconv"
"strings" "strings"
) )
@ -12,24 +13,45 @@ type SubscriptionInfo struct {
Expire int64 Expire int64
} }
func NewSubscriptionInfo(userinfo string) (si *SubscriptionInfo, err error) { func NewSubscriptionInfo(str string) (si *SubscriptionInfo, err error) {
userinfo = strings.ToLower(userinfo) si = &SubscriptionInfo{}
userinfo = strings.ReplaceAll(userinfo, " ", "") str = strings.ToLower(str)
si = new(SubscriptionInfo) reTraffic := regexp2.MustCompile("upload=(\\d+); download=(\\d+); total=(\\d+)", 0)
for _, field := range strings.Split(userinfo, ";") { reExpire := regexp2.MustCompile("expire=(\\d+)", 0)
switch name, value, _ := strings.Cut(field, "="); name {
case "upload": match, err := reTraffic.FindStringMatch(str)
si.Upload, err = strconv.ParseInt(value, 10, 64) if err != nil || match == nil {
case "download": return nil, err
si.Download, err = strconv.ParseInt(value, 10, 64) }
case "total": group := match.Groups()
si.Total, err = strconv.ParseInt(value, 10, 64) si.Upload, err = str2uint64(group[1].String())
case "expire": if err != nil {
si.Expire, err = strconv.ParseInt(value, 10, 64) 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 { if err != nil {
return return nil, err
} }
} }
return return
} }
func str2uint64(str string) (int64, error) {
i, err := strconv.ParseInt(str, 10, 64)
return i, err
}

View File

@ -11,9 +11,10 @@ type Bool struct {
atomic.Bool atomic.Bool
} }
func NewBool(val bool) (i Bool) { func NewBool(val bool) *Bool {
i := &Bool{}
i.Store(val) i.Store(val)
return return i
} }
func (i *Bool) MarshalJSON() ([]byte, error) { func (i *Bool) MarshalJSON() ([]byte, error) {
@ -38,11 +39,12 @@ type Pointer[T any] struct {
atomic.Pointer[T] 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 { if v != nil {
p.Store(v) p.Store(v)
} }
return return &p
} }
func (p *Pointer[T]) MarshalJSON() ([]byte, error) { func (p *Pointer[T]) MarshalJSON() ([]byte, error) {
@ -66,9 +68,10 @@ type Int32 struct {
atomic.Int32 atomic.Int32
} }
func NewInt32(val int32) (i Int32) { func NewInt32(val int32) *Int32 {
i := &Int32{}
i.Store(val) i.Store(val)
return return i
} }
func (i *Int32) MarshalJSON() ([]byte, error) { func (i *Int32) MarshalJSON() ([]byte, error) {
@ -93,9 +96,10 @@ type Int64 struct {
atomic.Int64 atomic.Int64
} }
func NewInt64(val int64) (i Int64) { func NewInt64(val int64) *Int64 {
i := &Int64{}
i.Store(val) i.Store(val)
return return i
} }
func (i *Int64) MarshalJSON() ([]byte, error) { func (i *Int64) MarshalJSON() ([]byte, error) {
@ -120,9 +124,10 @@ type Uint32 struct {
atomic.Uint32 atomic.Uint32
} }
func NewUint32(val uint32) (i Uint32) { func NewUint32(val uint32) *Uint32 {
i := &Uint32{}
i.Store(val) i.Store(val)
return return i
} }
func (i *Uint32) MarshalJSON() ([]byte, error) { func (i *Uint32) MarshalJSON() ([]byte, error) {
@ -147,9 +152,10 @@ type Uint64 struct {
atomic.Uint64 atomic.Uint64
} }
func NewUint64(val uint64) (i Uint64) { func NewUint64(val uint64) *Uint64 {
i := &Uint64{}
i.Store(val) i.Store(val)
return return i
} }
func (i *Uint64) MarshalJSON() ([]byte, error) { func (i *Uint64) MarshalJSON() ([]byte, error) {
@ -174,9 +180,10 @@ type Uintptr struct {
atomic.Uintptr atomic.Uintptr
} }
func NewUintptr(val uintptr) (i Uintptr) { func NewUintptr(val uintptr) *Uintptr {
i := &Uintptr{}
i.Store(val) i.Store(val)
return return i
} }
func (i *Uintptr) MarshalJSON() ([]byte, error) { func (i *Uintptr) MarshalJSON() ([]byte, error) {

View File

@ -12,7 +12,6 @@ func DefaultValue[T any]() T {
type TypedValue[T any] struct { type TypedValue[T any] struct {
value atomic.Value value atomic.Value
_ noCopy
} }
func (t *TypedValue[T]) Load() T { func (t *TypedValue[T]) Load() T {
@ -52,13 +51,8 @@ func (t *TypedValue[T]) UnmarshalJSON(b []byte) error {
return nil 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) 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() {}

View File

@ -10,11 +10,19 @@ const BufferSize = buf.BufferSize
type Buffer = buf.Buffer type Buffer = buf.Buffer
var New = buf.New var New = buf.New
var NewPacket = buf.NewPacket
var NewSize = buf.NewSize var NewSize = buf.NewSize
var StackNew = buf.StackNew
var StackNewSize = buf.StackNewSize
var With = buf.With var With = buf.With
var As = buf.As var As = buf.As
var KeepAlive = common.KeepAlive
//go:norace
func Dup[T any](obj T) T {
return common.Dup(obj)
}
var ( var (
Must = common.Must Must = common.Must
Error = common.Error Error = common.Error

View File

@ -7,8 +7,6 @@ import (
"time" "time"
"github.com/Dreamacro/clash/common/generics/list" "github.com/Dreamacro/clash/common/generics/list"
"github.com/samber/lo"
) )
// Option is part of Functional Options Pattern // Option is part of Functional Options Pattern
@ -84,27 +82,9 @@ func New[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] {
// Get returns the any representation of a cached response and a bool // Get returns the any representation of a cached response and a bool
// set to true if the key was found. // set to true if the key was found.
func (c *LruCache[K, V]) Get(key K) (V, bool) { func (c *LruCache[K, V]) Get(key K) (V, bool) {
c.mu.Lock()
defer c.mu.Unlock()
el := c.get(key) el := c.get(key)
if el == nil { if el == nil {
return lo.Empty[V](), false return getZero[V](), false
}
value := el.value
return value, true
}
func (c *LruCache[K, V]) GetOrStore(key K, constructor func() V) (V, bool) {
c.mu.Lock()
defer c.mu.Unlock()
el := c.get(key)
if el == nil {
value := constructor()
c.set(key, value)
return value, false
} }
value := el.value value := el.value
@ -116,12 +96,9 @@ func (c *LruCache[K, V]) GetOrStore(key K, constructor func() V) (V, bool) {
// and a bool set to true if the key was found. // and a bool set to true if the key was found.
// This method will NOT check the maxAge of element and will NOT update the expires. // This method will NOT check the maxAge of element and will NOT update the expires.
func (c *LruCache[K, V]) GetWithExpire(key K) (V, time.Time, bool) { func (c *LruCache[K, V]) GetWithExpire(key K) (V, time.Time, bool) {
c.mu.Lock()
defer c.mu.Unlock()
el := c.get(key) el := c.get(key)
if el == nil { 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 return el.value, time.Unix(el.expires, 0), true
@ -138,18 +115,11 @@ func (c *LruCache[K, V]) Exist(key K) bool {
// Set stores the any representation of a response for a given key. // Set stores the any representation of a response for a given key.
func (c *LruCache[K, V]) Set(key K, value V) { func (c *LruCache[K, V]) Set(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
c.set(key, value)
}
func (c *LruCache[K, V]) set(key K, value V) {
expires := int64(0) expires := int64(0)
if c.maxAge > 0 { if c.maxAge > 0 {
expires = time.Now().Unix() + c.maxAge expires = time.Now().Unix() + c.maxAge
} }
c.setWithExpire(key, value, time.Unix(expires, 0)) c.SetWithExpire(key, value, time.Unix(expires, 0))
} }
// SetWithExpire stores the any representation of a response for a given key and given expires. // SetWithExpire stores the any representation of a response for a given key and given expires.
@ -158,10 +128,6 @@ func (c *LruCache[K, V]) SetWithExpire(key K, value V, expires time.Time) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
c.setWithExpire(key, value, expires)
}
func (c *LruCache[K, V]) setWithExpire(key K, value V, expires time.Time) {
if le, ok := c.cache[key]; ok { if le, ok := c.cache[key]; ok {
c.lru.MoveToBack(le) c.lru.MoveToBack(le)
e := le.Value e := le.Value
@ -199,6 +165,9 @@ func (c *LruCache[K, V]) CloneTo(n *LruCache[K, V]) {
} }
func (c *LruCache[K, V]) get(key K) *entry[K, V] { func (c *LruCache[K, V]) get(key K) *entry[K, V] {
c.mu.Lock()
defer c.mu.Unlock()
le, ok := c.cache[key] le, ok := c.cache[key]
if !ok { if !ok {
return nil return nil
@ -222,11 +191,12 @@ func (c *LruCache[K, V]) get(key K) *entry[K, V] {
// Delete removes the value associated with a key. // Delete removes the value associated with a key.
func (c *LruCache[K, V]) Delete(key K) { func (c *LruCache[K, V]) Delete(key K) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock()
if le, ok := c.cache[key]; ok { if le, ok := c.cache[key]; ok {
c.deleteElement(le) c.deleteElement(le)
} }
c.mu.Unlock()
} }
func (c *LruCache[K, V]) maybeDeleteOldest() { func (c *LruCache[K, V]) maybeDeleteOldest() {
@ -249,10 +219,10 @@ func (c *LruCache[K, V]) deleteElement(le *list.Element[*entry[K, V]]) {
func (c *LruCache[K, V]) Clear() error { func (c *LruCache[K, V]) Clear() error {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock()
c.cache = make(map[K]*list.Element[*entry[K, V]]) c.cache = make(map[K]*list.Element[*entry[K, V]])
c.mu.Unlock()
return nil return nil
} }
@ -261,3 +231,8 @@ type entry[K comparable, V any] struct {
value V value V
expires int64 expires int64
} }
func getZero[T any]() T {
var result T
return result
}

View File

@ -21,7 +21,7 @@ func TestSplitArgs(t *testing.T) {
func TestExecCmd(t *testing.T) { func TestExecCmd(t *testing.T) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
_, err := ExecCmd("cmd -c 'dir'") _, err := ExecCmd("dir")
assert.Nil(t, err) assert.Nil(t, err)
return return
} }

View File

@ -50,9 +50,7 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
hysteria["port"] = urlHysteria.Port() hysteria["port"] = urlHysteria.Port()
hysteria["sni"] = query.Get("peer") hysteria["sni"] = query.Get("peer")
hysteria["obfs"] = query.Get("obfs") hysteria["obfs"] = query.Get("obfs")
if alpn := query.Get("alpn"); alpn != "" { hysteria["alpn"] = []string{query.Get("alpn")}
hysteria["alpn"] = strings.Split(alpn, ",")
}
hysteria["auth_str"] = query.Get("auth") hysteria["auth_str"] = query.Get("auth")
hysteria["protocol"] = query.Get("protocol") hysteria["protocol"] = query.Get("protocol")
up := query.Get("up") 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")) hysteria["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure"))
proxies = append(proxies, hysteria) 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": case "trojan":
urlTrojan, err := url.Parse(line) urlTrojan, err := url.Parse(line)
@ -161,12 +86,10 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
trojan["udp"] = true trojan["udp"] = true
trojan["skip-cert-verify"], _ = strconv.ParseBool(query.Get("allowInsecure")) trojan["skip-cert-verify"], _ = strconv.ParseBool(query.Get("allowInsecure"))
if sni := query.Get("sni"); sni != "" { sni := query.Get("sni")
if sni != "" {
trojan["sni"] = sni trojan["sni"] = sni
} }
if alpn := query.Get("alpn"); alpn != "" {
trojan["alpn"] = strings.Split(alpn, ",")
}
network := strings.ToLower(query.Get("type")) network := strings.ToLower(query.Get("type"))
if network != "" { if network != "" {
@ -294,9 +217,6 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
if strings.HasSuffix(tls, "tls") { if strings.HasSuffix(tls, "tls") {
vmess["tls"] = true vmess["tls"] = true
} }
if alpn, ok := values["alpn"].(string); ok {
vmess["alpn"] = strings.Split(alpn, ",")
}
} }
switch network { switch network {
@ -412,7 +332,6 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
} }
} }
proxies = append(proxies, ss) proxies = append(proxies, ss)
case "ssr": case "ssr":
dcBuf, err := encRaw.DecodeString(body) dcBuf, err := encRaw.DecodeString(body)
if err != nil { if err != nil {

View File

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

View File

@ -24,6 +24,8 @@ func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy m
proxy["port"] = url.Port() proxy["port"] = url.Port()
proxy["uuid"] = url.User.Username() proxy["uuid"] = url.User.Username()
proxy["udp"] = true proxy["udp"] = true
proxy["skip-cert-verify"] = false
proxy["tls"] = false
tls := strings.ToLower(query.Get("security")) tls := strings.ToLower(query.Get("security"))
if strings.HasSuffix(tls, "tls") || tls == "reality" { if strings.HasSuffix(tls, "tls") || tls == "reality" {
proxy["tls"] = true proxy["tls"] = true
@ -32,9 +34,6 @@ func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy m
} else { } else {
proxy["client-fingerprint"] = fingerprint proxy["client-fingerprint"] = fingerprint
} }
if alpn := query.Get("alpn"); alpn != "" {
proxy["alpn"] = strings.Split(alpn, ",")
}
} }
if sni := query.Get("sni"); sni != "" { if sni := query.Get("sni"); sni != "" {
proxy["servername"] = sni proxy["servername"] = sni

View File

@ -22,16 +22,6 @@ func NewBufferedConn(c net.Conn) *BufferedConn {
return &BufferedConn{bufio.NewReader(c), NewExtendedConn(c), false} 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. // Reader returns the internal bufio.Reader.
func (c *BufferedConn) Reader() *bufio.Reader { func (c *BufferedConn) Reader() *bufio.Reader {
return c.r return c.r

View File

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

View File

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

View File

@ -4,11 +4,8 @@ import (
"fmt" "fmt"
"net" "net"
"strings" "strings"
"time"
) )
var KeepAliveInterval = 15 * time.Second
func SplitNetworkType(s string) (string, string, error) { func SplitNetworkType(s string) (string, string, error) {
var ( var (
shecme string shecme string
@ -47,10 +44,3 @@ func SplitHostPort(s string) (host, port string, hasPort bool, err error) {
host, port, err = net.SplitHostPort(temp) host, port, err = net.SplitHostPort(temp)
return return
} }
func TCPKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
_ = tcp.SetKeepAlive(true)
_ = tcp.SetKeepAlivePeriod(KeepAliveInterval)
}
}

View File

@ -10,11 +10,7 @@ import (
"math/big" "math/big"
) )
type Path interface { func ParseCert(certificate, privateKey string) (tls.Certificate, error) {
Resolve(path string) string
}
func ParseCert(certificate, privateKey string, path Path) (tls.Certificate, error) {
if certificate == "" && privateKey == "" { if certificate == "" && privateKey == "" {
return newRandomTLSKeyPair() return newRandomTLSKeyPair()
} }
@ -23,8 +19,6 @@ func ParseCert(certificate, privateKey string, path Path) (tls.Certificate, erro
return cert, nil return cert, nil
} }
certificate = path.Resolve(certificate)
privateKey = path.Resolve(privateKey)
cert, loadErr := tls.LoadX509KeyPair(certificate, privateKey) cert, loadErr := tls.LoadX509KeyPair(certificate, privateKey)
if loadErr != nil { if loadErr != nil {
return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())

View File

@ -47,7 +47,6 @@ func (p *Picker[T]) Wait() T {
p.wg.Wait() p.wg.Wait()
if p.cancel != nil { if p.cancel != nil {
p.cancel() p.cancel()
p.cancel = nil
} }
return p.result return p.result
} }
@ -70,7 +69,6 @@ func (p *Picker[T]) Go(f func() (T, error)) {
p.result = ret p.result = ret
if p.cancel != nil { if p.cancel != nil {
p.cancel() p.cancel()
p.cancel = nil
} }
}) })
} else { } else {
@ -80,13 +78,3 @@ func (p *Picker[T]) Go(f func() (T, error)) {
} }
}() }()
} }
// Close cancels the picker context and releases resources associated with it.
// If Wait has been called, then there is no need to call Close.
func (p *Picker[T]) Close() error {
if p.cancel != nil {
p.cancel()
p.cancel = nil
}
return nil
}

View File

@ -5,7 +5,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/samber/lo"
"github.com/stretchr/testify/assert" "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: case <-timer.C:
return input, nil return input, nil
case <-ctx.Done(): 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)) picker.Go(sleepAndSend(ctx, 20, 1))
number := picker.Wait() number := picker.Wait()
assert.Equal(t, number, lo.Empty[int]()) assert.Equal(t, number, getZero[int]())
assert.NotNil(t, picker.Error()) assert.NotNil(t, picker.Error())
} }
func getZero[T any]() T {
var result T
return result
}

View File

@ -32,32 +32,23 @@ func NewAllocator() *Allocator {
// Get a []byte from pool with most appropriate cap // Get a []byte from pool with most appropriate cap
func (alloc *Allocator) Get(size int) []byte { func (alloc *Allocator) Get(size int) []byte {
switch { if size <= 0 || size > 65536 {
case size < 0:
panic("alloc.Get: len out of range")
case size == 0:
return nil return nil
case size > 65536:
return make([]byte, size)
default:
bits := msb(size)
if size == 1<<bits {
return alloc.buffers[bits].Get().([]byte)[:size]
}
return alloc.buffers[bits+1].Get().([]byte)[:size]
} }
bits := msb(size)
if size == 1<<bits {
return alloc.buffers[bits].Get().([]byte)[:size]
}
return alloc.buffers[bits+1].Get().([]byte)[:size]
} }
// Put returns a []byte to pool for future use, // Put returns a []byte to pool for future use,
// which the cap must be exactly 2^n // which the cap must be exactly 2^n
func (alloc *Allocator) Put(buf []byte) error { func (alloc *Allocator) Put(buf []byte) error {
if cap(buf) == 0 || cap(buf) > 65536 {
return nil
}
bits := msb(cap(buf)) bits := msb(cap(buf))
if cap(buf) != 1<<bits { if cap(buf) == 0 || cap(buf) > 65536 || cap(buf) != 1<<bits {
return errors.New("allocator Put() incorrect buffer size") return errors.New("allocator Put() incorrect buffer size")
} }

View File

@ -19,17 +19,17 @@ func TestAllocGet(t *testing.T) {
assert.Equal(t, 1024, cap(alloc.Get(1023))) assert.Equal(t, 1024, cap(alloc.Get(1023)))
assert.Equal(t, 1024, len(alloc.Get(1024))) assert.Equal(t, 1024, len(alloc.Get(1024)))
assert.Equal(t, 65536, len(alloc.Get(65536))) assert.Equal(t, 65536, len(alloc.Get(65536)))
assert.Equal(t, 65537, len(alloc.Get(65537))) assert.Nil(t, alloc.Get(65537))
} }
func TestAllocPut(t *testing.T) { func TestAllocPut(t *testing.T) {
alloc := NewAllocator() alloc := NewAllocator()
assert.Nil(t, alloc.Put(nil), "put nil misbehavior") assert.NotNil(t, alloc.Put(nil), "put nil misbehavior")
assert.NotNil(t, alloc.Put(make([]byte, 3)), "put elem:3 []bytes misbehavior") assert.NotNil(t, alloc.Put(make([]byte, 3)), "put elem:3 []bytes misbehavior")
assert.Nil(t, alloc.Put(make([]byte, 4)), "put elem:4 []bytes misbehavior") assert.Nil(t, alloc.Put(make([]byte, 4)), "put elem:4 []bytes misbehavior")
assert.Nil(t, alloc.Put(make([]byte, 1023, 1024)), "put elem:1024 []bytes misbehavior") assert.Nil(t, alloc.Put(make([]byte, 1023, 1024)), "put elem:1024 []bytes misbehavior")
assert.Nil(t, alloc.Put(make([]byte, 65536)), "put elem:65536 []bytes misbehavior") assert.Nil(t, alloc.Put(make([]byte, 65536)), "put elem:65536 []bytes misbehavior")
assert.Nil(t, alloc.Put(make([]byte, 65537)), "put elem:65537 []bytes misbehavior") assert.NotNil(t, alloc.Put(make([]byte, 65537)), "put elem:65537 []bytes misbehavior")
} }
func TestAllocPutThenGet(t *testing.T) { func TestAllocPutThenGet(t *testing.T) {

View File

@ -2,8 +2,6 @@ package queue
import ( import (
"sync" "sync"
"github.com/samber/lo"
) )
// Queue is a simple concurrent safe queue // Queue is a simple concurrent safe queue
@ -26,7 +24,7 @@ func (q *Queue[T]) Put(items ...T) {
// Pop returns the head of items. // Pop returns the head of items.
func (q *Queue[T]) Pop() T { func (q *Queue[T]) Pop() T {
if len(q.items) == 0 { if len(q.items) == 0 {
return lo.Empty[T]() return GetZero[T]()
} }
q.lock.Lock() q.lock.Lock()
@ -39,7 +37,7 @@ func (q *Queue[T]) Pop() T {
// Last returns the last of item. // Last returns the last of item.
func (q *Queue[T]) Last() T { func (q *Queue[T]) Last() T {
if len(q.items) == 0 { if len(q.items) == 0 {
return lo.Empty[T]() return GetZero[T]()
} }
q.lock.RLock() q.lock.RLock()
@ -71,3 +69,8 @@ func New[T any](hint int64) *Queue[T] {
items: make([]T, 0, hint), items: make([]T, 0, hint),
} }
} }
func GetZero[T any]() T {
var result T
return result
}

View File

@ -96,11 +96,6 @@ func (d *Decoder) decode(name string, data any, val reflect.Value) error {
return d.decodeFloat(name, data, val) return d.decodeFloat(name, data, val)
} }
switch kind { switch kind {
case reflect.Pointer:
if val.IsNil() {
val.Set(reflect.New(val.Type().Elem()))
}
return d.decode(name, data, val.Elem())
case reflect.String: case reflect.String:
return d.decodeString(name, data, val) return d.decodeString(name, data, val)
case reflect.Bool: case reflect.Bool:
@ -287,9 +282,6 @@ func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error {
} }
valSlice := val valSlice := val
// make a new slice with cap(val)==cap(dataVal)
// the caller can determine whether the original configuration contains this item by judging whether the value is nil.
valSlice = reflect.MakeSlice(valType, 0, dataVal.Len())
for i := 0; i < dataVal.Len(); i++ { for i := 0; i < dataVal.Len(); i++ {
currentData := dataVal.Index(i).Interface() currentData := dataVal.Index(i).Interface()
for valSlice.Len() <= i { for valSlice.Len() <= i {

View File

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

View File

@ -2,20 +2,48 @@ package utils
import "unsafe" import "unsafe"
// sliceHeader is equivalent to reflect.SliceHeader, but represents the pointer
// to the underlying array as unsafe.Pointer rather than uintptr, allowing
// sliceHeaders to be directly converted to slice objects.
type sliceHeader struct {
Data unsafe.Pointer
Len int
Cap int
}
// slice returns a slice whose underlying array starts at ptr an which length
// and capacity are len.
func slice[T any](ptr *T, length int) []T {
var s []T
hdr := (*sliceHeader)(unsafe.Pointer(&s))
hdr.Data = unsafe.Pointer(ptr)
hdr.Len = length
hdr.Cap = length
return s
}
// stringHeader is equivalent to reflect.StringHeader, but represents the
// pointer to the underlying array as unsafe.Pointer rather than uintptr,
// allowing StringHeaders to be directly converted to strings.
type stringHeader struct {
Data unsafe.Pointer
Len int
}
// ImmutableBytesFromString is equivalent to []byte(s), except that it uses the // ImmutableBytesFromString is equivalent to []byte(s), except that it uses the
// same memory backing s instead of making a heap-allocated copy. This is only // same memory backing s instead of making a heap-allocated copy. This is only
// valid if the returned slice is never mutated. // valid if the returned slice is never mutated.
func ImmutableBytesFromString(s string) []byte { func ImmutableBytesFromString(s string) []byte {
b := unsafe.StringData(s) shdr := (*stringHeader)(unsafe.Pointer(&s))
return unsafe.Slice(b, len(s)) return slice((*byte)(shdr.Data), shdr.Len)
} }
// StringFromImmutableBytes is equivalent to string(bs), except that it uses // StringFromImmutableBytes is equivalent to string(bs), except that it uses
// the same memory backing bs instead of making a heap-allocated copy. This is // the same memory backing bs instead of making a heap-allocated copy. This is
// only valid if bs is never mutated after StringFromImmutableBytes returns. // only valid if bs is never mutated after StringFromImmutableBytes returns.
func StringFromImmutableBytes(bs []byte) string { func StringFromImmutableBytes(bs []byte) string {
if len(bs) == 0 { // This is cheaper than messing with StringHeader and SliceHeader, which as
return "" // of this writing produces many dead stores of zeroes. Compare
} // strings.Builder.String().
return unsafe.String(&bs[0], len(bs)) return *(*string)(unsafe.Pointer(&bs))
} }

View File

@ -1,5 +1,9 @@
package auth package auth
import (
"sync"
)
type Authenticator interface { type Authenticator interface {
Verify(user string, pass string) bool Verify(user string, pass string) bool
Users() []string Users() []string
@ -11,12 +15,12 @@ type AuthUser struct {
} }
type inMemoryAuthenticator struct { type inMemoryAuthenticator struct {
storage map[string]string storage *sync.Map
usernames []string usernames []string
} }
func (au *inMemoryAuthenticator) Verify(user string, pass string) bool { func (au *inMemoryAuthenticator) Verify(user string, pass string) bool {
realPass, ok := au.storage[user] realPass, ok := au.storage.Load(user)
return ok && realPass == pass return ok && realPass == pass
} }
@ -26,13 +30,17 @@ func NewAuthenticator(users []AuthUser) Authenticator {
if len(users) == 0 { if len(users) == 0 {
return nil return nil
} }
au := &inMemoryAuthenticator{
storage: make(map[string]string), au := &inMemoryAuthenticator{storage: &sync.Map{}}
usernames: make([]string, 0, len(users)),
}
for _, user := range users { for _, user := range users {
au.storage[user.User] = user.Pass au.storage.Store(user.User, user.Pass)
au.usernames = append(au.usernames, user.User)
} }
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 return au
} }

View File

@ -14,15 +14,5 @@ func ListenDHCPClient(ctx context.Context, ifaceName string) (net.PacketConn, er
listenAddr = "255.255.255.255:68" listenAddr = "255.255.255.255:68"
} }
options := []dialer.Option{ return dialer.ListenPacket(ctx, "udp4", listenAddr, dialer.WithInterface(ifaceName), dialer.WithAddrReuse(true))
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...)
} }

View File

@ -3,7 +3,6 @@ package dialer
import ( import (
"net" "net"
"net/netip" "net/netip"
"strconv"
"strings" "strings"
"github.com/Dreamacro/clash/component/iface" "github.com/Dreamacro/clash/component/iface"
@ -15,7 +14,7 @@ func LookupLocalAddrFromIfaceName(ifaceName string, network string, destination
return nil, err return nil, err
} }
var addr netip.Prefix var addr *netip.Prefix
switch network { switch network {
case "udp4", "tcp4": case "udp4", "tcp4":
addr, err = ifaceObj.PickIPv4Addr(destination) addr, err = ifaceObj.PickIPv4Addr(destination)
@ -50,52 +49,3 @@ func LookupLocalAddrFromIfaceName(ifaceName string, network string, destination
return nil, iface.ErrAddrNotFound 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
}

View File

@ -5,16 +5,55 @@ package dialer
import ( import (
"net" "net"
"net/netip" "net/netip"
"strconv"
"strings"
) )
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination netip.Addr) error { 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) { func bindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, address string) (string, error) {
return fallbackBindIfaceToListenConfig(ifaceName, lc, network, address) _, 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 { 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
} }

View File

@ -20,20 +20,3 @@ func addControlToListenConfig(lc *net.ListenConfig, fn controlFn) {
return fn(context.Background(), network, address, c) return fn(context.Background(), network, address, c)
} }
} }
func addControlToDialer(d *net.Dialer, fn controlFn) {
ld := *d
d.ControlContext = func(ctx context.Context, network, address string, c syscall.RawConn) (err error) {
switch {
case ld.ControlContext != nil:
if err = ld.ControlContext(ctx, network, address, c); err != nil {
return
}
case ld.Control != nil:
if err = ld.Control(network, address, c); err != nil {
return
}
}
return fn(ctx, network, address, c)
}
}

View File

@ -0,0 +1,22 @@
//go:build !go1.20
package dialer
import (
"context"
"net"
"syscall"
)
func addControlToDialer(d *net.Dialer, fn controlFn) {
ld := *d
d.Control = func(network, address string, c syscall.RawConn) (err error) {
switch {
case ld.Control != nil:
if err = ld.Control(network, address, c); err != nil {
return
}
}
return fn(context.Background(), network, address, c)
}
}

View File

@ -0,0 +1,26 @@
//go:build go1.20
package dialer
import (
"context"
"net"
"syscall"
)
func addControlToDialer(d *net.Dialer, fn controlFn) {
ld := *d
d.ControlContext = func(ctx context.Context, network, address string, c syscall.RawConn) (err error) {
switch {
case ld.ControlContext != nil:
if err = ld.ControlContext(ctx, network, address, c); err != nil {
return
}
case ld.Control != nil:
if err = ld.Control(network, address, c); err != nil {
return
}
}
return fn(ctx, network, address, c)
}
}

View File

@ -2,7 +2,6 @@ package dialer
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
@ -74,11 +73,7 @@ func ListenPacket(ctx context.Context, network, address string, options ...Optio
lc := &net.ListenConfig{} lc := &net.ListenConfig{}
if cfg.interfaceName != "" { if cfg.interfaceName != "" {
bind := bindIfaceToListenConfig addr, err := bindIfaceToListenConfig(cfg.interfaceName, lc, network, address)
if cfg.fallbackBind {
bind = fallbackBindIfaceToListenConfig
}
addr, err := bind(cfg.interfaceName, lc, network, address)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -129,20 +124,13 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
dialer := netDialer.(*net.Dialer) dialer := netDialer.(*net.Dialer)
if opt.interfaceName != "" { if opt.interfaceName != "" {
bind := bindIfaceToDialer if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
if opt.fallbackBind {
bind = fallbackBindIfaceToDialer
}
if err := bind(opt.interfaceName, dialer, network, destination); err != nil {
return nil, err return nil, err
} }
} }
if opt.routingMark != 0 { if opt.routingMark != 0 {
bindMarkToDialer(opt.routingMark, dialer, network, destination) bindMarkToDialer(opt.routingMark, dialer, network, destination)
} }
if opt.mpTcp {
setMultiPathTCP(dialer)
}
if opt.tfo { if opt.tfo {
return dialTFO(ctx, *dialer, network, address) return dialTFO(ctx, *dialer, network, address)
} }
@ -170,22 +158,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) { func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) {
ipv4s, ipv6s := resolver.SortationAddr(ips) ipv4s, ipv6s := resolver.SortationAddr(ips)
if len(ipv4s) == 0 && len(ipv6s) == 0 {
return nil, ErrorNoIpAddress
}
preferIPVersion := opt.prefer preferIPVersion := opt.prefer
fallbackTicker := time.NewTicker(fallbackTimeout) fallbackTicker := time.NewTicker(fallbackTimeout)
defer fallbackTicker.Stop() defer fallbackTicker.Stop()
results := make(chan dialResult) results := make(chan dialResult)
returned := make(chan struct{}) returned := make(chan struct{})
defer close(returned) defer close(returned)
var wg sync.WaitGroup
racer := func(ips []netip.Addr, isPrimary bool) { racer := func(ips []netip.Addr, isPrimary bool) {
defer wg.Done()
result := dialResult{isPrimary: isPrimary} result := dialResult{isPrimary: isPrimary}
defer func() { defer func() {
select { select {
@ -198,36 +178,18 @@ func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string,
}() }()
result.Conn, result.error = dialFn(ctx, network, ips, port, opt) result.Conn, result.error = dialFn(ctx, network, ips, port, opt)
} }
go racer(ipv4s, preferIPVersion != 6)
if len(ipv4s) != 0 { go racer(ipv6s, preferIPVersion != 4)
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)
}()
var fallback dialResult var fallback dialResult
var errs []error var errs []error
for i := 0; i < 2; {
loop:
for {
select { select {
case <-fallbackTicker.C: case <-fallbackTicker.C:
if fallback.error == nil && fallback.Conn != nil { if fallback.error == nil && fallback.Conn != nil {
return fallback.Conn, nil return fallback.Conn, nil
} }
case res, ok := <-results: case res := <-results:
if !ok { i++
break loop
}
if res.error == nil { if res.error == nil {
if res.isPrimary { if res.isPrimary {
return res.Conn, nil return res.Conn, nil
@ -242,11 +204,10 @@ loop:
} }
} }
} }
if fallback.error == nil && fallback.Conn != nil { if fallback.error == nil && fallback.Conn != nil {
return fallback.Conn, nil return fallback.Conn, nil
} }
return nil, errors.Join(errs...) return nil, errorsJoin(errs...)
} }
func parallelDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { func parallelDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) {
@ -283,7 +244,7 @@ func parallelDialContext(ctx context.Context, network string, ips []netip.Addr,
} }
if len(errs) > 0 { if len(errs) > 0 {
return nil, errors.Join(errs...) return nil, errorsJoin(errs...)
} }
return nil, os.ErrDeadlineExceeded return nil, os.ErrDeadlineExceeded
} }
@ -300,7 +261,7 @@ func serialDialContext(ctx context.Context, network string, ips []netip.Addr, po
errs = append(errs, err) errs = append(errs, err)
} }
} }
return nil, errors.Join(errs...) return nil, errorsJoin(errs...)
} }
type dialResult struct { type dialResult struct {

View File

@ -2,9 +2,17 @@ package dialer
import ( import (
"errors" "errors"
E "github.com/sagernet/sing/common/exceptions"
) )
var ( var (
ErrorNoIpAddress = errors.New("no ip address") ErrorNoIpAddress = errors.New("no ip address")
ErrorInvalidedNetworkStack = errors.New("invalided network stack") ErrorInvalidedNetworkStack = errors.New("invalided network stack")
) )
func errorsJoin(errs ...error) error {
// compatibility with golang<1.20
// maybe use errors.Join(errs...) is better after we drop the old version's support
return E.Errors(errs...)
}

View File

@ -1,12 +0,0 @@
//go:build !go1.21
package dialer
import (
"net"
)
const multipathTCPAvailable = false
func setMultiPathTCP(dialer *net.Dialer) {
}

View File

@ -1,11 +0,0 @@
//go:build go1.21
package dialer
import "net"
const multipathTCPAvailable = true
func setMultiPathTCP(dialer *net.Dialer) {
dialer.SetMultipathTCP(true)
}

View File

@ -20,13 +20,11 @@ type NetDialer interface {
type option struct { type option struct {
interfaceName string interfaceName string
fallbackBind bool
addrReuse bool addrReuse bool
routingMark int routingMark int
network int network int
prefer int prefer int
tfo bool tfo bool
mpTcp bool
resolver resolver.Resolver resolver resolver.Resolver
netDialer NetDialer netDialer NetDialer
} }
@ -39,12 +37,6 @@ func WithInterface(name string) Option {
} }
} }
func WithFallbackBind(fallback bool) Option {
return func(opt *option) {
opt.fallbackBind = fallback
}
}
func WithAddrReuse(reuse bool) Option { func WithAddrReuse(reuse bool) Option {
return func(opt *option) { return func(opt *option) {
opt.addrReuse = reuse opt.addrReuse = reuse
@ -91,12 +83,6 @@ func WithTFO(tfo bool) Option {
} }
} }
func WithMPTCP(mpTcp bool) Option {
return func(opt *option) {
opt.mpTcp = mpTcp
}
}
func WithNetDialer(netDialer NetDialer) Option { func WithNetDialer(netDialer NetDialer) Option {
return func(opt *option) { return func(opt *option) {
opt.netDialer = netDialer opt.netDialer = netDialer

View File

@ -36,7 +36,7 @@ type Pool struct {
cycle bool cycle bool
mux sync.Mutex mux sync.Mutex
host *trie.DomainTrie[struct{}] host *trie.DomainTrie[struct{}]
ipnet netip.Prefix ipnet *netip.Prefix
store store store store
} }
@ -91,7 +91,7 @@ func (p *Pool) Broadcast() netip.Addr {
} }
// IPNet return raw ipnet // IPNet return raw ipnet
func (p *Pool) IPNet() netip.Prefix { func (p *Pool) IPNet() *netip.Prefix {
return p.ipnet return p.ipnet
} }
@ -153,7 +153,7 @@ func (p *Pool) restoreState() {
} }
type Options struct { type Options struct {
IPNet netip.Prefix IPNet *netip.Prefix
Host *trie.DomainTrie[struct{}] Host *trie.DomainTrie[struct{}]
// Size sets the maximum number of entries in memory // Size sets the maximum number of entries in memory
@ -171,7 +171,7 @@ func New(options Options) (*Pool, error) {
hostAddr = options.IPNet.Masked().Addr() hostAddr = options.IPNet.Masked().Addr()
gateway = hostAddr.Next() gateway = hostAddr.Next()
first = gateway.Next().Next().Next() // default start with 198.18.0.4 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) { if !options.IPNet.IsValid() || !first.IsValid() || !first.Less(last) {

View File

@ -51,7 +51,7 @@ func createCachefileStore(options Options) (*Pool, string, error) {
func TestPool_Basic(t *testing.T) { func TestPool_Basic(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.0/28") ipnet := netip.MustParsePrefix("192.168.0.0/28")
pools, tempfile, err := createPools(Options{ pools, tempfile, err := createPools(Options{
IPNet: ipnet, IPNet: &ipnet,
Size: 10, Size: 10,
}) })
assert.Nil(t, err) assert.Nil(t, err)
@ -79,7 +79,7 @@ func TestPool_Basic(t *testing.T) {
func TestPool_BasicV6(t *testing.T) { func TestPool_BasicV6(t *testing.T) {
ipnet := netip.MustParsePrefix("2001:4860:4860::8888/118") ipnet := netip.MustParsePrefix("2001:4860:4860::8888/118")
pools, tempfile, err := createPools(Options{ pools, tempfile, err := createPools(Options{
IPNet: ipnet, IPNet: &ipnet,
Size: 10, Size: 10,
}) })
assert.Nil(t, err) assert.Nil(t, err)
@ -107,7 +107,7 @@ func TestPool_BasicV6(t *testing.T) {
func TestPool_Case_Insensitive(t *testing.T) { func TestPool_Case_Insensitive(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.1/29") ipnet := netip.MustParsePrefix("192.168.0.1/29")
pools, tempfile, err := createPools(Options{ pools, tempfile, err := createPools(Options{
IPNet: ipnet, IPNet: &ipnet,
Size: 10, Size: 10,
}) })
assert.Nil(t, err) assert.Nil(t, err)
@ -128,7 +128,7 @@ func TestPool_Case_Insensitive(t *testing.T) {
func TestPool_CycleUsed(t *testing.T) { func TestPool_CycleUsed(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.16/28") ipnet := netip.MustParsePrefix("192.168.0.16/28")
pools, tempfile, err := createPools(Options{ pools, tempfile, err := createPools(Options{
IPNet: ipnet, IPNet: &ipnet,
Size: 10, Size: 10,
}) })
assert.Nil(t, err) assert.Nil(t, err)
@ -152,7 +152,7 @@ func TestPool_Skip(t *testing.T) {
tree := trie.New[struct{}]() tree := trie.New[struct{}]()
tree.Insert("example.com", struct{}{}) tree.Insert("example.com", struct{}{})
pools, tempfile, err := createPools(Options{ pools, tempfile, err := createPools(Options{
IPNet: ipnet, IPNet: &ipnet,
Size: 10, Size: 10,
Host: tree, Host: tree,
}) })
@ -168,7 +168,7 @@ func TestPool_Skip(t *testing.T) {
func TestPool_MaxCacheSize(t *testing.T) { func TestPool_MaxCacheSize(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.1/24") ipnet := netip.MustParsePrefix("192.168.0.1/24")
pool, _ := New(Options{ pool, _ := New(Options{
IPNet: ipnet, IPNet: &ipnet,
Size: 2, Size: 2,
}) })
@ -183,7 +183,7 @@ func TestPool_MaxCacheSize(t *testing.T) {
func TestPool_DoubleMapping(t *testing.T) { func TestPool_DoubleMapping(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.1/24") ipnet := netip.MustParsePrefix("192.168.0.1/24")
pool, _ := New(Options{ pool, _ := New(Options{
IPNet: ipnet, IPNet: &ipnet,
Size: 2, Size: 2,
}) })
@ -213,7 +213,7 @@ func TestPool_DoubleMapping(t *testing.T) {
func TestPool_Clone(t *testing.T) { func TestPool_Clone(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.1/24") ipnet := netip.MustParsePrefix("192.168.0.1/24")
pool, _ := New(Options{ pool, _ := New(Options{
IPNet: ipnet, IPNet: &ipnet,
Size: 2, Size: 2,
}) })
@ -223,7 +223,7 @@ func TestPool_Clone(t *testing.T) {
assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 5})) assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 5}))
newPool, _ := New(Options{ newPool, _ := New(Options{
IPNet: ipnet, IPNet: &ipnet,
Size: 2, Size: 2,
}) })
newPool.CloneFrom(pool) newPool.CloneFrom(pool)
@ -236,7 +236,7 @@ func TestPool_Clone(t *testing.T) {
func TestPool_Error(t *testing.T) { func TestPool_Error(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.1/31") ipnet := netip.MustParsePrefix("192.168.0.1/31")
_, err := New(Options{ _, err := New(Options{
IPNet: ipnet, IPNet: &ipnet,
Size: 10, Size: 10,
}) })
@ -246,7 +246,7 @@ func TestPool_Error(t *testing.T) {
func TestPool_FlushFileCache(t *testing.T) { func TestPool_FlushFileCache(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.1/28") ipnet := netip.MustParsePrefix("192.168.0.1/28")
pools, tempfile, err := createPools(Options{ pools, tempfile, err := createPools(Options{
IPNet: ipnet, IPNet: &ipnet,
Size: 10, Size: 10,
}) })
assert.Nil(t, err) assert.Nil(t, err)
@ -278,7 +278,7 @@ func TestPool_FlushFileCache(t *testing.T) {
func TestPool_FlushMemoryCache(t *testing.T) { func TestPool_FlushMemoryCache(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.1/28") ipnet := netip.MustParsePrefix("192.168.0.1/28")
pool, _ := New(Options{ pool, _ := New(Options{
IPNet: ipnet, IPNet: &ipnet,
Size: 10, Size: 10,
}) })

View File

@ -2,22 +2,22 @@ package http
import ( import (
"context" "context"
"crypto/tls"
"io" "io"
"net" "net"
"net/http" "net/http"
URL "net/url" URL "net/url"
"runtime"
"strings" "strings"
"time" "time"
"github.com/Dreamacro/clash/component/ca" "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/listener/inner" "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) { 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) method = strings.ToUpper(method)
urlRes, err := URL.Parse(url) urlRes, err := URL.Parse(url)
if err != nil { if err != nil {
@ -48,7 +48,6 @@ func HttpRequest(ctx context.Context, url, method string, header map[string][]st
transport := &http.Transport{ transport := &http.Transport{
// from http.DefaultTransport // from http.DefaultTransport
DisableKeepAlives: runtime.GOOS == "android",
MaxIdleConns: 100, MaxIdleConns: 100,
IdleConnTimeout: 30 * time.Second, IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * 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) return d.DialContext(ctx, network, address)
} }
}, },
TLSClientConfig: ca.GetGlobalTLSConfig(&tls.Config{}), TLSClientConfig: tls.GetDefaultTLSConfig(),
} }
client := http.Client{Transport: transport} client := http.Client{Transport: transport}

View File

@ -13,7 +13,7 @@ import (
type Interface struct { type Interface struct {
Index int Index int
Name string Name string
Addrs []netip.Prefix Addrs []*netip.Prefix
HardwareAddr net.HardwareAddr HardwareAddr net.HardwareAddr
} }
@ -24,6 +24,8 @@ var (
var interfaces = singledo.NewSingle[map[string]*Interface](time.Second * 20) var interfaces = singledo.NewSingle[map[string]*Interface](time.Second * 20)
const FlagRunning = 32 // interface is in running state, compatibility with golang<1.20
func ResolveInterface(name string) (*Interface, error) { func ResolveInterface(name string) (*Interface, error) {
value, err, _ := interfaces.Do(func() (map[string]*Interface, error) { value, err, _ := interfaces.Do(func() (map[string]*Interface, error) {
ifaces, err := net.Interfaces() ifaces, err := net.Interfaces()
@ -39,11 +41,11 @@ func ResolveInterface(name string) (*Interface, error) {
continue continue
} }
// if not available device like Meta, dummy0, docker0, etc. // if not available device like Meta, dummy0, docker0, etc.
if (iface.Flags&net.FlagMulticast == 0) || (iface.Flags&net.FlagPointToPoint != 0) || (iface.Flags&net.FlagRunning == 0) { if (iface.Flags&net.FlagMulticast == 0) || (iface.Flags&net.FlagPointToPoint != 0) || (iface.Flags&FlagRunning == 0) {
continue continue
} }
ipNets := make([]netip.Prefix, 0, len(addrs)) ipNets := make([]*netip.Prefix, 0, len(addrs))
for _, addr := range addrs { for _, addr := range addrs {
ipNet := addr.(*net.IPNet) ipNet := addr.(*net.IPNet)
ip, _ := netip.AddrFromSlice(ipNet.IP) ip, _ := netip.AddrFromSlice(ipNet.IP)
@ -59,7 +61,7 @@ func ResolveInterface(name string) (*Interface, error) {
} }
pf := netip.PrefixFrom(ip, ones) pf := netip.PrefixFrom(ip, ones)
ipNets = append(ipNets, pf) ipNets = append(ipNets, &pf)
} }
r[iface.Name] = &Interface{ r[iface.Name] = &Interface{
@ -89,27 +91,27 @@ func FlushCache() {
interfaces.Reset() interfaces.Reset()
} }
func (iface *Interface) PickIPv4Addr(destination netip.Addr) (netip.Prefix, error) { func (iface *Interface) PickIPv4Addr(destination netip.Addr) (*netip.Prefix, error) {
return iface.pickIPAddr(destination, func(addr netip.Prefix) bool { return iface.pickIPAddr(destination, func(addr *netip.Prefix) bool {
return addr.Addr().Is4() return addr.Addr().Is4()
}) })
} }
func (iface *Interface) PickIPv6Addr(destination netip.Addr) (netip.Prefix, error) { func (iface *Interface) PickIPv6Addr(destination netip.Addr) (*netip.Prefix, error) {
return iface.pickIPAddr(destination, func(addr netip.Prefix) bool { return iface.pickIPAddr(destination, func(addr *netip.Prefix) bool {
return addr.Addr().Is6() return addr.Addr().Is6()
}) })
} }
func (iface *Interface) pickIPAddr(destination netip.Addr, accept func(addr netip.Prefix) bool) (netip.Prefix, error) { func (iface *Interface) pickIPAddr(destination netip.Addr, accept func(addr *netip.Prefix) bool) (*netip.Prefix, error) {
var fallback netip.Prefix var fallback *netip.Prefix
for _, addr := range iface.Addrs { for _, addr := range iface.Addrs {
if !accept(addr) { if !accept(addr) {
continue continue
} }
if !fallback.IsValid() && !addr.Addr().IsLinkLocalUnicast() { if fallback == nil && !addr.Addr().IsLinkLocalUnicast() {
fallback = addr fallback = addr
if !destination.IsValid() { if !destination.IsValid() {
@ -122,8 +124,8 @@ func (iface *Interface) pickIPAddr(destination netip.Addr, accept func(addr neti
} }
} }
if !fallback.IsValid() { if fallback == nil {
return netip.Prefix{}, ErrAddrNotFound return nil, ErrAddrNotFound
} }
return fallback, nil return fallback, nil

View File

@ -12,68 +12,42 @@ import (
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
"github.com/oschwald/maxminddb-golang" "github.com/oschwald/geoip2-golang"
)
type databaseType = uint8
const (
typeMaxmind databaseType = iota
typeSing
typeMetaV0
) )
var ( var (
reader Reader mmdb *geoip2.Reader
once sync.Once once sync.Once
) )
func LoadFromBytes(buffer []byte) { func LoadFromBytes(buffer []byte) {
once.Do(func() { once.Do(func() {
mmdb, err := maxminddb.FromBytes(buffer) var err error
mmdb, err = geoip2.FromBytes(buffer)
if err != nil { if err != nil {
log.Fatalln("Can't load mmdb: %s", err.Error()) log.Fatalln("Can't load mmdb: %s", err.Error())
} }
reader = Reader{Reader: mmdb}
switch mmdb.Metadata.DatabaseType {
case "sing-geoip":
reader.databaseType = typeSing
case "Meta-geoip0":
reader.databaseType = typeMetaV0
default:
reader.databaseType = typeMaxmind
}
}) })
} }
func Verify() bool { func Verify() bool {
instance, err := maxminddb.Open(C.Path.MMDB()) instance, err := geoip2.Open(C.Path.MMDB())
if err == nil { if err == nil {
instance.Close() instance.Close()
} }
return err == nil return err == nil
} }
func Instance() Reader { func Instance() *geoip2.Reader {
once.Do(func() { once.Do(func() {
mmdbPath := C.Path.MMDB() var err error
log.Debugln("Load MMDB file: %s", mmdbPath) mmdb, err = geoip2.Open(C.Path.MMDB())
mmdb, err := maxminddb.Open(mmdbPath)
if err != nil { if err != nil {
log.Fatalln("Can't load MMDB: %s", err.Error()) log.Fatalln("Can't load mmdb: %s", err.Error())
}
reader = Reader{Reader: mmdb}
switch mmdb.Metadata.DatabaseType {
case "sing-geoip":
reader.databaseType = typeSing
case "Meta-geoip0":
reader.databaseType = typeMetaV0
default:
reader.databaseType = typeMaxmind
} }
}) })
return reader return mmdb
} }
func DownloadMMDB(path string) (err error) { func DownloadMMDB(path string) (err error) {

View File

@ -1,56 +0,0 @@
package mmdb
import (
"fmt"
"net"
"github.com/oschwald/maxminddb-golang"
"github.com/sagernet/sing/common"
)
type geoip2Country struct {
Country struct {
IsoCode string `maxminddb:"iso_code"`
} `maxminddb:"country"`
}
type Reader struct {
*maxminddb.Reader
databaseType
}
func (r Reader) LookupCode(ipAddress net.IP) []string {
switch r.databaseType {
case typeMaxmind:
var country geoip2Country
_ = r.Lookup(ipAddress, &country)
if country.Country.IsoCode == "" {
return []string{}
}
return []string{country.Country.IsoCode}
case typeSing:
var code string
_ = r.Lookup(ipAddress, &code)
if code == "" {
return []string{}
}
return []string{code}
case typeMetaV0:
var record any
_ = r.Lookup(ipAddress, &record)
switch record := record.(type) {
case string:
return []string{record}
case []any: // lookup returned type of slice is []any
return common.Map(record, func(it any) string {
return it.(string)
})
}
return []string{}
default:
panic(fmt.Sprint("unknown geoip database type:", r.databaseType))
}
}

View File

@ -5,28 +5,23 @@ import (
"sync" "sync"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/puzpuzpuz/xsync/v2"
) )
type Table struct { type Table struct {
mapping *xsync.MapOf[string, *Entry] mapping sync.Map
lockMap *xsync.MapOf[string, *sync.Cond]
} }
type Entry struct { type Entry struct {
PacketConn C.PacketConn PacketConn C.PacketConn
WriteBackProxy C.WriteBackProxy WriteBackProxy C.WriteBackProxy
LocalUDPConnMap *xsync.MapOf[string, *net.UDPConn] LocalUDPConnMap sync.Map
LocalLockMap *xsync.MapOf[string, *sync.Cond]
} }
func (t *Table) Set(key string, e C.PacketConn, w C.WriteBackProxy) { func (t *Table) Set(key string, e C.PacketConn, w C.WriteBackProxy) {
t.mapping.Store(key, &Entry{ t.mapping.Store(key, &Entry{
PacketConn: e, PacketConn: e,
WriteBackProxy: w, WriteBackProxy: w,
LocalUDPConnMap: xsync.NewMapOf[*net.UDPConn](), LocalUDPConnMap: sync.Map{},
LocalLockMap: xsync.NewMapOf[*sync.Cond](),
}) })
} }
@ -39,19 +34,15 @@ func (t *Table) Get(key string) (C.PacketConn, C.WriteBackProxy) {
} }
func (t *Table) GetOrCreateLock(key string) (*sync.Cond, bool) { func (t *Table) GetOrCreateLock(key string) (*sync.Cond, bool) {
item, loaded := t.lockMap.LoadOrCompute(key, makeLock) item, loaded := t.mapping.LoadOrStore(key, sync.NewCond(&sync.Mutex{}))
return item, loaded return item.(*sync.Cond), loaded
} }
func (t *Table) Delete(key string) { func (t *Table) Delete(key string) {
t.mapping.Delete(key) t.mapping.Delete(key)
} }
func (t *Table) DeleteLock(lockKey string) { func (t *Table) GetLocalConn(lAddr, rAddr string) *net.UDPConn {
t.lockMap.Delete(lockKey)
}
func (t *Table) GetForLocalConn(lAddr, rAddr string) *net.UDPConn {
entry, exist := t.getEntry(lAddr) entry, exist := t.getEntry(lAddr)
if !exist { if !exist {
return nil return nil
@ -60,10 +51,10 @@ func (t *Table) GetForLocalConn(lAddr, rAddr string) *net.UDPConn {
if !exist { if !exist {
return nil 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) entry, exist := t.getEntry(lAddr)
if !exist { if !exist {
return false return false
@ -72,7 +63,7 @@ func (t *Table) AddForLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool {
return true 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) entry, exist := t.getEntry(lAddr)
if !exist { if !exist {
return return
@ -85,11 +76,11 @@ func (t *Table) GetOrCreateLockForLocalConn(lAddr, key string) (*sync.Cond, bool
if !loaded { if !loaded {
return nil, false return nil, false
} }
item, loaded := entry.LocalLockMap.LoadOrCompute(key, makeLock) item, loaded := entry.LocalUDPConnMap.LoadOrStore(key, sync.NewCond(&sync.Mutex{}))
return item, loaded return item.(*sync.Cond), loaded
} }
func (t *Table) DeleteForLocalConn(lAddr, key string) { func (t *Table) DeleteLocalConnMap(lAddr, key string) {
entry, loaded := t.getEntry(lAddr) entry, loaded := t.getEntry(lAddr)
if !loaded { if !loaded {
return return
@ -97,26 +88,17 @@ func (t *Table) DeleteForLocalConn(lAddr, key string) {
entry.LocalUDPConnMap.Delete(key) 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) { func (t *Table) getEntry(key string) (*Entry, bool) {
return t.mapping.Load(key) item, ok := t.mapping.Load(key)
} // This should not happen usually since this function called after PacketConn created
if !ok {
func makeLock() *sync.Cond { return nil, false
return sync.NewCond(&sync.Mutex{}) }
entry, ok := item.(*Entry)
return entry, ok
} }
// New return *Cache // New return *Cache
func New() *Table { func New() *Table {
return &Table{ return &Table{}
mapping: xsync.NewMapOf[*Entry](),
lockMap: xsync.NewMapOf[*sync.Cond](),
}
} }

View File

@ -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) { 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) return p.listenPacket(ctx, currentMeta)
} }

View File

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

View File

@ -4,7 +4,6 @@ import (
"errors" "errors"
"net/netip" "net/netip"
"strings" "strings"
_ "unsafe"
"github.com/Dreamacro/clash/common/utils" "github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/trie" "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` // Return the search result and whether to match the parameter `isDomain`
func (h *Hosts) Search(domain string, isDomain bool) (*HostValue, bool) { func (h *Hosts) Search(domain string, isDomain bool) (*HostValue, bool) {
if value := h.DomainTrie.Search(domain); value != nil { value := h.DomainTrie.Search(domain)
hostValue := value.Data() if value == nil {
for { return nil, false
if isDomain && hostValue.IsDomain { }
return &hostValue, true 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 { } else {
if node := h.DomainTrie.Search(hostValue.Domain); node != nil { break
hostValue = node.Data()
} else {
break
}
} }
} }
if isDomain == hostValue.IsDomain {
return &hostValue, true
}
return &hostValue, false
} }
if !isDomain { if isDomain == hostValue.IsDomain {
addr, _ := lookupStaticHost(domain) return &hostValue, true
if hostValue, err := NewHostValue(addr); err == nil {
return &hostValue, true
}
} }
return nil, false return &hostValue, false
} }
type HostValue struct { type HostValue struct {

View File

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

View File

@ -9,8 +9,6 @@ import (
types "github.com/Dreamacro/clash/constant/provider" types "github.com/Dreamacro/clash/constant/provider"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
"github.com/samber/lo"
) )
var ( var (
@ -67,7 +65,7 @@ func (f *Fetcher[V]) Initial() (V, error) {
} }
if err != nil { if err != nil {
return lo.Empty[V](), err return getZero[V](), err
} }
var contents V var contents V
@ -87,18 +85,18 @@ func (f *Fetcher[V]) Initial() (V, error) {
if err != nil { if err != nil {
if !isLocal { if !isLocal {
return lo.Empty[V](), err return getZero[V](), err
} }
// parse local file error, fallback to remote // parse local file error, fallback to remote
buf, err = f.vehicle.Read() buf, err = f.vehicle.Read()
if err != nil { if err != nil {
return lo.Empty[V](), err return getZero[V](), err
} }
contents, err = f.parser(buf) contents, err = f.parser(buf)
if err != nil { if err != nil {
return lo.Empty[V](), err return getZero[V](), err
} }
isLocal = false isLocal = false
@ -106,7 +104,7 @@ func (f *Fetcher[V]) Initial() (V, error) {
if f.vehicle.Type() != types.File && !isLocal { if f.vehicle.Type() != types.File && !isLocal {
if err := safeWrite(f.vehicle.Path(), buf); err != nil { 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) { func (f *Fetcher[V]) Update() (V, bool, error) {
buf, err := f.vehicle.Read() buf, err := f.vehicle.Read()
if err != nil { if err != nil {
return lo.Empty[V](), false, err return getZero[V](), false, err
} }
now := time.Now() now := time.Now()
@ -131,17 +129,17 @@ func (f *Fetcher[V]) Update() (V, bool, error) {
if bytes.Equal(f.hash[:], hash[:]) { if bytes.Equal(f.hash[:], hash[:]) {
f.UpdatedAt = &now f.UpdatedAt = &now
_ = os.Chtimes(f.vehicle.Path(), now, now) _ = os.Chtimes(f.vehicle.Path(), now, now)
return lo.Empty[V](), true, nil return getZero[V](), true, nil
} }
contents, err := f.parser(buf) contents, err := f.parser(buf)
if err != nil { if err != nil {
return lo.Empty[V](), false, err return getZero[V](), false, err
} }
if f.vehicle.Type() != types.File { if f.vehicle.Type() != types.File {
if err := safeWrite(f.vehicle.Path(), buf); err != nil { 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, interval: interval,
} }
} }
func getZero[V any]() V {
var result V
return result
}

View File

@ -2,14 +2,12 @@ package resource
import ( import (
"context" "context"
"errors" clashHttp "github.com/Dreamacro/clash/component/http"
types "github.com/Dreamacro/clash/constant/provider"
"io" "io"
"net/http" "net/http"
"os" "os"
"time" "time"
clashHttp "github.com/Dreamacro/clash/component/http"
types "github.com/Dreamacro/clash/constant/provider"
) )
type FileVehicle struct { type FileVehicle struct {
@ -56,10 +54,8 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return nil, errors.New(resp.Status)
}
buf, err := io.ReadAll(resp.Body) buf, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -23,8 +23,8 @@ func (*BaseSniffer) Protocol() string {
return "unknown" return "unknown"
} }
// SniffData implements sniffer.Sniffer // SniffTCP implements sniffer.Sniffer
func (*BaseSniffer) SniffData(bytes []byte) (string, error) { func (*BaseSniffer) SniffTCP(bytes []byte) (string, error) {
return "", errors.New("TODO") return "", errors.New("TODO")
} }

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
"strconv"
"sync" "sync"
"time" "time"
@ -25,55 +26,29 @@ var (
var Dispatcher *SnifferDispatcher var Dispatcher *SnifferDispatcher
type SnifferDispatcher struct { type SnifferDispatcher struct {
enable bool enable bool
sniffers map[sniffer.Sniffer]SnifferConfig sniffers map[sniffer.Sniffer]SnifferConfig
forceDomain *trie.DomainSet forceDomain *trie.DomainSet
skipSNI *trie.DomainSet skipSNI *trie.DomainSet
skipList *cache.LruCache[string, uint8] skipList *cache.LruCache[string, uint8]
rwMux sync.RWMutex rwMux sync.RWMutex
forceDnsMapping bool forceDnsMapping bool
parsePureIp bool parsePureIp bool
} }
func (sd *SnifferDispatcher) shouldOverride(metadata *C.Metadata) bool { func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) {
return (metadata.Host == "" && sd.parsePureIp) || if (metadata.Host == "" && sd.parsePureIp) || sd.forceDomain.Has(metadata.Host) || (metadata.DNSMode == C.DNSMapping && sd.forceDnsMapping) {
sd.forceDomain.Has(metadata.Host) || port, err := strconv.ParseUint(metadata.DstPort, 10, 16)
(metadata.DNSMode == C.DNSMapping && sd.forceDnsMapping) if err != nil {
} log.Debugln("[Sniffer] Dst port is error")
return
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) {
inWhitelist := false inWhitelist := false
overrideDest := false overrideDest := false
for sniffer, config := range sd.sniffers { for sniffer, config := range sd.sniffers {
if sniffer.SupportNetwork() == C.TCP || sniffer.SupportNetwork() == C.ALLNet { if sniffer.SupportNetwork() == C.TCP || sniffer.SupportNetwork() == C.ALLNet {
inWhitelist = sniffer.SupportPort(metadata.DstPort) inWhitelist = sniffer.SupportPort(uint16(port))
if inWhitelist { if inWhitelist {
overrideDest = config.OverrideDest overrideDest = config.OverrideDest
break break
@ -82,26 +57,26 @@ func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata
} }
if !inWhitelist { if !inWhitelist {
return false return
} }
sd.rwMux.RLock() sd.rwMux.RLock()
dst := fmt.Sprintf("%s:%d", metadata.DstIP, metadata.DstPort) dst := fmt.Sprintf("%s:%s", metadata.DstIP, metadata.DstPort)
if count, ok := sd.skipList.Get(dst); ok && count > 5 { if count, ok := sd.skipList.Get(dst); ok && count > 5 {
log.Debugln("[Sniffer] Skip sniffing[%s] due to multiple failures", dst) log.Debugln("[Sniffer] Skip sniffing[%s] due to multiple failures", dst)
defer sd.rwMux.RUnlock() defer sd.rwMux.RUnlock()
return false return
} }
sd.rwMux.RUnlock() sd.rwMux.RUnlock()
if host, err := sd.sniffDomain(conn, metadata); err != nil { if host, err := sd.sniffDomain(conn, metadata); err != nil {
sd.cacheSniffFailed(metadata) 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) log.Debugln("[Sniffer] All sniffing sniff failed with from [%s:%s] to [%s:%s]", metadata.SrcIP, metadata.SrcPort, metadata.String(), metadata.DstPort)
return false return
} else { } else {
if sd.skipSNI.Has(host) { if sd.skipSNI.Has(host) {
log.Debugln("[Sniffer] Skip sni[%s]", host) log.Debugln("[Sniffer] Skip sni[%s]", host)
return false return
} }
sd.rwMux.RLock() sd.rwMux.RLock()
@ -109,24 +84,20 @@ func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata
sd.rwMux.RUnlock() sd.rwMux.RUnlock()
sd.replaceDomain(metadata, host, overrideDest) sd.replaceDomain(metadata, host, overrideDest)
return true
} }
} }
return false
} }
func (sd *SnifferDispatcher) replaceDomain(metadata *C.Metadata, host string, overrideDest bool) { 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 metadata.SniffHost = host
if overrideDest { if overrideDest {
metadata.Host = host metadata.Host = host
} }
metadata.DNSMode = C.DNSNormal 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 { func (sd *SnifferDispatcher) Enable() bool {
@ -157,7 +128,7 @@ func (sd *SnifferDispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metad
continue continue
} }
host, err := s.SniffData(bytes) host, err := s.SniffTCP(bytes)
if err != nil { if err != nil {
//log.Debugln("[Sniffer] [%s] Sniff data failed %s", s.Protocol(), metadata.DstIP) //log.Debugln("[Sniffer] [%s] Sniff data failed %s", s.Protocol(), metadata.DstIP)
continue continue
@ -178,7 +149,7 @@ func (sd *SnifferDispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metad
func (sd *SnifferDispatcher) cacheSniffFailed(metadata *C.Metadata) { func (sd *SnifferDispatcher) cacheSniffFailed(metadata *C.Metadata) {
sd.rwMux.Lock() sd.rwMux.Lock()
dst := fmt.Sprintf("%s:%d", metadata.DstIP, metadata.DstPort) dst := fmt.Sprintf("%s:%s", metadata.DstIP, metadata.DstPort)
count, _ := sd.skipList.Get(dst) count, _ := sd.skipList.Get(dst)
if count <= 5 { if count <= 5 {
count++ count++
@ -226,8 +197,6 @@ func NewSniffer(name sniffer.Type, snifferConfig SnifferConfig) (sniffer.Sniffer
return NewTLSSniffer(snifferConfig) return NewTLSSniffer(snifferConfig)
case sniffer.HTTP: case sniffer.HTTP:
return NewHTTPSniffer(snifferConfig) return NewHTTPSniffer(snifferConfig)
case sniffer.QUIC:
return NewQuicSniffer(snifferConfig)
default: default:
return nil, ErrorUnsupportedSniffer return nil, ErrorUnsupportedSniffer
} }

View File

@ -58,7 +58,7 @@ func (http *HTTPSniffer) SupportNetwork() C.NetWork {
return C.TCP return C.TCP
} }
func (http *HTTPSniffer) SniffData(bytes []byte) (string, error) { func (http *HTTPSniffer) SniffTCP(bytes []byte) (string, error) {
domain, err := SniffHTTP(bytes) domain, err := SniffHTTP(bytes)
if err == nil { if err == nil {
return *domain, nil return *domain, nil

View File

@ -1,287 +1,3 @@
package sniffer package sniffer
import ( //TODO
"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
}

View File

@ -1,40 +1,9 @@
package sniffer package sniffer
import ( import (
"bytes"
"encoding/hex"
"github.com/stretchr/testify/assert"
"testing" "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) { func TestTLSHeaders(t *testing.T) {
cases := []struct { cases := []struct {
input []byte input []byte
@ -173,7 +142,6 @@ func TestTLSHeaders(t *testing.T) {
} }
for _, test := range cases { for _, test := range cases {
input := bytes.Clone(test.input)
domain, err := SniffTLS(test.input) domain, err := SniffTLS(test.input)
if test.err { if test.err {
if err == nil { if err == nil {
@ -187,6 +155,5 @@ func TestTLSHeaders(t *testing.T) {
t.Error("expect domain ", test.domain, " but got ", domain) t.Error("expect domain ", test.domain, " but got ", domain)
} }
} }
assert.Equal(t, input, test.input)
} }
} }

View File

@ -39,7 +39,7 @@ func (tls *TLSSniffer) SupportNetwork() C.NetWork {
return C.TCP return C.TCP
} }
func (tls *TLSSniffer) SniffData(bytes []byte) (string, error) { func (tls *TLSSniffer) SniffTCP(bytes []byte) (string, error) {
domain, err := SniffTLS(bytes) domain, err := SniffTLS(bytes)
if err == nil { if err == nil {
return *domain, nil return *domain, nil

View File

@ -1,4 +1,4 @@
package ca package tls
import ( import (
"bytes" "bytes"
@ -8,13 +8,14 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"os"
"strings" "strings"
"sync" "sync"
xtls "github.com/xtls/go"
) )
var trustCerts []*x509.Certificate var trustCerts []*x509.Certificate
var globalCertPool *x509.CertPool var certPool *x509.CertPool
var mutex sync.RWMutex var mutex sync.RWMutex
var errNotMatch = errors.New("certificate fingerprints do not match") var errNotMatch = errors.New("certificate fingerprints do not match")
@ -34,12 +35,12 @@ func AddCertificate(certificate string) error {
func initializeCertPool() { func initializeCertPool() {
var err error var err error
globalCertPool, err = x509.SystemCertPool() certPool, err = x509.SystemCertPool()
if err != nil { if err != nil {
globalCertPool = x509.NewCertPool() certPool = x509.NewCertPool()
} }
for _, cert := range trustCerts { for _, cert := range trustCerts {
globalCertPool.AddCert(cert) certPool.AddCert(cert)
} }
} }
@ -54,15 +55,15 @@ func getCertPool() *x509.CertPool {
if len(trustCerts) == 0 { if len(trustCerts) == 0 {
return nil return nil
} }
if globalCertPool == nil { if certPool == nil {
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
if globalCertPool != nil { if certPool != nil {
return globalCertPool return certPool
} }
initializeCertPool() initializeCertPool()
} }
return globalCertPool return certPool
} }
func verifyFingerprint(fingerprint *[32]byte) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { func verifyFingerprint(fingerprint *[32]byte) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
@ -95,49 +96,53 @@ func convertFingerprint(fingerprint string) (*[32]byte, error) {
return (*[32]byte)(fpByte), nil return (*[32]byte)(fpByte), nil
} }
// GetTLSConfig specified fingerprint, customCA and customCAString func GetDefaultTLSConfig() *tls.Config {
func GetTLSConfig(tlsConfig *tls.Config, fingerprint string, customCA string, customCAString string) (*tls.Config, error) { return GetGlobalTLSConfig(nil)
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
} }
// GetSpecifiedFingerprintTLSConfig specified fingerprint // GetSpecifiedFingerprintTLSConfig specified fingerprint
func GetSpecifiedFingerprintTLSConfig(tlsConfig *tls.Config, fingerprint string) (*tls.Config, error) { 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 { 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
}
// GetSpecifiedFingerprintXTLSConfig specified fingerprint
func GetSpecifiedFingerprintXTLSConfig(tlsConfig *xtls.Config, fingerprint string) (*xtls.Config, error) {
if fingerprintBytes, err := convertFingerprint(fingerprint); err != nil {
return nil, err
} else {
tlsConfig = GetGlobalXTLSConfig(tlsConfig)
tlsConfig.VerifyPeerCertificate = verifyFingerprint(fingerprintBytes)
tlsConfig.InsecureSkipVerify = true
return tlsConfig, nil
}
}
func GetGlobalXTLSConfig(tlsConfig *xtls.Config) *xtls.Config {
certPool := getCertPool()
if tlsConfig == nil {
return &xtls.Config{
RootCAs: certPool,
}
}
tlsConfig.RootCAs = certPool
return tlsConfig return tlsConfig
} }

View File

@ -22,7 +22,6 @@ import (
"github.com/Dreamacro/clash/common/utils" "github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/ntp"
utls "github.com/sagernet/utls" utls "github.com/sagernet/utls"
"github.com/zhangyunhao116/fastrand" "github.com/zhangyunhao116/fastrand"
@ -43,8 +42,7 @@ type RealityConfig struct {
func aesgcmPreferred(ciphers []uint16) bool func aesgcmPreferred(ciphers []uint16) bool
func GetRealityConn(ctx context.Context, conn net.Conn, ClientFingerprint string, tlsConfig *tls.Config, realityConfig *RealityConfig) (net.Conn, error) { func GetRealityConn(ctx context.Context, conn net.Conn, ClientFingerprint string, tlsConfig *tls.Config, realityConfig *RealityConfig) (net.Conn, error) {
retry := 0 if fingerprint, exists := GetFingerprint(ClientFingerprint); exists {
for fingerprint, exists := GetFingerprint(ClientFingerprint); exists; retry++ {
verifier := &realityVerifier{ verifier := &realityVerifier{
serverName: tlsConfig.ServerName, serverName: tlsConfig.ServerName,
} }
@ -72,7 +70,7 @@ func GetRealityConn(ctx context.Context, conn net.Conn, ClientFingerprint string
rawSessionID[i] = 0 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[:]) copy(hello.SessionId[8:], realityConfig.ShortID[:])
hello.SessionId[0] = 1 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]) //log.Debugln("REALITY hello.sessionId[:16]: %v", hello.SessionId[:16])
ecdheParams := uConn.HandshakeState.State13.EcdheParams authKey := uConn.HandshakeState.State13.EcdheParams.SharedKey(realityConfig.PublicKey[:])
if ecdheParams == nil {
// WTF???
if retry > 2 {
return nil, errors.New("nil ecdheParams")
}
continue // retry
}
authKey := ecdheParams.SharedKey(realityConfig.PublicKey[:])
if authKey == nil { if authKey == nil {
return nil, errors.New("nil auth_key") return nil, errors.New("nil auth_key")
} }

View File

@ -21,7 +21,7 @@ type UClientHelloID struct {
var initRandomFingerprint UClientHelloID var initRandomFingerprint UClientHelloID
var initUtlsClient string 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{ utlsConn := utls.UClient(c, copyConfig(config), utls.ClientHelloID{
Client: fingerprint.Client, Client: fingerprint.Client,
Version: fingerprint.Version, 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 // 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 // Build the handshake state. This will apply every variable of the TLS of the
// fingerprint in the UConn // fingerprint in the UConn
if err := c.BuildHandshakeState(); err != nil { if err := c.BuildHandshakeState(); err != nil {
@ -119,11 +120,11 @@ func (c *UConn) BuildWebsocketHandshakeState() error {
if !hasALPNExtension { // Append extension if doesn't exists if !hasALPNExtension { // Append extension if doesn't exists
c.Extensions = append(c.Extensions, &utls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}}) 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 { if err := c.BuildHandshakeState(); err != nil {
return err return err
} }
return nil return c.Handshake()
} }
func SetGlobalUtlsClient(Client string) { func SetGlobalUtlsClient(Client string) {

View File

@ -8,7 +8,6 @@ import (
"net/netip" "net/netip"
"net/url" "net/url"
"os" "os"
"path"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@ -17,9 +16,9 @@ import (
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapter/outboundgroup" "github.com/Dreamacro/clash/adapter/outboundgroup"
"github.com/Dreamacro/clash/adapter/provider" "github.com/Dreamacro/clash/adapter/provider"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/utils" "github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/auth" "github.com/Dreamacro/clash/component/auth"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/fakeip" "github.com/Dreamacro/clash/component/fakeip"
"github.com/Dreamacro/clash/component/geodata" "github.com/Dreamacro/clash/component/geodata"
"github.com/Dreamacro/clash/component/geodata/router" "github.com/Dreamacro/clash/component/geodata/router"
@ -52,7 +51,6 @@ type General struct {
IPv6 bool `json:"ipv6"` IPv6 bool `json:"ipv6"`
Interface string `json:"interface-name"` Interface string `json:"interface-name"`
RoutingMark int `json:"-"` RoutingMark int `json:"-"`
GeoXUrl GeoXUrl `json:"geox-url"`
GeodataMode bool `json:"geodata-mode"` GeodataMode bool `json:"geodata-mode"`
GeodataLoader string `json:"geodata-loader"` GeodataLoader string `json:"geodata-loader"`
TCPConcurrent bool `json:"tcp-concurrent"` TCPConcurrent bool `json:"tcp-concurrent"`
@ -60,26 +58,23 @@ type General struct {
Sniffing bool `json:"sniffing"` Sniffing bool `json:"sniffing"`
EBpf EBpf `json:"-"` EBpf EBpf `json:"-"`
GlobalClientFingerprint string `json:"global-client-fingerprint"` GlobalClientFingerprint string `json:"global-client-fingerprint"`
GlobalUA string `json:"global-ua"`
} }
// Inbound config // Inbound config
type Inbound struct { type Inbound struct {
Port int `json:"port"` Port int `json:"port"`
SocksPort int `json:"socks-port"` SocksPort int `json:"socks-port"`
RedirPort int `json:"redir-port"` RedirPort int `json:"redir-port"`
TProxyPort int `json:"tproxy-port"` TProxyPort int `json:"tproxy-port"`
MixedPort int `json:"mixed-port"` MixedPort int `json:"mixed-port"`
Tun LC.Tun `json:"tun"` Tun LC.Tun `json:"tun"`
TuicServer LC.TuicServer `json:"tuic-server"` TuicServer LC.TuicServer `json:"tuic-server"`
ShadowSocksConfig string `json:"ss-config"` ShadowSocksConfig string `json:"ss-config"`
VmessConfig string `json:"vmess-config"` VmessConfig string `json:"vmess-config"`
Authentication []string `json:"authentication"` Authentication []string `json:"authentication"`
SkipAuthPrefixes []netip.Prefix `json:"skip-auth-prefixes"` AllowLan bool `json:"allow-lan"`
AllowLan bool `json:"allow-lan"` BindAddress string `json:"bind-address"`
BindAddress string `json:"bind-address"` InboundTfo bool `json:"inbound-tfo"`
InboundTfo bool `json:"inbound-tfo"`
InboundMPTCP bool `json:"inbound-mptcp"`
} }
// Controller config // Controller config
@ -90,16 +85,6 @@ type Controller struct {
Secret string `json:"-"` 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 // DNS config
type DNS struct { type DNS struct {
Enable bool `yaml:"enable"` Enable bool `yaml:"enable"`
@ -122,7 +107,7 @@ type DNS struct {
type FallbackFilter struct { type FallbackFilter struct {
GeoIP bool `yaml:"geoip"` GeoIP bool `yaml:"geoip"`
GeoIPCode string `yaml:"geoip-code"` GeoIPCode string `yaml:"geoip-code"`
IPCIDR []netip.Prefix `yaml:"ipcidr"` IPCIDR []*netip.Prefix `yaml:"ipcidr"`
Domain []string `yaml:"domain"` Domain []string `yaml:"domain"`
GeoSite []*router.DomainMatcher `yaml:"geosite"` GeoSite []*router.DomainMatcher `yaml:"geosite"`
} }
@ -157,16 +142,13 @@ type Sniffer struct {
// Experimental config // Experimental config
type Experimental struct { type Experimental struct {
Fingerprints []string `yaml:"fingerprints"` Fingerprints []string `yaml:"fingerprints"`
QUICGoDisableGSO bool `yaml:"quic-go-disable-gso"`
QUICGoDisableECN bool `yaml:"quic-go-disable-ecn"`
} }
// Config is clash config manager // Config is clash config manager
type Config struct { type Config struct {
General *General General *General
IPTables *IPTables IPTables *IPTables
NTP *NTP
DNS *DNS DNS *DNS
Experimental *Experimental Experimental *Experimental
Hosts *trie.DomainTrie[resolver.HostValue] Hosts *trie.DomainTrie[resolver.HostValue]
@ -183,15 +165,6 @@ type Config struct {
TLS *TLS 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 { type RawDNS struct {
Enable bool `yaml:"enable"` Enable bool `yaml:"enable"`
PreferH3 bool `yaml:"prefer-h3"` PreferH3 bool `yaml:"prefer-h3"`
@ -228,23 +201,21 @@ type RawTun struct {
RedirectToTun []string `yaml:"-" json:"-"` RedirectToTun []string `yaml:"-" json:"-"`
MTU uint32 `yaml:"mtu" json:"mtu,omitempty"` MTU uint32 `yaml:"mtu" json:"mtu,omitempty"`
//Inet4Address []netip.Prefix `yaml:"inet4-address" json:"inet4_address,omitempty"` //Inet4Address []LC.ListenPrefix `yaml:"inet4-address" json:"inet4_address,omitempty"`
Inet6Address []netip.Prefix `yaml:"inet6-address" json:"inet6_address,omitempty"` Inet6Address []LC.ListenPrefix `yaml:"inet6-address" json:"inet6_address,omitempty"`
StrictRoute bool `yaml:"strict-route" json:"strict_route,omitempty"` StrictRoute bool `yaml:"strict-route" json:"strict_route,omitempty"`
Inet4RouteAddress []netip.Prefix `yaml:"inet4-route-address" json:"inet4_route_address,omitempty"` Inet4RouteAddress []LC.ListenPrefix `yaml:"inet4_route_address" json:"inet4_route_address,omitempty"`
Inet6RouteAddress []netip.Prefix `yaml:"inet6-route-address" json:"inet6_route_address,omitempty"` Inet6RouteAddress []LC.ListenPrefix `yaml:"inet6_route_address" json:"inet6_route_address,omitempty"`
Inet4RouteExcludeAddress []netip.Prefix `yaml:"inet4-route-exclude-address" json:"inet4_route_exclude_address,omitempty"` IncludeUID []uint32 `yaml:"include-uid" json:"include_uid,omitempty"`
Inet6RouteExcludeAddress []netip.Prefix `yaml:"inet6-route-exclude-address" json:"inet6_route_exclude_address,omitempty"` IncludeUIDRange []string `yaml:"include-uid-range" json:"include_uid_range,omitempty"`
IncludeUID []uint32 `yaml:"include-uid" json:"include_uid,omitempty"` ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude_uid,omitempty"`
IncludeUIDRange []string `yaml:"include-uid-range" json:"include_uid_range,omitempty"` ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude_uid_range,omitempty"`
ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude_uid,omitempty"` IncludeAndroidUser []int `yaml:"include-android-user" json:"include_android_user,omitempty"`
ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude_uid_range,omitempty"` IncludePackage []string `yaml:"include-package" json:"include_package,omitempty"`
IncludeAndroidUser []int `yaml:"include-android-user" json:"include_android_user,omitempty"` ExcludePackage []string `yaml:"exclude-package" json:"exclude_package,omitempty"`
IncludePackage []string `yaml:"include-package" json:"include_package,omitempty"` EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint_independent_nat,omitempty"`
ExcludePackage []string `yaml:"exclude-package" json:"exclude_package,omitempty"` UDPTimeout int64 `yaml:"udp-timeout" json:"udp_timeout,omitempty"`
EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint_independent_nat,omitempty"` FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"`
UDPTimeout int64 `yaml:"udp-timeout" json:"udp_timeout,omitempty"`
FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"`
} }
type RawTuicServer struct { type RawTuicServer struct {
@ -271,9 +242,7 @@ type RawConfig struct {
ShadowSocksConfig string `yaml:"ss-config"` ShadowSocksConfig string `yaml:"ss-config"`
VmessConfig string `yaml:"vmess-config"` VmessConfig string `yaml:"vmess-config"`
InboundTfo bool `yaml:"inbound-tfo"` InboundTfo bool `yaml:"inbound-tfo"`
InboundMPTCP bool `yaml:"inbound-mptcp"`
Authentication []string `yaml:"authentication"` Authentication []string `yaml:"authentication"`
SkipAuthPrefixes []netip.Prefix `yaml:"skip-auth-prefixes"`
AllowLan bool `yaml:"allow-lan"` AllowLan bool `yaml:"allow-lan"`
BindAddress string `yaml:"bind-address"` BindAddress string `yaml:"bind-address"`
Mode T.TunnelMode `yaml:"mode"` Mode T.TunnelMode `yaml:"mode"`
@ -283,8 +252,6 @@ type RawConfig struct {
ExternalController string `yaml:"external-controller"` ExternalController string `yaml:"external-controller"`
ExternalControllerTLS string `yaml:"external-controller-tls"` ExternalControllerTLS string `yaml:"external-controller-tls"`
ExternalUI string `yaml:"external-ui"` 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"` Secret string `yaml:"secret"`
Interface string `yaml:"interface-name"` Interface string `yaml:"interface-name"`
RoutingMark int `yaml:"routing-mark"` RoutingMark int `yaml:"routing-mark"`
@ -294,14 +261,11 @@ type RawConfig struct {
TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"` TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"`
FindProcessMode P.FindProcessMode `yaml:"find-process-mode" json:"find-process-mode"` FindProcessMode P.FindProcessMode `yaml:"find-process-mode" json:"find-process-mode"`
GlobalClientFingerprint string `yaml:"global-client-fingerprint"` GlobalClientFingerprint string `yaml:"global-client-fingerprint"`
GlobalUA string `yaml:"global-ua"`
KeepAliveInterval int `yaml:"keep-alive-interval"`
Sniffer RawSniffer `yaml:"sniffer"` Sniffer RawSniffer `yaml:"sniffer"`
ProxyProvider map[string]map[string]any `yaml:"proxy-providers"` ProxyProvider map[string]map[string]any `yaml:"proxy-providers"`
RuleProvider map[string]map[string]any `yaml:"rule-providers"` RuleProvider map[string]map[string]any `yaml:"rule-providers"`
Hosts map[string]any `yaml:"hosts"` Hosts map[string]any `yaml:"hosts"`
NTP RawNTP `yaml:"ntp"`
DNS RawDNS `yaml:"dns"` DNS RawDNS `yaml:"dns"`
Tun RawTun `yaml:"tun"` Tun RawTun `yaml:"tun"`
TuicServer RawTuicServer `yaml:"tuic-server"` TuicServer RawTuicServer `yaml:"tuic-server"`
@ -309,7 +273,7 @@ type RawConfig struct {
IPTables IPTables `yaml:"iptables"` IPTables IPTables `yaml:"iptables"`
Experimental Experimental `yaml:"experimental"` Experimental Experimental `yaml:"experimental"`
Profile Profile `yaml:"profile"` Profile Profile `yaml:"profile"`
GeoXUrl GeoXUrl `yaml:"geox-url"` GeoXUrl RawGeoXUrl `yaml:"geox-url"`
Proxy []map[string]any `yaml:"proxies"` Proxy []map[string]any `yaml:"proxies"`
ProxyGroup []map[string]any `yaml:"proxy-groups"` ProxyGroup []map[string]any `yaml:"proxy-groups"`
Rule []string `yaml:"rules"` Rule []string `yaml:"rules"`
@ -318,7 +282,7 @@ type RawConfig struct {
Listeners []map[string]any `yaml:"listeners"` Listeners []map[string]any `yaml:"listeners"`
} }
type GeoXUrl struct { type RawGeoXUrl struct {
GeoIp string `yaml:"geoip" json:"geoip"` GeoIp string `yaml:"geoip" json:"geoip"`
Mmdb string `yaml:"mmdb" json:"mmdb"` Mmdb string `yaml:"mmdb" json:"mmdb"`
GeoSite string `yaml:"geosite" json:"geosite"` GeoSite string `yaml:"geosite" json:"geosite"`
@ -381,7 +345,6 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
ProxyGroup: []map[string]any{}, ProxyGroup: []map[string]any{},
TCPConcurrent: false, TCPConcurrent: false,
FindProcessMode: P.FindProcessStrict, FindProcessMode: P.FindProcessStrict,
GlobalUA: "clash.meta",
Tun: RawTun{ Tun: RawTun{
Enable: false, Enable: false,
Device: "", Device: "",
@ -389,7 +352,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query
AutoRoute: true, AutoRoute: true,
AutoDetectInterface: 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{ TuicServer: RawTuicServer{
Enable: false, Enable: false,
@ -413,13 +376,6 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
InboundInterface: "lo", InboundInterface: "lo",
Bypass: []string{}, Bypass: []string{},
}, },
NTP: RawNTP{
Enable: false,
WriteToSystem: false,
Server: "time.apple.com",
ServerPort: 123,
Interval: 30,
},
DNS: RawDNS{ DNS: RawDNS{
Enable: false, Enable: false,
IPv6: false, IPv6: false,
@ -462,12 +418,11 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
Profile: Profile{ Profile: Profile{
StoreSelected: true, StoreSelected: true,
}, },
GeoXUrl: GeoXUrl{ GeoXUrl: RawGeoXUrl{
Mmdb: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb", Mmdb: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/country.mmdb",
GeoIp: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat", GeoIp: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat",
GeoSite: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat", GeoSite: "https://testingcf.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 { if err := yaml.Unmarshal(buf, rawCfg); err != nil {
@ -493,7 +448,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
config.General = general config.General = general
if len(config.General.GlobalClientFingerprint) != 0 { if len(config.General.GlobalClientFingerprint) != 0 {
log.Debugln("GlobalClientFingerprint: %s", config.General.GlobalClientFingerprint) log.Debugln("GlobalClientFingerprint:%s", config.General.GlobalClientFingerprint)
tlsC.SetGlobalUtlsClient(config.General.GlobalClientFingerprint) tlsC.SetGlobalUtlsClient(config.General.GlobalClientFingerprint)
} }
@ -535,9 +490,6 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
} }
config.Hosts = hosts config.Hosts = hosts
ntpCfg := paresNTP(rawCfg)
config.NTP = ntpCfg
dnsCfg, err := parseDNS(rawCfg, hosts, rules, ruleProviders) dnsCfg, err := parseDNS(rawCfg, hosts, rules, ruleProviders)
if err != nil { if err != nil {
return nil, err return nil, err
@ -578,40 +530,15 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
} }
func parseGeneral(cfg *RawConfig) (*General, error) { func parseGeneral(cfg *RawConfig) (*General, error) {
externalUI := cfg.ExternalUI
geodata.SetLoader(cfg.GeodataLoader) 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 // checkout externalUI exist
if ExternalUIPath != "" { if externalUI != "" {
ExternalUIPath = C.Path.Resolve(ExternalUIPath) externalUI = C.Path.Resolve(externalUI)
if _, err := os.Stat(ExternalUIPath); os.IsNotExist(err) { if _, err := os.Stat(externalUI); os.IsNotExist(err) {
defaultUIpath := path.Join(C.Path.HomeDir(), "ui") return nil, fmt.Errorf("external-ui: %s not exist", externalUI)
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
} }
} }
// 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 cfg.Tun.RedirectToTun = cfg.EBpf.RedirectToTun
return &General{ return &General{
Inbound: Inbound{ Inbound: Inbound{
@ -623,10 +550,8 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
ShadowSocksConfig: cfg.ShadowSocksConfig, ShadowSocksConfig: cfg.ShadowSocksConfig,
VmessConfig: cfg.VmessConfig, VmessConfig: cfg.VmessConfig,
AllowLan: cfg.AllowLan, AllowLan: cfg.AllowLan,
SkipAuthPrefixes: cfg.SkipAuthPrefixes,
BindAddress: cfg.BindAddress, BindAddress: cfg.BindAddress,
InboundTfo: cfg.InboundTfo, InboundTfo: cfg.InboundTfo,
InboundMPTCP: cfg.InboundMPTCP,
}, },
Controller: Controller{ Controller: Controller{
ExternalController: cfg.ExternalController, ExternalController: cfg.ExternalController,
@ -640,14 +565,12 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
IPv6: cfg.IPv6, IPv6: cfg.IPv6,
Interface: cfg.Interface, Interface: cfg.Interface,
RoutingMark: cfg.RoutingMark, RoutingMark: cfg.RoutingMark,
GeoXUrl: cfg.GeoXUrl,
GeodataMode: cfg.GeodataMode, GeodataMode: cfg.GeodataMode,
GeodataLoader: cfg.GeodataLoader, GeodataLoader: cfg.GeodataLoader,
TCPConcurrent: cfg.TCPConcurrent, TCPConcurrent: cfg.TCPConcurrent,
FindProcessMode: cfg.FindProcessMode, FindProcessMode: cfg.FindProcessMode,
EBpf: cfg.EBpf, EBpf: cfg.EBpf,
GlobalClientFingerprint: cfg.GlobalClientFingerprint, GlobalClientFingerprint: cfg.GlobalClientFingerprint,
GlobalUA: cfg.GlobalUA,
}, nil }, nil
} }
@ -789,9 +712,6 @@ func parseRuleProviders(cfg *RawConfig) (ruleProviders map[string]providerTypes.
func parseSubRules(cfg *RawConfig, proxies map[string]C.Proxy) (subRules map[string][]C.Rule, err error) { func parseSubRules(cfg *RawConfig, proxies map[string]C.Proxy) (subRules map[string][]C.Rule, err error) {
subRules = map[string][]C.Rule{} subRules = map[string][]C.Rule{}
for name := range cfg.SubRules {
subRules[name] = make([]C.Rule, 0)
}
for name, rawRules := range cfg.SubRules { for name, rawRules := range cfg.SubRules {
if len(name) == 0 { if len(name) == 0 {
return nil, fmt.Errorf("sub-rule name is empty") return nil, fmt.Errorf("sub-rule name is empty")
@ -1049,6 +969,7 @@ func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error)
Net: dnsNetType, Net: dnsNetType,
Addr: addr, Addr: addr,
ProxyName: proxyName, ProxyName: proxyName,
Interface: dialer.DefaultInterface,
Params: params, Params: params,
PreferH3: preferH3, PreferH3: preferH3,
}, },
@ -1150,15 +1071,15 @@ func parseNameServerPolicy(nsPolicy map[string]any, ruleProviders map[string]pro
return policy, nil return policy, nil
} }
func parseFallbackIPCIDR(ips []string) ([]netip.Prefix, error) { func parseFallbackIPCIDR(ips []string) ([]*netip.Prefix, error) {
var ipNets []netip.Prefix var ipNets []*netip.Prefix
for idx, ip := range ips { for idx, ip := range ips {
ipnet, err := netip.ParsePrefix(ip) ipnet, err := netip.ParsePrefix(ip)
if err != nil { if err != nil {
return nil, fmt.Errorf("DNS FallbackIP[%d] format error: %s", idx, err.Error()) return nil, fmt.Errorf("DNS FallbackIP[%d] format error: %s", idx, err.Error())
} }
ipNets = append(ipNets, ipnet) ipNets = append(ipNets, &ipnet)
} }
return ipNets, nil return ipNets, nil
@ -1199,19 +1120,6 @@ func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainM
return sites, nil 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) { func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rules []C.Rule, ruleProviders map[string]providerTypes.RuleProvider) (*DNS, error) {
cfg := rawCfg.DNS cfg := rawCfg.DNS
if cfg.Enable && len(cfg.NameServer) == 0 { if cfg.Enable && len(cfg.NameServer) == 0 {
@ -1226,7 +1134,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rul
IPv6: cfg.IPv6, IPv6: cfg.IPv6,
EnhancedMode: cfg.EnhancedMode, EnhancedMode: cfg.EnhancedMode,
FallbackFilter: FallbackFilter{ FallbackFilter: FallbackFilter{
IPCIDR: []netip.Prefix{}, IPCIDR: []*netip.Prefix{},
GeoSite: []*router.DomainMatcher{}, GeoSite: []*router.DomainMatcher{},
}, },
} }
@ -1300,7 +1208,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rul
} }
pool, err := fakeip.New(fakeip.Options{ pool, err := fakeip.New(fakeip.Options{
IPNet: fakeIPRange, IPNet: &fakeIPRange,
Size: 1000, Size: 1000,
Host: host, Host: host,
Persistence: rawCfg.Profile.StoreFakeIP, Persistence: rawCfg.Profile.StoreFakeIP,
@ -1363,24 +1271,22 @@ func parseTun(rawTun RawTun, general *General) error {
AutoDetectInterface: rawTun.AutoDetectInterface, AutoDetectInterface: rawTun.AutoDetectInterface,
RedirectToTun: rawTun.RedirectToTun, RedirectToTun: rawTun.RedirectToTun,
MTU: rawTun.MTU, MTU: rawTun.MTU,
Inet4Address: []netip.Prefix{tunAddressPrefix}, Inet4Address: []LC.ListenPrefix{LC.ListenPrefix(tunAddressPrefix)},
Inet6Address: rawTun.Inet6Address, Inet6Address: rawTun.Inet6Address,
StrictRoute: rawTun.StrictRoute, StrictRoute: rawTun.StrictRoute,
Inet4RouteAddress: rawTun.Inet4RouteAddress, Inet4RouteAddress: rawTun.Inet4RouteAddress,
Inet6RouteAddress: rawTun.Inet6RouteAddress, Inet6RouteAddress: rawTun.Inet6RouteAddress,
Inet4RouteExcludeAddress: rawTun.Inet4RouteExcludeAddress, IncludeUID: rawTun.IncludeUID,
Inet6RouteExcludeAddress: rawTun.Inet6RouteExcludeAddress, IncludeUIDRange: rawTun.IncludeUIDRange,
IncludeUID: rawTun.IncludeUID, ExcludeUID: rawTun.ExcludeUID,
IncludeUIDRange: rawTun.IncludeUIDRange, ExcludeUIDRange: rawTun.ExcludeUIDRange,
ExcludeUID: rawTun.ExcludeUID, IncludeAndroidUser: rawTun.IncludeAndroidUser,
ExcludeUIDRange: rawTun.ExcludeUIDRange, IncludePackage: rawTun.IncludePackage,
IncludeAndroidUser: rawTun.IncludeAndroidUser, ExcludePackage: rawTun.ExcludePackage,
IncludePackage: rawTun.IncludePackage, EndpointIndependentNat: rawTun.EndpointIndependentNat,
ExcludePackage: rawTun.ExcludePackage, UDPTimeout: rawTun.UDPTimeout,
EndpointIndependentNat: rawTun.EndpointIndependentNat, FileDescriptor: rawTun.FileDescriptor,
UDPTimeout: rawTun.UDPTimeout,
FileDescriptor: rawTun.FileDescriptor,
} }
return nil return nil

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