Compare commits

..

55 Commits

Author SHA1 Message Date
3610d3dd84 Feature: add path to script config
script:
    path: ./script.star
2022-06-06 05:20:59 +08:00
663017a775 Feature: add match_provider to script shortcuts 2022-06-06 04:35:52 +08:00
05f0c1060b Refactor: script auto load geosite as a rule provider 2022-06-06 04:28:54 +08:00
e03f1d0565 Chore: merge branch 'with-tun' into plus-pro 2022-06-06 03:22:48 +08:00
c1821e28d3 Refactor: load geo domain matcher 2022-06-06 03:13:10 +08:00
763929997b Chore: code style 2022-06-06 02:37:10 +08:00
c8e2b30540 Chore: update build 2022-06-04 02:24:15 +08:00
dd95d335d9 Chore: merge branch 'with-tun' into plus-pro 2022-06-04 01:35:18 +08:00
bf9eb000d2 Chore: update dependencies 2022-06-03 23:53:58 +08:00
0563abae13 Chore: update build 2022-06-03 23:50:30 +08:00
3dbba5d8d2 Chore: mix the proxy adapter and interface to dns client 2022-06-03 11:27:41 +08:00
a4d135ed21 Feature: add regexp filter to use proxy provider in proxy group 2022-06-03 05:09:43 +08:00
af5bd0f65e Feature: add custom request header to proxy provider
`header` value is type of string array
header:
  Accept:
    - 'application/vnd.github.v3.raw'
  Authorization:
    - ' token xxxxxxxxxxx'
  User-Agent:
    - 'Clash/v1.10.6'

`prefix-name` add a prefix to proxy name
prefix-name: 'XXX-'
2022-06-03 05:09:43 +08:00
8ed868b0f5 Feature: add V2Ray subscription support to proxy provider 2022-06-03 05:09:42 +08:00
e7b8c9b9db Chore: make hadowsocks2 lib embed 2022-06-02 22:17:14 +08:00
ea8a5409ad Chore: merge branch 'with-tun' into plus-pro 2022-05-29 00:57:07 +08:00
39d524dc18 Chore: update dependencies 2022-05-29 00:45:29 +08:00
0be8fc387a Chore: change GEO databases source 2022-05-29 00:45:13 +08:00
985dc99b5d Refactor: use native Win32 API to detect interface changed on Windows 2022-05-28 09:50:09 +08:00
67905bcf7e Feature: make wintun driver embed 2022-05-27 09:20:46 +08:00
b37e1fb2b9 Chore: yaml bump version from v2 to v3 2022-05-27 09:08:30 +08:00
22449da5d3 Fix: cache cleanup panic 2022-05-25 02:00:24 +08:00
6ad2cde909 Feature: support relay Socks5 UDP
supports relaying of all UDP traffic except the HTTP outbound.
2022-05-25 01:39:58 +08:00
68cf94a866 Chore: test cases 2022-05-25 01:36:27 +08:00
7f41f94fff Fix: benchmark read bytes 2022-05-23 12:58:18 +08:00
d1f0dac302 Fix: test broken on opensource repo 2022-05-23 12:30:54 +08:00
afb3e00067 Chore: add benchmark r/w 2022-05-23 12:27:52 +08:00
5b49414b49 Chore: merge branch 'with-tun' into plus-pro 2022-05-22 05:50:29 +08:00
fe44a762c2 Chore: update dependencies 2022-05-22 05:32:36 +08:00
ce1014eae3 Feature: support relay UDP traffic 2022-05-22 05:32:15 +08:00
9a31ad6151 Chore: cleanup test go.mod 2022-05-21 17:46:34 +08:00
09cc6b69e3 Chore: cleanup test code 2022-05-21 17:38:17 +08:00
622b10d34d Chore: adjust iptables 2022-05-21 09:35:02 +08:00
88b5741ad8 Fix: addrToMetadata err should be nil 2022-05-21 08:19:33 +08:00
d11d28c358 Feature: add force-cert-verify to general config
force verify TLS Certificate, prevent machine-in-the-middle attacks.
2022-05-19 04:27:22 +08:00
03499fcea6 Refactor: fetcher by generics 2022-05-19 04:27:22 +08:00
f788411154 Refactor: use raw proxy adapter to get proxy connection by dns client 2022-05-18 20:35:59 +08:00
3d2b4b1f3a Refactor: get default route interface by syscall on darwin 2022-05-18 05:58:58 +08:00
5642d9c98e Fix: should flush interface cache by switch network 2022-05-18 04:45:19 +08:00
7a406b991e Fix: module clash-test 2022-05-18 04:08:35 +08:00
8603ac40a1 Chore: make linter happy 2022-05-17 19:58:33 +08:00
178c70a320 Chore: merge branch 'with-tun' into plus-pro 2022-05-16 03:01:05 +08:00
34eeb58bfa Chore: update dependencies 2022-05-16 02:24:05 +08:00
3d25f16b3b Feature: make tls sni sniffing switch config 2022-05-16 01:43:24 +08:00
891a56fd99 Feature: apply destination IP to tracker by Direct outbound for fake-ip mode 2022-05-16 01:43:24 +08:00
ffbdcfcbfd Feature: add update GEO databases to rest api 2022-05-16 01:43:23 +08:00
72b9b829e9 Fix: set mitm outbound 2022-05-16 01:43:23 +08:00
8b3e42bf19 Refactor: tun config 2022-05-16 01:43:23 +08:00
e92bea8401 Chore: merge branch 'ogn-dev' into with-tun 2022-05-16 01:41:02 +08:00
b384449717 Fix: fix upgrade header detect (#2134) 2022-05-15 09:12:53 +08:00
53c83118bc Chore: merge branch 'ogn-dev' into with-tun 2022-05-14 02:29:50 +08:00
da7ffc0da9 Fix: add length check for ssr auth_aes128_sha1 (#2129) 2022-05-13 11:21:39 +08:00
6fe19944ad Chore: code style 2022-05-09 09:12:30 +08:00
9f00907647 Chore: merge branch 'with-tun' into plus-pro 2022-05-09 08:11:07 +08:00
ace84ff548 Chore: code style 2022-05-09 08:10:20 +08:00
108 changed files with 4402 additions and 2515 deletions

View File

@ -42,7 +42,7 @@ jobs:
echo "::set-output name=file_sha::$(git describe --tags --always)" echo "::set-output name=file_sha::$(git describe --tags --always)"
echo "::set-output name=file_date::$(Get-Date -Format 'yyyyMMdd')" echo "::set-output name=file_date::$(Get-Date -Format 'yyyyMMdd')"
((Get-Content -path constant/version.go -Raw) -replace 'unknown version',$(git describe --tags --always)) | Set-Content -Path constant/version.go ((Get-Content -path constant/version.go -Raw) -replace 'unknown version',$(Get-Date -Format 'yyyy.MM.dd')) | Set-Content -Path constant/version.go
((Get-Content -path constant/version.go -Raw) -replace 'unknown time',$(Get-Date)) | Set-Content -Path constant/version.go ((Get-Content -path constant/version.go -Raw) -replace 'unknown time',$(Get-Date)) | Set-Content -Path constant/version.go
# go test # go test
@ -51,25 +51,92 @@ jobs:
- name: Build - name: Build
#if: startsWith(github.ref, 'refs/tags/') #if: startsWith(github.ref, 'refs/tags/')
run: | run: |
$env:CGO_ENABLED=1; go build -tags build_actions -trimpath -ldflags '-w -s -buildid=' -o bin/clash-windows-amd64.exe $env:CGO_ENABLED=1; go build -tags build_actions -trimpath -ldflags '-w -s -buildid=' -o bin/clash-plus-pro-windows-amd64.exe
$env:GOAMD64="v3"; $env:CGO_ENABLED=1; go build -tags build_actions -trimpath -ldflags '-w -s -buildid=' -o bin/clash-windows-amd64-v3.exe $env:GOAMD64="v3"; $env:CGO_ENABLED=1; go build -tags build_actions -trimpath -ldflags '-w -s -buildid=' -o bin/clash-plus-pro-windows-amd64-v3.exe
$version = Get-Date -Format 'yyyy.MM.dd'
cd bin/ cd bin/
Compress-Archive -Path clash-windows-amd64.exe -DestinationPath clash-plus-windows-amd64-$(git describe --tags --always)-$(Get-Date -Format 'yyyy.MM.dd').zip Compress-Archive -Path clash-plus-pro-windows-amd64.exe -DestinationPath clash-plus-pro-windows-amd64-$version.zip
Compress-Archive -Path clash-windows-amd64-v3.exe -DestinationPath clash-plus-windows-amd64-v3-$(git describe --tags --always)-$(Get-Date -Format 'yyyy.MM.dd').zip Compress-Archive -Path clash-plus-pro-windows-amd64-v3.exe -DestinationPath clash-plus-pro-windows-amd64-v3-$version.zip
Remove-Item -Force clash-windows-amd64.exe Remove-Item -Force clash-plus-pro-windows-amd64.exe
Remove-Item -Force clash-windows-amd64-v3.exe Remove-Item -Force clash-plus-pro-windows-amd64-v3.exe
"$version" | Out-File version.txt -NoNewLine
- name: Upload files to Artifacts - name: Upload files to tag
uses: actions/upload-artifact@v2
if: startsWith(github.ref, 'refs/tags/') == false if: startsWith(github.ref, 'refs/tags/') == false
with: env:
name: clash-windows-amd64-${{ steps.test.outputs.file_sha }}-${{ steps.test.outputs.file_date }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
path: | run: |
bin/* $version = Get-Date -Format 'yyyy.MM.dd'
$plus_pro = curl `
-H "Accept: application/vnd.github.v3+json" `
-H "Authorization: token $env:GITHUB_TOKEN" `
https://api.github.com/repos/yaling888/clash/releases/tags/plus_pro | ConvertFrom-Json
$plus_pro_url = $plus_pro.url
$upload_url = $plus_pro.upload_url
$plus_pro_upload_url = $upload_url.Substring(0,$upload_url.Length-13)
curl `
-X PATCH `
-H "Accept: application/vnd.github.v3+json" `
-H "Authorization: token $env:GITHUB_TOKEN" `
"$plus_pro_url" `
-d "{`"name`":`"Plus Pro $version`",`"draft`":true}" | Out-Null
foreach ($asset in $plus_pro.assets)
{
curl `
-X DELETE `
-H "Accept: application/vnd.github.v3+json" `
-H "Authorization: token $env:GITHUB_TOKEN" `
"$($asset.url)" | Out-Null
}
- name: Delete workflow runs curl `
uses: GitRML/delete-workflow-runs@main -X POST `
-H "Content-Type: application/zip" `
-T "bin/clash-plus-pro-windows-amd64-$version.zip" `
-H "Accept: application/vnd.github.v3+json" `
-H "Authorization: token $env:GITHUB_TOKEN" `
"$plus_pro_upload_url?name=clash-plus-pro-windows-amd64-$version.zip" | Out-Null
curl `
-X POST `
-H "Content-Type: application/zip" `
-T "bin/clash-plus-pro-windows-amd64-v3-$version.zip" `
-H "Accept: application/vnd.github.v3+json" `
-H "Authorization: token $env:GITHUB_TOKEN" `
"$plus_pro_upload_url?name=clash-plus-pro-windows-amd64-v3-$version.zip" | Out-Null
curl `
-X POST `
-H "Content-Type: text/plain" `
-T "bin/version.txt" `
-H "Accept: application/vnd.github.v3+json" `
-H "Authorization: token $env:GITHUB_TOKEN" `
"$plus_pro_upload_url?name=version.txt" | Out-Null
#- name: Upload files to Artifacts
# uses: actions/upload-artifact@v2
# with:
# name: clash-windows-amd64-${{ steps.test.outputs.file_sha }}-${{ steps.test.outputs.file_date }}
# path: |
# bin/*
- name: Upload Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with: with:
retain_days: 1 files: bin/*
keep_minimum_runs: 2 draft: true
prerelease: false
generate_release_notes: false
#- name: Delete workflow runs
# uses: GitRML/delete-workflow-runs@main
# with:
# retain_days: 1
# keep_minimum_runs: 2

View File

@ -3,8 +3,7 @@ on:
push: push:
branches: branches:
- rm - rm
tags:
- '*'
jobs: jobs:
build: build:

View File

@ -103,11 +103,11 @@ jobs:
prerelease: true prerelease: true
generate_release_notes: true generate_release_notes: true
- name: Delete workflow runs #- name: Delete workflow runs
uses: GitRML/delete-workflow-runs@main # uses: GitRML/delete-workflow-runs@main
with: # with:
retain_days: 1 # retain_days: 1
keep_minimum_runs: 2 # keep_minimum_runs: 2
- name: Remove old Releases - name: Remove old Releases
uses: dev-drprasad/delete-older-releases@v0.2.0 uses: dev-drprasad/delete-older-releases@v0.2.0

155
README.md
View File

@ -40,7 +40,7 @@ Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash
This branch requires cgo and Python3.9, so make sure you set up Python3.9 before building. This branch requires cgo and Python3.9, so make sure you set up Python3.9 before building.
For example, build on macOS: For example, build on macOS:
```shell ```sh
brew update brew update
brew install python@3.9 brew install python@3.9
@ -50,7 +50,7 @@ git clone -b plus-pro https://github.com/yaling888/clash.git
cd clash cd clash
# build # build
make cleancache && make local make local
# or make local-v3 # or make local-v3
ls bin/ ls bin/
@ -59,6 +59,13 @@ ls bin/
sudo bin/clash-local sudo bin/clash-local
``` ```
### General configuration
```yaml
sniffing: true # Sniff TLS SNI
force-cert-verify: true # force verify TLS Certificate, prevent machine-in-the-middle attacks
```
### MITM configuration ### MITM configuration
A root CA certificate is required, the A root CA certificate is required, the
MITM proxy server will generate a CA certificate file and a CA private key file in your Clash home directory, you can use your own certificate replace it. MITM proxy server will generate a CA certificate file and a CA private key file in your Clash home directory, you can use your own certificate replace it.
@ -92,7 +99,7 @@ mitm:
``` ```
### DNS configuration ### DNS configuration
Support resolve ip with a proxy tunnel. Support resolve ip with a proxy tunnel or interface.
Support `geosite` with `fallback-filter`. Support `geosite` with `fallback-filter`.
@ -112,8 +119,8 @@ Use `curl -X POST controllerip:port/cache/fakeip/flush` to flush persistence fak
- https://doh.pub/dns-query - https://doh.pub/dns-query
- tls://223.5.5.5:853 - tls://223.5.5.5:853
fallback: fallback:
- 'tls://8.8.4.4:853#proxy or interface'
- 'https://1.0.0.1/dns-query#Proxy' # append the proxy adapter name to the end of DNS URL with '#' prefix. - 'https://1.0.0.1/dns-query#Proxy' # append the proxy adapter name to the end of DNS URL with '#' prefix.
- 'tls://8.8.4.4:853#Proxy'
fallback-filter: fallback-filter:
geoip: false geoip: false
geosite: geosite:
@ -125,19 +132,59 @@ Use `curl -X POST controllerip:port/cache/fakeip/flush` to flush persistence fak
``` ```
### TUN configuration ### TUN configuration
Supports macOS, Linux and Windows. Simply add the following to the main configuration:
#### NOTE:
> auto-route and auto-detect-interface only available on macOS, Windows and Linux, receive IPv4 traffic
On Windows, you should download the [Wintun](https://www.wintun.net) driver and copy `wintun.dll` into the system32 directory.
```yaml ```yaml
# Enable the TUN listener
tun: tun:
enable: true enable: true
stack: gvisor # System or gVisor stack: system # or gvisor
# device: tun://utun8 # or fd://xxx, it's optional # device: tun://utun8 # or fd://xxx, it's optional
dns-hijack: # dns-hijack:
- 0.0.0.0:53 # hijack all public # - 8.8.8.8:53
# - tcp://8.8.8.8:53
# - any:53
# - tcp://any:53
auto-route: true # auto set global route
auto-detect-interface: true # conflict with interface-name
```
or
```yaml
interface-name: en0
tun:
enable: true
stack: system # or gvisor
# dns-hijack:
# - 8.8.8.8:53
# - tcp://8.8.8.8:53
auto-route: true # auto set global route auto-route: true # auto set global route
``` ```
It's recommended to use fake-ip mode for the DNS server.
Clash needs elevated permission to create TUN device:
```sh
$ sudo ./clash
```
Then manually create the default route and DNS server. If your device already has some TUN device, Clash TUN might not work. In this case, fake-ip-filter may helpful.
Enjoy! :)
#### For Windows:
```yaml
tun:
enable: true
stack: gvisor # or system
dns-hijack:
- 198.18.0.2:53 # when `fake-ip-range` is 198.18.0.1/16, should hijack 198.18.0.2:53
auto-route: true # auto set global route for Windows
# It is recommended to use `interface-name`
auto-detect-interface: true # auto detect interface, conflict with `interface-name`
```
Finally, open the Clash
### Rules configuration ### Rules configuration
- Support rule `GEOSITE`. - Support rule `GEOSITE`.
- Support rule `USER-AGENT`. - Support rule `USER-AGENT`.
@ -156,11 +203,13 @@ script:
shortcuts: shortcuts:
quic: 'network == "udp" and dst_port == 443' quic: 'network == "udp" and dst_port == 443'
privacy: '"analytics" in host or "adservice" in host or "firebase" in host or "safebrowsing" in host or "doubleclick" in host' privacy: '"analytics" in host or "adservice" in host or "firebase" in host or "safebrowsing" in host or "doubleclick" in host'
BilibiliUdp: |
network == "udp" and match_provider("geosite:bilibili")
rules: rules:
# rule SCRIPT # rule SCRIPT shortcuts
- SCRIPT,quic,REJECT # Disable QUIC, same as rule "DST-PORT,443,REJECT,udp" - SCRIPT,quic,REJECT # Disable QUIC, same as rule "DST-PORT,443,REJECT,udp"
- SCRIPT,privacy,REJECT - SCRIPT,privacy,REJECT
- SCRIPT,BilibiliUdp,REJECT # same as rule "GEOSITE,bilibili,REJECT,udp"
# network condition for all rules # network condition for all rules
- DOMAIN-SUFFIX,example.com,DIRECT,tcp - DOMAIN-SUFFIX,example.com,DIRECT,tcp
@ -202,13 +251,8 @@ Script enables users to programmatically select a policy for the packets with mo
```yaml ```yaml
mode: script mode: script
rules:
# the rule GEOSITE just as a rule provider in mode script
- GEOSITE,category-ads-all,Whatever
- GEOSITE,youtube,Whatever
- GEOSITE,geolocation-cn,Whatever
script: script:
# path: ./script.star
code: | code: |
def main(ctx, metadata): def main(ctx, metadata):
if metadata["process_name"] == 'apsd': if metadata["process_name"] == 'apsd':
@ -276,7 +320,11 @@ Support outbound protocol `VLESS`.
Support `Trojan` with XTLS. Support `Trojan` with XTLS.
Currently XTLS only supports TCP transport. Support relay `UDP` traffic.
Support filtering proxy providers in proxy groups.
Support custom http request header, prefix name and V2Ray subscription URL in proxy providers.
```yaml ```yaml
proxies: proxies:
# VLESS # VLESS
@ -313,6 +361,56 @@ proxies:
# udp: true # udp: true
# sni: example.com # aka server name # sni: example.com # aka server name
# skip-cert-verify: true # skip-cert-verify: true
proxy-groups:
# Relay chains the proxies. proxies shall not contain a relay.
# Support relay UDP traffic.
# Traffic: clash <-> ss1 <-> trojan <-> vmess <-> ss2 <-> Internet
- name: "relay-udp-over-tcp"
type: relay
proxies:
- ss1
- trojan
- vmess
- ss2
- name: "relay-raw-udp"
type: relay
proxies:
- ss1
- ss2
- ss3
- name: "filtering-proxy-providers"
type: url-test
url: "http://www.gstatic.com/generate_204"
interval: 300
tolerance: 200
# lazy: true
filter: "XXX" # a regular expression
use:
- provider1
proxy-providers:
provider1:
type: http
url: "url" # support V2Ray subscription URL
interval: 3600
path: ./providers/provider1.yaml
# filter: "xxx"
# prefix-name: "XXX-"
header: # custom http request header
User-Agent:
- "Clash/v1.10.6"
# Accept:
# - 'application/vnd.github.v3.raw'
# Authorization:
# - ' token xxxxxxxxxxx'
health-check:
enable: false
interval: 1200
# lazy: false # default value is true
url: http://www.gstatic.com/generate_204
``` ```
### IPTABLES configuration ### IPTABLES configuration
@ -329,7 +427,7 @@ iptables:
Run Clash as a daemon. Run Clash as a daemon.
Create the systemd configuration file at /etc/systemd/system/clash.service: Create the systemd configuration file at /etc/systemd/system/clash.service:
```shell ```sh
[Unit] [Unit]
Description=Clash daemon, A rule-based proxy in Go. Description=Clash daemon, A rule-based proxy in Go.
After=network.target After=network.target
@ -344,22 +442,23 @@ ExecStart=/usr/local/bin/clash -d /etc/clash
WantedBy=multi-user.target WantedBy=multi-user.target
``` ```
Launch clashd on system startup with: Launch clashd on system startup with:
```shell ```sh
$ systemctl enable clash $ systemctl enable clash
``` ```
Launch clashd immediately with: Launch clashd immediately with:
```shell ```sh
$ systemctl start clash $ systemctl start clash
``` ```
### Display Process name ### Display Process name
To display process name online by click [https://yaling888.github.io/yacd/](https://yaling888.github.io/yacd/). To display process name online by click [http://yacd.clash-plus.cf](http://yacd.clash-plus.cf) for local API by Safari or [https://yacd.clash-plus.cf](https://yacd.clash-plus.cf) for local API by Chrome.
You can download the [Dashboard](https://github.com/yaling888/yacd/archive/gh-pages.zip) into Clash home directory: You can download the [Dashboard](https://github.com/yaling888/yacd/archive/gh-pages.zip) into Clash home directory:
```shell ```sh
cd ~/.config/clash $ cd ~/.config/clash
curl -LJ https://github.com/yaling888/yacd/archive/gh-pages.zip -o dashboard.zip $ curl -LJ https://github.com/yaling888/yacd/archive/gh-pages.zip -o yacd-gh-pages.zip
unzip dashboard.zip $ unzip yacd-gh-pages.zip
$ mv yacd-gh-pages dashboard
``` ```
Add to config file: Add to config file:
@ -370,7 +469,7 @@ external-ui: dashboard
Open [http://127.0.0.1:9090/ui/](http://127.0.0.1:9090/ui/) by web browser. Open [http://127.0.0.1:9090/ui/](http://127.0.0.1:9090/ui/) by web browser.
## Plus Pro Release ## Plus Pro Release
[Release](https://github.com/yaling888/clash/releases/tag/plus) [Release](https://github.com/yaling888/clash/releases/tag/plus_pro)
## Development ## 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) 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)

View File

@ -3,14 +3,12 @@ package adapter
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"net/netip" "net/netip"
"net/url" "net/url"
"time" "time"
_ "unsafe"
"github.com/Dreamacro/clash/common/queue" "github.com/Dreamacro/clash/common/queue"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
@ -19,9 +17,6 @@ import (
"go.uber.org/atomic" "go.uber.org/atomic"
) )
//go:linkname errCanceled net.errCanceled
var errCanceled error
type Proxy struct { type Proxy struct {
C.ProxyAdapter C.ProxyAdapter
history *queue.Queue[C.DelayHistory] history *queue.Queue[C.DelayHistory]
@ -43,7 +38,7 @@ func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...) conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
p.alive.Store(err == nil || errors.Is(err, errCanceled)) p.alive.Store(err == nil)
return conn, err return conn, err
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"io"
"net" "net"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
@ -30,12 +31,17 @@ func (b *Base) Type() C.AdapterType {
} }
// StreamConn implements C.ProxyAdapter // StreamConn implements C.ProxyAdapter
func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (b *Base) StreamConn(c net.Conn, _ *C.Metadata) (net.Conn, error) {
return c, errors.New("no support")
}
// StreamPacketConn implements C.ProxyAdapter
func (b *Base) StreamPacketConn(c net.Conn, _ *C.Metadata) (net.Conn, error) {
return c, errors.New("no support") return c, errors.New("no support")
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (b *Base) ListenPacketContext(_ context.Context, _ *C.Metadata, _ ...dialer.Option) (C.PacketConn, error) {
return nil, errors.New("no support") return nil, errors.New("no support")
} }
@ -57,7 +63,7 @@ func (b *Base) Addr() string {
} }
// Unwrap implements C.ProxyAdapter // Unwrap implements C.ProxyAdapter
func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy { func (b *Base) Unwrap(_ *C.Metadata) C.Proxy {
return nil return nil
} }
@ -133,6 +139,40 @@ func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name()) c.chain = append(c.chain, a.Name())
} }
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn { func NewPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
return &packetConn{pc, []string{a.Name()}} return &packetConn{pc, []string{a.Name()}}
} }
type wrapConn struct {
net.PacketConn
}
func (*wrapConn) Read([]byte) (int, error) {
return 0, io.EOF
}
func (*wrapConn) Write([]byte) (int, error) {
return 0, io.EOF
}
func (*wrapConn) RemoteAddr() net.Addr {
return nil
}
func WrapConn(packetConn net.PacketConn) net.Conn {
return &wrapConn{
PacketConn: packetConn,
}
}
func IsPacketConn(c net.Conn) bool {
if _, ok := c.(net.PacketConn); !ok {
return false
}
if ua, ok := c.LocalAddr().(*net.UnixAddr); ok {
return ua.Net == "unixgram"
}
return true
}

View File

@ -3,6 +3,7 @@ package outbound
import ( import (
"context" "context"
"net" "net"
"net/netip"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
@ -19,18 +20,26 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
if err != nil { if err != nil {
return nil, err return nil, err
} }
tcpKeepAlive(c) tcpKeepAlive(c)
if !metadata.DstIP.IsValid() && c.RemoteAddr() != nil {
if h, _, err := net.SplitHostPort(c.RemoteAddr().String()); err == nil {
metadata.DstIP = netip.MustParseAddr(h)
}
}
return NewConn(c, d), nil return NewConn(c, d), nil
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (d *Direct) ListenPacketContext(ctx context.Context, _ *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
opts = append(opts, dialer.WithDirect()) opts = append(opts, dialer.WithDirect())
pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...) pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return newPacketConn(&directPacketConn{pc}, d), nil return NewPacketConn(&directPacketConn{pc}, d), nil
} }
type directPacketConn struct { type directPacketConn struct {

View File

@ -48,7 +48,7 @@ func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return newPacketConn(&nopPacketConn{}, r), nil return NewPacketConn(&nopPacketConn{}, r), nil
} }
func NewReject() *Reject { func NewReject() *Reject {

View File

@ -10,11 +10,10 @@ import (
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
"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/transport/shadowsocks/core"
obfs "github.com/Dreamacro/clash/transport/simple-obfs" obfs "github.com/Dreamacro/clash/transport/simple-obfs"
"github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/socks5"
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin" v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
"github.com/Dreamacro/go-shadowsocks2/core"
) )
type ShadowSocks struct { type ShadowSocks struct {
@ -74,6 +73,21 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e
return c, err return c, err
} }
// StreamPacketConn implements C.ProxyAdapter
func (ss *ShadowSocks) StreamPacketConn(c net.Conn, _ *C.Metadata) (net.Conn, error) {
if !IsPacketConn(c) {
return c, fmt.Errorf("%s connect error: can not convert net.Conn to net.PacketConn", ss.addr)
}
addr, err := resolveUDPAddr("udp", ss.addr)
if err != nil {
return c, err
}
pc := ss.cipher.PacketConn(c.(net.PacketConn))
return WrapConn(&ssPacketConn{PacketConn: pc, rAddr: addr}), nil
}
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...) c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
@ -95,14 +109,13 @@ func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Meta
return nil, err return nil, err
} }
addr, err := resolveUDPAddr("udp", ss.addr) c, err := ss.StreamPacketConn(WrapConn(pc), metadata)
if err != nil { if err != nil {
pc.Close() _ = pc.Close()
return nil, err return nil, err
} }
pc = ss.cipher.PacketConn(pc) return NewPacketConn(c.(net.PacketConn), ss), nil
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ss), nil
} }
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {

View File

@ -8,12 +8,11 @@ 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/transport/shadowsocks/core"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowaead"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowstream"
"github.com/Dreamacro/clash/transport/ssr/obfs" "github.com/Dreamacro/clash/transport/ssr/obfs"
"github.com/Dreamacro/clash/transport/ssr/protocol" "github.com/Dreamacro/clash/transport/ssr/protocol"
"github.com/Dreamacro/go-shadowsocks2/core"
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
"github.com/Dreamacro/go-shadowsocks2/shadowstream"
) )
type ShadowSocksR struct { type ShadowSocksR struct {
@ -59,6 +58,22 @@ func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn,
return c, err return c, err
} }
// StreamPacketConn implements C.ProxyAdapter
func (ssr *ShadowSocksR) StreamPacketConn(c net.Conn, _ *C.Metadata) (net.Conn, error) {
if !IsPacketConn(c) {
return c, fmt.Errorf("%s connect error: can not convert net.Conn to net.PacketConn", ssr.addr)
}
addr, err := resolveUDPAddr("udp", ssr.addr)
if err != nil {
return c, err
}
pc := ssr.cipher.PacketConn(c.(net.PacketConn))
pc = ssr.protocol.PacketConn(pc)
return WrapConn(&ssPacketConn{PacketConn: pc, rAddr: addr}), nil
}
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ssr.addr, ssr.Base.DialOptions(opts...)...) c, err := dialer.DialContext(ctx, "tcp", ssr.addr, ssr.Base.DialOptions(opts...)...)
@ -80,15 +95,13 @@ func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Me
return nil, err return nil, err
} }
addr, err := resolveUDPAddr("udp", ssr.addr) c, err := ssr.StreamPacketConn(WrapConn(pc), metadata)
if err != nil { if err != nil {
pc.Close() _ = pc.Close()
return nil, err return nil, err
} }
pc = ssr.cipher.PacketConn(pc) return NewPacketConn(c.(net.PacketConn), ssr), nil
pc = ssr.protocol.PacketConn(pc)
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil
} }
func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) { func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {

View File

@ -58,6 +58,18 @@ func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return c, err return c, err
} }
// StreamPacketConn implements C.ProxyAdapter
func (s *Snell) StreamPacketConn(c net.Conn, _ *C.Metadata) (net.Conn, error) {
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
err := snell.WriteUDPHeader(c, s.version)
if err != nil {
return c, err
}
return WrapConn(snell.PacketConn(c)), nil
}
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
if s.version == snell.Version2 && len(opts) == 0 { if s.version == snell.Version2 && len(opts) == 0 {
@ -68,7 +80,7 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16) port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil { if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil {
c.Close() _ = c.Close()
return nil, err return nil, err
} }
return NewConn(c, s), err return NewConn(c, s), err
@ -93,15 +105,14 @@ func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
return nil, err return nil, err
} }
tcpKeepAlive(c) tcpKeepAlive(c)
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
err = snell.WriteUDPHeader(c, s.version) pc, err := s.StreamPacketConn(c, metadata)
if err != nil { if err != nil {
_ = c.Close()
return nil, err return nil, err
} }
pc := snell.PacketConn(c) return NewPacketConn(pc.(net.PacketConn), s), nil
return newPacketConn(pc, s), nil
} }
func NewSnell(option SnellOption) (*Snell, error) { func NewSnell(option SnellOption) (*Snell, error) {

View File

@ -37,12 +37,59 @@ type Socks5Option struct {
// StreamConn implements C.ProxyAdapter // StreamConn implements C.ProxyAdapter
func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
c, _, err = ss.streamConn(c, metadata)
return c, err
}
func (ss *Socks5) StreamSocks5PacketConn(c net.Conn, pc net.PacketConn, metadata *C.Metadata) (net.PacketConn, error) {
if c == nil {
return pc, fmt.Errorf("%s connect error: parameter net.Conn is nil", ss.addr)
}
if pc == nil {
return pc, fmt.Errorf("%s connect error: parameter net.PacketConn is nil", ss.addr)
}
cc, bindAddr, err := ss.streamConn(c, metadata)
if err != nil {
return pc, err
}
c = cc
go func() {
_, _ = io.Copy(io.Discard, c)
_ = c.Close()
// A UDP association terminates when the TCP connection that the UDP
// ASSOCIATE request arrived on terminates. RFC1928
_ = pc.Close()
}()
// Support unspecified UDP bind address.
bindUDPAddr := bindAddr.UDPAddr()
if bindUDPAddr == nil {
return pc, errors.New("invalid UDP bind address")
} else if bindUDPAddr.IP.IsUnspecified() {
serverAddr, err := resolveUDPAddr("udp", ss.Addr())
if err != nil {
return pc, err
}
bindUDPAddr.IP = serverAddr.IP
}
return &socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, nil
}
func (ss *Socks5) streamConn(c net.Conn, metadata *C.Metadata) (_ net.Conn, bindAddr socks5.Addr, err error) {
if ss.tls { if ss.tls {
cc := tls.Client(c, ss.tlsConfig) cc := tls.Client(c, ss.tlsConfig)
err := cc.Handshake() err := cc.Handshake()
c = cc c = cc
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return c, nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
} }
@ -53,10 +100,14 @@ func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
Password: ss.pass, Password: ss.pass,
} }
} }
if _, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdConnect, user); err != nil {
return nil, err if metadata.NetWork == C.UDP {
bindAddr, err = socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdUDPAssociate, user)
} else {
bindAddr, err = socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdConnect, user)
} }
return c, nil
return c, bindAddr, err
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
@ -81,61 +132,24 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts ..
func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...) c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
err = fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
return
}
if ss.tls {
cc := tls.Client(c, ss.tlsConfig)
err = cc.Handshake()
c = cc
} }
defer safeConnClose(c, err) defer safeConnClose(c, err)
tcpKeepAlive(c)
var user *socks5.User
if ss.user != "" {
user = &socks5.User{
Username: ss.user,
Password: ss.pass,
}
}
bindAddr, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdUDPAssociate, user)
if err != nil {
err = fmt.Errorf("client hanshake error: %w", err)
return
}
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...) pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return return
} }
go func() { tcpKeepAlive(c)
io.Copy(io.Discard, c)
c.Close()
// A UDP association terminates when the TCP connection that the UDP
// ASSOCIATE request arrived on terminates. RFC1928
pc.Close()
}()
// Support unspecified UDP bind address. pc, err = ss.StreamSocks5PacketConn(c, pc, metadata)
bindUDPAddr := bindAddr.UDPAddr() if err != nil {
if bindUDPAddr == nil {
err = errors.New("invalid UDP bind address")
return return
} else if bindUDPAddr.IP.IsUnspecified() {
serverAddr, err := resolveUDPAddr("udp", ss.Addr())
if err != nil {
return nil, err
}
bindUDPAddr.IP = serverAddr.IP
} }
return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil return NewPacketConn(pc, ss), nil
} }
func NewSocks5(option Socks5Option) *Socks5 { func NewSocks5(option Socks5Option) *Socks5 {
@ -199,6 +213,6 @@ func (uc *socksPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
} }
func (uc *socksPacketConn) Close() error { func (uc *socksPacketConn) Close() error {
uc.tcpConn.Close() _ = uc.tcpConn.Close()
return uc.PacketConn.Close() return uc.PacketConn.Close()
} }

View File

@ -72,8 +72,7 @@ func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) {
return t.instance.StreamConn(c) return t.instance.StreamConn(c)
} }
// StreamConn implements C.ProxyAdapter func (t *Trojan) trojanStream(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error var err error
if t.transport != nil { if t.transport != nil {
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig) c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig)
@ -85,39 +84,63 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
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) c, err = t.instance.PrepareXTLSConn(c)
if err != nil { if err != nil {
return nil, err return c, err
}
if metadata.NetWork == C.UDP {
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
return c, err
} }
err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)) err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata))
return c, err return c, err
} }
// StreamConn implements C.ProxyAdapter
func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return t.trojanStream(c, metadata)
}
// StreamPacketConn implements C.ProxyAdapter
func (t *Trojan) StreamPacketConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
c, err = t.trojanStream(c, metadata)
if err != nil {
return c, err
}
pc := t.instance.PacketConn(c)
return WrapConn(pc), nil
}
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
var c net.Conn
// gun transport // gun transport
if t.transport != nil && len(opts) == 0 { if t.transport != nil && len(opts) == 0 {
c, err := gun.StreamGunWithTransport(t.transport, t.gunConfig) c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
c, err = t.instance.PresetXTLSConn(c) defer safeConnClose(c, err)
c, err = t.instance.PrepareXTLSConn(c)
if err != nil { if err != nil {
c.Close()
return nil, err 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()
return nil, err return nil, err
} }
return NewConn(c, t), nil return NewConn(c, t), nil
} }
c, err := dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...) c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
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)
} }
@ -137,33 +160,44 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
var c net.Conn var c net.Conn
// grpc transport // gun transport
if t.transport != nil && len(opts) == 0 { if t.transport != nil && len(opts) == 0 {
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig) c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, err
} }
defer safeConnClose(c, err) defer safeConnClose(c, err)
} else {
c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...) c, err = t.instance.PrepareXTLSConn(c)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, err
} }
defer safeConnClose(c, err)
tcpKeepAlive(c) if err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata)); err != nil {
c, err = t.plainStream(c) return nil, err
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
pc := t.instance.PacketConn(c)
return NewPacketConn(pc, t), nil
} }
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata)) c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = t.StreamPacketConn(c, metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }
pc := t.instance.PacketConn(c) return NewPacketConn(c.(net.PacketConn), t), nil
return newPacketConn(pc, t), err
} }
func NewTrojan(option TrojanOption) (*Trojan, error) { func NewTrojan(option TrojanOption) (*Trojan, error) {

View File

@ -52,7 +52,7 @@ func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
} }
func safeConnClose(c net.Conn, err error) { func safeConnClose(c net.Conn, err error) {
if err != nil { if err != nil && c != nil {
_ = c.Close() _ = c.Close()
} }
} }

View File

@ -58,6 +58,7 @@ type VlessOption struct {
ServerName string `proxy:"servername,omitempty"` ServerName string `proxy:"servername,omitempty"`
} }
// StreamConn implements C.ProxyAdapter
func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error var err error
switch v.option.Network { switch v.option.Network {
@ -147,6 +148,26 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return v.client.StreamConn(c, parseVlessAddr(metadata)) return v.client.StreamConn(c, parseVlessAddr(metadata))
} }
// StreamPacketConn implements C.ProxyAdapter
func (v *Vless) StreamPacketConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
// vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
var err error
c, err = v.StreamConn(c, metadata)
if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err)
}
return WrapConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}), nil
}
func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error) { func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error) {
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
@ -219,18 +240,18 @@ func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
// vless use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
var c net.Conn var c net.Conn
// gun transport // gun transport
if v.transport != nil && len(opts) == 0 { if v.transport != nil && len(opts) == 0 {
// vless use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig) c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
@ -238,22 +259,27 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
defer safeConnClose(c, err) defer safeConnClose(c, err)
c, err = v.client.StreamConn(c, parseVlessAddr(metadata)) c, err = v.client.StreamConn(c, parseVlessAddr(metadata))
} else {
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("new vless client error: %v", err)
} }
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = v.StreamConn(c, metadata) return NewPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
} }
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = v.StreamPacketConn(c, metadata)
if err != nil { if err != nil {
return nil, fmt.Errorf("new vless client error: %v", err) return nil, fmt.Errorf("new vless client error: %v", err)
} }
return newPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil return NewPacketConn(c.(net.PacketConn), v), nil
} }
func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr { func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr {
@ -292,7 +318,7 @@ type vlessPacketConn struct {
mux sync.Mutex mux sync.Mutex
} }
func (vc *vlessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { func (vc *vlessPacketConn) WriteTo(b []byte, _ net.Addr) (int, error) {
total := len(b) total := len(b)
if total == 0 { if total == 0 {
return 0, nil return 0, nil

View File

@ -3,13 +3,13 @@ package outbound
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"github.com/Dreamacro/clash/common/convert"
"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"
@ -116,6 +116,9 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
} else if host := wsOpts.Headers.Get("Host"); host != "" { } else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host wsOpts.TLSConfig.ServerName = host
} }
} else {
wsOpts.Headers.Set("Host", convert.RandHost())
convert.SetUserAgent(wsOpts.Headers)
} }
c, err = vmess.StreamWebsocketConn(c, wsOpts) c, err = vmess.StreamWebsocketConn(c, wsOpts)
case "http": case "http":
@ -135,6 +138,9 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else {
http.Header(v.option.HTTPOpts.Headers).Set("Host", convert.RandHost())
convert.SetUserAgent(v.option.HTTPOpts.Headers)
} }
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
@ -195,6 +201,26 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return v.client.StreamConn(c, parseVmessAddr(metadata)) return v.client.StreamConn(c, parseVmessAddr(metadata))
} }
// StreamPacketConn implements C.ProxyAdapter
func (v *Vmess) StreamPacketConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
// vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host)
if err != nil {
return c, fmt.Errorf("can't resolve ip: %w", err)
}
metadata.DstIP = ip
}
var err error
c, err = v.StreamConn(c, metadata)
if err != nil {
return c, fmt.Errorf("new vmess client error: %v", err)
}
return WrapConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}), nil
}
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
// gun transport // gun transport
@ -226,18 +252,18 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
// vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
var c net.Conn var c net.Conn
// gun transport // gun transport
if v.transport != nil && len(opts) == 0 { if v.transport != nil && len(opts) == 0 {
// vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host)
if err != nil {
return nil, fmt.Errorf("can't resolve ip: %w", err)
}
metadata.DstIP = ip
}
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig) c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
@ -245,22 +271,27 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
defer safeConnClose(c, err) defer safeConnClose(c, err)
c, err = v.client.StreamConn(c, parseVmessAddr(metadata)) c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
} else {
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("new vmess client error: %v", err)
} }
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = v.StreamConn(c, metadata) return NewPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
} }
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = v.StreamPacketConn(c, metadata)
if err != nil { if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err) return nil, fmt.Errorf("new vmess client error: %v", err)
} }
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil return NewPacketConn(c.(net.PacketConn), v), nil
} }
func NewVmess(option VmessOption) (*Vmess, error) { func NewVmess(option VmessOption) (*Vmess, error) {
@ -368,7 +399,7 @@ type vmessPacketConn struct {
rAddr net.Addr rAddr net.Addr
} }
func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { func (uc *vmessPacketConn) WriteTo(b []byte, _ net.Addr) (int, error) {
return uc.Conn.Write(b) return uc.Conn.Write(b)
} }

View File

@ -3,6 +3,7 @@ package outboundgroup
import ( import (
"errors" "errors"
"fmt" "fmt"
"regexp"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapter/provider" "github.com/Dreamacro/clash/adapter/provider"
@ -29,6 +30,7 @@ type GroupCommonOption struct {
Interval int `group:"interval,omitempty"` Interval int `group:"interval,omitempty"`
Lazy bool `group:"lazy,omitempty"` Lazy bool `group:"lazy,omitempty"`
DisableUDP bool `group:"disable-udp,omitempty"` DisableUDP bool `group:"disable-udp,omitempty"`
Filter string `group:"filter,omitempty"`
} }
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) { func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) {
@ -37,10 +39,23 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
groupOption := &GroupCommonOption{ groupOption := &GroupCommonOption{
Lazy: true, Lazy: true,
} }
if err := decoder.Decode(config, groupOption); err != nil {
var (
filterRegx *regexp.Regexp
err error
)
if err = decoder.Decode(config, groupOption); err != nil {
return nil, errFormat return nil, errFormat
} }
if groupOption.Filter != "" {
filterRegx, err = regexp.Compile(groupOption.Filter)
if err != nil {
return nil, fmt.Errorf("invalid filter regex: %w", err)
}
}
if groupOption.Type == "" || groupOption.Name == "" { if groupOption.Type == "" || groupOption.Name == "" {
return nil, errFormat return nil, errFormat
} }
@ -90,7 +105,7 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
} }
if len(groupOption.Use) != 0 { if len(groupOption.Use) != 0 {
list, err := getProviders(providersMap, groupOption.Use) list, err := getProviders(providersMap, groupOption, filterRegx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -130,8 +145,13 @@ func getProxies(mapping map[string]C.Proxy, list []string) ([]C.Proxy, error) {
return ps, nil return ps, nil
} }
func getProviders(mapping map[string]types.ProxyProvider, list []string) ([]types.ProxyProvider, error) { func getProviders(mapping map[string]types.ProxyProvider, groupOption *GroupCommonOption, filterRegx *regexp.Regexp) ([]types.ProxyProvider, error) {
var ps []types.ProxyProvider var (
ps []types.ProxyProvider
list = groupOption.Use
groupName = groupOption.Name
)
for _, name := range list { for _, name := range list {
p, ok := mapping[name] p, ok := mapping[name]
if !ok { if !ok {
@ -141,6 +161,27 @@ func getProviders(mapping map[string]types.ProxyProvider, list []string) ([]type
if p.VehicleType() == types.Compatible { if p.VehicleType() == types.Compatible {
return nil, fmt.Errorf("proxy group %s can't contains in `use`", name) return nil, fmt.Errorf("proxy group %s can't contains in `use`", name)
} }
if filterRegx != nil {
var hc *provider.HealthCheck
if groupOption.Type == "select" || groupOption.Type == "relay" {
hc = provider.NewHealthCheck([]C.Proxy{}, "", 0, true)
} else {
if groupOption.URL == "" || groupOption.Interval == 0 {
return nil, errMissHealthCheck
}
hc = provider.NewHealthCheck([]C.Proxy{}, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
}
if _, ok = mapping[groupName]; ok {
groupName += "->" + p.Name()
}
pd := p.(*provider.ProxySetProvider)
p = provider.NewProxyFilterProvider(groupName, pd, hc, filterRegx)
pd.RegisterProvidersInUse(p)
}
ps = append(ps, p) ps = append(ps, p)
} }
return ps, nil return ps, nil

View File

@ -4,10 +4,14 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"net/netip"
"github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo" "github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider" "github.com/Dreamacro/clash/constant/provider"
) )
@ -34,30 +38,12 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
return proxies[0].DialContext(ctx, metadata, r.Base.DialOptions(opts...)...) return proxies[0].DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
} }
first := proxies[0] c, err := r.streamContext(ctx, proxies, r.Base.DialOptions(opts...)...)
last := proxies[len(proxies)-1]
c, err := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err) return nil, err
}
tcpKeepAlive(c)
var currentMeta *C.Metadata
for _, proxy := range proxies[1:] {
currentMeta, err = addrToMetadata(proxy.Addr())
if err != nil {
return nil, err
}
c, err = first.StreamConn(c, currentMeta)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
first = proxy
} }
last := proxies[len(proxies)-1]
c, err = last.StreamConn(c, metadata) c, err = last.StreamConn(c, metadata)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err) return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err)
@ -66,6 +52,147 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
return outbound.NewConn(c, r), nil return outbound.NewConn(c, r), nil
} }
// ListenPacketContext implements C.ProxyAdapter
func (r *Relay) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
var proxies []C.Proxy
for _, proxy := range r.proxies(metadata, true) {
if proxy.Type() != C.Direct {
proxies = append(proxies, proxy)
}
}
length := len(proxies)
switch length {
case 0:
return outbound.NewDirect().ListenPacketContext(ctx, metadata, r.Base.DialOptions(opts...)...)
case 1:
proxy := proxies[0]
if !proxy.SupportUDP() {
return nil, fmt.Errorf("%s connect error: proxy [%s] UDP is not supported", proxy.Addr(), proxy.Name())
}
return proxy.ListenPacketContext(ctx, metadata, r.Base.DialOptions(opts...)...)
}
var (
firstIndex = 0
nextIndex = 1
lastUDPOverTCPIndex = -1
rawUDPRelay = false
first = proxies[firstIndex]
last = proxies[length-1]
c net.Conn
cc net.Conn
err error
currentMeta *C.Metadata
)
if !last.SupportUDP() {
return nil, fmt.Errorf("%s connect error: proxy [%s] UDP is not supported in relay chains", last.Addr(), last.Name())
}
rawUDPRelay, lastUDPOverTCPIndex = isRawUDPRelay(proxies)
if first.Type() == C.Socks5 {
cc1, err1 := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...)
if err1 != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
cc = cc1
tcpKeepAlive(cc)
var pc net.PacketConn
pc, err = dialer.ListenPacket(ctx, "udp", "", r.Base.DialOptions(opts...)...)
c = outbound.WrapConn(pc)
} else if rawUDPRelay {
var pc net.PacketConn
pc, err = dialer.ListenPacket(ctx, "udp", "", r.Base.DialOptions(opts...)...)
c = outbound.WrapConn(pc)
} else {
firstIndex = lastUDPOverTCPIndex
nextIndex = firstIndex + 1
first = proxies[firstIndex]
c, err = r.streamContext(ctx, proxies[:nextIndex], r.Base.DialOptions(opts...)...)
}
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
if nextIndex < length {
for i, proxy := range proxies[nextIndex:] { // raw udp in loop
currentMeta, err = addrToMetadata(proxy.Addr())
if err != nil {
return nil, err
}
currentMeta.NetWork = C.UDP
if !isRawUDP(first) && !first.SupportUDP() {
return nil, fmt.Errorf("%s connect error: proxy [%s] UDP is not supported in relay chains", first.Addr(), first.Name())
}
if needResolveIP(first, currentMeta) {
var ip netip.Addr
ip, err = resolver.ResolveProxyServerHost(currentMeta.Host)
if err != nil {
return nil, fmt.Errorf("can't resolve ip: %w", err)
}
currentMeta.DstIP = ip
}
if cc != nil { // socks5
c, err = streamSocks5PacketConn(first, cc, c, currentMeta)
cc = nil
} else {
c, err = first.StreamPacketConn(c, currentMeta)
}
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
if proxy.Type() == C.Socks5 {
endIndex := nextIndex + i + 1
cc, err = r.streamContext(ctx, proxies[:endIndex], r.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
}
first = proxy
}
}
if cc != nil {
c, err = streamSocks5PacketConn(last, cc, c, metadata)
} else {
c, err = last.StreamPacketConn(c, metadata)
}
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err)
}
return outbound.NewPacketConn(c.(net.PacketConn), r), nil
}
// SupportUDP implements C.ProxyAdapter
func (r *Relay) SupportUDP() bool {
proxies := r.rawProxies(true)
l := len(proxies)
if l == 0 {
return true
}
last := proxies[l-1]
return isRawUDP(last) || last.SupportUDP()
}
// MarshalJSON implements C.ProxyAdapter // MarshalJSON implements C.ProxyAdapter
func (r *Relay) MarshalJSON() ([]byte, error) { func (r *Relay) MarshalJSON() ([]byte, error) {
var all []string var all []string
@ -100,6 +227,83 @@ func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
return proxies return proxies
} }
func (r *Relay) streamContext(ctx context.Context, proxies []C.Proxy, opts ...dialer.Option) (net.Conn, error) {
first := proxies[0]
c, err := dialer.DialContext(ctx, "tcp", first.Addr(), opts...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
tcpKeepAlive(c)
if len(proxies) > 1 {
var currentMeta *C.Metadata
for _, proxy := range proxies[1:] {
currentMeta, err = addrToMetadata(proxy.Addr())
if err != nil {
return nil, err
}
c, err = first.StreamConn(c, currentMeta)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
first = proxy
}
}
return c, nil
}
func streamSocks5PacketConn(proxy C.Proxy, cc, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
pc, err := proxy.(*adapter.Proxy).ProxyAdapter.(*outbound.Socks5).StreamSocks5PacketConn(cc, c.(net.PacketConn), metadata)
return outbound.WrapConn(pc), err
}
func isRawUDPRelay(proxies []C.Proxy) (bool, int) {
var (
lastIndex = len(proxies) - 1
last = proxies[lastIndex]
isLastRawUDP = isRawUDP(last)
isUDPOverTCP = false
lastUDPOverTCPIndex = -1
)
for i := lastIndex; i >= 0; i-- {
p := proxies[i]
isUDPOverTCP = isUDPOverTCP || !isRawUDP(p)
if isLastRawUDP && isUDPOverTCP && lastUDPOverTCPIndex == -1 {
lastUDPOverTCPIndex = i
}
}
if !isLastRawUDP {
lastUDPOverTCPIndex = lastIndex
}
return !isUDPOverTCP, lastUDPOverTCPIndex
}
func isRawUDP(proxy C.ProxyAdapter) bool {
if proxy.Type() == C.Shadowsocks || proxy.Type() == C.ShadowsocksR || proxy.Type() == C.Socks5 {
return true
}
return false
}
func needResolveIP(proxy C.ProxyAdapter, metadata *C.Metadata) bool {
if metadata.Resolved() {
return false
}
if proxy.Type() != C.Vmess && proxy.Type() != C.Vless {
return false
}
return true
}
func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay { func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
return &Relay{ return &Relay{
Base: outbound.NewBase(outbound.BaseOption{ Base: outbound.NewBase(outbound.BaseOption{

View File

@ -24,7 +24,7 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
DstIP: netip.Addr{}, DstIP: netip.Addr{},
DstPort: port, DstPort: port,
} }
return return addr, nil
} else if ip.Is4() { } else if ip.Is4() {
addr = &C.Metadata{ addr = &C.Metadata{
AddrType: C.AtypIPv4, AddrType: C.AtypIPv4,

View File

@ -8,7 +8,7 @@ import (
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
func ParseProxy(mapping map[string]any) (C.Proxy, error) { func ParseProxy(mapping map[string]any, forceCertVerify bool) (C.Proxy, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true}) decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true})
proxyType, existType := mapping["type"].(string) proxyType, existType := mapping["type"].(string)
if !existType { if !existType {
@ -40,6 +40,9 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
if err != nil { if err != nil {
break break
} }
if forceCertVerify {
socksOption.SkipCertVerify = false
}
proxy = outbound.NewSocks5(*socksOption) proxy = outbound.NewSocks5(*socksOption)
case "http": case "http":
httpOption := &outbound.HttpOption{} httpOption := &outbound.HttpOption{}
@ -47,6 +50,9 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
if err != nil { if err != nil {
break break
} }
if forceCertVerify {
httpOption.SkipCertVerify = false
}
proxy = outbound.NewHttp(*httpOption) proxy = outbound.NewHttp(*httpOption)
case "vmess": case "vmess":
vmessOption := &outbound.VmessOption{ vmessOption := &outbound.VmessOption{
@ -59,6 +65,9 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
if err != nil { if err != nil {
break break
} }
if forceCertVerify {
vmessOption.SkipCertVerify = false
}
proxy, err = outbound.NewVmess(*vmessOption) proxy, err = outbound.NewVmess(*vmessOption)
case "vless": case "vless":
vlessOption := &outbound.VlessOption{} vlessOption := &outbound.VlessOption{}
@ -66,6 +75,9 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
if err != nil { if err != nil {
break break
} }
if forceCertVerify {
vlessOption.SkipCertVerify = false
}
proxy, err = outbound.NewVless(*vlessOption) proxy, err = outbound.NewVless(*vlessOption)
case "snell": case "snell":
snellOption := &outbound.SnellOption{} snellOption := &outbound.SnellOption{}
@ -80,6 +92,9 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
if err != nil { if err != nil {
break break
} }
if forceCertVerify {
trojanOption.SkipCertVerify = false
}
proxy, err = outbound.NewTrojan(*trojanOption) proxy, err = outbound.NewTrojan(*trojanOption)
default: default:
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)

View File

@ -16,28 +16,28 @@ var (
dirMode os.FileMode = 0o755 dirMode os.FileMode = 0o755
) )
type parser = func([]byte) (any, error) type parser[V any] func([]byte) (V, error)
type fetcher struct { type fetcher[V any] struct {
name string name string
vehicle types.Vehicle vehicle types.Vehicle
updatedAt *time.Time updatedAt *time.Time
ticker *time.Ticker ticker *time.Ticker
done chan struct{} done chan struct{}
hash [16]byte hash [16]byte
parser parser parser parser[V]
onUpdate func(any) onUpdate func(V)
} }
func (f *fetcher) Name() string { func (f *fetcher[V]) Name() string {
return f.name return f.name
} }
func (f *fetcher) VehicleType() types.VehicleType { func (f *fetcher[V]) VehicleType() types.VehicleType {
return f.vehicle.Type() return f.vehicle.Type()
} }
func (f *fetcher) Initial() (any, error) { func (f *fetcher[V]) Initial() (V, error) {
var ( var (
buf []byte buf []byte
err error err error
@ -53,24 +53,24 @@ func (f *fetcher) Initial() (any, error) {
} }
if err != nil { if err != nil {
return nil, err return getZero[V](), err
} }
proxies, err := f.parser(buf) proxies, err := f.parser(buf)
if err != nil { if err != nil {
if !isLocal { if !isLocal {
return nil, 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 nil, err return getZero[V](), err
} }
proxies, err = f.parser(buf) proxies, err = f.parser(buf)
if err != nil { if err != nil {
return nil, err return getZero[V](), err
} }
isLocal = false isLocal = false
@ -78,7 +78,7 @@ func (f *fetcher) Initial() (any, 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 nil, err return getZero[V](), err
} }
} }
@ -92,28 +92,28 @@ func (f *fetcher) Initial() (any, error) {
return proxies, nil return proxies, nil
} }
func (f *fetcher) Update() (any, 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 nil, false, err return getZero[V](), false, err
} }
now := time.Now() now := time.Now()
hash := md5.Sum(buf) hash := md5.Sum(buf)
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 nil, true, nil return getZero[V](), true, nil
} }
proxies, err := f.parser(buf) proxies, err := f.parser(buf)
if err != nil { if err != nil {
return nil, 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 nil, false, err return getZero[V](), false, err
} }
} }
@ -123,14 +123,14 @@ func (f *fetcher) Update() (any, bool, error) {
return proxies, false, nil return proxies, false, nil
} }
func (f *fetcher) Destroy() error { func (f *fetcher[V]) Destroy() error {
if f.ticker != nil { if f.ticker != nil {
f.done <- struct{}{} f.done <- struct{}{}
} }
return nil return nil
} }
func (f *fetcher) pullLoop() { func (f *fetcher[V]) pullLoop() {
for { for {
select { select {
case <-f.ticker.C: case <-f.ticker.C:
@ -168,13 +168,13 @@ func safeWrite(path string, buf []byte) error {
return os.WriteFile(path, buf, fileMode) return os.WriteFile(path, buf, fileMode)
} }
func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, parser parser, onUpdate func(any)) *fetcher { func newFetcher[V any](name string, interval time.Duration, vehicle types.Vehicle, parser parser[V], onUpdate func(V)) *fetcher[V] {
var ticker *time.Ticker var ticker *time.Ticker
if interval != 0 { if interval != 0 {
ticker = time.NewTicker(interval) ticker = time.NewTicker(interval)
} }
return &fetcher{ return &fetcher[V]{
name: name, name: name,
ticker: ticker, ticker: ticker,
vehicle: vehicle, vehicle: vehicle,
@ -183,3 +183,8 @@ func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, pars
onUpdate: onUpdate, onUpdate: onUpdate,
} }
} }
func getZero[V any]() V {
var result V
return result
}

View File

@ -65,8 +65,13 @@ func (hc *HealthCheck) touch() {
} }
func (hc *HealthCheck) check() { func (hc *HealthCheck) check() {
proxies := hc.proxies
if len(proxies) == 0 {
return
}
b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10)) b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10))
for _, proxy := range hc.proxies { for _, proxy := range proxies {
p := proxy p := proxy
b.Go(p.Name(), func() (bool, error) { b.Go(p.Name(), func() (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout) ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)

View File

@ -20,15 +20,18 @@ type healthCheckSchema struct {
} }
type proxyProviderSchema struct { type proxyProviderSchema struct {
Type string `provider:"type"` Type string `provider:"type"`
Path string `provider:"path"` Path string `provider:"path"`
URL string `provider:"url,omitempty"` URL string `provider:"url,omitempty"`
Interval int `provider:"interval,omitempty"` Interval int `provider:"interval,omitempty"`
Filter string `provider:"filter,omitempty"` Filter string `provider:"filter,omitempty"`
HealthCheck healthCheckSchema `provider:"health-check,omitempty"` HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
ForceCertVerify bool `provider:"force-cert-verify,omitempty"`
PrefixName string `provider:"prefix-name,omitempty"`
Header map[string][]string `provider:"header,omitempty"`
} }
func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvider, error) { func ParseProxyProvider(name string, mapping map[string]any, forceCertVerify bool) (types.ProxyProvider, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true}) decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
schema := &proxyProviderSchema{ schema := &proxyProviderSchema{
@ -36,6 +39,11 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
Lazy: true, Lazy: true,
}, },
} }
if forceCertVerify {
schema.ForceCertVerify = true
}
if err := decoder.Decode(mapping, schema); err != nil { if err := decoder.Decode(mapping, schema); err != nil {
return nil, err return nil, err
} }
@ -53,12 +61,12 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
case "file": case "file":
vehicle = NewFileVehicle(path) vehicle = NewFileVehicle(path)
case "http": case "http":
vehicle = NewHTTPVehicle(schema.URL, path) vehicle = NewHTTPVehicle(schema.URL, path, schema.Header)
default: default:
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type) return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
} }
interval := time.Duration(uint(schema.Interval)) * time.Second interval := time.Duration(uint(schema.Interval)) * time.Second
filter := schema.Filter filter := schema.Filter
return NewProxySetProvider(name, interval, filter, vehicle, hc) return NewProxySetProvider(name, interval, filter, vehicle, hc, schema.ForceCertVerify, schema.PrefixName)
} }

View File

@ -9,10 +9,11 @@ import (
"time" "time"
"github.com/Dreamacro/clash/adapter" "github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/common/convert"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider" types "github.com/Dreamacro/clash/constant/provider"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
) )
const ( const (
@ -23,15 +24,16 @@ type ProxySchema struct {
Proxies []map[string]any `yaml:"proxies"` Proxies []map[string]any `yaml:"proxies"`
} }
// for auto gc // ProxySetProvider for auto gc
type ProxySetProvider struct { type ProxySetProvider struct {
*proxySetProvider *proxySetProvider
} }
type proxySetProvider struct { type proxySetProvider struct {
*fetcher *fetcher[[]C.Proxy]
proxies []C.Proxy proxies []C.Proxy
healthCheck *HealthCheck healthCheck *HealthCheck
providersInUse []types.ProxyProvider
} }
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) { func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
@ -86,17 +88,22 @@ func (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy {
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) { func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
pp.proxies = proxies pp.proxies = proxies
pp.healthCheck.setProxy(proxies) pp.healthCheck.setProxy(proxies)
if pp.healthCheck.auto() {
go pp.healthCheck.check() for _, use := range pp.providersInUse {
_ = use.Update()
} }
} }
func (pp *proxySetProvider) RegisterProvidersInUse(providers ...types.ProxyProvider) {
pp.providersInUse = append(pp.providersInUse, providers...)
}
func stopProxyProvider(pd *ProxySetProvider) { func stopProxyProvider(pd *ProxySetProvider) {
pd.healthCheck.close() pd.healthCheck.close()
pd.fetcher.Destroy() _ = pd.fetcher.Destroy()
} }
func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) { func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck, forceCertVerify bool, prefixName string) (*ProxySetProvider, error) {
filterReg, err := regexp.Compile(filter) filterReg, err := regexp.Compile(filter)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid filter regex: %w", err) return nil, fmt.Errorf("invalid filter regex: %w", err)
@ -111,45 +118,7 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, veh
healthCheck: hc, healthCheck: hc,
} }
onUpdate := func(elm any) { fetcher := newFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, filterReg, forceCertVerify, prefixName), proxiesOnUpdate(pd))
ret := elm.([]C.Proxy)
pd.setProxies(ret)
}
proxiesParseAndFilter := func(buf []byte) (any, error) {
schema := &ProxySchema{}
if err := yaml.Unmarshal(buf, schema); err != nil {
return nil, err
}
if schema.Proxies == nil {
return nil, errors.New("file must have a `proxies` field")
}
proxies := []C.Proxy{}
for idx, mapping := range schema.Proxies {
if name, ok := mapping["name"]; ok && len(filter) > 0 && !filterReg.MatchString(name.(string)) {
continue
}
proxy, err := adapter.ParseProxy(mapping)
if err != nil {
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
}
proxies = append(proxies, proxy)
}
if len(proxies) == 0 {
if len(filter) > 0 {
return nil, errors.New("doesn't match any proxy, please check your filter")
}
return nil, errors.New("file doesn't have any proxy")
}
return proxies, nil
}
fetcher := newFetcher(name, interval, vehicle, proxiesParseAndFilter, onUpdate)
pd.fetcher = fetcher pd.fetcher = fetcher
wrapper := &ProxySetProvider{pd} wrapper := &ProxySetProvider{pd}
@ -157,7 +126,7 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, veh
return wrapper, nil return wrapper, nil
} }
// for auto gc // CompatibleProvider for auto gc
type CompatibleProvider struct { type CompatibleProvider struct {
*compatibleProvider *compatibleProvider
} }
@ -233,3 +202,145 @@ func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*Co
runtime.SetFinalizer(wrapper, stopCompatibleProvider) runtime.SetFinalizer(wrapper, stopCompatibleProvider)
return wrapper, nil return wrapper, nil
} }
// ProxyFilterProvider for filter provider
type ProxyFilterProvider struct {
*proxyFilterProvider
}
type proxyFilterProvider struct {
name string
psd *ProxySetProvider
proxies []C.Proxy
filter *regexp.Regexp
healthCheck *HealthCheck
}
func (pf *proxyFilterProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"name": pf.Name(),
"type": pf.Type().String(),
"vehicleType": pf.VehicleType().String(),
"proxies": pf.Proxies(),
})
}
func (pf *proxyFilterProvider) Name() string {
return pf.name
}
func (pf *proxyFilterProvider) HealthCheck() {
pf.healthCheck.check()
}
func (pf *proxyFilterProvider) Update() error {
var proxies []C.Proxy
if pf.filter != nil {
for _, proxy := range pf.psd.Proxies() {
if !pf.filter.MatchString(proxy.Name()) {
continue
}
proxies = append(proxies, proxy)
}
} else {
proxies = pf.psd.Proxies()
}
pf.proxies = proxies
pf.healthCheck.setProxy(proxies)
return nil
}
func (pf *proxyFilterProvider) Initial() error {
return nil
}
func (pf *proxyFilterProvider) VehicleType() types.VehicleType {
return pf.psd.VehicleType()
}
func (pf *proxyFilterProvider) Type() types.ProviderType {
return types.Proxy
}
func (pf *proxyFilterProvider) Proxies() []C.Proxy {
return pf.proxies
}
func (pf *proxyFilterProvider) ProxiesWithTouch() []C.Proxy {
pf.healthCheck.touch()
return pf.Proxies()
}
func stopProxyFilterProvider(pf *ProxyFilterProvider) {
pf.healthCheck.close()
}
func NewProxyFilterProvider(name string, psd *ProxySetProvider, hc *HealthCheck, filterRegx *regexp.Regexp) *ProxyFilterProvider {
pd := &proxyFilterProvider{
psd: psd,
name: name,
healthCheck: hc,
filter: filterRegx,
}
_ = pd.Update()
if hc.auto() {
go hc.process()
}
wrapper := &ProxyFilterProvider{pd}
runtime.SetFinalizer(wrapper, stopProxyFilterProvider)
return wrapper
}
func proxiesOnUpdate(pd *proxySetProvider) func([]C.Proxy) {
return func(elm []C.Proxy) {
pd.setProxies(elm)
}
}
func proxiesParseAndFilter(filter string, filterReg *regexp.Regexp, forceCertVerify bool, prefixName string) parser[[]C.Proxy] {
return func(buf []byte) ([]C.Proxy, error) {
schema := &ProxySchema{}
if err := yaml.Unmarshal(buf, schema); err != nil {
proxies, err1 := convert.ConvertsV2Ray(buf)
if err1 != nil {
return nil, fmt.Errorf("%w, %s", err, err1.Error())
}
schema.Proxies = proxies
}
if schema.Proxies == nil {
return nil, errors.New("file must have a `proxies` field")
}
proxies := []C.Proxy{}
for idx, mapping := range schema.Proxies {
if name, ok := mapping["name"]; ok && len(filter) > 0 && !filterReg.MatchString(name.(string)) {
continue
}
if prefixName != "" {
mapping["name"] = prefixName + mapping["name"].(string)
}
proxy, err := adapter.ParseProxy(mapping, forceCertVerify)
if err != nil {
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
}
proxies = append(proxies, proxy)
}
if len(proxies) == 0 {
if len(filter) > 0 {
return nil, errors.New("doesn't match any proxy, please check your filter")
}
return nil, errors.New("file doesn't have any proxy")
}
return proxies, nil
}
}

View File

@ -9,7 +9,9 @@ import (
"os" "os"
"time" "time"
"github.com/Dreamacro/clash/common/convert"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider" types "github.com/Dreamacro/clash/constant/provider"
) )
@ -34,8 +36,9 @@ func NewFileVehicle(path string) *FileVehicle {
} }
type HTTPVehicle struct { type HTTPVehicle struct {
url string url string
path string path string
header http.Header
} }
func (h *HTTPVehicle) Type() types.VehicleType { func (h *HTTPVehicle) Type() types.VehicleType {
@ -60,11 +63,17 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
return nil, err return nil, err
} }
if h.header != nil {
req.Header = h.header
}
if user := uri.User; user != nil { if user := uri.User; user != nil {
password, _ := user.Password() password, _ := user.Password()
req.SetBasicAuth(user.Username(), password) req.SetBasicAuth(user.Username(), password)
} }
convert.SetUserAgent(req.Header)
req = req.WithContext(ctx) req = req.WithContext(ctx)
transport := &http.Transport{ transport := &http.Transport{
@ -73,8 +82,13 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
IdleConnTimeout: 90 * time.Second, IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 1 * time.Second,
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { DialContext: func(ctx context.Context, network, address string) (conn net.Conn, err error) {
return dialer.DialContext(ctx, network, address) conn, err = dialer.DialContext(ctx, network, address) // with direct
if err != nil {
// fallback to tun if tun enabled
conn, err = (&net.Dialer{Timeout: C.DefaultTCPTimeout}).Dial(network, address)
}
return
}, },
} }
@ -83,7 +97,9 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close() defer func() {
_ = resp.Body.Close()
}()
buf, err := io.ReadAll(resp.Body) buf, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
@ -93,6 +109,6 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
return buf, nil return buf, nil
} }
func NewHTTPVehicle(url string, path string) *HTTPVehicle { func NewHTTPVehicle(url string, path string, header http.Header) *HTTPVehicle {
return &HTTPVehicle{url, path} return &HTTPVehicle{url, path, header}
} }

View File

@ -61,7 +61,7 @@ func (c *cache[K, V]) GetWithExpire(key K) (payload V, expired time.Time) {
func (c *cache[K, V]) cleanup() { func (c *cache[K, V]) cleanup() {
c.mapping.Range(func(k, v any) bool { c.mapping.Range(func(k, v any) bool {
key := k.(string) key := k
elm := v.(*element[V]) elm := v.(*element[V])
if time.Since(elm.Expired) > 0 { if time.Since(elm.Expired) > 0 {
c.mapping.Delete(key) c.mapping.Delete(key)

344
common/convert/converter.go Normal file
View File

@ -0,0 +1,344 @@
package convert
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"strings"
)
var enc = base64.StdEncoding
func DecodeBase64(buf []byte) ([]byte, error) {
dBuf := make([]byte, enc.DecodedLen(len(buf)))
n, err := enc.Decode(dBuf, buf)
if err != nil {
return nil, err
}
return dBuf[:n], nil
}
// DecodeBase64StringToString decode base64 string to string
func DecodeBase64StringToString(s string) (string, error) {
dBuf, err := enc.DecodeString(s)
if err != nil {
return "", err
}
return string(dBuf), nil
}
// ConvertsV2Ray convert V2Ray subscribe proxies data to clash proxies config
func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
data, err := DecodeBase64(buf)
if err != nil {
data = buf
}
arr := strings.Split(string(data), "\n")
proxies := make([]map[string]any, 0, len(arr))
names := make(map[string]int, 200)
for _, line := range arr {
line = strings.TrimRight(line, " \r")
if line == "" {
continue
}
scheme, body, found := strings.Cut(line, "://")
if !found {
continue
}
scheme = strings.ToLower(scheme)
switch scheme {
case "trojan":
urlTrojan, err := url.Parse(line)
if err != nil {
continue
}
query := urlTrojan.Query()
name := uniqueName(names, urlTrojan.Fragment)
trojan := make(map[string]any, 20)
trojan["name"] = name
trojan["type"] = scheme
trojan["server"] = urlTrojan.Hostname()
trojan["port"] = urlTrojan.Port()
trojan["password"] = urlTrojan.User.Username()
trojan["udp"] = true
trojan["skip-cert-verify"] = false
sni := query.Get("sni")
if sni != "" {
trojan["sni"] = sni
}
network := strings.ToLower(query.Get("type"))
if network != "" {
trojan["network"] = network
}
if network == "ws" {
headers := make(map[string]any)
wsOpts := make(map[string]any)
headers["Host"] = RandHost()
headers["User-Agent"] = RandUserAgent()
wsOpts["path"] = query.Get("path")
wsOpts["headers"] = headers
trojan["ws-opts"] = wsOpts
}
proxies = append(proxies, trojan)
case "vmess":
dcBuf, err := enc.DecodeString(body)
if err != nil {
continue
}
jsonDc := json.NewDecoder(bytes.NewReader(dcBuf))
values := make(map[string]any, 20)
if jsonDc.Decode(&values) != nil {
continue
}
name := uniqueName(names, values["ps"].(string))
vmess := make(map[string]any, 20)
vmess["name"] = name
vmess["type"] = scheme
vmess["server"] = values["add"]
vmess["port"] = values["port"]
vmess["uuid"] = values["id"]
vmess["alterId"] = values["aid"]
vmess["cipher"] = "auto"
vmess["udp"] = true
vmess["skip-cert-verify"] = false
host := values["host"]
network := strings.ToLower(values["net"].(string))
vmess["network"] = network
tls := strings.ToLower(values["tls"].(string))
if tls != "" && tls != "0" && tls != "null" {
if host != nil {
vmess["servername"] = host
}
vmess["tls"] = true
}
if network == "ws" {
headers := make(map[string]any)
wsOpts := make(map[string]any)
headers["Host"] = RandHost()
headers["User-Agent"] = RandUserAgent()
if values["path"] != nil {
wsOpts["path"] = values["path"]
}
wsOpts["headers"] = headers
vmess["ws-opts"] = wsOpts
}
proxies = append(proxies, vmess)
case "ss":
urlSS, err := url.Parse(line)
if err != nil {
continue
}
name := uniqueName(names, urlSS.Fragment)
port := urlSS.Port()
if port == "" {
dcBuf, err := enc.DecodeString(urlSS.Host)
if err != nil {
continue
}
urlSS, err = url.Parse("ss://" + string(dcBuf))
if err != nil {
continue
}
}
var (
cipher = urlSS.User.Username()
password string
)
if password, found = urlSS.User.Password(); !found {
dcBuf, err := enc.DecodeString(cipher)
if err != nil {
continue
}
cipher, password, found = strings.Cut(string(dcBuf), ":")
if !found {
continue
}
}
ss := make(map[string]any, 20)
ss["name"] = name
ss["type"] = scheme
ss["server"] = urlSS.Hostname()
ss["port"] = urlSS.Port()
ss["cipher"] = cipher
ss["password"] = password
ss["udp"] = true
proxies = append(proxies, ss)
case "ssr":
dcBuf, err := enc.DecodeString(body)
if err != nil {
continue
}
// ssr://host:port:protocol:method:obfs:urlsafebase64pass/?obfsparam=urlsafebase64&protoparam=&remarks=urlsafebase64&group=urlsafebase64&udpport=0&uot=1
before, after, ok := strings.Cut(string(dcBuf), "/?")
if !ok {
continue
}
beforeArr := strings.Split(before, ":")
if len(beforeArr) != 6 {
continue
}
host := beforeArr[0]
port := beforeArr[1]
protocol := beforeArr[2]
method := beforeArr[3]
obfs := beforeArr[4]
password := decodeUrlSafe(urlSafe(beforeArr[5]))
query, err := url.ParseQuery(urlSafe(after))
if err != nil {
continue
}
remarks := decodeUrlSafe(query.Get("remarks"))
name := uniqueName(names, remarks)
obfsParam := decodeUrlSafe(query.Get("obfsparam"))
protocolParam := query.Get("protoparam")
ssr := make(map[string]any, 20)
ssr["name"] = name
ssr["type"] = scheme
ssr["server"] = host
ssr["port"] = port
ssr["cipher"] = method
ssr["password"] = password
ssr["obfs"] = obfs
ssr["protocol"] = protocol
ssr["udp"] = true
if obfsParam != "" {
ssr["obfs-param"] = obfsParam
}
if protocolParam != "" {
ssr["protocol-param"] = protocolParam
}
proxies = append(proxies, ssr)
case "vless":
urlVless, err := url.Parse(line)
if err != nil {
continue
}
query := urlVless.Query()
name := uniqueName(names, urlVless.Fragment)
vless := make(map[string]any, 20)
vless["name"] = name
vless["type"] = scheme
vless["server"] = urlVless.Hostname()
vless["port"] = urlVless.Port()
vless["uuid"] = urlVless.User.Username()
vless["udp"] = true
vless["skip-cert-verify"] = false
sni := query.Get("sni")
if sni != "" {
vless["servername"] = sni
}
flow := strings.ToLower(query.Get("flow"))
if flow != "" {
vless["flow"] = flow
}
network := strings.ToLower(query.Get("type"))
if network != "" {
vless["network"] = network
}
if network == "ws" {
headers := make(map[string]any)
wsOpts := make(map[string]any)
headers["Host"] = RandHost()
headers["User-Agent"] = RandUserAgent()
wsOpts["path"] = query.Get("path")
wsOpts["headers"] = headers
vless["ws-opts"] = wsOpts
}
proxies = append(proxies, vless)
}
}
if len(proxies) == 0 {
return nil, fmt.Errorf("convert v2ray subscribe error: format invalid")
}
return proxies, nil
}
func urlSafe(data string) string {
return strings.ReplaceAll(strings.ReplaceAll(data, "+", "-"), "/", "_")
}
func decodeUrlSafe(data string) string {
dcBuf, err := base64.URLEncoding.DecodeString(data)
if err != nil {
return ""
}
return string(dcBuf)
}
func uniqueName(names map[string]int, name string) string {
if index, ok := names[name]; ok {
index++
names[name] = index
name = fmt.Sprintf("%s-%02d", name, index)
} else {
index = 0
names[name] = index
}
return name
}

315
common/convert/util.go Normal file
View File

@ -0,0 +1,315 @@
package convert
import (
"encoding/base64"
"math/rand"
"net/http"
"strings"
"github.com/gofrs/uuid"
)
var hostsSuffix = []string{
"-cdn.aliyuncs.com",
".alicdn.com",
".pan.baidu.com",
".tbcache.com",
".aliyuncdn.com",
".vod.miguvideo.com",
".cibntv.net",
".myqcloud.com",
".smtcdns.com",
".alikunlun.com",
".smtcdns.net",
".apcdns.net",
".cdn-go.cn",
".cdntip.com",
".cdntips.com",
".alidayu.com",
".alidns.com",
".cdngslb.com",
".mxhichina.com",
}
var userAgents = []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; Moto C Build/NRD90M.059) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1.1; SM-J120M Build/LMY47X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; Moto G (5) Build/NPPS25.137-93-14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G570M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; CAM-L03 Build/HUAWEICAM-L03) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3",
"Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.237 Safari/534.10",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36",
"Mozilla/5.0 (X11; Datanyze; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1.1; SM-J111M Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-J700M Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36",
"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Slackware/Chrome/12.0.742.100 Safari/534.30",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.167 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Linux; Android 8.0.0; WAS-LX3 Build/HUAWEIWAS-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.1805 Safari/537.36 MVisionPlayer/1.0.0.0",
"Mozilla/5.0 (Linux; Android 7.0; TRT-LX3 Build/HUAWEITRT-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; vivo 1610 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36",
"Mozilla/5.0 (Linux; Android 4.4.2; de-de; SAMSUNG GT-I9195 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36",
"Mozilla/5.0 (Linux; Android 8.0.0; ANE-LX3 Build/HUAWEIANE-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (X11; U; Linux i586; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G610M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-J500M Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.104 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; vivo 1606 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G610M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1; vivo 1716 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.93 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G570M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; MYA-L22 Build/HUAWEIMYA-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1; A1601 Build/LMY47I) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; TRT-LX2 Build/HUAWEITRT-LX2; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/59.0.3071.125 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17",
"Mozilla/5.0 (Linux; Android 6.0; CAM-L21 Build/HUAWEICAM-L21; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24",
"Mozilla/5.0 (Linux; Android 7.1.2; Redmi 4X Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
"Mozilla/5.0 (Linux; Android 4.4.2; SM-G7102 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1; HUAWEI CUN-L22 Build/HUAWEICUN-L22; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1.1; A37fw Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-J730GM Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G610F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1.2; Redmi Note 5A Build/N2G47H; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; Redmi Note 4 Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
"Mozilla/5.0 (Unknown; Linux) AppleWebKit/538.1 (KHTML, like Gecko) Chrome/v1.0.0 Safari/538.1",
"Mozilla/5.0 (Linux; Android 7.0; BLL-L22 Build/HUAWEIBLL-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-J710F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1.1; CPH1723 Build/N6F26Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
"Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3 Build/HUAWEIFIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1; Mi A1 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36 MVisionPlayer/1.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1; A37f Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; CPH1607 Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; vivo 1603 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; Redmi 4A Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.116 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.143 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532G Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.83 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; vivo 1713 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
}
var (
hostsLen = len(hostsSuffix)
uaLen = len(userAgents)
)
func RandHost() string {
id, _ := uuid.NewV4()
base := strings.ToLower(base64.RawURLEncoding.EncodeToString(id.Bytes()))
base = strings.ReplaceAll(base, "-", "")
base = strings.ReplaceAll(base, "_", "")
buf := []byte(base)
prefix := string(buf[:3]) + "---"
prefix += string(buf[6:8]) + "-"
prefix += string(buf[len(buf)-8:])
return prefix + hostsSuffix[rand.Intn(hostsLen)]
}
func RandUserAgent() string {
return userAgents[rand.Intn(uaLen)]
}
func SetUserAgent(header http.Header) {
if header.Get("User-Agent") != "" {
return
}
userAgent := RandUserAgent()
header.Set("User-Agent", userAgent)
}

View File

@ -52,8 +52,8 @@ func (alloc *Allocator) Put(buf []byte) error {
return errors.New("allocator Put() incorrect buffer size") return errors.New("allocator Put() incorrect buffer size")
} }
//lint:ignore SA6002 ignore temporarily
//nolint //nolint
//lint:ignore SA6002 ignore temporarily
alloc.buffers[bits].Put(buf) alloc.buffers[bits].Put(buf)
return nil return nil
} }

View File

@ -25,7 +25,6 @@ type Result[T any] struct {
} }
// Do single.Do likes sync.singleFlight // Do single.Do likes sync.singleFlight
//lint:ignore ST1008 it likes sync.singleFlight
func (s *Single[T]) Do(fn func() (T, error)) (v T, err error, shared bool) { func (s *Single[T]) Do(fn func() (T, error)) (v T, err error, shared bool) {
s.mux.Lock() s.mux.Lock()
now := time.Now() now := time.Now()

View File

@ -2,9 +2,11 @@ package geodata
import ( import (
"github.com/Dreamacro/clash/component/geodata/router" "github.com/Dreamacro/clash/component/geodata/router"
"golang.org/x/exp/maps"
) )
func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error) { func loadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error) {
geoLoaderName := "standard" geoLoaderName := "standard"
geoLoader, err := GetGeoDataLoader(geoLoaderName) geoLoader, err := GetGeoDataLoader(geoLoaderName)
if err != nil { if err != nil {
@ -28,3 +30,33 @@ func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error)
return matcher, len(domains), nil return matcher, len(domains), nil
} }
var ruleProviders = make(map[string]*router.DomainMatcher)
// HasProvider has geo site provider by county code
func HasProvider(countyCode string) (ok bool) {
_, ok = ruleProviders[countyCode]
return ok
}
// GetProvidersList get geo site providers
func GetProvidersList(countyCode string) []*router.DomainMatcher {
return maps.Values(ruleProviders)
}
// GetProviderByCode get geo site provider by county code
func GetProviderByCode(countyCode string) (matcher *router.DomainMatcher, ok bool) {
matcher, ok = ruleProviders[countyCode]
return
}
func LoadProviderByCode(countyCode string) (matcher *router.DomainMatcher, count int, err error) {
var ok bool
matcher, ok = ruleProviders[countyCode]
if !ok {
if matcher, count, err = loadGeoSiteMatcher(countyCode); err == nil {
ruleProviders[countyCode] = matcher
}
}
return
}

View File

@ -683,8 +683,9 @@ int call_shortcut(PyObject *shortcut_fn,
PyObject *args; PyObject *args;
PyObject *result; PyObject *result;
args = Py_BuildValue("{s:O, s:s, s:s, s:s, s:s, s:s, s:H, s:s, s:H}", args = Py_BuildValue("{s:O, s:s, s:s, s:s, s:s, s:s, s:s, s:H, s:s, s:H}",
"ctx", clash_context, "ctx", clash_context,
"type", type,
"network", network, "network", network,
"process_name", process_name, "process_name", process_name,
"process_path", process_path, "process_path", process_path,

View File

@ -20,6 +20,7 @@ import (
"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"
_ "github.com/Dreamacro/clash/component/geodata/standard"
S "github.com/Dreamacro/clash/component/script" S "github.com/Dreamacro/clash/component/script"
"github.com/Dreamacro/clash/component/trie" "github.com/Dreamacro/clash/component/trie"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
@ -31,7 +32,7 @@ import (
R "github.com/Dreamacro/clash/rule" R "github.com/Dreamacro/clash/rule"
T "github.com/Dreamacro/clash/tunnel" T "github.com/Dreamacro/clash/tunnel"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
) )
// General config // General config
@ -41,6 +42,7 @@ type General struct {
Mode T.TunnelMode `json:"mode"` Mode T.TunnelMode `json:"mode"`
LogLevel log.LogLevel `json:"log-level"` LogLevel log.LogLevel `json:"log-level"`
IPv6 bool `json:"ipv6"` IPv6 bool `json:"ipv6"`
Sniffing bool `json:"sniffing"`
Interface string `json:"-"` Interface string `json:"-"`
RoutingMark int `json:"-"` RoutingMark int `json:"-"`
Tun Tun `json:"tun"` Tun Tun `json:"tun"`
@ -99,16 +101,18 @@ type Profile struct {
// Tun config // Tun config
type Tun struct { type Tun struct {
Enable bool `yaml:"enable" json:"enable"` Enable bool `yaml:"enable" json:"enable"`
Device string `yaml:"device" json:"device"` Device string `yaml:"device" json:"device"`
Stack C.TUNStack `yaml:"stack" json:"stack"` Stack C.TUNStack `yaml:"stack" json:"stack"`
DNSHijack []netip.AddrPort `yaml:"dns-hijack" json:"dns-hijack"` DNSHijack []C.DNSUrl `yaml:"dns-hijack" json:"dns-hijack"`
AutoRoute bool `yaml:"auto-route" json:"auto-route"` AutoRoute bool `yaml:"auto-route" json:"auto-route"`
AutoDetectInterface bool `yaml:"auto-detect-interface" json:"auto-detect-interface"`
} }
// Script config // Script config
type Script struct { type Script struct {
MainCode string `yaml:"code" json:"code"` MainCode string `yaml:"code" json:"code"`
MainPath string `yaml:"path" json:"path"`
ShortcutsCode map[string]string `yaml:"shortcuts" json:"shortcuts"` ShortcutsCode map[string]string `yaml:"shortcuts" json:"shortcuts"`
} }
@ -130,7 +134,6 @@ type Experimental struct{}
// Config is clash config manager // Config is clash config manager
type Config struct { type Config struct {
General *General General *General
Tun *Tun
IPTables *IPTables IPTables *IPTables
Mitm *Mitm Mitm *Mitm
DNS *DNS DNS *DNS
@ -168,14 +171,6 @@ type RawFallbackFilter struct {
GeoSite []string `yaml:"geosite"` GeoSite []string `yaml:"geosite"`
} }
type RawTun struct {
Enable bool `yaml:"enable" json:"enable"`
Device string `yaml:"device" json:"device"`
Stack C.TUNStack `yaml:"stack" json:"stack"`
DNSHijack []string `yaml:"dns-hijack" json:"dns-hijack"`
AutoRoute bool `yaml:"auto-route" json:"auto-route"`
}
type RawMitm struct { type RawMitm struct {
Hosts []string `yaml:"hosts" json:"hosts"` Hosts []string `yaml:"hosts" json:"hosts"`
Rules []string `yaml:"rules" json:"rules"` Rules []string `yaml:"rules" json:"rules"`
@ -199,11 +194,13 @@ type RawConfig struct {
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"`
Sniffing bool `yaml:"sniffing"`
ForceCertVerify bool `yaml:"force-cert-verify"`
ProxyProvider map[string]map[string]any `yaml:"proxy-providers"` ProxyProvider map[string]map[string]any `yaml:"proxy-providers"`
Hosts map[string]string `yaml:"hosts"` Hosts map[string]string `yaml:"hosts"`
DNS RawDNS `yaml:"dns"` DNS RawDNS `yaml:"dns"`
Tun RawTun `yaml:"tun"` Tun Tun `yaml:"tun"`
IPTables IPTables `yaml:"iptables"` IPTables IPTables `yaml:"iptables"`
MITM RawMitm `yaml:"mitm"` MITM RawMitm `yaml:"mitm"`
Experimental Experimental `yaml:"experimental"` Experimental Experimental `yaml:"experimental"`
@ -227,21 +224,37 @@ func Parse(buf []byte) (*Config, error) {
func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
// config with default value // config with default value
rawCfg := &RawConfig{ rawCfg := &RawConfig{
AllowLan: false, AllowLan: false,
BindAddress: "*", Sniffing: false,
Mode: T.Rule, ForceCertVerify: false,
Authentication: []string{}, BindAddress: "*",
LogLevel: log.INFO, Mode: T.Rule,
Hosts: map[string]string{}, Authentication: []string{},
Rule: []string{}, LogLevel: log.INFO,
Proxy: []map[string]any{}, Hosts: map[string]string{},
ProxyGroup: []map[string]any{}, Rule: []string{},
Tun: RawTun{ Proxy: []map[string]any{},
Enable: false, ProxyGroup: []map[string]any{},
Device: "", Tun: Tun{
Stack: C.TunGvisor, Enable: false,
DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query Device: "",
AutoRoute: true, Stack: C.TunGvisor,
DNSHijack: []C.DNSUrl{ // default hijack all dns lookup
{
Network: "udp",
AddrPort: C.DNSAddrPort{
AddrPort: netip.MustParseAddrPort("0.0.0.0:53"),
},
},
{
Network: "tcp",
AddrPort: C.DNSAddrPort{
AddrPort: netip.MustParseAddrPort("0.0.0.0:53"),
},
},
},
AutoRoute: false,
AutoDetectInterface: false,
}, },
IPTables: IPTables{ IPTables: IPTables{
Enable: false, Enable: false,
@ -263,7 +276,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
"223.5.5.5", "223.5.5.5",
}, },
NameServer: []string{ // default if user not set NameServer: []string{ // default if user not set
"https://doh.pub/dns-query", "https://120.53.53.53/dns-query",
"tls://223.5.5.5:853", "tls://223.5.5.5:853",
}, },
}, },
@ -296,14 +309,6 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
} }
config.General = general config.General = general
tunCfg, err := parseTun(rawCfg.Tun, config.General)
if err != nil {
return nil, err
}
config.Tun = tunCfg
dialer.DefaultInterface.Store(config.General.Interface)
proxies, providers, err := parseProxies(rawCfg) proxies, providers, err := parseProxies(rawCfg)
if err != nil { if err != nil {
return nil, err return nil, err
@ -311,9 +316,11 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
config.Proxies = proxies config.Proxies = proxies
config.Providers = providers config.Providers = providers
if err = parseScript(rawCfg.Script); err != nil { rawRules, err := parseScript(rawCfg.Script, rawCfg.Rule)
if err != nil {
return nil, err return nil, err
} }
rawCfg.Rule = rawRules
rules, ruleProviders, err := parseRules(rawCfg, proxies) rules, ruleProviders, err := parseRules(rawCfg, proxies)
if err != nil { if err != nil {
@ -328,7 +335,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
} }
config.Hosts = hosts config.Hosts = hosts
dnsCfg, err := parseDNS(rawCfg, hosts, rules) dnsCfg, err := parseDNS(rawCfg, hosts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -357,6 +364,19 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
} }
} }
if cfg.Tun.Enable && cfg.Tun.AutoDetectInterface {
outboundInterface, err := commons.GetAutoDetectInterface()
if err != nil && cfg.Interface == "" {
return nil, fmt.Errorf("get auto detect interface fail: %w", err)
}
if outboundInterface != "" {
cfg.Interface = outboundInterface
}
}
dialer.DefaultInterface.Store(cfg.Interface)
return &General{ return &General{
Inbound: Inbound{ Inbound: Inbound{
Port: cfg.Port, Port: cfg.Port,
@ -378,6 +398,8 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
IPv6: cfg.IPv6, IPv6: cfg.IPv6,
Interface: cfg.Interface, Interface: cfg.Interface,
RoutingMark: cfg.RoutingMark, RoutingMark: cfg.RoutingMark,
Sniffing: cfg.Sniffing,
Tun: cfg.Tun,
}, nil }, nil
} }
@ -387,6 +409,7 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
proxiesConfig := cfg.Proxy proxiesConfig := cfg.Proxy
groupsConfig := cfg.ProxyGroup groupsConfig := cfg.ProxyGroup
providersConfig := cfg.ProxyProvider providersConfig := cfg.ProxyProvider
forceCertVerify := cfg.ForceCertVerify
var proxyList []string var proxyList []string
@ -396,7 +419,7 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
// parse proxy // parse proxy
for idx, mapping := range proxiesConfig { for idx, mapping := range proxiesConfig {
proxy, err := adapter.ParseProxy(mapping) proxy, err := adapter.ParseProxy(mapping, forceCertVerify)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("proxy %d: %w", idx, err) return nil, nil, fmt.Errorf("proxy %d: %w", idx, err)
} }
@ -428,7 +451,7 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
return nil, nil, fmt.Errorf("can not defined a provider called `%s`", provider.ReservedName) return nil, nil, fmt.Errorf("can not defined a provider called `%s`", provider.ReservedName)
} }
pd, err := provider.ParseProxyProvider(name, mapping) pd, err := provider.ParseProxyProvider(name, mapping, forceCertVerify)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("parse proxy provider %s error: %w", name, err) return nil, nil, fmt.Errorf("parse proxy provider %s error: %w", name, err)
} }
@ -437,7 +460,7 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
} }
for _, proxyProvider := range providersMap { for _, proxyProvider := range providersMap {
log.Infoln("Start initial provider %s", proxyProvider.Name()) log.Infoln("Start initial proxy provider %s", proxyProvider.Name())
if err := proxyProvider.Initial(); err != nil { if err := proxyProvider.Initial(); err != nil {
return nil, nil, fmt.Errorf("initial proxy provider %s error: %w", proxyProvider.Name(), err) return nil, nil, fmt.Errorf("initial proxy provider %s error: %w", proxyProvider.Name(), err)
} }
@ -492,10 +515,10 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[strin
var ( var (
rules []C.Rule rules []C.Rule
providerNames []string providerNames []string
foundRP bool
ruleProviders = map[string]C.Rule{} ruleProviders = map[string]C.Rule{}
rulesConfig = cfg.Rule rulesConfig = cfg.Rule
mode = cfg.Mode
) )
// parse rules // parse rules
@ -530,10 +553,16 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[strin
target = rule[l-1] target = rule[l-1]
params = rule[l:] params = rule[l:]
if _, ok := proxies[target]; !ok && (mode != T.Script || ruleName != "GEOSITE") { if _, ok := proxies[target]; !ok && ruleName != "GEOSITE" && target != C.ScriptRuleGeoSiteTarget {
return nil, nil, fmt.Errorf("rules[%d] [%s] error: proxy [%s] not found", idx, line, target) return nil, nil, fmt.Errorf("rules[%d] [%s] error: proxy [%s] not found", idx, line, target)
} }
pvName := "geosite:" + strings.ToLower(payload)
_, foundRP = ruleProviders[pvName]
if ruleName == "GEOSITE" && target == C.ScriptRuleGeoSiteTarget && foundRP {
continue
}
params = trimArr(params) params = trimArr(params)
parsed, parseErr := R.ParseRule(ruleName, payload, target, params) parsed, parseErr := R.ParseRule(ruleName, payload, target, params)
@ -541,8 +570,7 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[strin
return nil, nil, fmt.Errorf("rules[%d] [%s] error: %s", idx, line, parseErr.Error()) return nil, nil, fmt.Errorf("rules[%d] [%s] error: %s", idx, line, parseErr.Error())
} }
if ruleName == "GEOSITE" { if ruleName == "GEOSITE" && !foundRP {
pvName := "geosite:" + strings.ToLower(payload)
providerNames = append(providerNames, pvName) providerNames = append(providerNames, pvName)
ruleProviders[pvName] = parsed ruleProviders[pvName] = parsed
} }
@ -687,37 +715,27 @@ func parseFallbackIPCIDR(ips []string) ([]*netip.Prefix, error) {
return ipNets, nil return ipNets, nil
} }
func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainMatcher, error) { func parseFallbackGeoSite(countries []string) ([]*router.DomainMatcher, error) {
var sites []*router.DomainMatcher var sites []*router.DomainMatcher
for _, country := range countries { for _, country := range countries {
found := false matcher, recordsCount, err := geodata.LoadProviderByCode(country)
for _, rule := range rules { if err != nil {
if rule.RuleType() == C.GEOSITE { return nil, err
if strings.EqualFold(country, rule.Payload()) {
found = true
sites = append(sites, rule.(C.RuleGeoSite).GetDomainMatcher())
log.Infoln("Start initial GeoSite dns fallback filter from rule `%s`", country)
}
}
} }
if !found { sites = append(sites, matcher)
matcher, recordsCount, err := geodata.LoadGeoSiteMatcher(country)
if err != nil {
return nil, err
}
sites = append(sites, matcher) cont := fmt.Sprintf("%d", recordsCount)
if recordsCount == 0 {
log.Infoln("Start initial GeoSite dns fallback filter `%s`, records: %d", country, recordsCount) cont = "from cache"
} }
log.Infoln("Start initial GeoSite dns fallback filter `%s`, records: %s", country, cont)
} }
runtime.GC() runtime.GC()
return sites, nil return sites, nil
} }
func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.Rule) (*DNS, error) { func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr]) (*DNS, error) {
cfg := rawCfg.DNS cfg := rawCfg.DNS
if cfg.Enable && len(cfg.NameServer) == 0 { if cfg.Enable && len(cfg.NameServer) == 0 {
return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty") return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty")
@ -811,7 +829,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.R
dnsCfg.FallbackFilter.IPCIDR = fallbackip dnsCfg.FallbackFilter.IPCIDR = fallbackip
} }
dnsCfg.FallbackFilter.Domain = cfg.FallbackFilter.Domain dnsCfg.FallbackFilter.Domain = cfg.FallbackFilter.Domain
fallbackGeoSite, err := parseFallbackGeoSite(cfg.FallbackFilter.GeoSite, rules) fallbackGeoSite, err := parseFallbackGeoSite(cfg.FallbackFilter.GeoSite)
if err != nil { if err != nil {
return nil, fmt.Errorf("load GeoSite dns fallback filter error, %w", err) return nil, fmt.Errorf("load GeoSite dns fallback filter error, %w", err)
} }
@ -826,7 +844,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.R
} }
func parseAuthentication(rawRecords []string) []auth.AuthUser { func parseAuthentication(rawRecords []string) []auth.AuthUser {
users := []auth.AuthUser{} var users []auth.AuthUser
for _, line := range rawRecords { for _, line := range rawRecords {
if user, pass, found := strings.Cut(line, ":"); found { if user, pass, found := strings.Cut(line, ":"); found {
users = append(users, auth.AuthUser{User: user, Pass: pass}) users = append(users, auth.AuthUser{User: user, Pass: pass})
@ -835,44 +853,28 @@ func parseAuthentication(rawRecords []string) []auth.AuthUser {
return users return users
} }
func parseTun(rawTun RawTun, general *General) (*Tun, error) { func parseScript(script Script, rawRules []string) ([]string, error) {
if (rawTun.Enable || general.TProxyPort != 0) && general.Interface == "" { var (
autoDetectInterfaceName, err := commons.GetAutoDetectInterface() path = script.MainPath
if err != nil || autoDetectInterfaceName == "" { mainCode = script.MainCode
return nil, fmt.Errorf("can not find auto detect interface: %w. you must be detect `interface-name` if tun set to enable or `tproxy-port` isn't zore", err) shortcutsCode = script.ShortcutsCode
)
if path != "" {
if !strings.HasSuffix(path, ".star") {
return nil, fmt.Errorf("initialized script file failure, script path [%s] invalid", path)
} }
path = C.Path.Resolve(path)
general.Interface = autoDetectInterfaceName if _, err := os.Stat(path); os.IsNotExist(err) {
} return nil, fmt.Errorf("initialized script file failure, script path invalid: %w", err)
var dnsHijack []netip.AddrPort
for _, d := range rawTun.DNSHijack {
if _, after, ok := strings.Cut(d, "://"); ok {
d = after
} }
data, err := os.ReadFile(path)
addrPort, err := netip.ParseAddrPort(d)
if err != nil { if err != nil {
return nil, fmt.Errorf("parse dns-hijack url error: %w", err) return nil, fmt.Errorf("initialized script file failure, read file error: %w", err)
} }
mainCode = string(data)
dnsHijack = append(dnsHijack, addrPort)
} }
return &Tun{
Enable: rawTun.Enable,
Device: rawTun.Device,
Stack: rawTun.Stack,
DNSHijack: dnsHijack,
AutoRoute: rawTun.AutoRoute,
}, nil
}
func parseScript(script Script) error {
mainCode := script.MainCode
shortcutsCode := script.ShortcutsCode
if strings.TrimSpace(mainCode) == "" { if strings.TrimSpace(mainCode) == "" {
mainCode = ` mainCode = `
def main(ctx, metadata): def main(ctx, metadata):
@ -882,6 +884,10 @@ def main(ctx, metadata):
mainCode = cleanPyKeywords(mainCode) mainCode = cleanPyKeywords(mainCode)
} }
if !strings.Contains(mainCode, "def main(ctx, metadata):") {
return nil, fmt.Errorf(`initialized script code failure, the function 'def main(ctx, metadata):' is required`)
}
content := `# -*- coding: UTF-8 -*- content := `# -*- coding: UTF-8 -*-
from datetime import datetime as whatever from datetime import datetime as whatever
@ -898,6 +904,18 @@ class ClashTime:
time = ClashTime() time = ClashTime()
class ClashRuleProvider:
def __init__(self, c, m):
self.__ctx = c
self.__metadata = m
def match_provider(self, providerName):
try:
return self.__ctx.rule_providers[providerName].match(self.__metadata)
except Exception as err:
self.__ctx.log("[SCRIPT] shortcuts error: rule provider {0} not found".format(err))
return False
` `
content += mainCode + "\n\n" content += mainCode + "\n\n"
@ -906,28 +924,57 @@ time = ClashTime()
v = cleanPyKeywords(v) v = cleanPyKeywords(v)
v = strings.TrimSpace(v) v = strings.TrimSpace(v)
if v == "" { if v == "" {
return fmt.Errorf("initialized rule SCRIPT failure, shortcut [%s] code invalid syntax", k) return nil, fmt.Errorf("initialized rule SCRIPT failure, shortcut [%s] code syntax invalid", k)
} }
content += "def " + strings.ToLower(k) + "(ctx, network, process_name, process_path, host, src_ip, src_port, dst_ip, dst_port):\n return " + v + "\n\n" content += "def " + strings.ToLower(k) + "(ctx, type, network, process_name, process_path, host, src_ip, src_port, dst_ip, dst_port):"
if strings.Contains(v, "match_provider") {
content += `
metadata = {
"type": type,
"network": network,
"process_name": process_name,
"process_path": process_path,
"host": host,
"src_ip": src_ip,
"src_port": src_port,
"dst_ip": dst_ip,
"dst_port": dst_port
}
crp = ClashRuleProvider(ctx, metadata)
match_provider = crp.match_provider`
}
content += `
now = time.now()
return ` + v + "\n\n"
} }
err := os.WriteFile(C.Path.Script(), []byte(content), 0o644) err := os.WriteFile(C.Path.Script(), []byte(content), 0o644)
if err != nil { if err != nil {
return fmt.Errorf("initialized script module failure, %w", err) return nil, fmt.Errorf("initialized script module failure, %w", err)
} }
if err = S.Py_Initialize(C.Path.GetExecutableFullPath(), C.Path.ScriptDir()); err != nil { if err = S.Py_Initialize(C.Path.GetExecutableFullPath(), C.Path.ScriptDir()); err != nil {
return fmt.Errorf("initialized script module failure, %w", err) return nil, fmt.Errorf("initialized script module failure, %w", err)
} }
if err = S.LoadMainFunction(); err != nil { if err = S.LoadMainFunction(); err != nil {
return fmt.Errorf("initialized script module failure, %w", err) return nil, fmt.Errorf("initialized script module failure, %w", err)
}
rpdArr := findRuleProvidersName(content)
for _, v := range rpdArr {
if !strings.HasPrefix(v, "geosite:") {
return nil, fmt.Errorf("initialized script module failure, rule provider name must be start with \"geosite:\"")
}
v = strings.ToLower(v)
rule := fmt.Sprintf("GEOSITE,%s,%s", strings.TrimPrefix(v, "geosite:"), C.ScriptRuleGeoSiteTarget)
rawRules = append(rawRules, rule)
} }
log.Infoln("Start initial script module successful, version: %s", S.Py_GetVersion()) log.Infoln("Start initial script module successful, version: %s", S.Py_GetVersion())
return nil return rawRules, nil
} }
func cleanPyKeywords(code string) string { func cleanPyKeywords(code string) string {
@ -940,6 +987,23 @@ func cleanPyKeywords(code string) string {
return code return code
} }
func findRuleProvidersName(s string) []string {
ruleProviderRegx := regexp.MustCompile("ctx.rule_providers\\[[\"'](\\S+)[\"']\\]\\.match|match_provider\\([\"'](\\S+)[\"']\\)")
arr := ruleProviderRegx.FindAllStringSubmatch(s, -1)
var rpd []string
for _, rpdArr := range arr {
for i, v := range rpdArr {
if i == 0 || v == "" {
continue
}
rpd = append(rpd, v)
}
}
return rpd
}
func parseMitm(rawMitm RawMitm) (*Mitm, error) { func parseMitm(rawMitm RawMitm) (*Mitm, error) {
var ( var (
req []C.Rewrite req []C.Rewrite

View File

@ -6,13 +6,14 @@ import (
"net/http" "net/http"
"os" "os"
"github.com/Dreamacro/clash/common/convert"
"github.com/Dreamacro/clash/component/mmdb" "github.com/Dreamacro/clash/component/mmdb"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
) )
func downloadMMDB(path string) (err error) { func downloadMMDB(path string) (err error) {
resp, err := http.Get("https://cdn.jsdelivr.net/gh/Loyalsoldier/geoip@release/Country.mmdb") resp, err := doGet("https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb")
if err != nil { if err != nil {
return return
} }
@ -32,18 +33,18 @@ func initMMDB() error {
if _, err := os.Stat(C.Path.MMDB()); os.IsNotExist(err) { if _, err := os.Stat(C.Path.MMDB()); os.IsNotExist(err) {
log.Infoln("Can't find MMDB, start download") log.Infoln("Can't find MMDB, start download")
if err := downloadMMDB(C.Path.MMDB()); err != nil { if err := downloadMMDB(C.Path.MMDB()); err != nil {
return fmt.Errorf("can't download MMDB: %s", err.Error()) return fmt.Errorf("can't download MMDB: %w", err)
} }
} }
if !mmdb.Verify() { if !mmdb.Verify() {
log.Warnln("MMDB invalid, remove and download") log.Warnln("MMDB invalid, remove and download")
if err := os.Remove(C.Path.MMDB()); err != nil { if err := os.Remove(C.Path.MMDB()); err != nil {
return fmt.Errorf("can't remove invalid MMDB: %s", err.Error()) return fmt.Errorf("can't remove invalid MMDB: %w", err)
} }
if err := downloadMMDB(C.Path.MMDB()); err != nil { if err := downloadMMDB(C.Path.MMDB()); err != nil {
return fmt.Errorf("can't download MMDB: %s", err.Error()) return fmt.Errorf("can't download MMDB: %w", err)
} }
} }
@ -51,7 +52,7 @@ func initMMDB() error {
} }
func downloadGeoSite(path string) (err error) { func downloadGeoSite(path string) (err error) {
resp, err := http.Get("https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat") resp, err := doGet("https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat")
if err != nil { if err != nil {
return return
} }
@ -71,7 +72,7 @@ func initGeoSite() error {
if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) { if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) {
log.Infoln("Can't find GeoSite.dat, start download") log.Infoln("Can't find GeoSite.dat, start download")
if err := downloadGeoSite(C.Path.GeoSite()); err != nil { if err := downloadGeoSite(C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't download GeoSite.dat: %s", err.Error()) return fmt.Errorf("can't download GeoSite.dat: %w", err)
} }
log.Infoln("Download GeoSite.dat finish") log.Infoln("Download GeoSite.dat finish")
} }
@ -84,7 +85,7 @@ func Init(dir string) error {
// initial homedir // initial homedir
if _, err := os.Stat(dir); os.IsNotExist(err) { if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0o777); err != nil { if err := os.MkdirAll(dir, 0o777); err != nil {
return fmt.Errorf("can't create config directory %s: %s", dir, err.Error()) return fmt.Errorf("can't create config directory %s: %w", dir, err)
} }
} }
@ -93,10 +94,10 @@ func Init(dir string) error {
log.Infoln("Can't find config, create a initial config file") log.Infoln("Can't find config, create a initial config file")
f, err := os.OpenFile(C.Path.Config(), os.O_CREATE|os.O_WRONLY, 0o644) f, err := os.OpenFile(C.Path.Config(), os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil { if err != nil {
return fmt.Errorf("can't create file %s: %s", C.Path.Config(), err.Error()) return fmt.Errorf("can't create file %s: %w", C.Path.Config(), err)
} }
f.Write([]byte(`mixed-port: 7890`)) _, _ = f.Write([]byte(`mixed-port: 7890`))
f.Close() _ = f.Close()
} }
// initial mmdb // initial mmdb
@ -110,3 +111,16 @@ func Init(dir string) error {
} }
return nil return nil
} }
func doGet(url string) (resp *http.Response, err error) {
var req *http.Request
req, err = http.NewRequest("GET", url, nil)
if err != nil {
return
}
convert.SetUserAgent(req.Header)
resp, err = http.DefaultClient.Do(req)
return
}

68
config/updateGeo.go Normal file
View File

@ -0,0 +1,68 @@
package config
import (
"fmt"
"os"
"runtime"
"github.com/Dreamacro/clash/component/geodata"
C "github.com/Dreamacro/clash/constant"
"github.com/oschwald/geoip2-golang"
)
func UpdateGeoDatabases() error {
var (
tmpMMDB = C.Path.Resolve("temp_country.mmdb")
tmpGeoSite = C.Path.Resolve("temp_geosite.dat")
)
if err := downloadMMDB(tmpMMDB); err != nil {
return fmt.Errorf("can't download MMDB database file: %w", err)
}
if err := verifyMMDB(tmpMMDB); err != nil {
_ = os.Remove(tmpMMDB)
return fmt.Errorf("invalid MMDB database file, %w", err)
}
if err := os.Rename(tmpMMDB, C.Path.MMDB()); err != nil {
return fmt.Errorf("can't rename MMDB database file: %w", err)
}
if err := downloadGeoSite(tmpGeoSite); err != nil {
return fmt.Errorf("can't download GeoSite database file: %w", err)
}
if err := verifyGeoSite(tmpGeoSite); err != nil {
_ = os.Remove(tmpGeoSite)
return fmt.Errorf("invalid GeoSite database file, %w", err)
}
if err := os.Rename(tmpGeoSite, C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't rename GeoSite database file: %w", err)
}
return nil
}
func verifyMMDB(path string) error {
instance, err := geoip2.Open(path)
if err == nil {
_ = instance.Close()
}
return err
}
func verifyGeoSite(path string) error {
geoLoader, err := geodata.GetGeoDataLoader("standard")
if err != nil {
return err
}
_, err = geoLoader.LoadSite(path, "cn")
runtime.GC()
return err
}

View File

@ -93,9 +93,14 @@ type ProxyAdapter interface {
// a new session (if any) // a new session (if any)
StreamConn(c net.Conn, metadata *Metadata) (net.Conn, error) StreamConn(c net.Conn, metadata *Metadata) (net.Conn, error)
// StreamPacketConn wraps a UDP protocol around net.Conn with Metadata.
StreamPacketConn(c net.Conn, metadata *Metadata) (net.Conn, error)
// DialContext return a C.Conn with protocol which // DialContext return a C.Conn with protocol which
// contains multiplexing-related reuse logic (if any) // contains multiplexing-related reuse logic (if any)
DialContext(ctx context.Context, metadata *Metadata, opts ...dialer.Option) (Conn, error) DialContext(ctx context.Context, metadata *Metadata, opts ...dialer.Option) (Conn, error)
// ListenPacketContext listen for a PacketConn
ListenPacketContext(ctx context.Context, metadata *Metadata, opts ...dialer.Option) (PacketConn, error) ListenPacketContext(ctx context.Context, metadata *Metadata, opts ...dialer.Option) (PacketConn, error)
// Unwrap extracts the proxy from a proxy-group. It returns nil when nothing to extract. // Unwrap extracts the proxy from a proxy-group. It returns nil when nothing to extract.

View File

@ -101,8 +101,8 @@ func (m *Metadata) Resolved() bool {
// Pure is used to solve unexpected behavior // Pure is used to solve unexpected behavior
// when dialing proxy connection in DNSMapping mode. // when dialing proxy connection in DNSMapping mode.
func (m *Metadata) Pure() *Metadata { func (m *Metadata) Pure(isMitmOutbound bool) *Metadata {
if m.DNSMode == DNSMapping && m.DstIP.IsValid() { if !isMitmOutbound && m.DNSMode == DNSMapping && m.DstIP.IsValid() {
copyM := *m copyM := *m
copyM.Host = "" copyM.Host = ""
if copyM.DstIP.Is4() { if copyM.DstIP.Is4() {

View File

@ -7,6 +7,8 @@ import (
"github.com/Dreamacro/clash/component/geodata/router" "github.com/Dreamacro/clash/component/geodata/router"
) )
const ScriptRuleGeoSiteTarget = "__WhateverTarget__"
type RuleExtra struct { type RuleExtra struct {
Network NetWork Network NetWork
SourceIPs []*netip.Prefix SourceIPs []*netip.Prefix

View File

@ -3,7 +3,11 @@ package constant
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"net/netip"
"strconv"
"strings" "strings"
"golang.org/x/exp/slices"
) )
var StackTypeMapping = map[string]TUNStack{ var StackTypeMapping = map[string]TUNStack{
@ -14,10 +18,21 @@ var StackTypeMapping = map[string]TUNStack{
const ( const (
TunGvisor TUNStack = iota TunGvisor TUNStack = iota
TunSystem TunSystem
TunDisabled TUNState = iota
TunEnabled
TunPaused
) )
type TUNStack int type TUNStack int
type TUNState int
type TUNChangeCallback interface {
Pause()
Resume()
}
// UnmarshalYAML unserialize TUNStack with yaml // UnmarshalYAML unserialize TUNStack with yaml
func (e *TUNStack) UnmarshalYAML(unmarshal func(any) error) error { func (e *TUNStack) UnmarshalYAML(unmarshal func(any) error) error {
var tp string var tp string
@ -40,7 +55,7 @@ func (e TUNStack) MarshalYAML() (any, error) {
// UnmarshalJSON unserialize TUNStack with json // UnmarshalJSON unserialize TUNStack with json
func (e *TUNStack) UnmarshalJSON(data []byte) error { func (e *TUNStack) UnmarshalJSON(data []byte) error {
var tp string var tp string
json.Unmarshal(data, &tp) _ = json.Unmarshal(data, &tp)
mode, exist := StackTypeMapping[strings.ToUpper(tp)] mode, exist := StackTypeMapping[strings.ToUpper(tp)]
if !exist { if !exist {
return errors.New("invalid tun stack") return errors.New("invalid tun stack")
@ -64,3 +79,113 @@ func (e TUNStack) String() string {
return "unknown" return "unknown"
} }
} }
type DNSAddrPort struct {
netip.AddrPort
}
func (p *DNSAddrPort) UnmarshalText(text []byte) error {
if len(text) == 0 {
*p = DNSAddrPort{}
return nil
}
addrPort := string(text)
if strings.HasPrefix(addrPort, "any") {
_, port, _ := strings.Cut(addrPort, "any")
addrPort = "0.0.0.0" + port
}
ap, err := netip.ParseAddrPort(addrPort)
*p = DNSAddrPort{AddrPort: ap}
return err
}
func (p DNSAddrPort) String() string {
addrPort := p.AddrPort.String()
if p.AddrPort.Addr().IsUnspecified() {
addrPort = "any:" + strconv.Itoa(int(p.AddrPort.Port()))
}
return addrPort
}
type DNSUrl struct {
Network string
AddrPort DNSAddrPort
}
func (d *DNSUrl) UnmarshalYAML(unmarshal func(any) error) error {
var text string
if err := unmarshal(&text); err != nil {
return err
}
text = strings.ToLower(text)
network := "udp"
if before, after, found := strings.Cut(text, "://"); found {
network = before
text = after
}
if network != "udp" && network != "tcp" {
return errors.New("invalid dns url schema")
}
ap := &DNSAddrPort{}
if err := ap.UnmarshalText([]byte(text)); err != nil {
return err
}
*d = DNSUrl{Network: network, AddrPort: *ap}
return nil
}
func (d DNSUrl) MarshalYAML() (any, error) {
return d.String(), nil
}
func (d *DNSUrl) UnmarshalJSON(data []byte) error {
var text string
if err := json.Unmarshal(data, &text); err != nil {
return err
}
text = strings.ToLower(text)
network := "udp"
if before, after, found := strings.Cut(text, "://"); found {
network = before
text = after
}
if network != "udp" && network != "tcp" {
return errors.New("invalid dns url schema")
}
ap := &DNSAddrPort{}
if err := ap.UnmarshalText([]byte(text)); err != nil {
return err
}
*d = DNSUrl{Network: network, AddrPort: *ap}
return nil
}
func (d DNSUrl) MarshalJSON() ([]byte, error) {
return json.Marshal(d.String())
}
func (d DNSUrl) String() string {
return d.Network + "://" + d.AddrPort.String()
}
func RemoveDuplicateDNSUrl(slice []DNSUrl) []DNSUrl {
slices.SortFunc[DNSUrl](slice, func(a, b DNSUrl) bool {
return a.Network < b.Network || (a.Network == b.Network && a.AddrPort.Addr().Less(b.AddrPort.Addr()))
})
return slices.CompactFunc[[]DNSUrl, DNSUrl](slice, func(a, b DNSUrl) bool {
return a.Network == b.Network && a.AddrPort == b.AddrPort
})
}

View File

@ -54,10 +54,14 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error)
} }
var conn net.Conn var conn net.Conn
if c.proxyAdapter == "" { if c.proxyAdapter != "" {
conn, err = dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), c.port), options...)
} else {
conn, err = dialContextWithProxyAdapter(ctx, c.proxyAdapter, network, ip, c.port, options...) conn, err = dialContextWithProxyAdapter(ctx, c.proxyAdapter, network, ip, c.port, options...)
if err == errProxyNotFound {
options = append(options[:0], dialer.WithInterface(c.proxyAdapter), dialer.WithRoutingMark(0))
conn, err = dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), c.port), options...)
}
} else {
conn, err = dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), c.port), options...)
} }
if err != nil { if err != nil {

View File

@ -69,7 +69,9 @@ func (dc *dohClient) doRequest(req *http.Request) (msg *D.Msg, err error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close() defer func() {
_ = resp.Body.Close()
}()
buf, err := io.ReadAll(resp.Body) buf, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
@ -97,11 +99,17 @@ func newDoHClient(url string, r *Resolver, proxyAdapter string) *dohClient {
return nil, err return nil, err
} }
if proxyAdapter == "" { if proxyAdapter != "" {
return dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip.String(), port)) var conn net.Conn
} else { conn, err = dialContextWithProxyAdapter(ctx, proxyAdapter, "tcp", ip, port)
return dialContextWithProxyAdapter(ctx, proxyAdapter, "tcp", ip, port) if err == errProxyNotFound {
options := []dialer.Option{dialer.WithInterface(proxyAdapter), dialer.WithRoutingMark(0)}
conn, err = dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip.String(), port), options...)
}
return conn, err
} }
return dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip.String(), port))
}, },
}, },
} }

View File

@ -3,11 +3,13 @@ package dns
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
"time" "time"
"github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/common/cache" "github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/common/nnip" "github.com/Dreamacro/clash/common/nnip"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
@ -18,6 +20,8 @@ import (
D "github.com/miekg/dns" D "github.com/miekg/dns"
) )
var errProxyNotFound = errors.New("proxy adapter not found")
func putMsgToCache(c *cache.LruCache[string, *D.Msg], key string, msg *D.Msg) { func putMsgToCache(c *cache.LruCache[string, *D.Msg], key string, msg *D.Msg) {
var ttl uint32 var ttl uint32
switch { switch {
@ -132,16 +136,13 @@ func (wpc *wrapPacketConn) RemoteAddr() net.Addr {
} }
func dialContextWithProxyAdapter(ctx context.Context, adapterName string, network string, dstIP netip.Addr, port string, opts ...dialer.Option) (net.Conn, error) { func dialContextWithProxyAdapter(ctx context.Context, adapterName string, network string, dstIP netip.Addr, port string, opts ...dialer.Option) (net.Conn, error) {
adapter, ok := tunnel.Proxies()[adapterName] proxy, ok := tunnel.Proxies()[adapterName]
if !ok { if !ok {
return nil, fmt.Errorf("proxy adapter [%s] not found", adapterName) return nil, errProxyNotFound
} }
networkType := C.TCP networkType := C.TCP
if network == "udp" { if network == "udp" {
if !adapter.SupportUDP() {
return nil, fmt.Errorf("proxy adapter [%s] UDP is not supported", adapterName)
}
networkType = C.UDP networkType = C.UDP
} }
@ -158,8 +159,14 @@ func dialContextWithProxyAdapter(ctx context.Context, adapterName string, networ
DstPort: port, DstPort: port,
} }
rawAdapter := fetchRawProxyAdapter(proxy.(*adapter.Proxy).ProxyAdapter, metadata)
if networkType == C.UDP { if networkType == C.UDP {
packetConn, err := adapter.ListenPacketContext(ctx, metadata, opts...) if !rawAdapter.SupportUDP() {
return nil, fmt.Errorf("proxy adapter [%s] UDP is not supported", rawAdapter.Name())
}
packetConn, err := rawAdapter.ListenPacketContext(ctx, metadata, opts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -170,5 +177,13 @@ func dialContextWithProxyAdapter(ctx context.Context, adapterName string, networ
}, nil }, nil
} }
return adapter.DialContext(ctx, metadata, opts...) return rawAdapter.DialContext(ctx, metadata, opts...)
}
func fetchRawProxyAdapter(proxyAdapter C.ProxyAdapter, metadata *C.Metadata) C.ProxyAdapter {
if p := proxyAdapter.Unwrap(metadata); p != nil {
return fetchRawProxyAdapter(p.(*adapter.Proxy).ProxyAdapter, metadata)
}
return proxyAdapter
} }

23
go.mod
View File

@ -3,14 +3,13 @@ module github.com/Dreamacro/clash
go 1.18 go 1.18
require ( require (
github.com/Dreamacro/go-shadowsocks2 v0.1.8
github.com/go-chi/chi/v5 v5.0.7 github.com/go-chi/chi/v5 v5.0.7
github.com/go-chi/cors v1.2.1 github.com/go-chi/cors v1.2.1
github.com/go-chi/render v1.0.1 github.com/go-chi/render v1.0.1
github.com/gofrs/uuid v4.2.0+incompatible github.com/gofrs/uuid v4.2.0+incompatible
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f
github.com/miekg/dns v1.1.48 github.com/miekg/dns v1.1.49
github.com/oschwald/geoip2-golang v1.7.0 github.com/oschwald/geoip2-golang v1.7.0
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.1 github.com/stretchr/testify v1.7.1
@ -18,17 +17,18 @@ require (
go.etcd.io/bbolt v1.3.6 go.etcd.io/bbolt v1.3.6
go.uber.org/atomic v1.9.0 go.uber.org/atomic v1.9.0
go.uber.org/automaxprocs v1.5.1 go.uber.org/automaxprocs v1.5.1
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/net v0.0.0-20220531201128-c960675eff93
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab
golang.org/x/time v0.0.0-20220411224347-583f2d630306 golang.org/x/time v0.0.0-20220411224347-583f2d630306
golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d golang.zx2c4.com/wireguard v0.0.0-20220601130007-6a08d81f6bc4
golang.zx2c4.com/wireguard/windows v0.5.4-0.20220328111914-004c22c5647e golang.zx2c4.com/wireguard/windows v0.5.4-0.20220328111914-004c22c5647e
google.golang.org/protobuf v1.28.0 google.golang.org/protobuf v1.28.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1
gvisor.dev/gvisor v0.0.0-20220506231117-8ef340c14150 gvisor.dev/gvisor v0.0.0-20220601233344-46e478629075
) )
require ( require (
@ -39,9 +39,8 @@ require (
github.com/oschwald/maxminddb-golang v1.9.0 // indirect github.com/oschwald/maxminddb-golang v1.9.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 // indirect golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/tools v0.1.9 // indirect golang.org/x/tools v0.1.10 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
) )

45
go.sum
View File

@ -1,5 +1,3 @@
github.com/Dreamacro/go-shadowsocks2 v0.1.8 h1:Ixejp5JscEc866gAvm/l6TFd7BOBvDviKgwb1quWw3g=
github.com/Dreamacro/go-shadowsocks2 v0.1.8/go.mod h1:51y4Q6tJoCE7e8TmYXcQRqfoxPfE9Cvn79V6pB6Df7Y=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -47,8 +45,8 @@ github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcK
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
github.com/miekg/dns v1.1.48 h1:Ucfr7IIVyMBz4lRE8qmGUuZ4Wt3/ZGu9hmcMT3Uu4tQ= github.com/miekg/dns v1.1.49 h1:qe0mQU3Z/XpFeE+AEBo2rqaS1IPBJ3anmqZ4XiZJVG8=
github.com/miekg/dns v1.1.48/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/miekg/dns v1.1.49/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/oschwald/geoip2-golang v1.7.0 h1:JW1r5AKi+vv2ujSxjKthySK3jo8w8oKWPyXsw+Qs/S8= github.com/oschwald/geoip2-golang v1.7.0 h1:JW1r5AKi+vv2ujSxjKthySK3jo8w8oKWPyXsw+Qs/S8=
github.com/oschwald/geoip2-golang v1.7.0/go.mod h1:mdI/C7iK7NVMcIDDtf4bCKMJ7r0o7UwGeCo9eiitCMQ= github.com/oschwald/geoip2-golang v1.7.0/go.mod h1:mdI/C7iK7NVMcIDDtf4bCKMJ7r0o7UwGeCo9eiitCMQ=
github.com/oschwald/maxminddb-golang v1.9.0 h1:tIk4nv6VT9OiPyrnDAfJS1s1xKDQMZOsGojab6EjC1Y= github.com/oschwald/maxminddb-golang v1.9.0 h1:tIk4nv6VT9OiPyrnDAfJS1s1xKDQMZOsGojab6EjC1Y=
@ -80,11 +78,13 @@ go.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f h1:KK6mxegmt5hGJRcAnEDjSNLxIRhZxDcgwMbcO/lMCRM=
golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f/go.mod h1:yh0Ynu2b5ZUe3MQfp2nM0ecK7wsgouWTDN0FNeJuIys=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -97,11 +97,12 @@ golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= golang.org/x/net v0.0.0-20220531201128-c960675eff93 h1:MYimHLfoXEpOhqd/zgoA/uoXzHB86AEky4LAx5ij9xA=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -122,8 +123,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -136,8 +137,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8= golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -145,8 +146,8 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY= golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY=
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d h1:q4JksJ2n0fmbXC0Aj0eOs6E0AcPqnKglxWXWFqGD6x0= golang.zx2c4.com/wireguard v0.0.0-20220601130007-6a08d81f6bc4 h1:QlbNZ9SwDAepRQwgeWHLi3rfEMo/kVEU4SmgsNM7HmQ=
golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U= golang.zx2c4.com/wireguard v0.0.0-20220601130007-6a08d81f6bc4/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U=
golang.zx2c4.com/wireguard/windows v0.5.4-0.20220328111914-004c22c5647e h1:yV04h6Tx19uDR6LvuEbR19cDU+3QrB9LuGjtF7F5G0w= golang.zx2c4.com/wireguard/windows v0.5.4-0.20220328111914-004c22c5647e h1:yV04h6Tx19uDR6LvuEbR19cDU+3QrB9LuGjtF7F5G0w=
golang.zx2c4.com/wireguard/windows v0.5.4-0.20220328111914-004c22c5647e/go.mod h1:1CeiatTZwcwSFA3cAtMm8CQoroviTldnxd7DOgM/vI4= golang.zx2c4.com/wireguard/windows v0.5.4-0.20220328111914-004c22c5647e/go.mod h1:1CeiatTZwcwSFA3cAtMm8CQoroviTldnxd7DOgM/vI4=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
@ -154,10 +155,8 @@ google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscL
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20220506231117-8ef340c14150 h1:bspdBY1iCLtW6JXold8yhXHkAiE9UoWfmHShNkTc9JA= gvisor.dev/gvisor v0.0.0-20220601233344-46e478629075 h1:ucwwit0X39HmMQ1iSeNCIXw4g/B8bi+O6TSxxDbPs9E=
gvisor.dev/gvisor v0.0.0-20220506231117-8ef340c14150/go.mod h1:tWwEcFvJavs154OdjFCw78axNrsDlz4Zh8jvPqwcpGI= gvisor.dev/gvisor v0.0.0-20220601233344-46e478629075/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM=

View File

@ -84,10 +84,9 @@ func ApplyConfig(cfg *config.Config, force bool) {
updateHosts(cfg.Hosts) updateHosts(cfg.Hosts)
updateMitm(cfg.Mitm) updateMitm(cfg.Mitm)
updateProfile(cfg) updateProfile(cfg)
updateDNS(cfg.DNS, cfg.Tun) updateDNS(cfg.DNS, cfg.General.Tun)
updateGeneral(cfg.General, force) updateGeneral(cfg.General, force)
updateIPTables(cfg) updateIPTables(cfg)
updateTun(cfg.Tun, cfg.DNS)
updateExperimental(cfg) updateExperimental(cfg)
log.SetLevel(cfg.General.LogLevel) log.SetLevel(cfg.General.LogLevel)
@ -96,8 +95,8 @@ func ApplyConfig(cfg *config.Config, force bool) {
func GetGeneral() *config.General { func GetGeneral() *config.General {
ports := P.GetPorts() ports := P.GetPorts()
authenticator := []string{} authenticator := []string{}
if auth := authStore.Authenticator(); auth != nil { if authM := authStore.Authenticator(); authM != nil {
authenticator = auth.Users() authenticator = authM.Users()
} }
general := &config.General{ general := &config.General{
@ -115,15 +114,16 @@ func GetGeneral() *config.General {
Mode: tunnel.Mode(), Mode: tunnel.Mode(),
LogLevel: log.Level(), LogLevel: log.Level(),
IPv6: !resolver.DisableIPv6, IPv6: !resolver.DisableIPv6,
Sniffing: tunnel.Sniffing(),
Tun: P.GetTunConf(), Tun: P.GetTunConf(),
} }
return general return general
} }
func updateExperimental(c *config.Config) {} func updateExperimental(_ *config.Config) {}
func updateDNS(c *config.DNS, t *config.Tun) { func updateDNS(c *config.DNS, t config.Tun) {
cfg := dns.Config{ cfg := dns.Config{
Main: c.NameServer, Main: c.NameServer,
Fallback: c.Fallback, Fallback: c.Fallback,
@ -174,6 +174,10 @@ func updateDNS(c *config.DNS, t *config.Tun) {
} }
dns.ReCreateServer("", nil, nil) dns.ReCreateServer("", nil, nil)
} }
if cfg.Pool != nil {
P.SetTunAddressPrefix(cfg.Pool.IPNet())
}
} }
func updateHosts(tree *trie.DomainTrie[netip.Addr]) { func updateHosts(tree *trie.DomainTrie[netip.Addr]) {
@ -192,15 +196,6 @@ func updateRuleProviders(providers map[string]C.Rule) {
S.UpdateRuleProviders(providers) S.UpdateRuleProviders(providers)
} }
func updateTun(tun *config.Tun, dns *config.DNS) {
var tunAddressPrefix *netip.Prefix
if dns.FakeIPRange != nil {
tunAddressPrefix = dns.FakeIPRange.IPNet()
}
P.ReCreateTun(tun, tunAddressPrefix, tunnel.TCPIn(), tunnel.UDPIn())
}
func updateGeneral(general *config.General, force bool) { func updateGeneral(general *config.General, force bool) {
tunnel.SetMode(general.Mode) tunnel.SetMode(general.Mode)
resolver.DisableIPv6 = !general.IPv6 resolver.DisableIPv6 = !general.IPv6
@ -229,6 +224,11 @@ func updateGeneral(general *config.General, force bool) {
bindAddress := general.BindAddress bindAddress := general.BindAddress
P.SetBindAddress(bindAddress) P.SetBindAddress(bindAddress)
sniffing := general.Sniffing
tunnel.SetSniffing(sniffing)
log.Infoln("Use TLS SNI sniffer: %v", sniffing)
tcpIn := tunnel.TCPIn() tcpIn := tunnel.TCPIn()
udpIn := tunnel.UDPIn() udpIn := tunnel.UDPIn()
@ -238,6 +238,7 @@ func updateGeneral(general *config.General, force bool) {
P.ReCreateTProxy(general.TProxyPort, tcpIn, udpIn) P.ReCreateTProxy(general.TProxyPort, tcpIn, udpIn)
P.ReCreateMixed(general.MixedPort, tcpIn, udpIn) P.ReCreateMixed(general.MixedPort, tcpIn, udpIn)
P.ReCreateMitm(general.MitmPort, tcpIn) P.ReCreateMitm(general.MitmPort, tcpIn)
P.ReCreateTun(&general.Tun, tcpIn, udpIn)
} }
func updateUsers(users []auth.AuthUser) { func updateUsers(users []auth.AuthUser) {
@ -279,7 +280,7 @@ func patchSelectGroup(proxies map[string]C.Proxy) {
continue continue
} }
selector.Set(selected) _ = selector.Set(selected)
} }
} }

View File

@ -1,15 +1,17 @@
package route package route
import ( import (
"encoding/json"
"net/http" "net/http"
"net/netip"
"path/filepath" "path/filepath"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
"github.com/Dreamacro/clash/config" "github.com/Dreamacro/clash/config"
"github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/hub/executor" "github.com/Dreamacro/clash/hub/executor"
P "github.com/Dreamacro/clash/listener" P "github.com/Dreamacro/clash/listener"
"github.com/Dreamacro/clash/listener/tun/ipstack/commons"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel" "github.com/Dreamacro/clash/tunnel"
@ -26,26 +28,28 @@ func configRouter() http.Handler {
} }
type configSchema struct { type configSchema struct {
Port *int `json:"port"` Port *int `json:"port,omitempty"`
SocksPort *int `json:"socks-port"` SocksPort *int `json:"socks-port,omitempty"`
RedirPort *int `json:"redir-port"` RedirPort *int `json:"redir-port,omitempty"`
TProxyPort *int `json:"tproxy-port"` TProxyPort *int `json:"tproxy-port,omitempty"`
MixedPort *int `json:"mixed-port"` MixedPort *int `json:"mixed-port,omitempty"`
MitmPort *int `json:"mitm-port"` MitmPort *int `json:"mitm-port,omitempty"`
AllowLan *bool `json:"allow-lan"` AllowLan *bool `json:"allow-lan,omitempty"`
BindAddress *string `json:"bind-address"` BindAddress *string `json:"bind-address,omitempty"`
Mode *tunnel.TunnelMode `json:"mode"` Mode *tunnel.TunnelMode `json:"mode,omitempty"`
LogLevel *log.LogLevel `json:"log-level"` LogLevel *log.LogLevel `json:"log-level,omitempty"`
IPv6 *bool `json:"ipv6"` IPv6 *bool `json:"ipv6,omitempty"`
Tun *tunConfigSchema `json:"tun"` Sniffing *bool `json:"sniffing,omitempty"`
Tun *tunConfigSchema `json:"tun,omitempty"`
} }
type tunConfigSchema struct { type tunConfigSchema struct {
Enable *bool `json:"enable"` Enable *bool `json:"enable,omitempty"`
Device *string `json:"device"` Device *string `json:"device,omitempty"`
Stack *constant.TUNStack `json:"stack"` Stack *constant.TUNStack `json:"stack,omitempty"`
DNSHijack *[]netip.AddrPort `json:"dns-hijack"` DNSHijack *[]constant.DNSUrl `json:"dns-hijack,omitempty"`
AutoRoute *bool `json:"auto-route"` AutoRoute *bool `json:"auto-route,omitempty"`
AutoDetectInterface *bool `json:"auto-detect-interface,omitempty"`
} }
func getConfigs(w http.ResponseWriter, r *http.Request) { func getConfigs(w http.ResponseWriter, r *http.Request) {
@ -101,6 +105,10 @@ func patchConfigs(w http.ResponseWriter, r *http.Request) {
resolver.DisableIPv6 = !*general.IPv6 resolver.DisableIPv6 = !*general.IPv6
} }
if general.Sniffing != nil {
tunnel.SetSniffing(*general.Sniffing)
}
if general.Tun != nil { if general.Tun != nil {
tunSchema := general.Tun tunSchema := general.Tun
tunConf := P.GetTunConf() tunConf := P.GetTunConf()
@ -120,10 +128,26 @@ func patchConfigs(w http.ResponseWriter, r *http.Request) {
if tunSchema.AutoRoute != nil { if tunSchema.AutoRoute != nil {
tunConf.AutoRoute = *tunSchema.AutoRoute tunConf.AutoRoute = *tunSchema.AutoRoute
} }
if tunSchema.AutoDetectInterface != nil {
tunConf.AutoDetectInterface = *tunSchema.AutoDetectInterface
}
P.ReCreateTun(&tunConf, nil, tcpIn, udpIn) if dialer.DefaultInterface.Load() == "" && tunConf.Enable {
outboundInterface, err := commons.GetAutoDetectInterface()
if err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError("Get auto detect interface fail: "+err.Error()))
return
}
dialer.DefaultInterface.Store(outboundInterface)
}
P.ReCreateTun(&tunConf, tcpIn, udpIn)
} }
msg, _ := json.Marshal(general)
log.Warnln("[REST-API] patch config by: %s", string(msg))
render.NoContent(w, r) render.NoContent(w, r)
} }
@ -145,6 +169,7 @@ func updateConfigs(w http.ResponseWriter, r *http.Request) {
var err error var err error
if req.Payload != "" { if req.Payload != "" {
log.Warnln("[REST-API] update config by payload")
cfg, err = executor.ParseWithBytes([]byte(req.Payload)) cfg, err = executor.ParseWithBytes([]byte(req.Payload))
if err != nil { if err != nil {
render.Status(r, http.StatusBadRequest) render.Status(r, http.StatusBadRequest)
@ -161,6 +186,7 @@ func updateConfigs(w http.ResponseWriter, r *http.Request) {
return return
} }
log.Warnln("[REST-API] reload config from path: %s", req.Path)
cfg, err = executor.ParseWithPath(req.Path) cfg, err = executor.ParseWithPath(req.Path)
if err != nil { if err != nil {
render.Status(r, http.StatusBadRequest) render.Status(r, http.StatusBadRequest)

64
hub/route/configsGeo.go Normal file
View File

@ -0,0 +1,64 @@
package route
import (
"net/http"
"sync"
"github.com/Dreamacro/clash/config"
"github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/hub/executor"
"github.com/Dreamacro/clash/log"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
var (
updatingGeo bool
updateGeoMux sync.Mutex
)
func configGeoRouter() http.Handler {
r := chi.NewRouter()
r.Post("/", updateGeoDatabases)
return r
}
func updateGeoDatabases(w http.ResponseWriter, r *http.Request) {
updateGeoMux.Lock()
if updatingGeo {
updateGeoMux.Unlock()
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError("updating..."))
return
}
updatingGeo = true
updateGeoMux.Unlock()
go func() {
defer func() {
updatingGeo = false
}()
log.Warnln("[REST-API] updating GEO databases...")
if err := config.UpdateGeoDatabases(); err != nil {
log.Errorln("[REST-API] update GEO databases failed: %v", err)
return
}
log.Warnln("[REST-API] update GEO databases successful, apply config...")
cfg, err := executor.ParseWithPath(constant.Path.Config())
if err != nil {
log.Errorln("[REST-API] update GEO databases failed: %v", err)
return
}
executor.ApplyConfig(cfg, false)
}()
render.NoContent(w, r)
}

View File

@ -11,6 +11,7 @@ import (
"github.com/Dreamacro/clash/adapter/outboundgroup" "github.com/Dreamacro/clash/adapter/outboundgroup"
"github.com/Dreamacro/clash/component/profile/cachefile" "github.com/Dreamacro/clash/component/profile/cachefile"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
"github.com/Dreamacro/clash/tunnel" "github.com/Dreamacro/clash/tunnel"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
@ -43,6 +44,10 @@ func findProxyByName(next http.Handler) http.Handler {
name := r.Context().Value(CtxKeyProxyName).(string) name := r.Context().Value(CtxKeyProxyName).(string)
proxies := tunnel.Proxies() proxies := tunnel.Proxies()
proxy, exist := proxies[name] proxy, exist := proxies[name]
if !exist {
proxy, exist = findProxyInNonCompatibleProviderByName(name)
}
if !exist { if !exist {
render.Status(r, http.StatusNotFound) render.Status(r, http.StatusNotFound)
render.JSON(w, r, ErrNotFound) render.JSON(w, r, ErrNotFound)
@ -128,3 +133,20 @@ func getProxyDelay(w http.ResponseWriter, r *http.Request) {
"delay": delay, "delay": delay,
}) })
} }
func findProxyInNonCompatibleProviderByName(name string) (proxy C.Proxy, found bool) {
providers := tunnel.Providers()
for _, pd := range providers {
if pd.VehicleType() == provider.Compatible {
continue
}
for _, pp := range pd.Proxies() {
found = pp.Name() == name
if found {
proxy = pp
return
}
}
}
return
}

View File

@ -67,6 +67,7 @@ func Start(addr string, secret string) {
r.Get("/traffic", traffic) r.Get("/traffic", traffic)
r.Get("/version", version) r.Get("/version", version)
r.Mount("/configs", configRouter()) r.Mount("/configs", configRouter())
r.Mount("/configs/geo", configGeoRouter())
r.Mount("/proxies", proxyRouter()) r.Mount("/proxies", proxyRouter())
r.Mount("/rules", ruleRouter()) r.Mount("/rules", ruleRouter())
r.Mount("/connections", connectionRouter()) r.Mount("/connections", connectionRouter())

View File

@ -13,7 +13,15 @@ import (
) )
func isUpgradeRequest(req *http.Request) bool { func isUpgradeRequest(req *http.Request) bool {
return strings.EqualFold(req.Header.Get("Connection"), "Upgrade") for _, header := range req.Header["Connection"] {
for _, elm := range strings.Split(header, ",") {
if strings.EqualFold(strings.TrimSpace(elm), "Upgrade") {
return true
}
}
}
return false
} }
func HandleUpgrade(localConn net.Conn, serverConn *N.BufferedConn, request *http.Request, in chan<- C.ConnContext) (resp *http.Response) { func HandleUpgrade(localConn net.Conn, serverConn *N.BufferedConn, request *http.Request, in chan<- C.ConnContext) (resp *http.Response) {

View File

@ -8,7 +8,6 @@ import (
"net" "net"
"net/netip" "net/netip"
"os" "os"
"sort"
"strconv" "strconv"
"sync" "sync"
"time" "time"
@ -36,6 +35,7 @@ var (
bindAddress = "*" bindAddress = "*"
lastTunConf *config.Tun lastTunConf *config.Tun
lastTunAddressPrefix *netip.Prefix lastTunAddressPrefix *netip.Prefix
tunAddressPrefix *netip.Prefix
socksListener *socks.Listener socksListener *socks.Listener
socksUDPListener *socks.UDPListener socksUDPListener *socks.UDPListener
@ -70,11 +70,24 @@ type Ports struct {
func GetTunConf() config.Tun { func GetTunConf() config.Tun {
if lastTunConf == nil { if lastTunConf == nil {
addrPort := C.DNSAddrPort{
AddrPort: netip.MustParseAddrPort("0.0.0.0:53"),
}
return config.Tun{ return config.Tun{
Enable: false, Enable: false,
Stack: C.TunGvisor, Stack: C.TunGvisor,
DNSHijack: []netip.AddrPort{netip.MustParseAddrPort("0.0.0.0:53")}, DNSHijack: []C.DNSUrl{ // default hijack all dns query
AutoRoute: true, {
Network: "udp",
AddrPort: addrPort,
},
{
Network: "tcp",
AddrPort: addrPort,
},
},
AutoRoute: true,
AutoDetectInterface: false,
} }
} }
return *lastTunConf return *lastTunConf
@ -96,6 +109,10 @@ func SetBindAddress(host string) {
bindAddress = host bindAddress = host
} }
func SetTunAddressPrefix(tunAddress *netip.Prefix) {
tunAddressPrefix = tunAddress
}
func ReCreateHTTP(port int, tcpIn chan<- C.ConnContext) { func ReCreateHTTP(port int, tcpIn chan<- C.ConnContext) {
httpMux.Lock() httpMux.Lock()
defer httpMux.Unlock() defer httpMux.Unlock()
@ -335,7 +352,7 @@ func ReCreateMixed(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.P
log.Infoln("Mixed(http+socks) proxy listening at: %s", mixedListener.Address()) log.Infoln("Mixed(http+socks) proxy listening at: %s", mixedListener.Address())
} }
func ReCreateTun(tunConf *config.Tun, tunAddressPrefix *netip.Prefix, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { func ReCreateTun(tunConf *config.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) {
tunMux.Lock() tunMux.Lock()
defer tunMux.Unlock() defer tunMux.Unlock()
@ -350,6 +367,8 @@ func ReCreateTun(tunConf *config.Tun, tunAddressPrefix *netip.Prefix, tcpIn chan
tunAddressPrefix = lastTunAddressPrefix tunAddressPrefix = lastTunAddressPrefix
} }
tunConf.DNSHijack = C.RemoveDuplicateDNSUrl(tunConf.DNSHijack)
if tunStackListener != nil { if tunStackListener != nil {
if !hasTunConfigChange(tunConf, tunAddressPrefix) { if !hasTunConfigChange(tunConf, tunAddressPrefix) {
return return
@ -366,7 +385,13 @@ func ReCreateTun(tunConf *config.Tun, tunAddressPrefix *netip.Prefix, tcpIn chan
return return
} }
tunStackListener, err = tun.New(tunConf, tunAddressPrefix, tcpIn, udpIn) callback := &tunChangeCallback{
tunConf: *tunConf,
tcpIn: tcpIn,
udpIn: udpIn,
}
tunStackListener, err = tun.New(tunConf, tunAddressPrefix, tcpIn, udpIn, callback)
if err != nil { if err != nil {
return return
} }
@ -519,14 +544,6 @@ func hasTunConfigChange(tunConf *config.Tun, tunAddressPrefix *netip.Prefix) boo
return true return true
} }
sort.Slice(lastTunConf.DNSHijack, func(i, j int) bool {
return lastTunConf.DNSHijack[i].Addr().Less(lastTunConf.DNSHijack[j].Addr())
})
sort.Slice(tunConf.DNSHijack, func(i, j int) bool {
return tunConf.DNSHijack[i].Addr().Less(tunConf.DNSHijack[j].Addr())
})
for i, dns := range tunConf.DNSHijack { for i, dns := range tunConf.DNSHijack {
if dns != lastTunConf.DNSHijack[i] { if dns != lastTunConf.DNSHijack[i] {
return true return true
@ -536,7 +553,8 @@ func hasTunConfigChange(tunConf *config.Tun, tunAddressPrefix *netip.Prefix) boo
if lastTunConf.Enable != tunConf.Enable || if lastTunConf.Enable != tunConf.Enable ||
lastTunConf.Device != tunConf.Device || lastTunConf.Device != tunConf.Device ||
lastTunConf.Stack != tunConf.Stack || lastTunConf.Stack != tunConf.Stack ||
lastTunConf.AutoRoute != tunConf.AutoRoute { lastTunConf.AutoRoute != tunConf.AutoRoute ||
lastTunConf.AutoDetectInterface != tunConf.AutoDetectInterface {
return true return true
} }
@ -551,6 +569,24 @@ func hasTunConfigChange(tunConf *config.Tun, tunAddressPrefix *netip.Prefix) boo
return false return false
} }
type tunChangeCallback struct {
tunConf config.Tun
tcpIn chan<- C.ConnContext
udpIn chan<- *inbound.PacketAdapter
}
func (t *tunChangeCallback) Pause() {
conf := t.tunConf
conf.Enable = false
ReCreateTun(&conf, t.tcpIn, t.udpIn)
}
func (t *tunChangeCallback) Resume() {
conf := t.tunConf
conf.Enable = true
ReCreateTun(&conf, t.tcpIn, t.udpIn)
}
func initCert() error { func initCert() error {
if _, err := os.Stat(C.Path.RootCA()); os.IsNotExist(err) { if _, err := os.Stat(C.Path.RootCA()); os.IsNotExist(err) {
log.Infoln("Can't find mitm_ca.crt, start generate") log.Infoln("Can't find mitm_ca.crt, start generate")

View File

@ -162,15 +162,12 @@ func CleanupTProxyIPTables() {
func addLocalnetworkToChain(chain string) { func addLocalnetworkToChain(chain string) {
execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 0.0.0.0/8 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 0.0.0.0/8 -j RETURN", chain))
execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 10.0.0.0/8 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 10.0.0.0/8 -j RETURN", chain))
execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 100.64.0.0/10 -j RETURN", chain))
execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 127.0.0.0/8 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 127.0.0.0/8 -j RETURN", chain))
execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 169.254.0.0/16 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 169.254.0.0/16 -j RETURN", chain))
execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 172.16.0.0/12 -j RETURN", chain))
execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 192.0.0.0/24 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 192.0.0.0/24 -j RETURN", chain))
execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 192.0.2.0/24 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 192.0.2.0/24 -j RETURN", chain))
execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 192.88.99.0/24 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 192.88.99.0/24 -j RETURN", chain))
execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 192.168.0.0/16 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 192.168.0.0/16 -j RETURN", chain))
execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 198.51.100.0/24 -j RETURN", chain))
execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 203.0.113.0/24 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 203.0.113.0/24 -j RETURN", chain))
execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 224.0.0.0/4 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 224.0.0.0/4 -j RETURN", chain))
execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 240.0.0.0/4 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 240.0.0.0/4 -j RETURN", chain))

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,216 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2021 WireGuard LLC. All Rights Reserved.
*/
package driver
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
"syscall"
"unsafe"
"github.com/Dreamacro/clash/log"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/windows/driver/memmod"
)
//go:linkname modwintun golang.zx2c4.com/wintun.modwintun
//go:linkname procWintunCreateAdapter golang.zx2c4.com/wintun.procWintunCreateAdapter
//go:linkname procWintunOpenAdapter golang.zx2c4.com/wintun.procWintunOpenAdapter
//go:linkname procWintunCloseAdapter golang.zx2c4.com/wintun.procWintunCloseAdapter
//go:linkname procWintunDeleteDriver golang.zx2c4.com/wintun.procWintunDeleteDriver
//go:linkname procWintunGetAdapterLUID golang.zx2c4.com/wintun.procWintunGetAdapterLUID
//go:linkname procWintunGetRunningDriverVersion golang.zx2c4.com/wintun.procWintunGetRunningDriverVersion
//go:linkname procWintunAllocateSendPacket golang.zx2c4.com/wintun.procWintunAllocateSendPacket
//go:linkname procWintunEndSession golang.zx2c4.com/wintun.procWintunEndSession
//go:linkname procWintunGetReadWaitEvent golang.zx2c4.com/wintun.procWintunGetReadWaitEvent
//go:linkname procWintunReceivePacket golang.zx2c4.com/wintun.procWintunReceivePacket
//go:linkname procWintunReleaseReceivePacket golang.zx2c4.com/wintun.procWintunReleaseReceivePacket
//go:linkname procWintunSendPacket golang.zx2c4.com/wintun.procWintunSendPacket
//go:linkname procWintunStartSession golang.zx2c4.com/wintun.procWintunStartSession
var (
modwintun = newLazyDLL("wintun.dll", setupLogger)
procWintunCreateAdapter = modwintun.NewProc("WintunCreateAdapter")
procWintunOpenAdapter = modwintun.NewProc("WintunOpenAdapter")
procWintunCloseAdapter = modwintun.NewProc("WintunCloseAdapter")
procWintunDeleteDriver = modwintun.NewProc("WintunDeleteDriver")
procWintunGetAdapterLUID = modwintun.NewProc("WintunGetAdapterLUID")
procWintunGetRunningDriverVersion = modwintun.NewProc("WintunGetRunningDriverVersion")
procWintunAllocateSendPacket = modwintun.NewProc("WintunAllocateSendPacket")
procWintunEndSession = modwintun.NewProc("WintunEndSession")
procWintunGetReadWaitEvent = modwintun.NewProc("WintunGetReadWaitEvent")
procWintunReceivePacket = modwintun.NewProc("WintunReceivePacket")
procWintunReleaseReceivePacket = modwintun.NewProc("WintunReleaseReceivePacket")
procWintunSendPacket = modwintun.NewProc("WintunSendPacket")
procWintunStartSession = modwintun.NewProc("WintunStartSession")
)
type loggerLevel int
const (
logInfo loggerLevel = iota
logWarn
logErr
)
func InitWintun() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("init wintun.dll error: %v", r)
}
}()
if err = modwintun.Load(); err != nil {
return
}
procWintunCreateAdapter.Addr()
procWintunOpenAdapter.Addr()
procWintunCloseAdapter.Addr()
procWintunDeleteDriver.Addr()
procWintunGetAdapterLUID.Addr()
procWintunGetRunningDriverVersion.Addr()
procWintunAllocateSendPacket.Addr()
procWintunEndSession.Addr()
procWintunGetReadWaitEvent.Addr()
procWintunReceivePacket.Addr()
procWintunReleaseReceivePacket.Addr()
procWintunSendPacket.Addr()
procWintunStartSession.Addr()
return
}
func newLazyDLL(name string, onLoad func(d *lazyDLL)) *lazyDLL {
return &lazyDLL{Name: name, onLoad: onLoad}
}
func logMessage(level loggerLevel, _ uint64, msg *uint16) int {
switch level {
case logInfo:
log.Infoln("[TUN] %s", windows.UTF16PtrToString(msg))
case logWarn:
log.Warnln("[TUN] %s", windows.UTF16PtrToString(msg))
case logErr:
log.Errorln("[TUN] %s", windows.UTF16PtrToString(msg))
default:
log.Debugln("[TUN] %s", windows.UTF16PtrToString(msg))
}
return 0
}
func setupLogger(dll *lazyDLL) {
var callback uintptr
if runtime.GOARCH == "386" {
callback = windows.NewCallback(func(level loggerLevel, _, _ uint32, msg *uint16) int {
return logMessage(level, 0, msg)
})
} else if runtime.GOARCH == "arm" {
callback = windows.NewCallback(func(level loggerLevel, _, _, _ uint32, msg *uint16) int {
return logMessage(level, 0, msg)
})
} else if runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" {
callback = windows.NewCallback(logMessage)
}
_, _, _ = syscall.SyscallN(dll.NewProc("WintunSetLogger").Addr(), callback)
}
func (d *lazyDLL) NewProc(name string) *lazyProc {
return &lazyProc{dll: d, Name: name}
}
type lazyProc struct {
Name string
mu sync.Mutex
dll *lazyDLL
addr uintptr
}
func (p *lazyProc) Find() error {
if atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&p.addr))) != nil {
return nil
}
p.mu.Lock()
defer p.mu.Unlock()
if p.addr != 0 {
return nil
}
err := p.dll.Load()
if err != nil {
return fmt.Errorf("error loading DLL: %s, MODULE: %s, error: %w", p.dll.Name, p.Name, err)
}
addr, err := p.nameToAddr()
if err != nil {
return fmt.Errorf("error getting %s address: %w", p.Name, err)
}
atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&p.addr)), unsafe.Pointer(addr))
return nil
}
func (p *lazyProc) Addr() uintptr {
err := p.Find()
if err != nil {
panic(err)
}
return p.addr
}
func (p *lazyProc) Load() error {
return p.dll.Load()
}
type lazyDLL struct {
Name string
Base windows.Handle
mu sync.Mutex
module *memmod.Module
onLoad func(d *lazyDLL)
}
func (d *lazyDLL) Load() error {
if atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&d.module))) != nil {
return nil
}
d.mu.Lock()
defer d.mu.Unlock()
if d.module != nil {
return nil
}
module, err := memmod.LoadLibrary(dllData)
if err != nil {
return fmt.Errorf("unable to load library: %w", err)
}
d.Base = windows.Handle(module.BaseAddr())
atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&d.module)), unsafe.Pointer(module))
if d.onLoad != nil {
d.onLoad(d)
}
return nil
}
func (p *lazyProc) nameToAddr() (uintptr, error) {
return p.dll.module.ProcAddressByName(p.Name)
}

View File

@ -0,0 +1,13 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2021 WireGuard LLC. All Rights Reserved.
*/
package driver
import (
_ "embed"
)
//go:embed x86/wintun.dll
var dllData []byte

View File

@ -0,0 +1,13 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2021 WireGuard LLC. All Rights Reserved.
*/
package driver
import (
_ "embed"
)
//go:embed amd64/wintun.dll
var dllData []byte

View File

@ -0,0 +1,13 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2021 WireGuard LLC. All Rights Reserved.
*/
package driver
import (
_ "embed"
)
//go:embed arm/wintun.dll
var dllData []byte

View File

@ -0,0 +1,13 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2021 WireGuard LLC. All Rights Reserved.
*/
package driver
import (
_ "embed"
)
//go:embed arm64/wintun.dll
var dllData []byte

View File

@ -0,0 +1,10 @@
//go:build windows
// https://git.zx2c4.com/wireguard-go/tree/tun/wintun
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
*/
package driver

Binary file not shown.

View File

@ -3,7 +3,6 @@
package tun package tun
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"runtime" "runtime"
@ -43,11 +42,11 @@ func Open(name string, mtu uint32) (_ device.Device, err error) {
forcedMTU = int(t.mtu) forcedMTU = int(t.mtu)
} }
nt, err := tun.CreateTUN(t.name, forcedMTU) // forcedMTU do not work on wintun, need to be setting by other way nt, err := newDevice(t.name, forcedMTU) // forcedMTU do not work on wintun, need to be setting by other way
// retry if abnormal exit on Windows at last time // retry if abnormal exit at last time on Windows
if err != nil && runtime.GOOS == "windows" && errors.Is(err, os.ErrExist) { if err != nil && runtime.GOOS == "windows" && os.IsExist(err) {
nt, err = tun.CreateTUN(t.name, forcedMTU) nt, err = newDevice(t.name, forcedMTU)
} }
if err != nil { if err != nil {

View File

@ -2,7 +2,13 @@
package tun package tun
import "golang.zx2c4.com/wireguard/tun"
const ( const (
offset = 4 /* 4 bytes TUN_PI */ offset = 4 /* 4 bytes TUN_PI */
defaultMTU = 1500 defaultMTU = 1500
) )
func newDevice(name string, mtu int) (tun.Device, error) {
return tun.CreateTUN(name, mtu)
}

View File

@ -1,6 +1,8 @@
package tun package tun
import ( import (
"github.com/Dreamacro/clash/listener/tun/device/tun/driver"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/tun" "golang.zx2c4.com/wireguard/tun"
) )
@ -20,3 +22,11 @@ func init() {
func (t *TUN) LUID() uint64 { func (t *TUN) LUID() uint64 {
return t.nt.LUID() return t.nt.LUID()
} }
func newDevice(name string, mtu int) (nt tun.Device, err error) {
if err = driver.InitWintun(); err != nil {
return
}
return tun.CreateTUN(name, mtu)
}

View File

@ -5,15 +5,16 @@ import (
"time" "time"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
D "github.com/miekg/dns" D "github.com/miekg/dns"
) )
const DefaultDnsReadTimeout = time.Second * 10 const DefaultDnsReadTimeout = time.Second * 10
func ShouldHijackDns(dnsAdds []netip.AddrPort, targetAddr netip.AddrPort) bool { func ShouldHijackDns(dnsHijack []C.DNSUrl, targetAddr netip.AddrPort, network string) bool {
for _, addrPort := range dnsAdds { for _, dns := range dnsHijack {
if addrPort == targetAddr || (addrPort.Addr().IsUnspecified() && targetAddr.Port() == 53) { if dns.Network == network && (dns.AddrPort.AddrPort == targetAddr || (dns.AddrPort.Addr().IsUnspecified() && dns.AddrPort.Port() == targetAddr.Port())) {
return true return true
} }
} }

View File

@ -1,18 +1,26 @@
package commons package commons
import ( import (
"errors"
"fmt" "fmt"
"net" "net"
"sync"
"time" "time"
"github.com/Dreamacro/clash/component/dialer" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
) )
var ( var (
defaultRoutes = []string{"1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5", "16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1"} defaultRoutes = []string{"1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5", "16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1"}
defaultInterfaceMonitorDuration = 20 * time.Second monitorDuration = 10 * time.Second
monitorStarted = false
monitorStop = make(chan struct{}, 2)
monitorMux sync.Mutex
tunStatus = C.TunDisabled
tunChangeCallback C.TUNChangeCallback
errInterfaceNotFound = errors.New("default interface not found")
) )
func ipv4MaskString(bits int) string { func ipv4MaskString(bits int) string {
@ -24,26 +32,6 @@ func ipv4MaskString(bits int) string {
return fmt.Sprintf("%d.%d.%d.%d", m[0], m[1], m[2], m[3]) return fmt.Sprintf("%d.%d.%d.%d", m[0], m[1], m[2], m[3])
} }
func defaultInterfaceChangeMonitor() { func SetTunChangeCallback(callback C.TUNChangeCallback) {
t := time.NewTicker(defaultInterfaceMonitorDuration) tunChangeCallback = callback
defer t.Stop()
for {
<-t.C
interfaceName, err := GetAutoDetectInterface()
if err != nil {
log.Warnln("[TUN] default interface monitor exited, cause: %v", err)
break
}
old := dialer.DefaultInterface.Load()
if interfaceName == old {
continue
}
dialer.DefaultInterface.Store(interfaceName)
log.Warnln("[TUN] default interface changed by monitor, %s => %s", old, interfaceName)
}
} }

View File

@ -2,17 +2,30 @@ package commons
import ( import (
"fmt" "fmt"
"net"
"net/netip" "net/netip"
"strings"
"syscall"
"time"
"github.com/Dreamacro/clash/common/cmd" "github.com/Dreamacro/clash/common/cmd"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/iface"
"github.com/Dreamacro/clash/listener/tun/device" "github.com/Dreamacro/clash/listener/tun/device"
"github.com/Dreamacro/clash/log"
"golang.org/x/net/route"
) )
func GetAutoDetectInterface() (string, error) { func GetAutoDetectInterface() (string, error) {
return cmd.ExecCmd("/bin/bash -c /sbin/route -n get default | grep 'interface:' | awk -F ' ' 'NR==1{print $2}' | xargs echo -n") ifaceM, err := defaultRouteInterface()
if err != nil {
return "", err
}
return ifaceM.Name, nil
} }
func ConfigInterfaceAddress(dev device.Device, addr netip.Prefix, forceMTU int, autoRoute bool) error { func ConfigInterfaceAddress(dev device.Device, addr netip.Prefix, _ int, autoRoute bool) error {
if !addr.Addr().Is4() { if !addr.Addr().Is4() {
return fmt.Errorf("supported ipv4 only") return fmt.Errorf("supported ipv4 only")
} }
@ -54,8 +67,6 @@ func configInterfaceRouting(interfaceName string, addr netip.Prefix) error {
} }
} }
go defaultInterfaceChangeMonitor()
return execRouterCmd("add", "-inet6", "2000::/3", interfaceName) return execRouterCmd("add", "-inet6", "2000::/3", interfaceName)
} }
@ -63,3 +74,95 @@ func execRouterCmd(action, inet, route string, interfaceName string) error {
_, err := cmd.ExecCmd(fmt.Sprintf("/sbin/route %s %s %s -interface %s", action, inet, route, interfaceName)) _, err := cmd.ExecCmd(fmt.Sprintf("/sbin/route %s %s %s -interface %s", action, inet, route, interfaceName))
return err return err
} }
func defaultRouteInterface() (*net.Interface, error) {
rib, err := route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
if err != nil {
return nil, fmt.Errorf("route.FetchRIB: %w", err)
}
msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
if err != nil {
return nil, fmt.Errorf("route.ParseRIB: %w", err)
}
for _, message := range msgs {
routeMessage := message.(*route.RouteMessage)
if routeMessage.Flags&(syscall.RTF_UP|syscall.RTF_GATEWAY|syscall.RTF_STATIC) == 0 {
continue
}
addresses := routeMessage.Addrs
if (addresses[0].Family() == syscall.AF_INET && addresses[0].(*route.Inet4Addr).IP != *(*[4]byte)(net.IPv4zero)) ||
(addresses[0].Family() == syscall.AF_INET6 && addresses[0].(*route.Inet6Addr).IP != *(*[16]byte)(net.IPv6zero)) {
continue
}
ifaceM, err1 := net.InterfaceByIndex(routeMessage.Index)
if err1 != nil {
continue
}
if strings.HasPrefix(ifaceM.Name, "utun") {
continue
}
return ifaceM, nil
}
return nil, fmt.Errorf("ambiguous gateway interfaces found")
}
func StartDefaultInterfaceChangeMonitor() {
monitorMux.Lock()
if monitorStarted {
monitorMux.Unlock()
return
}
monitorStarted = true
monitorMux.Unlock()
select {
case <-monitorStop:
default:
}
t := time.NewTicker(monitorDuration)
defer t.Stop()
for {
select {
case <-t.C:
interfaceName, err := GetAutoDetectInterface()
if err != nil {
log.Warnln("[TUN] default interface monitor err: %v", err)
continue
}
old := dialer.DefaultInterface.Load()
if interfaceName == old {
continue
}
dialer.DefaultInterface.Store(interfaceName)
iface.FlushCache()
log.Warnln("[TUN] default interface changed by monitor, %s => %s", old, interfaceName)
case <-monitorStop:
break
}
}
}
func StopDefaultInterfaceChangeMonitor() {
monitorMux.Lock()
defer monitorMux.Unlock()
if monitorStarted {
monitorStop <- struct{}{}
monitorStarted = false
}
}

View File

@ -3,16 +3,20 @@ package commons
import ( import (
"fmt" "fmt"
"net/netip" "net/netip"
"time"
"github.com/Dreamacro/clash/common/cmd" "github.com/Dreamacro/clash/common/cmd"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/iface"
"github.com/Dreamacro/clash/listener/tun/device" "github.com/Dreamacro/clash/listener/tun/device"
"github.com/Dreamacro/clash/log"
) )
func GetAutoDetectInterface() (string, error) { func GetAutoDetectInterface() (string, error) {
return cmd.ExecCmd("bash -c ip route show | grep 'default via' | awk -F ' ' 'NR==1{print $5}' | xargs echo -n") return cmd.ExecCmd("bash -c ip route show | grep 'default via' | awk -F ' ' 'NR==1{print $5}' | xargs echo -n")
} }
func ConfigInterfaceAddress(dev device.Device, addr netip.Prefix, forceMTU int, autoRoute bool) error { func ConfigInterfaceAddress(dev device.Device, addr netip.Prefix, _ int, autoRoute bool) error {
var ( var (
interfaceName = dev.Name() interfaceName = dev.Name()
ip = addr.Masked().Addr().Next() ip = addr.Masked().Addr().Next()
@ -42,8 +46,6 @@ func configInterfaceRouting(interfaceName string, addr netip.Prefix) error {
} }
} }
go defaultInterfaceChangeMonitor()
return nil return nil
} }
@ -53,3 +55,55 @@ func execRouterCmd(action, route string, interfaceName string, linkIP string) er
_, err := cmd.ExecCmd(cmdStr) _, err := cmd.ExecCmd(cmdStr)
return err return err
} }
func StartDefaultInterfaceChangeMonitor() {
monitorMux.Lock()
if monitorStarted {
monitorMux.Unlock()
return
}
monitorStarted = true
monitorMux.Unlock()
select {
case <-monitorStop:
default:
}
t := time.NewTicker(monitorDuration)
defer t.Stop()
for {
select {
case <-t.C:
interfaceName, err := GetAutoDetectInterface()
if err != nil {
log.Warnln("[TUN] default interface monitor err: %v", err)
continue
}
old := dialer.DefaultInterface.Load()
if interfaceName == old {
continue
}
dialer.DefaultInterface.Store(interfaceName)
iface.FlushCache()
log.Warnln("[TUN] default interface changed by monitor, %s => %s", old, interfaceName)
case <-monitorStop:
break
}
}
}
func StopDefaultInterfaceChangeMonitor() {
monitorMux.Lock()
defer monitorMux.Unlock()
if monitorStarted {
monitorStop <- struct{}{}
monitorStarted = false
}
}

View File

@ -17,3 +17,7 @@ func GetAutoDetectInterface() (string, error) {
func ConfigInterfaceAddress(device.Device, netip.Prefix, int, bool) error { func ConfigInterfaceAddress(device.Device, netip.Prefix, int, bool) error {
return fmt.Errorf("unsupported on this OS: %s", runtime.GOOS) return fmt.Errorf("unsupported on this OS: %s", runtime.GOOS)
} }
func StartDefaultInterfaceChangeMonitor() {}
func StopDefaultInterfaceChangeMonitor() {}

View File

@ -1,12 +1,16 @@
package commons package commons
import ( import (
"errors"
"fmt" "fmt"
"net"
"net/netip" "net/netip"
"sync"
"time" "time"
"github.com/Dreamacro/clash/common/nnip" "github.com/Dreamacro/clash/common/nnip"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/iface"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/listener/tun/device" "github.com/Dreamacro/clash/listener/tun/device"
"github.com/Dreamacro/clash/listener/tun/device/tun" "github.com/Dreamacro/clash/listener/tun/device/tun"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
@ -16,7 +20,11 @@ import (
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
) )
var wintunInterfaceName string var (
wintunInterfaceName string
unicastAddressChangeCallback *winipcfg.UnicastAddressChangeCallback
unicastAddressChangeLock sync.Mutex
)
func GetAutoDetectInterface() (string, error) { func GetAutoDetectInterface() (string, error) {
ifname, err := getAutoDetectInterfaceByFamily(winipcfg.AddressFamily(windows.AF_INET)) ifname, err := getAutoDetectInterfaceByFamily(winipcfg.AddressFamily(windows.AF_INET))
@ -205,8 +213,6 @@ startOver:
wintunInterfaceName = dev.Name() wintunInterfaceName = dev.Name()
go defaultInterfaceChangeMonitor()
return nil return nil
} }
@ -222,15 +228,15 @@ func cleanupAddressesOnDisconnectedInterfaces(family winipcfg.AddressFamily, add
if err != nil { if err != nil {
return return
} }
for _, iface := range interfaces { for _, ifaceM := range interfaces {
if iface.OperStatus == winipcfg.IfOperStatusUp { if ifaceM.OperStatus == winipcfg.IfOperStatusUp {
continue continue
} }
for address := iface.FirstUnicastAddress; address != nil; address = address.Next { for address := ifaceM.FirstUnicastAddress; address != nil; address = address.Next {
if ip := nnip.IpToAddr(address.Address.IP()); addrHash[ip] { if ip := nnip.IpToAddr(address.Address.IP()); addrHash[ip] {
prefix := netip.PrefixFrom(ip, int(address.OnLinkPrefixLength)) prefix := netip.PrefixFrom(ip, int(address.OnLinkPrefixLength))
log.Infoln("[TUN] cleaning up stale address %s from interface %s", prefix.String(), iface.FriendlyName()) log.Infoln("[TUN] cleaning up stale address %s from interface %s", prefix.String(), ifaceM.FriendlyName())
_ = iface.LUID.DeleteIPAddress(prefix) _ = ifaceM.LUID.DeleteIPAddress(prefix)
} }
} }
} }
@ -239,7 +245,7 @@ func cleanupAddressesOnDisconnectedInterfaces(family winipcfg.AddressFamily, add
func getAutoDetectInterfaceByFamily(family winipcfg.AddressFamily) (string, error) { func getAutoDetectInterfaceByFamily(family winipcfg.AddressFamily) (string, error) {
interfaces, err := winipcfg.GetAdaptersAddresses(family, winipcfg.GAAFlagIncludeGateways) interfaces, err := winipcfg.GetAdaptersAddresses(family, winipcfg.GAAFlagIncludeGateways)
if err != nil { if err != nil {
return "", fmt.Errorf("get ethernet interface failure. %w", err) return "", fmt.Errorf("get default interface failure. %w", err)
} }
var destination netip.Prefix var destination netip.Prefix
@ -249,25 +255,96 @@ func getAutoDetectInterfaceByFamily(family winipcfg.AddressFamily) (string, erro
destination = netip.PrefixFrom(netip.IPv6Unspecified(), 0) destination = netip.PrefixFrom(netip.IPv6Unspecified(), 0)
} }
for _, iface := range interfaces { for _, ifaceM := range interfaces {
if iface.OperStatus != winipcfg.IfOperStatusUp { if ifaceM.OperStatus != winipcfg.IfOperStatusUp {
continue continue
} }
ifname := iface.FriendlyName() ifname := ifaceM.FriendlyName()
if wintunInterfaceName == ifname { if wintunInterfaceName == ifname {
continue continue
} }
for gatewayAddress := iface.FirstGatewayAddress; gatewayAddress != nil; gatewayAddress = gatewayAddress.Next { for gatewayAddress := ifaceM.FirstGatewayAddress; gatewayAddress != nil; gatewayAddress = gatewayAddress.Next {
nextHop := nnip.IpToAddr(gatewayAddress.Address.IP()) nextHop := nnip.IpToAddr(gatewayAddress.Address.IP())
if _, err = iface.LUID.Route(destination, nextHop); err == nil { if _, err = ifaceM.LUID.Route(destination, nextHop); err == nil {
return ifname, nil return ifname, nil
} }
} }
} }
return "", errors.New("ethernet interface not found") return "", errInterfaceNotFound
}
func unicastAddressChange(_ winipcfg.MibNotificationType, unicastAddress *winipcfg.MibUnicastIPAddressRow) {
unicastAddressChangeLock.Lock()
defer unicastAddressChangeLock.Unlock()
interfaceName, err := GetAutoDetectInterface()
if err != nil {
if err == errInterfaceNotFound && tunStatus == C.TunEnabled {
log.Warnln("[TUN] lost the default interface, pause tun adapter")
tunStatus = C.TunPaused
tunChangeCallback.Pause()
}
return
}
ifaceM, err := net.InterfaceByIndex(int(unicastAddress.InterfaceIndex))
if err != nil {
log.Warnln("[TUN] default interface monitor err: %v", err)
return
}
newName := ifaceM.Name
if newName != interfaceName {
return
}
dialer.DefaultInterface.Store(interfaceName)
iface.FlushCache()
if tunStatus == C.TunPaused {
log.Warnln("[TUN] found interface %s(%s), resume tun adapter", interfaceName, unicastAddress.Address.Addr())
tunStatus = C.TunEnabled
tunChangeCallback.Resume()
return
}
log.Warnln("[TUN] default interface changed to %s(%s) by monitor", interfaceName, unicastAddress.Address.Addr())
}
func StartDefaultInterfaceChangeMonitor() {
if unicastAddressChangeCallback != nil {
return
}
var err error
unicastAddressChangeCallback, err = winipcfg.RegisterUnicastAddressChangeCallback(unicastAddressChange)
if err != nil {
log.Errorln("[TUN] register uni-cast address change callback failed: %v", err)
return
}
tunStatus = C.TunEnabled
log.Infoln("[TUN] register uni-cast address change callback")
}
func StopDefaultInterfaceChangeMonitor() {
if unicastAddressChangeCallback == nil || tunStatus == C.TunPaused {
return
}
_ = unicastAddressChangeCallback.Unregister()
unicastAddressChangeCallback = nil
tunChangeCallback = nil
tunStatus = C.TunDisabled
} }

View File

@ -20,7 +20,7 @@ var _ adapter.Handler = (*gvHandler)(nil)
type gvHandler struct { type gvHandler struct {
gateway netip.Addr gateway netip.Addr
dnsHijack []netip.AddrPort dnsHijack []C.DNSUrl
tcpIn chan<- C.ConnContext tcpIn chan<- C.ConnContext
udpIn chan<- *inbound.PacketAdapter udpIn chan<- *inbound.PacketAdapter
@ -37,7 +37,7 @@ func (gh *gvHandler) HandleTCP(tunConn adapter.TCPConn) {
rAddrPort := netip.AddrPortFrom(nnip.IpToAddr(rAddr.IP), id.LocalPort) rAddrPort := netip.AddrPortFrom(nnip.IpToAddr(rAddr.IP), id.LocalPort)
if D.ShouldHijackDns(gh.dnsHijack, rAddrPort) { if D.ShouldHijackDns(gh.dnsHijack, rAddrPort, "tcp") {
go func() { go func() {
log.Debugln("[TUN] hijack dns tcp: %s", rAddrPort.String()) log.Debugln("[TUN] hijack dns tcp: %s", rAddrPort.String())
@ -111,7 +111,7 @@ func (gh *gvHandler) HandleUDP(tunConn adapter.UDPConn) {
payload := buf[:n] payload := buf[:n]
if D.ShouldHijackDns(gh.dnsHijack, rAddrPort) { if D.ShouldHijackDns(gh.dnsHijack, rAddrPort, "udp") {
go func() { go func() {
defer func() { defer func() {
_ = pool.Put(buf) _ = pool.Put(buf)

View File

@ -8,6 +8,7 @@ import (
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/listener/tun/device" "github.com/Dreamacro/clash/listener/tun/device"
"github.com/Dreamacro/clash/listener/tun/ipstack" "github.com/Dreamacro/clash/listener/tun/ipstack"
"github.com/Dreamacro/clash/listener/tun/ipstack/commons"
"github.com/Dreamacro/clash/listener/tun/ipstack/gvisor/option" "github.com/Dreamacro/clash/listener/tun/ipstack/gvisor/option"
"gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip"
@ -25,6 +26,8 @@ type gvStack struct {
} }
func (s *gvStack) Close() error { func (s *gvStack) Close() error {
commons.StopDefaultInterfaceChangeMonitor()
var err error var err error
if s.device != nil { if s.device != nil {
err = s.device.Close() err = s.device.Close()
@ -37,7 +40,7 @@ func (s *gvStack) Close() error {
} }
// New allocates a new *gvStack with given options. // New allocates a new *gvStack with given options.
func New(device device.Device, dnsHijack []netip.AddrPort, tunAddress netip.Prefix, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter, opts ...option.Option) (ipstack.Stack, error) { func New(device device.Device, dnsHijack []C.DNSUrl, tunAddress netip.Prefix, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter, opts ...option.Option) (ipstack.Stack, error) {
s := &gvStack{ s := &gvStack{
Stack: stack.New(stack.Options{ Stack: stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{ NetworkProtocols: []stack.NetworkProtocolFactory{

View File

@ -34,6 +34,8 @@ type sysStack struct {
} }
func (s *sysStack) Close() error { func (s *sysStack) Close() error {
D.StopDefaultInterfaceChangeMonitor()
defer func() { defer func() {
if s.device != nil { if s.device != nil {
_ = s.device.Close() _ = s.device.Close()
@ -49,7 +51,7 @@ func (s *sysStack) Close() error {
return err return err
} }
func New(device device.Device, dnsHijack []netip.AddrPort, tunAddress netip.Prefix, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) (ipstack.Stack, error) { func New(device device.Device, dnsHijack []C.DNSUrl, tunAddress netip.Prefix, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) (ipstack.Stack, error) {
var ( var (
gateway = tunAddress.Masked().Addr().Next() gateway = tunAddress.Masked().Addr().Next()
portal = gateway.Next() portal = gateway.Next()
@ -91,7 +93,7 @@ func New(device device.Device, dnsHijack []netip.AddrPort, tunAddress netip.Pref
continue continue
} }
if D.ShouldHijackDns(dnsAddr, rAddrPort) { if D.ShouldHijackDns(dnsAddr, rAddrPort, "tcp") {
go func() { go func() {
log.Debugln("[TUN] hijack dns tcp: %s", rAddrPort.String()) log.Debugln("[TUN] hijack dns tcp: %s", rAddrPort.String())
@ -175,7 +177,7 @@ func New(device device.Device, dnsHijack []netip.AddrPort, tunAddress netip.Pref
continue continue
} }
if D.ShouldHijackDns(dnsAddr, rAddrPort) { if D.ShouldHijackDns(dnsAddr, rAddrPort, "udp") {
go func() { go func() {
msg, err := D.RelayDnsPacket(raw) msg, err := D.RelayDnsPacket(raw)
if err != nil { if err != nil {

View File

@ -24,7 +24,7 @@ import (
) )
// New TunAdapter // New TunAdapter
func New(tunConf *config.Tun, tunAddressPrefix *netip.Prefix, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) (ipstack.Stack, error) { func New(tunConf *config.Tun, tunAddressPrefix *netip.Prefix, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter, tunChangeCallback C.TUNChangeCallback) (ipstack.Stack, error) {
var ( var (
tunAddress = netip.Prefix{} tunAddress = netip.Prefix{}
devName = tunConf.Device devName = tunConf.Device
@ -38,6 +38,12 @@ func New(tunConf *config.Tun, tunAddressPrefix *netip.Prefix, tcpIn chan<- C.Con
err error err error
) )
defer func() {
if err != nil && tunDevice != nil {
_ = tunDevice.Close()
}
}()
if devName == "" { if devName == "" {
devName = generateDeviceName() devName = generateDeviceName()
} }
@ -63,26 +69,22 @@ func New(tunConf *config.Tun, tunAddressPrefix *netip.Prefix, tcpIn chan<- C.Con
case C.TunGvisor: case C.TunGvisor:
err = tunDevice.UseEndpoint() err = tunDevice.UseEndpoint()
if err != nil { if err != nil {
_ = tunDevice.Close()
return nil, fmt.Errorf("can't attach endpoint to tun: %w", err) return nil, fmt.Errorf("can't attach endpoint to tun: %w", err)
} }
tunStack, err = gvisor.New(tunDevice, tunConf.DNSHijack, tunAddress, tcpIn, udpIn, option.WithDefault()) tunStack, err = gvisor.New(tunDevice, tunConf.DNSHijack, tunAddress, tcpIn, udpIn, option.WithDefault())
if err != nil { if err != nil {
_ = tunDevice.Close()
return nil, fmt.Errorf("can't New gvisor stack: %w", err) return nil, fmt.Errorf("can't New gvisor stack: %w", err)
} }
case C.TunSystem: case C.TunSystem:
err = tunDevice.UseIOBased() err = tunDevice.UseIOBased()
if err != nil { if err != nil {
_ = tunDevice.Close()
return nil, fmt.Errorf("can't New system stack: %w", err) return nil, fmt.Errorf("can't New system stack: %w", err)
} }
tunStack, err = system.New(tunDevice, tunConf.DNSHijack, tunAddress, tcpIn, udpIn) tunStack, err = system.New(tunDevice, tunConf.DNSHijack, tunAddress, tcpIn, udpIn)
if err != nil { if err != nil {
_ = tunDevice.Close()
return nil, fmt.Errorf("can't New system stack: %w", err) return nil, fmt.Errorf("can't New system stack: %w", err)
} }
default: default:
@ -92,10 +94,14 @@ func New(tunConf *config.Tun, tunAddressPrefix *netip.Prefix, tcpIn chan<- C.Con
// setting address and routing // setting address and routing
err = commons.ConfigInterfaceAddress(tunDevice, tunAddress, mtu, autoRoute) err = commons.ConfigInterfaceAddress(tunDevice, tunAddress, mtu, autoRoute)
if err != nil { if err != nil {
_ = tunDevice.Close()
return nil, fmt.Errorf("setting interface address and routing failed: %w", err) return nil, fmt.Errorf("setting interface address and routing failed: %w", err)
} }
if tunConf.AutoDetectInterface {
commons.SetTunChangeCallback(tunChangeCallback)
go commons.StartDefaultInterfaceChangeMonitor()
}
setAtLatest(stackType, devName) setAtLatest(stackType, devName)
log.Infoln("TUN stack listening at: %s(%s), mtu: %d, auto route: %v, ip stack: %s", tunDevice.Name(), tunAddress.Masked().Addr().Next().String(), mtu, autoRoute, stackType) log.Infoln("TUN stack listening at: %s(%s), mtu: %d, auto route: %v, ip stack: %s", tunDevice.Name(), tunAddress.Masked().Addr().Next().String(), mtu, autoRoute, stackType)

View File

@ -5,7 +5,6 @@ import (
"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"
_ "github.com/Dreamacro/clash/component/geodata/standard"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
) )
@ -47,12 +46,19 @@ func (gs *GEOSITE) GetDomainMatcher() *router.DomainMatcher {
} }
func NewGEOSITE(country string, adapter string) (*GEOSITE, error) { func NewGEOSITE(country string, adapter string) (*GEOSITE, error) {
matcher, recordsCount, err := geodata.LoadGeoSiteMatcher(country) matcher, recordsCount, err := geodata.LoadProviderByCode(country)
if err != nil { if err != nil {
return nil, fmt.Errorf("load GeoSite data error, %s", err.Error()) return nil, fmt.Errorf("load GeoSite data error, %s", err.Error())
} }
log.Infoln("Start initial GeoSite rule %s => %s, records: %d", country, adapter, recordsCount) cont := fmt.Sprintf("%d", recordsCount)
if recordsCount == 0 {
cont = "from cache"
}
if adapter == C.ScriptRuleGeoSiteTarget {
adapter = "Script"
}
log.Infoln("Start initial GeoSite rule %s => %s, records: %s", country, adapter, cont)
geoSite := &GEOSITE{ geoSite := &GEOSITE{
Base: &Base{}, Base: &Base{},

16
test/.golangci.yaml Normal file
View File

@ -0,0 +1,16 @@
linters:
disable-all: true
enable:
- gofumpt
- govet
- gci
- staticcheck
linters-settings:
gci:
sections:
- standard
- prefix(github.com/Dreamacro/clash)
- default
staticcheck:
go: '1.18'

View File

@ -1,8 +1,9 @@
lint: lint:
golangci-lint run --disable-all -E govet -E gofumpt -E megacheck ./... GOOS=darwin golangci-lint run ./...
GOOS=linux golangci-lint run ./...
test: test:
go test -p 1 -v ./... go test -p 1 -v ./...
benchmark: benchmark:
go test -benchmem -run=^$ -bench . go test -benchmem -run=^$$ -bench .

View File

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"io" "io"
"net" "net"
"net/netip"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -24,6 +25,7 @@ import (
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
const ( const (
@ -38,7 +40,7 @@ const (
var ( var (
waitTime = time.Second waitTime = time.Second
localIP = net.ParseIP("127.0.0.1") localIP = netip.MustParseAddr("127.0.0.1")
defaultExposedPorts = nat.PortSet{ defaultExposedPorts = nat.PortSet{
"10002/tcp": struct{}{}, "10002/tcp": struct{}{},
@ -52,13 +54,10 @@ var (
{HostPort: "10002", HostIP: "0.0.0.0"}, {HostPort: "10002", HostIP: "0.0.0.0"},
}, },
} }
isDarwin = runtime.GOOS == "darwin"
) )
func init() { func init() {
if runtime.GOOS == "darwin" {
isDarwin = true
}
currentDir, err := os.Getwd() currentDir, err := os.Getwd()
if err != nil { if err != nil {
panic(err) panic(err)
@ -67,10 +66,11 @@ func init() {
C.SetHomeDir(homeDir) C.SetHomeDir(homeDir)
if isDarwin { if isDarwin {
localIP, err = defaultRouteIP() routeIp, err := defaultRouteIP()
if err != nil { if err != nil {
panic(err) panic(err)
} }
localIP = netip.MustParseAddr(routeIp.String())
} }
c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
@ -110,6 +110,7 @@ func init() {
continue continue
} }
println("pulling image:", image)
imageStream, err := c.ImagePull(context.Background(), image, types.ImagePullOptions{}) imageStream, err := c.ImagePull(context.Background(), image, types.ImagePullOptions{})
if err != nil { if err != nil {
panic(err) panic(err)
@ -214,46 +215,35 @@ func testPingPongWithSocksPort(t *testing.T, port int) {
pingCh, pongCh, test := newPingPongPair() pingCh, pongCh, test := newPingPongPair()
go func() { go func() {
l, err := Listen("tcp", ":10001") l, err := Listen("tcp", ":10001")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
defer l.Close() defer l.Close()
c, err := l.Accept() c, err := l.Accept()
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
buf := make([]byte, 4) buf := make([]byte, 4)
if _, err := io.ReadFull(c, buf); err != nil { _, err = io.ReadFull(c, buf)
assert.FailNow(t, err.Error()) require.NoError(t, err)
}
pingCh <- buf pingCh <- buf
if _, err := c.Write([]byte("pong")); err != nil { _, err = c.Write([]byte("pong"))
assert.FailNow(t, err.Error()) require.NoError(t, err)
}
}() }()
go func() { go func() {
c, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) c, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port))
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
defer c.Close() defer c.Close()
if _, err := socks5.ClientHandshake(c, socks5.ParseAddr("127.0.0.1:10001"), socks5.CmdConnect, nil); err != nil { _, err = socks5.ClientHandshake(c, socks5.ParseAddr("127.0.0.1:10001"), socks5.CmdConnect, nil)
assert.FailNow(t, err.Error()) require.NoError(t, err)
}
if _, err := c.Write([]byte("ping")); err != nil { _, err = c.Write([]byte("ping"))
assert.FailNow(t, err.Error()) require.NoError(t, err)
}
buf := make([]byte, 4) buf := make([]byte, 4)
if _, err := io.ReadFull(c, buf); err != nil { _, err = io.ReadFull(c, buf)
assert.FailNow(t, err.Error()) require.NoError(t, err)
}
pongCh <- buf pongCh <- buf
}() }()
@ -304,12 +294,10 @@ func testPingPongWithConn(t *testing.T, c net.Conn) error {
func testPingPongWithPacketConn(t *testing.T, pc net.PacketConn) error { func testPingPongWithPacketConn(t *testing.T, pc net.PacketConn) error {
l, err := ListenPacket("udp", ":10001") l, err := ListenPacket("udp", ":10001")
if err != nil { require.NoError(t, err)
return err
}
defer l.Close() defer l.Close()
rAddr := &net.UDPAddr{IP: localIP, Port: 10001} rAddr := &net.UDPAddr{IP: localIP.AsSlice(), Port: 10001}
pingCh, pongCh, test := newPingPongPair() pingCh, pongCh, test := newPingPongPair()
go func() { go func() {
@ -349,9 +337,7 @@ type hashPair struct {
func testLargeDataWithConn(t *testing.T, c net.Conn) error { func testLargeDataWithConn(t *testing.T, c net.Conn) error {
l, err := Listen("tcp", ":10001") l, err := Listen("tcp", ":10001")
if err != nil { require.NoError(t, err)
return err
}
defer l.Close() defer l.Close()
times := 100 times := 100
@ -443,12 +429,10 @@ func testLargeDataWithConn(t *testing.T, c net.Conn) error {
func testLargeDataWithPacketConn(t *testing.T, pc net.PacketConn) error { func testLargeDataWithPacketConn(t *testing.T, pc net.PacketConn) error {
l, err := ListenPacket("udp", ":10001") l, err := ListenPacket("udp", ":10001")
if err != nil { require.NoError(t, err)
return err
}
defer l.Close() defer l.Close()
rAddr := &net.UDPAddr{IP: localIP, Port: 10001} rAddr := &net.UDPAddr{IP: localIP.AsSlice(), Port: 10001}
times := 50 times := 50
chunkSize := int64(1024) chunkSize := int64(1024)
@ -541,7 +525,7 @@ func testLargeDataWithPacketConn(t *testing.T, pc net.PacketConn) error {
func testPacketConnTimeout(t *testing.T, pc net.PacketConn) error { func testPacketConnTimeout(t *testing.T, pc net.PacketConn) error {
err := pc.SetReadDeadline(time.Now().Add(time.Millisecond * 300)) err := pc.SetReadDeadline(time.Now().Add(time.Millisecond * 300))
assert.NoError(t, err) require.NoError(t, err)
errCh := make(chan error, 1) errCh := make(chan error, 1)
go func() { go func() {
@ -564,9 +548,7 @@ func testSuit(t *testing.T, proxy C.ProxyAdapter) {
DstPort: "10001", DstPort: "10001",
AddrType: socks5.AtypDomainName, AddrType: socks5.AtypDomainName,
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
defer conn.Close() defer conn.Close()
assert.NoError(t, testPingPongWithConn(t, conn)) assert.NoError(t, testPingPongWithConn(t, conn))
@ -575,9 +557,7 @@ func testSuit(t *testing.T, proxy C.ProxyAdapter) {
DstPort: "10001", DstPort: "10001",
AddrType: socks5.AtypDomainName, AddrType: socks5.AtypDomainName,
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
defer conn.Close() defer conn.Close()
assert.NoError(t, testLargeDataWithConn(t, conn)) assert.NoError(t, testLargeDataWithConn(t, conn))
@ -591,9 +571,7 @@ func testSuit(t *testing.T, proxy C.ProxyAdapter) {
DstPort: "10001", DstPort: "10001",
AddrType: socks5.AtypIPv4, AddrType: socks5.AtypIPv4,
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
defer pc.Close() defer pc.Close()
assert.NoError(t, testPingPongWithPacketConn(t, pc)) assert.NoError(t, testPingPongWithPacketConn(t, pc))
@ -604,9 +582,7 @@ func testSuit(t *testing.T, proxy C.ProxyAdapter) {
DstPort: "10001", DstPort: "10001",
AddrType: socks5.AtypIPv4, AddrType: socks5.AtypIPv4,
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
defer pc.Close() defer pc.Close()
assert.NoError(t, testLargeDataWithPacketConn(t, pc)) assert.NoError(t, testLargeDataWithPacketConn(t, pc))
@ -617,9 +593,7 @@ func testSuit(t *testing.T, proxy C.ProxyAdapter) {
DstPort: "10001", DstPort: "10001",
AddrType: socks5.AtypIPv4, AddrType: socks5.AtypIPv4,
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
defer pc.Close() defer pc.Close()
assert.NoError(t, testPacketConnTimeout(t, pc)) assert.NoError(t, testPacketConnTimeout(t, pc))
@ -627,40 +601,55 @@ func testSuit(t *testing.T, proxy C.ProxyAdapter) {
func benchmarkProxy(b *testing.B, proxy C.ProxyAdapter) { func benchmarkProxy(b *testing.B, proxy C.ProxyAdapter) {
l, err := Listen("tcp", ":10001") l, err := Listen("tcp", ":10001")
if err != nil { require.NoError(b, err)
assert.FailNow(b, err.Error())
}
defer l.Close() defer l.Close()
go func() {
c, err := l.Accept()
if err != nil {
assert.FailNow(b, err.Error())
}
defer c.Close()
io.Copy(io.Discard, c)
}()
chunkSize := int64(16 * 1024) chunkSize := int64(16 * 1024)
chunk := make([]byte, chunkSize) chunk := make([]byte, chunkSize)
rand.Read(chunk) rand.Read(chunk)
go func() {
c, err := l.Accept()
if err != nil {
return
}
defer c.Close()
go func() {
for {
_, err := c.Write(chunk)
if err != nil {
return
}
}
}()
io.Copy(io.Discard, c)
}()
conn, err := proxy.DialContext(context.Background(), &C.Metadata{ conn, err := proxy.DialContext(context.Background(), &C.Metadata{
Host: localIP.String(), Host: localIP.String(),
DstPort: "10001", DstPort: "10001",
AddrType: socks5.AtypDomainName, AddrType: socks5.AtypDomainName,
}) })
if err != nil { require.NoError(b, err)
assert.FailNow(b, err.Error())
}
b.SetBytes(chunkSize) _, err = conn.Write([]byte("skip protocol handshake"))
b.ResetTimer() require.NoError(b, err)
for i := 0; i < b.N; i++ {
if _, err := conn.Write(chunk); err != nil { b.Run("Write", func(b *testing.B) {
assert.FailNow(b, err.Error()) b.SetBytes(chunkSize)
for i := 0; i < b.N; i++ {
conn.Write(chunk)
} }
} })
b.Run("Read", func(b *testing.B) {
b.SetBytes(chunkSize)
buf := make([]byte, chunkSize)
for i := 0; i < b.N; i++ {
io.ReadFull(conn, buf)
}
})
} }
func TestClash_Basic(t *testing.T) { func TestClash_Basic(t *testing.T) {
@ -669,12 +658,11 @@ mixed-port: 10000
log-level: silent log-level: silent
` `
if err := parseAndApply(basic); err != nil { err := parseAndApply(basic)
assert.FailNow(t, err.Error()) require.NoError(t, err)
}
defer cleanup() defer cleanup()
time.Sleep(waitTime) require.True(t, TCPing(net.JoinHostPort(localIP.String(), "10000")))
testPingPongWithSocksPort(t, 10000) testPingPongWithSocksPort(t, 10000)
} }

View File

@ -6,6 +6,7 @@ import (
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func exchange(address, domain string, tp uint16) ([]dns.RR, error) { func exchange(address, domain string, tp uint16) ([]dns.RR, error) {
@ -30,18 +31,15 @@ dns:
- 119.29.29.29 - 119.29.29.29
` `
if err := parseAndApply(basic); err != nil { err := parseAndApply(basic)
assert.FailNow(t, err.Error()) require.NoError(t, err)
}
defer cleanup() defer cleanup()
time.Sleep(waitTime) time.Sleep(waitTime)
rr, err := exchange("127.0.0.1:8553", "1.1.1.1.nip.io", dns.TypeA) rr, err := exchange("127.0.0.1:8553", "1.1.1.1.nip.io", dns.TypeA)
assert.NoError(t, err) assert.NoError(t, err)
if !assert.NotEmpty(t, rr) { assert.NotEmptyf(t, rr, "record empty")
assert.FailNow(t, "record empty")
}
record := rr[0].(*dns.A) record := rr[0].(*dns.A)
assert.Equal(t, record.A.String(), "1.1.1.1") assert.Equal(t, record.A.String(), "1.1.1.1")
@ -68,9 +66,8 @@ dns:
- 119.29.29.29 - 119.29.29.29
` `
if err := parseAndApply(basic); err != nil { err := parseAndApply(basic)
assert.FailNow(t, err.Error()) require.NoError(t, err)
}
defer cleanup() defer cleanup()
time.Sleep(waitTime) time.Sleep(waitTime)

View File

@ -8,8 +8,6 @@ import (
"github.com/docker/docker/client" "github.com/docker/docker/client"
) )
var isDarwin = false
func startContainer(cfg *container.Config, hostCfg *container.HostConfig, name string) (string, error) { func startContainer(cfg *container.Config, hostCfg *container.HostConfig, name string) (string, error) {
c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil { if err != nil {

View File

@ -3,31 +3,28 @@ module clash-test
go 1.18 go 1.18
require ( require (
github.com/Dreamacro/clash v1.7.2-0.20211108085948-bd2ea2b917aa github.com/Dreamacro/clash v0.0.0
github.com/docker/docker v20.10.13+incompatible github.com/docker/docker v20.10.16+incompatible
github.com/docker/go-connections v0.4.0 github.com/docker/go-connections v0.4.0
github.com/miekg/dns v1.1.48 github.com/miekg/dns v1.1.49
github.com/stretchr/testify v1.7.1 github.com/stretchr/testify v1.7.1
golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 golang.org/x/net v0.0.0-20220531201128-c960675eff93
) )
replace github.com/Dreamacro/clash => ../ replace github.com/Dreamacro/clash => ../
require ( require (
github.com/Dreamacro/go-shadowsocks2 v0.1.7 // indirect github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/containerd/containerd v1.6.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/go-units v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect
github.com/gofrs/uuid v4.2.0+incompatible // indirect github.com/gofrs/uuid v4.2.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/btree v1.0.1 // indirect github.com/google/btree v1.0.1 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect
github.com/insomniacslk/dhcp v0.0.0-20220119180841-3c283ff8b7dd // indirect github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/oschwald/geoip2-golang v1.7.0 // indirect github.com/oschwald/geoip2-golang v1.7.0 // indirect
@ -39,22 +36,21 @@ require (
github.com/xtls/go v0.0.0-20210920065950-d4af136d3672 // indirect github.com/xtls/go v0.0.0-20210920065950-d4af136d3672 // indirect
go.etcd.io/bbolt v1.3.6 // indirect go.etcd.io/bbolt v1.3.6 // indirect
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 // indirect golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab // indirect golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab // indirect
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
golang.org/x/tools v0.1.9 // indirect golang.org/x/tools v0.1.10 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
golang.zx2c4.com/wireguard v0.0.0-20220318042302-193cf8d6a5d6 // indirect golang.zx2c4.com/wireguard v0.0.0-20220601130007-6a08d81f6bc4 // indirect
golang.zx2c4.com/wireguard/windows v0.5.4-0.20220317000008-6432784c2469 // indirect golang.zx2c4.com/wireguard/windows v0.5.4-0.20220328111914-004c22c5647e // indirect
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
google.golang.org/grpc v1.45.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.1.0 // indirect gotest.tools/v3 v3.1.0 // indirect
gvisor.dev/gvisor v0.0.0-20220422224113-2cca6b79d9f4 // indirect gvisor.dev/gvisor v0.0.0-20220601233344-46e478629075 // indirect
) )

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@ import (
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/require"
) )
func TestClash_SnellObfsHTTP(t *testing.T) { func TestClash_SnellObfsHTTP(t *testing.T) {
@ -24,9 +24,7 @@ func TestClash_SnellObfsHTTP(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "snell-http") id, err := startContainer(cfg, hostCfg, "snell-http")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
t.Cleanup(func() { t.Cleanup(func() {
cleanContainer(id) cleanContainer(id)
@ -41,9 +39,7 @@ func TestClash_SnellObfsHTTP(t *testing.T) {
"mode": "http", "mode": "http",
}, },
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -61,9 +57,7 @@ func TestClash_SnellObfsTLS(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "snell-tls") id, err := startContainer(cfg, hostCfg, "snell-tls")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
t.Cleanup(func() { t.Cleanup(func() {
cleanContainer(id) cleanContainer(id)
@ -78,9 +72,7 @@ func TestClash_SnellObfsTLS(t *testing.T) {
"mode": "tls", "mode": "tls",
}, },
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -98,9 +90,7 @@ func TestClash_Snell(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "snell") id, err := startContainer(cfg, hostCfg, "snell")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
t.Cleanup(func() { t.Cleanup(func() {
cleanContainer(id) cleanContainer(id)
@ -112,9 +102,7 @@ func TestClash_Snell(t *testing.T) {
Port: 10002, Port: 10002,
Psk: "password", Psk: "password",
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -132,9 +120,7 @@ func TestClash_Snellv3(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "snell") id, err := startContainer(cfg, hostCfg, "snell")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
t.Cleanup(func() { t.Cleanup(func() {
cleanContainer(id) cleanContainer(id)
@ -148,9 +134,7 @@ func TestClash_Snellv3(t *testing.T) {
UDP: true, UDP: true,
Version: 3, Version: 3,
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -167,10 +151,8 @@ func Benchmark_Snell(b *testing.B) {
Binds: []string{fmt.Sprintf("%s:/config.conf", C.Path.Resolve("snell-http.conf"))}, Binds: []string{fmt.Sprintf("%s:/config.conf", C.Path.Resolve("snell-http.conf"))},
} }
id, err := startContainer(cfg, hostCfg, "snell-http") id, err := startContainer(cfg, hostCfg, "snell-bench")
if err != nil { require.NoError(b, err)
assert.FailNow(b, err.Error())
}
b.Cleanup(func() { b.Cleanup(func() {
cleanContainer(id) cleanContainer(id)
@ -185,9 +167,7 @@ func Benchmark_Snell(b *testing.B) {
"mode": "http", "mode": "http",
}, },
}) })
if err != nil { require.NoError(b, err)
assert.FailNow(b, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
benchmarkProxy(b, proxy) benchmarkProxy(b, proxy)

View File

@ -1,13 +1,14 @@
package main package main
import ( import (
"net"
"testing" "testing"
"time" "time"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/require"
) )
func TestClash_Shadowsocks(t *testing.T) { func TestClash_Shadowsocks(t *testing.T) {
@ -22,9 +23,7 @@ func TestClash_Shadowsocks(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "ss") id, err := startContainer(cfg, hostCfg, "ss")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
t.Cleanup(func() { t.Cleanup(func() {
cleanContainer(id) cleanContainer(id)
@ -38,9 +37,7 @@ func TestClash_Shadowsocks(t *testing.T) {
Cipher: "chacha20-ietf-poly1305", Cipher: "chacha20-ietf-poly1305",
UDP: true, UDP: true,
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -60,9 +57,7 @@ func TestClash_ShadowsocksObfsHTTP(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "ss-obfs-http") id, err := startContainer(cfg, hostCfg, "ss-obfs-http")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
t.Cleanup(func() { t.Cleanup(func() {
cleanContainer(id) cleanContainer(id)
@ -80,9 +75,7 @@ func TestClash_ShadowsocksObfsHTTP(t *testing.T) {
"mode": "http", "mode": "http",
}, },
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -102,9 +95,7 @@ func TestClash_ShadowsocksObfsTLS(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "ss-obfs-tls") id, err := startContainer(cfg, hostCfg, "ss-obfs-tls")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
t.Cleanup(func() { t.Cleanup(func() {
cleanContainer(id) cleanContainer(id)
@ -122,9 +113,7 @@ func TestClash_ShadowsocksObfsTLS(t *testing.T) {
"mode": "tls", "mode": "tls",
}, },
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -144,9 +133,7 @@ func TestClash_ShadowsocksV2RayPlugin(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "ss-v2ray-plugin") id, err := startContainer(cfg, hostCfg, "ss-v2ray-plugin")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
t.Cleanup(func() { t.Cleanup(func() {
cleanContainer(id) cleanContainer(id)
@ -164,9 +151,7 @@ func TestClash_ShadowsocksV2RayPlugin(t *testing.T) {
"mode": "websocket", "mode": "websocket",
}, },
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -183,10 +168,8 @@ func Benchmark_Shadowsocks(b *testing.B) {
PortBindings: defaultPortBindings, PortBindings: defaultPortBindings,
} }
id, err := startContainer(cfg, hostCfg, "ss") id, err := startContainer(cfg, hostCfg, "ss-bench")
if err != nil { require.NoError(b, err)
assert.FailNow(b, err.Error())
}
b.Cleanup(func() { b.Cleanup(func() {
cleanContainer(id) cleanContainer(id)
@ -200,10 +183,8 @@ func Benchmark_Shadowsocks(b *testing.B) {
Cipher: "aes-256-gcm", Cipher: "aes-256-gcm",
UDP: true, UDP: true,
}) })
if err != nil { require.NoError(b, err)
assert.FailNow(b, err.Error())
}
time.Sleep(waitTime) require.True(b, TCPing(net.JoinHostPort(localIP.String(), "10002")))
benchmarkProxy(b, proxy) benchmarkProxy(b, proxy)
} }

View File

@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"net"
"testing" "testing"
"time" "time"
@ -9,7 +10,7 @@ import (
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/require"
) )
func TestClash_Trojan(t *testing.T) { func TestClash_Trojan(t *testing.T) {
@ -27,9 +28,7 @@ func TestClash_Trojan(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "trojan") id, err := startContainer(cfg, hostCfg, "trojan")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
t.Cleanup(func() { t.Cleanup(func() {
cleanContainer(id) cleanContainer(id)
@ -44,9 +43,7 @@ func TestClash_Trojan(t *testing.T) {
SkipCertVerify: true, SkipCertVerify: true,
UDP: true, UDP: true,
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -67,10 +64,10 @@ func TestClash_TrojanGrpc(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "trojan-grpc") id, err := startContainer(cfg, hostCfg, "trojan-grpc")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error()) t.Cleanup(func() {
} cleanContainer(id)
defer cleanContainer(id) })
proxy, err := outbound.NewTrojan(outbound.TrojanOption{ proxy, err := outbound.NewTrojan(outbound.TrojanOption{
Name: "trojan", Name: "trojan",
@ -85,9 +82,7 @@ func TestClash_TrojanGrpc(t *testing.T) {
GrpcServiceName: "example", GrpcServiceName: "example",
}, },
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -108,10 +103,10 @@ func TestClash_TrojanWebsocket(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "trojan-ws") id, err := startContainer(cfg, hostCfg, "trojan-ws")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error()) t.Cleanup(func() {
} cleanContainer(id)
defer cleanContainer(id) })
proxy, err := outbound.NewTrojan(outbound.TrojanOption{ proxy, err := outbound.NewTrojan(outbound.TrojanOption{
Name: "trojan", Name: "trojan",
@ -123,9 +118,7 @@ func TestClash_TrojanWebsocket(t *testing.T) {
UDP: true, UDP: true,
Network: "ws", Network: "ws",
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -146,10 +139,11 @@ func TestClash_TrojanXTLS(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "trojan-xtls") id, err := startContainer(cfg, hostCfg, "trojan-xtls")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
} t.Cleanup(func() {
defer cleanContainer(id) cleanContainer(id)
})
proxy, err := outbound.NewTrojan(outbound.TrojanOption{ proxy, err := outbound.NewTrojan(outbound.TrojanOption{
Name: "trojan", Name: "trojan",
@ -163,9 +157,7 @@ func TestClash_TrojanXTLS(t *testing.T) {
Flow: "xtls-rprx-direct", Flow: "xtls-rprx-direct",
FlowShow: true, FlowShow: true,
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -185,10 +177,8 @@ func Benchmark_Trojan(b *testing.B) {
}, },
} }
id, err := startContainer(cfg, hostCfg, "trojan") id, err := startContainer(cfg, hostCfg, "trojan-bench")
if err != nil { require.NoError(b, err)
assert.FailNow(b, err.Error())
}
b.Cleanup(func() { b.Cleanup(func() {
cleanContainer(id) cleanContainer(id)
@ -203,10 +193,8 @@ func Benchmark_Trojan(b *testing.B) {
SkipCertVerify: true, SkipCertVerify: true,
UDP: true, UDP: true,
}) })
if err != nil { require.NoError(b, err)
assert.FailNow(b, err.Error())
}
time.Sleep(waitTime) require.True(b, TCPing(net.JoinHostPort(localIP.String(), "10002")))
benchmarkProxy(b, proxy) benchmarkProxy(b, proxy)
} }

View File

@ -35,3 +35,16 @@ func ListenPacket(network, address string) (net.PacketConn, error) {
} }
return nil, lastErr return nil, lastErr
} }
func TCPing(addr string) bool {
for i := 0; i < 10; i++ {
conn, err := net.Dial("tcp", addr)
if err == nil {
conn.Close()
return true
}
time.Sleep(time.Millisecond * 500)
}
return false
}

View File

@ -9,7 +9,7 @@ import (
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/require"
) )
func TestClash_VlessTLS(t *testing.T) { func TestClash_VlessTLS(t *testing.T) {
@ -27,10 +27,10 @@ func TestClash_VlessTLS(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "vless-tls") id, err := startContainer(cfg, hostCfg, "vless-tls")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error()) t.Cleanup(func() {
} cleanContainer(id)
defer cleanContainer(id) })
proxy, err := outbound.NewVless(outbound.VlessOption{ proxy, err := outbound.NewVless(outbound.VlessOption{
Name: "vless", Name: "vless",
@ -41,9 +41,7 @@ func TestClash_VlessTLS(t *testing.T) {
ServerName: "example.org", ServerName: "example.org",
UDP: true, UDP: true,
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -64,10 +62,10 @@ func TestClash_VlessXTLS(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "vless-xtls") id, err := startContainer(cfg, hostCfg, "vless-xtls")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error()) t.Cleanup(func() {
} cleanContainer(id)
defer cleanContainer(id) })
proxy, err := outbound.NewVless(outbound.VlessOption{ proxy, err := outbound.NewVless(outbound.VlessOption{
Name: "vless", Name: "vless",
@ -80,9 +78,7 @@ func TestClash_VlessXTLS(t *testing.T) {
Flow: "xtls-rprx-direct", Flow: "xtls-rprx-direct",
FlowShow: true, FlowShow: true,
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -103,10 +99,10 @@ func TestClash_VlessWS(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "vless-ws") id, err := startContainer(cfg, hostCfg, "vless-ws")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error()) t.Cleanup(func() {
} cleanContainer(id)
defer cleanContainer(id) })
proxy, err := outbound.NewVless(outbound.VlessOption{ proxy, err := outbound.NewVless(outbound.VlessOption{
Name: "vless", Name: "vless",
@ -118,9 +114,7 @@ func TestClash_VlessWS(t *testing.T) {
Network: "ws", Network: "ws",
UDP: true, UDP: true,
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)

View File

@ -9,7 +9,7 @@ import (
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/require"
) )
func TestClash_Vmess(t *testing.T) { func TestClash_Vmess(t *testing.T) {
@ -25,9 +25,7 @@ func TestClash_Vmess(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "vmess") id, err := startContainer(cfg, hostCfg, "vmess")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
t.Cleanup(func() { t.Cleanup(func() {
cleanContainer(id) cleanContainer(id)
@ -41,9 +39,7 @@ func TestClash_Vmess(t *testing.T) {
Cipher: "auto", Cipher: "auto",
UDP: true, UDP: true,
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -64,10 +60,10 @@ func TestClash_VmessTLS(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "vmess-tls") id, err := startContainer(cfg, hostCfg, "vmess-tls")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error()) t.Cleanup(func() {
} cleanContainer(id)
defer cleanContainer(id) })
proxy, err := outbound.NewVmess(outbound.VmessOption{ proxy, err := outbound.NewVmess(outbound.VmessOption{
Name: "vmess", Name: "vmess",
@ -80,9 +76,7 @@ func TestClash_VmessTLS(t *testing.T) {
ServerName: "example.org", ServerName: "example.org",
UDP: true, UDP: true,
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -103,10 +97,10 @@ func TestClash_VmessHTTP2(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "vmess-http2") id, err := startContainer(cfg, hostCfg, "vmess-http2")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error()) t.Cleanup(func() {
} cleanContainer(id)
defer cleanContainer(id) })
proxy, err := outbound.NewVmess(outbound.VmessOption{ proxy, err := outbound.NewVmess(outbound.VmessOption{
Name: "vmess", Name: "vmess",
@ -124,9 +118,7 @@ func TestClash_VmessHTTP2(t *testing.T) {
Path: "/test", Path: "/test",
}, },
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -145,10 +137,10 @@ func TestClash_VmessHTTP(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "vmess-http") id, err := startContainer(cfg, hostCfg, "vmess-http")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error()) t.Cleanup(func() {
} cleanContainer(id)
defer cleanContainer(id) })
proxy, err := outbound.NewVmess(outbound.VmessOption{ proxy, err := outbound.NewVmess(outbound.VmessOption{
Name: "vmess", Name: "vmess",
@ -176,9 +168,7 @@ func TestClash_VmessHTTP(t *testing.T) {
}, },
}, },
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -197,10 +187,10 @@ func TestClash_VmessWebsocket(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "vmess-ws") id, err := startContainer(cfg, hostCfg, "vmess-ws")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error()) t.Cleanup(func() {
} cleanContainer(id)
defer cleanContainer(id) })
proxy, err := outbound.NewVmess(outbound.VmessOption{ proxy, err := outbound.NewVmess(outbound.VmessOption{
Name: "vmess", Name: "vmess",
@ -211,9 +201,7 @@ func TestClash_VmessWebsocket(t *testing.T) {
Network: "ws", Network: "ws",
UDP: true, UDP: true,
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -234,10 +222,10 @@ func TestClash_VmessWebsocketTLS(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "vmess-ws") id, err := startContainer(cfg, hostCfg, "vmess-ws")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error()) t.Cleanup(func() {
} cleanContainer(id)
defer cleanContainer(id) })
proxy, err := outbound.NewVmess(outbound.VmessOption{ proxy, err := outbound.NewVmess(outbound.VmessOption{
Name: "vmess", Name: "vmess",
@ -250,9 +238,7 @@ func TestClash_VmessWebsocketTLS(t *testing.T) {
SkipCertVerify: true, SkipCertVerify: true,
UDP: true, UDP: true,
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -273,10 +259,10 @@ func TestClash_VmessGrpc(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "vmess-grpc") id, err := startContainer(cfg, hostCfg, "vmess-grpc")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error()) t.Cleanup(func() {
} cleanContainer(id)
defer cleanContainer(id) })
proxy, err := outbound.NewVmess(outbound.VmessOption{ proxy, err := outbound.NewVmess(outbound.VmessOption{
Name: "vmess", Name: "vmess",
@ -293,9 +279,7 @@ func TestClash_VmessGrpc(t *testing.T) {
GrpcServiceName: "example!", GrpcServiceName: "example!",
}, },
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -314,10 +298,10 @@ func TestClash_VmessWebsocket0RTT(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "vmess-ws-0rtt") id, err := startContainer(cfg, hostCfg, "vmess-ws-0rtt")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error()) t.Cleanup(func() {
} cleanContainer(id)
defer cleanContainer(id) })
proxy, err := outbound.NewVmess(outbound.VmessOption{ proxy, err := outbound.NewVmess(outbound.VmessOption{
Name: "vmess", Name: "vmess",
@ -333,9 +317,7 @@ func TestClash_VmessWebsocket0RTT(t *testing.T) {
EarlyDataHeaderName: "Sec-WebSocket-Protocol", EarlyDataHeaderName: "Sec-WebSocket-Protocol",
}, },
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
@ -354,10 +336,10 @@ func TestClash_VmessWebsocketXray0RTT(t *testing.T) {
} }
id, err := startContainer(cfg, hostCfg, "vmess-xray-ws-0rtt") id, err := startContainer(cfg, hostCfg, "vmess-xray-ws-0rtt")
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error()) t.Cleanup(func() {
} cleanContainer(id)
defer cleanContainer(id) })
proxy, err := outbound.NewVmess(outbound.VmessOption{ proxy, err := outbound.NewVmess(outbound.VmessOption{
Name: "vmess", Name: "vmess",
@ -372,16 +354,14 @@ func TestClash_VmessWebsocketXray0RTT(t *testing.T) {
Path: "/?ed=2048", Path: "/?ed=2048",
}, },
}) })
if err != nil { require.NoError(t, err)
assert.FailNow(t, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
testSuit(t, proxy) testSuit(t, proxy)
} }
func Benchmark_Vmess(b *testing.B) { func Benchmark_Vmess(b *testing.B) {
configPath := C.Path.Resolve("vmess-aead.json") configPath := C.Path.Resolve("vmess.json")
cfg := &container.Config{ cfg := &container.Config{
Image: ImageVmess, Image: ImageVmess,
@ -392,10 +372,8 @@ func Benchmark_Vmess(b *testing.B) {
Binds: []string{fmt.Sprintf("%s:/etc/v2ray/config.json", configPath)}, Binds: []string{fmt.Sprintf("%s:/etc/v2ray/config.json", configPath)},
} }
id, err := startContainer(cfg, hostCfg, "vmess-aead") id, err := startContainer(cfg, hostCfg, "vmess-bench")
if err != nil { require.NoError(b, err)
assert.FailNow(b, err.Error())
}
b.Cleanup(func() { b.Cleanup(func() {
cleanContainer(id) cleanContainer(id)
@ -410,9 +388,7 @@ func Benchmark_Vmess(b *testing.B) {
AlterID: 0, AlterID: 0,
UDP: true, UDP: true,
}) })
if err != nil { require.NoError(b, err)
assert.FailNow(b, err.Error())
}
time.Sleep(waitTime) time.Sleep(waitTime)
benchmarkProxy(b, proxy) benchmarkProxy(b, proxy)

View File

@ -0,0 +1,5 @@
## Embedded go-shadowsocks2
from https://github.com/Dreamacro/go-shadowsocks2
origin https://github.com/riobard/go-shadowsocks2

View File

@ -0,0 +1,164 @@
package core
import (
"crypto/md5"
"errors"
"net"
"sort"
"strings"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowaead"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowstream"
)
type Cipher interface {
StreamConnCipher
PacketConnCipher
}
type StreamConnCipher interface {
StreamConn(net.Conn) net.Conn
}
type PacketConnCipher interface {
PacketConn(net.PacketConn) net.PacketConn
}
// ErrCipherNotSupported occurs when a cipher is not supported (likely because of security concerns).
var ErrCipherNotSupported = errors.New("cipher not supported")
const (
aeadAes128Gcm = "AEAD_AES_128_GCM"
aeadAes192Gcm = "AEAD_AES_192_GCM"
aeadAes256Gcm = "AEAD_AES_256_GCM"
aeadChacha20Poly1305 = "AEAD_CHACHA20_POLY1305"
aeadXChacha20Poly1305 = "AEAD_XCHACHA20_POLY1305"
)
// List of AEAD ciphers: key size in bytes and constructor
var aeadList = map[string]struct {
KeySize int
New func([]byte) (shadowaead.Cipher, error)
}{
aeadAes128Gcm: {16, shadowaead.AESGCM},
aeadAes192Gcm: {24, shadowaead.AESGCM},
aeadAes256Gcm: {32, shadowaead.AESGCM},
aeadChacha20Poly1305: {32, shadowaead.Chacha20Poly1305},
aeadXChacha20Poly1305: {32, shadowaead.XChacha20Poly1305},
}
// List of stream ciphers: key size in bytes and constructor
var streamList = map[string]struct {
KeySize int
New func(key []byte) (shadowstream.Cipher, error)
}{
"RC4-MD5": {16, shadowstream.RC4MD5},
"AES-128-CTR": {16, shadowstream.AESCTR},
"AES-192-CTR": {24, shadowstream.AESCTR},
"AES-256-CTR": {32, shadowstream.AESCTR},
"AES-128-CFB": {16, shadowstream.AESCFB},
"AES-192-CFB": {24, shadowstream.AESCFB},
"AES-256-CFB": {32, shadowstream.AESCFB},
"CHACHA20-IETF": {32, shadowstream.Chacha20IETF},
"XCHACHA20": {32, shadowstream.Xchacha20},
}
// ListCipher returns a list of available cipher names sorted alphabetically.
func ListCipher() []string {
var l []string
for k := range aeadList {
l = append(l, k)
}
for k := range streamList {
l = append(l, k)
}
sort.Strings(l)
return l
}
// PickCipher returns a Cipher of the given name. Derive key from password if given key is empty.
func PickCipher(name string, key []byte, password string) (Cipher, error) {
name = strings.ToUpper(name)
switch name {
case "DUMMY":
return &dummy{}, nil
case "CHACHA20-IETF-POLY1305":
name = aeadChacha20Poly1305
case "XCHACHA20-IETF-POLY1305":
name = aeadXChacha20Poly1305
case "AES-128-GCM":
name = aeadAes128Gcm
case "AES-192-GCM":
name = aeadAes192Gcm
case "AES-256-GCM":
name = aeadAes256Gcm
}
if choice, ok := aeadList[name]; ok {
if len(key) == 0 {
key = Kdf(password, choice.KeySize)
}
if len(key) != choice.KeySize {
return nil, shadowaead.KeySizeError(choice.KeySize)
}
aead, err := choice.New(key)
return &AeadCipher{Cipher: aead, Key: key}, err
}
if choice, ok := streamList[name]; ok {
if len(key) == 0 {
key = Kdf(password, choice.KeySize)
}
if len(key) != choice.KeySize {
return nil, shadowstream.KeySizeError(choice.KeySize)
}
ciph, err := choice.New(key)
return &StreamCipher{Cipher: ciph, Key: key}, err
}
return nil, ErrCipherNotSupported
}
type AeadCipher struct {
shadowaead.Cipher
Key []byte
}
func (aead *AeadCipher) StreamConn(c net.Conn) net.Conn { return shadowaead.NewConn(c, aead) }
func (aead *AeadCipher) PacketConn(c net.PacketConn) net.PacketConn {
return shadowaead.NewPacketConn(c, aead)
}
type StreamCipher struct {
shadowstream.Cipher
Key []byte
}
func (ciph *StreamCipher) StreamConn(c net.Conn) net.Conn { return shadowstream.NewConn(c, ciph) }
func (ciph *StreamCipher) PacketConn(c net.PacketConn) net.PacketConn {
return shadowstream.NewPacketConn(c, ciph)
}
// dummy cipher does not encrypt
type dummy struct{}
func (dummy) StreamConn(c net.Conn) net.Conn { return c }
func (dummy) PacketConn(c net.PacketConn) net.PacketConn { return c }
// key-derivation function from original Shadowsocks
func Kdf(password string, keyLen int) []byte {
var b, prev []byte
h := md5.New()
for len(b) < keyLen {
h.Write(prev)
h.Write([]byte(password))
b = h.Sum(b)
prev = b[len(b)-h.Size():]
h.Reset()
}
return b[:keyLen]
}

View File

@ -0,0 +1,94 @@
package shadowaead
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha1"
"io"
"strconv"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/hkdf"
)
type Cipher interface {
KeySize() int
SaltSize() int
Encrypter(salt []byte) (cipher.AEAD, error)
Decrypter(salt []byte) (cipher.AEAD, error)
}
type KeySizeError int
func (e KeySizeError) Error() string {
return "key size error: need " + strconv.Itoa(int(e)) + " bytes"
}
func hkdfSHA1(secret, salt, info, outkey []byte) {
r := hkdf.New(sha1.New, secret, salt, info)
if _, err := io.ReadFull(r, outkey); err != nil {
panic(err) // should never happen
}
}
type metaCipher struct {
psk []byte
makeAEAD func(key []byte) (cipher.AEAD, error)
}
func (a *metaCipher) KeySize() int { return len(a.psk) }
func (a *metaCipher) SaltSize() int {
if ks := a.KeySize(); ks > 16 {
return ks
}
return 16
}
func (a *metaCipher) Encrypter(salt []byte) (cipher.AEAD, error) {
subkey := make([]byte, a.KeySize())
hkdfSHA1(a.psk, salt, []byte("ss-subkey"), subkey)
return a.makeAEAD(subkey)
}
func (a *metaCipher) Decrypter(salt []byte) (cipher.AEAD, error) {
subkey := make([]byte, a.KeySize())
hkdfSHA1(a.psk, salt, []byte("ss-subkey"), subkey)
return a.makeAEAD(subkey)
}
func aesGCM(key []byte) (cipher.AEAD, error) {
blk, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return cipher.NewGCM(blk)
}
// AESGCM creates a new Cipher with a pre-shared key. len(psk) must be
// one of 16, 24, or 32 to select AES-128/196/256-GCM.
func AESGCM(psk []byte) (Cipher, error) {
switch l := len(psk); l {
case 16, 24, 32: // AES 128/196/256
default:
return nil, aes.KeySizeError(l)
}
return &metaCipher{psk: psk, makeAEAD: aesGCM}, nil
}
// Chacha20Poly1305 creates a new Cipher with a pre-shared key. len(psk)
// must be 32.
func Chacha20Poly1305(psk []byte) (Cipher, error) {
if len(psk) != chacha20poly1305.KeySize {
return nil, KeySizeError(chacha20poly1305.KeySize)
}
return &metaCipher{psk: psk, makeAEAD: chacha20poly1305.New}, nil
}
// XChacha20Poly1305 creates a new Cipher with a pre-shared key. len(psk)
// must be 32.
func XChacha20Poly1305(psk []byte) (Cipher, error) {
if len(psk) != chacha20poly1305.KeySize {
return nil, KeySizeError(chacha20poly1305.KeySize)
}
return &metaCipher{psk: psk, makeAEAD: chacha20poly1305.NewX}, nil
}

View File

@ -0,0 +1,95 @@
package shadowaead
import (
"crypto/rand"
"errors"
"io"
"net"
"github.com/Dreamacro/clash/common/pool"
)
// ErrShortPacket means that the packet is too short for a valid encrypted packet.
var ErrShortPacket = errors.New("short packet")
var _zerononce [128]byte // read-only. 128 bytes is more than enough.
// Pack encrypts plaintext using Cipher with a randomly generated salt and
// returns a slice of dst containing the encrypted packet and any error occurred.
// Ensure len(dst) >= ciph.SaltSize() + len(plaintext) + aead.Overhead().
func Pack(dst, plaintext []byte, ciph Cipher) ([]byte, error) {
saltSize := ciph.SaltSize()
salt := dst[:saltSize]
if _, err := rand.Read(salt); err != nil {
return nil, err
}
aead, err := ciph.Encrypter(salt)
if err != nil {
return nil, err
}
if len(dst) < saltSize+len(plaintext)+aead.Overhead() {
return nil, io.ErrShortBuffer
}
b := aead.Seal(dst[saltSize:saltSize], _zerononce[:aead.NonceSize()], plaintext, nil)
return dst[:saltSize+len(b)], nil
}
// Unpack decrypts pkt using Cipher and returns a slice of dst containing the decrypted payload and any error occurred.
// Ensure len(dst) >= len(pkt) - aead.SaltSize() - aead.Overhead().
func Unpack(dst, pkt []byte, ciph Cipher) ([]byte, error) {
saltSize := ciph.SaltSize()
if len(pkt) < saltSize {
return nil, ErrShortPacket
}
salt := pkt[:saltSize]
aead, err := ciph.Decrypter(salt)
if err != nil {
return nil, err
}
if len(pkt) < saltSize+aead.Overhead() {
return nil, ErrShortPacket
}
if saltSize+len(dst)+aead.Overhead() < len(pkt) {
return nil, io.ErrShortBuffer
}
b, err := aead.Open(dst[:0], _zerononce[:aead.NonceSize()], pkt[saltSize:], nil)
return b, err
}
type PacketConn struct {
net.PacketConn
Cipher
}
const maxPacketSize = 64 * 1024
// NewPacketConn wraps a net.PacketConn with cipher
func NewPacketConn(c net.PacketConn, ciph Cipher) *PacketConn {
return &PacketConn{PacketConn: c, Cipher: ciph}
}
// WriteTo encrypts b and write to addr using the embedded PacketConn.
func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
buf := pool.Get(maxPacketSize)
defer pool.Put(buf)
buf, err := Pack(buf, b, c)
if err != nil {
return 0, err
}
_, err = c.PacketConn.WriteTo(buf, addr)
return len(b), err
}
// ReadFrom reads from the embedded PacketConn and decrypts into b.
func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, addr, err := c.PacketConn.ReadFrom(b)
if err != nil {
return n, addr, err
}
bb, err := Unpack(b[c.Cipher.SaltSize():], b[:n], c)
if err != nil {
return n, addr, err
}
copy(b, bb)
return len(bb), addr, err
}

View File

@ -0,0 +1,285 @@
package shadowaead
import (
"crypto/cipher"
"crypto/rand"
"errors"
"io"
"net"
"github.com/Dreamacro/clash/common/pool"
)
const (
// payloadSizeMask is the maximum size of payload in bytes.
payloadSizeMask = 0x3FFF // 16*1024 - 1
bufSize = 17 * 1024 // >= 2+aead.Overhead()+payloadSizeMask+aead.Overhead()
)
var ErrZeroChunk = errors.New("zero chunk")
type Writer struct {
io.Writer
cipher.AEAD
nonce [32]byte // should be sufficient for most nonce sizes
}
// NewWriter wraps an io.Writer with authenticated encryption.
func NewWriter(w io.Writer, aead cipher.AEAD) *Writer { return &Writer{Writer: w, AEAD: aead} }
// Write encrypts p and writes to the embedded io.Writer.
func (w *Writer) Write(p []byte) (n int, err error) {
buf := pool.Get(bufSize)
defer pool.Put(buf)
nonce := w.nonce[:w.NonceSize()]
tag := w.Overhead()
off := 2 + tag
// compatible with snell
if len(p) == 0 {
buf = buf[:off]
buf[0], buf[1] = byte(0), byte(0)
w.Seal(buf[:0], nonce, buf[:2], nil)
increment(nonce)
_, err = w.Writer.Write(buf)
return
}
for nr := 0; n < len(p) && err == nil; n += nr {
nr = payloadSizeMask
if n+nr > len(p) {
nr = len(p) - n
}
buf = buf[:off+nr+tag]
buf[0], buf[1] = byte(nr>>8), byte(nr) // big-endian payload size
w.Seal(buf[:0], nonce, buf[:2], nil)
increment(nonce)
w.Seal(buf[:off], nonce, p[n:n+nr], nil)
increment(nonce)
_, err = w.Writer.Write(buf)
}
return
}
// ReadFrom reads from the given io.Reader until EOF or error, encrypts and
// writes to the embedded io.Writer. Returns number of bytes read from r and
// any error encountered.
func (w *Writer) ReadFrom(r io.Reader) (n int64, err error) {
buf := pool.Get(bufSize)
defer pool.Put(buf)
nonce := w.nonce[:w.NonceSize()]
tag := w.Overhead()
off := 2 + tag
for {
nr, er := r.Read(buf[off : off+payloadSizeMask])
n += int64(nr)
buf[0], buf[1] = byte(nr>>8), byte(nr)
w.Seal(buf[:0], nonce, buf[:2], nil)
increment(nonce)
w.Seal(buf[:off], nonce, buf[off:off+nr], nil)
increment(nonce)
if _, ew := w.Writer.Write(buf[:off+nr+tag]); ew != nil {
err = ew
return
}
if er != nil {
if er != io.EOF { // ignore EOF as per io.ReaderFrom contract
err = er
}
return
}
}
}
type Reader struct {
io.Reader
cipher.AEAD
nonce [32]byte // should be sufficient for most nonce sizes
buf []byte // to be put back into bufPool
off int // offset to unconsumed part of buf
}
// NewReader wraps an io.Reader with authenticated decryption.
func NewReader(r io.Reader, aead cipher.AEAD) *Reader { return &Reader{Reader: r, AEAD: aead} }
// Read and decrypt a record into p. len(p) >= max payload size + AEAD overhead.
func (r *Reader) read(p []byte) (int, error) {
nonce := r.nonce[:r.NonceSize()]
tag := r.Overhead()
// decrypt payload size
p = p[:2+tag]
if _, err := io.ReadFull(r.Reader, p); err != nil {
return 0, err
}
_, err := r.Open(p[:0], nonce, p, nil)
increment(nonce)
if err != nil {
return 0, err
}
// decrypt payload
size := (int(p[0])<<8 + int(p[1])) & payloadSizeMask
if size == 0 {
return 0, ErrZeroChunk
}
p = p[:size+tag]
if _, err := io.ReadFull(r.Reader, p); err != nil {
return 0, err
}
_, err = r.Open(p[:0], nonce, p, nil)
increment(nonce)
if err != nil {
return 0, err
}
return size, nil
}
// Read reads from the embedded io.Reader, decrypts and writes to p.
func (r *Reader) Read(p []byte) (int, error) {
if r.buf == nil {
if len(p) >= payloadSizeMask+r.Overhead() {
return r.read(p)
}
b := pool.Get(bufSize)
n, err := r.read(b)
if err != nil {
return 0, err
}
r.buf = b[:n]
r.off = 0
}
n := copy(p, r.buf[r.off:])
r.off += n
if r.off == len(r.buf) {
pool.Put(r.buf[:cap(r.buf)])
r.buf = nil
}
return n, nil
}
// WriteTo reads from the embedded io.Reader, decrypts and writes to w until
// there's no more data to write or when an error occurs. Return number of
// bytes written to w and any error encountered.
func (r *Reader) WriteTo(w io.Writer) (n int64, err error) {
if r.buf == nil {
r.buf = pool.Get(bufSize)
r.off = len(r.buf)
}
for {
for r.off < len(r.buf) {
nw, ew := w.Write(r.buf[r.off:])
r.off += nw
n += int64(nw)
if ew != nil {
if r.off == len(r.buf) {
pool.Put(r.buf[:cap(r.buf)])
r.buf = nil
}
err = ew
return
}
}
nr, er := r.read(r.buf)
if er != nil {
if er != io.EOF {
err = er
}
return
}
r.buf = r.buf[:nr]
r.off = 0
}
}
// increment little-endian encoded unsigned integer b. Wrap around on overflow.
func increment(b []byte) {
for i := range b {
b[i]++
if b[i] != 0 {
return
}
}
}
type Conn struct {
net.Conn
Cipher
r *Reader
w *Writer
}
// NewConn wraps a stream-oriented net.Conn with cipher.
func NewConn(c net.Conn, ciph Cipher) *Conn { return &Conn{Conn: c, Cipher: ciph} }
func (c *Conn) initReader() error {
salt := make([]byte, c.SaltSize())
if _, err := io.ReadFull(c.Conn, salt); err != nil {
return err
}
aead, err := c.Decrypter(salt)
if err != nil {
return err
}
c.r = NewReader(c.Conn, aead)
return nil
}
func (c *Conn) Read(b []byte) (int, error) {
if c.r == nil {
if err := c.initReader(); err != nil {
return 0, err
}
}
return c.r.Read(b)
}
func (c *Conn) WriteTo(w io.Writer) (int64, error) {
if c.r == nil {
if err := c.initReader(); err != nil {
return 0, err
}
}
return c.r.WriteTo(w)
}
func (c *Conn) initWriter() error {
salt := make([]byte, c.SaltSize())
if _, err := rand.Read(salt); err != nil {
return err
}
aead, err := c.Encrypter(salt)
if err != nil {
return err
}
_, err = c.Conn.Write(salt)
if err != nil {
return err
}
c.w = NewWriter(c.Conn, aead)
return nil
}
func (c *Conn) Write(b []byte) (int, error) {
if c.w == nil {
if err := c.initWriter(); err != nil {
return 0, err
}
}
return c.w.Write(b)
}
func (c *Conn) ReadFrom(r io.Reader) (int64, error) {
if c.w == nil {
if err := c.initWriter(); err != nil {
return 0, err
}
}
return c.w.ReadFrom(r)
}

View File

@ -0,0 +1,116 @@
package shadowstream
import (
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rc4"
"strconv"
"golang.org/x/crypto/chacha20"
)
// Cipher generates a pair of stream ciphers for encryption and decryption.
type Cipher interface {
IVSize() int
Encrypter(iv []byte) cipher.Stream
Decrypter(iv []byte) cipher.Stream
}
type KeySizeError int
func (e KeySizeError) Error() string {
return "key size error: need " + strconv.Itoa(int(e)) + " bytes"
}
// CTR mode
type ctrStream struct{ cipher.Block }
func (b *ctrStream) IVSize() int { return b.BlockSize() }
func (b *ctrStream) Decrypter(iv []byte) cipher.Stream { return b.Encrypter(iv) }
func (b *ctrStream) Encrypter(iv []byte) cipher.Stream { return cipher.NewCTR(b, iv) }
func AESCTR(key []byte) (Cipher, error) {
blk, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return &ctrStream{blk}, nil
}
// CFB mode
type cfbStream struct{ cipher.Block }
func (b *cfbStream) IVSize() int { return b.BlockSize() }
func (b *cfbStream) Decrypter(iv []byte) cipher.Stream { return cipher.NewCFBDecrypter(b, iv) }
func (b *cfbStream) Encrypter(iv []byte) cipher.Stream { return cipher.NewCFBEncrypter(b, iv) }
func AESCFB(key []byte) (Cipher, error) {
blk, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return &cfbStream{blk}, nil
}
// IETF-variant of chacha20
type chacha20ietfkey []byte
func (k chacha20ietfkey) IVSize() int { return chacha20.NonceSize }
func (k chacha20ietfkey) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) }
func (k chacha20ietfkey) Encrypter(iv []byte) cipher.Stream {
ciph, err := chacha20.NewUnauthenticatedCipher(k, iv)
if err != nil {
panic(err) // should never happen
}
return ciph
}
func Chacha20IETF(key []byte) (Cipher, error) {
if len(key) != chacha20.KeySize {
return nil, KeySizeError(chacha20.KeySize)
}
return chacha20ietfkey(key), nil
}
type xchacha20key []byte
func (k xchacha20key) IVSize() int { return chacha20.NonceSizeX }
func (k xchacha20key) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) }
func (k xchacha20key) Encrypter(iv []byte) cipher.Stream {
ciph, err := chacha20.NewUnauthenticatedCipher(k, iv)
if err != nil {
panic(err) // should never happen
}
return ciph
}
func Xchacha20(key []byte) (Cipher, error) {
if len(key) != chacha20.KeySize {
return nil, KeySizeError(chacha20.KeySize)
}
return xchacha20key(key), nil
}
type rc4Md5Key []byte
func (k rc4Md5Key) IVSize() int {
return 16
}
func (k rc4Md5Key) Encrypter(iv []byte) cipher.Stream {
h := md5.New()
h.Write([]byte(k))
h.Write(iv)
rc4key := h.Sum(nil)
c, _ := rc4.NewCipher(rc4key)
return c
}
func (k rc4Md5Key) Decrypter(iv []byte) cipher.Stream {
return k.Encrypter(iv)
}
func RC4MD5(key []byte) (Cipher, error) {
return rc4Md5Key(key), nil
}

View File

@ -0,0 +1,79 @@
package shadowstream
import (
"crypto/rand"
"errors"
"io"
"net"
"github.com/Dreamacro/clash/common/pool"
)
// ErrShortPacket means the packet is too short to be a valid encrypted packet.
var ErrShortPacket = errors.New("short packet")
// Pack encrypts plaintext using stream cipher s and a random IV.
// Returns a slice of dst containing random IV and ciphertext.
// Ensure len(dst) >= s.IVSize() + len(plaintext).
func Pack(dst, plaintext []byte, s Cipher) ([]byte, error) {
if len(dst) < s.IVSize()+len(plaintext) {
return nil, io.ErrShortBuffer
}
iv := dst[:s.IVSize()]
_, err := rand.Read(iv)
if err != nil {
return nil, err
}
s.Encrypter(iv).XORKeyStream(dst[len(iv):], plaintext)
return dst[:len(iv)+len(plaintext)], nil
}
// Unpack decrypts pkt using stream cipher s.
// Returns a slice of dst containing decrypted plaintext.
func Unpack(dst, pkt []byte, s Cipher) ([]byte, error) {
if len(pkt) < s.IVSize() {
return nil, ErrShortPacket
}
if len(dst) < len(pkt)-s.IVSize() {
return nil, io.ErrShortBuffer
}
iv := pkt[:s.IVSize()]
s.Decrypter(iv).XORKeyStream(dst, pkt[len(iv):])
return dst[:len(pkt)-len(iv)], nil
}
type PacketConn struct {
net.PacketConn
Cipher
}
// NewPacketConn wraps a net.PacketConn with stream cipher encryption/decryption.
func NewPacketConn(c net.PacketConn, ciph Cipher) *PacketConn {
return &PacketConn{PacketConn: c, Cipher: ciph}
}
const maxPacketSize = 64 * 1024
func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
buf := pool.Get(maxPacketSize)
defer pool.Put(buf)
buf, err := Pack(buf, b, c.Cipher)
if err != nil {
return 0, err
}
_, err = c.PacketConn.WriteTo(buf, addr)
return len(b), err
}
func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, addr, err := c.PacketConn.ReadFrom(b)
if err != nil {
return n, addr, err
}
bb, err := Unpack(b[c.IVSize():], b[:n], c.Cipher)
if err != nil {
return n, addr, err
}
copy(b, bb)
return len(bb), addr, err
}

View File

@ -0,0 +1,197 @@
package shadowstream
import (
"crypto/cipher"
"crypto/rand"
"io"
"net"
)
const bufSize = 2048
type Writer struct {
io.Writer
cipher.Stream
buf [bufSize]byte
}
// NewWriter wraps an io.Writer with stream cipher encryption.
func NewWriter(w io.Writer, s cipher.Stream) *Writer { return &Writer{Writer: w, Stream: s} }
func (w *Writer) Write(p []byte) (n int, err error) {
buf := w.buf[:]
for nw := 0; n < len(p) && err == nil; n += nw {
end := n + len(buf)
if end > len(p) {
end = len(p)
}
w.XORKeyStream(buf, p[n:end])
nw, err = w.Writer.Write(buf[:end-n])
}
return
}
func (w *Writer) ReadFrom(r io.Reader) (n int64, err error) {
buf := w.buf[:]
for {
nr, er := r.Read(buf)
n += int64(nr)
b := buf[:nr]
w.XORKeyStream(b, b)
if _, err = w.Writer.Write(b); err != nil {
return
}
if er != nil {
if er != io.EOF { // ignore EOF as per io.ReaderFrom contract
err = er
}
return
}
}
}
type Reader struct {
io.Reader
cipher.Stream
buf [bufSize]byte
}
// NewReader wraps an io.Reader with stream cipher decryption.
func NewReader(r io.Reader, s cipher.Stream) *Reader { return &Reader{Reader: r, Stream: s} }
func (r *Reader) Read(p []byte) (n int, err error) {
n, err = r.Reader.Read(p)
if err != nil {
return 0, err
}
r.XORKeyStream(p, p[:n])
return
}
func (r *Reader) WriteTo(w io.Writer) (n int64, err error) {
buf := r.buf[:]
for {
nr, er := r.Reader.Read(buf)
if nr > 0 {
r.XORKeyStream(buf, buf[:nr])
nw, ew := w.Write(buf[:nr])
n += int64(nw)
if ew != nil {
err = ew
return
}
}
if er != nil {
if er != io.EOF { // ignore EOF as per io.Copy contract (using src.WriteTo shortcut)
err = er
}
return
}
}
}
// A Conn represents a Shadowsocks connection. It implements the net.Conn interface.
type Conn struct {
net.Conn
Cipher
r *Reader
w *Writer
readIV []byte
writeIV []byte
}
// NewConn wraps a stream-oriented net.Conn with stream cipher encryption/decryption.
func NewConn(c net.Conn, ciph Cipher) *Conn { return &Conn{Conn: c, Cipher: ciph} }
func (c *Conn) initReader() error {
if c.r == nil {
iv, err := c.ObtainReadIV()
if err != nil {
return err
}
c.r = NewReader(c.Conn, c.Decrypter(iv))
}
return nil
}
func (c *Conn) Read(b []byte) (int, error) {
if c.r == nil {
if err := c.initReader(); err != nil {
return 0, err
}
}
return c.r.Read(b)
}
func (c *Conn) WriteTo(w io.Writer) (int64, error) {
if c.r == nil {
if err := c.initReader(); err != nil {
return 0, err
}
}
return c.r.WriteTo(w)
}
func (c *Conn) initWriter() error {
if c.w == nil {
iv, err := c.ObtainWriteIV()
if err != nil {
return err
}
if _, err := c.Conn.Write(iv); err != nil {
return err
}
c.w = NewWriter(c.Conn, c.Encrypter(iv))
}
return nil
}
func (c *Conn) Write(b []byte) (int, error) {
if c.w == nil {
if err := c.initWriter(); err != nil {
return 0, err
}
}
return c.w.Write(b)
}
func (c *Conn) ReadFrom(r io.Reader) (int64, error) {
if c.w == nil {
if err := c.initWriter(); err != nil {
return 0, err
}
}
return c.w.ReadFrom(r)
}
func (c *Conn) ObtainWriteIV() ([]byte, error) {
if len(c.writeIV) == c.IVSize() {
return c.writeIV, nil
}
iv := make([]byte, c.IVSize())
if _, err := rand.Read(iv); err != nil {
return nil, err
}
c.writeIV = iv
return iv, nil
}
func (c *Conn) ObtainReadIV() ([]byte, error) {
if len(c.readIV) == c.IVSize() {
return c.readIV, nil
}
iv := make([]byte, c.IVSize())
if _, err := io.ReadFull(c.Conn, iv); err != nil {
return nil, err
}
c.readIV = iv
return iv, nil
}

View File

@ -4,7 +4,8 @@ import (
"crypto/aes" "crypto/aes"
"crypto/cipher" "crypto/cipher"
"github.com/Dreamacro/go-shadowsocks2/shadowaead" "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead"
"golang.org/x/crypto/argon2" "golang.org/x/crypto/argon2"
"golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/chacha20poly1305"
) )

View File

@ -6,8 +6,7 @@ import (
"time" "time"
"github.com/Dreamacro/clash/component/pool" "github.com/Dreamacro/clash/component/pool"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowaead"
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
) )
type Pool struct { type Pool struct {

View File

@ -9,9 +9,8 @@ import (
"sync" "sync"
"github.com/Dreamacro/clash/common/pool" "github.com/Dreamacro/clash/common/pool"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowaead"
"github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/socks5"
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
) )
const ( const (

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