Compare commits

...

169 Commits

Author SHA1 Message Date
dff1e8f1ce Chore: update dependencies 2021-07-03 21:01:41 +08:00
995aa7a8fc Fix: remove ClientSessionCache and add NextProtos for vmess to fix #1468 2021-07-03 20:34:44 +08:00
3ca5d17c40 Fix: enable DNS server message compression (#1451) 2021-06-24 13:38:44 +08:00
244cb370a4 Change: config reload API use default path when both path and payload don't exist (#1447) 2021-06-21 17:33:34 +08:00
c35cb24bda Chore: use unix.ByteSliceToString transform cstring 2021-06-15 21:03:47 +08:00
b6ff08074c Refactor: plain http proxy (#1443) 2021-06-15 17:13:40 +08:00
70d53fd45a Chore: update development wiki to README.md 2021-06-13 23:11:49 +08:00
f231a63e93 Chore: Listener should not expose original net.Listener 2021-06-13 23:05:22 +08:00
6091fcdfec Style: code style 2021-06-13 17:23:10 +08:00
bcfc15e398 chore: expose udp field to proxies API (#1441) 2021-06-10 15:08:33 +08:00
045edc188c Style: code style 2021-06-10 14:05:56 +08:00
0778591524 Feature: dns resolve domain through nameserver-policy (#1406) 2021-05-19 11:17:35 +08:00
d5e52bed43 Feature: add protocol test 2021-05-17 20:33:00 +08:00
06fdd3abe0 Fix: vmess http should use Host header on request 2021-05-16 20:05:41 +08:00
4e5898197a Fix: build broken 2021-05-13 22:39:33 +08:00
f96ebab99f Chore: split component to transport 2021-05-13 22:19:34 +08:00
3c54f99fea Chore: update dependencies 2021-05-08 19:29:12 +08:00
824f5bd731 Fix: reuse http connection broken on previous commit 2021-05-07 11:08:46 +08:00
3f3db8476e Fix: HTTP inbound leak 2021-05-06 22:34:37 +08:00
f375f080da Fix: skip deleted node from url-test group (#1378)
Co-authored-by: fish <fish@youme.im>
2021-05-01 17:21:09 +08:00
e19e9ef5a4 Style: code style 2021-04-29 11:23:14 +08:00
682e65cb54 Style: code style 2021-04-26 20:42:17 +08:00
16a6d409d9 Feature: add freebsd arm64 to Makefile (#1370) 2021-04-22 16:38:13 +08:00
4186bcf1b2 Fix: should write file if provider initialize from HTTP (#1365) 2021-04-19 17:40:38 +08:00
df5112175f Fix: io timeout when snell v2 reuse connection (#1362) 2021-04-19 14:36:06 +08:00
d9341a49ea Fix: trojan should safe close connection 2021-04-19 12:20:37 +08:00
4e9e4b6cde Fix: grpc transport concurrent write 2021-04-14 21:46:05 +08:00
936b7012ba Feature: PROCESS-NAME support freebsd 13, fix panic on unsupported platforms (#1351) 2021-04-14 17:57:17 +08:00
a9cbd9ec98 Fix: use bufio.Reader on grpc to avoid panic 2021-04-14 00:16:59 +08:00
c9943fb857 Fix: grpc implementation SetDeadline for udp issue 2021-04-13 23:34:33 +08:00
a40274e2a2 Fix: vmess aead writer concurrent write (#1350) 2021-04-13 23:32:53 +08:00
b59d45c660 Feature: add CodeQL security checks (#1349) 2021-04-13 21:25:55 +08:00
7b01e103c2 Chore: use correctly vmess http2 default host 2021-04-10 12:10:10 +08:00
93a8acecce Fix: vmess h2 use server as host if host option is empty 2021-04-09 18:15:46 +08:00
586bb91c0c Fix: grpc transport panic 2021-04-09 18:11:07 +08:00
baf03b81e3 Fix: remove unused function 2021-04-08 22:27:41 +08:00
9807e1189c Chore: update dependencies 2021-04-08 22:15:30 +08:00
3d5a0d9f73 Fix: trojan/vmess grpc broken 2021-04-07 22:57:46 +08:00
cc96187f58 Fix: trojan grpc udp broken 2021-04-05 23:26:13 +08:00
3aefa1d924 Chore: some chores 2021-04-05 13:31:10 +08:00
42e21b3733 Chore: refine go import 2021-04-05 13:00:49 +08:00
0a35237915 Fix: should reset fast node when tolerance enable and not alive on url-test group 2021-04-04 17:40:25 +08:00
a1f3a5ea26 Chore: -v add golang version 2021-04-04 17:36:22 +08:00
e63f995258 Chore: update dependencies (#1331) 2021-04-03 14:59:03 +08:00
d0c829c578 Fix: domain dns should follow hosts config, close #1318 2021-04-01 21:20:44 +08:00
4ad9761b32 Fix: don't resolve AAAA record when ipv6 is false and use go dns resolver 2021-04-01 18:03:30 +08:00
1f593d37fb Chore: use mixed-port instead of port when initial config (#1319) 2021-04-01 15:35:33 +08:00
109bfcb0f9 Feature: add vmess aead header support 2021-03-30 17:34:16 +08:00
7ee49f5171 Fix: HTTP server should close when Connection is close 2021-03-30 16:33:49 +08:00
d759d16944 Style: cleanup code 2021-03-24 01:00:21 +08:00
807d53c1e7 Chore: Clarify the definition of StreamConn and DialContext 2021-03-22 23:26:20 +08:00
1355196b7c Fix: grpc connection panic 2021-03-18 23:19:00 +08:00
573316bcde Feature: add gRPC Transport for vmess/trojan (#1287)
Co-authored-by: eMeab <32988354+eMeab@users.noreply.github.com>
Co-authored-by: Dreamacro <8615343+Dreamacro@users.noreply.github.com>
2021-03-18 19:40:34 +08:00
784c28266c Fix: vmess http broken 2021-03-18 17:11:10 +08:00
5da1b2a8aa Fix: set metadata.AddrType if host is ip string after remove host (#1291) 2021-03-12 17:41:37 +08:00
0976d27cb1 Fix: github actions remove prerelease option 2021-03-10 21:22:22 +08:00
6c83ff3496 Chore: update dependencies 2021-03-10 21:13:23 +08:00
f7f97ef625 Fix: some HTTP proxy request broken 2021-03-10 16:23:55 +08:00
5acdd72a1d Fix: remove host if host is ip string 2021-03-10 12:49:30 +08:00
f53686103d Chore: reset udp timeout after sending each packet (#1260) 2021-02-26 10:40:55 +08:00
f63c9eb22f Chore: update staticcheck command on actions 2021-02-21 19:37:37 +08:00
a37243cf30 Fix: store cache correctly 2021-02-21 01:07:22 +08:00
b3c1b4a840 Chore: update dependencies 2021-02-19 20:35:10 +08:00
14bbf6eedc Feature: support store group selected node to cache (enable by default) 2021-02-18 23:41:50 +08:00
aa81193d5b Feature: add darwin arm64 to Makefile (Apple Silicon) (#1234) 2021-02-18 18:15:09 +08:00
9eb98e399d Improve: refactor ssr and fix #995 (#1189)
Co-authored-by: goomada <madao@DESKTOP-IOEBS0C.localdomain>
2021-02-15 14:32:03 +08:00
d48cfecf60 Chore: API support patch ipv6 config (#1217) 2021-02-05 16:43:42 +08:00
6036fb63ba Chore: avoid provider unnecessary write file operations (#1210) 2021-02-02 17:52:46 +08:00
cd48f69b1f Fix: wrap net.Conn to avoid using *net.TCPConn.(ReadFrom) (#1209) 2021-02-01 20:06:45 +08:00
fcc594ae26 Chore: use jsdelivr CDN for Country.mmdb (#1057) 2021-01-30 00:40:35 +08:00
f4de055aa1 Refactor: make inbound request contextual 2021-01-23 14:58:09 +08:00
35925cb3da Chore: standardized Dockerfile label (#1191)
Signed-off-by: Junjie Yuan <yuan@junjie.pro>
2021-01-20 16:08:24 +08:00
ff430df845 Fix: connectivity of ssr auth_chain_(ab) protocol (#1180) 2021-01-13 23:35:41 +08:00
e4cdea2111 chore: use singleDo to get interface info 2021-01-13 17:30:54 +08:00
b6ee47a541 Fix: get general should return correct result (#1172) 2021-01-07 13:59:39 +08:00
b25009cde7 Fix: unnecessary write operation on provider (#1170) 2021-01-06 14:20:15 +08:00
6fedd7ec84 Fix: dns client should not bind local address 2021-01-04 00:51:53 +08:00
9619c3fb20 Fix: support unspecified UDP bind address (#1159) 2020-12-31 18:58:03 +08:00
02d029dd2d Fix: close http Response body on provider (#1154) 2020-12-29 11:28:22 +08:00
09c28e0355 Fix: fallback bind fn should not bind global unicast 2020-12-28 22:24:58 +08:00
3600077f3b Chore: update dependencies 2020-12-27 18:59:59 +08:00
de7656a787 Chore: update premium README 2020-12-27 00:14:24 +08:00
5dfe7f8561 Fix: handle keep alive on http connect proxy 2020-12-24 14:55:11 +08:00
ed27898a33 Fix: snell should support the config without obfs 2020-12-24 13:47:56 +08:00
532396d25c Fix: PROCESS-NAME rule for UDP sessions on Windows (#1140) 2020-12-22 15:13:44 +08:00
4b1b494164 Chore: move find process name to a single part 2020-12-17 22:17:27 +08:00
0d33dc3eb9 Chore: health checks return immediately if completed (#1097) 2020-11-24 22:52:23 +08:00
994cbff215 Fix: should not log rule when rule = nil 2020-11-22 23:38:12 +08:00
bea2ee8bf2 Chore: log rule msg on dial error 2020-11-22 19:12:36 +08:00
1e5593f1a9 Chore: update dependencies 2020-11-20 20:36:20 +08:00
34febc4579 Chore: more detailed error when dial failed 2020-11-20 00:27:37 +08:00
97581148b5 Fix: static check 2020-11-19 00:56:36 +08:00
0402878daa Feature: add lazy for proxy group and provider 2020-11-19 00:53:22 +08:00
4735f61fd1 Feature: add disable-udp option for all proxy group 2020-11-13 21:48:52 +08:00
16ae107e70 Chore: push image to github docker registry 2020-11-10 15:19:12 +08:00
83efe2ae57 Feature: add TCP TPROXY support (#1049) 2020-11-09 10:46:10 +08:00
87e4d94290 Fix: tunnel manager & tracker race condition (#1048) 2020-10-29 17:51:14 +08:00
b98e9ea202 Improve: #1038 and #1041 2020-10-29 00:32:31 +08:00
9a62b1081d Feature: support round-robin strategy for load-balance group (#1044) 2020-10-28 22:35:02 +08:00
2cd1b890ce Fix: tunnel UDP race condition (#1043) 2020-10-28 21:26:50 +08:00
ba060bd0ee Fix: should not bind interface on local address 2020-10-25 20:31:01 +08:00
b1795b1e3d Fix: stale typo 2020-10-25 11:53:03 +08:00
76c9820065 Fix: undefined variable 2020-10-23 17:49:34 +08:00
2db4ce57ef Chore: make stale time into 60 days 2020-10-23 00:30:17 +08:00
50b3d497f6 Feature: use native syscall to bind interface on Linux and macOS 2020-10-22 22:32:03 +08:00
2321e9139d Chore: deprecated eapache/channels 2020-10-20 17:44:39 +08:00
baabf21340 Chore: update github workflow 2020-10-17 13:46:05 +08:00
d3bb4c65a8 Fix: missing fake-ip record should return error 2020-10-17 12:52:43 +08:00
8c3e2a7559 Chore: fix typo (#1017) 2020-10-14 19:56:02 +08:00
bc52f8e4fd Chore: return empty record in SVCB/HTTPSSVC on fake-ip mode 2020-10-13 00:15:49 +08:00
d3b14c325f Fix: the priority of fake-ip-filter 2020-10-09 00:04:24 +08:00
4859b158b4 Chore: make builds reproducible (#1006) 2020-10-08 17:54:38 +08:00
d65b51c62b Feature: http support custom sni 2020-10-02 11:34:40 +08:00
a6444bb449 Feature: support domain in fallback filter (#964) 2020-09-28 22:17:10 +08:00
e09931dcf7 Chore: remove broken test temporarily 2020-09-26 20:36:52 +08:00
5bd189f2d0 Feature: support VMess HTTP/2 transport (#903) 2020-09-26 20:33:57 +08:00
8766287e72 Chore: sync necessary changes from premium 2020-09-21 22:22:07 +08:00
10f9571c9e Fix: pool gc test 2020-09-21 00:44:47 +08:00
96a8259c42 Feature: support snell v2 (#952)
Co-authored-by: Dreamacro <8615343+Dreamacro@users.noreply.github.com>
2020-09-21 00:33:13 +08:00
68dd0622b8 Chore: code style 2020-09-20 15:53:27 +08:00
558ac6b965 Chore: split enhanced mode instance (#936)
Co-authored-by: Dreamacro <305009791@qq.com>
2020-09-17 10:48:42 +08:00
e773f95f21 Fix: PROCESS-NAME on FreeBSD 11.x (#947) 2020-09-07 17:43:34 +08:00
314ce1c249 Feature: vmess network http support TLS (https) 2020-09-04 21:27:19 +08:00
13275b1aa6 Chore: use only one goroutine to handle statistic (#940) 2020-09-03 10:30:18 +08:00
02d9169b5d Fix: potential PCB buffer overflow on bsd systems (#941) 2020-09-03 10:27:20 +08:00
7631bcc99e Improve: use atomic for connection statistic (#938) 2020-09-02 16:34:12 +08:00
a32ee13fc9 Feature: reuse dns resolver cache when hot reload 2020-08-31 00:32:18 +08:00
b8ed738238 Chore: update actions version 2020-08-30 23:06:21 +08:00
687c2a21cf Fix: vmess UDP option should be effect 2020-08-30 22:49:55 +08:00
ad18064e6b Chore: code style (#933) 2020-08-30 19:53:00 +08:00
c9735ef75b Fix: static check 2020-08-25 22:36:38 +08:00
b70882f01a Chore: add static check 2020-08-25 22:32:23 +08:00
5805334ccd Chore: pass staticcheck 2020-08-25 22:19:59 +08:00
c1b4382fe8 Feature: add Windows ARM32 build (#902)
Co-authored-by: MarksonHon <50002150+MarksonHon@users.noreply.github.com>
2020-08-16 13:50:56 +08:00
008743f20b Chore: update dependencies 2020-08-16 11:32:51 +08:00
50d778da3c Chore: cache process name when resolve failed (#900) 2020-08-15 16:55:55 +08:00
8b7c731fd6 Fix: ssr broken (#895) 2020-08-12 20:50:56 +08:00
0b7918de9c Migration: go 1.15 2020-08-12 13:47:50 +08:00
4f61c04519 Fix: ssr typo (#887) 2020-08-11 10:35:30 +08:00
89cf06036d Feature: dns server could lookup hosts (#872) 2020-08-11 10:28:17 +08:00
4ba6f248bc Fix: ssr bounds out of range panic (#882) 2020-08-11 10:17:40 +08:00
83a684c551 Change: adjust tolerance logic (#864) 2020-08-06 20:12:03 +08:00
92a23f1eab Feature: PROCESS-NAME for windows (#840) 2020-08-06 19:59:20 +08:00
622ac45258 Feature: PROCESS-NAME for freebsd (#855) 2020-07-31 20:01:19 +08:00
791d203b5f Fix: update cache if a process was found (#850) 2020-07-30 17:15:06 +08:00
77d6f9ae6f Fix: handle snell server reported error message properly (#848) 2020-07-30 15:54:26 +08:00
b1d9dfd6bf Improve: simplify macOS process searching 2020-07-29 11:27:18 +08:00
6532947e71 Fix: invert should resolve ip (#836) 2020-07-27 13:47:00 +08:00
6c5f23f552 Merge branch 'dev' of github.com:Dreamacro/clash into dev 2020-07-27 11:58:02 +08:00
78c3034158 Chore: rename NoResolveIP to ShouldResolveIP 2020-07-27 11:57:55 +08:00
8f0098092d Fix: protect alive with atomic value (#834) 2020-07-25 17:47:11 +08:00
33a6579a3a Feature: add ssr support (#805)
* Refactor ssr stream cipher to expose iv and key

References:
https://github.com/Dreamacro/go-shadowsocks2
https://github.com/sh4d0wfiend/go-shadowsocksr2

* Implement ssr obfs

Reference:
https://github.com/mzz2017/shadowsocksR

* Implement ssr protocol

References:
https://github.com/mzz2017/shadowsocksR
https://github.com/shadowsocksRb/shadowsocksr-libev
https://github.com/shadowsocksr-backup/shadowsocksr
2020-07-22 23:02:15 +08:00
b4221d4b74 Chore: README.md style fixed (#825)
make every item in TODO list has the same style
2020-07-22 21:34:37 +08:00
0e4b9daaad Improve: add cache for macOS PROCESS-NAME 2020-07-22 20:35:27 +08:00
ee72865f48 Fix: recycle buf on http obfs 2020-07-22 20:29:39 +08:00
6521acf8f1 Improve: check uid on process search & fix typo (#824) 2020-07-22 20:22:34 +08:00
4f73410618 Feature: add PROCESS-NAME rule for linux (#822) 2020-07-22 19:05:10 +08:00
20eff200b1 Fix: dns should put msg to cache while exchangeWithoutCache (#820) 2020-07-20 21:16:36 +08:00
ae1e1dc9f6 Feature: support PROCESS-NAME on macOS 2020-07-19 13:18:23 +08:00
cf9e1545a4 Improve: fix go test race detect 2020-07-18 20:56:13 +08:00
6c7a8fffe0 Chore: should not write file on file provider 2020-07-18 19:32:40 +08:00
3a3e2c05af Chore: add rule payload in log 2020-07-18 19:22:09 +08:00
02c7fd8d70 Fix: write msg cache multiple times (#812)
Co-authored-by: john.xu <john.xu@bytedance.com>
2020-07-17 17:34:40 +08:00
e6aa452b51 Fix: ticker leak 2020-07-13 00:25:54 +08:00
35449bfa17 Feature: add github stale action 2020-07-09 10:27:05 +08:00
acd51bbc90 Fix: obfs host should not have 80 port 2020-07-01 00:01:36 +08:00
f44cd9180c Chore: update GitHub issue template 2020-06-30 13:55:26 +08:00
93c987a6cb Fix: typo in dialer.go (#767) 2020-06-28 10:59:04 +08:00
3f0584ac09 Chore: move documentations to wiki (#766) 2020-06-28 10:39:30 +08:00
221 changed files with 10220 additions and 2610 deletions

View File

@ -7,48 +7,54 @@ assignees: ''
--- ---
<!-- The English version is available. --> <!--
感谢你向 Clash Core 提交 issue 感谢你向 Clash Core 提交 issue
在提交之前,请确认: 在提交之前,请确认:
- [ ] 我已经在 [Issue Tracker](……/) 中找过我要提出的问题
- [ ] 这是 Clash 核心的问题,并非我所使用的 Clash 衍生版本(如 Openclash、Koolclash 等)的特定问题
- [ ] 我已经使用 Clash core 的 dev 分支版本测试过,问题依旧存在
- [ ] 如果你可以自己 debug 并解决的话,提交 PR 吧! - [ ] 如果你可以自己 debug 并解决的话,提交 PR 吧!
- [ ] 我已经在 [Issue Tracker](……/) 中找过我要提出的问题
- [ ] 我已经使用 dev 分支版本测试过,问题依旧存在
- [ ] 我已经仔细看过 [Documentation](https://github.com/Dreamacro/clash/wiki/) 并无法自行解决问题
- [ ] 这是 Clash 核心的问题,并非我所使用的 Clash 衍生版本(如 OpenClash、KoolClash 等)的特定问题
请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。 请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。
<!-- Thanks for opening an issue towards the Clash core!
Thanks for submitting an issue towards the Clash core!
But before so, please do the following checklist: But before so, please do the following checklist:
- [ ] Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome. - [ ] Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome.
- [ ] Your issue may already be reported! Please search on the [issue tracker](……/) before creating one. - [ ] I have searched on the [issue tracker](……/) for a related issue.
- [ ] I have tested using the dev branch, and the issue still exists. - [ ] I have tested using the dev branch, and the issue still exists.
- [ ] This is an issue related to the Clash core, not to the derivatives of Clash, like Openclash or Koolclash - [ ] I have read the [documentation](https://github.com/Dreamacro/clash/wiki/) and was unable to solve the issue
- [ ] This is an issue of the Clash core *per se*, not to the derivatives of Clash, like OpenClash or KoolClash
Please understand that we close issues that fail to follow the issue template. Please understand that we close issues that fail to follow this issue template.
--> -->
我都确认过了,我要继续提交。
<!-- None of the above, create a bug report -->
------------------------------------------------------------------ ------------------------------------------------------------------
<!--
请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 issue 加上 *Needs more information* 标记并在收到更多资讯之前关闭 issue。 请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 issue 加上 *Needs more information* 标记并在收到更多资讯之前关闭 issue。
<!-- Make sure to add **all the information needed to understand the bug** so that someone can help. If the info is missing we'll add the 'Needs more information' label and close the issue until there is enough information. --> Make sure to add **all the information needed to understand the bug** so that someone can help. If the info is missing we'll add the 'Needs more information' label and close the issue until there is enough information.
-->
### clash core config ### Clash config
<!-- <!--
在下方附上 Clash core 脱敏后配置文件的内容 在下方附上 Clash core 脱敏后配置文件的内容
Paste the Clash core configuration below. Paste the Clash core configuration below.
--> -->
``` <details>
<summary>config.yaml</summary>
```yaml
…… ……
``` ```
</details>
### Clash log ### Clash log
<!-- <!--
在下方附上 Clash Core 的日志log level 最好使用 DEBUG 在下方附上 Clash Core 的日志log level 使用 DEBUG
Paste the Clash core log below with the log level set to `DEBUG`. Paste the Clash core log below with the log level set to `DEBUG`.
--> -->
``` ```
@ -57,9 +63,7 @@ Paste the Clash core log below with the log level set to `DEBUG`.
### 环境 Environment ### 环境 Environment
* Clash Core 的操作系统 (the OS that the Clash core is running on) * 操作系统 (the OS that the Clash core is running on)
……
* 使用者的操作系统 (the OS running on the client)
…… ……
* 网路环境或拓扑 (network conditions/topology) * 网路环境或拓扑 (network conditions/topology)
…… ……
@ -67,7 +71,7 @@ Paste the Clash core log below with the log level set to `DEBUG`.
…… ……
* ISP 有没有进行 DNS 污染 (is your ISP performing DNS pollution?) * ISP 有没有进行 DNS 污染 (is your ISP performing DNS pollution?)
…… ……
* 其他 * 其他 (any other information that would be useful)
…… ……
### 说明 Description ### 说明 Description
@ -85,7 +89,7 @@ Paste the Clash core log below with the log level set to `DEBUG`.
**我预期会发生……?** **我预期会发生……?**
<!-- **Expected behavior:** [What you expected to happen] --> <!-- **Expected behavior:** [What you expected to happen] -->
**实际上发生了什** **实际上发生了什**
<!-- **Actual behavior:** [What actually happened] --> <!-- **Actual behavior:** [What actually happened] -->
### 可能的解决方案 Possible Solution ### 可能的解决方案 Possible Solution

30
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,30 @@
name: "CodeQL"
on:
push:
branches: [ master, dev ]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@ -17,37 +17,60 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
with:
platforms: all
- name: Set up docker buildx - name: Set up docker buildx
id: buildx id: buildx
uses: crazy-max/ghaction-docker-buildx@v2 uses: docker/setup-buildx-action@v1
with: with:
buildx-version: latest version: latest
skip-cache: false
qemu-version: latest
- name: Docker login - name: Login to DockerHub
env: uses: docker/login-action@v1
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} with:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} username: ${{ secrets.DOCKER_USERNAME }}
run: | password: ${{ secrets.DOCKER_PASSWORD }}
echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
- name: Docker buildx image and push on dev branch - name: Login to Github Package
if: github.ref == 'refs/heads/dev' uses: docker/login-action@v1
run: | with:
docker buildx build --output "type=image,push=true" --platform=linux/amd64,linux/arm/v7,linux/arm64 --tag dreamacro/clash:dev . registry: ghcr.io
username: Dreamacro
password: ${{ secrets.PACKAGE_TOKEN }}
- name: Replace tag without `v` - name: Build dev branch and push
if: github.ref == 'refs/heads/dev'
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm/v7,linux/arm64
push: true
tags: 'dreamacro/clash:dev,ghcr.io/dreamacro/clash:dev'
- name: Get all docker tags
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
uses: actions/github-script@v1 uses: actions/github-script@v3
id: version id: tags
with: with:
script: | script: |
return context.payload.ref.replace(/\/?refs\/tags\/v/, '') const ref = `${context.payload.ref.replace(/\/?refs\/tags\//, '')}`
const tags = [
'dreamacro/clash:latest',
`dreamacro/clash:${ref}`,
'ghcr.io/dreamacro/clash:latest',
`ghcr.io/dreamacro/clash:${ref}`
]
return tags.join(',')
result-encoding: string result-encoding: string
- name: Docker buildx image and push on release - name: Build release and push
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
run: | uses: docker/build-push-action@v2
docker buildx build --output "type=image,push=true" --platform=linux/amd64,linux/arm/v7,linux/arm64 --tag dreamacro/clash:${{steps.version.outputs.result}} . with:
docker buildx build --output "type=image,push=true" --platform=linux/amd64,linux/arm/v7,linux/arm64 --tag dreamacro/clash:latest . context: .
platforms: linux/amd64,linux/arm/v7,linux/arm64
push: true
tags: ${{steps.tags.outputs.result}}

View File

@ -9,7 +9,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.14.x go-version: 1.16
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v2
@ -22,9 +22,12 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-go- ${{ runner.os }}-go-
- name: Get dependencies and run test - name: Get dependencies, run test and static check
run: | run: |
go test ./... go test ./...
go vet ./...
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck -- $(go list ./...)
- name: Build - name: Build
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
@ -41,4 +44,3 @@ jobs:
with: with:
files: bin/* files: bin/*
draft: true draft: true
prerelease: true

19
.github/workflows/stale.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: Mark stale issues and pull requests
on:
schedule:
- cron: "30 1 * * *"
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
days-before-stale: 60
days-before-close: 5

View File

@ -10,6 +10,7 @@ RUN go mod download && \
mv ./bin/clash-docker /clash mv ./bin/clash-docker /clash
FROM alpine:latest FROM alpine:latest
LABEL org.opencontainers.image.source="https://github.com/Dreamacro/clash"
RUN apk add --no-cache ca-certificates RUN apk add --no-cache ca-certificates
COPY --from=builder /Country.mmdb /root/.config/clash/ COPY --from=builder /Country.mmdb /root/.config/clash/

View File

@ -4,10 +4,11 @@ VERSION=$(shell git describe --tags || echo "unknown version")
BUILDTIME=$(shell date -u) BUILDTIME=$(shell date -u)
GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \ GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \
-X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \ -X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \
-w -s' -w -s -buildid='
PLATFORM_LIST = \ PLATFORM_LIST = \
darwin-amd64 \ darwin-amd64 \
darwin-arm64 \
linux-386 \ linux-386 \
linux-amd64 \ linux-amd64 \
linux-armv5 \ linux-armv5 \
@ -21,11 +22,13 @@ PLATFORM_LIST = \
linux-mips64 \ linux-mips64 \
linux-mips64le \ linux-mips64le \
freebsd-386 \ freebsd-386 \
freebsd-amd64 freebsd-amd64 \
freebsd-arm64
WINDOWS_ARCH_LIST = \ WINDOWS_ARCH_LIST = \
windows-386 \ windows-386 \
windows-amd64 windows-amd64 \
windows-arm32v7
all: linux-amd64 darwin-amd64 windows-amd64 # Most used all: linux-amd64 darwin-amd64 windows-amd64 # Most used
@ -35,6 +38,9 @@ docker:
darwin-amd64: darwin-amd64:
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-arm64:
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-386: linux-386:
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@ -77,11 +83,17 @@ freebsd-386:
freebsd-amd64: freebsd-amd64:
GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-arm64:
GOARCH=arm64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
windows-386: windows-386:
GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-amd64: windows-amd64:
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-arm32v7:
GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
gz_releases=$(addsuffix .gz, $(PLATFORM_LIST)) gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST)) zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST))

372
README.md
View File

@ -19,372 +19,39 @@
## Features ## Features
- Local HTTP/HTTPS/SOCKS server with/without authentication - Local HTTP/HTTPS/SOCKS server with authentication support
- VMess, Shadowsocks, Trojan (experimental), Snell protocol support for remote connections. UDP is supported. - VMess, Shadowsocks, Trojan, Snell protocol support for remote connections
- Built-in DNS server that aims to minimize DNS pollution attacks, supports DoH/DoT upstream. Fake IP is also supported. - Built-in DNS server that aims to minimize DNS pollution attack impact, supports DoH/DoT upstream and fake IP.
- Rules based off domains, GEOIP, IP CIDR or ports to forward packets to different nodes - Rules based off domains, GEOIP, IP CIDR or ports to forward packets to different nodes
- Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node based off latency - Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node based off latency
- Remote providers, allowing users to get node lists remotely instead of hardcoding in config - Remote providers, allowing users to get node lists remotely instead of hardcoding in config
- Netfilter TCP redirecting. You can deploy Clash on your Internet gateway with `iptables`. - Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`.
- Comprehensive HTTP API controller - Comprehensive HTTP RESTful API controller
## Install ## Premium Features
Clash requires Go >= 1.13. You can build it from source: - TUN mode on macOS, Linux and Windows. [Doc](https://github.com/Dreamacro/clash/wiki/premium-core-features#tun-device)
- Match your tunnel by [Script](https://github.com/Dreamacro/clash/wiki/premium-core-features#script)
- [Rule Provider](https://github.com/Dreamacro/clash/wiki/premium-core-features#rule-providers)
```sh ## Getting Started
$ go get -u -v github.com/Dreamacro/clash Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
```
Pre-built binaries are available here: [release](https://github.com/Dreamacro/clash/releases) ## Premium Release
Pre-built Premium binaries are available here: [premium release](https://github.com/Dreamacro/clash/releases/tag/premium). Source is not currently available. [Release](https://github.com/Dreamacro/clash/releases/tag/premium)
Check Clash version with: ## 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)
```sh
$ clash -v
```
## Daemonize Clash
We recommend using third-party daemon management tools like PM2, Supervisor or the like to keep Clash running as a service. ([Wiki](https://github.com/Dreamacro/clash/wiki/Clash-as-a-daemon))
In the case of [pm2](https://github.com/Unitech/pm2), start the daemon this way:
```sh
$ pm2 start clash
```
If you have Docker installed, it's recommended to deploy Clash directly using `docker-compose`: [run Clash in Docker](https://github.com/Dreamacro/clash/wiki/Run-clash-in-docker)
## Config
The default configuration directory is `$HOME/.config/clash`.
The name of the configuration file is `config.yaml`.
If you want to use another directory, use `-d` to control the configuration directory.
For example, you can use the current directory as the configuration directory:
```sh
$ clash -d .
```
<details>
<summary>This is an example configuration file (click to expand)</summary>
```yml
# port of HTTP
port: 7890
# port of SOCKS5
socks-port: 7891
# (HTTP and SOCKS5 in one port)
# mixed-port: 7890
# redir port for Linux and macOS
# redir-port: 7892
allow-lan: false
# Only applicable when setting allow-lan to true
# "*": bind all IP addresses
# 192.168.122.11: bind a single IPv4 address
# "[aaaa::a8aa:ff:fe09:57d8]": bind a single IPv6 address
# bind-address: "*"
# ipv6: false # when ipv6 is false, each clash dial with ipv6, but it's not affect the response of the dns server, default is false
# rule / global / direct (default is rule)
mode: rule
# set log level to stdout (default is info)
# info / warning / error / debug / silent
log-level: info
# RESTful API for clash
external-controller: 127.0.0.1:9090
# you can put the static web resource (such as clash-dashboard) to a directory, and clash would serve in `${API}/ui`
# input is a relative path to the configuration directory or an absolute path
# external-ui: folder
# Secret for RESTful API (Optional)
# secret: ""
# interface-name: en0 # outbound interface name
# authentication of local SOCKS5/HTTP(S) server
# authentication:
# - "user1:pass1"
# - "user2:pass2"
# # hosts, support wildcard (e.g. *.clash.dev Even *.foo.*.example.com)
# # static domain has a higher priority than wildcard domain (foo.example.com > *.example.com > .example.com)
# # +.foo.com equal .foo.com and foo.com
# hosts:
# '*.clash.dev': 127.0.0.1
# '.dev': 127.0.0.1
# 'alpha.clash.dev': '::1'
# '+.foo.dev': 127.0.0.1
# dns:
# enable: true # set true to enable dns (default is false)
# ipv6: false # it only affect the dns server response, default is false
# listen: 0.0.0.0:53
# # default-nameserver: # resolve dns nameserver host, should fill pure IP
# # - 114.114.114.114
# # - 8.8.8.8
# enhanced-mode: redir-host # or fake-ip
# # fake-ip-range: 198.18.0.1/16 # if you don't know what it is, don't change it
# fake-ip-filter: # fake ip white domain list
# - '*.lan'
# - localhost.ptlogin2.qq.com
# nameserver:
# - 114.114.114.114
# - tls://dns.rubyfish.cn:853 # dns over tls
# - https://1.1.1.1/dns-query # dns over https
# fallback: # concurrent request with nameserver, fallback used when GEOIP country isn't CN
# - tcp://1.1.1.1
# fallback-filter:
# geoip: true # default
# ipcidr: # ips in these subnets will be considered polluted
# - 240.0.0.0/4
proxies:
# shadowsocks
# The supported ciphers(encrypt methods):
# aes-128-gcm aes-192-gcm aes-256-gcm
# aes-128-cfb aes-192-cfb aes-256-cfb
# aes-128-ctr aes-192-ctr aes-256-ctr
# rc4-md5 chacha20-ietf xchacha20
# chacha20-ietf-poly1305 xchacha20-ietf-poly1305
- name: "ss1"
type: ss
server: server
port: 443
cipher: chacha20-ietf-poly1305
password: "password"
# udp: true
- name: "ss2"
type: ss
server: server
port: 443
cipher: chacha20-ietf-poly1305
password: "password"
plugin: obfs
plugin-opts:
mode: tls # or http
# host: bing.com
- name: "ss3"
type: ss
server: server
port: 443
cipher: chacha20-ietf-poly1305
password: "password"
plugin: v2ray-plugin
plugin-opts:
mode: websocket # no QUIC now
# tls: true # wss
# skip-cert-verify: true
# host: bing.com
# path: "/"
# mux: true
# headers:
# custom: value
# vmess
# cipher support auto/aes-128-gcm/chacha20-poly1305/none
- name: "vmess"
type: vmess
server: server
port: 443
uuid: uuid
alterId: 32
cipher: auto
# udp: true
# tls: true
# skip-cert-verify: true
# servername: example.com # priority over wss host
# network: ws
# ws-path: /path
# ws-headers:
# Host: v2ray.com
- name: "vmess-http"
type: vmess
server: server
port: 443
uuid: uuid
alterId: 32
cipher: auto
# udp: true
# network: http
# http-opts:
# # method: "GET"
# # path:
# # - '/'
# # - '/video'
# # headers:
# # Connection:
# # - keep-alive
# socks5
- name: "socks"
type: socks5
server: server
port: 443
# username: username
# password: password
# tls: true
# skip-cert-verify: true
# udp: true
# http
- name: "http"
type: http
server: server
port: 443
# username: username
# password: password
# tls: true # https
# skip-cert-verify: true
# snell
- name: "snell"
type: snell
server: server
port: 44046
psk: yourpsk
# obfs-opts:
# mode: http # or tls
# host: bing.com
# trojan
- name: "trojan"
type: trojan
server: server
port: 443
password: yourpsk
# udp: true
# sni: example.com # aka server name
# alpn:
# - h2
# - http/1.1
# skip-cert-verify: true
proxy-groups:
# relay chains the proxies. proxies shall not contain a relay. No UDP support.
# Traffic: clash <-> http <-> vmess <-> ss1 <-> ss2 <-> Internet
- name: "relay"
type: relay
proxies:
- http
- vmess
- ss1
- ss2
# url-test select which proxy will be used by benchmarking speed to a URL.
- name: "auto"
type: url-test
proxies:
- ss1
- ss2
- vmess1
# tolerance: 150
url: 'http://www.gstatic.com/generate_204'
interval: 300
# fallback select an available policy by priority. The availability is tested by accessing an URL, just like an auto url-test group.
- name: "fallback-auto"
type: fallback
proxies:
- ss1
- ss2
- vmess1
url: 'http://www.gstatic.com/generate_204'
interval: 300
# load-balance: The request of the same eTLD will be dial on the same proxy.
- name: "load-balance"
type: load-balance
proxies:
- ss1
- ss2
- vmess1
url: 'http://www.gstatic.com/generate_204'
interval: 300
# select is used for selecting proxy or proxy group
# you can use RESTful API to switch proxy, is recommended for use in GUI.
- name: Proxy
type: select
proxies:
- ss1
- ss2
- vmess1
- auto
- name: UseProvider
type: select
use:
- provider1
proxies:
- Proxy
- DIRECT
proxy-providers:
provider1:
type: http
url: "url"
interval: 3600
path: ./hk.yaml
health-check:
enable: true
interval: 600
url: http://www.gstatic.com/generate_204
test:
type: file
path: /test.yaml
health-check:
enable: true
interval: 36000
url: http://www.gstatic.com/generate_204
rules:
- DOMAIN-SUFFIX,google.com,auto
- DOMAIN-KEYWORD,google,auto
- DOMAIN,google.com,auto
- DOMAIN-SUFFIX,ad.com,REJECT
# rename SOURCE-IP-CIDR and would remove after prerelease
- SRC-IP-CIDR,192.168.1.201/32,DIRECT
# optional param "no-resolve" for IP rules (GEOIP IP-CIDR)
- IP-CIDR,127.0.0.0/8,DIRECT
- GEOIP,CN,DIRECT
- DST-PORT,80,DIRECT
- SRC-PORT,7777,DIRECT
- MATCH,auto
```
</details>
## Advanced
[Provider](https://github.com/Dreamacro/clash/wiki/Provider)
## Documentations
https://clash.gitbook.io/
## Credits ## Credits
[riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2) * [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
* [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
[v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
## License ## License
This software is released under the GPL-3.0 license.
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FDreamacro%2Fclash.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FDreamacro%2Fclash.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large)
## TODO ## TODO
@ -393,4 +60,3 @@ https://clash.gitbook.io/
- [x] Redir proxy - [x] Redir proxy
- [x] UDP support - [x] UDP support
- [x] Connection manager - [x] Connection manager
- ~~[ ] Event API~~

View File

@ -1,124 +1,48 @@
package outbound package adapter
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "fmt"
"net" "net"
"net/http" "net/http"
"net/url"
"time" "time"
"github.com/Dreamacro/clash/common/queue" "github.com/Dreamacro/clash/common/queue"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"go.uber.org/atomic"
) )
var (
defaultURLTestTimeout = time.Second * 5
)
type Base struct {
name string
addr string
tp C.AdapterType
udp bool
}
func (b *Base) Name() string {
return b.name
}
func (b *Base) Type() C.AdapterType {
return b.tp
}
func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return c, errors.New("no support")
}
func (b *Base) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
return nil, errors.New("no support")
}
func (b *Base) SupportUDP() bool {
return b.udp
}
func (b *Base) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": b.Type().String(),
})
}
func (b *Base) Addr() string {
return b.addr
}
func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy {
return nil
}
func NewBase(name string, addr string, tp C.AdapterType, udp bool) *Base {
return &Base{name, addr, tp, udp}
}
type conn struct {
net.Conn
chain C.Chain
}
func (c *conn) Chains() C.Chain {
return c.chain
}
func (c *conn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name())
}
func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
return &conn{c, []string{a.Name()}}
}
type packetConn struct {
net.PacketConn
chain C.Chain
}
func (c *packetConn) Chains() C.Chain {
return c.chain
}
func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name())
}
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
return &packetConn{pc, []string{a.Name()}}
}
type Proxy struct { type Proxy struct {
C.ProxyAdapter C.ProxyAdapter
history *queue.Queue history *queue.Queue
alive bool alive *atomic.Bool
} }
// Alive implements C.Proxy
func (p *Proxy) Alive() bool { func (p *Proxy) Alive() bool {
return p.alive return p.alive.Load()
} }
// Dial implements C.Proxy
func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) { func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout) ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
defer cancel() defer cancel()
return p.DialContext(ctx, metadata) return p.DialContext(ctx, metadata)
} }
// DialContext implements C.ProxyAdapter
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
conn, err := p.ProxyAdapter.DialContext(ctx, metadata) conn, err := p.ProxyAdapter.DialContext(ctx, metadata)
if err != nil { if err != nil {
p.alive = false p.alive.Store(false)
} }
return conn, err return conn, err
} }
// DelayHistory implements C.Proxy
func (p *Proxy) DelayHistory() []C.DelayHistory { func (p *Proxy) DelayHistory() []C.DelayHistory {
queue := p.history.Copy() queue := p.history.Copy()
histories := []C.DelayHistory{} histories := []C.DelayHistory{}
@ -129,9 +53,10 @@ func (p *Proxy) DelayHistory() []C.DelayHistory {
} }
// LastDelay return last history record. if proxy is not alive, return the max value of uint16. // LastDelay return last history record. if proxy is not alive, return the max value of uint16.
// implements C.Proxy
func (p *Proxy) LastDelay() (delay uint16) { func (p *Proxy) LastDelay() (delay uint16) {
var max uint16 = 0xffff var max uint16 = 0xffff
if !p.alive { if !p.alive.Load() {
return max return max
} }
@ -146,6 +71,7 @@ func (p *Proxy) LastDelay() (delay uint16) {
return history.Delay return history.Delay
} }
// MarshalJSON implements C.ProxyAdapter
func (p *Proxy) MarshalJSON() ([]byte, error) { func (p *Proxy) MarshalJSON() ([]byte, error) {
inner, err := p.ProxyAdapter.MarshalJSON() inner, err := p.ProxyAdapter.MarshalJSON()
if err != nil { if err != nil {
@ -156,13 +82,15 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
json.Unmarshal(inner, &mapping) json.Unmarshal(inner, &mapping)
mapping["history"] = p.DelayHistory() mapping["history"] = p.DelayHistory()
mapping["name"] = p.Name() mapping["name"] = p.Name()
mapping["udp"] = p.SupportUDP()
return json.Marshal(mapping) return json.Marshal(mapping)
} }
// URLTest get the delay for the specified URL // URLTest get the delay for the specified URL
// implements C.Proxy
func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) { func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
defer func() { defer func() {
p.alive = err == nil p.alive.Store(err == nil)
record := C.DelayHistory{Time: time.Now()} record := C.DelayHistory{Time: time.Now()}
if err == nil { if err == nil {
record.Delay = t record.Delay = t
@ -218,5 +146,33 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
} }
func NewProxy(adapter C.ProxyAdapter) *Proxy { func NewProxy(adapter C.ProxyAdapter) *Proxy {
return &Proxy{adapter, queue.New(10), true} return &Proxy{adapter, queue.New(10), atomic.NewBool(true)}
}
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
u, err := url.Parse(rawURL)
if err != nil {
return
}
port := u.Port()
if port == "" {
switch u.Scheme {
case "https":
port = "443"
case "http":
port = "80"
default:
err = fmt.Errorf("%s scheme not Support", rawURL)
return
}
}
addr = C.Metadata{
AddrType: C.AtypDomainName,
Host: u.Hostname(),
DstIP: nil,
DstPort: port,
}
return
} }

21
adapter/inbound/http.go Normal file
View File

@ -0,0 +1,21 @@
package inbound
import (
"net"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/transport/socks5"
)
// NewHTTP receive normal http request and return HTTPContext
func NewHTTP(target string, source net.Addr, conn net.Conn) *context.ConnContext {
metadata := parseSocksAddr(socks5.ParseAddr(target))
metadata.NetWork = C.TCP
metadata.Type = C.HTTP
if ip, port, err := parseAddr(source.String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
return context.NewConnContext(conn, metadata)
}

View File

@ -5,18 +5,16 @@ import (
"net/http" "net/http"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
) )
// NewHTTPS is HTTPAdapter generator // NewHTTPS receive CONNECT request and return ConnContext
func NewHTTPS(request *http.Request, conn net.Conn) *SocketAdapter { func NewHTTPS(request *http.Request, conn net.Conn) *context.ConnContext {
metadata := parseHTTPAddr(request) metadata := parseHTTPAddr(request)
metadata.Type = C.HTTPCONNECT metadata.Type = C.HTTPCONNECT
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil { if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
metadata.SrcIP = ip metadata.SrcIP = ip
metadata.SrcPort = port metadata.SrcPort = port
} }
return &SocketAdapter{ return context.NewConnContext(conn, metadata)
metadata: metadata,
Conn: conn,
}
} }

View File

@ -1,8 +1,8 @@
package inbound package inbound
import ( import (
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
) )
// PacketAdapter is a UDP Packet adapter for socks/redir/tun // PacketAdapter is a UDP Packet adapter for socks/redir/tun

22
adapter/inbound/socket.go Normal file
View File

@ -0,0 +1,22 @@
package inbound
import (
"net"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/transport/socks5"
)
// NewSocket receive TCP inbound and return ConnContext
func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *context.ConnContext {
metadata := parseSocksAddr(target)
metadata.NetWork = C.TCP
metadata.Type = source
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
return context.NewConnContext(conn, metadata)
}

View File

@ -6,8 +6,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
) )
func parseSocksAddr(target socks5.Addr) *C.Metadata { func parseSocksAddr(target socks5.Addr) *C.Metadata {

100
adapter/outbound/base.go Normal file
View File

@ -0,0 +1,100 @@
package outbound
import (
"encoding/json"
"errors"
"net"
C "github.com/Dreamacro/clash/constant"
)
type Base struct {
name string
addr string
tp C.AdapterType
udp bool
}
// Name implements C.ProxyAdapter
func (b *Base) Name() string {
return b.name
}
// Type implements C.ProxyAdapter
func (b *Base) Type() C.AdapterType {
return b.tp
}
// StreamConn implements C.ProxyAdapter
func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return c, errors.New("no support")
}
// DialUDP implements C.ProxyAdapter
func (b *Base) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
return nil, errors.New("no support")
}
// SupportUDP implements C.ProxyAdapter
func (b *Base) SupportUDP() bool {
return b.udp
}
// MarshalJSON implements C.ProxyAdapter
func (b *Base) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": b.Type().String(),
})
}
// Addr implements C.ProxyAdapter
func (b *Base) Addr() string {
return b.addr
}
// Unwrap implements C.ProxyAdapter
func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy {
return nil
}
func NewBase(name string, addr string, tp C.AdapterType, udp bool) *Base {
return &Base{name, addr, tp, udp}
}
type conn struct {
net.Conn
chain C.Chain
}
// Chains implements C.Connection
func (c *conn) Chains() C.Chain {
return c.chain
}
// AppendToChains implements C.Connection
func (c *conn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name())
}
func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
return &conn{c, []string{a.Name()}}
}
type packetConn struct {
net.PacketConn
chain C.Chain
}
// Chains implements C.Connection
func (c *packetConn) Chains() C.Chain {
return c.chain
}
// AppendToChains implements C.Connection
func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name())
}
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
return &packetConn{pc, []string{a.Name()}}
}

View File

@ -12,6 +12,7 @@ type Direct struct {
*Base *Base
} }
// DialContext implements C.ProxyAdapter
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
address := net.JoinHostPort(metadata.String(), metadata.DstPort) address := net.JoinHostPort(metadata.String(), metadata.DstPort)
@ -23,6 +24,7 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
return NewConn(c, d), nil return NewConn(c, d), nil
} }
// DialUDP implements C.ProxyAdapter
func (d *Direct) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { func (d *Direct) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := dialer.ListenPacket("udp", "") pc, err := dialer.ListenPacket("udp", "")
if err != nil { if err != nil {

View File

@ -31,9 +31,11 @@ type HttpOption struct {
UserName string `proxy:"username,omitempty"` UserName string `proxy:"username,omitempty"`
Password string `proxy:"password,omitempty"` Password string `proxy:"password,omitempty"`
TLS bool `proxy:"tls,omitempty"` TLS bool `proxy:"tls,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
} }
// StreamConn implements C.ProxyAdapter
func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if h.tlsConfig != nil { if h.tlsConfig != nil {
cc := tls.Client(c, h.tlsConfig) cc := tls.Client(c, h.tlsConfig)
@ -50,13 +52,16 @@ func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return c, nil return c, nil
} }
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { // DialContext implements C.ProxyAdapter
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", h.addr) c, err := dialer.DialContext(ctx, "tcp", h.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", h.addr, err) return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = h.StreamConn(c, metadata) c, err = h.StreamConn(c, metadata)
if err != nil { if err != nil {
return nil, err return nil, err
@ -114,10 +119,13 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
func NewHttp(option HttpOption) *Http { func NewHttp(option HttpOption) *Http {
var tlsConfig *tls.Config var tlsConfig *tls.Config
if option.TLS { if option.TLS {
sni := option.Server
if option.SNI != "" {
sni = option.SNI
}
tlsConfig = &tls.Config{ tlsConfig = &tls.Config{
InsecureSkipVerify: option.SkipCertVerify, InsecureSkipVerify: option.SkipCertVerify,
ClientSessionCache: getClientSessionCache(), ServerName: sni,
ServerName: option.Server,
} }
} }

View File

@ -14,10 +14,12 @@ type Reject struct {
*Base *Base
} }
// DialContext implements C.ProxyAdapter
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
return NewConn(&NopConn{}, r), nil return NewConn(&NopConn{}, r), nil
} }
// DialUDP implements C.ProxyAdapter
func (r *Reject) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { func (r *Reject) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
return nil, errors.New("match reject rule") return nil, errors.New("match reject rule")
} }

View File

@ -2,7 +2,6 @@ package outbound
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net" "net"
@ -10,10 +9,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"
obfs "github.com/Dreamacro/clash/component/simple-obfs"
"github.com/Dreamacro/clash/component/socks5"
v2rayObfs "github.com/Dreamacro/clash/component/v2ray-plugin"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
"github.com/Dreamacro/clash/transport/socks5"
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
"github.com/Dreamacro/go-shadowsocks2/core" "github.com/Dreamacro/go-shadowsocks2/core"
) )
@ -40,7 +39,7 @@ type ShadowSocksOption struct {
} }
type simpleObfsOption struct { type simpleObfsOption struct {
Mode string `obfs:"mode"` Mode string `obfs:"mode,omitempty"`
Host string `obfs:"host,omitempty"` Host string `obfs:"host,omitempty"`
} }
@ -54,6 +53,7 @@ type v2rayObfsOption struct {
Mux bool `obfs:"mux,omitempty"` Mux bool `obfs:"mux,omitempty"`
} }
// StreamConn implements C.ProxyAdapter
func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
switch ss.obfsMode { switch ss.obfsMode {
case "tls": case "tls":
@ -73,17 +73,21 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e
return c, err return c, err
} }
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { // DialContext implements C.ProxyAdapter
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr) c, err := dialer.DialContext(ctx, "tcp", ss.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = ss.StreamConn(c, metadata) c, err = ss.StreamConn(c, metadata)
return NewConn(c, ss), err return NewConn(c, ss), err
} }
// DialUDP implements C.ProxyAdapter
func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := dialer.ListenPacket("udp", "") pc, err := dialer.ListenPacket("udp", "")
if err != nil { if err != nil {
@ -92,6 +96,7 @@ func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
addr, err := resolveUDPAddr("udp", ss.addr) addr, err := resolveUDPAddr("udp", ss.addr)
if err != nil { if err != nil {
pc.Close()
return nil, err return nil, err
} }
@ -99,12 +104,6 @@ func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ss), nil return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ss), nil
} }
func (ss *ShadowSocks) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": ss.Type().String(),
})
}
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
cipher := option.Cipher cipher := option.Cipher
@ -150,7 +149,6 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
if opts.TLS { if opts.TLS {
v2rayOption.TLS = true v2rayOption.TLS = true
v2rayOption.SkipCertVerify = opts.SkipCertVerify v2rayOption.SkipCertVerify = opts.SkipCertVerify
v2rayOption.SessionCache = getClientSessionCache()
} }
} }

View File

@ -0,0 +1,148 @@
package outbound
import (
"context"
"fmt"
"net"
"strconv"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/ssr/obfs"
"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 {
*Base
cipher core.Cipher
obfs obfs.Obfs
protocol protocol.Protocol
}
type ShadowSocksROption struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Password string `proxy:"password"`
Cipher string `proxy:"cipher"`
Obfs string `proxy:"obfs"`
ObfsParam string `proxy:"obfs-param,omitempty"`
Protocol string `proxy:"protocol"`
ProtocolParam string `proxy:"protocol-param,omitempty"`
UDP bool `proxy:"udp,omitempty"`
}
// StreamConn implements C.ProxyAdapter
func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c = ssr.obfs.StreamConn(c)
c = ssr.cipher.StreamConn(c)
var (
iv []byte
err error
)
switch conn := c.(type) {
case *shadowstream.Conn:
iv, err = conn.ObtainWriteIV()
if err != nil {
return nil, err
}
case *shadowaead.Conn:
return nil, fmt.Errorf("invalid connection type")
}
c = ssr.protocol.StreamConn(c, iv)
_, err = c.Write(serializesSocksAddr(metadata))
return c, err
}
// DialContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ssr.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err)
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = ssr.StreamConn(c, metadata)
return NewConn(c, ssr), err
}
// DialUDP implements C.ProxyAdapter
func (ssr *ShadowSocksR) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := dialer.ListenPacket("udp", "")
if err != nil {
return nil, err
}
addr, err := resolveUDPAddr("udp", ssr.addr)
if err != nil {
pc.Close()
return nil, err
}
pc = ssr.cipher.PacketConn(pc)
pc = ssr.protocol.PacketConn(pc)
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil
}
func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
cipher := option.Cipher
password := option.Password
coreCiph, err := core.PickCipher(cipher, nil, password)
if err != nil {
return nil, fmt.Errorf("ssr %s initialize error: %w", addr, err)
}
var (
ivSize int
key []byte
)
if option.Cipher == "dummy" {
ivSize = 0
key = core.Kdf(option.Password, 16)
} else {
ciph, ok := coreCiph.(*core.StreamCipher)
if !ok {
return nil, fmt.Errorf("%s is not dummy or a supported stream cipher in ssr", cipher)
}
ivSize = ciph.IVSize()
key = ciph.Key
}
obfs, obfsOverhead, err := obfs.PickObfs(option.Obfs, &obfs.Base{
Host: option.Server,
Port: option.Port,
Key: key,
IVSize: ivSize,
Param: option.ObfsParam,
})
if err != nil {
return nil, fmt.Errorf("ssr %s initialize obfs error: %w", addr, err)
}
protocol, err := protocol.PickProtocol(option.Protocol, &protocol.Base{
Key: key,
Overhead: obfsOverhead,
Param: option.ProtocolParam,
})
if err != nil {
return nil, fmt.Errorf("ssr %s initialize protocol error: %w", addr, err)
}
return &ShadowSocksR{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.ShadowsocksR,
udp: option.UDP,
},
cipher: coreCiph,
obfs: obfs,
protocol: protocol,
}, nil
}

135
adapter/outbound/snell.go Normal file
View File

@ -0,0 +1,135 @@
package outbound
import (
"context"
"fmt"
"net"
"strconv"
"github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
"github.com/Dreamacro/clash/transport/snell"
)
type Snell struct {
*Base
psk []byte
pool *snell.Pool
obfsOption *simpleObfsOption
version int
}
type SnellOption struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Psk string `proxy:"psk"`
Version int `proxy:"version,omitempty"`
ObfsOpts map[string]interface{} `proxy:"obfs-opts,omitempty"`
}
type streamOption struct {
psk []byte
version int
addr string
obfsOption *simpleObfsOption
}
func streamConn(c net.Conn, option streamOption) *snell.Snell {
switch option.obfsOption.Mode {
case "tls":
c = obfs.NewTLSObfs(c, option.obfsOption.Host)
case "http":
_, port, _ := net.SplitHostPort(option.addr)
c = obfs.NewHTTPObfs(c, option.obfsOption.Host, port)
}
return snell.StreamConn(c, option.psk, option.version)
}
// StreamConn implements C.ProxyAdapter
func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
port, _ := strconv.Atoi(metadata.DstPort)
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
return c, err
}
// DialContext implements C.ProxyAdapter
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
if s.version == snell.Version2 {
c, err := s.pool.Get()
if err != nil {
return nil, err
}
port, _ := strconv.Atoi(metadata.DstPort)
if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil {
c.Close()
return nil, err
}
return NewConn(c, s), err
}
c, err := dialer.DialContext(ctx, "tcp", s.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = s.StreamConn(c, metadata)
return NewConn(c, s), err
}
func NewSnell(option SnellOption) (*Snell, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
psk := []byte(option.Psk)
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
obfsOption := &simpleObfsOption{Host: "bing.com"}
if err := decoder.Decode(option.ObfsOpts, obfsOption); err != nil {
return nil, fmt.Errorf("snell %s initialize obfs error: %w", addr, err)
}
switch obfsOption.Mode {
case "tls", "http", "":
break
default:
return nil, fmt.Errorf("snell %s obfs mode error: %s", addr, obfsOption.Mode)
}
// backward compatible
if option.Version == 0 {
option.Version = snell.DefaultSnellVersion
}
if option.Version != snell.Version1 && option.Version != snell.Version2 {
return nil, fmt.Errorf("snell version error: %d", option.Version)
}
s := &Snell{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Snell,
},
psk: psk,
obfsOption: obfsOption,
version: option.Version,
}
if option.Version == snell.Version2 {
s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) {
c, err := dialer.DialContext(ctx, "tcp", addr)
if err != nil {
return nil, err
}
tcpKeepAlive(c)
return streamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil
})
}
return s, nil
}

View File

@ -11,8 +11,8 @@ import (
"strconv" "strconv"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
) )
type Socks5 struct { type Socks5 struct {
@ -35,6 +35,7 @@ type Socks5Option struct {
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
} }
// 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) {
if ss.tls { if ss.tls {
cc := tls.Client(c, ss.tlsConfig) cc := tls.Client(c, ss.tlsConfig)
@ -58,13 +59,16 @@ func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
return c, nil return c, nil
} }
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { // DialContext implements C.ProxyAdapter
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr) c, err := dialer.DialContext(ctx, "tcp", ss.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = ss.StreamConn(c, metadata) c, err = ss.StreamConn(c, metadata)
if err != nil { if err != nil {
return nil, err return nil, err
@ -73,8 +77,9 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn
return NewConn(c, ss), nil return NewConn(c, ss), nil
} }
// DialUDP implements C.ProxyAdapter
func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) { func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout) ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
defer cancel() defer cancel()
c, err := dialer.DialContext(ctx, "tcp", ss.addr) c, err := dialer.DialContext(ctx, "tcp", ss.addr)
if err != nil { if err != nil {
@ -88,11 +93,7 @@ func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
c = cc c = cc
} }
defer func() { defer safeConnClose(c, err)
if err != nil {
c.Close()
}
}()
tcpKeepAlive(c) tcpKeepAlive(c)
var user *socks5.User var user *socks5.User
@ -122,7 +123,21 @@ func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
pc.Close() pc.Close()
}() }()
return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindAddr.UDPAddr(), tcpConn: c}, ss), nil // Support unspecified UDP bind address.
bindUDPAddr := bindAddr.UDPAddr()
if bindUDPAddr == nil {
err = errors.New("invalid UDP bind address")
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
} }
func NewSocks5(option Socks5Option) *Socks5 { func NewSocks5(option Socks5Option) *Socks5 {
@ -130,7 +145,6 @@ func NewSocks5(option Socks5Option) *Socks5 {
if option.TLS { if option.TLS {
tlsConfig = &tls.Config{ tlsConfig = &tls.Config{
InsecureSkipVerify: option.SkipCertVerify, InsecureSkipVerify: option.SkipCertVerify,
ClientSessionCache: getClientSessionCache(),
ServerName: option.Server, ServerName: option.Server,
} }
} }

176
adapter/outbound/trojan.go Normal file
View File

@ -0,0 +1,176 @@
package outbound
import (
"context"
"crypto/tls"
"fmt"
"net"
"strconv"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/trojan"
"golang.org/x/net/http2"
)
type Trojan struct {
*Base
instance *trojan.Trojan
// for gun mux
gunTLSConfig *tls.Config
gunConfig *gun.Config
transport *http2.Transport
}
type TrojanOption struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Password string `proxy:"password"`
ALPN []string `proxy:"alpn,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
}
// StreamConn implements C.ProxyAdapter
func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
if t.transport != nil {
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig)
} else {
c, err = t.instance.StreamConn(c)
}
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata))
return c, err
}
// DialContext implements C.ProxyAdapter
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
// gun transport
if t.transport != nil {
c, err := gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil {
return nil, err
}
if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil {
c.Close()
return nil, err
}
return NewConn(c, t), nil
}
c, err := dialer.DialContext(ctx, "tcp", t.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = t.StreamConn(c, metadata)
if err != nil {
return nil, err
}
return NewConn(c, t), err
}
// DialUDP implements C.ProxyAdapter
func (t *Trojan) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
var c net.Conn
// grpc transport
if t.transport != nil {
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
defer safeConnClose(c, err)
} else {
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
defer cancel()
c, err = dialer.DialContext(ctx, "tcp", t.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
defer safeConnClose(c, err)
tcpKeepAlive(c)
c, err = t.instance.StreamConn(c)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
}
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
if err != nil {
return nil, err
}
pc := t.instance.PacketConn(c)
return newPacketConn(pc, t), err
}
func NewTrojan(option TrojanOption) (*Trojan, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
tOption := &trojan.Option{
Password: option.Password,
ALPN: option.ALPN,
ServerName: option.Server,
SkipCertVerify: option.SkipCertVerify,
}
if option.SNI != "" {
tOption.ServerName = option.SNI
}
t := &Trojan{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Trojan,
udp: option.UDP,
},
instance: trojan.New(tOption),
}
if option.Network == "grpc" {
dialFn := func(network, addr string) (net.Conn, error) {
c, err := dialer.DialContext(context.Background(), "tcp", t.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error())
}
tcpKeepAlive(c)
return c, nil
}
tlsConfig := &tls.Config{
NextProtos: option.ALPN,
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: tOption.SkipCertVerify,
ServerName: tOption.ServerName,
}
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
t.gunTLSConfig = tlsConfig
t.gunConfig = &gun.Config{
ServiceName: option.GrpcOpts.GrpcServiceName,
Host: tOption.ServerName,
}
}
return t, nil
}

View File

@ -2,56 +2,15 @@ package outbound
import ( import (
"bytes" "bytes"
"crypto/tls"
"fmt"
"net" "net"
"net/url"
"strconv" "strconv"
"sync"
"time" "time"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
) )
const (
tcpTimeout = 5 * time.Second
)
var (
globalClientSessionCache tls.ClientSessionCache
once sync.Once
)
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
u, err := url.Parse(rawURL)
if err != nil {
return
}
port := u.Port()
if port == "" {
switch u.Scheme {
case "https":
port = "443"
case "http":
port = "80"
default:
err = fmt.Errorf("%s scheme not Support", rawURL)
return
}
}
addr = C.Metadata{
AddrType: C.AtypDomainName,
Host: u.Hostname(),
DstIP: nil,
DstPort: port,
}
return
}
func tcpKeepAlive(c net.Conn) { func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok { if tcp, ok := c.(*net.TCPConn); ok {
tcp.SetKeepAlive(true) tcp.SetKeepAlive(true)
@ -59,13 +18,6 @@ func tcpKeepAlive(c net.Conn) {
} }
} }
func getClientSessionCache() tls.ClientSessionCache {
once.Do(func() {
globalClientSessionCache = tls.NewLRUClientSessionCache(128)
})
return globalClientSessionCache
}
func serializesSocksAddr(metadata *C.Metadata) []byte { func serializesSocksAddr(metadata *C.Metadata) []byte {
var buf [][]byte var buf [][]byte
aType := uint8(metadata.AddrType) aType := uint8(metadata.AddrType)
@ -98,3 +50,9 @@ func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
} }
return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port)) return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port))
} }
func safeConnClose(c net.Conn, err error) {
if err != nil {
c.Close()
}
}

View File

@ -2,6 +2,7 @@ package outbound
import ( import (
"context" "context"
"crypto/tls"
"errors" "errors"
"fmt" "fmt"
"net" "net"
@ -11,14 +12,22 @@ import (
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
"github.com/Dreamacro/clash/component/vmess"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/vmess"
"golang.org/x/net/http2"
) )
type Vmess struct { type Vmess struct {
*Base *Base
client *vmess.Client client *vmess.Client
option *VmessOption option *VmessOption
// for gun mux
gunTLSConfig *tls.Config
gunConfig *gun.Config
transport *http2.Transport
} }
type VmessOption struct { type VmessOption struct {
@ -32,6 +41,8 @@ type VmessOption struct {
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"` Network string `proxy:"network,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSPath string `proxy:"ws-path,omitempty"` WSPath string `proxy:"ws-path,omitempty"`
WSHeaders map[string]string `proxy:"ws-headers,omitempty"` WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
@ -44,6 +55,16 @@ type HTTPOptions struct {
Headers map[string][]string `proxy:"headers,omitempty"` Headers map[string][]string `proxy:"headers,omitempty"`
} }
type HTTP2Options struct {
Host []string `proxy:"host,omitempty"`
Path string `proxy:"path,omitempty"`
}
type GrpcOptions struct {
GrpcServiceName string `proxy:"grpc-service-name,omitempty"`
}
// StreamConn implements C.ProxyAdapter
func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error var err error
switch v.option.Network { switch v.option.Network {
@ -65,12 +86,29 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if v.option.TLS { if v.option.TLS {
wsOpts.TLS = true wsOpts.TLS = true
wsOpts.SessionCache = getClientSessionCache()
wsOpts.SkipCertVerify = v.option.SkipCertVerify wsOpts.SkipCertVerify = v.option.SkipCertVerify
wsOpts.ServerName = v.option.ServerName wsOpts.ServerName = v.option.ServerName
} }
c, err = vmess.StreamWebsocketConn(c, wsOpts) c, err = vmess.StreamWebsocketConn(c, wsOpts)
case "http": case "http":
// readability first, so just copy default TLS logic
if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := &vmess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
}
if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName
}
c, err = vmess.StreamTLSConn(c, tlsOpts)
if err != nil {
return nil, err
}
}
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
httpOpts := &vmess.HTTPConfig{ httpOpts := &vmess.HTTPConfig{
Host: host, Host: host,
@ -80,6 +118,31 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
} }
c = vmess.StreamHTTPConn(c, httpOpts) c = vmess.StreamHTTPConn(c, httpOpts)
case "h2":
host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := vmess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
NextProtos: []string{"h2"},
}
if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName
}
c, err = vmess.StreamTLSConn(c, &tlsOpts)
if err != nil {
return nil, err
}
h2Opts := &vmess.H2Config{
Hosts: v.option.HTTP2Opts.Host,
Path: v.option.HTTP2Opts.Path,
}
c, err = vmess.StreamH2Conn(c, h2Opts)
case "grpc":
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig)
default: default:
// handle TLS // handle TLS
if v.option.TLS { if v.option.TLS {
@ -87,7 +150,6 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
tlsOpts := &vmess.TLSConfig{ tlsOpts := &vmess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
SessionCache: getClientSessionCache(),
} }
if v.option.ServerName != "" { if v.option.ServerName != "" {
@ -105,19 +167,38 @@ 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))
} }
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { // DialContext implements C.ProxyAdapter
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
// gun transport
if v.transport != nil {
c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil {
return nil, err
}
defer safeConnClose(c, err)
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
if err != nil {
return nil, err
}
return NewConn(c, v), nil
}
c, err := dialer.DialContext(ctx, "tcp", v.addr) c, err := dialer.DialContext(ctx, "tcp", v.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = v.StreamConn(c, metadata) c, err = v.StreamConn(c, metadata)
return NewConn(c, v), err return NewConn(c, v), err
} }
func (v *Vmess) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { // DialUDP implements C.ProxyAdapter
// vmess use stream-oriented udp, so clash needs a net.UDPAddr func (v *Vmess) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
// vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() { if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host) ip, err := resolver.ResolveIP(metadata.Host)
if err != nil { if err != nil {
@ -126,17 +207,33 @@ func (v *Vmess) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
metadata.DstIP = ip metadata.DstIP = ip
} }
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout) var c net.Conn
defer cancel() // gun transport
c, err := dialer.DialContext(ctx, "tcp", v.addr) if v.transport != nil {
if err != nil { c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) if err != nil {
return nil, err
}
defer safeConnClose(c, err)
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
} else {
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
defer cancel()
c, err = dialer.DialContext(ctx, "tcp", v.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = v.StreamConn(c, metadata)
} }
tcpKeepAlive(c)
c, err = v.StreamConn(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(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
} }
@ -148,21 +245,66 @@ func NewVmess(option VmessOption) (*Vmess, error) {
Security: security, Security: security,
HostName: option.Server, HostName: option.Server,
Port: strconv.Itoa(option.Port), Port: strconv.Itoa(option.Port),
IsAead: option.AlterID == 0,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Vmess{ switch option.Network {
case "h2", "grpc":
if !option.TLS {
return nil, fmt.Errorf("TLS must be true with h2/grpc network")
}
}
v := &Vmess{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vmess, tp: C.Vmess,
udp: true, udp: option.UDP,
}, },
client: client, client: client,
option: &option, option: &option,
}, nil }
switch option.Network {
case "h2":
if len(option.HTTP2Opts.Host) == 0 {
option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com")
}
case "grpc":
dialFn := func(network, addr string) (net.Conn, error) {
c, err := dialer.DialContext(context.Background(), "tcp", v.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
return c, nil
}
gunConfig := &gun.Config{
ServiceName: v.option.GrpcOpts.GrpcServiceName,
Host: v.option.ServerName,
}
tlsConfig := &tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName,
}
if v.option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr)
tlsConfig.ServerName = host
gunConfig.Host = host
}
v.gunTLSConfig = tlsConfig
v.gunConfig = gunConfig
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
}
return v, nil
} }
func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr { func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {

View File

@ -0,0 +1,24 @@
package outboundgroup
import (
"time"
"github.com/Dreamacro/clash/adapter/provider"
C "github.com/Dreamacro/clash/constant"
)
const (
defaultGetProxiesDuration = time.Second * 5
)
func getProvidersProxies(providers []provider.ProxyProvider, touch bool) []C.Proxy {
proxies := []C.Proxy{}
for _, provider := range providers {
if touch {
proxies = append(proxies, provider.ProxiesWithTouch()...)
} else {
proxies = append(proxies, provider.Proxies()...)
}
}
return proxies
}

View File

@ -4,25 +4,27 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/Dreamacro/clash/adapters/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapters/provider" "github.com/Dreamacro/clash/adapter/provider"
"github.com/Dreamacro/clash/common/singledo" "github.com/Dreamacro/clash/common/singledo"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
type Fallback struct { type Fallback struct {
*outbound.Base *outbound.Base
single *singledo.Single disableUDP bool
providers []provider.ProxyProvider single *singledo.Single
providers []provider.ProxyProvider
} }
func (f *Fallback) Now() string { func (f *Fallback) Now() string {
proxy := f.findAliveProxy() proxy := f.findAliveProxy(false)
return proxy.Name() return proxy.Name()
} }
// DialContext implements C.ProxyAdapter
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
proxy := f.findAliveProxy() proxy := f.findAliveProxy(true)
c, err := proxy.DialContext(ctx, metadata) c, err := proxy.DialContext(ctx, metadata)
if err == nil { if err == nil {
c.AppendToChains(f) c.AppendToChains(f)
@ -30,8 +32,9 @@ func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Con
return c, err return c, err
} }
// DialUDP implements C.ProxyAdapter
func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
proxy := f.findAliveProxy() proxy := f.findAliveProxy(true)
pc, err := proxy.DialUDP(metadata) pc, err := proxy.DialUDP(metadata)
if err == nil { if err == nil {
pc.AppendToChains(f) pc.AppendToChains(f)
@ -39,14 +42,20 @@ func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
return pc, err return pc, err
} }
// SupportUDP implements C.ProxyAdapter
func (f *Fallback) SupportUDP() bool { func (f *Fallback) SupportUDP() bool {
proxy := f.findAliveProxy() if f.disableUDP {
return false
}
proxy := f.findAliveProxy(false)
return proxy.SupportUDP() return proxy.SupportUDP()
} }
// MarshalJSON implements C.ProxyAdapter
func (f *Fallback) MarshalJSON() ([]byte, error) { func (f *Fallback) MarshalJSON() ([]byte, error) {
var all []string var all []string
for _, proxy := range f.proxies() { for _, proxy := range f.proxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]interface{}{ return json.Marshal(map[string]interface{}{
@ -56,34 +65,36 @@ func (f *Fallback) MarshalJSON() ([]byte, error) {
}) })
} }
// Unwrap implements C.ProxyAdapter
func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy { func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
proxy := f.findAliveProxy() proxy := f.findAliveProxy(true)
return proxy return proxy
} }
func (f *Fallback) proxies() []C.Proxy { func (f *Fallback) proxies(touch bool) []C.Proxy {
elm, _, _ := f.single.Do(func() (interface{}, error) { elm, _, _ := f.single.Do(func() (interface{}, error) {
return getProvidersProxies(f.providers), nil return getProvidersProxies(f.providers, touch), nil
}) })
return elm.([]C.Proxy) return elm.([]C.Proxy)
} }
func (f *Fallback) findAliveProxy() C.Proxy { func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
proxies := f.proxies() proxies := f.proxies(touch)
for _, proxy := range proxies { for _, proxy := range proxies {
if proxy.Alive() { if proxy.Alive() {
return proxy return proxy
} }
} }
return f.proxies()[0] return proxies[0]
} }
func NewFallback(name string, providers []provider.ProxyProvider) *Fallback { func NewFallback(options *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
return &Fallback{ return &Fallback{
Base: outbound.NewBase(name, "", C.Fallback, false), Base: outbound.NewBase(options.Name, "", C.Fallback, false),
single: singledo.NewSingle(defaultGetProxiesDuration), single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers, providers: providers,
disableUDP: options.DisableUDP,
} }
} }

View File

@ -0,0 +1,179 @@
package outboundgroup
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapter/provider"
"github.com/Dreamacro/clash/common/murmur3"
"github.com/Dreamacro/clash/common/singledo"
C "github.com/Dreamacro/clash/constant"
"golang.org/x/net/publicsuffix"
)
type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy
type LoadBalance struct {
*outbound.Base
disableUDP bool
single *singledo.Single
providers []provider.ProxyProvider
strategyFn strategyFn
}
var errStrategy = errors.New("unsupported strategy")
func parseStrategy(config map[string]interface{}) string {
if elm, ok := config["strategy"]; ok {
if strategy, ok := elm.(string); ok {
return strategy
}
}
return "consistent-hashing"
}
func getKey(metadata *C.Metadata) string {
if metadata.Host != "" {
// ip host
if ip := net.ParseIP(metadata.Host); ip != nil {
return metadata.Host
}
if etld, err := publicsuffix.EffectiveTLDPlusOne(metadata.Host); err == nil {
return etld
}
}
if metadata.DstIP == nil {
return ""
}
return metadata.DstIP.String()
}
func jumpHash(key uint64, buckets int32) int32 {
var b, j int64
for j < int64(buckets) {
b = j
key = key*2862933555777941757 + 1
j = int64(float64(b+1) * (float64(int64(1)<<31) / float64((key>>33)+1)))
}
return int32(b)
}
// DialContext implements C.ProxyAdapter
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
defer func() {
if err == nil {
c.AppendToChains(lb)
}
}()
proxy := lb.Unwrap(metadata)
c, err = proxy.DialContext(ctx, metadata)
return
}
// DialUDP implements C.ProxyAdapter
func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, err error) {
defer func() {
if err == nil {
pc.AppendToChains(lb)
}
}()
proxy := lb.Unwrap(metadata)
return proxy.DialUDP(metadata)
}
// SupportUDP implements C.ProxyAdapter
func (lb *LoadBalance) SupportUDP() bool {
return !lb.disableUDP
}
func strategyRoundRobin() strategyFn {
idx := 0
return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
length := len(proxies)
for i := 0; i < length; i++ {
idx = (idx + 1) % length
proxy := proxies[idx]
if proxy.Alive() {
return proxy
}
}
return proxies[0]
}
}
func strategyConsistentHashing() strategyFn {
maxRetry := 5
return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
buckets := int32(len(proxies))
for i := 0; i < maxRetry; i, key = i+1, key+1 {
idx := jumpHash(key, buckets)
proxy := proxies[idx]
if proxy.Alive() {
return proxy
}
}
return proxies[0]
}
}
// Unwrap implements C.ProxyAdapter
func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
proxies := lb.proxies(true)
return lb.strategyFn(proxies, metadata)
}
func (lb *LoadBalance) proxies(touch bool) []C.Proxy {
elm, _, _ := lb.single.Do(func() (interface{}, error) {
return getProvidersProxies(lb.providers, touch), nil
})
return elm.([]C.Proxy)
}
// MarshalJSON implements C.ProxyAdapter
func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range lb.proxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
"type": lb.Type().String(),
"all": all,
})
}
func NewLoadBalance(options *GroupCommonOption, providers []provider.ProxyProvider, strategy string) (lb *LoadBalance, err error) {
var strategyFn strategyFn
switch strategy {
case "consistent-hashing":
strategyFn = strategyConsistentHashing()
case "round-robin":
strategyFn = strategyRoundRobin()
default:
return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
}
return &LoadBalance{
Base: outbound.NewBase(options.Name, "", C.LoadBalance, false),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
strategyFn: strategyFn,
disableUDP: options.DisableUDP,
}, nil
}

View File

@ -4,7 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/Dreamacro/clash/adapters/provider" "github.com/Dreamacro/clash/adapter/provider"
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
@ -12,25 +12,28 @@ import (
var ( var (
errFormat = errors.New("format error") errFormat = errors.New("format error")
errType = errors.New("unsupport type") errType = errors.New("unsupport type")
errMissUse = errors.New("`use` field should not be empty")
errMissProxy = errors.New("`use` or `proxies` missing") errMissProxy = errors.New("`use` or `proxies` missing")
errMissHealthCheck = errors.New("`url` or `interval` missing") errMissHealthCheck = errors.New("`url` or `interval` missing")
errDuplicateProvider = errors.New("`duplicate provider name") errDuplicateProvider = errors.New("`duplicate provider name")
) )
type GroupCommonOption struct { type GroupCommonOption struct {
Name string `group:"name"` Name string `group:"name"`
Type string `group:"type"` Type string `group:"type"`
Proxies []string `group:"proxies,omitempty"` Proxies []string `group:"proxies,omitempty"`
Use []string `group:"use,omitempty"` Use []string `group:"use,omitempty"`
URL string `group:"url,omitempty"` URL string `group:"url,omitempty"`
Interval int `group:"interval,omitempty"` Interval int `group:"interval,omitempty"`
Lazy bool `group:"lazy,omitempty"`
DisableUDP bool `group:"disable-udp,omitempty"`
} }
func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, providersMap map[string]provider.ProxyProvider) (C.ProxyAdapter, error) { func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, providersMap map[string]provider.ProxyProvider) (C.ProxyAdapter, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true}) decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true})
groupOption := &GroupCommonOption{} groupOption := &GroupCommonOption{
Lazy: true,
}
if err := decoder.Decode(config, groupOption); err != nil { if err := decoder.Decode(config, groupOption); err != nil {
return nil, errFormat return nil, errFormat
} }
@ -55,7 +58,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
// if Use not empty, drop health check options // if Use not empty, drop health check options
if len(groupOption.Use) != 0 { if len(groupOption.Use) != 0 {
hc := provider.NewHealthCheck(ps, "", 0) hc := provider.NewHealthCheck(ps, "", 0, true)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc) pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil { if err != nil {
return nil, err return nil, err
@ -63,9 +66,13 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
providers = append(providers, pd) providers = append(providers, pd)
} else { } else {
if _, ok := providersMap[groupName]; ok {
return nil, errDuplicateProvider
}
// select don't need health check // select don't need health check
if groupOption.Type == "select" || groupOption.Type == "relay" { if groupOption.Type == "select" || groupOption.Type == "relay" {
hc := provider.NewHealthCheck(ps, "", 0) hc := provider.NewHealthCheck(ps, "", 0, true)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc) pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil { if err != nil {
return nil, err return nil, err
@ -78,7 +85,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
return nil, errMissHealthCheck return nil, errMissHealthCheck
} }
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval)) hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc) pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil { if err != nil {
return nil, err return nil, err
@ -102,15 +109,16 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
switch groupOption.Type { switch groupOption.Type {
case "url-test": case "url-test":
opts := parseURLTestOption(config) opts := parseURLTestOption(config)
group = NewURLTest(groupName, providers, opts...) group = NewURLTest(groupOption, providers, opts...)
case "select": case "select":
group = NewSelector(groupName, providers) group = NewSelector(groupOption, providers)
case "fallback": case "fallback":
group = NewFallback(groupName, providers) group = NewFallback(groupOption, providers)
case "load-balance": case "load-balance":
group = NewLoadBalance(groupName, providers) strategy := parseStrategy(config)
return NewLoadBalance(groupOption, providers, strategy)
case "relay": case "relay":
group = NewRelay(groupName, providers) group = NewRelay(groupOption, providers)
default: default:
return nil, fmt.Errorf("%w: %s", errType, groupOption.Type) return nil, fmt.Errorf("%w: %s", errType, groupOption.Type)
} }

View File

@ -6,8 +6,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/Dreamacro/clash/adapters/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapters/provider" "github.com/Dreamacro/clash/adapter/provider"
"github.com/Dreamacro/clash/common/singledo" "github.com/Dreamacro/clash/common/singledo"
"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,10 +19,11 @@ type Relay struct {
providers []provider.ProxyProvider providers []provider.ProxyProvider
} }
// DialContext implements C.ProxyAdapter
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
proxies := r.proxies(metadata) proxies := r.proxies(metadata, true)
if len(proxies) == 0 { if len(proxies) == 0 {
return nil, errors.New("Proxy does not exist") return nil, errors.New("proxy does not exist")
} }
first := proxies[0] first := proxies[0]
last := proxies[len(proxies)-1] last := proxies[len(proxies)-1]
@ -56,9 +57,10 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
return outbound.NewConn(c, r), nil return outbound.NewConn(c, r), nil
} }
// MarshalJSON implements C.ProxyAdapter
func (r *Relay) MarshalJSON() ([]byte, error) { func (r *Relay) MarshalJSON() ([]byte, error) {
var all []string var all []string
for _, proxy := range r.rawProxies() { for _, proxy := range r.rawProxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]interface{}{ return json.Marshal(map[string]interface{}{
@ -67,16 +69,16 @@ func (r *Relay) MarshalJSON() ([]byte, error) {
}) })
} }
func (r *Relay) rawProxies() []C.Proxy { func (r *Relay) rawProxies(touch bool) []C.Proxy {
elm, _, _ := r.single.Do(func() (interface{}, error) { elm, _, _ := r.single.Do(func() (interface{}, error) {
return getProvidersProxies(r.providers), nil return getProvidersProxies(r.providers, touch), nil
}) })
return elm.([]C.Proxy) return elm.([]C.Proxy)
} }
func (r *Relay) proxies(metadata *C.Metadata) []C.Proxy { func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
proxies := r.rawProxies() proxies := r.rawProxies(touch)
for n, proxy := range proxies { for n, proxy := range proxies {
subproxy := proxy.Unwrap(metadata) subproxy := proxy.Unwrap(metadata)
@ -89,9 +91,9 @@ func (r *Relay) proxies(metadata *C.Metadata) []C.Proxy {
return proxies return proxies
} }
func NewRelay(name string, providers []provider.ProxyProvider) *Relay { func NewRelay(options *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
return &Relay{ return &Relay{
Base: outbound.NewBase(name, "", C.Relay, false), Base: outbound.NewBase(options.Name, "", C.Relay, false),
single: singledo.NewSingle(defaultGetProxiesDuration), single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers, providers: providers,
} }

View File

@ -5,42 +5,51 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"github.com/Dreamacro/clash/adapters/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapters/provider" "github.com/Dreamacro/clash/adapter/provider"
"github.com/Dreamacro/clash/common/singledo" "github.com/Dreamacro/clash/common/singledo"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
type Selector struct { type Selector struct {
*outbound.Base *outbound.Base
single *singledo.Single disableUDP bool
selected string single *singledo.Single
providers []provider.ProxyProvider selected string
providers []provider.ProxyProvider
} }
// DialContext implements C.ProxyAdapter
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
c, err := s.selectedProxy().DialContext(ctx, metadata) c, err := s.selectedProxy(true).DialContext(ctx, metadata)
if err == nil { if err == nil {
c.AppendToChains(s) c.AppendToChains(s)
} }
return c, err return c, err
} }
// DialUDP implements C.ProxyAdapter
func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := s.selectedProxy().DialUDP(metadata) pc, err := s.selectedProxy(true).DialUDP(metadata)
if err == nil { if err == nil {
pc.AppendToChains(s) pc.AppendToChains(s)
} }
return pc, err return pc, err
} }
// SupportUDP implements C.ProxyAdapter
func (s *Selector) SupportUDP() bool { func (s *Selector) SupportUDP() bool {
return s.selectedProxy().SupportUDP() if s.disableUDP {
return false
}
return s.selectedProxy(false).SupportUDP()
} }
// MarshalJSON implements C.ProxyAdapter
func (s *Selector) MarshalJSON() ([]byte, error) { func (s *Selector) MarshalJSON() ([]byte, error) {
var all []string var all []string
for _, proxy := range getProvidersProxies(s.providers) { for _, proxy := range getProvidersProxies(s.providers, false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
@ -52,11 +61,11 @@ func (s *Selector) MarshalJSON() ([]byte, error) {
} }
func (s *Selector) Now() string { func (s *Selector) Now() string {
return s.selectedProxy().Name() return s.selectedProxy(false).Name()
} }
func (s *Selector) Set(name string) error { func (s *Selector) Set(name string) error {
for _, proxy := range getProvidersProxies(s.providers) { for _, proxy := range getProvidersProxies(s.providers, false) {
if proxy.Name() == name { if proxy.Name() == name {
s.selected = name s.selected = name
s.single.Reset() s.single.Reset()
@ -64,16 +73,17 @@ func (s *Selector) Set(name string) error {
} }
} }
return errors.New("Proxy does not exist") return errors.New("proxy not exist")
} }
// Unwrap implements C.ProxyAdapter
func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy { func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy {
return s.selectedProxy() return s.selectedProxy(true)
} }
func (s *Selector) selectedProxy() C.Proxy { func (s *Selector) selectedProxy(touch bool) C.Proxy {
elm, _, _ := s.single.Do(func() (interface{}, error) { elm, _, _ := s.single.Do(func() (interface{}, error) {
proxies := getProvidersProxies(s.providers) proxies := getProvidersProxies(s.providers, touch)
for _, proxy := range proxies { for _, proxy := range proxies {
if proxy.Name() == s.selected { if proxy.Name() == s.selected {
return proxy, nil return proxy, nil
@ -86,12 +96,13 @@ func (s *Selector) selectedProxy() C.Proxy {
return elm.(C.Proxy) return elm.(C.Proxy)
} }
func NewSelector(name string, providers []provider.ProxyProvider) *Selector { func NewSelector(options *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
selected := providers[0].Proxies()[0].Name() selected := providers[0].Proxies()[0].Name()
return &Selector{ return &Selector{
Base: outbound.NewBase(name, "", C.Selector, false), Base: outbound.NewBase(options.Name, "", C.Selector, false),
single: singledo.NewSingle(defaultGetProxiesDuration), single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers, providers: providers,
selected: selected, selected: selected,
disableUDP: options.DisableUDP,
} }
} }

View File

@ -5,8 +5,8 @@ import (
"encoding/json" "encoding/json"
"time" "time"
"github.com/Dreamacro/clash/adapters/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapters/provider" "github.com/Dreamacro/clash/adapter/provider"
"github.com/Dreamacro/clash/common/singledo" "github.com/Dreamacro/clash/common/singledo"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
@ -22,7 +22,7 @@ func urlTestWithTolerance(tolerance uint16) urlTestOption {
type URLTest struct { type URLTest struct {
*outbound.Base *outbound.Base
tolerance uint16 tolerance uint16
lastDelay uint16 disableUDP bool
fastNode C.Proxy fastNode C.Proxy
single *singledo.Single single *singledo.Single
fastSingle *singledo.Single fastSingle *singledo.Single
@ -30,50 +30,52 @@ type URLTest struct {
} }
func (u *URLTest) Now() string { func (u *URLTest) Now() string {
return u.fast().Name() return u.fast(false).Name()
} }
// DialContext implements C.ProxyAdapter
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) { func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
c, err = u.fast().DialContext(ctx, metadata) c, err = u.fast(true).DialContext(ctx, metadata)
if err == nil { if err == nil {
c.AppendToChains(u) c.AppendToChains(u)
} }
return c, err return c, err
} }
// DialUDP implements C.ProxyAdapter
func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := u.fast().DialUDP(metadata) pc, err := u.fast(true).DialUDP(metadata)
if err == nil { if err == nil {
pc.AppendToChains(u) pc.AppendToChains(u)
} }
return pc, err return pc, err
} }
// Unwrap implements C.ProxyAdapter
func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy { func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy {
return u.fast() return u.fast(true)
} }
func (u *URLTest) proxies() []C.Proxy { func (u *URLTest) proxies(touch bool) []C.Proxy {
elm, _, _ := u.single.Do(func() (interface{}, error) { elm, _, _ := u.single.Do(func() (interface{}, error) {
return getProvidersProxies(u.providers), nil return getProvidersProxies(u.providers, touch), nil
}) })
return elm.([]C.Proxy) return elm.([]C.Proxy)
} }
func (u *URLTest) fast() C.Proxy { func (u *URLTest) fast(touch bool) C.Proxy {
elm, _, _ := u.fastSingle.Do(func() (interface{}, error) { elm, _, _ := u.fastSingle.Do(func() (interface{}, error) {
// tolerance proxies := u.proxies(touch)
if u.tolerance != 0 && u.fastNode != nil {
if u.fastNode.LastDelay() < u.lastDelay+u.tolerance {
return u.fastNode, nil
}
}
proxies := u.proxies()
fast := proxies[0] fast := proxies[0]
min := fast.LastDelay() min := fast.LastDelay()
fastNotExist := true
for _, proxy := range proxies[1:] { for _, proxy := range proxies[1:] {
if u.fastNode != nil && proxy.Name() == u.fastNode.Name() {
fastNotExist = false
}
if !proxy.Alive() { if !proxy.Alive() {
continue continue
} }
@ -85,21 +87,30 @@ func (u *URLTest) fast() C.Proxy {
} }
} }
u.fastNode = fast // tolerance
u.lastDelay = fast.LastDelay() if u.fastNode == nil || fastNotExist || !u.fastNode.Alive() || u.fastNode.LastDelay() > fast.LastDelay()+u.tolerance {
return fast, nil u.fastNode = fast
}
return u.fastNode, nil
}) })
return elm.(C.Proxy) return elm.(C.Proxy)
} }
// SupportUDP implements C.ProxyAdapter
func (u *URLTest) SupportUDP() bool { func (u *URLTest) SupportUDP() bool {
return u.fast().SupportUDP() if u.disableUDP {
return false
}
return u.fast(false).SupportUDP()
} }
// MarshalJSON implements C.ProxyAdapter
func (u *URLTest) MarshalJSON() ([]byte, error) { func (u *URLTest) MarshalJSON() ([]byte, error) {
var all []string var all []string
for _, proxy := range u.proxies() { for _, proxy := range u.proxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]interface{}{ return json.Marshal(map[string]interface{}{
@ -122,12 +133,13 @@ func parseURLTestOption(config map[string]interface{}) []urlTestOption {
return opts return opts
} }
func NewURLTest(name string, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest { func NewURLTest(commonOptions *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
urlTest := &URLTest{ urlTest := &URLTest{
Base: outbound.NewBase(name, "", C.URLTest, false), Base: outbound.NewBase(commonOptions.Name, "", C.URLTest, false),
single: singledo.NewSingle(defaultGetProxiesDuration), single: singledo.NewSingle(defaultGetProxiesDuration),
fastSingle: singledo.NewSingle(time.Second * 10), fastSingle: singledo.NewSingle(time.Second * 10),
providers: providers, providers: providers,
disableUDP: commonOptions.DisableUDP,
} }
for _, option := range options { for _, option := range options {

View File

@ -16,25 +16,7 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
} }
ip := net.ParseIP(host) ip := net.ParseIP(host)
if ip != nil { if ip == nil {
if ip.To4() != nil {
addr = &C.Metadata{
AddrType: C.AtypIPv4,
Host: "",
DstIP: ip,
DstPort: port,
}
return
} else {
addr = &C.Metadata{
AddrType: C.AtypIPv6,
Host: "",
DstIP: ip,
DstPort: port,
}
return
}
} else {
addr = &C.Metadata{ addr = &C.Metadata{
AddrType: C.AtypDomainName, AddrType: C.AtypDomainName,
Host: host, Host: host,
@ -42,7 +24,23 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
DstPort: port, DstPort: port,
} }
return return
} else if ip4 := ip.To4(); ip4 != nil {
addr = &C.Metadata{
AddrType: C.AtypIPv4,
Host: "",
DstIP: ip4,
DstPort: port,
}
return
} }
addr = &C.Metadata{
AddrType: C.AtypIPv6,
Host: "",
DstIP: ip,
DstPort: port,
}
return
} }
func tcpKeepAlive(c net.Conn) { func tcpKeepAlive(c net.Conn) {

View File

@ -1,8 +1,9 @@
package outbound package adapter
import ( import (
"fmt" "fmt"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
@ -11,36 +12,45 @@ func ParseProxy(mapping map[string]interface{}) (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 {
return nil, fmt.Errorf("Missing type") return nil, fmt.Errorf("missing type")
} }
var proxy C.ProxyAdapter var (
err := fmt.Errorf("Cannot parse") proxy C.ProxyAdapter
err error
)
switch proxyType { switch proxyType {
case "ss": case "ss":
ssOption := &ShadowSocksOption{} ssOption := &outbound.ShadowSocksOption{}
err = decoder.Decode(mapping, ssOption) err = decoder.Decode(mapping, ssOption)
if err != nil { if err != nil {
break break
} }
proxy, err = NewShadowSocks(*ssOption) proxy, err = outbound.NewShadowSocks(*ssOption)
case "ssr":
ssrOption := &outbound.ShadowSocksROption{}
err = decoder.Decode(mapping, ssrOption)
if err != nil {
break
}
proxy, err = outbound.NewShadowSocksR(*ssrOption)
case "socks5": case "socks5":
socksOption := &Socks5Option{} socksOption := &outbound.Socks5Option{}
err = decoder.Decode(mapping, socksOption) err = decoder.Decode(mapping, socksOption)
if err != nil { if err != nil {
break break
} }
proxy = NewSocks5(*socksOption) proxy = outbound.NewSocks5(*socksOption)
case "http": case "http":
httpOption := &HttpOption{} httpOption := &outbound.HttpOption{}
err = decoder.Decode(mapping, httpOption) err = decoder.Decode(mapping, httpOption)
if err != nil { if err != nil {
break break
} }
proxy = NewHttp(*httpOption) proxy = outbound.NewHttp(*httpOption)
case "vmess": case "vmess":
vmessOption := &VmessOption{ vmessOption := &outbound.VmessOption{
HTTPOpts: HTTPOptions{ HTTPOpts: outbound.HTTPOptions{
Method: "GET", Method: "GET",
Path: []string{"/"}, Path: []string{"/"},
}, },
@ -49,23 +59,23 @@ func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
if err != nil { if err != nil {
break break
} }
proxy, err = NewVmess(*vmessOption) proxy, err = outbound.NewVmess(*vmessOption)
case "snell": case "snell":
snellOption := &SnellOption{} snellOption := &outbound.SnellOption{}
err = decoder.Decode(mapping, snellOption) err = decoder.Decode(mapping, snellOption)
if err != nil { if err != nil {
break break
} }
proxy, err = NewSnell(*snellOption) proxy, err = outbound.NewSnell(*snellOption)
case "trojan": case "trojan":
trojanOption := &TrojanOption{} trojanOption := &outbound.TrojanOption{}
err = decoder.Decode(mapping, trojanOption) err = decoder.Decode(mapping, trojanOption)
if err != nil { if err != nil {
break break
} }
proxy, err = 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)
} }
if err != nil { if err != nil {

View File

@ -23,6 +23,7 @@ type fetcher struct {
vehicle Vehicle vehicle Vehicle
updatedAt *time.Time updatedAt *time.Time
ticker *time.Ticker ticker *time.Ticker
done chan struct{}
hash [16]byte hash [16]byte
parser parser parser parser
onUpdate func(interface{}) onUpdate func(interface{})
@ -71,10 +72,14 @@ func (f *fetcher) Initial() (interface{}, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
isLocal = false
} }
if err := safeWrite(f.vehicle.Path(), buf); err != nil { if f.vehicle.Type() != File && !isLocal {
return nil, err if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return nil, err
}
} }
f.hash = md5.Sum(buf) f.hash = md5.Sum(buf)
@ -105,8 +110,10 @@ func (f *fetcher) Update() (interface{}, bool, error) {
return nil, false, err return nil, false, err
} }
if err := safeWrite(f.vehicle.Path(), buf); err != nil { if f.vehicle.Type() != File {
return nil, false, err if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return nil, false, err
}
} }
f.updatedAt = &now f.updatedAt = &now
@ -117,27 +124,33 @@ func (f *fetcher) Update() (interface{}, bool, error) {
func (f *fetcher) Destroy() error { func (f *fetcher) Destroy() error {
if f.ticker != nil { if f.ticker != nil {
f.ticker.Stop() f.done <- struct{}{}
} }
return nil return nil
} }
func (f *fetcher) pullLoop() { func (f *fetcher) pullLoop() {
for range f.ticker.C { for {
elm, same, err := f.Update() select {
if err != nil { case <-f.ticker.C:
log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error()) elm, same, err := f.Update()
continue if err != nil {
} log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error())
continue
}
if same { if same {
log.Debugln("[Provider] %s's proxies doesn't change", f.Name()) log.Debugln("[Provider] %s's proxies doesn't change", f.Name())
continue continue
} }
log.Infoln("[Provider] %s's proxies update", f.Name()) log.Infoln("[Provider] %s's proxies update", f.Name())
if f.onUpdate != nil { if f.onUpdate != nil {
f.onUpdate(elm) f.onUpdate(elm)
}
case <-f.done:
f.ticker.Stop()
return
} }
} }
} }
@ -165,6 +178,7 @@ func newFetcher(name string, interval time.Duration, vehicle Vehicle, parser par
ticker: ticker, ticker: ticker,
vehicle: vehicle, vehicle: vehicle,
parser: parser, parser: parser,
done: make(chan struct{}, 1),
onUpdate: onUpdate, onUpdate: onUpdate,
} }
} }

View File

@ -2,9 +2,12 @@ package provider
import ( import (
"context" "context"
"sync"
"time" "time"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"go.uber.org/atomic"
) )
const ( const (
@ -17,10 +20,12 @@ type HealthCheckOption struct {
} }
type HealthCheck struct { type HealthCheck struct {
url string url string
proxies []C.Proxy proxies []C.Proxy
interval uint interval uint
done chan struct{} lazy bool
lastTouch *atomic.Int64
done chan struct{}
} }
func (hc *HealthCheck) process() { func (hc *HealthCheck) process() {
@ -30,7 +35,10 @@ func (hc *HealthCheck) process() {
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
hc.check() now := time.Now().Unix()
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
hc.check()
}
case <-hc.done: case <-hc.done:
ticker.Stop() ticker.Stop()
return return
@ -46,13 +54,24 @@ func (hc *HealthCheck) auto() bool {
return hc.interval != 0 return hc.interval != 0
} }
func (hc *HealthCheck) touch() {
hc.lastTouch.Store(time.Now().Unix())
}
func (hc *HealthCheck) check() { func (hc *HealthCheck) check() {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout) ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
wg := &sync.WaitGroup{}
for _, proxy := range hc.proxies { for _, proxy := range hc.proxies {
go proxy.URLTest(ctx, hc.url) wg.Add(1)
go func(p C.Proxy) {
p.URLTest(ctx, hc.url)
wg.Done()
}(proxy)
} }
<-ctx.Done() wg.Wait()
cancel() cancel()
} }
@ -60,11 +79,13 @@ func (hc *HealthCheck) close() {
hc.done <- struct{}{} hc.done <- struct{}{}
} }
func NewHealthCheck(proxies []C.Proxy, url string, interval uint) *HealthCheck { func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool) *HealthCheck {
return &HealthCheck{ return &HealthCheck{
proxies: proxies, proxies: proxies,
url: url, url: url,
interval: interval, interval: interval,
done: make(chan struct{}, 1), lazy: lazy,
lastTouch: atomic.NewInt64(0),
done: make(chan struct{}, 1),
} }
} }

View File

@ -17,6 +17,7 @@ type healthCheckSchema struct {
Enable bool `provider:"enable"` Enable bool `provider:"enable"`
URL string `provider:"url"` URL string `provider:"url"`
Interval int `provider:"interval"` Interval int `provider:"interval"`
Lazy bool `provider:"lazy,omitempty"`
} }
type proxyProviderSchema struct { type proxyProviderSchema struct {
@ -30,16 +31,20 @@ type proxyProviderSchema struct {
func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvider, error) { func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvider, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true}) decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
schema := &proxyProviderSchema{} schema := &proxyProviderSchema{
HealthCheck: healthCheckSchema{
Lazy: true,
},
}
if err := decoder.Decode(mapping, schema); err != nil { if err := decoder.Decode(mapping, schema); err != nil {
return nil, err return nil, err
} }
var hcInterval uint = 0 var hcInterval uint
if schema.HealthCheck.Enable { if schema.HealthCheck.Enable {
hcInterval = uint(schema.HealthCheck.Interval) hcInterval = uint(schema.HealthCheck.Interval)
} }
hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval) hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval, schema.HealthCheck.Lazy)
path := C.Path.Resolve(schema.Path) path := C.Path.Resolve(schema.Path)

View File

@ -7,7 +7,7 @@ import (
"runtime" "runtime"
"time" "time"
"github.com/Dreamacro/clash/adapters/outbound" "github.com/Dreamacro/clash/adapter"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
@ -50,6 +50,9 @@ type Provider interface {
type ProxyProvider interface { type ProxyProvider interface {
Provider Provider
Proxies() []C.Proxy Proxies() []C.Proxy
// ProxiesWithTouch is used to inform the provider that the proxy is actually being used while getting the list of proxies.
// Commonly used in Dial and DialUDP
ProxiesWithTouch() []C.Proxy
HealthCheck() HealthCheck()
} }
@ -112,6 +115,11 @@ func (pp *proxySetProvider) Proxies() []C.Proxy {
return pp.proxies return pp.proxies
} }
func (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy {
pp.healthCheck.touch()
return pp.Proxies()
}
func proxiesParse(buf []byte) (interface{}, error) { func proxiesParse(buf []byte) (interface{}, error) {
schema := &ProxySchema{} schema := &ProxySchema{}
@ -120,20 +128,20 @@ func proxiesParse(buf []byte) (interface{}, error) {
} }
if schema.Proxies == nil { if schema.Proxies == nil {
return nil, errors.New("File must have a `proxies` field") return nil, errors.New("file must have a `proxies` field")
} }
proxies := []C.Proxy{} proxies := []C.Proxy{}
for idx, mapping := range schema.Proxies { for idx, mapping := range schema.Proxies {
proxy, err := outbound.ParseProxy(mapping) proxy, err := adapter.ParseProxy(mapping)
if err != nil { if err != nil {
return nil, fmt.Errorf("Proxy %d error: %w", idx, err) return nil, fmt.Errorf("proxy %d error: %w", idx, err)
} }
proxies = append(proxies, proxy) proxies = append(proxies, proxy)
} }
if len(proxies) == 0 { if len(proxies) == 0 {
return nil, errors.New("File doesn't have any valid proxy") return nil, errors.New("file doesn't have any valid proxy")
} }
return proxies, nil return proxies, nil
@ -223,6 +231,11 @@ func (cp *compatibleProvider) Proxies() []C.Proxy {
return cp.proxies return cp.proxies
} }
func (cp *compatibleProvider) ProxiesWithTouch() []C.Proxy {
cp.healthCheck.touch()
return cp.Proxies()
}
func stopCompatibleProvider(pd *CompatibleProvider) { func stopCompatibleProvider(pd *CompatibleProvider) {
pd.healthCheck.close() pd.healthCheck.close()
} }

View File

@ -107,6 +107,7 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close()
buf, err := ioutil.ReadAll(resp.Body) buf, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {

View File

@ -1,60 +0,0 @@
package inbound
import (
"net"
"net/http"
"strings"
C "github.com/Dreamacro/clash/constant"
)
// HTTPAdapter is a adapter for HTTP connection
type HTTPAdapter struct {
net.Conn
metadata *C.Metadata
R *http.Request
}
// Metadata return destination metadata
func (h *HTTPAdapter) Metadata() *C.Metadata {
return h.metadata
}
// NewHTTP is HTTPAdapter generator
func NewHTTP(request *http.Request, conn net.Conn) *HTTPAdapter {
metadata := parseHTTPAddr(request)
metadata.Type = C.HTTP
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
return &HTTPAdapter{
metadata: metadata,
R: request,
Conn: conn,
}
}
// RemoveHopByHopHeaders remove hop-by-hop header
func RemoveHopByHopHeaders(header http.Header) {
// Strip hop-by-hop header based on RFC:
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
// https://www.mnot.net/blog/2011/07/11/what_proxies_must_do
header.Del("Proxy-Connection")
header.Del("Proxy-Authenticate")
header.Del("Proxy-Authorization")
header.Del("TE")
header.Del("Trailers")
header.Del("Transfer-Encoding")
header.Del("Upgrade")
connections := header.Get("Connection")
header.Del("Connection")
if len(connections) == 0 {
return
}
for _, h := range strings.Split(connections, ",") {
header.Del(strings.TrimSpace(h))
}
}

View File

@ -1,35 +0,0 @@
package inbound
import (
"net"
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant"
)
// SocketAdapter is a adapter for socks and redir connection
type SocketAdapter struct {
net.Conn
metadata *C.Metadata
}
// Metadata return destination metadata
func (s *SocketAdapter) Metadata() *C.Metadata {
return s.metadata
}
// NewSocket is SocketAdapter generator
func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *SocketAdapter {
metadata := parseSocksAddr(target)
metadata.NetWork = C.TCP
metadata.Type = source
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
return &SocketAdapter{
Conn: conn,
metadata: metadata,
}
}

View File

@ -1,78 +0,0 @@
package outbound
import (
"context"
"fmt"
"net"
"strconv"
"github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/component/dialer"
obfs "github.com/Dreamacro/clash/component/simple-obfs"
"github.com/Dreamacro/clash/component/snell"
C "github.com/Dreamacro/clash/constant"
)
type Snell struct {
*Base
psk []byte
obfsOption *simpleObfsOption
}
type SnellOption struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Psk string `proxy:"psk"`
ObfsOpts map[string]interface{} `proxy:"obfs-opts,omitempty"`
}
func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
switch s.obfsOption.Mode {
case "tls":
c = obfs.NewTLSObfs(c, s.obfsOption.Host)
case "http":
_, port, _ := net.SplitHostPort(s.addr)
c = obfs.NewHTTPObfs(c, s.obfsOption.Host, port)
}
c = snell.StreamConn(c, s.psk)
port, _ := strconv.Atoi(metadata.DstPort)
err := snell.WriteHeader(c, metadata.String(), uint(port))
return c, err
}
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
c, err := dialer.DialContext(ctx, "tcp", s.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
}
tcpKeepAlive(c)
c, err = s.StreamConn(c, metadata)
return NewConn(c, s), err
}
func NewSnell(option SnellOption) (*Snell, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
psk := []byte(option.Psk)
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
obfsOption := &simpleObfsOption{Host: "bing.com"}
if err := decoder.Decode(option.ObfsOpts, obfsOption); err != nil {
return nil, fmt.Errorf("snell %s initialize obfs error: %w", addr, err)
}
if obfsOption.Mode != "tls" && obfsOption.Mode != "http" {
return nil, fmt.Errorf("snell %s obfs mode error: %s", addr, obfsOption.Mode)
}
return &Snell{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Snell,
},
psk: psk,
obfsOption: obfsOption,
}, nil
}

View File

@ -1,107 +0,0 @@
package outbound
import (
"context"
"encoding/json"
"fmt"
"net"
"strconv"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/trojan"
C "github.com/Dreamacro/clash/constant"
)
type Trojan struct {
*Base
instance *trojan.Trojan
}
type TrojanOption struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Password string `proxy:"password"`
ALPN []string `proxy:"alpn,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
UDP bool `proxy:"udp,omitempty"`
}
func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c, err := t.instance.StreamConn(c)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata))
return c, err
}
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
c, err := dialer.DialContext(ctx, "tcp", t.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
tcpKeepAlive(c)
c, err = t.StreamConn(c, metadata)
if err != nil {
return nil, err
}
return NewConn(c, t), err
}
func (t *Trojan) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
defer cancel()
c, err := dialer.DialContext(ctx, "tcp", t.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
tcpKeepAlive(c)
c, err = t.instance.StreamConn(c)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
if err != nil {
return nil, err
}
pc := t.instance.PacketConn(c)
return newPacketConn(pc, t), err
}
func (t *Trojan) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": t.Type().String(),
})
}
func NewTrojan(option TrojanOption) (*Trojan, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
tOption := &trojan.Option{
Password: option.Password,
ALPN: option.ALPN,
ServerName: option.Server,
SkipCertVerify: option.SkipCertVerify,
ClientSessionCache: getClientSessionCache(),
}
if option.SNI != "" {
tOption.ServerName = option.SNI
}
return &Trojan{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Trojan,
udp: option.UDP,
},
instance: trojan.New(tOption),
}, nil
}

View File

@ -1,20 +0,0 @@
package outboundgroup
import (
"time"
"github.com/Dreamacro/clash/adapters/provider"
C "github.com/Dreamacro/clash/constant"
)
const (
defaultGetProxiesDuration = time.Second * 5
)
func getProvidersProxies(providers []provider.ProxyProvider) []C.Proxy {
proxies := []C.Proxy{}
for _, provider := range providers {
proxies = append(proxies, provider.Proxies()...)
}
return proxies
}

View File

@ -1,125 +0,0 @@
package outboundgroup
import (
"context"
"encoding/json"
"net"
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/common/murmur3"
"github.com/Dreamacro/clash/common/singledo"
C "github.com/Dreamacro/clash/constant"
"golang.org/x/net/publicsuffix"
)
type LoadBalance struct {
*outbound.Base
single *singledo.Single
maxRetry int
providers []provider.ProxyProvider
}
func getKey(metadata *C.Metadata) string {
if metadata.Host != "" {
// ip host
if ip := net.ParseIP(metadata.Host); ip != nil {
return metadata.Host
}
if etld, err := publicsuffix.EffectiveTLDPlusOne(metadata.Host); err == nil {
return etld
}
}
if metadata.DstIP == nil {
return ""
}
return metadata.DstIP.String()
}
func jumpHash(key uint64, buckets int32) int32 {
var b, j int64
for j < int64(buckets) {
b = j
key = key*2862933555777941757 + 1
j = int64(float64(b+1) * (float64(int64(1)<<31) / float64((key>>33)+1)))
}
return int32(b)
}
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
defer func() {
if err == nil {
c.AppendToChains(lb)
}
}()
proxy := lb.Unwrap(metadata)
c, err = proxy.DialContext(ctx, metadata)
return
}
func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, err error) {
defer func() {
if err == nil {
pc.AppendToChains(lb)
}
}()
proxy := lb.Unwrap(metadata)
return proxy.DialUDP(metadata)
}
func (lb *LoadBalance) SupportUDP() bool {
return true
}
func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
proxies := lb.proxies()
buckets := int32(len(proxies))
for i := 0; i < lb.maxRetry; i, key = i+1, key+1 {
idx := jumpHash(key, buckets)
proxy := proxies[idx]
if proxy.Alive() {
return proxy
}
}
return proxies[0]
}
func (lb *LoadBalance) proxies() []C.Proxy {
elm, _, _ := lb.single.Do(func() (interface{}, error) {
return getProvidersProxies(lb.providers), nil
})
return elm.([]C.Proxy)
}
func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range lb.proxies() {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
"type": lb.Type().String(),
"all": all,
})
}
func NewLoadBalance(name string, providers []provider.ProxyProvider) *LoadBalance {
return &LoadBalance{
Base: outbound.NewBase(name, "", C.LoadBalance, false),
single: singledo.NewSingle(defaultGetProxiesDuration),
maxRetry: 3,
providers: providers,
}
}

View File

@ -121,7 +121,7 @@ func (c *LruCache) Set(key interface{}, value interface{}) {
c.SetWithExpire(key, value, time.Unix(expires, 0)) c.SetWithExpire(key, value, time.Unix(expires, 0))
} }
// SetWithExpire stores the interface{} representation of a response for a given key and given exires. // SetWithExpire stores the interface{} representation of a response for a given key and given expires.
// The expires time will round to second. // The expires time will round to second.
func (c *LruCache) SetWithExpire(key interface{}, value interface{}, expires time.Time) { func (c *LruCache) SetWithExpire(key interface{}, value interface{}, expires time.Time) {
c.mu.Lock() c.mu.Lock()
@ -146,6 +146,23 @@ func (c *LruCache) SetWithExpire(key interface{}, value interface{}, expires tim
c.maybeDeleteOldest() c.maybeDeleteOldest()
} }
// CloneTo clone and overwrite elements to another LruCache
func (c *LruCache) CloneTo(n *LruCache) {
c.mu.Lock()
defer c.mu.Unlock()
n.mu.Lock()
defer n.mu.Unlock()
n.lru = list.New()
n.cache = make(map[interface{}]*list.Element)
for e := c.lru.Front(); e != nil; e = e.Next() {
elm := e.Value.(*entry)
n.cache[elm.key] = n.lru.PushBack(elm)
}
}
func (c *LruCache) get(key interface{}) *entry { func (c *LruCache) get(key interface{}) *entry {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
@ -171,7 +188,7 @@ func (c *LruCache) get(key interface{}) *entry {
} }
// Delete removes the value associated with a key. // Delete removes the value associated with a key.
func (c *LruCache) Delete(key string) { func (c *LruCache) Delete(key interface{}) {
c.mu.Lock() c.mu.Lock()
if le, ok := c.cache[key]; ok { if le, ok := c.cache[key]; ok {

View File

@ -164,3 +164,21 @@ func TestStale(t *testing.T) {
assert.Equal(t, tenSecBefore, expires) assert.Equal(t, tenSecBefore, expires)
assert.Equal(t, true, exist) assert.Equal(t, true, exist)
} }
func TestCloneTo(t *testing.T) {
o := NewLRUCache(WithSize(10))
o.Set("1", 1)
o.Set("2", 2)
n := NewLRUCache(WithSize(2))
n.Set("3", 3)
n.Set("4", 4)
o.CloneTo(n)
assert.False(t, n.Exist("3"))
assert.True(t, n.Exist("1"))
n.Set("5", 5)
assert.False(t, n.Exist("1"))
}

View File

@ -1,4 +1,4 @@
package mixed package net
import ( import (
"bufio" "bufio"

11
common/net/io.go Normal file
View File

@ -0,0 +1,11 @@
package net
import "io"
type ReadOnlyReader struct {
io.Reader
}
type WriteOnlyWriter struct {
io.Writer
}

View File

@ -1,12 +1,12 @@
package observable package observable
import ( import (
"runtime"
"sync" "sync"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"go.uber.org/atomic"
) )
func iterator(item []interface{}) chan interface{} { func iterator(item []interface{}) chan interface{} {
@ -33,25 +33,25 @@ func TestObservable(t *testing.T) {
assert.Equal(t, count, 5) assert.Equal(t, count, 5)
} }
func TestObservable_MutilSubscribe(t *testing.T) { func TestObservable_MultiSubscribe(t *testing.T) {
iter := iterator([]interface{}{1, 2, 3, 4, 5}) iter := iterator([]interface{}{1, 2, 3, 4, 5})
src := NewObservable(iter) src := NewObservable(iter)
ch1, _ := src.Subscribe() ch1, _ := src.Subscribe()
ch2, _ := src.Subscribe() ch2, _ := src.Subscribe()
count := 0 var count = atomic.NewInt32(0)
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(2) wg.Add(2)
waitCh := func(ch <-chan interface{}) { waitCh := func(ch <-chan interface{}) {
for range ch { for range ch {
count++ count.Inc()
} }
wg.Done() wg.Done()
} }
go waitCh(ch1) go waitCh(ch1)
go waitCh(ch2) go waitCh(ch2)
wg.Wait() wg.Wait()
assert.Equal(t, count, 10) assert.Equal(t, int32(10), count.Load())
} }
func TestObservable_UnSubscribe(t *testing.T) { func TestObservable_UnSubscribe(t *testing.T) {
@ -82,9 +82,6 @@ func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {
} }
func TestObservable_SubscribeGoroutineLeak(t *testing.T) { func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
// waiting for other goroutine recycle
time.Sleep(120 * time.Millisecond)
init := runtime.NumGoroutine()
iter := iterator([]interface{}{1, 2, 3, 4, 5}) iter := iterator([]interface{}{1, 2, 3, 4, 5})
src := NewObservable(iter) src := NewObservable(iter)
max := 100 max := 100
@ -107,6 +104,43 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
go waitCh(ch) go waitCh(ch)
} }
wg.Wait() wg.Wait()
now := runtime.NumGoroutine()
assert.Equal(t, init, now) for _, sub := range list {
_, more := <-sub
assert.False(t, more)
}
_, more := <-list[0]
assert.False(t, more)
}
func Benchmark_Observable_1000(b *testing.B) {
ch := make(chan interface{})
o := NewObservable(ch)
num := 1000
subs := []Subscription{}
for i := 0; i < num; i++ {
sub, _ := o.Subscribe()
subs = append(subs, sub)
}
wg := sync.WaitGroup{}
wg.Add(num)
b.ResetTimer()
for _, sub := range subs {
go func(s Subscription) {
for range s {
}
wg.Done()
}(sub)
}
for i := 0; i < b.N; i++ {
ch <- i
}
close(ch)
wg.Wait()
} }

View File

@ -2,34 +2,32 @@ package observable
import ( import (
"sync" "sync"
"gopkg.in/eapache/channels.v1"
) )
type Subscription <-chan interface{} type Subscription <-chan interface{}
type Subscriber struct { type Subscriber struct {
buffer *channels.InfiniteChannel buffer chan interface{}
once sync.Once once sync.Once
} }
func (s *Subscriber) Emit(item interface{}) { func (s *Subscriber) Emit(item interface{}) {
s.buffer.In() <- item s.buffer <- item
} }
func (s *Subscriber) Out() Subscription { func (s *Subscriber) Out() Subscription {
return s.buffer.Out() return s.buffer
} }
func (s *Subscriber) Close() { func (s *Subscriber) Close() {
s.once.Do(func() { s.once.Do(func() {
s.buffer.Close() close(s.buffer)
}) })
} }
func newSubscriber() *Subscriber { func newSubscriber() *Subscriber {
sub := &Subscriber{ sub := &Subscriber{
buffer: channels.NewInfiniteChannel(), buffer: make(chan interface{}, 200),
} }
return sub return sub
} }

View File

@ -55,11 +55,13 @@ func (alloc *Allocator) Put(buf []byte) error {
if cap(buf) == 0 || cap(buf) > 65536 || cap(buf) != 1<<bits { if cap(buf) == 0 || cap(buf) > 65536 || cap(buf) != 1<<bits {
return errors.New("allocator Put() incorrect buffer size") return errors.New("allocator Put() incorrect buffer size")
} }
//lint:ignore SA6002 ignore temporarily
alloc.buffers[bits].Put(buf) alloc.buffers[bits].Put(buf)
return nil return nil
} }
// msb return the pos of most significiant bit // msb return the pos of most significant bit
func msb(size int) uint16 { func msb(size int) uint16 {
return uint16(bits.Len32(uint32(size)) - 1) return uint16(bits.Len32(uint32(size)) - 1)
} }

View File

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

View File

@ -24,6 +24,8 @@ type Result struct {
Err error Err error
} }
// Do single.Do likes sync.singleFlight
//lint:ignore ST1008 it likes sync.singleFlight
func (s *Single) Do(fn func() (interface{}, error)) (v interface{}, err error, shared bool) { func (s *Single) Do(fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
s.mux.Lock() s.mux.Lock()
now := time.Now() now := time.Now()
@ -44,9 +46,12 @@ func (s *Single) Do(fn func() (interface{}, error)) (v interface{}, err error, s
s.mux.Unlock() s.mux.Unlock()
call.val, call.err = fn() call.val, call.err = fn()
call.wg.Done() call.wg.Done()
s.mux.Lock()
s.call = nil s.call = nil
s.result = &Result{call.val, call.err} s.result = &Result{call.val, call.err}
s.last = now s.last = now
s.mux.Unlock()
return call.val, call.err, false return call.val, call.err, false
} }

View File

@ -6,12 +6,13 @@ import (
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"go.uber.org/atomic"
) )
func TestBasic(t *testing.T) { func TestBasic(t *testing.T) {
single := NewSingle(time.Millisecond * 30) single := NewSingle(time.Millisecond * 30)
foo := 0 foo := 0
shardCount := 0 var shardCount = atomic.NewInt32(0)
call := func() (interface{}, error) { call := func() (interface{}, error) {
foo++ foo++
time.Sleep(time.Millisecond * 5) time.Sleep(time.Millisecond * 5)
@ -25,7 +26,7 @@ func TestBasic(t *testing.T) {
go func() { go func() {
_, _, shard := single.Do(call) _, _, shard := single.Do(call)
if shard { if shard {
shardCount++ shardCount.Inc()
} }
wg.Done() wg.Done()
}() }()
@ -33,7 +34,7 @@ func TestBasic(t *testing.T) {
wg.Wait() wg.Wait()
assert.Equal(t, 1, foo) assert.Equal(t, 1, foo)
assert.Equal(t, 4, shardCount) assert.Equal(t, int32(4), shardCount.Load())
} }
func TestTimer(t *testing.T) { func TestTimer(t *testing.T) {

118
component/dialer/bind.go Normal file
View File

@ -0,0 +1,118 @@
package dialer
import (
"errors"
"net"
"time"
"github.com/Dreamacro/clash/common/singledo"
)
// In some OS, such as Windows, it takes a little longer to get interface information
var ifaceSingle = singledo.NewSingle(time.Second * 20)
var (
errPlatformNotSupport = errors.New("unsupport platform")
)
func lookupTCPAddr(ip net.IP, addrs []net.Addr) (*net.TCPAddr, error) {
ipv4 := ip.To4() != nil
for _, elm := range addrs {
addr, ok := elm.(*net.IPNet)
if !ok {
continue
}
addrV4 := addr.IP.To4() != nil
if addrV4 && ipv4 {
return &net.TCPAddr{IP: addr.IP, Port: 0}, nil
} else if !addrV4 && !ipv4 {
return &net.TCPAddr{IP: addr.IP, Port: 0}, nil
}
}
return nil, ErrAddrNotFound
}
func lookupUDPAddr(ip net.IP, addrs []net.Addr) (*net.UDPAddr, error) {
ipv4 := ip.To4() != nil
for _, elm := range addrs {
addr, ok := elm.(*net.IPNet)
if !ok {
continue
}
addrV4 := addr.IP.To4() != nil
if addrV4 && ipv4 {
return &net.UDPAddr{IP: addr.IP, Port: 0}, nil
} else if !addrV4 && !ipv4 {
return &net.UDPAddr{IP: addr.IP, Port: 0}, nil
}
}
return nil, ErrAddrNotFound
}
func fallbackBindToDialer(dialer *net.Dialer, network string, ip net.IP, name string) error {
if !ip.IsGlobalUnicast() {
return nil
}
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) {
return net.InterfaceByName(name)
})
if err != nil {
return err
}
addrs, err := iface.(*net.Interface).Addrs()
if err != nil {
return err
}
switch network {
case "tcp", "tcp4", "tcp6":
if addr, err := lookupTCPAddr(ip, addrs); err == nil {
dialer.LocalAddr = addr
} else {
return err
}
case "udp", "udp4", "udp6":
if addr, err := lookupUDPAddr(ip, addrs); err == nil {
dialer.LocalAddr = addr
} else {
return err
}
}
return nil
}
func fallbackBindToListenConfig(name string) (string, error) {
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) {
return net.InterfaceByName(name)
})
if err != nil {
return "", err
}
addrs, err := iface.(*net.Interface).Addrs()
if err != nil {
return "", err
}
for _, elm := range addrs {
addr, ok := elm.(*net.IPNet)
if !ok || addr.IP.To4() == nil {
continue
}
return net.JoinHostPort(addr.IP.String(), "0"), nil
}
return "", ErrAddrNotFound
}

View File

@ -0,0 +1,53 @@
package dialer
import (
"net"
"syscall"
)
type controlFn = func(network, address string, c syscall.RawConn) error
func bindControl(ifaceIdx int) controlFn {
return func(network, address string, c syscall.RawConn) error {
ipStr, _, err := net.SplitHostPort(address)
if err == nil {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return nil
}
}
return c.Control(func(fd uintptr) {
switch network {
case "tcp4", "udp4":
syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_BOUND_IF, ifaceIdx)
case "tcp6", "udp6":
syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_BOUND_IF, ifaceIdx)
}
})
}
}
func bindIfaceToDialer(dialer *net.Dialer, ifaceName string) error {
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) {
return net.InterfaceByName(ifaceName)
})
if err != nil {
return err
}
dialer.Control = bindControl(iface.(*net.Interface).Index)
return nil
}
func bindIfaceToListenConfig(lc *net.ListenConfig, ifaceName string) error {
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) {
return net.InterfaceByName(ifaceName)
})
if err != nil {
return err
}
lc.Control = bindControl(iface.(*net.Interface).Index)
return nil
}

View File

@ -0,0 +1,36 @@
package dialer
import (
"net"
"syscall"
)
type controlFn = func(network, address string, c syscall.RawConn) error
func bindControl(ifaceName string) controlFn {
return func(network, address string, c syscall.RawConn) error {
ipStr, _, err := net.SplitHostPort(address)
if err == nil {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return nil
}
}
return c.Control(func(fd uintptr) {
syscall.BindToDevice(int(fd), ifaceName)
})
}
}
func bindIfaceToDialer(dialer *net.Dialer, ifaceName string) error {
dialer.Control = bindControl(ifaceName)
return nil
}
func bindIfaceToListenConfig(lc *net.ListenConfig, ifaceName string) error {
lc.Control = bindControl(ifaceName)
return nil
}

View File

@ -0,0 +1,13 @@
// +build !linux,!darwin
package dialer
import "net"
func bindIfaceToDialer(dialer *net.Dialer, ifaceName string) error {
return errPlatformNotSupport
}
func bindIfaceToListenConfig(lc *net.ListenConfig, ifaceName string) error {
return errPlatformNotSupport
}

View File

@ -19,17 +19,6 @@ func Dialer() (*net.Dialer, error) {
return dialer, nil return dialer, nil
} }
func ListenConfig() (*net.ListenConfig, error) {
cfg := &net.ListenConfig{}
if ListenConfigHook != nil {
if err := ListenConfigHook(cfg); err != nil {
return nil, err
}
}
return cfg, nil
}
func Dial(network, address string) (net.Conn, error) { func Dial(network, address string) (net.Conn, error) {
return DialContext(context.Background(), network, address) return DialContext(context.Background(), network, address)
} }
@ -66,29 +55,26 @@ func DialContext(ctx context.Context, network, address string) (net.Conn, error)
} }
return dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port)) return dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
case "tcp", "udp": case "tcp", "udp":
return dualStackDailContext(ctx, network, address) return dualStackDialContext(ctx, network, address)
default: default:
return nil, errors.New("network invalid") return nil, errors.New("network invalid")
} }
} }
func ListenPacket(network, address string) (net.PacketConn, error) { func ListenPacket(network, address string) (net.PacketConn, error) {
lc, err := ListenConfig() cfg := &net.ListenConfig{}
if err != nil { if ListenPacketHook != nil {
return nil, err var err error
} address, err = ListenPacketHook(cfg, address)
if ListenPacketHook != nil && address == "" {
ip, err := ListenPacketHook()
if err != nil { if err != nil {
return nil, err return nil, err
} }
address = net.JoinHostPort(ip.String(), "0")
} }
return lc.ListenPacket(context.Background(), network, address)
return cfg.ListenPacket(context.Background(), network, address)
} }
func dualStackDailContext(ctx context.Context, network, address string) (net.Conn, error) { func dualStackDialContext(ctx context.Context, network, address string) (net.Conn, error) {
host, port, err := net.SplitHostPort(address) host, port, err := net.SplitHostPort(address)
if err != nil { if err != nil {
return nil, err return nil, err
@ -147,28 +133,27 @@ func dualStackDailContext(ctx context.Context, network, address string) (net.Con
go startRacer(ctx, network+"4", host, false) go startRacer(ctx, network+"4", host, false)
go startRacer(ctx, network+"6", host, true) go startRacer(ctx, network+"6", host, true)
for { for res := range results {
select { if res.error == nil {
case res := <-results: return res.Conn, nil
if res.error == nil { }
return res.Conn, nil
}
if !res.ipv6 { if !res.ipv6 {
primary = res primary = res
} else {
fallback = res
}
if primary.done && fallback.done {
if primary.resolved {
return nil, primary.error
} else if fallback.resolved {
return nil, fallback.error
} else { } else {
fallback = res return nil, primary.error
}
if primary.done && fallback.done {
if primary.resolved {
return nil, primary.error
} else if fallback.resolved {
return nil, fallback.error
} else {
return nil, primary.error
}
} }
} }
} }
return nil, errors.New("never touched")
} }

View File

@ -3,20 +3,15 @@ package dialer
import ( import (
"errors" "errors"
"net" "net"
"time"
"github.com/Dreamacro/clash/common/singledo"
) )
type DialerHookFunc = func(dialer *net.Dialer) error type DialerHookFunc = func(dialer *net.Dialer) error
type DialHookFunc = func(dialer *net.Dialer, network string, ip net.IP) error type DialHookFunc = func(dialer *net.Dialer, network string, ip net.IP) error
type ListenConfigHookFunc = func(*net.ListenConfig) error type ListenPacketHookFunc = func(lc *net.ListenConfig, address string) (string, error)
type ListenPacketHookFunc = func() (net.IP, error)
var ( var (
DialerHook DialerHookFunc DialerHook DialerHookFunc
DialHook DialHookFunc DialHook DialHookFunc
ListenConfigHook ListenConfigHookFunc
ListenPacketHook ListenPacketHookFunc ListenPacketHook ListenPacketHookFunc
) )
@ -25,124 +20,24 @@ var (
ErrNetworkNotSupport = errors.New("network not support") ErrNetworkNotSupport = errors.New("network not support")
) )
func lookupTCPAddr(ip net.IP, addrs []net.Addr) (*net.TCPAddr, error) {
ipv4 := ip.To4() != nil
for _, elm := range addrs {
addr, ok := elm.(*net.IPNet)
if !ok {
continue
}
addrV4 := addr.IP.To4() != nil
if addrV4 && ipv4 {
return &net.TCPAddr{IP: addr.IP, Port: 0}, nil
} else if !addrV4 && !ipv4 {
return &net.TCPAddr{IP: addr.IP, Port: 0}, nil
}
}
return nil, ErrAddrNotFound
}
func lookupUDPAddr(ip net.IP, addrs []net.Addr) (*net.UDPAddr, error) {
ipv4 := ip.To4() != nil
for _, elm := range addrs {
addr, ok := elm.(*net.IPNet)
if !ok {
continue
}
addrV4 := addr.IP.To4() != nil
if addrV4 && ipv4 {
return &net.UDPAddr{IP: addr.IP, Port: 0}, nil
} else if !addrV4 && !ipv4 {
return &net.UDPAddr{IP: addr.IP, Port: 0}, nil
}
}
return nil, ErrAddrNotFound
}
func ListenPacketWithInterface(name string) ListenPacketHookFunc { func ListenPacketWithInterface(name string) ListenPacketHookFunc {
single := singledo.NewSingle(5 * time.Second) return func(lc *net.ListenConfig, address string) (string, error) {
err := bindIfaceToListenConfig(lc, name)
return func() (net.IP, error) { if err == errPlatformNotSupport {
elm, err, _ := single.Do(func() (interface{}, error) { address, err = fallbackBindToListenConfig(name)
iface, err := net.InterfaceByName(name)
if err != nil {
return nil, err
}
addrs, err := iface.Addrs()
if err != nil {
return nil, err
}
return addrs, nil
})
if err != nil {
return nil, err
} }
addrs := elm.([]net.Addr) return address, err
for _, elm := range addrs {
addr, ok := elm.(*net.IPNet)
if !ok || addr.IP.To4() == nil {
continue
}
return addr.IP, nil
}
return nil, ErrAddrNotFound
} }
} }
func DialerWithInterface(name string) DialHookFunc { func DialerWithInterface(name string) DialHookFunc {
single := singledo.NewSingle(5 * time.Second)
return func(dialer *net.Dialer, network string, ip net.IP) error { return func(dialer *net.Dialer, network string, ip net.IP) error {
elm, err, _ := single.Do(func() (interface{}, error) { err := bindIfaceToDialer(dialer, name)
iface, err := net.InterfaceByName(name) if err == errPlatformNotSupport {
if err != nil { err = fallbackBindToDialer(dialer, network, ip, name)
return nil, err
}
addrs, err := iface.Addrs()
if err != nil {
return nil, err
}
return addrs, nil
})
if err != nil {
return err
} }
addrs := elm.([]net.Addr) return err
switch network {
case "tcp", "tcp4", "tcp6":
if addr, err := lookupTCPAddr(ip, addrs); err == nil {
dialer.LocalAddr = addr
} else {
return err
}
case "udp", "udp4", "udp6":
if addr, err := lookupUDPAddr(ip, addrs); err == nil {
dialer.LocalAddr = addr
} else {
return err
}
}
return nil
} }
} }

View File

@ -17,6 +17,7 @@ type Pool struct {
offset uint32 offset uint32
mux sync.Mutex mux sync.Mutex
host *trie.DomainTrie host *trie.DomainTrie
ipnet *net.IPNet
cache *cache.LruCache cache *cache.LruCache
} }
@ -89,6 +90,16 @@ func (p *Pool) Gateway() net.IP {
return uintToIP(p.gateway) return uintToIP(p.gateway)
} }
// IPNet return raw ipnet
func (p *Pool) IPNet() *net.IPNet {
return p.ipnet
}
// PatchFrom clone cache from old pool
func (p *Pool) PatchFrom(o *Pool) {
o.cache.CloneTo(p.cache)
}
func (p *Pool) get(host string) net.IP { func (p *Pool) get(host string) net.IP {
current := p.offset current := p.offset
for { for {
@ -116,7 +127,7 @@ func ipToUint(ip net.IP) uint32 {
} }
func uintToIP(v uint32) net.IP { func uintToIP(v uint32) net.IP {
return net.IPv4(byte(v>>24), byte(v>>16), byte(v>>8), byte(v)) return net.IP{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}
} }
// New return Pool instance // New return Pool instance
@ -136,6 +147,7 @@ func New(ipnet *net.IPNet, size int, host *trie.DomainTrie) (*Pool, error) {
max: max, max: max,
gateway: min - 1, gateway: min - 1,
host: host, host: host,
ipnet: ipnet,
cache: cache.NewLRUCache(cache.WithSize(size * 2)), cache: cache.NewLRUCache(cache.WithSize(size * 2)),
}, nil }, nil
} }

View File

@ -22,9 +22,9 @@ func (t *Table) Get(key string) C.PacketConn {
return item.(C.PacketConn) return item.(C.PacketConn)
} }
func (t *Table) GetOrCreateLock(key string) (*sync.WaitGroup, bool) { func (t *Table) GetOrCreateLock(key string) (*sync.Cond, bool) {
item, loaded := t.mapping.LoadOrStore(key, &sync.WaitGroup{}) item, loaded := t.mapping.LoadOrStore(key, sync.NewCond(&sync.Mutex{}))
return item.(*sync.WaitGroup), loaded return item.(*sync.Cond), loaded
} }
func (t *Table) Delete(key string) { func (t *Table) Delete(key string) {

114
component/pool/pool.go Normal file
View File

@ -0,0 +1,114 @@
package pool
import (
"context"
"runtime"
"time"
)
type Factory = func(context.Context) (interface{}, error)
type entry struct {
elm interface{}
time time.Time
}
type Option func(*pool)
// WithEvict set the evict callback
func WithEvict(cb func(interface{})) Option {
return func(p *pool) {
p.evict = cb
}
}
// WithAge defined element max age (millisecond)
func WithAge(maxAge int64) Option {
return func(p *pool) {
p.maxAge = maxAge
}
}
// WithSize defined max size of Pool
func WithSize(maxSize int) Option {
return func(p *pool) {
p.ch = make(chan interface{}, maxSize)
}
}
// Pool is for GC, see New for detail
type Pool struct {
*pool
}
type pool struct {
ch chan interface{}
factory Factory
evict func(interface{})
maxAge int64
}
func (p *pool) GetContext(ctx context.Context) (interface{}, error) {
now := time.Now()
for {
select {
case item := <-p.ch:
elm := item.(*entry)
if p.maxAge != 0 && now.Sub(item.(*entry).time).Milliseconds() > p.maxAge {
if p.evict != nil {
p.evict(elm.elm)
}
continue
}
return elm.elm, nil
default:
return p.factory(ctx)
}
}
}
func (p *pool) Get() (interface{}, error) {
return p.GetContext(context.Background())
}
func (p *pool) Put(item interface{}) {
e := &entry{
elm: item,
time: time.Now(),
}
select {
case p.ch <- e:
return
default:
// pool is full
if p.evict != nil {
p.evict(item)
}
return
}
}
func recycle(p *Pool) {
for item := range p.pool.ch {
if p.pool.evict != nil {
p.pool.evict(item.(*entry).elm)
}
}
}
func New(factory Factory, options ...Option) *Pool {
p := &pool{
ch: make(chan interface{}, 10),
factory: factory,
}
for _, option := range options {
option(p)
}
P := &Pool{p}
runtime.SetFinalizer(P, recycle)
return P
}

View File

@ -0,0 +1,73 @@
package pool
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func lg() Factory {
initial := -1
return func(context.Context) (interface{}, error) {
initial++
return initial, nil
}
}
func TestPool_Basic(t *testing.T) {
g := lg()
pool := New(g)
elm, _ := pool.Get()
assert.Equal(t, 0, elm.(int))
pool.Put(elm)
elm, _ = pool.Get()
assert.Equal(t, 0, elm.(int))
elm, _ = pool.Get()
assert.Equal(t, 1, elm.(int))
}
func TestPool_MaxSize(t *testing.T) {
g := lg()
size := 5
pool := New(g, WithSize(size))
items := []interface{}{}
for i := 0; i < size; i++ {
item, _ := pool.Get()
items = append(items, item)
}
extra, _ := pool.Get()
assert.Equal(t, size, extra.(int))
for _, item := range items {
pool.Put(item)
}
pool.Put(extra)
for _, item := range items {
elm, _ := pool.Get()
assert.Equal(t, item.(int), elm.(int))
}
}
func TestPool_MaxAge(t *testing.T) {
g := lg()
pool := New(g, WithAge(20))
elm, _ := pool.Get()
pool.Put(elm)
elm, _ = pool.Get()
assert.Equal(t, 0, elm.(int))
pool.Put(elm)
time.Sleep(time.Millisecond * 22)
elm, _ = pool.Get()
assert.Equal(t, 1, elm.(int))
}

View File

@ -0,0 +1,21 @@
package process
import (
"errors"
"net"
)
var (
ErrInvalidNetwork = errors.New("invalid network")
ErrPlatformNotSupport = errors.New("not support on this platform")
ErrNotFound = errors.New("process not found")
)
const (
TCP = "tcp"
UDP = "udp"
)
func FindProcessName(network string, srcIP net.IP, srcPort int) (string, error) {
return findProcessName(network, srcIP, srcPort)
}

View File

@ -0,0 +1,104 @@
package process
import (
"encoding/binary"
"net"
"path/filepath"
"syscall"
"unsafe"
"golang.org/x/sys/unix"
)
const (
procpidpathinfo = 0xb
procpidpathinfosize = 1024
proccallnumpidinfo = 0x2
)
func findProcessName(network string, ip net.IP, port int) (string, error) {
var spath string
switch network {
case TCP:
spath = "net.inet.tcp.pcblist_n"
case UDP:
spath = "net.inet.udp.pcblist_n"
default:
return "", ErrInvalidNetwork
}
isIPv4 := ip.To4() != nil
value, err := syscall.Sysctl(spath)
if err != nil {
return "", err
}
buf := []byte(value)
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
// size/offset are round up (aligned) to 8 bytes in darwin
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
itemSize := 384
if network == TCP {
// rup8(sizeof(xtcpcb_n))
itemSize += 208
}
// skip the first xinpgen(24 bytes) block
for i := 24; i+itemSize <= len(buf); i += itemSize {
// offset of xinpcb_n and xsocket_n
inp, so := i, i+104
srcPort := binary.BigEndian.Uint16(buf[inp+18 : inp+20])
if uint16(port) != srcPort {
continue
}
// xinpcb_n.inp_vflag
flag := buf[inp+44]
var srcIP net.IP
switch {
case flag&0x1 > 0 && isIPv4:
// ipv4
srcIP = net.IP(buf[inp+76 : inp+80])
case flag&0x2 > 0 && !isIPv4:
// ipv6
srcIP = net.IP(buf[inp+64 : inp+80])
default:
continue
}
if !ip.Equal(srcIP) {
continue
}
// xsocket_n.so_last_pid
pid := readNativeUint32(buf[so+68 : so+72])
return getExecPathFromPID(pid)
}
return "", ErrNotFound
}
func getExecPathFromPID(pid uint32) (string, error) {
buf := make([]byte, procpidpathinfosize)
_, _, errno := syscall.Syscall6(
syscall.SYS_PROC_INFO,
proccallnumpidinfo,
uintptr(pid),
procpidpathinfo,
0,
uintptr(unsafe.Pointer(&buf[0])),
procpidpathinfosize)
if errno != 0 {
return "", errno
}
return filepath.Base(unix.ByteSliceToString(buf)), nil
}
func readNativeUint32(b []byte) uint32 {
return *(*uint32)(unsafe.Pointer(&b[0]))
}

View File

@ -0,0 +1,234 @@
package process
import (
"encoding/binary"
"fmt"
"net"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"unsafe"
"github.com/Dreamacro/clash/log"
)
// store process name for when dealing with multiple PROCESS-NAME rules
var (
defaultSearcher *searcher
once sync.Once
)
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
once.Do(func() {
if err := initSearcher(); err != nil {
log.Errorln("Initialize PROCESS-NAME failed: %s", err.Error())
log.Warnln("All PROCESS-NAME rules will be skipped")
return
}
})
if defaultSearcher == nil {
return "", ErrPlatformNotSupport
}
var spath string
isTCP := network == TCP
switch network {
case TCP:
spath = "net.inet.tcp.pcblist"
case UDP:
spath = "net.inet.udp.pcblist"
default:
return "", ErrInvalidNetwork
}
value, err := syscall.Sysctl(spath)
if err != nil {
return "", err
}
buf := []byte(value)
pid, err := defaultSearcher.Search(buf, ip, uint16(srcPort), isTCP)
if err != nil {
return "", err
}
return getExecPathFromPID(pid)
}
func getExecPathFromPID(pid uint32) (string, error) {
buf := make([]byte, 2048)
size := uint64(len(buf))
// CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, pid
mib := [4]uint32{1, 14, 12, pid}
_, _, errno := syscall.Syscall6(
syscall.SYS___SYSCTL,
uintptr(unsafe.Pointer(&mib[0])),
uintptr(len(mib)),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)),
0,
0)
if errno != 0 || size == 0 {
return "", errno
}
return filepath.Base(string(buf[:size-1])), nil
}
func readNativeUint32(b []byte) uint32 {
return *(*uint32)(unsafe.Pointer(&b[0]))
}
type searcher struct {
// sizeof(struct xinpgen)
headSize int
// sizeof(struct xtcpcb)
tcpItemSize int
// sizeof(struct xinpcb)
udpItemSize int
udpInpOffset int
port int
ip int
vflag int
socket int
// sizeof(struct xfile)
fileItemSize int
data int
pid int
}
func (s *searcher) Search(buf []byte, ip net.IP, port uint16, isTCP bool) (uint32, error) {
var itemSize int
var inpOffset int
if isTCP {
// struct xtcpcb
itemSize = s.tcpItemSize
inpOffset = 8
} else {
// struct xinpcb
itemSize = s.udpItemSize
inpOffset = s.udpInpOffset
}
isIPv4 := ip.To4() != nil
// skip the first xinpgen block
for i := s.headSize; i+itemSize <= len(buf); i += itemSize {
inp := i + inpOffset
srcPort := binary.BigEndian.Uint16(buf[inp+s.port : inp+s.port+2])
if port != srcPort {
continue
}
// xinpcb.inp_vflag
flag := buf[inp+s.vflag]
var srcIP net.IP
switch {
case flag&0x1 > 0 && isIPv4:
// ipv4
srcIP = net.IP(buf[inp+s.ip : inp+s.ip+4])
case flag&0x2 > 0 && !isIPv4:
// ipv6
srcIP = net.IP(buf[inp+s.ip-12 : inp+s.ip+4])
default:
continue
}
if !ip.Equal(srcIP) {
continue
}
// xsocket.xso_so, interpreted as big endian anyway since it's only used for comparison
socket := binary.BigEndian.Uint64(buf[inp+s.socket : inp+s.socket+8])
return s.searchSocketPid(socket)
}
return 0, ErrNotFound
}
func (s *searcher) searchSocketPid(socket uint64) (uint32, error) {
value, err := syscall.Sysctl("kern.file")
if err != nil {
return 0, err
}
buf := []byte(value)
// struct xfile
itemSize := s.fileItemSize
for i := 0; i+itemSize <= len(buf); i += itemSize {
// xfile.xf_data
data := binary.BigEndian.Uint64(buf[i+s.data : i+s.data+8])
if data == socket {
// xfile.xf_pid
pid := readNativeUint32(buf[i+s.pid : i+s.pid+4])
return pid, nil
}
}
return 0, ErrNotFound
}
func newSearcher(major int) *searcher {
var s *searcher
switch major {
case 11:
s = &searcher{
headSize: 32,
tcpItemSize: 1304,
udpItemSize: 632,
port: 198,
ip: 228,
vflag: 116,
socket: 88,
fileItemSize: 80,
data: 56,
pid: 8,
udpInpOffset: 8,
}
case 12:
fallthrough
case 13:
s = &searcher{
headSize: 64,
tcpItemSize: 744,
udpItemSize: 400,
port: 254,
ip: 284,
vflag: 392,
socket: 16,
fileItemSize: 128,
data: 56,
pid: 8,
}
}
return s
}
func initSearcher() error {
osRelease, err := syscall.Sysctl("kern.osrelease")
if err != nil {
return err
}
dot := strings.Index(osRelease, ".")
if dot != -1 {
osRelease = osRelease[:dot]
}
major, err := strconv.Atoi(osRelease)
if err != nil {
return err
}
defaultSearcher = newSearcher(major)
if defaultSearcher == nil {
return fmt.Errorf("unsupported freebsd version %d", major)
}
return nil
}

View File

@ -0,0 +1,238 @@
package process
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"io/ioutil"
"net"
"path"
"path/filepath"
"syscall"
"unsafe"
"github.com/Dreamacro/clash/common/pool"
)
// from https://github.com/vishvananda/netlink/blob/bca67dfc8220b44ef582c9da4e9172bf1c9ec973/nl/nl_linux.go#L52-L62
func init() {
var x uint32 = 0x01020304
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
nativeEndian = binary.BigEndian
} else {
nativeEndian = binary.LittleEndian
}
}
type SocketResolver func(network string, ip net.IP, srcPort int) (inode, uid int, err error)
type ProcessNameResolver func(inode, uid int) (name string, err error)
// export for android
var (
DefaultSocketResolver SocketResolver = resolveSocketByNetlink
DefaultProcessNameResolver ProcessNameResolver = resolveProcessNameByProcSearch
)
const (
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48
socketDiagByFamily = 20
pathProc = "/proc"
)
var nativeEndian binary.ByteOrder = binary.LittleEndian
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
inode, uid, err := DefaultSocketResolver(network, ip, srcPort)
if err != nil {
return "", err
}
return DefaultProcessNameResolver(inode, uid)
}
func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int, int, error) {
var family byte
var protocol byte
switch network {
case TCP:
protocol = syscall.IPPROTO_TCP
case UDP:
protocol = syscall.IPPROTO_UDP
default:
return 0, 0, ErrInvalidNetwork
}
if ip.To4() != nil {
family = syscall.AF_INET
} else {
family = syscall.AF_INET6
}
req := packSocketDiagRequest(family, protocol, ip, uint16(srcPort))
socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG)
if err != nil {
return 0, 0, err
}
defer syscall.Close(socket)
syscall.SetNonblock(socket, true)
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 50})
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 50})
if err := syscall.Connect(socket, &syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK,
Pad: 0,
Pid: 0,
Groups: 0,
}); err != nil {
return 0, 0, err
}
if _, err := syscall.Write(socket, req); err != nil {
return 0, 0, err
}
rb := pool.Get(pool.RelayBufferSize)
defer pool.Put(rb)
n, err := syscall.Read(socket, rb)
if err != nil {
return 0, 0, err
}
messages, err := syscall.ParseNetlinkMessage(rb[:n])
if err != nil {
return 0, 0, err
} else if len(messages) == 0 {
return 0, 0, io.ErrUnexpectedEOF
}
message := messages[0]
if message.Header.Type&syscall.NLMSG_ERROR != 0 {
return 0, 0, syscall.ESRCH
}
uid, inode := unpackSocketDiagResponse(&messages[0])
return int(uid), int(inode), nil
}
func packSocketDiagRequest(family, protocol byte, source net.IP, sourcePort uint16) []byte {
s := make([]byte, 16)
if v4 := source.To4(); v4 != nil {
copy(s, v4)
} else {
copy(s, source)
}
buf := make([]byte, sizeOfSocketDiagRequest)
nativeEndian.PutUint32(buf[0:4], sizeOfSocketDiagRequest)
nativeEndian.PutUint16(buf[4:6], socketDiagByFamily)
nativeEndian.PutUint16(buf[6:8], syscall.NLM_F_REQUEST|syscall.NLM_F_DUMP)
nativeEndian.PutUint32(buf[8:12], 0)
nativeEndian.PutUint32(buf[12:16], 0)
buf[16] = family
buf[17] = protocol
buf[18] = 0
buf[19] = 0
nativeEndian.PutUint32(buf[20:24], 0xFFFFFFFF)
binary.BigEndian.PutUint16(buf[24:26], sourcePort)
binary.BigEndian.PutUint16(buf[26:28], 0)
copy(buf[28:44], s)
copy(buf[44:60], net.IPv6zero)
nativeEndian.PutUint32(buf[60:64], 0)
nativeEndian.PutUint64(buf[64:72], 0xFFFFFFFFFFFFFFFF)
return buf
}
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
if len(msg.Data) < 72 {
return 0, 0
}
data := msg.Data
uid = nativeEndian.Uint32(data[64:68])
inode = nativeEndian.Uint32(data[68:72])
return
}
func resolveProcessNameByProcSearch(inode, uid int) (string, error) {
files, err := ioutil.ReadDir(pathProc)
if err != nil {
return "", err
}
buffer := make([]byte, syscall.PathMax)
socket := []byte(fmt.Sprintf("socket:[%d]", inode))
for _, f := range files {
if !f.IsDir() || !isPid(f.Name()) {
continue
}
if f.Sys().(*syscall.Stat_t).Uid != uint32(uid) {
continue
}
processPath := path.Join(pathProc, f.Name())
fdPath := path.Join(processPath, "fd")
fds, err := ioutil.ReadDir(fdPath)
if err != nil {
continue
}
for _, fd := range fds {
n, err := syscall.Readlink(path.Join(fdPath, fd.Name()), buffer)
if err != nil {
continue
}
if bytes.Equal(buffer[:n], socket) {
cmdline, err := ioutil.ReadFile(path.Join(processPath, "cmdline"))
if err != nil {
return "", err
}
return splitCmdline(cmdline), nil
}
}
}
return "", syscall.ESRCH
}
func splitCmdline(cmdline []byte) string {
indexOfEndOfString := len(cmdline)
for i, c := range cmdline {
if c == 0 {
indexOfEndOfString = i
break
}
}
return filepath.Base(string(cmdline[:indexOfEndOfString]))
}
func isPid(s string) bool {
for _, s := range s {
if s < '0' || s > '9' {
return false
}
}
return true
}

View File

@ -0,0 +1,10 @@
// +build !darwin,!linux,!windows
// +build !freebsd !amd64
package process
import "net"
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
return "", ErrPlatformNotSupport
}

View File

@ -0,0 +1,224 @@
package process
import (
"fmt"
"net"
"path/filepath"
"sync"
"syscall"
"unsafe"
"github.com/Dreamacro/clash/log"
"golang.org/x/sys/windows"
)
const (
tcpTableFunc = "GetExtendedTcpTable"
tcpTablePidConn = 4
udpTableFunc = "GetExtendedUdpTable"
udpTablePid = 1
queryProcNameFunc = "QueryFullProcessImageNameW"
)
var (
getExTCPTable uintptr
getExUDPTable uintptr
queryProcName uintptr
once sync.Once
)
func initWin32API() error {
h, err := windows.LoadLibrary("iphlpapi.dll")
if err != nil {
return fmt.Errorf("LoadLibrary iphlpapi.dll failed: %s", err.Error())
}
getExTCPTable, err = windows.GetProcAddress(h, tcpTableFunc)
if err != nil {
return fmt.Errorf("GetProcAddress of %s failed: %s", tcpTableFunc, err.Error())
}
getExUDPTable, err = windows.GetProcAddress(h, udpTableFunc)
if err != nil {
return fmt.Errorf("GetProcAddress of %s failed: %s", udpTableFunc, err.Error())
}
h, err = windows.LoadLibrary("kernel32.dll")
if err != nil {
return fmt.Errorf("LoadLibrary kernel32.dll failed: %s", err.Error())
}
queryProcName, err = windows.GetProcAddress(h, queryProcNameFunc)
if err != nil {
return fmt.Errorf("GetProcAddress of %s failed: %s", queryProcNameFunc, err.Error())
}
return nil
}
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
once.Do(func() {
err := initWin32API()
if err != nil {
log.Errorln("Initialize PROCESS-NAME failed: %s", err.Error())
log.Warnln("All PROCESS-NAMES rules will be skiped")
return
}
})
family := windows.AF_INET
if ip.To4() == nil {
family = windows.AF_INET6
}
var class int
var fn uintptr
switch network {
case TCP:
fn = getExTCPTable
class = tcpTablePidConn
case UDP:
fn = getExUDPTable
class = udpTablePid
default:
return "", ErrInvalidNetwork
}
buf, err := getTransportTable(fn, family, class)
if err != nil {
return "", err
}
s := newSearcher(family == windows.AF_INET, network == TCP)
pid, err := s.Search(buf, ip, uint16(srcPort))
if err != nil {
return "", err
}
return getExecPathFromPID(pid)
}
type searcher struct {
itemSize int
port int
ip int
ipSize int
pid int
tcpState int
}
func (s *searcher) Search(b []byte, ip net.IP, port uint16) (uint32, error) {
n := int(readNativeUint32(b[:4]))
itemSize := s.itemSize
for i := 0; i < n; i++ {
row := b[4+itemSize*i : 4+itemSize*(i+1)]
if s.tcpState >= 0 {
tcpState := readNativeUint32(row[s.tcpState : s.tcpState+4])
// MIB_TCP_STATE_ESTAB, only check established connections for TCP
if tcpState != 5 {
continue
}
}
// according to MSDN, only the lower 16 bits of dwLocalPort are used and the port number is in network endian.
// this field can be illustrated as follows depends on different machine endianess:
// little endian: [ MSB LSB 0 0 ] interpret as native uint32 is ((LSB<<8)|MSB)
// big endian: [ 0 0 MSB LSB ] interpret as native uint32 is ((MSB<<8)|LSB)
// so we need an syscall.Ntohs on the lower 16 bits after read the port as native uint32
srcPort := syscall.Ntohs(uint16(readNativeUint32(row[s.port : s.port+4])))
if srcPort != port {
continue
}
srcIP := net.IP(row[s.ip : s.ip+s.ipSize])
// windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto
if !ip.Equal(srcIP) && (!srcIP.IsUnspecified() || s.tcpState != -1) {
continue
}
pid := readNativeUint32(row[s.pid : s.pid+4])
return pid, nil
}
return 0, ErrNotFound
}
func newSearcher(isV4, isTCP bool) *searcher {
var itemSize, port, ip, ipSize, pid int
tcpState := -1
switch {
case isV4 && isTCP:
// struct MIB_TCPROW_OWNER_PID
itemSize, port, ip, ipSize, pid, tcpState = 24, 8, 4, 4, 20, 0
case isV4 && !isTCP:
// struct MIB_UDPROW_OWNER_PID
itemSize, port, ip, ipSize, pid = 12, 4, 0, 4, 8
case !isV4 && isTCP:
// struct MIB_TCP6ROW_OWNER_PID
itemSize, port, ip, ipSize, pid, tcpState = 56, 20, 0, 16, 52, 48
case !isV4 && !isTCP:
// struct MIB_UDP6ROW_OWNER_PID
itemSize, port, ip, ipSize, pid = 28, 20, 0, 16, 24
}
return &searcher{
itemSize: itemSize,
port: port,
ip: ip,
ipSize: ipSize,
pid: pid,
tcpState: tcpState,
}
}
func getTransportTable(fn uintptr, family int, class int) ([]byte, error) {
for size, buf := uint32(8), make([]byte, 8); ; {
ptr := unsafe.Pointer(&buf[0])
err, _, _ := syscall.Syscall6(fn, 6, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0)
switch err {
case 0:
return buf, nil
case uintptr(syscall.ERROR_INSUFFICIENT_BUFFER):
buf = make([]byte, size)
default:
return nil, fmt.Errorf("syscall error: %d", err)
}
}
}
func readNativeUint32(b []byte) uint32 {
return *(*uint32)(unsafe.Pointer(&b[0]))
}
func getExecPathFromPID(pid uint32) (string, error) {
// kernel process starts with a colon in order to distinguish with normal processes
switch pid {
case 0:
// reserved pid for system idle process
return ":System Idle Process", nil
case 4:
// reserved pid for windows kernel image
return ":System", nil
}
h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
if err != nil {
return "", err
}
defer windows.CloseHandle(h)
buf := make([]uint16, syscall.MAX_LONG_PATH)
size := uint32(len(buf))
r1, _, err := syscall.Syscall6(
queryProcName, 4,
uintptr(h),
uintptr(1),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)),
0, 0)
if r1 == 0 {
return "", err
}
return filepath.Base(syscall.UTF16ToString(buf[:size])), nil
}

View File

@ -0,0 +1,101 @@
package cachefile
import (
"bytes"
"encoding/gob"
"io/ioutil"
"os"
"sync"
"github.com/Dreamacro/clash/component/profile"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
)
var (
initOnce sync.Once
fileMode os.FileMode = 0666
defaultCache *CacheFile
)
type cache struct {
Selected map[string]string
}
// CacheFile store and update the cache file
type CacheFile struct {
path string
model *cache
buf *bytes.Buffer
mux sync.Mutex
}
func (c *CacheFile) SetSelected(group, selected string) {
if !profile.StoreSelected.Load() {
return
}
c.mux.Lock()
defer c.mux.Unlock()
model := c.element()
model.Selected[group] = selected
c.buf.Reset()
if err := gob.NewEncoder(c.buf).Encode(model); err != nil {
log.Warnln("[CacheFile] encode gob failed: %s", err.Error())
return
}
if err := ioutil.WriteFile(c.path, c.buf.Bytes(), fileMode); err != nil {
log.Warnln("[CacheFile] write cache to %s failed: %s", c.path, err.Error())
return
}
}
func (c *CacheFile) SelectedMap() map[string]string {
if !profile.StoreSelected.Load() {
return nil
}
c.mux.Lock()
defer c.mux.Unlock()
model := c.element()
mapping := map[string]string{}
for k, v := range model.Selected {
mapping[k] = v
}
return mapping
}
func (c *CacheFile) element() *cache {
if c.model != nil {
return c.model
}
model := &cache{
Selected: map[string]string{},
}
if buf, err := ioutil.ReadFile(c.path); err == nil {
bufReader := bytes.NewBuffer(buf)
gob.NewDecoder(bufReader).Decode(model)
}
c.model = model
return c.model
}
// Cache return singleton of CacheFile
func Cache() *CacheFile {
initOnce.Do(func() {
defaultCache = &CacheFile{
path: C.Path.Cache(),
buf: &bytes.Buffer{},
}
})
return defaultCache
}

View File

@ -0,0 +1,10 @@
package profile
import (
"go.uber.org/atomic"
)
var (
// StoreSelected is a global switch for storing selected proxy to cache
StoreSelected = atomic.NewBool(true)
)

View File

@ -0,0 +1,55 @@
package resolver
import (
"net"
)
var DefaultHostMapper Enhancer
type Enhancer interface {
FakeIPEnabled() bool
MappingEnabled() bool
IsFakeIP(net.IP) bool
IsExistFakeIP(net.IP) bool
FindHostByIP(net.IP) (string, bool)
}
func FakeIPEnabled() bool {
if mapper := DefaultHostMapper; mapper != nil {
return mapper.FakeIPEnabled()
}
return false
}
func MappingEnabled() bool {
if mapper := DefaultHostMapper; mapper != nil {
return mapper.MappingEnabled()
}
return false
}
func IsFakeIP(ip net.IP) bool {
if mapper := DefaultHostMapper; mapper != nil {
return mapper.IsFakeIP(ip)
}
return false
}
func IsExistFakeIP(ip net.IP) bool {
if mapper := DefaultHostMapper; mapper != nil {
return mapper.IsExistFakeIP(ip)
}
return false
}
func FindHostByIP(ip net.IP) (string, bool) {
if mapper := DefaultHostMapper; mapper != nil {
return mapper.FindHostByIP(ip)
}
return "", false
}

View File

@ -1,9 +1,12 @@
package resolver package resolver
import ( import (
"context"
"errors" "errors"
"math/rand"
"net" "net"
"strings" "strings"
"time"
"github.com/Dreamacro/clash/component/trie" "github.com/Dreamacro/clash/component/trie"
) )
@ -18,6 +21,9 @@ var (
// DefaultHosts aim to resolve hosts // DefaultHosts aim to resolve hosts
DefaultHosts = trie.New() DefaultHosts = trie.New()
// DefaultDNSTimeout defined the default dns request timeout
DefaultDNSTimeout = time.Second * 5
) )
var ( var (
@ -52,18 +58,16 @@ func ResolveIPv4(host string) (net.IP, error) {
return DefaultResolver.ResolveIPv4(host) return DefaultResolver.ResolveIPv4(host)
} }
ipAddrs, err := net.LookupIP(host) ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout)
defer cancel()
ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip4", host)
if err != nil { if err != nil {
return nil, err return nil, err
} else if len(ipAddrs) == 0 {
return nil, ErrIPNotFound
} }
for _, ip := range ipAddrs { return ipAddrs[rand.Intn(len(ipAddrs))], nil
if ip4 := ip.To4(); ip4 != nil {
return ip4, nil
}
}
return nil, ErrIPNotFound
} }
// ResolveIPv6 with a host, return ipv6 // ResolveIPv6 with a host, return ipv6
@ -90,31 +94,29 @@ func ResolveIPv6(host string) (net.IP, error) {
return DefaultResolver.ResolveIPv6(host) return DefaultResolver.ResolveIPv6(host)
} }
ipAddrs, err := net.LookupIP(host) ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout)
defer cancel()
ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip6", host)
if err != nil { if err != nil {
return nil, err return nil, err
} else if len(ipAddrs) == 0 {
return nil, ErrIPNotFound
} }
for _, ip := range ipAddrs { return ipAddrs[rand.Intn(len(ipAddrs))], nil
if ip.To4() == nil {
return ip, nil
}
}
return nil, ErrIPNotFound
} }
// ResolveIP with a host, return ip // ResolveIPWithResolver same as ResolveIP, but with a resolver
func ResolveIP(host string) (net.IP, error) { func ResolveIPWithResolver(host string, r Resolver) (net.IP, error) {
if node := DefaultHosts.Search(host); node != nil { if node := DefaultHosts.Search(host); node != nil {
return node.Data.(net.IP), nil return node.Data.(net.IP), nil
} }
if DefaultResolver != nil { if r != nil {
if DisableIPv6 { if DisableIPv6 {
return DefaultResolver.ResolveIPv4(host) return r.ResolveIPv4(host)
} }
return DefaultResolver.ResolveIP(host) return r.ResolveIP(host)
} else if DisableIPv6 { } else if DisableIPv6 {
return ResolveIPv4(host) return ResolveIPv4(host)
} }
@ -131,3 +133,8 @@ func ResolveIP(host string) (net.IP, error) {
return ipAddr.IP, nil return ipAddr.IP, nil
} }
// ResolveIP with a host, return ip
func ResolveIP(host string) (net.IP, error) {
return ResolveIPWithResolver(host, DefaultResolver)
}

View File

@ -1,21 +0,0 @@
package snell
import (
"crypto/cipher"
"golang.org/x/crypto/argon2"
)
type snellCipher struct {
psk []byte
makeAEAD func(key []byte) (cipher.AEAD, error)
}
func (sc *snellCipher) KeySize() int { return 32 }
func (sc *snellCipher) SaltSize() int { return 16 }
func (sc *snellCipher) Encrypter(salt []byte) (cipher.AEAD, error) {
return sc.makeAEAD(argon2.IDKey(sc.psk, salt, 3, 8, 1, uint32(sc.KeySize())))
}
func (sc *snellCipher) Decrypter(salt []byte) (cipher.AEAD, error) {
return sc.makeAEAD(argon2.IDKey(sc.psk, salt, 3, 8, 1, uint32(sc.KeySize())))
}

View File

@ -23,7 +23,7 @@ type DomainTrie struct {
root *Node root *Node
} }
func validAndSplitDomain(domain string) ([]string, bool) { func ValidAndSplitDomain(domain string) ([]string, bool) {
if domain != "" && domain[len(domain)-1] == '.' { if domain != "" && domain[len(domain)-1] == '.' {
return nil, false return nil, false
} }
@ -54,7 +54,7 @@ func validAndSplitDomain(domain string) ([]string, bool) {
// 4. .example.com // 4. .example.com
// 5. +.example.com // 5. +.example.com
func (t *DomainTrie) Insert(domain string, data interface{}) error { func (t *DomainTrie) Insert(domain string, data interface{}) error {
parts, valid := validAndSplitDomain(domain) parts, valid := ValidAndSplitDomain(domain)
if !valid { if !valid {
return ErrInvalidDomain return ErrInvalidDomain
} }
@ -91,7 +91,7 @@ func (t *DomainTrie) insert(parts []string, data interface{}) {
// 2. wildcard domain // 2. wildcard domain
// 2. dot wildcard domain // 2. dot wildcard domain
func (t *DomainTrie) Search(domain string) *Node { func (t *DomainTrie) Search(domain string) *Node {
parts, valid := validAndSplitDomain(domain) parts, valid := ValidAndSplitDomain(domain)
if !valid || parts[0] == "" { if !valid || parts[0] == "" {
return nil return nil
} }
@ -122,11 +122,7 @@ func (t *DomainTrie) search(node *Node, parts []string) *Node {
} }
} }
if c := node.getChild(dotWildcard); c != nil { return node.getChild(dotWildcard)
return c
}
return nil
} }
// New returns a new, empty Trie. // New returns a new, empty Trie.

View File

@ -8,16 +8,17 @@ import (
"os" "os"
"strings" "strings"
"github.com/Dreamacro/clash/adapters/outbound" "github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/adapters/outboundgroup" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapters/provider" "github.com/Dreamacro/clash/adapter/outboundgroup"
"github.com/Dreamacro/clash/adapter/provider"
"github.com/Dreamacro/clash/component/auth" "github.com/Dreamacro/clash/component/auth"
"github.com/Dreamacro/clash/component/fakeip" "github.com/Dreamacro/clash/component/fakeip"
"github.com/Dreamacro/clash/component/trie" "github.com/Dreamacro/clash/component/trie"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/dns" "github.com/Dreamacro/clash/dns"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
R "github.com/Dreamacro/clash/rules" R "github.com/Dreamacro/clash/rule"
T "github.com/Dreamacro/clash/tunnel" T "github.com/Dreamacro/clash/tunnel"
yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v2"
@ -30,7 +31,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"`
Interface string `json:"interface-name"` Interface string `json:"-"`
} }
// Inbound // Inbound
@ -38,6 +39,7 @@ type Inbound struct {
Port int `json:"port"` Port int `json:"port"`
SocksPort int `json:"socks-port"` SocksPort int `json:"socks-port"`
RedirPort int `json:"redir-port"` RedirPort int `json:"redir-port"`
TProxyPort int `json:"tproxy-port"`
MixedPort int `json:"mixed-port"` MixedPort int `json:"mixed-port"`
Authentication []string `json:"authentication"` Authentication []string `json:"authentication"`
AllowLan bool `json:"allow-lan"` AllowLan bool `json:"allow-lan"`
@ -62,12 +64,20 @@ type DNS struct {
EnhancedMode dns.EnhancedMode `yaml:"enhanced-mode"` EnhancedMode dns.EnhancedMode `yaml:"enhanced-mode"`
DefaultNameserver []dns.NameServer `yaml:"default-nameserver"` DefaultNameserver []dns.NameServer `yaml:"default-nameserver"`
FakeIPRange *fakeip.Pool FakeIPRange *fakeip.Pool
Hosts *trie.DomainTrie
NameServerPolicy map[string]dns.NameServer
} }
// FallbackFilter config // FallbackFilter config
type FallbackFilter struct { type FallbackFilter struct {
GeoIP bool `yaml:"geoip"` GeoIP bool `yaml:"geoip"`
IPCIDR []*net.IPNet `yaml:"ipcidr"` IPCIDR []*net.IPNet `yaml:"ipcidr"`
Domain []string `yaml:"domain"`
}
// Profile config
type Profile struct {
StoreSelected bool `yaml:"store-selected"`
} }
// Experimental config // Experimental config
@ -79,6 +89,7 @@ type Config struct {
DNS *DNS DNS *DNS
Experimental *Experimental Experimental *Experimental
Hosts *trie.DomainTrie Hosts *trie.DomainTrie
Profile *Profile
Rules []C.Rule Rules []C.Rule
Users []auth.AuthUser Users []auth.AuthUser
Proxies map[string]C.Proxy Proxies map[string]C.Proxy
@ -88,6 +99,7 @@ type Config struct {
type RawDNS struct { type RawDNS struct {
Enable bool `yaml:"enable"` Enable bool `yaml:"enable"`
IPv6 bool `yaml:"ipv6"` IPv6 bool `yaml:"ipv6"`
UseHosts bool `yaml:"use-hosts"`
NameServer []string `yaml:"nameserver"` NameServer []string `yaml:"nameserver"`
Fallback []string `yaml:"fallback"` Fallback []string `yaml:"fallback"`
FallbackFilter RawFallbackFilter `yaml:"fallback-filter"` FallbackFilter RawFallbackFilter `yaml:"fallback-filter"`
@ -96,17 +108,20 @@ type RawDNS struct {
FakeIPRange string `yaml:"fake-ip-range"` FakeIPRange string `yaml:"fake-ip-range"`
FakeIPFilter []string `yaml:"fake-ip-filter"` FakeIPFilter []string `yaml:"fake-ip-filter"`
DefaultNameserver []string `yaml:"default-nameserver"` DefaultNameserver []string `yaml:"default-nameserver"`
NameServerPolicy map[string]string `yaml:"nameserver-policy"`
} }
type RawFallbackFilter struct { type RawFallbackFilter struct {
GeoIP bool `yaml:"geoip"` GeoIP bool `yaml:"geoip"`
IPCIDR []string `yaml:"ipcidr"` IPCIDR []string `yaml:"ipcidr"`
Domain []string `yaml:"domain"`
} }
type RawConfig struct { type RawConfig struct {
Port int `yaml:"port"` Port int `yaml:"port"`
SocksPort int `yaml:"socks-port"` SocksPort int `yaml:"socks-port"`
RedirPort int `yaml:"redir-port"` RedirPort int `yaml:"redir-port"`
TProxyPort int `yaml:"tproxy-port"`
MixedPort int `yaml:"mixed-port"` MixedPort int `yaml:"mixed-port"`
Authentication []string `yaml:"authentication"` Authentication []string `yaml:"authentication"`
AllowLan bool `yaml:"allow-lan"` AllowLan bool `yaml:"allow-lan"`
@ -123,6 +138,7 @@ type RawConfig struct {
Hosts map[string]string `yaml:"hosts"` Hosts map[string]string `yaml:"hosts"`
DNS RawDNS `yaml:"dns"` DNS RawDNS `yaml:"dns"`
Experimental Experimental `yaml:"experimental"` Experimental Experimental `yaml:"experimental"`
Profile Profile `yaml:"profile"`
Proxy []map[string]interface{} `yaml:"proxies"` Proxy []map[string]interface{} `yaml:"proxies"`
ProxyGroup []map[string]interface{} `yaml:"proxy-groups"` ProxyGroup []map[string]interface{} `yaml:"proxy-groups"`
Rule []string `yaml:"rules"` Rule []string `yaml:"rules"`
@ -139,7 +155,7 @@ func Parse(buf []byte) (*Config, error) {
} }
func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
// config with some default value // config with default value
rawCfg := &RawConfig{ rawCfg := &RawConfig{
AllowLan: false, AllowLan: false,
BindAddress: "*", BindAddress: "*",
@ -152,6 +168,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
ProxyGroup: []map[string]interface{}{}, ProxyGroup: []map[string]interface{}{},
DNS: RawDNS{ DNS: RawDNS{
Enable: false, Enable: false,
UseHosts: true,
FakeIPRange: "198.18.0.1/16", FakeIPRange: "198.18.0.1/16",
FallbackFilter: RawFallbackFilter{ FallbackFilter: RawFallbackFilter{
GeoIP: true, GeoIP: true,
@ -162,6 +179,9 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
"8.8.8.8", "8.8.8.8",
}, },
}, },
Profile: Profile{
StoreSelected: true,
},
} }
if err := yaml.Unmarshal(buf, &rawCfg); err != nil { if err := yaml.Unmarshal(buf, &rawCfg); err != nil {
@ -175,6 +195,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
config := &Config{} config := &Config{}
config.Experimental = &rawCfg.Experimental config.Experimental = &rawCfg.Experimental
config.Profile = &rawCfg.Profile
general, err := parseGeneral(rawCfg) general, err := parseGeneral(rawCfg)
if err != nil { if err != nil {
@ -195,18 +216,18 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
} }
config.Rules = rules config.Rules = rules
dnsCfg, err := parseDNS(rawCfg.DNS)
if err != nil {
return nil, err
}
config.DNS = dnsCfg
hosts, err := parseHosts(rawCfg) hosts, err := parseHosts(rawCfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
config.Hosts = hosts config.Hosts = hosts
dnsCfg, err := parseDNS(rawCfg.DNS, hosts)
if err != nil {
return nil, err
}
config.DNS = dnsCfg
config.Users = parseAuthentication(rawCfg.Authentication) config.Users = parseAuthentication(rawCfg.Authentication)
return config, nil return config, nil
@ -229,6 +250,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
Port: cfg.Port, Port: cfg.Port,
SocksPort: cfg.SocksPort, SocksPort: cfg.SocksPort,
RedirPort: cfg.RedirPort, RedirPort: cfg.RedirPort,
TProxyPort: cfg.TProxyPort,
MixedPort: cfg.MixedPort, MixedPort: cfg.MixedPort,
AllowLan: cfg.AllowLan, AllowLan: cfg.AllowLan,
BindAddress: cfg.BindAddress, BindAddress: cfg.BindAddress,
@ -253,19 +275,19 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
groupsConfig := cfg.ProxyGroup groupsConfig := cfg.ProxyGroup
providersConfig := cfg.ProxyProvider providersConfig := cfg.ProxyProvider
proxies["DIRECT"] = outbound.NewProxy(outbound.NewDirect()) proxies["DIRECT"] = adapter.NewProxy(outbound.NewDirect())
proxies["REJECT"] = outbound.NewProxy(outbound.NewReject()) proxies["REJECT"] = adapter.NewProxy(outbound.NewReject())
proxyList = append(proxyList, "DIRECT", "REJECT") proxyList = append(proxyList, "DIRECT", "REJECT")
// parse proxy // parse proxy
for idx, mapping := range proxiesConfig { for idx, mapping := range proxiesConfig {
proxy, err := outbound.ParseProxy(mapping) proxy, err := adapter.ParseProxy(mapping)
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)
} }
if _, exist := proxies[proxy.Name()]; exist { if _, exist := proxies[proxy.Name()]; exist {
return nil, nil, fmt.Errorf("Proxy %s is the duplicate name", proxy.Name()) return nil, nil, fmt.Errorf("proxy %s is the duplicate name", proxy.Name())
} }
proxies[proxy.Name()] = proxy proxies[proxy.Name()] = proxy
proxyList = append(proxyList, proxy.Name()) proxyList = append(proxyList, proxy.Name())
@ -275,7 +297,7 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
for idx, mapping := range groupsConfig { for idx, mapping := range groupsConfig {
groupName, existName := mapping["name"].(string) groupName, existName := mapping["name"].(string)
if !existName { if !existName {
return nil, nil, fmt.Errorf("ProxyGroup %d: missing name", idx) return nil, nil, fmt.Errorf("proxy group %d: missing name", idx)
} }
proxyList = append(proxyList, groupName) proxyList = append(proxyList, groupName)
} }
@ -310,15 +332,15 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
for idx, mapping := range groupsConfig { for idx, mapping := range groupsConfig {
group, err := outboundgroup.ParseProxyGroup(mapping, proxies, providersMap) group, err := outboundgroup.ParseProxyGroup(mapping, proxies, providersMap)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("ProxyGroup[%d]: %w", idx, err) return nil, nil, fmt.Errorf("proxy group[%d]: %w", idx, err)
} }
groupName := group.Name() groupName := group.Name()
if _, exist := proxies[groupName]; exist { if _, exist := proxies[groupName]; exist {
return nil, nil, fmt.Errorf("ProxyGroup %s: the duplicate name", groupName) return nil, nil, fmt.Errorf("proxy group %s: the duplicate name", groupName)
} }
proxies[groupName] = outbound.NewProxy(group) proxies[groupName] = adapter.NewProxy(group)
} }
// initial compatible provider // initial compatible provider
@ -337,12 +359,17 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
for _, v := range proxyList { for _, v := range proxyList {
ps = append(ps, proxies[v]) ps = append(ps, proxies[v])
} }
hc := provider.NewHealthCheck(ps, "", 0) hc := provider.NewHealthCheck(ps, "", 0, true)
pd, _ := provider.NewCompatibleProvider(provider.ReservedName, ps, hc) pd, _ := provider.NewCompatibleProvider(provider.ReservedName, ps, hc)
providersMap[provider.ReservedName] = pd providersMap[provider.ReservedName] = pd
global := outboundgroup.NewSelector("GLOBAL", []provider.ProxyProvider{pd}) global := outboundgroup.NewSelector(
proxies["GLOBAL"] = outbound.NewProxy(global) &outboundgroup.GroupCommonOption{
Name: "GLOBAL",
},
[]provider.ProxyProvider{pd},
)
proxies["GLOBAL"] = adapter.NewProxy(global)
return proxies, providersMap, nil return proxies, providersMap, nil
} }
@ -370,11 +397,11 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, error) {
target = rule[2] target = rule[2]
params = rule[3:] params = rule[3:]
default: default:
return nil, fmt.Errorf("Rules[%d] [%s] error: format invalid", idx, line) return nil, fmt.Errorf("rules[%d] [%s] error: format invalid", idx, line)
} }
if _, ok := proxies[target]; !ok { if _, ok := proxies[target]; !ok {
return nil, fmt.Errorf("Rules[%d] [%s] error: proxy [%s] not found", idx, line, target) return nil, fmt.Errorf("rules[%d] [%s] error: proxy [%s] not found", idx, line, target)
} }
rule = trimArr(rule) rule = trimArr(rule)
@ -382,7 +409,7 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, error) {
parsed, parseErr := R.ParseRule(rule[0], payload, target, params) parsed, parseErr := R.ParseRule(rule[0], payload, target, params)
if parseErr != nil { if parseErr != nil {
return nil, fmt.Errorf("Rules[%d] [%s] error: %s", idx, line, parseErr.Error()) return nil, fmt.Errorf("rules[%d] [%s] error: %s", idx, line, parseErr.Error())
} }
rules = append(rules, parsed) rules = append(rules, parsed)
@ -396,7 +423,7 @@ func parseHosts(cfg *RawConfig) (*trie.DomainTrie, error) {
// add default hosts // add default hosts
if err := tree.Insert("localhost", net.IP{127, 0, 0, 1}); err != nil { if err := tree.Insert("localhost", net.IP{127, 0, 0, 1}); err != nil {
println(err.Error()) log.Errorln("insert localhost to host error: %s", err.Error())
} }
if len(cfg.Hosts) != 0 { if len(cfg.Hosts) != 0 {
@ -476,6 +503,23 @@ func parseNameServer(servers []string) ([]dns.NameServer, error) {
return nameservers, nil return nameservers, nil
} }
func parseNameServerPolicy(nsPolicy map[string]string) (map[string]dns.NameServer, error) {
policy := map[string]dns.NameServer{}
for domain, server := range nsPolicy {
nameservers, err := parseNameServer([]string{server})
if err != nil {
return nil, err
}
if _, valid := trie.ValidAndSplitDomain(domain); !valid {
return nil, fmt.Errorf("DNS ResoverRule invalid domain: %s", domain)
}
policy[domain] = nameservers[0]
}
return policy, nil
}
func parseFallbackIPCIDR(ips []string) ([]*net.IPNet, error) { func parseFallbackIPCIDR(ips []string) ([]*net.IPNet, error) {
ipNets := []*net.IPNet{} ipNets := []*net.IPNet{}
@ -490,9 +534,9 @@ func parseFallbackIPCIDR(ips []string) ([]*net.IPNet, error) {
return ipNets, nil return ipNets, nil
} }
func parseDNS(cfg RawDNS) (*DNS, error) { func parseDNS(cfg RawDNS, hosts *trie.DomainTrie) (*DNS, error) {
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")
} }
dnsCfg := &DNS{ dnsCfg := &DNS{
@ -513,6 +557,10 @@ func parseDNS(cfg RawDNS) (*DNS, error) {
return nil, err return nil, err
} }
if dnsCfg.NameServerPolicy, err = parseNameServerPolicy(cfg.NameServerPolicy); err != nil {
return nil, err
}
if len(cfg.DefaultNameserver) == 0 { if len(cfg.DefaultNameserver) == 0 {
return nil, errors.New("default nameserver should have at least one nameserver") return nil, errors.New("default nameserver should have at least one nameserver")
} }
@ -554,6 +602,11 @@ func parseDNS(cfg RawDNS) (*DNS, error) {
if fallbackip, err := parseFallbackIPCIDR(cfg.FallbackFilter.IPCIDR); err == nil { if fallbackip, err := parseFallbackIPCIDR(cfg.FallbackFilter.IPCIDR); err == nil {
dnsCfg.FallbackFilter.IPCIDR = fallbackip dnsCfg.FallbackFilter.IPCIDR = fallbackip
} }
dnsCfg.FallbackFilter.Domain = cfg.FallbackFilter.Domain
if cfg.UseHosts {
dnsCfg.Hosts = hosts
}
return dnsCfg, nil return dnsCfg, nil
} }

View File

@ -12,7 +12,7 @@ import (
) )
func downloadMMDB(path string) (err error) { func downloadMMDB(path string) (err error) {
resp, err := http.Get("https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb") resp, err := http.Get("https://cdn.jsdelivr.net/gh/Dreamacro/maxmind-geoip@release/Country.mmdb")
if err != nil { if err != nil {
return return
} }
@ -32,18 +32,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: %s", err.Error())
} }
} }
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: %s", err.Error())
} }
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: %s", err.Error())
} }
} }
@ -55,7 +55,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, 0777); err != nil { if err := os.MkdirAll(dir, 0777); err != nil {
return fmt.Errorf("Can't create config directory %s: %s", dir, err.Error()) return fmt.Errorf("can't create config directory %s: %s", dir, err.Error())
} }
} }
@ -64,15 +64,15 @@ 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, 0644) f, err := os.OpenFile(C.Path.Config(), os.O_CREATE|os.O_WRONLY, 0644)
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: %s", C.Path.Config(), err.Error())
} }
f.Write([]byte(`port: 7890`)) f.Write([]byte(`mixed-port: 7890`))
f.Close() f.Close()
} }
// initial mmdb // initial mmdb
if err := initMMDB(); err != nil { if err := initMMDB(); err != nil {
return fmt.Errorf("Can't initial MMDB: %w", err) return fmt.Errorf("can't initial MMDB: %w", err)
} }
return nil return nil
} }

View File

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/Dreamacro/clash/adapters/outboundgroup" "github.com/Dreamacro/clash/adapter/outboundgroup"
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
) )
@ -15,15 +15,6 @@ func trimArr(arr []string) (r []string) {
return return
} }
func or(pointers ...*int) *int {
for _, p := range pointers {
if p != nil {
return p
}
}
return pointers[len(pointers)-1]
}
// Check if ProxyGroups form DAG(Directed Acyclic Graph), and sort all ProxyGroups by dependency order. // Check if ProxyGroups form DAG(Directed Acyclic Graph), and sort all ProxyGroups by dependency order.
// Meanwhile, record the original index in the config file. // Meanwhile, record the original index in the config file.
// If loop is detected, return an error with location of loop. // If loop is detected, return an error with location of loop.
@ -32,7 +23,7 @@ func proxyGroupsDagSort(groupsConfig []map[string]interface{}) error {
indegree int indegree int
// topological order // topological order
topo int topo int
// the origional data in `groupsConfig` // the original data in `groupsConfig`
data map[string]interface{} data map[string]interface{}
// `outdegree` and `from` are used in loop locating // `outdegree` and `from` are used in loop locating
outdegree int outdegree int
@ -74,7 +65,7 @@ func proxyGroupsDagSort(groupsConfig []map[string]interface{}) error {
index := 0 index := 0
queue := make([]string, 0) queue := make([]string, 0)
for name, node := range graph { for name, node := range graph {
// in the begning, put nodes that have `node.indegree == 0` into queue. // in the beginning, put nodes that have `node.indegree == 0` into queue.
if node.indegree == 0 { if node.indegree == 0 {
queue = append(queue, name) queue = append(queue, name)
} }
@ -153,5 +144,5 @@ func proxyGroupsDagSort(groupsConfig []map[string]interface{}) error {
loopElements = append(loopElements, name) loopElements = append(loopElements, name)
delete(graph, name) delete(graph, name)
} }
return fmt.Errorf("Loop is detected in ProxyGroup, please check following ProxyGroups: %v", loopElements) return fmt.Errorf("loop is detected in ProxyGroup, please check following ProxyGroups: %v", loopElements)
} }

View File

@ -13,6 +13,7 @@ const (
Reject Reject
Shadowsocks Shadowsocks
ShadowsocksR
Snell Snell
Socks5 Socks5
Http Http
@ -26,10 +27,9 @@ const (
LoadBalance LoadBalance
) )
type ServerAdapter interface { const (
net.Conn DefaultTCPTimeout = 5 * time.Second
Metadata() *Metadata )
}
type Connection interface { type Connection interface {
Chains() Chain Chains() Chain
@ -49,6 +49,15 @@ func (c Chain) String() string {
} }
} }
func (c Chain) Last() string {
switch len(c) {
case 0:
return ""
default:
return c[0]
}
}
type Conn interface { type Conn interface {
net.Conn net.Conn
Connection Connection
@ -64,8 +73,21 @@ type PacketConn interface {
type ProxyAdapter interface { type ProxyAdapter interface {
Name() string Name() string
Type() AdapterType Type() AdapterType
// StreamConn wraps a protocol around net.Conn with Metadata.
//
// Examples:
// conn, _ := net.Dial("tcp", "host:port")
// conn, _ = adapter.StreamConn(conn, metadata)
//
// It returns a C.Conn with protocol which start with
// a new session (if any)
StreamConn(c net.Conn, metadata *Metadata) (net.Conn, error) StreamConn(c net.Conn, metadata *Metadata) (net.Conn, error)
// DialContext return a C.Conn with protocol which
// contains multiplexing-related reuse logic (if any)
DialContext(ctx context.Context, metadata *Metadata) (Conn, error) DialContext(ctx context.Context, metadata *Metadata) (Conn, error)
DialUDP(metadata *Metadata) (PacketConn, error) DialUDP(metadata *Metadata) (PacketConn, error)
SupportUDP() bool SupportUDP() bool
MarshalJSON() ([]byte, error) MarshalJSON() ([]byte, error)
@ -100,6 +122,8 @@ func (at AdapterType) String() string {
case Shadowsocks: case Shadowsocks:
return "Shadowsocks" return "Shadowsocks"
case ShadowsocksR:
return "ShadowsocksR"
case Snell: case Snell:
return "Snell" return "Snell"
case Socks5: case Socks5:
@ -134,7 +158,7 @@ type UDPPacket interface {
// WriteBack writes the payload with source IP/Port equals addr // WriteBack writes the payload with source IP/Port equals addr
// - variable source IP/Port is important to STUN // - variable source IP/Port is important to STUN
// - if addr is not provided, WriteBack will wirte out UDP packet with SourceIP/Prot equals to origional Target, // - if addr is not provided, WriteBack will write out UDP packet with SourceIP/Port equals to original Target,
// this is important when using Fake-IP. // this is important when using Fake-IP.
WriteBack(b []byte, addr net.Addr) (n int, err error) WriteBack(b []byte, addr net.Addr) (n int, err error)

23
constant/context.go Normal file
View File

@ -0,0 +1,23 @@
package constant
import (
"net"
"github.com/gofrs/uuid"
)
type PlainContext interface {
ID() uuid.UUID
}
type ConnContext interface {
PlainContext
Metadata() *Metadata
Conn() net.Conn
}
type PacketConnContext interface {
PlainContext
Metadata() *Metadata
PacketConn() net.PacketConn
}

View File

@ -19,6 +19,7 @@ const (
HTTPCONNECT HTTPCONNECT
SOCKS SOCKS
REDIR REDIR
TPROXY
) )
type NetWork int type NetWork int
@ -46,6 +47,8 @@ func (t Type) String() string {
return "Socks5" return "Socks5"
case REDIR: case REDIR:
return "Redir" return "Redir"
case TPROXY:
return "TProxy"
default: default:
return "Unknown" return "Unknown"
} }

View File

@ -56,3 +56,7 @@ func (p *path) Resolve(path string) string {
func (p *path) MMDB() string { func (p *path) MMDB() string {
return P.Join(p.homeDir, "Country.mmdb") return P.Join(p.homeDir, "Country.mmdb")
} }
func (p *path) Cache() string {
return P.Join(p.homeDir, ".cache")
}

View File

@ -10,6 +10,7 @@ const (
SrcIPCIDR SrcIPCIDR
SrcPort SrcPort
DstPort DstPort
Process
MATCH MATCH
) )
@ -33,6 +34,8 @@ func (rt RuleType) String() string {
return "SrcPort" return "SrcPort"
case DstPort: case DstPort:
return "DstPort" return "DstPort"
case Process:
return "Process"
case MATCH: case MATCH:
return "Match" return "Match"
default: default:
@ -45,5 +48,5 @@ type Rule interface {
Match(metadata *Metadata) bool Match(metadata *Metadata) bool
Adapter() string Adapter() string
Payload() string Payload() string
NoResolveIP() bool ShouldResolveIP() bool
} }

39
context/conn.go Normal file
View File

@ -0,0 +1,39 @@
package context
import (
"net"
C "github.com/Dreamacro/clash/constant"
"github.com/gofrs/uuid"
)
type ConnContext struct {
id uuid.UUID
metadata *C.Metadata
conn net.Conn
}
func NewConnContext(conn net.Conn, metadata *C.Metadata) *ConnContext {
id, _ := uuid.NewV4()
return &ConnContext{
id: id,
metadata: metadata,
conn: conn,
}
}
// ID implement C.ConnContext ID
func (c *ConnContext) ID() uuid.UUID {
return c.id
}
// Metadata implement C.ConnContext Metadata
func (c *ConnContext) Metadata() *C.Metadata {
return c.metadata
}
// Conn implement C.ConnContext Conn
func (c *ConnContext) Conn() net.Conn {
return c.conn
}

41
context/dns.go Normal file
View File

@ -0,0 +1,41 @@
package context
import (
"github.com/gofrs/uuid"
"github.com/miekg/dns"
)
const (
DNSTypeHost = "host"
DNSTypeFakeIP = "fakeip"
DNSTypeRaw = "raw"
)
type DNSContext struct {
id uuid.UUID
msg *dns.Msg
tp string
}
func NewDNSContext(msg *dns.Msg) *DNSContext {
id, _ := uuid.NewV4()
return &DNSContext{
id: id,
msg: msg,
}
}
// ID implement C.PlainContext ID
func (c *DNSContext) ID() uuid.UUID {
return c.id
}
// SetType set type of response
func (c *DNSContext) SetType(tp string) {
c.tp = tp
}
// Type return type of response
func (c *DNSContext) Type() string {
return c.tp
}

43
context/packetconn.go Normal file
View File

@ -0,0 +1,43 @@
package context
import (
"net"
C "github.com/Dreamacro/clash/constant"
"github.com/gofrs/uuid"
)
type PacketConnContext struct {
id uuid.UUID
metadata *C.Metadata
packetConn net.PacketConn
}
func NewPacketConnContext(metadata *C.Metadata) *PacketConnContext {
id, _ := uuid.NewV4()
return &PacketConnContext{
id: id,
metadata: metadata,
}
}
// ID implement C.PacketConnContext ID
func (pc *PacketConnContext) ID() uuid.UUID {
return pc.id
}
// Metadata implement C.PacketConnContext Metadata
func (pc *PacketConnContext) Metadata() *C.Metadata {
return pc.metadata
}
// PacketConn implement C.PacketConnContext PacketConn
func (pc *PacketConnContext) PacketConn() net.PacketConn {
return pc.packetConn
}
// InjectPacketConn injectPacketConn manually
func (pc *PacketConnContext) InjectPacketConn(pconn C.PacketConn) {
pc.packetConn = pconn
}

View File

@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver"
D "github.com/miekg/dns" D "github.com/miekg/dns"
) )
@ -28,8 +29,7 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err
// a default ip dns // a default ip dns
ip = net.ParseIP(c.host) ip = net.ParseIP(c.host)
} else { } else {
var err error if ip, err = resolver.ResolveIPWithResolver(c.host, c.r); err != nil {
if ip, err = c.r.ResolveIP(c.host); err != nil {
return nil, fmt.Errorf("use default dns resolve failed: %w", err) return nil, fmt.Errorf("use default dns resolve failed: %w", err)
} }
} }
@ -39,7 +39,7 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err
return nil, err return nil, err
} }
if dialer.DialHook != nil { if ip != nil && ip.IsGlobalUnicast() && dialer.DialHook != nil {
network := "udp" network := "udp"
if strings.HasPrefix(c.Client.Net, "tcp") { if strings.HasPrefix(c.Client.Net, "tcp") {
network = "tcp" network = "tcp"

View File

@ -3,12 +3,12 @@ package dns
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/tls"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver"
D "github.com/miekg/dns" D "github.com/miekg/dns"
) )
@ -75,7 +75,6 @@ func newDoHClient(url string, r *Resolver) *dohClient {
return &dohClient{ return &dohClient{
url: url, url: url,
transport: &http.Transport{ transport: &http.Transport{
TLSClientConfig: &tls.Config{ClientSessionCache: globalSessionCache},
ForceAttemptHTTP2: true, ForceAttemptHTTP2: true,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, err := net.SplitHostPort(addr) host, port, err := net.SplitHostPort(addr)
@ -83,12 +82,12 @@ func newDoHClient(url string, r *Resolver) *dohClient {
return nil, err return nil, err
} }
ip, err := r.ResolveIPv4(host) ip, err := resolver.ResolveIPWithResolver(host, r)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return dialer.DialContext(ctx, "tcp4", net.JoinHostPort(ip.String(), port)) return dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip.String(), port))
}, },
}, },
} }

88
dns/enhancer.go Normal file
View File

@ -0,0 +1,88 @@
package dns
import (
"net"
"github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/component/fakeip"
)
type ResolverEnhancer struct {
mode EnhancedMode
fakePool *fakeip.Pool
mapping *cache.LruCache
}
func (h *ResolverEnhancer) FakeIPEnabled() bool {
return h.mode == FAKEIP
}
func (h *ResolverEnhancer) MappingEnabled() bool {
return h.mode == FAKEIP || h.mode == MAPPING
}
func (h *ResolverEnhancer) IsExistFakeIP(ip net.IP) bool {
if !h.FakeIPEnabled() {
return false
}
if pool := h.fakePool; pool != nil {
return pool.Exist(ip)
}
return false
}
func (h *ResolverEnhancer) IsFakeIP(ip net.IP) bool {
if !h.FakeIPEnabled() {
return false
}
if pool := h.fakePool; pool != nil {
return pool.IPNet().Contains(ip) && !pool.Gateway().Equal(ip)
}
return false
}
func (h *ResolverEnhancer) FindHostByIP(ip net.IP) (string, bool) {
if pool := h.fakePool; pool != nil {
if host, existed := pool.LookBack(ip); existed {
return host, true
}
}
if mapping := h.mapping; mapping != nil {
if host, existed := h.mapping.Get(ip.String()); existed {
return host.(string), true
}
}
return "", false
}
func (h *ResolverEnhancer) PatchFrom(o *ResolverEnhancer) {
if h.mapping != nil && o.mapping != nil {
o.mapping.CloneTo(h.mapping)
}
if h.fakePool != nil && o.fakePool != nil {
h.fakePool.PatchFrom(o.fakePool)
}
}
func NewEnhancer(cfg Config) *ResolverEnhancer {
var fakePool *fakeip.Pool
var mapping *cache.LruCache
if cfg.EnhancedMode != NORMAL {
fakePool = cfg.Pool
mapping = cache.NewLRUCache(cache.WithSize(4096), cache.WithStale(true))
}
return &ResolverEnhancer{
mode: cfg.EnhancedMode,
fakePool: fakePool,
mapping: mapping,
}
}

View File

@ -4,9 +4,10 @@ import (
"net" "net"
"github.com/Dreamacro/clash/component/mmdb" "github.com/Dreamacro/clash/component/mmdb"
"github.com/Dreamacro/clash/component/trie"
) )
type fallbackFilter interface { type fallbackIPFilter interface {
Match(net.IP) bool Match(net.IP) bool
} }
@ -24,3 +25,22 @@ type ipnetFilter struct {
func (inf *ipnetFilter) Match(ip net.IP) bool { func (inf *ipnetFilter) Match(ip net.IP) bool {
return inf.ipnet.Contains(ip) return inf.ipnet.Contains(ip)
} }
type fallbackDomainFilter interface {
Match(domain string) bool
}
type domainFilter struct {
tree *trie.DomainTrie
}
func NewDomainFilter(domains []string) *domainFilter {
df := domainFilter{tree: trie.New()}
for _, domain := range domains {
df.tree.Insert(domain, "")
}
return &df
}
func (df *domainFilter) Match(domain string) bool {
return df.tree.Search(domain) != nil
}

View File

@ -1,41 +1,121 @@
package dns package dns
import ( import (
"net"
"strings" "strings"
"time"
"github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/component/fakeip" "github.com/Dreamacro/clash/component/fakeip"
"github.com/Dreamacro/clash/component/trie"
"github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
D "github.com/miekg/dns" D "github.com/miekg/dns"
) )
type handler func(w D.ResponseWriter, r *D.Msg) type handler func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error)
type middleware func(next handler) handler type middleware func(next handler) handler
func withFakeIP(fakePool *fakeip.Pool) middleware { func withHosts(hosts *trie.DomainTrie) middleware {
return func(next handler) handler { return func(next handler) handler {
return func(w D.ResponseWriter, r *D.Msg) { return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) {
q := r.Question[0] q := r.Question[0]
if q.Qtype == D.TypeAAAA { if !isIPRequest(q) {
msg := &D.Msg{} return next(ctx, r)
msg.Answer = []D.RR{} }
msg.SetRcode(r, D.RcodeSuccess) record := hosts.Search(strings.TrimRight(q.Name, "."))
msg.Authoritative = true if record == nil {
msg.RecursionAvailable = true return next(ctx, r)
}
w.WriteMsg(msg) ip := record.Data.(net.IP)
return msg := r.Copy()
} else if q.Qtype != D.TypeA {
next(w, r) if v4 := ip.To4(); v4 != nil && q.Qtype == D.TypeA {
return rr := &D.A{}
rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: dnsDefaultTTL}
rr.A = v4
msg.Answer = []D.RR{rr}
} else if v6 := ip.To16(); v6 != nil && q.Qtype == D.TypeAAAA {
rr := &D.AAAA{}
rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeAAAA, Class: D.ClassINET, Ttl: dnsDefaultTTL}
rr.AAAA = v6
msg.Answer = []D.RR{rr}
} else {
return next(ctx, r)
}
ctx.SetType(context.DNSTypeHost)
msg.SetRcode(r, D.RcodeSuccess)
msg.Authoritative = true
msg.RecursionAvailable = true
return msg, nil
}
}
}
func withMapping(mapping *cache.LruCache) middleware {
return func(next handler) handler {
return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) {
q := r.Question[0]
if !isIPRequest(q) {
return next(ctx, r)
}
msg, err := next(ctx, r)
if err != nil {
return nil, err
} }
host := strings.TrimRight(q.Name, ".")
for _, ans := range msg.Answer {
var ip net.IP
var ttl uint32
switch a := ans.(type) {
case *D.A:
ip = a.A
ttl = a.Hdr.Ttl
case *D.AAAA:
ip = a.AAAA
ttl = a.Hdr.Ttl
default:
continue
}
mapping.SetWithExpire(ip.String(), host, time.Now().Add(time.Second*time.Duration(ttl)))
}
return msg, nil
}
}
}
func withFakeIP(fakePool *fakeip.Pool) middleware {
return func(next handler) handler {
return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) {
q := r.Question[0]
host := strings.TrimRight(q.Name, ".") host := strings.TrimRight(q.Name, ".")
if fakePool.LookupHost(host) { if fakePool.LookupHost(host) {
next(w, r) return next(ctx, r)
return }
switch q.Qtype {
case D.TypeAAAA, D.TypeSVCB, D.TypeHTTPS:
return handleMsgWithEmptyAnswer(r), nil
}
if q.Qtype != D.TypeA {
return next(ctx, r)
} }
rr := &D.A{} rr := &D.A{}
@ -45,44 +125,36 @@ func withFakeIP(fakePool *fakeip.Pool) middleware {
msg := r.Copy() msg := r.Copy()
msg.Answer = []D.RR{rr} msg.Answer = []D.RR{rr}
ctx.SetType(context.DNSTypeFakeIP)
setMsgTTL(msg, 1) setMsgTTL(msg, 1)
msg.SetRcode(r, D.RcodeSuccess) msg.SetRcode(r, D.RcodeSuccess)
msg.Authoritative = true msg.Authoritative = true
msg.RecursionAvailable = true msg.RecursionAvailable = true
w.WriteMsg(msg) return msg, nil
return
} }
} }
} }
func withResolver(resolver *Resolver) handler { func withResolver(resolver *Resolver) handler {
return func(w D.ResponseWriter, r *D.Msg) { return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) {
ctx.SetType(context.DNSTypeRaw)
q := r.Question[0] q := r.Question[0]
// return a empty AAAA msg when ipv6 disabled // return a empty AAAA msg when ipv6 disabled
if !resolver.ipv6 && q.Qtype == D.TypeAAAA { if !resolver.ipv6 && q.Qtype == D.TypeAAAA {
msg := &D.Msg{} return handleMsgWithEmptyAnswer(r), nil
msg.Answer = []D.RR{}
msg.SetRcode(r, D.RcodeSuccess)
msg.Authoritative = true
msg.RecursionAvailable = true
w.WriteMsg(msg)
return
} }
msg, err := resolver.Exchange(r) msg, err := resolver.Exchange(r)
if err != nil { if err != nil {
log.Debugln("[DNS Server] Exchange %s failed: %v", q.String(), err) log.Debugln("[DNS Server] Exchange %s failed: %v", q.String(), err)
D.HandleFailed(w, r) return msg, err
return
} }
msg.SetRcode(r, msg.Rcode) msg.SetRcode(r, msg.Rcode)
msg.Authoritative = true msg.Authoritative = true
w.WriteMsg(msg)
return return msg, nil
} }
} }
@ -97,11 +169,19 @@ func compose(middlewares []middleware, endpoint handler) handler {
return h return h
} }
func newHandler(resolver *Resolver) handler { func newHandler(resolver *Resolver, mapper *ResolverEnhancer) handler {
middlewares := []middleware{} middlewares := []middleware{}
if resolver.FakeIPEnabled() { if resolver.hosts != nil {
middlewares = append(middlewares, withFakeIP(resolver.pool)) middlewares = append(middlewares, withHosts(resolver.hosts))
}
if mapper.mode == FAKEIP {
middlewares = append(middlewares, withFakeIP(mapper.fakePool))
}
if mapper.mode != NORMAL {
middlewares = append(middlewares, withMapping(mapper.mapping))
} }
return compose(middlewares, withResolver(resolver)) return compose(middlewares, withResolver(resolver))

View File

@ -2,7 +2,6 @@ package dns
import ( import (
"context" "context"
"crypto/tls"
"errors" "errors"
"fmt" "fmt"
"math/rand" "math/rand"
@ -14,15 +13,12 @@ import (
"github.com/Dreamacro/clash/common/picker" "github.com/Dreamacro/clash/common/picker"
"github.com/Dreamacro/clash/component/fakeip" "github.com/Dreamacro/clash/component/fakeip"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
"github.com/Dreamacro/clash/component/trie"
D "github.com/miekg/dns" D "github.com/miekg/dns"
"golang.org/x/sync/singleflight" "golang.org/x/sync/singleflight"
) )
var (
globalSessionCache = tls.NewLRUClientSessionCache(64)
)
type dnsClient interface { type dnsClient interface {
Exchange(m *D.Msg) (msg *D.Msg, err error) Exchange(m *D.Msg) (msg *D.Msg, err error)
ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error)
@ -34,15 +30,15 @@ type result struct {
} }
type Resolver struct { type Resolver struct {
ipv6 bool ipv6 bool
mapping bool hosts *trie.DomainTrie
fakeip bool main []dnsClient
pool *fakeip.Pool fallback []dnsClient
main []dnsClient fallbackDomainFilters []fallbackDomainFilter
fallback []dnsClient fallbackIPFilters []fallbackIPFilter
fallbackFilters []fallbackFilter group singleflight.Group
group singleflight.Group lruCache *cache.LruCache
lruCache *cache.LruCache policy *trie.DomainTrie
} }
// ResolveIP request with TypeA and TypeAAAA, priority return TypeA // ResolveIP request with TypeA and TypeAAAA, priority return TypeA
@ -80,8 +76,8 @@ func (r *Resolver) ResolveIPv6(host string) (ip net.IP, err error) {
return r.resolveIP(host, D.TypeAAAA) return r.resolveIP(host, D.TypeAAAA)
} }
func (r *Resolver) shouldFallback(ip net.IP) bool { func (r *Resolver) shouldIPFallback(ip net.IP) bool {
for _, filter := range r.fallbackFilters { for _, filter := range r.fallbackIPFilters {
if filter.Match(ip) { if filter.Match(ip) {
return true return true
} }
@ -104,7 +100,7 @@ func (r *Resolver) Exchange(m *D.Msg) (msg *D.Msg, err error) {
setMsgTTL(msg, uint32(1)) // Continue fetch setMsgTTL(msg, uint32(1)) // Continue fetch
go r.exchangeWithoutCache(m) go r.exchangeWithoutCache(m)
} else { } else {
setMsgTTL(msg, uint32(expireTime.Sub(time.Now()).Seconds())) setMsgTTL(msg, uint32(time.Until(expireTime).Seconds()))
} }
return return
} }
@ -115,26 +111,25 @@ func (r *Resolver) Exchange(m *D.Msg) (msg *D.Msg, err error) {
func (r *Resolver) exchangeWithoutCache(m *D.Msg) (msg *D.Msg, err error) { func (r *Resolver) exchangeWithoutCache(m *D.Msg) (msg *D.Msg, err error) {
q := m.Question[0] q := m.Question[0]
defer func() { ret, err, shared := r.group.Do(q.String(), func() (result interface{}, err error) {
if msg == nil { defer func() {
return if err != nil {
} return
putMsgToCache(r.lruCache, q.String(), msg)
if r.mapping || r.fakeip {
ips := r.msgToIP(msg)
for _, ip := range ips {
putMsgToCache(r.lruCache, ip.String(), msg)
} }
}
}()
ret, err, shared := r.group.Do(q.String(), func() (interface{}, error) { msg := result.(*D.Msg)
putMsgToCache(r.lruCache, q.String(), msg)
}()
isIPReq := isIPRequest(q) isIPReq := isIPRequest(q)
if isIPReq { if isIPReq {
return r.fallbackExchange(m) return r.ipExchange(m)
} }
if matched := r.matchPolicy(m); len(matched) != 0 {
return r.batchExchange(matched, m)
}
return r.batchExchange(r.main, m) return r.batchExchange(r.main, m)
}) })
@ -148,42 +143,8 @@ func (r *Resolver) exchangeWithoutCache(m *D.Msg) (msg *D.Msg, err error) {
return return
} }
// IPToHost return fake-ip or redir-host mapping host
func (r *Resolver) IPToHost(ip net.IP) (string, bool) {
if r.fakeip {
record, existed := r.pool.LookBack(ip)
if existed {
return record, true
}
}
cache, _ := r.lruCache.Get(ip.String())
if cache == nil {
return "", false
}
fqdn := cache.(*D.Msg).Question[0].Name
return strings.TrimRight(fqdn, "."), true
}
func (r *Resolver) IsMapping() bool {
return r.mapping
}
// FakeIPEnabled returns if fake-ip is enabled
func (r *Resolver) FakeIPEnabled() bool {
return r.fakeip
}
// IsFakeIP determine if given ip is a fake-ip
func (r *Resolver) IsFakeIP(ip net.IP) bool {
if r.FakeIPEnabled() {
return r.pool.Exist(ip)
}
return false
}
func (r *Resolver) batchExchange(clients []dnsClient, m *D.Msg) (msg *D.Msg, err error) { func (r *Resolver) batchExchange(clients []dnsClient, m *D.Msg) (msg *D.Msg, err error) {
fast, ctx := picker.WithTimeout(context.Background(), time.Second*5) fast, ctx := picker.WithTimeout(context.Background(), resolver.DefaultDNSTimeout)
for _, client := range clients { for _, client := range clients {
r := client r := client
fast.Go(func() (interface{}, error) { fast.Go(func() (interface{}, error) {
@ -199,7 +160,7 @@ func (r *Resolver) batchExchange(clients []dnsClient, m *D.Msg) (msg *D.Msg, err
elm := fast.Wait() elm := fast.Wait()
if elm == nil { if elm == nil {
err := errors.New("All DNS requests failed") err := errors.New("all DNS requests failed")
if fErr := fast.Error(); fErr != nil { if fErr := fast.Error(); fErr != nil {
err = fmt.Errorf("%w, first error: %s", err, fErr.Error()) err = fmt.Errorf("%w, first error: %s", err, fErr.Error())
} }
@ -210,19 +171,72 @@ func (r *Resolver) batchExchange(clients []dnsClient, m *D.Msg) (msg *D.Msg, err
return return
} }
func (r *Resolver) fallbackExchange(m *D.Msg) (msg *D.Msg, err error) { func (r *Resolver) matchPolicy(m *D.Msg) []dnsClient {
if r.policy == nil {
return nil
}
domain := r.msgToDomain(m)
if domain == "" {
return nil
}
record := r.policy.Search(domain)
if record == nil {
return nil
}
return record.Data.([]dnsClient)
}
func (r *Resolver) shouldOnlyQueryFallback(m *D.Msg) bool {
if r.fallback == nil || len(r.fallbackDomainFilters) == 0 {
return false
}
domain := r.msgToDomain(m)
if domain == "" {
return false
}
for _, df := range r.fallbackDomainFilters {
if df.Match(domain) {
return true
}
}
return false
}
func (r *Resolver) ipExchange(m *D.Msg) (msg *D.Msg, err error) {
if matched := r.matchPolicy(m); len(matched) != 0 {
res := <-r.asyncExchange(matched, m)
return res.Msg, res.Error
}
onlyFallback := r.shouldOnlyQueryFallback(m)
if onlyFallback {
res := <-r.asyncExchange(r.fallback, m)
return res.Msg, res.Error
}
msgCh := r.asyncExchange(r.main, m) msgCh := r.asyncExchange(r.main, m)
if r.fallback == nil {
if r.fallback == nil { // directly return if no fallback servers are available
res := <-msgCh res := <-msgCh
msg, err = res.Msg, res.Error msg, err = res.Msg, res.Error
return return
} }
fallbackMsg := r.asyncExchange(r.fallback, m) fallbackMsg := r.asyncExchange(r.fallback, m)
res := <-msgCh res := <-msgCh
if res.Error == nil { if res.Error == nil {
if ips := r.msgToIP(res.Msg); len(ips) != 0 { if ips := msgToIP(res.Msg); len(ips) != 0 {
if !r.shouldFallback(ips[0]) { if !r.shouldIPFallback(ips[0]) {
msg = res.Msg msg = res.Msg // no need to wait for fallback result
err = res.Error err = res.Error
return msg, err return msg, err
} }
@ -255,7 +269,7 @@ func (r *Resolver) resolveIP(host string, dnsType uint16) (ip net.IP, err error)
return nil, err return nil, err
} }
ips := r.msgToIP(msg) ips := msgToIP(msg)
ipLength := len(ips) ipLength := len(ips)
if ipLength == 0 { if ipLength == 0 {
return nil, resolver.ErrIPNotFound return nil, resolver.ErrIPNotFound
@ -265,19 +279,12 @@ func (r *Resolver) resolveIP(host string, dnsType uint16) (ip net.IP, err error)
return return
} }
func (r *Resolver) msgToIP(msg *D.Msg) []net.IP { func (r *Resolver) msgToDomain(msg *D.Msg) string {
ips := []net.IP{} if len(msg.Question) > 0 {
return strings.TrimRight(msg.Question[0].Name, ".")
for _, answer := range msg.Answer {
switch ans := answer.(type) {
case *D.AAAA:
ips = append(ips, ans.AAAA)
case *D.A:
ips = append(ips, ans.A)
}
} }
return ips return ""
} }
func (r *Resolver) asyncExchange(client []dnsClient, msg *D.Msg) <-chan *result { func (r *Resolver) asyncExchange(client []dnsClient, msg *D.Msg) <-chan *result {
@ -297,6 +304,7 @@ type NameServer struct {
type FallbackFilter struct { type FallbackFilter struct {
GeoIP bool GeoIP bool
IPCIDR []*net.IPNet IPCIDR []*net.IPNet
Domain []string
} }
type Config struct { type Config struct {
@ -306,9 +314,11 @@ type Config struct {
EnhancedMode EnhancedMode EnhancedMode EnhancedMode
FallbackFilter FallbackFilter FallbackFilter FallbackFilter
Pool *fakeip.Pool Pool *fakeip.Pool
Hosts *trie.DomainTrie
Policy map[string]NameServer
} }
func New(config Config) *Resolver { func NewResolver(config Config) *Resolver {
defaultResolver := &Resolver{ defaultResolver := &Resolver{
main: transform(config.Default, nil), main: transform(config.Default, nil),
lruCache: cache.NewLRUCache(cache.WithSize(4096), cache.WithStale(true)), lruCache: cache.NewLRUCache(cache.WithSize(4096), cache.WithStale(true)),
@ -318,23 +328,33 @@ func New(config Config) *Resolver {
ipv6: config.IPv6, ipv6: config.IPv6,
main: transform(config.Main, defaultResolver), main: transform(config.Main, defaultResolver),
lruCache: cache.NewLRUCache(cache.WithSize(4096), cache.WithStale(true)), lruCache: cache.NewLRUCache(cache.WithSize(4096), cache.WithStale(true)),
mapping: config.EnhancedMode == MAPPING, hosts: config.Hosts,
fakeip: config.EnhancedMode == FAKEIP,
pool: config.Pool,
} }
if len(config.Fallback) != 0 { if len(config.Fallback) != 0 {
r.fallback = transform(config.Fallback, defaultResolver) r.fallback = transform(config.Fallback, defaultResolver)
} }
fallbackFilters := []fallbackFilter{} if len(config.Policy) != 0 {
r.policy = trie.New()
for domain, nameserver := range config.Policy {
r.policy.Insert(domain, transform([]NameServer{nameserver}, defaultResolver))
}
}
fallbackIPFilters := []fallbackIPFilter{}
if config.FallbackFilter.GeoIP { if config.FallbackFilter.GeoIP {
fallbackFilters = append(fallbackFilters, &geoipFilter{}) fallbackIPFilters = append(fallbackIPFilters, &geoipFilter{})
} }
for _, ipnet := range config.FallbackFilter.IPCIDR { for _, ipnet := range config.FallbackFilter.IPCIDR {
fallbackFilters = append(fallbackFilters, &ipnetFilter{ipnet: ipnet}) fallbackIPFilters = append(fallbackIPFilters, &ipnetFilter{ipnet: ipnet})
}
r.fallbackIPFilters = fallbackIPFilters
if len(config.FallbackFilter.Domain) != 0 {
fallbackDomainFilters := []fallbackDomainFilter{NewDomainFilter(config.FallbackFilter.Domain)}
r.fallbackDomainFilters = fallbackDomainFilters
} }
r.fallbackFilters = fallbackFilters
return r return r
} }

View File

@ -1,9 +1,11 @@
package dns package dns
import ( import (
"errors"
"net" "net"
"github.com/Dreamacro/clash/common/sockopt" "github.com/Dreamacro/clash/common/sockopt"
"github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
D "github.com/miekg/dns" D "github.com/miekg/dns"
@ -21,22 +23,33 @@ type Server struct {
handler handler handler handler
} }
// ServeDNS implement D.Handler ServeDNS
func (s *Server) ServeDNS(w D.ResponseWriter, r *D.Msg) { func (s *Server) ServeDNS(w D.ResponseWriter, r *D.Msg) {
if len(r.Question) == 0 { msg, err := handlerWithContext(s.handler, r)
if err != nil {
D.HandleFailed(w, r) D.HandleFailed(w, r)
return return
} }
msg.Compress = true
w.WriteMsg(msg)
}
s.handler(w, r) func handlerWithContext(handler handler, msg *D.Msg) (*D.Msg, error) {
if len(msg.Question) == 0 {
return nil, errors.New("at least one question is required")
}
ctx := context.NewDNSContext(msg)
return handler(ctx, msg)
} }
func (s *Server) setHandler(handler handler) { func (s *Server) setHandler(handler handler) {
s.handler = handler s.handler = handler
} }
func ReCreateServer(addr string, resolver *Resolver) error { func ReCreateServer(addr string, resolver *Resolver, mapper *ResolverEnhancer) error {
if addr == address && resolver != nil { if addr == address && resolver != nil {
handler := newHandler(resolver) handler := newHandler(resolver, mapper)
server.setHandler(handler) server.setHandler(handler)
return nil return nil
} }
@ -68,7 +81,7 @@ func ReCreateServer(addr string, resolver *Resolver) error {
} }
address = addr address = addr
handler := newHandler(resolver) handler := newHandler(resolver, mapper)
server = &Server{handler: handler} server = &Server{handler: handler}
server.Server = &D.Server{Addr: addr, PacketConn: p, Handler: server} server.Server = &D.Server{Addr: addr, PacketConn: p, Handler: server}

View File

@ -127,7 +127,6 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient {
Client: &D.Client{ Client: &D.Client{
Net: s.Net, Net: s.Net,
TLSConfig: &tls.Config{ TLSConfig: &tls.Config{
ClientSessionCache: globalSessionCache,
// alpn identifier, see https://tools.ietf.org/html/draft-hoffman-dprive-dns-tls-alpn-00#page-6 // alpn identifier, see https://tools.ietf.org/html/draft-hoffman-dprive-dns-tls-alpn-00#page-6
NextProtos: []string{"dns"}, NextProtos: []string{"dns"},
ServerName: host, ServerName: host,
@ -142,3 +141,29 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient {
} }
return ret return ret
} }
func handleMsgWithEmptyAnswer(r *D.Msg) *D.Msg {
msg := &D.Msg{}
msg.Answer = []D.RR{}
msg.SetRcode(r, D.RcodeSuccess)
msg.Authoritative = true
msg.RecursionAvailable = true
return msg
}
func msgToIP(msg *D.Msg) []net.IP {
ips := []net.IP{}
for _, answer := range msg.Answer {
switch ans := answer.(type) {
case *D.AAAA:
ips = append(ips, ans.AAAA)
case *D.A:
ips = append(ips, ans.A)
}
}
return ips
}

30
go.mod
View File

@ -1,22 +1,22 @@
module github.com/Dreamacro/clash module github.com/Dreamacro/clash
go 1.14 go 1.16
require ( require (
github.com/Dreamacro/go-shadowsocks2 v0.1.5 github.com/Dreamacro/go-shadowsocks2 v0.1.7
github.com/eapache/queue v1.1.0 // indirect github.com/go-chi/chi/v5 v5.0.3
github.com/go-chi/chi v4.1.2+incompatible github.com/go-chi/cors v1.2.0
github.com/go-chi/cors v1.1.1
github.com/go-chi/render v1.0.1 github.com/go-chi/render v1.0.1
github.com/gofrs/uuid v3.3.0+incompatible github.com/gofrs/uuid v4.0.0+incompatible
github.com/gorilla/websocket v1.4.2 github.com/gorilla/websocket v1.4.2
github.com/miekg/dns v1.1.29 github.com/miekg/dns v1.1.43
github.com/oschwald/geoip2-golang v1.4.0 github.com/oschwald/geoip2-golang v1.5.0
github.com/sirupsen/logrus v1.6.0 github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.7.0
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 go.uber.org/atomic v1.8.0
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a golang.org/x/net v0.0.0-20210614182718-04defd469f4e
gopkg.in/eapache/channels.v1 v1.1.0 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/yaml.v2 v2.3.0 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
gopkg.in/yaml.v2 v2.4.0
) )

94
go.sum
View File

@ -1,68 +1,60 @@
github.com/Dreamacro/go-shadowsocks2 v0.1.5 h1:BizWSjmwzAyQoslz6YhJYMiAGT99j9cnm9zlxVr+kyI= github.com/Dreamacro/go-shadowsocks2 v0.1.7 h1:8CtbE1HoPPMfrQZGXmlluq6dO2lL31W6WRRE8fabc4Q=
github.com/Dreamacro/go-shadowsocks2 v0.1.5/go.mod h1:LSXCjyHesPY3pLjhwff1mQX72ItcBT/N2xNC685cYeU= github.com/Dreamacro/go-shadowsocks2 v0.1.7/go.mod h1:8p5G4cAj5ZlXwUR+Ww63gfSikr8kvw8uw3TDwLAJpUc=
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=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/go-chi/chi/v5 v5.0.3 h1:khYQBdPivkYG1s1TAzDQG1f6eX4kD2TItYVZexL5rS4=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/go-chi/chi/v5 v5.0.3/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= github.com/go-chi/cors v1.2.0 h1:tV1g1XENQ8ku4Bq3K9ub2AtgG+p16SmzeMSGTwrOKdE=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/cors v1.2.0/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/cors v1.1.1 h1:eHuqxsIw89iXcWnWUN8R72JMibABJTN/4IOYI5WERvw=
github.com/go-chi/cors v1.1.1/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg= github.com/oschwald/geoip2-golang v1.5.0 h1:igg2yQIrrcRccB1ytFXqBfOHCjXWIoMv85lVJ1ONZzw=
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/oschwald/geoip2-golang v1.5.0/go.mod h1:xdvYt5xQzB8ORWFqPnqMwZpCpgNagttWdoZLlJQzg7s=
github.com/oschwald/geoip2-golang v1.4.0 h1:5RlrjCgRyIGDz/mBmPfnAF4h8k0IAcRv9PvrpOfz+Ug= github.com/oschwald/maxminddb-golang v1.8.0 h1:Uh/DSnGoxsyp/KYbY1AuP0tYEwfs0sCph9p/UMXK/Hk=
github.com/oschwald/geoip2-golang v1.4.0/go.mod h1:8QwxJvRImBH+Zl6Aa6MaIcs5YdlZSTKtzmPGzQqi9ng= github.com/oschwald/maxminddb-golang v1.8.0/go.mod h1:RXZtst0N6+FY/3qCNmZMBApR19cdQj43/NM9VkrNAis=
github.com/oschwald/maxminddb-golang v1.6.0 h1:KAJSjdHQ8Kv45nFIbtoLGrGWqHFajOIm7skTyz/+Dls=
github.com/oschwald/maxminddb-golang v1.6.0/go.mod h1:DUJFucBg2cvqx42YmDa/+xHvb0elJtOm3o4aFQ/nb/w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= go.uber.org/atomic v1.8.0 h1:CUhrE4N1rqSE6FM9ecihEjRkLQu8cDfgDyoOs83mEY4=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM= go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
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/eapache/channels.v1 v1.1.0 h1:5bGAyKKvyCTWjSj7mhefG6Lc68VyN4MH1v8/7OoeeB4= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/eapache/channels.v1 v1.1.0/go.mod h1:BHIBujSvu9yMTrTYbTCjDD43gUhtmaOtTWDe7sTv1js= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
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=

View File

@ -6,17 +6,21 @@ import (
"os" "os"
"sync" "sync"
"github.com/Dreamacro/clash/adapters/provider" "github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/adapter/outboundgroup"
"github.com/Dreamacro/clash/adapter/provider"
"github.com/Dreamacro/clash/component/auth" "github.com/Dreamacro/clash/component/auth"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/profile"
"github.com/Dreamacro/clash/component/profile/cachefile"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
"github.com/Dreamacro/clash/component/trie" "github.com/Dreamacro/clash/component/trie"
"github.com/Dreamacro/clash/config" "github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/dns" "github.com/Dreamacro/clash/dns"
P "github.com/Dreamacro/clash/listener"
authStore "github.com/Dreamacro/clash/listener/auth"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
P "github.com/Dreamacro/clash/proxy"
authStore "github.com/Dreamacro/clash/proxy/auth"
"github.com/Dreamacro/clash/tunnel" "github.com/Dreamacro/clash/tunnel"
) )
@ -34,7 +38,7 @@ func readConfig(path string) ([]byte, error) {
} }
if len(data) == 0 { if len(data) == 0 {
return nil, fmt.Errorf("Configuration file %s is empty", path) return nil, fmt.Errorf("configuration file %s is empty", path)
} }
return data, err return data, err
@ -72,6 +76,7 @@ func ApplyConfig(cfg *config.Config, force bool) {
updateDNS(cfg.DNS) updateDNS(cfg.DNS)
updateHosts(cfg.Hosts) updateHosts(cfg.Hosts)
updateExperimental(cfg) updateExperimental(cfg)
updateProfile(cfg)
} }
func GetGeneral() *config.General { func GetGeneral() *config.General {
@ -86,6 +91,7 @@ func GetGeneral() *config.General {
Port: ports.Port, Port: ports.Port,
SocksPort: ports.SocksPort, SocksPort: ports.SocksPort,
RedirPort: ports.RedirPort, RedirPort: ports.RedirPort,
TProxyPort: ports.TProxyPort,
MixedPort: ports.MixedPort, MixedPort: ports.MixedPort,
Authentication: authenticator, Authentication: authenticator,
AllowLan: P.AllowLan(), AllowLan: P.AllowLan(),
@ -93,6 +99,7 @@ func GetGeneral() *config.General {
}, },
Mode: tunnel.Mode(), Mode: tunnel.Mode(),
LogLevel: log.Level(), LogLevel: log.Level(),
IPv6: !resolver.DisableIPv6,
} }
return general return general
@ -101,27 +108,41 @@ func GetGeneral() *config.General {
func updateExperimental(c *config.Config) {} func updateExperimental(c *config.Config) {}
func updateDNS(c *config.DNS) { func updateDNS(c *config.DNS) {
if c.Enable == false { if !c.Enable {
resolver.DefaultResolver = nil resolver.DefaultResolver = nil
tunnel.SetResolver(nil) resolver.DefaultHostMapper = nil
dns.ReCreateServer("", nil) dns.ReCreateServer("", nil, nil)
return return
} }
r := dns.New(dns.Config{
cfg := dns.Config{
Main: c.NameServer, Main: c.NameServer,
Fallback: c.Fallback, Fallback: c.Fallback,
IPv6: c.IPv6, IPv6: c.IPv6,
EnhancedMode: c.EnhancedMode, EnhancedMode: c.EnhancedMode,
Pool: c.FakeIPRange, Pool: c.FakeIPRange,
Hosts: c.Hosts,
FallbackFilter: dns.FallbackFilter{ FallbackFilter: dns.FallbackFilter{
GeoIP: c.FallbackFilter.GeoIP, GeoIP: c.FallbackFilter.GeoIP,
IPCIDR: c.FallbackFilter.IPCIDR, IPCIDR: c.FallbackFilter.IPCIDR,
Domain: c.FallbackFilter.Domain,
}, },
Default: c.DefaultNameserver, Default: c.DefaultNameserver,
}) Policy: c.NameServerPolicy,
}
r := dns.NewResolver(cfg)
m := dns.NewEnhancer(cfg)
// reuse cache of old host mapper
if old := resolver.DefaultHostMapper; old != nil {
m.PatchFrom(old.(*dns.ResolverEnhancer))
}
resolver.DefaultResolver = r resolver.DefaultResolver = r
tunnel.SetResolver(r) resolver.DefaultHostMapper = m
if err := dns.ReCreateServer(c.Listen, r); err != nil {
if err := dns.ReCreateServer(c.Listen, r, m); err != nil {
log.Errorln("Start DNS server error: %s", err.Error()) log.Errorln("Start DNS server error: %s", err.Error())
return return
} }
@ -166,19 +187,26 @@ func updateGeneral(general *config.General, force bool) {
bindAddress := general.BindAddress bindAddress := general.BindAddress
P.SetBindAddress(bindAddress) P.SetBindAddress(bindAddress)
if err := P.ReCreateHTTP(general.Port); err != nil { tcpIn := tunnel.TCPIn()
udpIn := tunnel.UDPIn()
if err := P.ReCreateHTTP(general.Port, tcpIn); err != nil {
log.Errorln("Start HTTP server error: %s", err.Error()) log.Errorln("Start HTTP server error: %s", err.Error())
} }
if err := P.ReCreateSocks(general.SocksPort); err != nil { if err := P.ReCreateSocks(general.SocksPort, tcpIn, udpIn); err != nil {
log.Errorln("Start SOCKS5 server error: %s", err.Error()) log.Errorln("Start SOCKS5 server error: %s", err.Error())
} }
if err := P.ReCreateRedir(general.RedirPort); err != nil { if err := P.ReCreateRedir(general.RedirPort, tcpIn, udpIn); err != nil {
log.Errorln("Start Redir server error: %s", err.Error()) log.Errorln("Start Redir server error: %s", err.Error())
} }
if err := P.ReCreateMixed(general.MixedPort); err != nil { if err := P.ReCreateTProxy(general.TProxyPort, tcpIn, udpIn); err != nil {
log.Errorln("Start TProxy server error: %s", err.Error())
}
if err := P.ReCreateMixed(general.MixedPort, tcpIn, udpIn); err != nil {
log.Errorln("Start Mixed(http and socks5) server error: %s", err.Error()) log.Errorln("Start Mixed(http and socks5) server error: %s", err.Error())
} }
} }
@ -190,3 +218,38 @@ func updateUsers(users []auth.AuthUser) {
log.Infoln("Authentication of local server updated") log.Infoln("Authentication of local server updated")
} }
} }
func updateProfile(cfg *config.Config) {
profileCfg := cfg.Profile
profile.StoreSelected.Store(profileCfg.StoreSelected)
if profileCfg.StoreSelected {
patchSelectGroup(cfg.Proxies)
}
}
func patchSelectGroup(proxies map[string]C.Proxy) {
mapping := cachefile.Cache().SelectedMap()
if mapping == nil {
return
}
for name, proxy := range proxies {
outbound, ok := proxy.(*adapter.Proxy)
if !ok {
continue
}
selector, ok := outbound.ProxyAdapter.(*outboundgroup.Selector)
if !ok {
continue
}
selected, exist := mapping[name]
if !exist {
continue
}
selector.Set(selected)
}
}

View File

@ -4,7 +4,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"github.com/go-chi/chi" "github.com/go-chi/chi/v5"
) )
// When name is composed of a partial escape string, Golang does not unescape it // When name is composed of a partial escape string, Golang does not unescape it

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