Compare commits

...

264 Commits

Author SHA1 Message Date
8c7c8f4374 Chore: update dependencies 2022-07-07 22:15:50 +08:00
65a8e8f59c Fix: process rule type (#2206) 2022-07-06 13:44:04 +08:00
5497adaba1 Fix: fakeip udp should not replace with another ip 2022-07-05 21:09:29 +08:00
aaf08dadff Change: remove AddrType on Metadata (#2199) 2022-07-05 20:26:43 +08:00
557297ac9a Chore: load balance hash need to have fallback strategy 2022-07-04 21:36:33 +08:00
77a1e3a653 Chore: cleanup bind mark code 2022-06-30 17:27:57 +08:00
27e1d6cdae Chore: cleanup code 2022-06-30 17:12:06 +08:00
91c22b16bf Fix: proxy provider filter validation (#2198) 2022-06-30 17:08:53 +08:00
fc5c9b931b Fix: try to unmap lAddr on tproxy udp listener 2022-06-29 23:36:45 +08:00
c231fd1466 Chore: update dependencies 2022-06-19 13:01:43 +08:00
fbb27b84d1 Chore: add redir-host deprecated warnning 2022-06-14 11:26:04 +08:00
e0c5a85314 Fix: missing import 2022-06-12 21:22:02 +08:00
2fa1a5c4b9 Chore: update tproxy udp packet read logic 2022-06-12 19:37:51 +08:00
06d75da257 Chore: adjust Relay copy memory alloc logic 2022-06-11 20:38:16 +08:00
09d49bac95 Chore: embed shadowsocks2 2022-06-01 21:43:20 +08:00
3360839fe3 Chore: make CodeQL happy 2022-06-01 21:38:05 +08:00
c1285adbf8 Feature: can set custom interface for dns nameserver (#2126) 2022-06-01 10:50:54 +08:00
9d2fc976e2 Chore: upgrade to yaml v3 2022-05-26 17:47:05 +08:00
7f41f94fff Fix: benchmark read bytes 2022-05-23 12:58:18 +08:00
d1f0dac302 Fix: test broken on opensource repo 2022-05-23 12:30:54 +08:00
afb3e00067 Chore: add benchmark r/w 2022-05-23 12:27:52 +08:00
9a31ad6151 Chore: cleanup test go.mod 2022-05-21 17:46:34 +08:00
09cc6b69e3 Chore: cleanup test code 2022-05-21 17:38:17 +08:00
8603ac40a1 Chore: make linter happy 2022-05-17 19:58:33 +08:00
b384449717 Fix: fix upgrade header detect (#2134) 2022-05-15 09:12:53 +08:00
da7ffc0da9 Fix: add length check for ssr auth_aes128_sha1 (#2129) 2022-05-13 11:21:39 +08:00
5dd94c8298 Chore: update dependencies 2022-05-07 21:08:15 +08:00
412b44a981 Fix: decode nil value in slice decoder (#2102) 2022-05-07 11:00:58 +08:00
aef4dd3fe7 Fix: make log api unblocked 2022-04-26 22:36:10 +08:00
6a92c6af4e Fix: http proxy Upgrade behavior (#2097) 2022-04-25 19:50:20 +08:00
e010940b61 Improve: replace bootstrap dns (#2080) 2022-04-16 15:31:26 +08:00
2c9a4d276a Chore: add more github action cache 2022-04-14 23:37:41 +08:00
4dfba73e5c Fix: SyscallN should not use nargs 2022-04-14 23:37:19 +08:00
c282d662ca Fix: make golangci lint support multi GOOS 2022-04-13 17:51:21 +08:00
b3d7594813 Chore: add none alias to dummy on ShadowsocksR (#2056) 2022-04-13 10:06:06 +08:00
dd9bdf4e2f Fix: convert size to unit32 in getoridst to solve some mips64 devices cannot get redirect origin dst (#2041)
Change-Id: I40aa73dcea692132e38db980320a8a07ed427fe6

Co-authored-by: Zhao Guowei <zhaoguowei@bytedance.com>
2022-03-28 14:48:51 +08:00
275cc7edf3 Chore: structure support weakly type from float to int (#2042) 2022-03-25 15:22:31 +08:00
8c9e0b3884 Chore: use GOAMD64 v1 on build docker image 2022-03-20 11:32:18 +08:00
30d4668008 Chore: fix typo (#2033) 2022-03-19 13:58:51 +08:00
02333a859a Chore: split amd64 v3 to special release 2022-03-19 13:42:06 +08:00
f9cc1cc363 Fix: routing-mark option doesn't work on proxies (#2028) 2022-03-19 13:29:30 +08:00
fb7d340233 Fix: docker build makefile 2022-03-16 12:13:59 +08:00
6a661bff0c Migration: go 1.18 2022-03-16 12:10:13 +08:00
d1dd21417b Feature: add tzdata to Dockerfile (#2027)
Co-authored-by: suyaqi <suyaqi@wy.net>
2022-03-15 11:30:52 +08:00
b866f06414 Chore: move find connection process to tunnel (#2016) 2022-03-12 19:07:53 +08:00
9683c297a7 Chore: add more details to process resolving (#2017) 2022-03-09 13:41:50 +08:00
f6c7281bb7 Chore: update github action workflow 2022-03-06 21:48:37 +08:00
83bfe521b1 Fix: should split linux process name with space (#2008) 2022-03-05 18:25:16 +08:00
b52d0c16e9 Chore: vmess test remove all alterid 2022-02-27 18:00:04 +08:00
132a6a6a2f Fix: listener tcp keepalive & reuse net.BufferedConn (#1987) 2022-02-23 11:22:46 +08:00
03e4b5d525 Chore: use golangci-lint config file 2022-02-19 00:08:51 +08:00
a0221bf897 Fix: routing-mark should effect on root 2022-02-17 14:23:47 +08:00
b1a639feae Fix: domain trie search 2022-01-26 22:28:13 +08:00
cfe7354c07 Improve: change provider file modify time when updated (#1918) 2022-01-18 13:32:47 +08:00
9732efe938 Fix: tls handshake requires a timeout (#1893) 2022-01-15 19:33:21 +08:00
8f3385bbb6 Feature: support snell v3 (#1884) 2022-01-10 20:24:20 +08:00
d237b041b3 Fix: ignore empty dns server error 2022-01-05 11:41:31 +08:00
3cb87e083c Fix: duplicate provider err typo 2022-01-03 17:21:27 +08:00
8c6d0c6757 Chore: fix docker dependencies security warning 2022-01-02 11:15:40 +08:00
cb95326aca Chore: update dependencies 2022-01-02 01:15:49 +08:00
8679968ab0 Fix: multiple port string parsing overflow (#1868)
Ports in TCP and UDP should be parsed as an unsigned integer,
otherwise ports > 32767 get truncated to 32767. As this is
the case with Metadata.UDPAddr(), this fundamentally breaks
UDP connections where demand for high port numbers is high.

This commit fixes all known cases where ParseInt is used for ports,
and has been verified to fix Discord voice connections on port
50001~50004.

Fixes: d40e5e4fe6

Co-authored-by: Hamster Tian <haotia@gmail.com>
2022-01-02 01:09:29 +08:00
204a72bbd3 Chore: remove forward compatible code 2022-01-02 00:48:57 +08:00
7267c58913 Chore: ReCreate* do side effect job (#1849) 2021-12-26 22:08:53 +08:00
14ae87fcd0 Chore: remove reduce regex compile (#1855) 2021-12-26 20:47:12 +08:00
Fan
ee6fc12709 Fix: when both providers and proxies are present, use the health check configuration for proxies (#1821)
Co-authored-by: Ho <ho@fluidex.com>
2021-12-12 20:37:30 +08:00
78e105f3b2 Chore: builtin right mime of .js (#1808) 2021-12-08 13:38:25 +08:00
08607fb6b4 Feature: add linux/arm/v6 for the container image (#1771) 2021-12-02 21:12:45 +08:00
075d8ed094 Fix: fakeip pool cycle used 2021-11-23 22:01:49 +08:00
b1bed7623d Fix: provider filter potential panic 2021-11-21 17:44:03 +08:00
1401a82bb0 Feature: add filter on proxy provider (#1511) 2021-11-20 23:38:49 +08:00
4524cf4418 Fix: should return io.EOF immediately 2021-11-20 12:44:31 +08:00
0db15d46c3 Change: use nop packet conn for reject 2021-11-20 12:34:14 +08:00
08c43b8876 Fix: revert ssr udp fix 2021-11-14 14:48:00 +08:00
499beb7344 Fix: bind iface should throw control error 2021-11-10 22:19:11 +08:00
c9be614821 Fix: windows arm7 build 2021-11-08 21:24:39 +08:00
b56d35040d Chore: update dependencies and rename profile props 2021-11-08 20:48:29 +08:00
bd2ea2b917 Feature: mark on socket (#1705) 2021-11-08 16:59:48 +08:00
e622d8dd38 Fix: parse dial interface option 2021-11-08 13:31:08 +08:00
d40e5e4fe6 Fix: codeql alerts 2021-11-08 00:32:21 +08:00
1a7830f18e Feature: dial different NIC for all proxies (#1714) 2021-11-07 16:48:51 +08:00
bcb301b730 Chore: adjust all udp alloc size 2021-11-03 22:29:24 +08:00
ebbc9604ce Chore: use uber max procs 2021-10-27 21:27:19 +08:00
a7aea12aa6 Fix: remove ResponseHeaderTimeout limitation (#1690) 2021-10-20 13:44:05 +08:00
c6cceeb0c5 Chore: use alpn http 1.1 only on trojan websocket by default 2021-10-19 22:34:18 +08:00
967932d02c Fix: set dnsmode behavior 2021-10-18 23:03:25 +08:00
81d5da51a3 Fix: unexpected proxy dial behavior on mapping mode 2021-10-18 21:08:27 +08:00
fea9d1c5e2 Fix: replace vmess grpc test image 2021-10-16 20:35:06 +08:00
df3a491d40 Feature: support trojan websocket 2021-10-16 20:19:59 +08:00
68753b4ae1 Chore: contexify ProxyAdapter ListenPacket 2021-10-15 21:44:53 +08:00
583b2a5ace Change: use interface HardwareAddr for dhcp discovery 2021-10-14 22:54:43 +08:00
13bd601cac Fix: #1660 panic 2021-10-11 21:05:38 +08:00
3d5681cffd Feature: persistence fakeip (#1662) 2021-10-11 20:48:58 +08:00
a1c2478e74 Chore: actions split lint and release 2021-10-11 20:08:18 +08:00
f1cf7e9269 Style: use gofumpt for fmt 2021-10-10 23:44:09 +08:00
4ce35870fe Chore: remove deprecated ioutil 2021-10-09 20:35:06 +08:00
1996bef9e6 Chore: doh request should with id 0 (#1660) 2021-10-07 22:57:55 +08:00
66cb0b1218 Fix: cache kv db should not block on init 2021-10-05 22:47:26 +08:00
b9d470cf79 Fix: dhcp client should request special interface 2021-10-05 13:31:19 +08:00
4f1fac02ab Chore: add remove TODO 2021-10-05 12:42:21 +08:00
537b672fcf Change: use bbolt as cache db 2021-10-04 19:20:11 +08:00
ced9749104 Fix: http proxy should response correct http version (#1651) 2021-09-30 16:30:07 +08:00
9aeb4c8cfe Improve: avoid bufconn twice (#1650) 2021-09-28 23:15:53 +08:00
70c8605cca Improve: use one bytes.Buffer pool 2021-09-20 21:02:18 +08:00
5b1a0a523f Chore: update README.md 2021-09-20 17:22:40 +08:00
b398f1e6f3 Chore: force set latest go version to action 2021-09-18 00:18:47 +08:00
b3cd4ebbd3 Fix: use 1.17.x on github actions 2021-09-15 20:21:30 +08:00
b0f83e401f Fix: socks4 request continues after authentication failed (#1624) 2021-09-15 16:45:57 +08:00
f5806d9263 Fix: http/https proxy authentication (#1613) 2021-09-14 00:08:23 +08:00
55600c49c9 Fix: potential pitfalls 2021-09-13 23:58:48 +08:00
beb88cc46f Fix: should not trust address of http.Client (#1616) 2021-09-13 23:46:39 +08:00
d49b38b00f Fix: should not unmarshal to pointer (#1615) 2021-09-13 23:43:28 +08:00
0c79d1207e Fix: potential overflow in ssr (#1600) 2021-09-09 20:30:34 +08:00
400dc923e0 Fix: vmess ws headers not set properly (#1595) 2021-09-08 14:44:24 +08:00
5b7f0de48b Chore: update dependencies 2021-09-07 20:16:07 +08:00
a5b950a779 Feature: add dhcp type dns client (#1509) 2021-09-06 23:07:34 +08:00
a2d59d6ef5 Feature: skip DIRECT proxies in relay (#1583) 2021-09-06 21:39:28 +08:00
8ef5cdb8be Test: use local clash pkg 2021-09-05 14:38:07 +08:00
c7b718f651 Test: release resources correctly 2021-09-05 14:25:55 +08:00
ff56e5c5de Test: fix direct listen fail 2021-09-04 22:44:18 +08:00
661c417fce Test: use shadowsocks-rust for ss benchmark 2021-09-04 22:31:08 +08:00
7d20097465 Fix: ssr auth aes128 udp hmac verify 2021-08-30 00:15:57 +08:00
a20b9a3960 Chore: make geoip match case-insensitive (#1574) 2021-08-29 22:19:22 +08:00
e0d3f926b7 Feature: add geoip-code option 2021-08-25 15:15:13 +08:00
121bc910f6 Chore: adjust vmess 0rtt code and split xray test 2021-08-22 16:16:45 +08:00
4522cdc551 Feature: support xray's ws-0rtt path (#1558) 2021-08-22 16:03:46 +08:00
410772e81c Test: add vmess ws 0-rtt test 2021-08-22 01:17:29 +08:00
0267b2efad Feature: add vmess WebSocket early data (#1505)
Co-authored-by: ShinyGwyn <79344143+ShinyGwyn@users.noreply.github.com>
2021-08-22 00:25:29 +08:00
c6d375eda2 Fix: HTTP proxy internal linkage signature (#1555) 2021-08-20 23:38:47 +08:00
847f41952e Fix: grpc transport path should not escape 2021-08-19 22:11:56 +08:00
47044ec0d8 Fix: dependabot alerts 2021-08-18 20:20:00 +08:00
426ca36118 Chore: upgrade test package 2021-08-18 13:31:34 +08:00
571d2a0075 Migration: go 1.17 2021-08-18 13:26:23 +08:00
1be09f5751 Chore: fix issue template render type 2021-08-13 22:44:22 +08:00
2663cb2e6e Chore: update github issue template 2021-08-13 22:35:48 +08:00
9b0bbb90ff Chore: upgrade github actions 2021-08-07 22:27:23 +08:00
588645a2c3 Fix: interface nil check panic from previous commit 2021-08-04 23:52:50 +08:00
1bfebd0d03 Fix: listener patch diff 2021-08-01 00:35:37 +08:00
3705996974 Chore: split SOCKS version inbound metadata type (#1513) 2021-07-27 13:58:29 +08:00
09697b7679 Chore: adjust batch 2021-07-23 00:30:23 +08:00
4578b2c826 Fix: remove Content-Length from CONNECT response (#1502) 2021-07-22 18:06:03 +08:00
b3a293ab07 Chore: benchmark explanation 2021-07-22 00:01:11 +08:00
507ba16065 Fix: incorrect use batch 2021-07-21 23:53:31 +08:00
aa9f8a39a3 Fix: socks inbound packet typo 2021-07-21 23:08:52 +08:00
8d37220566 Fix: limit concurrency number of provider health check 2021-07-21 17:01:15 +08:00
53e17a916b Chore: logging remote port on request (#1494) 2021-07-19 15:31:38 +08:00
247dd84970 Chore: logging real listen port (#1492) 2021-07-19 14:07:51 +08:00
c2f3111922 Test: add direct benchmark 2021-07-18 19:26:51 +08:00
44872300e9 Test: ss use aes-256-gcm and vmess-aead for benchmark 2021-07-18 17:37:23 +08:00
91ed0118f6 Test: add basic protocol benchmark 2021-07-18 17:23:30 +08:00
a461c2306a Feature: SOCKS4/SOCKS4A Inbound Compatible Support (#1491) 2021-07-18 16:09:09 +08:00
46f4f84442 Chore: use iife replace init in some cases 2021-07-11 19:43:25 +08:00
250a9f4f84 Fix: reorder apply config to ensure update proxies and rules 2021-07-10 17:01:40 +08:00
b4292d0972 Fix: staticcheck error 2021-07-06 00:33:13 +08:00
d755383e39 Chore: move provider interface to constant 2021-07-06 00:31:13 +08:00
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
310 changed files with 14634 additions and 7315 deletions

View File

@ -1,100 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: "[Bug]"
labels: ''
assignees: ''
---
<!--
感谢你向 Clash Core 提交 issue
在提交之前,请确认:
- [ ] 如果你可以自己 debug 并解决的话,提交 PR 吧!
- [ ] 我已经在 [Issue Tracker](……/) 中找过我要提出的问题
- [ ] 我已经使用 dev 分支版本测试过,问题依旧存在
- [ ] 我已经仔细看过 [Documentation](https://github.com/Dreamacro/clash/wiki/) 并无法自行解决问题
- [ ] 这是 Clash 核心的问题,并非我所使用的 Clash 衍生版本(如 OpenClash、KoolClash 等)的特定问题
请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。
Thanks for opening an issue towards the Clash core!
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.
- [ ] 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 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 this issue template.
-->
------------------------------------------------------------------
<!--
请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 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.
-->
### Clash config
<!--
在下方附上 Clash core 脱敏后配置文件的内容
Paste the Clash core configuration below.
-->
<details>
<summary>config.yaml</summary>
```yaml
……
```
</details>
### Clash log
<!--
在下方附上 Clash Core 的日志log level 使用 DEBUG
Paste the Clash core log below with the log level set to `DEBUG`.
-->
```
……
```
### 环境 Environment
* 操作系统 (the OS that the Clash core is running on)
……
* 网路环境或拓扑 (network conditions/topology)
……
* iptables如果适用 (if applicable)
……
* ISP 有没有进行 DNS 污染 (is your ISP performing DNS pollution?)
……
* 其他 (any other information that would be useful)
……
### 说明 Description
<!--
请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?
-->
### 重现问题的具体布骤 Steps to Reproduce
1. [First Step]
2. [Second Step]
3. ……
**我预期会发生……?**
<!-- **Expected behavior:** [What you expected to happen] -->
**实际上发生了什么?**
<!-- **Actual behavior:** [What actually happened] -->
### 可能的解决方案 Possible Solution
<!-- 此项非必须,但是如果你有想法的话欢迎提出。 -->
<!-- Not obligatory, but suggest a fix/reason for the bug, -->
<!-- or ideas how to implement the addition or change -->
### 更多信息 More Information

76
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,76 @@
name: Bug report
description: Create a report to help us improve
title: "[Bug] "
body:
- type: checkboxes
id: ensure
attributes:
label: Verify steps
description: "
在提交之前,请确认
Please verify that you've followed these steps
"
options:
- label: "
如果你可以自己 debug 并解决的话,提交 PR 吧
Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome.
"
required: true
- label: "
我已经在 [Issue Tracker](……/) 中找过我要提出的问题
I have searched on the [issue tracker](……/) for a related issue.
"
required: true
- label: "
我已经使用 dev 分支版本测试过,问题依旧存在
I have tested using the dev branch, and the issue still exists.
"
required: true
- label: "
我已经仔细看过 [Documentation](https://github.com/Dreamacro/clash/wiki/) 并无法自行解决问题
I have read the [documentation](https://github.com/Dreamacro/clash/wiki/) and was unable to solve the issue.
"
required: true
- label: "
这是 Clash 核心的问题,并非我所使用的 Clash 衍生版本(如 OpenClash、KoolClash 等)的特定问题
This is an issue of the Clash core *per se*, not to the derivatives of Clash, like OpenClash or KoolClash.
"
required: true
- type: input
attributes:
label: Clash version
validations:
required: true
- type: dropdown
id: os
attributes:
label: What OS are you seeing the problem on?
multiple: true
options:
- macOS
- Windows
- Linux
- OpenBSD/FreeBSD
- type: textarea
attributes:
render: yaml
label: "Clash config"
description: "
在下方附上 Clash core 脱敏后配置文件的内容
Paste the Clash core configuration below.
"
validations:
required: true
- type: textarea
attributes:
render: shell
label: Clash log
description: "
在下方附上 Clash Core 的日志log level 使用 DEBUG
Paste the Clash core log below with the log level set to `DEBUG`.
"
- type: textarea
attributes:
label: Description
validations:
required: true

6
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,6 @@
blank_issues_enabled: false
contact_links:
- name: Get help in GitHub Discussions
url: https://github.com/Dreamacro/clash/discussions
about: Have a question? Not sure if your issue affects everyone reproducibly? The quickest way to get help is on Clash's GitHub Discussions!

View File

@ -1,78 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature]"
labels: ''
assignees: ''
---
<!-- The English version is available. -->
感谢你向 Clash Core 提交 Feature Request
在提交之前,请确认:
- [ ] 我已经在 [Issue Tracker](……/) 中找过我要提出的请求
请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。
<!--
Thanks for submitting a feature request towards the Clash core!
But before so, please do the following checklist:
- [ ] I have searched on the [issue tracker](……/) before creating the issue.
Please understand that we close issues that fail to follow the issue template.
-->
我都确认过了,我要继续提交。
<!-- None of the above, create a feature request -->
------------------------------------------------------------------
请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 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. -->
### Clash core config
<!--
在下方附上 Clash Core 脱敏后的配置内容
Paste the Clash core configuration below.
-->
```
……
```
### Clash log
<!--
在下方附上 Clash Core 的日志log level 请使用 DEBUG
Paste the Clash core log below with the log level set to `DEBUG`.
-->
```
……
```
### 环境 Environment
* Clash Core 的操作系统 (the OS that the Clash core is running on)
……
* 使用者的操作系统 (the OS running on the client)
……
* 网路环境或拓扑 (network conditions/topology)
……
* iptables如果适用 (if applicable)
……
* ISP 有没有进行 DNS 污染 (is your ISP performing DNS pollution?)
……
* 其他
……
### 说明 Description
<!--
请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?目前 Clash Core 的行为是什麽?
-->
### 可能的解决方案 Possible Solution
<!-- 此项非必须,但是如果你有想法的话欢迎提出。 -->
<!-- Not obligatory, but suggest a fix/reason for the bug, -->
<!-- or ideas how to implement the addition or change -->
### 更多信息 More Information

View File

@ -0,0 +1,36 @@
name: Feature request
description: Suggest an idea for this project
title: "[Feature] "
body:
- type: checkboxes
id: ensure
attributes:
label: Verify steps
description: "
在提交之前,请确认
Please verify that you've followed these steps
"
options:
- label: "
我已经在 [Issue Tracker](……/) 中找过我要提出的请求
I have searched on the [issue tracker](……/) for a related feature request.
"
required: true
- label: "
我已经仔细看过 [Documentation](https://github.com/Dreamacro/clash/wiki/) 并无法自行解决问题
I have read the [documentation](https://github.com/Dreamacro/clash/wiki/) and was unable to solve the issue.
"
required: true
- type: textarea
attributes:
label: Description
description: 请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?目前 Clash Core 的行为是什麽?
validations:
required: true
- type: textarea
attributes:
label: Possible Solution
description: "
此项非必须,但是如果你有想法的话欢迎提出。
Not obligatory, but suggest a fix/reason for the bug, or ideas how to implement the addition or change
"

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

@ -13,40 +13,68 @@ jobs:
steps: steps:
- 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@v3
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@v3 uses: docker/setup-buildx-action@v1
with: with:
buildx-version: latest version: latest
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/v6,linux/arm/v7,linux/arm64
push: true
tags: 'dreamacro/clash:dev,ghcr.io/dreamacro/clash:dev'
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Get all docker tags
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
uses: actions/github-script@v3 uses: actions/github-script@v6
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/v6,linux/arm/v7,linux/arm64
push: true
tags: ${{steps.tags.outputs.result}}
cache-from: type=gha
cache-to: type=gha,mode=max

22
.github/workflows/linter.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: Linter
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Get latest go version
id: version
run: |
echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g')
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: ${{ steps.version.outputs.go_version }}
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest

View File

@ -1,33 +1,35 @@
name: Go name: Release
on: [push, pull_request] on: [push]
jobs: jobs:
build: build:
name: Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Get latest go version
id: version
run: |
echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g')
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.15.x go-version: ${{ steps.version.outputs.go_version }}
- 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@v3
- name: Cache go module - name: Cache go module
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: ~/go/pkg/mod path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: | restore-keys: |
${{ runner.os }}-go- ${{ runner.os }}-go-
- name: Get dependencies, run test and static check - name: Get dependencies, run test
run: | run: |
go test ./... go test ./...
go vet ./...
go get -u honnef.co/go/tools/cmd/staticcheck
staticcheck -- $(go list ./...)
- name: Build - name: Build
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
@ -39,9 +41,6 @@ jobs:
- name: Upload Release - name: Upload Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
files: bin/* files: bin/*
draft: true draft: true
prerelease: true

View File

@ -11,9 +11,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v3 - uses: actions/stale@v5
with: 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'
stale-issue-message: 'This issue is stale because it has been open 120 days with no activity. Remove stale label or comment or this will be closed in 5 days' days-before-stale: 60
days-before-stale: 120
days-before-close: 5 days-before-close: 5

5
.gitignore vendored
View File

@ -12,7 +12,7 @@ bin/*
# Output of the go coverage tool, specifically when used with LiteIDE # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
# dep # go mod vendor
vendor vendor
# GoLand # GoLand
@ -20,3 +20,6 @@ vendor
# macOS file # macOS file
.DS_Store .DS_Store
# test suite
test/config/cache*

16
.golangci.yaml Normal file
View File

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

View File

@ -10,8 +10,9 @@ 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 tzdata
COPY --from=builder /Country.mmdb /root/.config/clash/ COPY --from=builder /Country.mmdb /root/.config/clash/
COPY --from=builder /clash / COPY --from=builder /clash /
ENTRYPOINT ["/clash"] ENTRYPOINT ["/clash"]

View File

@ -8,8 +8,11 @@ GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clas
PLATFORM_LIST = \ PLATFORM_LIST = \
darwin-amd64 \ darwin-amd64 \
darwin-amd64-v3 \
darwin-arm64 \
linux-386 \ linux-386 \
linux-amd64 \ linux-amd64 \
linux-amd64-v3 \
linux-armv5 \ linux-armv5 \
linux-armv6 \ linux-armv6 \
linux-armv7 \ linux-armv7 \
@ -21,11 +24,15 @@ PLATFORM_LIST = \
linux-mips64 \ linux-mips64 \
linux-mips64le \ linux-mips64le \
freebsd-386 \ freebsd-386 \
freebsd-amd64 freebsd-amd64 \
freebsd-amd64-v3 \
freebsd-arm64
WINDOWS_ARCH_LIST = \ WINDOWS_ARCH_LIST = \
windows-386 \ windows-386 \
windows-amd64 \ windows-amd64 \
windows-amd64-v3 \
windows-arm64 \
windows-arm32v7 windows-arm32v7
all: linux-amd64 darwin-amd64 windows-amd64 # Most used all: linux-amd64 darwin-amd64 windows-amd64 # Most used
@ -36,12 +43,21 @@ docker:
darwin-amd64: darwin-amd64:
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64-v3:
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(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)-$@
linux-amd64: linux-amd64:
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64-v3:
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv5: linux-armv5:
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@ -78,12 +94,24 @@ freebsd-386:
freebsd-amd64: freebsd-amd64:
GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-amd64-v3:
GOARCH=amd64 GOOS=freebsd GOAMD64=v3 $(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-amd64-v3:
GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-arm64:
GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-arm32v7: windows-arm32v7:
GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
@ -100,5 +128,13 @@ $(zip_releases): %.zip : %
all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST) all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST)
releases: $(gz_releases) $(zip_releases) releases: $(gz_releases) $(zip_releases)
lint:
GOOS=darwin golangci-lint run ./...
GOOS=windows golangci-lint run ./...
GOOS=linux golangci-lint run ./...
GOOS=freebsd golangci-lint run ./...
GOOS=openbsd golangci-lint run ./...
clean: clean:
rm $(BINDIR)/* rm $(BINDIR)/*

View File

@ -12,9 +12,13 @@
<a href="https://goreportcard.com/report/github.com/Dreamacro/clash"> <a href="https://goreportcard.com/report/github.com/Dreamacro/clash">
<img src="https://goreportcard.com/badge/github.com/Dreamacro/clash?style=flat-square"> <img src="https://goreportcard.com/badge/github.com/Dreamacro/clash?style=flat-square">
</a> </a>
<img src="https://img.shields.io/github/go-mod/go-version/Dreamacro/clash?style=flat-square">
<a href="https://github.com/Dreamacro/clash/releases"> <a href="https://github.com/Dreamacro/clash/releases">
<img src="https://img.shields.io/github/release/Dreamacro/clash/all.svg?style=flat-square"> <img src="https://img.shields.io/github/release/Dreamacro/clash/all.svg?style=flat-square">
</a> </a>
<a href="https://github.com/Dreamacro/clash/releases/tag/premium">
<img src="https://img.shields.io/badge/release-Premium-00b4f0?style=flat-square">
</a>
</p> </p>
## Features ## Features
@ -22,30 +26,35 @@
- Local HTTP/HTTPS/SOCKS server with authentication support - Local HTTP/HTTPS/SOCKS server with authentication support
- VMess, Shadowsocks, Trojan, Snell protocol support for remote connections - VMess, Shadowsocks, Trojan, Snell protocol support for remote connections
- Built-in DNS server that aims to minimize DNS pollution attack impact, supports DoH/DoT upstream and fake IP. - Built-in DNS server that aims to minimize DNS pollution attack impact, supports DoH/DoT upstream and fake IP.
- Rules based off domains, GEOIP, IP CIDR or ports to forward packets to different nodes - Rules based off domains, GEOIP, IPCIDR or Process to forward packets to different nodes
- Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node 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. Deploy Clash on your Internet gateway with `iptables`. - Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`.
- Comprehensive HTTP RESTful API controller - Comprehensive HTTP RESTful API controller
## Premium Features
- 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)
## Getting Started ## Getting Started
Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki). Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
## Premium Release
[Release](https://github.com/Dreamacro/clash/releases/tag/premium)
## 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)
## 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)
* [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go)
## License ## License
This software is released under the GPL-3.0 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
- [x] Complementing the necessary rule operators
- [x] Redir proxy
- [x] UDP support
- [x] Connection manager
- [ ] ~~Event API~~

View File

@ -1,121 +1,61 @@
package outbound package adapter
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "fmt"
"net" "net"
"net/http" "net/http"
"sync/atomic" "net/url"
"time" "time"
"github.com/Dreamacro/clash/common/queue" "github.com/Dreamacro/clash/common/queue"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"go.uber.org/atomic"
) )
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 uint32 alive *atomic.Bool
} }
// Alive implements C.Proxy
func (p *Proxy) Alive() bool { func (p *Proxy) Alive() bool {
return atomic.LoadUint32(&p.alive) > 0 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)
} }
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { // DialContext implements C.ProxyAdapter
conn, err := p.ProxyAdapter.DialContext(ctx, metadata) func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
if err != nil { conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
atomic.StoreUint32(&p.alive, 0) p.alive.Store(err == nil)
}
return conn, err return conn, err
} }
// DialUDP implements C.ProxyAdapter
func (p *Proxy) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultUDPTimeout)
defer cancel()
return p.ListenPacketContext(ctx, metadata)
}
// ListenPacketContext implements C.ProxyAdapter
func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
p.alive.Store(err == nil)
return pc, 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{}
@ -126,9 +66,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 atomic.LoadUint32(&p.alive) == 0 { if !p.alive.Load() {
return max return max
} }
@ -143,27 +84,26 @@ 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 {
return inner, err return inner, err
} }
mapping := map[string]interface{}{} mapping := map[string]any{}
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() {
if err == nil { p.alive.Store(err == nil)
atomic.StoreUint32(&p.alive, 1)
} else {
atomic.StoreUint32(&p.alive, 0)
}
record := C.DelayHistory{Time: time.Now()} record := C.DelayHistory{Time: time.Now()}
if err == nil { if err == nil {
record.Delay = t record.Delay = t
@ -209,6 +149,8 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
return http.ErrUseLastResponse return http.ErrUseLastResponse
}, },
} }
defer client.CloseIdleConnections()
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return return
@ -219,5 +161,32 @@ 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), 1} 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{
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 socks5.Addr, source net.Addr, conn net.Conn) *context.ConnContext {
metadata := parseSocksAddr(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,14 +6,12 @@ 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 {
metadata := &C.Metadata{ metadata := &C.Metadata{}
AddrType: int(target[0]),
}
switch target[0] { switch target[0] {
case socks5.AtypDomainName: case socks5.AtypDomainName:
@ -44,21 +42,13 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
host = strings.TrimRight(host, ".") host = strings.TrimRight(host, ".")
metadata := &C.Metadata{ metadata := &C.Metadata{
NetWork: C.TCP, NetWork: C.TCP,
AddrType: C.AtypDomainName, Host: host,
Host: host, DstIP: nil,
DstIP: nil, DstPort: port,
DstPort: port,
} }
ip := net.ParseIP(host) if ip := net.ParseIP(host); ip != nil {
if ip != nil {
switch {
case ip.To4() == nil:
metadata.AddrType = C.AtypIPv6
default:
metadata.AddrType = C.AtypIPv4
}
metadata.DstIP = ip metadata.DstIP = ip
} }

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

@ -0,0 +1,138 @@
package outbound
import (
"context"
"encoding/json"
"errors"
"net"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
)
type Base struct {
name string
addr string
iface string
tp C.AdapterType
udp bool
rmark int
}
// 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")
}
// ListenPacketContext implements C.ProxyAdapter
func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (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
}
// DialOptions return []dialer.Option from struct
func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option {
if b.iface != "" {
opts = append(opts, dialer.WithInterface(b.iface))
}
if b.rmark != 0 {
opts = append(opts, dialer.WithRoutingMark(b.rmark))
}
return opts
}
type BasicOption struct {
Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"`
RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"`
}
type BaseOption struct {
Name string
Addr string
Type C.AdapterType
UDP bool
Interface string
RoutingMark int
}
func NewBase(opt BaseOption) *Base {
return &Base{
name: opt.Name,
addr: opt.Addr,
tp: opt.Type,
udp: opt.UDP,
iface: opt.Interface,
rmark: opt.RoutingMark,
}
}
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,10 +12,9 @@ type Direct struct {
*Base *Base
} }
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { // DialContext implements C.ProxyAdapter
address := net.JoinHostPort(metadata.String(), metadata.DstPort) func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...)
c, err := dialer.DialContext(ctx, "tcp", address)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -23,8 +22,9 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
return NewConn(c, d), nil return NewConn(c, d), nil
} }
func (d *Direct) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { // ListenPacketContext implements C.ProxyAdapter
pc, err := dialer.ListenPacket("udp", "") func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -25,6 +25,7 @@ type Http struct {
} }
type HttpOption struct { type HttpOption struct {
BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
@ -35,6 +36,7 @@ type HttpOption struct {
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)
@ -51,13 +53,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
c, err := dialer.DialContext(ctx, "tcp", h.addr) func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", h.addr, h.Base.DialOptions(opts...)...)
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
@ -121,16 +126,17 @@ func NewHttp(option HttpOption) *Http {
} }
tlsConfig = &tls.Config{ tlsConfig = &tls.Config{
InsecureSkipVerify: option.SkipCertVerify, InsecureSkipVerify: option.SkipCertVerify,
ClientSessionCache: getClientSessionCache(),
ServerName: sni, ServerName: sni,
} }
} }
return &Http{ return &Http{
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.Http, tp: C.Http,
iface: option.Interface,
rmark: option.RoutingMark,
}, },
user: option.UserName, user: option.UserName,
pass: option.Password, pass: option.Password,

View File

@ -0,0 +1,62 @@
package outbound
import (
"context"
"io"
"net"
"time"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
)
type Reject struct {
*Base
}
// DialContext implements C.ProxyAdapter
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
return NewConn(&nopConn{}, r), nil
}
// ListenPacketContext implements C.ProxyAdapter
func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return newPacketConn(&nopPacketConn{}, r), nil
}
func NewReject() *Reject {
return &Reject{
Base: &Base{
name: "REJECT",
tp: C.Reject,
udp: true,
},
}
}
type nopConn struct{}
func (rw *nopConn) Read(b []byte) (int, error) {
return 0, io.EOF
}
func (rw *nopConn) Write(b []byte) (int, error) {
return 0, io.EOF
}
func (rw *nopConn) Close() error { return nil }
func (rw *nopConn) LocalAddr() net.Addr { return nil }
func (rw *nopConn) RemoteAddr() net.Addr { return nil }
func (rw *nopConn) SetDeadline(time.Time) error { return nil }
func (rw *nopConn) SetReadDeadline(time.Time) error { return nil }
func (rw *nopConn) SetWriteDeadline(time.Time) error { return nil }
type nopPacketConn struct{}
func (npc *nopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { return len(b), nil }
func (npc *nopPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { return 0, nil, io.EOF }
func (npc *nopPacketConn) Close() error { return nil }
func (npc *nopPacketConn) LocalAddr() net.Addr { return &net.UDPAddr{IP: net.IPv4zero, Port: 0} }
func (npc *nopPacketConn) SetDeadline(time.Time) error { return nil }
func (npc *nopPacketConn) SetReadDeadline(time.Time) error { return nil }
func (npc *nopPacketConn) SetWriteDeadline(time.Time) error { return nil }

View File

@ -2,7 +2,6 @@ package outbound
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net" "net"
@ -10,12 +9,11 @@ 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"
"github.com/Dreamacro/clash/transport/shadowsocks/core"
"github.com/Dreamacro/go-shadowsocks2/core" obfs "github.com/Dreamacro/clash/transport/simple-obfs"
"github.com/Dreamacro/clash/transport/socks5"
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
) )
type ShadowSocks struct { type ShadowSocks struct {
@ -29,18 +27,19 @@ type ShadowSocks struct {
} }
type ShadowSocksOption struct { type ShadowSocksOption struct {
Name string `proxy:"name"` BasicOption
Server string `proxy:"server"` Name string `proxy:"name"`
Port int `proxy:"port"` Server string `proxy:"server"`
Password string `proxy:"password"` Port int `proxy:"port"`
Cipher string `proxy:"cipher"` Password string `proxy:"password"`
UDP bool `proxy:"udp,omitempty"` Cipher string `proxy:"cipher"`
Plugin string `proxy:"plugin,omitempty"` UDP bool `proxy:"udp,omitempty"`
PluginOpts map[string]interface{} `proxy:"plugin-opts,omitempty"` Plugin string `proxy:"plugin,omitempty"`
PluginOpts map[string]any `proxy:"plugin-opts,omitempty"`
} }
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,25 +73,30 @@ 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
c, err := dialer.DialContext(ctx, "tcp", ss.addr) func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
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
} }
func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { // ListenPacketContext implements C.ProxyAdapter
pc, err := dialer.ListenPacket("udp", "") func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
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,16 +149,17 @@ 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()
} }
} }
return &ShadowSocks{ return &ShadowSocks{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: addr, addr: addr,
tp: C.Shadowsocks, tp: C.Shadowsocks,
udp: option.UDP, udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
}, },
cipher: ciph, cipher: ciph,

View File

@ -0,0 +1,157 @@
package outbound
import (
"context"
"fmt"
"net"
"strconv"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/shadowsocks/core"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowaead"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowstream"
"github.com/Dreamacro/clash/transport/ssr/obfs"
"github.com/Dreamacro/clash/transport/ssr/protocol"
)
type ShadowSocksR struct {
*Base
cipher core.Cipher
obfs obfs.Obfs
protocol protocol.Protocol
}
type ShadowSocksROption struct {
BasicOption
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, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ssr.addr, ssr.Base.DialOptions(opts...)...)
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
}
// ListenPacketContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := dialer.ListenPacket(ctx, "udp", "", ssr.Base.DialOptions(opts...)...)
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) {
// SSR protocol compatibility
// https://github.com/Dreamacro/clash/pull/2056
if option.Cipher == "none" {
option.Cipher = "dummy"
}
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 none 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,
iface: option.Interface,
rmark: option.RoutingMark,
},
cipher: coreCiph,
obfs: obfs,
protocol: protocol,
}, nil
}

View File

@ -8,9 +8,9 @@ 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/snell"
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/snell"
) )
type Snell struct { type Snell struct {
@ -22,12 +22,14 @@ type Snell struct {
} }
type SnellOption struct { type SnellOption struct {
Name string `proxy:"name"` BasicOption
Server string `proxy:"server"` Name string `proxy:"name"`
Port int `proxy:"port"` Server string `proxy:"server"`
Psk string `proxy:"psk"` Port int `proxy:"port"`
Version int `proxy:"version,omitempty"` Psk string `proxy:"psk"`
ObfsOpts map[string]interface{} `proxy:"obfs-opts,omitempty"` UDP bool `proxy:"udp,omitempty"`
Version int `proxy:"version,omitempty"`
ObfsOpts map[string]any `proxy:"obfs-opts,omitempty"`
} }
type streamOption struct { type streamOption struct {
@ -48,35 +50,60 @@ func streamConn(c net.Conn, option streamOption) *snell.Snell {
return snell.StreamConn(c, option.psk, option.version) 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) { 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}) c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
port, _ := strconv.Atoi(metadata.DstPort) port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version) err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
return c, err return c, err
} }
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { // DialContext implements C.ProxyAdapter
if s.version == snell.Version2 { func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
if s.version == snell.Version2 && len(opts) == 0 {
c, err := s.pool.Get() c, err := s.pool.Get()
if err != nil { if err != nil {
return nil, err return nil, err
} }
port, _ := strconv.Atoi(metadata.DstPort) port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
err = snell.WriteHeader(c, metadata.String(), uint(port), s.version) if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil {
c.Close()
return nil, err
}
return NewConn(c, s), err return NewConn(c, s), err
} }
c, err := dialer.DialContext(ctx, "tcp", s.addr) c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", s.addr, err) return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = s.StreamConn(c, metadata) c, err = s.StreamConn(c, metadata)
return NewConn(c, s), err return NewConn(c, s), err
} }
// ListenPacketContext implements C.ProxyAdapter
func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...)
if err != nil {
return nil, err
}
tcpKeepAlive(c)
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
err = snell.WriteUDPHeader(c, s.version)
if err != nil {
return nil, err
}
pc := snell.PacketConn(c)
return newPacketConn(pc, s), nil
}
func NewSnell(option SnellOption) (*Snell, error) { func NewSnell(option SnellOption) (*Snell, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
psk := []byte(option.Psk) psk := []byte(option.Psk)
@ -87,7 +114,10 @@ func NewSnell(option SnellOption) (*Snell, error) {
return nil, fmt.Errorf("snell %s initialize obfs error: %w", addr, err) return nil, fmt.Errorf("snell %s initialize obfs error: %w", addr, err)
} }
if obfsOption.Mode != "tls" && obfsOption.Mode != "http" { switch obfsOption.Mode {
case "tls", "http", "":
break
default:
return nil, fmt.Errorf("snell %s obfs mode error: %s", addr, obfsOption.Mode) return nil, fmt.Errorf("snell %s obfs mode error: %s", addr, obfsOption.Mode)
} }
@ -95,15 +125,24 @@ func NewSnell(option SnellOption) (*Snell, error) {
if option.Version == 0 { if option.Version == 0 {
option.Version = snell.DefaultSnellVersion option.Version = snell.DefaultSnellVersion
} }
if option.Version != snell.Version1 && option.Version != snell.Version2 { switch option.Version {
case snell.Version1, snell.Version2:
if option.UDP {
return nil, fmt.Errorf("snell version %d not support UDP", option.Version)
}
case snell.Version3:
default:
return nil, fmt.Errorf("snell version error: %d", option.Version) return nil, fmt.Errorf("snell version error: %d", option.Version)
} }
s := &Snell{ s := &Snell{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: addr, addr: addr,
tp: C.Snell, tp: C.Snell,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
}, },
psk: psk, psk: psk,
obfsOption: obfsOption, obfsOption: obfsOption,
@ -112,7 +151,7 @@ func NewSnell(option SnellOption) (*Snell, error) {
if option.Version == snell.Version2 { if option.Version == snell.Version2 {
s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) { s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) {
c, err := dialer.DialContext(ctx, "tcp", addr) c, err := dialer.DialContext(ctx, "tcp", addr, s.Base.DialOptions()...)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -6,13 +6,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net" "net"
"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 {
@ -25,6 +24,7 @@ type Socks5 struct {
} }
type Socks5Option struct { type Socks5Option struct {
BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
@ -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
c, err := dialer.DialContext(ctx, "tcp", ss.addr) func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
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,10 +77,9 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn
return NewConn(c, ss), nil return NewConn(c, ss), nil
} }
func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) { // ListenPacketContext implements C.ProxyAdapter
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout) func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
defer cancel() c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
if err != nil { if err != nil {
err = fmt.Errorf("%s connect error: %w", ss.addr, err) err = fmt.Errorf("%s connect error: %w", ss.addr, err)
return return
@ -88,11 +91,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
@ -109,20 +108,34 @@ func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
return return
} }
pc, err := dialer.ListenPacket("udp", "") pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return return
} }
go func() { go func() {
io.Copy(ioutil.Discard, c) io.Copy(io.Discard, c)
c.Close() c.Close()
// A UDP association terminates when the TCP connection that the UDP // A UDP association terminates when the TCP connection that the UDP
// ASSOCIATE request arrived on terminates. RFC1928 // ASSOCIATE request arrived on terminates. RFC1928
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,17 +143,18 @@ 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,
} }
} }
return &Socks5{ return &Socks5{
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.Socks5, tp: C.Socks5,
udp: option.UDP, udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
}, },
user: option.UserName, user: option.UserName,
pass: option.Password, pass: option.Password,

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

@ -0,0 +1,208 @@
package outbound
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"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
option *TrojanOption
// for gun mux
gunTLSConfig *tls.Config
gunConfig *gun.Config
transport *http2.Transport
}
type TrojanOption struct {
BasicOption
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"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
}
func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) {
if t.option.Network == "ws" {
host, port, _ := net.SplitHostPort(t.addr)
wsOpts := &trojan.WebsocketOption{
Host: host,
Port: port,
Path: t.option.WSOpts.Path,
}
if t.option.SNI != "" {
wsOpts.Host = t.option.SNI
}
if len(t.option.WSOpts.Headers) != 0 {
header := http.Header{}
for key, value := range t.option.WSOpts.Headers {
header.Add(key, value)
}
wsOpts.Headers = header
}
return t.instance.StreamWebsocketConn(c, wsOpts)
}
return t.instance.StreamConn(c)
}
// 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.plainStream(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, opts ...dialer.Option) (_ C.Conn, err error) {
// gun transport
if t.transport != nil && len(opts) == 0 {
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, t.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = t.StreamConn(c, metadata)
if err != nil {
return nil, err
}
return NewConn(c, t), err
}
// ListenPacketContext implements C.ProxyAdapter
func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
var c net.Conn
// grpc transport
if t.transport != nil && len(opts) == 0 {
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 {
c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
defer safeConnClose(c, err)
tcpKeepAlive(c)
c, err = t.plainStream(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,
iface: option.Interface,
rmark: option.RoutingMark,
},
instance: trojan.New(tOption),
option: &option,
}
if option.Network == "grpc" {
dialFn := func(network, addr string) (net.Conn, error) {
c, err := dialer.DialContext(context.Background(), "tcp", t.addr, t.Base.DialOptions()...)
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,19 +18,13 @@ 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) addrType := metadata.AddrType()
p, _ := strconv.Atoi(metadata.DstPort) aType := uint8(addrType)
p, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
port := []byte{uint8(p >> 8), uint8(p & 0xff)} port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch metadata.AddrType { switch addrType {
case socks5.AtypDomainName: case socks5.AtypDomainName:
len := uint8(len(metadata.Host)) len := uint8(len(metadata.Host))
host := []byte(metadata.Host) host := []byte(metadata.Host)
@ -98,3 +51,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()
}
}

368
adapter/outbound/vmess.go Normal file
View File

@ -0,0 +1,368 @@
package outbound
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/socks5"
"github.com/Dreamacro/clash/transport/vmess"
"golang.org/x/net/http2"
)
type Vmess struct {
*Base
client *vmess.Client
option *VmessOption
// for gun mux
gunTLSConfig *tls.Config
gunConfig *gun.Config
transport *http2.Transport
}
type VmessOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UUID string `proxy:"uuid"`
AlterID int `proxy:"alterId"`
Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
TLS bool `proxy:"tls,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
ServerName string `proxy:"servername,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
}
type HTTPOptions struct {
Method string `proxy:"method,omitempty"`
Path []string `proxy:"path,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"`
}
type WSOptions struct {
Path string `proxy:"path,omitempty"`
Headers map[string]string `proxy:"headers,omitempty"`
MaxEarlyData int `proxy:"max-early-data,omitempty"`
EarlyDataHeaderName string `proxy:"early-data-header-name,omitempty"`
}
// StreamConn implements C.ProxyAdapter
func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
switch v.option.Network {
case "ws":
host, port, _ := net.SplitHostPort(v.addr)
wsOpts := &vmess.WebsocketConfig{
Host: host,
Port: port,
Path: v.option.WSOpts.Path,
MaxEarlyData: v.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
}
if len(v.option.WSOpts.Headers) != 0 {
header := http.Header{}
for key, value := range v.option.WSOpts.Headers {
header.Add(key, value)
}
wsOpts.Headers = header
}
if v.option.TLS {
wsOpts.TLS = true
wsOpts.TLSConfig = &tls.Config{
ServerName: host,
InsecureSkipVerify: v.option.SkipCertVerify,
NextProtos: []string{"http/1.1"},
}
if v.option.ServerName != "" {
wsOpts.TLSConfig.ServerName = v.option.ServerName
} else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host
}
}
c, err = vmess.StreamWebsocketConn(c, wsOpts)
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)
httpOpts := &vmess.HTTPConfig{
Host: host,
Method: v.option.HTTPOpts.Method,
Path: v.option.HTTPOpts.Path,
Headers: v.option.HTTPOpts.Headers,
}
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:
// handle TLS
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
}
return v.client.StreamConn(c, parseVmessAddr(metadata))
}
// DialContext implements C.ProxyAdapter
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
// gun transport
if v.transport != nil && len(opts) == 0 {
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, v.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = v.StreamConn(c, metadata)
return NewConn(c, v), err
}
// ListenPacketContext implements C.ProxyAdapter
func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
// vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
var c net.Conn
// gun transport
if v.transport != nil && len(opts) == 0 {
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))
} else {
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = v.StreamConn(c, metadata)
}
if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err)
}
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
}
func NewVmess(option VmessOption) (*Vmess, error) {
security := strings.ToLower(option.Cipher)
client, err := vmess.NewClient(vmess.Config{
UUID: option.UUID,
AlterID: uint16(option.AlterID),
Security: security,
HostName: option.Server,
Port: strconv.Itoa(option.Port),
IsAead: option.AlterID == 0,
})
if err != nil {
return nil, err
}
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{
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vmess,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
},
client: client,
option: &option,
}
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, v.Base.DialOptions()...)
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 {
var addrType byte
var addr []byte
switch metadata.AddrType() {
case socks5.AtypIPv4:
addrType = byte(vmess.AtypIPv4)
addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.To4())
case socks5.AtypIPv6:
addrType = byte(vmess.AtypIPv6)
addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.To16())
case socks5.AtypDomainName:
addrType = byte(vmess.AtypDomainName)
addr = make([]byte, len(metadata.Host)+1)
addr[0] = byte(len(metadata.Host))
copy(addr[1:], []byte(metadata.Host))
}
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
return &vmess.DstAddr{
UDP: metadata.NetWork == C.UDP,
AddrType: addrType,
Addr: addr,
Port: uint(port),
}
}
type vmessPacketConn struct {
net.Conn
rAddr net.Addr
}
func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
return uc.Conn.Write(b)
}
func (uc *vmessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, err := uc.Conn.Read(b)
return n, uc.rAddr, err
}

View File

@ -0,0 +1,24 @@
package outboundgroup
import (
"time"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
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

@ -0,0 +1,106 @@
package outboundgroup
import (
"context"
"encoding/json"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
type Fallback struct {
*outbound.Base
disableUDP bool
single *singledo.Single
providers []provider.ProxyProvider
}
func (f *Fallback) Now() string {
proxy := f.findAliveProxy(false)
return proxy.Name()
}
// DialContext implements C.ProxyAdapter
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
proxy := f.findAliveProxy(true)
c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...)
if err == nil {
c.AppendToChains(f)
}
return c, err
}
// ListenPacketContext implements C.ProxyAdapter
func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
proxy := f.findAliveProxy(true)
pc, err := proxy.ListenPacketContext(ctx, metadata, f.Base.DialOptions(opts...)...)
if err == nil {
pc.AppendToChains(f)
}
return pc, err
}
// SupportUDP implements C.ProxyAdapter
func (f *Fallback) SupportUDP() bool {
if f.disableUDP {
return false
}
proxy := f.findAliveProxy(false)
return proxy.SupportUDP()
}
// MarshalJSON implements C.ProxyAdapter
func (f *Fallback) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range f.proxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
"type": f.Type().String(),
"now": f.Now(),
"all": all,
})
}
// Unwrap implements C.ProxyAdapter
func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
proxy := f.findAliveProxy(true)
return proxy
}
func (f *Fallback) proxies(touch bool) []C.Proxy {
elm, _, _ := f.single.Do(func() (any, error) {
return getProvidersProxies(f.providers, touch), nil
})
return elm.([]C.Proxy)
}
func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
proxies := f.proxies(touch)
for _, proxy := range proxies {
if proxy.Alive() {
return proxy
}
}
return proxies[0]
}
func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
return &Fallback{
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.Fallback,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
disableUDP: option.DisableUDP,
}
}

View File

@ -0,0 +1,189 @@
package outboundgroup
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/murmur3"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
"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]any) string {
if strategy, ok := config["strategy"].(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, opts ...dialer.Option) (c C.Conn, err error) {
defer func() {
if err == nil {
c.AppendToChains(lb)
}
}()
proxy := lb.Unwrap(metadata)
c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
return
}
// ListenPacketContext implements C.ProxyAdapter
func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (pc C.PacketConn, err error) {
defer func() {
if err == nil {
pc.AppendToChains(lb)
}
}()
proxy := lb.Unwrap(metadata)
return proxy.ListenPacketContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
}
// 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
}
}
// when availability is poor, traverse the entire list to get the available nodes
for _, proxy := range proxies {
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() (any, 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]any{
"type": lb.Type().String(),
"all": all,
})
}
func NewLoadBalance(option *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(outbound.BaseOption{
Name: option.Name,
Type: C.LoadBalance,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
strategyFn: strategyFn,
disableUDP: option.DisableUDP,
}, nil
}

View File

@ -4,9 +4,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/Dreamacro/clash/adapters/provider" "github.com/Dreamacro/clash/adapter/outbound"
"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"
types "github.com/Dreamacro/clash/constant/provider"
) )
var ( var (
@ -14,22 +16,27 @@ var (
errType = errors.New("unsupport type") errType = errors.New("unsupport type")
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"` outbound.BasicOption
Type string `group:"type"` Name string `group:"name"`
Proxies []string `group:"proxies,omitempty"` Type string `group:"type"`
Use []string `group:"use,omitempty"` Proxies []string `group:"proxies,omitempty"`
URL string `group:"url,omitempty"` Use []string `group:"use,omitempty"`
Interval int `group:"interval,omitempty"` URL string `group:"url,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]any, proxyMap map[string]C.Proxy, providersMap map[string]types.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
} }
@ -40,7 +47,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
groupName := groupOption.Name groupName := groupOption.Name
providers := []provider.ProxyProvider{} providers := []types.ProxyProvider{}
if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 { if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 {
return nil, errMissProxy return nil, errMissProxy
@ -52,44 +59,33 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
return nil, err return nil, err
} }
// if Use not empty, drop health check options if _, ok := providersMap[groupName]; ok {
if len(groupOption.Use) != 0 { return nil, errDuplicateProvider
hc := provider.NewHealthCheck(ps, "", 0) }
// select don't need health check
if groupOption.Type == "select" || groupOption.Type == "relay" {
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
} }
providers = append(providers, pd) providers = append(providers, pd)
providersMap[groupName] = pd
} else { } else {
if _, ok := providersMap[groupName]; ok { if groupOption.URL == "" || groupOption.Interval == 0 {
return nil, errDuplicateProvider return nil, errMissHealthCheck
} }
// select don't need health check hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
if groupOption.Type == "select" || groupOption.Type == "relay" { pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
hc := provider.NewHealthCheck(ps, "", 0) if err != nil {
pd, err := provider.NewCompatibleProvider(groupName, ps, hc) return nil, err
if err != nil {
return nil, err
}
providers = append(providers, pd)
providersMap[groupName] = pd
} else {
if groupOption.URL == "" || groupOption.Interval == 0 {
return nil, errMissHealthCheck
}
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval))
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, err
}
providers = append(providers, pd)
providersMap[groupName] = pd
} }
providers = append(providers, pd)
providersMap[groupName] = pd
} }
} }
@ -105,15 +101,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)
} }
@ -133,15 +130,15 @@ func getProxies(mapping map[string]C.Proxy, list []string) ([]C.Proxy, error) {
return ps, nil return ps, nil
} }
func getProviders(mapping map[string]provider.ProxyProvider, list []string) ([]provider.ProxyProvider, error) { func getProviders(mapping map[string]types.ProxyProvider, list []string) ([]types.ProxyProvider, error) {
var ps []provider.ProxyProvider var ps []types.ProxyProvider
for _, name := range list { for _, name := range list {
p, ok := mapping[name] p, ok := mapping[name]
if !ok { if !ok {
return nil, fmt.Errorf("'%s' not found", name) return nil, fmt.Errorf("'%s' not found", name)
} }
if p.VehicleType() == provider.Compatible { if p.VehicleType() == types.Compatible {
return nil, fmt.Errorf("proxy group %s can't contains in `use`", name) return nil, fmt.Errorf("proxy group %s can't contains in `use`", name)
} }
ps = append(ps, p) ps = append(ps, p)

View File

@ -3,14 +3,13 @@ package outboundgroup
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"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/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"
"github.com/Dreamacro/clash/constant/provider"
) )
type Relay struct { type Relay struct {
@ -19,15 +18,26 @@ type Relay struct {
providers []provider.ProxyProvider providers []provider.ProxyProvider
} }
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { // DialContext implements C.ProxyAdapter
proxies := r.proxies(metadata) func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
if len(proxies) == 0 { var proxies []C.Proxy
return nil, errors.New("proxy does not exist") for _, proxy := range r.proxies(metadata, true) {
if proxy.Type() != C.Direct {
proxies = append(proxies, proxy)
}
} }
switch len(proxies) {
case 0:
return outbound.NewDirect().DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
case 1:
return proxies[0].DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
}
first := proxies[0] first := proxies[0]
last := proxies[len(proxies)-1] last := proxies[len(proxies)-1]
c, err := dialer.DialContext(ctx, "tcp", first.Addr()) c, err := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err) return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
} }
@ -56,27 +66,28 @@ 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]any{
"type": r.Type().String(), "type": r.Type().String(),
"all": all, "all": all,
}) })
} }
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() (any, 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 +100,14 @@ func (r *Relay) proxies(metadata *C.Metadata) []C.Proxy {
return proxies return proxies
} }
func NewRelay(name string, providers []provider.ProxyProvider) *Relay { func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
return &Relay{ return &Relay{
Base: outbound.NewBase(name, "", C.Relay, false), Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.Relay,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration), single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers, providers: providers,
} }

View File

@ -0,0 +1,114 @@
package outboundgroup
import (
"context"
"encoding/json"
"errors"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
type Selector struct {
*outbound.Base
disableUDP bool
single *singledo.Single
selected string
providers []provider.ProxyProvider
}
// DialContext implements C.ProxyAdapter
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
c, err := s.selectedProxy(true).DialContext(ctx, metadata, s.Base.DialOptions(opts...)...)
if err == nil {
c.AppendToChains(s)
}
return c, err
}
// ListenPacketContext implements C.ProxyAdapter
func (s *Selector) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := s.selectedProxy(true).ListenPacketContext(ctx, metadata, s.Base.DialOptions(opts...)...)
if err == nil {
pc.AppendToChains(s)
}
return pc, err
}
// SupportUDP implements C.ProxyAdapter
func (s *Selector) SupportUDP() bool {
if s.disableUDP {
return false
}
return s.selectedProxy(false).SupportUDP()
}
// MarshalJSON implements C.ProxyAdapter
func (s *Selector) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range getProvidersProxies(s.providers, false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
"type": s.Type().String(),
"now": s.Now(),
"all": all,
})
}
func (s *Selector) Now() string {
return s.selectedProxy(false).Name()
}
func (s *Selector) Set(name string) error {
for _, proxy := range getProvidersProxies(s.providers, false) {
if proxy.Name() == name {
s.selected = name
s.single.Reset()
return nil
}
}
return errors.New("proxy not exist")
}
// Unwrap implements C.ProxyAdapter
func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy {
return s.selectedProxy(true)
}
func (s *Selector) selectedProxy(touch bool) C.Proxy {
elm, _, _ := s.single.Do(func() (any, error) {
proxies := getProvidersProxies(s.providers, touch)
for _, proxy := range proxies {
if proxy.Name() == s.selected {
return proxy, nil
}
}
return proxies[0], nil
})
return elm.(C.Proxy)
}
func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
selected := providers[0].Proxies()[0].Name()
return &Selector{
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.Selector,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
selected: selected,
disableUDP: option.DisableUDP,
}
}

View File

@ -0,0 +1,154 @@
package outboundgroup
import (
"context"
"encoding/json"
"time"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
type urlTestOption func(*URLTest)
func urlTestWithTolerance(tolerance uint16) urlTestOption {
return func(u *URLTest) {
u.tolerance = tolerance
}
}
type URLTest struct {
*outbound.Base
tolerance uint16
disableUDP bool
fastNode C.Proxy
single *singledo.Single
fastSingle *singledo.Single
providers []provider.ProxyProvider
}
func (u *URLTest) Now() string {
return u.fast(false).Name()
}
// DialContext implements C.ProxyAdapter
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
c, err = u.fast(true).DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
if err == nil {
c.AppendToChains(u)
}
return c, err
}
// ListenPacketContext implements C.ProxyAdapter
func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := u.fast(true).ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...)
if err == nil {
pc.AppendToChains(u)
}
return pc, err
}
// Unwrap implements C.ProxyAdapter
func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy {
return u.fast(true)
}
func (u *URLTest) proxies(touch bool) []C.Proxy {
elm, _, _ := u.single.Do(func() (any, error) {
return getProvidersProxies(u.providers, touch), nil
})
return elm.([]C.Proxy)
}
func (u *URLTest) fast(touch bool) C.Proxy {
elm, _, _ := u.fastSingle.Do(func() (any, error) {
proxies := u.proxies(touch)
fast := proxies[0]
min := fast.LastDelay()
fastNotExist := true
for _, proxy := range proxies[1:] {
if u.fastNode != nil && proxy.Name() == u.fastNode.Name() {
fastNotExist = false
}
if !proxy.Alive() {
continue
}
delay := proxy.LastDelay()
if delay < min {
fast = proxy
min = delay
}
}
// tolerance
if u.fastNode == nil || fastNotExist || !u.fastNode.Alive() || u.fastNode.LastDelay() > fast.LastDelay()+u.tolerance {
u.fastNode = fast
}
return u.fastNode, nil
})
return elm.(C.Proxy)
}
// SupportUDP implements C.ProxyAdapter
func (u *URLTest) SupportUDP() bool {
if u.disableUDP {
return false
}
return u.fast(false).SupportUDP()
}
// MarshalJSON implements C.ProxyAdapter
func (u *URLTest) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range u.proxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
"type": u.Type().String(),
"now": u.Now(),
"all": all,
})
}
func parseURLTestOption(config map[string]any) []urlTestOption {
opts := []urlTestOption{}
// tolerance
if tolerance, ok := config["tolerance"].(int); ok {
opts = append(opts, urlTestWithTolerance(uint16(tolerance)))
}
return opts
}
func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
urlTest := &URLTest{
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.URLTest,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration),
fastSingle: singledo.NewSingle(time.Second * 10),
providers: providers,
disableUDP: option.DisableUDP,
}
for _, option := range options {
option(urlTest)
}
return urlTest
}

View File

@ -16,33 +16,28 @@ 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, Host: host,
Host: host, DstIP: nil,
DstIP: nil, DstPort: port,
DstPort: port, }
return
} else if ip4 := ip.To4(); ip4 != nil {
addr = &C.Metadata{
Host: "",
DstIP: ip4,
DstPort: port,
} }
return return
} }
addr = &C.Metadata{
Host: "",
DstIP: ip,
DstPort: port,
}
return
} }
func tcpKeepAlive(c net.Conn) { func tcpKeepAlive(c net.Conn) {

View File

@ -1,13 +1,14 @@
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"
) )
func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) { func ParseProxy(mapping map[string]any) (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 {
@ -20,36 +21,36 @@ func ParseProxy(mapping map[string]interface{}) (C.Proxy, 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": case "ssr":
ssrOption := &ShadowSocksROption{} ssrOption := &outbound.ShadowSocksROption{}
err = decoder.Decode(mapping, ssrOption) err = decoder.Decode(mapping, ssrOption)
if err != nil { if err != nil {
break break
} }
proxy, err = NewShadowSocksR(*ssrOption) 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{"/"},
}, },
@ -58,21 +59,21 @@ 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)
} }

View File

@ -3,48 +3,48 @@ package provider
import ( import (
"bytes" "bytes"
"crypto/md5" "crypto/md5"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
types "github.com/Dreamacro/clash/constant/provider"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
) )
var ( var (
fileMode os.FileMode = 0666 fileMode os.FileMode = 0o666
dirMode os.FileMode = 0755 dirMode os.FileMode = 0o755
) )
type parser = func([]byte) (interface{}, error) type parser = func([]byte) (any, error)
type fetcher struct { type fetcher struct {
name string name string
vehicle Vehicle vehicle types.Vehicle
updatedAt *time.Time updatedAt *time.Time
ticker *time.Ticker ticker *time.Ticker
done chan struct{} done chan struct{}
hash [16]byte hash [16]byte
parser parser parser parser
onUpdate func(interface{}) onUpdate func(any)
} }
func (f *fetcher) Name() string { func (f *fetcher) Name() string {
return f.name return f.name
} }
func (f *fetcher) VehicleType() VehicleType { func (f *fetcher) VehicleType() types.VehicleType {
return f.vehicle.Type() return f.vehicle.Type()
} }
func (f *fetcher) Initial() (interface{}, error) { func (f *fetcher) Initial() (any, error) {
var ( var (
buf []byte buf []byte
err error err error
isLocal bool isLocal bool
) )
if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil { if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
buf, err = ioutil.ReadFile(f.vehicle.Path()) buf, err = os.ReadFile(f.vehicle.Path())
modTime := stat.ModTime() modTime := stat.ModTime()
f.updatedAt = &modTime f.updatedAt = &modTime
isLocal = true isLocal = true
@ -72,9 +72,11 @@ func (f *fetcher) Initial() (interface{}, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
isLocal = false
} }
if f.vehicle.Type() != File { if f.vehicle.Type() != types.File && !isLocal {
if err := safeWrite(f.vehicle.Path(), buf); err != nil { if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return nil, err return nil, err
} }
@ -90,7 +92,7 @@ func (f *fetcher) Initial() (interface{}, error) {
return proxies, nil return proxies, nil
} }
func (f *fetcher) Update() (interface{}, bool, error) { func (f *fetcher) Update() (any, bool, error) {
buf, err := f.vehicle.Read() buf, err := f.vehicle.Read()
if err != nil { if err != nil {
return nil, false, err return nil, false, err
@ -100,6 +102,7 @@ func (f *fetcher) Update() (interface{}, bool, error) {
hash := md5.Sum(buf) hash := md5.Sum(buf)
if bytes.Equal(f.hash[:], hash[:]) { if bytes.Equal(f.hash[:], hash[:]) {
f.updatedAt = &now f.updatedAt = &now
os.Chtimes(f.vehicle.Path(), now, now)
return nil, true, nil return nil, true, nil
} }
@ -108,8 +111,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() != types.File {
return nil, false, err if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return nil, false, err
}
} }
f.updatedAt = &now f.updatedAt = &now
@ -160,10 +165,10 @@ func safeWrite(path string, buf []byte) error {
} }
} }
return ioutil.WriteFile(path, buf, fileMode) return os.WriteFile(path, buf, fileMode)
} }
func newFetcher(name string, interval time.Duration, vehicle Vehicle, parser parser, onUpdate func(interface{})) *fetcher { func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, parser parser, onUpdate func(any)) *fetcher {
var ticker *time.Ticker var ticker *time.Ticker
if interval != 0 { if interval != 0 {
ticker = time.NewTicker(interval) ticker = time.NewTicker(interval)

View File

@ -0,0 +1,88 @@
package provider
import (
"context"
"time"
"github.com/Dreamacro/clash/common/batch"
C "github.com/Dreamacro/clash/constant"
"go.uber.org/atomic"
)
const (
defaultURLTestTimeout = time.Second * 5
)
type HealthCheckOption struct {
URL string
Interval uint
}
type HealthCheck struct {
url string
proxies []C.Proxy
interval uint
lazy bool
lastTouch *atomic.Int64
done chan struct{}
}
func (hc *HealthCheck) process() {
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
go hc.check()
for {
select {
case <-ticker.C:
now := time.Now().Unix()
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
hc.check()
}
case <-hc.done:
ticker.Stop()
return
}
}
}
func (hc *HealthCheck) setProxy(proxies []C.Proxy) {
hc.proxies = proxies
}
func (hc *HealthCheck) auto() bool {
return hc.interval != 0
}
func (hc *HealthCheck) touch() {
hc.lastTouch.Store(time.Now().Unix())
}
func (hc *HealthCheck) check() {
b, _ := batch.New(context.Background(), batch.WithConcurrencyNum(10))
for _, proxy := range hc.proxies {
p := proxy
b.Go(p.Name(), func() (any, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
defer cancel()
p.URLTest(ctx, hc.url)
return nil, nil
})
}
b.Wait()
}
func (hc *HealthCheck) close() {
hc.done <- struct{}{}
}
func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool) *HealthCheck {
return &HealthCheck{
proxies: proxies,
url: url,
interval: interval,
lazy: lazy,
lastTouch: atomic.NewInt64(0),
done: make(chan struct{}, 1),
}
}

View File

@ -7,16 +7,16 @@ import (
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider"
) )
var ( var errVehicleType = errors.New("unsupport vehicle type")
errVehicleType = errors.New("unsupport vehicle type")
)
type healthCheckSchema struct { 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 {
@ -24,26 +24,31 @@ type proxyProviderSchema struct {
Path string `provider:"path"` Path string `provider:"path"`
URL string `provider:"url,omitempty"` URL string `provider:"url,omitempty"`
Interval int `provider:"interval,omitempty"` Interval int `provider:"interval,omitempty"`
Filter string `provider:"filter,omitempty"`
HealthCheck healthCheckSchema `provider:"health-check,omitempty"` HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
} }
func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvider, error) { func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvider, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true}) decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
schema := &proxyProviderSchema{} schema := &proxyProviderSchema{
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)
var vehicle Vehicle var vehicle types.Vehicle
switch schema.Type { switch schema.Type {
case "file": case "file":
vehicle = NewFileVehicle(path) vehicle = NewFileVehicle(path)
@ -54,5 +59,6 @@ func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvi
} }
interval := time.Duration(uint(schema.Interval)) * time.Second interval := time.Duration(uint(schema.Interval)) * time.Second
return NewProxySetProvider(name, interval, vehicle, hc), nil filter := schema.Filter
return NewProxySetProvider(name, interval, filter, vehicle, hc)
} }

View File

@ -4,57 +4,23 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"regexp"
"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"
types "github.com/Dreamacro/clash/constant/provider"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
) )
const ( const (
ReservedName = "default" ReservedName = "default"
) )
// Provider Type
const (
Proxy ProviderType = iota
Rule
)
// ProviderType defined
type ProviderType int
func (pt ProviderType) String() string {
switch pt {
case Proxy:
return "Proxy"
case Rule:
return "Rule"
default:
return "Unknown"
}
}
// Provider interface
type Provider interface {
Name() string
VehicleType() VehicleType
Type() ProviderType
Initial() error
Update() error
}
// ProxyProvider interface
type ProxyProvider interface {
Provider
Proxies() []C.Proxy
HealthCheck()
}
type ProxySchema struct { type ProxySchema struct {
Proxies []map[string]interface{} `yaml:"proxies"` Proxies []map[string]any `yaml:"proxies"`
} }
// for auto gc // for auto gc
@ -69,7 +35,7 @@ type proxySetProvider struct {
} }
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) { func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{ return json.Marshal(map[string]any{
"name": pp.Name(), "name": pp.Name(),
"type": pp.Type().String(), "type": pp.Type().String(),
"vehicleType": pp.VehicleType().String(), "vehicleType": pp.VehicleType().String(),
@ -104,39 +70,17 @@ func (pp *proxySetProvider) Initial() error {
return nil return nil
} }
func (pp *proxySetProvider) Type() ProviderType { func (pp *proxySetProvider) Type() types.ProviderType {
return Proxy return types.Proxy
} }
func (pp *proxySetProvider) Proxies() []C.Proxy { func (pp *proxySetProvider) Proxies() []C.Proxy {
return pp.proxies return pp.proxies
} }
func proxiesParse(buf []byte) (interface{}, error) { func (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy {
schema := &ProxySchema{} pp.healthCheck.touch()
return pp.Proxies()
if err := yaml.Unmarshal(buf, schema); err != nil {
return nil, err
}
if schema.Proxies == nil {
return nil, errors.New("file must have a `proxies` field")
}
proxies := []C.Proxy{}
for idx, mapping := range schema.Proxies {
proxy, err := outbound.ParseProxy(mapping)
if err != nil {
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
}
proxies = append(proxies, proxy)
}
if len(proxies) == 0 {
return nil, errors.New("file doesn't have any valid proxy")
}
return proxies, nil
} }
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) { func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
@ -152,7 +96,12 @@ func stopProxyProvider(pd *ProxySetProvider) {
pd.fetcher.Destroy() pd.fetcher.Destroy()
} }
func NewProxySetProvider(name string, interval time.Duration, vehicle Vehicle, hc *HealthCheck) *ProxySetProvider { func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
filterReg, err := regexp.Compile(filter)
if err != nil {
return nil, fmt.Errorf("invalid filter regex: %w", err)
}
if hc.auto() { if hc.auto() {
go hc.process() go hc.process()
} }
@ -162,17 +111,50 @@ func NewProxySetProvider(name string, interval time.Duration, vehicle Vehicle, h
healthCheck: hc, healthCheck: hc,
} }
onUpdate := func(elm interface{}) { onUpdate := func(elm any) {
ret := elm.([]C.Proxy) ret := elm.([]C.Proxy)
pd.setProxies(ret) pd.setProxies(ret)
} }
fetcher := newFetcher(name, interval, vehicle, proxiesParse, onUpdate) proxiesParseAndFilter := func(buf []byte) (any, error) {
schema := &ProxySchema{}
if err := yaml.Unmarshal(buf, schema); err != nil {
return nil, err
}
if schema.Proxies == nil {
return nil, errors.New("file must have a `proxies` field")
}
proxies := []C.Proxy{}
for idx, mapping := range schema.Proxies {
if name, ok := mapping["name"].(string); ok && len(filter) > 0 && !filterReg.MatchString(name) {
continue
}
proxy, err := adapter.ParseProxy(mapping)
if err != nil {
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
}
proxies = append(proxies, proxy)
}
if len(proxies) == 0 {
if len(filter) > 0 {
return nil, errors.New("doesn't match any proxy, please check your filter")
}
return nil, errors.New("file doesn't have any proxy")
}
return proxies, nil
}
fetcher := newFetcher(name, interval, vehicle, proxiesParseAndFilter, onUpdate)
pd.fetcher = fetcher pd.fetcher = fetcher
wrapper := &ProxySetProvider{pd} wrapper := &ProxySetProvider{pd}
runtime.SetFinalizer(wrapper, stopProxyProvider) runtime.SetFinalizer(wrapper, stopProxyProvider)
return wrapper return wrapper, nil
} }
// for auto gc // for auto gc
@ -187,7 +169,7 @@ type compatibleProvider struct {
} }
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) { func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{ return json.Marshal(map[string]any{
"name": cp.Name(), "name": cp.Name(),
"type": cp.Type().String(), "type": cp.Type().String(),
"vehicleType": cp.VehicleType().String(), "vehicleType": cp.VehicleType().String(),
@ -211,25 +193,30 @@ func (cp *compatibleProvider) Initial() error {
return nil return nil
} }
func (cp *compatibleProvider) VehicleType() VehicleType { func (cp *compatibleProvider) VehicleType() types.VehicleType {
return Compatible return types.Compatible
} }
func (cp *compatibleProvider) Type() ProviderType { func (cp *compatibleProvider) Type() types.ProviderType {
return Proxy return types.Proxy
} }
func (cp *compatibleProvider) Proxies() []C.Proxy { 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()
} }
func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) { func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) {
if len(proxies) == 0 { if len(proxies) == 0 {
return nil, errors.New("Provider need one proxy at least") return nil, errors.New("provider need one proxy at least")
} }
if hc.auto() { if hc.auto() {

View File

@ -2,49 +2,23 @@ package provider
import ( import (
"context" "context"
"io/ioutil" "io"
"net"
"net/http" "net/http"
"net/url" "net/url"
"os"
"time" "time"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
types "github.com/Dreamacro/clash/constant/provider"
) )
// Vehicle Type
const (
File VehicleType = iota
HTTP
Compatible
)
// VehicleType defined
type VehicleType int
func (v VehicleType) String() string {
switch v {
case File:
return "File"
case HTTP:
return "HTTP"
case Compatible:
return "Compatible"
default:
return "Unknown"
}
}
type Vehicle interface {
Read() ([]byte, error)
Path() string
Type() VehicleType
}
type FileVehicle struct { type FileVehicle struct {
path string path string
} }
func (f *FileVehicle) Type() VehicleType { func (f *FileVehicle) Type() types.VehicleType {
return File return types.File
} }
func (f *FileVehicle) Path() string { func (f *FileVehicle) Path() string {
@ -52,7 +26,7 @@ func (f *FileVehicle) Path() string {
} }
func (f *FileVehicle) Read() ([]byte, error) { func (f *FileVehicle) Read() ([]byte, error) {
return ioutil.ReadFile(f.path) return os.ReadFile(f.path)
} }
func NewFileVehicle(path string) *FileVehicle { func NewFileVehicle(path string) *FileVehicle {
@ -64,8 +38,8 @@ type HTTPVehicle struct {
path string path string
} }
func (h *HTTPVehicle) Type() VehicleType { func (h *HTTPVehicle) Type() types.VehicleType {
return HTTP return types.HTTP
} }
func (h *HTTPVehicle) Path() string { func (h *HTTPVehicle) Path() string {
@ -99,7 +73,9 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
IdleConnTimeout: 90 * time.Second, IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 1 * time.Second,
DialContext: dialer.DialContext, DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
return dialer.DialContext(ctx, network, address)
},
} }
client := http.Client{Transport: transport} client := http.Client{Transport: transport}
@ -107,8 +83,9 @@ 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 := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }

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,61 +0,0 @@
package outbound
import (
"context"
"errors"
"io"
"net"
"time"
C "github.com/Dreamacro/clash/constant"
)
type Reject struct {
*Base
}
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
return NewConn(&NopConn{}, r), nil
}
func (r *Reject) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
return nil, errors.New("match reject rule")
}
func NewReject() *Reject {
return &Reject{
Base: &Base{
name: "REJECT",
tp: C.Reject,
udp: true,
},
}
}
type NopConn struct{}
func (rw *NopConn) Read(b []byte) (int, error) {
return 0, io.EOF
}
func (rw *NopConn) Write(b []byte) (int, error) {
return 0, io.EOF
}
// Close is fake function for net.Conn
func (rw *NopConn) Close() error { return nil }
// LocalAddr is fake function for net.Conn
func (rw *NopConn) LocalAddr() net.Addr { return nil }
// RemoteAddr is fake function for net.Conn
func (rw *NopConn) RemoteAddr() net.Addr { return nil }
// SetDeadline is fake function for net.Conn
func (rw *NopConn) SetDeadline(time.Time) error { return nil }
// SetReadDeadline is fake function for net.Conn
func (rw *NopConn) SetReadDeadline(time.Time) error { return nil }
// SetWriteDeadline is fake function for net.Conn
func (rw *NopConn) SetWriteDeadline(time.Time) error { return nil }

View File

@ -1,134 +0,0 @@
package outbound
import (
"context"
"encoding/json"
"fmt"
"net"
"strconv"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/ssr/obfs"
"github.com/Dreamacro/clash/component/ssr/protocol"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/go-shadowsocks2/core"
"github.com/Dreamacro/go-shadowsocks2/shadowstream"
)
type ShadowSocksR struct {
*Base
cipher *core.StreamCipher
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"`
}
func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c = obfs.NewConn(c, ssr.obfs)
c = ssr.cipher.StreamConn(c)
conn, ok := c.(*shadowstream.Conn)
if !ok {
return nil, fmt.Errorf("invalid connection type")
}
iv, err := conn.ObtainWriteIV()
if err != nil {
return nil, err
}
c = protocol.NewConn(c, ssr.protocol, iv)
_, err = c.Write(serializesSocksAddr(metadata))
return c, err
}
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, 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)
c, err = ssr.StreamConn(c, metadata)
return NewConn(c, ssr), err
}
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 {
return nil, err
}
pc = ssr.cipher.PacketConn(pc)
pc = protocol.NewPacketConn(pc, ssr.protocol)
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil
}
func (ssr *ShadowSocksR) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": ssr.Type().String(),
})
}
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 cipher error: %w", addr, err)
}
ciph, ok := coreCiph.(*core.StreamCipher)
if !ok {
return nil, fmt.Errorf("%s is not a supported stream cipher in ssr", cipher)
}
obfs, err := obfs.PickObfs(option.Obfs, &obfs.Base{
IVSize: ciph.IVSize(),
Key: ciph.Key,
HeadLen: 30,
Host: option.Server,
Port: option.Port,
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{
IV: nil,
Key: ciph.Key,
TCPMss: 1460,
Param: option.ProtocolParam,
})
if err != nil {
return nil, fmt.Errorf("ssr %s initialize protocol error: %w", addr, err)
}
protocol.SetOverhead(obfs.GetObfsOverhead() + protocol.GetProtocolOverhead())
return &ShadowSocksR{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.ShadowsocksR,
udp: option.UDP,
},
cipher: ciph,
obfs: obfs,
protocol: protocol,
}, 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,260 +0,0 @@
package outbound
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver"
"github.com/Dreamacro/clash/component/vmess"
C "github.com/Dreamacro/clash/constant"
)
type Vmess struct {
*Base
client *vmess.Client
option *VmessOption
}
type VmessOption struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UUID string `proxy:"uuid"`
AlterID int `proxy:"alterId"`
Cipher string `proxy:"cipher"`
TLS bool `proxy:"tls,omitempty"`
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
WSPath string `proxy:"ws-path,omitempty"`
WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
ServerName string `proxy:"servername,omitempty"`
}
type HTTPOptions struct {
Method string `proxy:"method,omitempty"`
Path []string `proxy:"path,omitempty"`
Headers map[string][]string `proxy:"headers,omitempty"`
}
type HTTP2Options struct {
Host []string `proxy:"host,omitempty"`
Path string `proxy:"path,omitempty"`
}
func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
switch v.option.Network {
case "ws":
host, port, _ := net.SplitHostPort(v.addr)
wsOpts := &vmess.WebsocketConfig{
Host: host,
Port: port,
Path: v.option.WSPath,
}
if len(v.option.WSHeaders) != 0 {
header := http.Header{}
for key, value := range v.option.WSHeaders {
header.Add(key, value)
}
wsOpts.Headers = header
}
if v.option.TLS {
wsOpts.TLS = true
wsOpts.SessionCache = getClientSessionCache()
wsOpts.SkipCertVerify = v.option.SkipCertVerify
wsOpts.ServerName = v.option.ServerName
}
c, err = vmess.StreamWebsocketConn(c, wsOpts)
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,
SessionCache: getClientSessionCache(),
}
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)
httpOpts := &vmess.HTTPConfig{
Host: host,
Method: v.option.HTTPOpts.Method,
Path: v.option.HTTPOpts.Path,
Headers: v.option.HTTPOpts.Headers,
}
c = vmess.StreamHTTPConn(c, httpOpts)
case "h2":
host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := vmess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
SessionCache: getClientSessionCache(),
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)
default:
// handle TLS
if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := &vmess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
SessionCache: getClientSessionCache(),
}
if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName
}
c, err = vmess.StreamTLSConn(c, tlsOpts)
}
}
if err != nil {
return nil, err
}
return v.client.StreamConn(c, parseVmessAddr(metadata))
}
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
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)
c, err = v.StreamConn(c, metadata)
return NewConn(c, v), err
}
func (v *Vmess) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
// vmess use stream-oriented udp, so clash needs a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
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)
c, err = v.StreamConn(c, metadata)
if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err)
}
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
}
func NewVmess(option VmessOption) (*Vmess, error) {
security := strings.ToLower(option.Cipher)
client, err := vmess.NewClient(vmess.Config{
UUID: option.UUID,
AlterID: uint16(option.AlterID),
Security: security,
HostName: option.Server,
Port: strconv.Itoa(option.Port),
})
if err != nil {
return nil, err
}
if option.Network == "h2" && !option.TLS {
return nil, fmt.Errorf("TLS must be true with h2 network")
}
return &Vmess{
Base: &Base{
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vmess,
udp: option.UDP,
},
client: client,
option: &option,
}, nil
}
func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
var addrType byte
var addr []byte
switch metadata.AddrType {
case C.AtypIPv4:
addrType = byte(vmess.AtypIPv4)
addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.To4())
case C.AtypIPv6:
addrType = byte(vmess.AtypIPv6)
addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.To16())
case C.AtypDomainName:
addrType = byte(vmess.AtypDomainName)
addr = make([]byte, len(metadata.Host)+1)
addr[0] = byte(len(metadata.Host))
copy(addr[1:], []byte(metadata.Host))
}
port, _ := strconv.Atoi(metadata.DstPort)
return &vmess.DstAddr{
UDP: metadata.NetWork == C.UDP,
AddrType: addrType,
Addr: addr,
Port: uint(port),
}
}
type vmessPacketConn struct {
net.Conn
rAddr net.Addr
}
func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
return uc.Conn.Write(b)
}
func (uc *vmessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, err := uc.Conn.Read(b)
return n, uc.rAddr, err
}

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,89 +0,0 @@
package outboundgroup
import (
"context"
"encoding/json"
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/common/singledo"
C "github.com/Dreamacro/clash/constant"
)
type Fallback struct {
*outbound.Base
single *singledo.Single
providers []provider.ProxyProvider
}
func (f *Fallback) Now() string {
proxy := f.findAliveProxy()
return proxy.Name()
}
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
proxy := f.findAliveProxy()
c, err := proxy.DialContext(ctx, metadata)
if err == nil {
c.AppendToChains(f)
}
return c, err
}
func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
proxy := f.findAliveProxy()
pc, err := proxy.DialUDP(metadata)
if err == nil {
pc.AppendToChains(f)
}
return pc, err
}
func (f *Fallback) SupportUDP() bool {
proxy := f.findAliveProxy()
return proxy.SupportUDP()
}
func (f *Fallback) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range f.proxies() {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
"type": f.Type().String(),
"now": f.Now(),
"all": all,
})
}
func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
proxy := f.findAliveProxy()
return proxy
}
func (f *Fallback) proxies() []C.Proxy {
elm, _, _ := f.single.Do(func() (interface{}, error) {
return getProvidersProxies(f.providers), nil
})
return elm.([]C.Proxy)
}
func (f *Fallback) findAliveProxy() C.Proxy {
proxies := f.proxies()
for _, proxy := range proxies {
if proxy.Alive() {
return proxy
}
}
return f.proxies()[0]
}
func NewFallback(name string, providers []provider.ProxyProvider) *Fallback {
return &Fallback{
Base: outbound.NewBase(name, "", C.Fallback, false),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
}
}

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

@ -1,97 +0,0 @@
package outboundgroup
import (
"context"
"encoding/json"
"errors"
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/common/singledo"
C "github.com/Dreamacro/clash/constant"
)
type Selector struct {
*outbound.Base
single *singledo.Single
selected string
providers []provider.ProxyProvider
}
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
c, err := s.selectedProxy().DialContext(ctx, metadata)
if err == nil {
c.AppendToChains(s)
}
return c, err
}
func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := s.selectedProxy().DialUDP(metadata)
if err == nil {
pc.AppendToChains(s)
}
return pc, err
}
func (s *Selector) SupportUDP() bool {
return s.selectedProxy().SupportUDP()
}
func (s *Selector) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range getProvidersProxies(s.providers) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
"type": s.Type().String(),
"now": s.Now(),
"all": all,
})
}
func (s *Selector) Now() string {
return s.selectedProxy().Name()
}
func (s *Selector) Set(name string) error {
for _, proxy := range getProvidersProxies(s.providers) {
if proxy.Name() == name {
s.selected = name
s.single.Reset()
return nil
}
}
return errors.New("proxy not exist")
}
func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy {
return s.selectedProxy()
}
func (s *Selector) selectedProxy() C.Proxy {
elm, _, _ := s.single.Do(func() (interface{}, error) {
proxies := getProvidersProxies(s.providers)
for _, proxy := range proxies {
if proxy.Name() == s.selected {
return proxy, nil
}
}
return proxies[0], nil
})
return elm.(C.Proxy)
}
func NewSelector(name string, providers []provider.ProxyProvider) *Selector {
selected := providers[0].Proxies()[0].Name()
return &Selector{
Base: outbound.NewBase(name, "", C.Selector, false),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
selected: selected,
}
}

View File

@ -1,133 +0,0 @@
package outboundgroup
import (
"context"
"encoding/json"
"time"
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/common/singledo"
C "github.com/Dreamacro/clash/constant"
)
type urlTestOption func(*URLTest)
func urlTestWithTolerance(tolerance uint16) urlTestOption {
return func(u *URLTest) {
u.tolerance = tolerance
}
}
type URLTest struct {
*outbound.Base
tolerance uint16
fastNode C.Proxy
single *singledo.Single
fastSingle *singledo.Single
providers []provider.ProxyProvider
}
func (u *URLTest) Now() string {
return u.fast().Name()
}
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
c, err = u.fast().DialContext(ctx, metadata)
if err == nil {
c.AppendToChains(u)
}
return c, err
}
func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := u.fast().DialUDP(metadata)
if err == nil {
pc.AppendToChains(u)
}
return pc, err
}
func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy {
return u.fast()
}
func (u *URLTest) proxies() []C.Proxy {
elm, _, _ := u.single.Do(func() (interface{}, error) {
return getProvidersProxies(u.providers), nil
})
return elm.([]C.Proxy)
}
func (u *URLTest) fast() C.Proxy {
elm, _, _ := u.fastSingle.Do(func() (interface{}, error) {
proxies := u.proxies()
fast := proxies[0]
min := fast.LastDelay()
for _, proxy := range proxies[1:] {
if !proxy.Alive() {
continue
}
delay := proxy.LastDelay()
if delay < min {
fast = proxy
min = delay
}
}
// tolerance
if u.fastNode == nil || u.fastNode.LastDelay() > fast.LastDelay()+u.tolerance {
u.fastNode = fast
}
return u.fastNode, nil
})
return elm.(C.Proxy)
}
func (u *URLTest) SupportUDP() bool {
return u.fast().SupportUDP()
}
func (u *URLTest) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range u.proxies() {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
"type": u.Type().String(),
"now": u.Now(),
"all": all,
})
}
func parseURLTestOption(config map[string]interface{}) []urlTestOption {
opts := []urlTestOption{}
// tolerance
if elm, ok := config["tolerance"]; ok {
if tolerance, ok := elm.(int); ok {
opts = append(opts, urlTestWithTolerance(uint16(tolerance)))
}
}
return opts
}
func NewURLTest(name string, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
urlTest := &URLTest{
Base: outbound.NewBase(name, "", C.URLTest, false),
single: singledo.NewSingle(defaultGetProxiesDuration),
fastSingle: singledo.NewSingle(time.Second * 10),
providers: providers,
}
for _, option := range options {
option(urlTest)
}
return urlTest
}

View File

@ -1,70 +0,0 @@
package provider
import (
"context"
"time"
C "github.com/Dreamacro/clash/constant"
)
const (
defaultURLTestTimeout = time.Second * 5
)
type HealthCheckOption struct {
URL string
Interval uint
}
type HealthCheck struct {
url string
proxies []C.Proxy
interval uint
done chan struct{}
}
func (hc *HealthCheck) process() {
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
go hc.check()
for {
select {
case <-ticker.C:
hc.check()
case <-hc.done:
ticker.Stop()
return
}
}
}
func (hc *HealthCheck) setProxy(proxies []C.Proxy) {
hc.proxies = proxies
}
func (hc *HealthCheck) auto() bool {
return hc.interval != 0
}
func (hc *HealthCheck) check() {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
for _, proxy := range hc.proxies {
go proxy.URLTest(ctx, hc.url)
}
<-ctx.Done()
cancel()
}
func (hc *HealthCheck) close() {
hc.done <- struct{}{}
}
func NewHealthCheck(proxies []C.Proxy, url string, interval uint) *HealthCheck {
return &HealthCheck{
proxies: proxies,
url: url,
interval: interval,
done: make(chan struct{}, 1),
}
}

105
common/batch/batch.go Normal file
View File

@ -0,0 +1,105 @@
package batch
import (
"context"
"sync"
)
type Option = func(b *Batch)
type Result struct {
Value any
Err error
}
type Error struct {
Key string
Err error
}
func WithConcurrencyNum(n int) Option {
return func(b *Batch) {
q := make(chan struct{}, n)
for i := 0; i < n; i++ {
q <- struct{}{}
}
b.queue = q
}
}
// Batch similar to errgroup, but can control the maximum number of concurrent
type Batch struct {
result map[string]Result
queue chan struct{}
wg sync.WaitGroup
mux sync.Mutex
err *Error
once sync.Once
cancel func()
}
func (b *Batch) Go(key string, fn func() (any, error)) {
b.wg.Add(1)
go func() {
defer b.wg.Done()
if b.queue != nil {
<-b.queue
defer func() {
b.queue <- struct{}{}
}()
}
value, err := fn()
if err != nil {
b.once.Do(func() {
b.err = &Error{key, err}
if b.cancel != nil {
b.cancel()
}
})
}
ret := Result{value, err}
b.mux.Lock()
defer b.mux.Unlock()
b.result[key] = ret
}()
}
func (b *Batch) Wait() *Error {
b.wg.Wait()
if b.cancel != nil {
b.cancel()
}
return b.err
}
func (b *Batch) WaitAndGetResult() (map[string]Result, *Error) {
err := b.Wait()
return b.Result(), err
}
func (b *Batch) Result() map[string]Result {
b.mux.Lock()
defer b.mux.Unlock()
copy := map[string]Result{}
for k, v := range b.result {
copy[k] = v
}
return copy
}
func New(ctx context.Context, opts ...Option) (*Batch, context.Context) {
ctx, cancel := context.WithCancel(ctx)
b := &Batch{
result: map[string]Result{},
}
for _, o := range opts {
o(b)
}
b.cancel = cancel
return b, ctx
}

View File

@ -0,0 +1,83 @@
package batch
import (
"context"
"errors"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestBatch(t *testing.T) {
b, _ := New(context.Background())
now := time.Now()
b.Go("foo", func() (any, error) {
time.Sleep(time.Millisecond * 100)
return "foo", nil
})
b.Go("bar", func() (any, error) {
time.Sleep(time.Millisecond * 150)
return "bar", nil
})
result, err := b.WaitAndGetResult()
assert.Nil(t, err)
duration := time.Since(now)
assert.Less(t, duration, time.Millisecond*200)
assert.Equal(t, 2, len(result))
for k, v := range result {
assert.NoError(t, v.Err)
assert.Equal(t, k, v.Value.(string))
}
}
func TestBatchWithConcurrencyNum(t *testing.T) {
b, _ := New(
context.Background(),
WithConcurrencyNum(3),
)
now := time.Now()
for i := 0; i < 7; i++ {
idx := i
b.Go(strconv.Itoa(idx), func() (any, error) {
time.Sleep(time.Millisecond * 100)
return strconv.Itoa(idx), nil
})
}
result, _ := b.WaitAndGetResult()
duration := time.Since(now)
assert.Greater(t, duration, time.Millisecond*260)
assert.Equal(t, 7, len(result))
for k, v := range result {
assert.NoError(t, v.Err)
assert.Equal(t, k, v.Value.(string))
}
}
func TestBatchContext(t *testing.T) {
b, ctx := New(context.Background())
b.Go("error", func() (any, error) {
time.Sleep(time.Millisecond * 100)
return nil, errors.New("test error")
})
b.Go("ctx", func() (any, error) {
<-ctx.Done()
return nil, ctx.Err()
})
result, err := b.WaitAndGetResult()
assert.NotNil(t, err)
assert.Equal(t, "error", err.Key)
assert.Equal(t, ctx.Err(), result["ctx"].Err)
}

10
common/cache/cache.go vendored
View File

@ -18,11 +18,11 @@ type cache struct {
type element struct { type element struct {
Expired time.Time Expired time.Time
Payload interface{} Payload any
} }
// Put element in Cache with its ttl // Put element in Cache with its ttl
func (c *cache) Put(key interface{}, payload interface{}, ttl time.Duration) { func (c *cache) Put(key any, payload any, ttl time.Duration) {
c.mapping.Store(key, &element{ c.mapping.Store(key, &element{
Payload: payload, Payload: payload,
Expired: time.Now().Add(ttl), Expired: time.Now().Add(ttl),
@ -30,7 +30,7 @@ func (c *cache) Put(key interface{}, payload interface{}, ttl time.Duration) {
} }
// Get element in Cache, and drop when it expired // Get element in Cache, and drop when it expired
func (c *cache) Get(key interface{}) interface{} { func (c *cache) Get(key any) any {
item, exist := c.mapping.Load(key) item, exist := c.mapping.Load(key)
if !exist { if !exist {
return nil return nil
@ -45,7 +45,7 @@ func (c *cache) Get(key interface{}) interface{} {
} }
// GetWithExpire element in Cache with Expire Time // GetWithExpire element in Cache with Expire Time
func (c *cache) GetWithExpire(key interface{}) (payload interface{}, expired time.Time) { func (c *cache) GetWithExpire(key any) (payload any, expired time.Time) {
item, exist := c.mapping.Load(key) item, exist := c.mapping.Load(key)
if !exist { if !exist {
return return
@ -60,7 +60,7 @@ func (c *cache) GetWithExpire(key interface{}) (payload interface{}, expired tim
} }
func (c *cache) cleanup() { func (c *cache) cleanup() {
c.mapping.Range(func(k, v interface{}) bool { c.mapping.Range(func(k, v any) bool {
key := k.(string) key := k.(string)
elm := v.(*element) elm := v.(*element)
if time.Since(elm.Expired) > 0 { if time.Since(elm.Expired) > 0 {

View File

@ -12,7 +12,7 @@ import (
type Option func(*LruCache) type Option func(*LruCache)
// EvictCallback is used to get a callback when a cache entry is evicted // EvictCallback is used to get a callback when a cache entry is evicted
type EvictCallback = func(key interface{}, value interface{}) type EvictCallback = func(key any, value any)
// WithEvict set the evict callback // WithEvict set the evict callback
func WithEvict(cb EvictCallback) Option { func WithEvict(cb EvictCallback) Option {
@ -57,7 +57,7 @@ type LruCache struct {
maxAge int64 maxAge int64
maxSize int maxSize int
mu sync.Mutex mu sync.Mutex
cache map[interface{}]*list.Element cache map[any]*list.Element
lru *list.List // Front is least-recent lru *list.List // Front is least-recent
updateAgeOnGet bool updateAgeOnGet bool
staleReturn bool staleReturn bool
@ -68,7 +68,7 @@ type LruCache struct {
func NewLRUCache(options ...Option) *LruCache { func NewLRUCache(options ...Option) *LruCache {
lc := &LruCache{ lc := &LruCache{
lru: list.New(), lru: list.New(),
cache: make(map[interface{}]*list.Element), cache: make(map[any]*list.Element),
} }
for _, option := range options { for _, option := range options {
@ -78,9 +78,9 @@ func NewLRUCache(options ...Option) *LruCache {
return lc return lc
} }
// Get returns the interface{} representation of a cached response and a bool // Get returns the any representation of a cached response and a bool
// set to true if the key was found. // set to true if the key was found.
func (c *LruCache) Get(key interface{}) (interface{}, bool) { func (c *LruCache) Get(key any) (any, bool) {
entry := c.get(key) entry := c.get(key)
if entry == nil { if entry == nil {
return nil, false return nil, false
@ -90,11 +90,11 @@ func (c *LruCache) Get(key interface{}) (interface{}, bool) {
return value, true return value, true
} }
// GetWithExpire returns the interface{} representation of a cached response, // GetWithExpire returns the any representation of a cached response,
// a time.Time Give expected expires, // a time.Time Give expected expires,
// and a bool set to true if the key was found. // and a bool set to true if the key was found.
// This method will NOT check the maxAge of element and will NOT update the expires. // This method will NOT check the maxAge of element and will NOT update the expires.
func (c *LruCache) GetWithExpire(key interface{}) (interface{}, time.Time, bool) { func (c *LruCache) GetWithExpire(key any) (any, time.Time, bool) {
entry := c.get(key) entry := c.get(key)
if entry == nil { if entry == nil {
return nil, time.Time{}, false return nil, time.Time{}, false
@ -104,7 +104,7 @@ func (c *LruCache) GetWithExpire(key interface{}) (interface{}, time.Time, bool)
} }
// Exist returns if key exist in cache but not put item to the head of linked list // Exist returns if key exist in cache but not put item to the head of linked list
func (c *LruCache) Exist(key interface{}) bool { func (c *LruCache) Exist(key any) bool {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
@ -112,8 +112,8 @@ func (c *LruCache) Exist(key interface{}) bool {
return ok return ok
} }
// Set stores the interface{} representation of a response for a given key. // Set stores the any representation of a response for a given key.
func (c *LruCache) Set(key interface{}, value interface{}) { func (c *LruCache) Set(key any, value any) {
expires := int64(0) expires := int64(0)
if c.maxAge > 0 { if c.maxAge > 0 {
expires = time.Now().Unix() + c.maxAge expires = time.Now().Unix() + c.maxAge
@ -121,9 +121,9 @@ 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 any 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 any, value any, expires time.Time) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
@ -155,7 +155,7 @@ func (c *LruCache) CloneTo(n *LruCache) {
defer n.mu.Unlock() defer n.mu.Unlock()
n.lru = list.New() n.lru = list.New()
n.cache = make(map[interface{}]*list.Element) n.cache = make(map[any]*list.Element)
for e := c.lru.Front(); e != nil; e = e.Next() { for e := c.lru.Front(); e != nil; e = e.Next() {
elm := e.Value.(*entry) elm := e.Value.(*entry)
@ -163,7 +163,7 @@ func (c *LruCache) CloneTo(n *LruCache) {
} }
} }
func (c *LruCache) get(key interface{}) *entry { func (c *LruCache) get(key any) *entry {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
@ -188,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 interface{}) { func (c *LruCache) Delete(key any) {
c.mu.Lock() c.mu.Lock()
if le, ok := c.cache[key]; ok { if le, ok := c.cache[key]; ok {
@ -217,7 +217,7 @@ func (c *LruCache) deleteElement(le *list.Element) {
} }
type entry struct { type entry struct {
key interface{} key any
value interface{} value any
expires int64 expires int64
} }

View File

@ -126,7 +126,7 @@ func TestExist(t *testing.T) {
func TestEvict(t *testing.T) { func TestEvict(t *testing.T) {
temp := 0 temp := 0
evict := func(key interface{}, value interface{}) { evict := func(key any, value any) {
temp = key.(int) + value.(int) temp = key.(int) + value.(int)
} }
@ -149,7 +149,6 @@ func TestSetWithExpire(t *testing.T) {
assert.Equal(t, nil, res) assert.Equal(t, nil, res)
assert.Equal(t, time.Time{}, expires) assert.Equal(t, time.Time{}, expires)
assert.Equal(t, false, exist) assert.Equal(t, false, exist)
} }
func TestStale(t *testing.T) { func TestStale(t *testing.T) {

View File

@ -67,7 +67,6 @@ func (d *digest32) bmix(p []byte) (tail []byte) {
} }
func (d *digest32) Sum32() (h1 uint32) { func (d *digest32) Sum32() (h1 uint32) {
h1 = d.h1 h1 = d.h1
var k1 uint32 var k1 uint32

View File

@ -1,4 +1,4 @@
package mixed package net
import ( import (
"bufio" "bufio"
@ -11,6 +11,9 @@ type BufferedConn struct {
} }
func NewBufferedConn(c net.Conn) *BufferedConn { func NewBufferedConn(c net.Conn) *BufferedConn {
if bc, ok := c.(*BufferedConn); ok {
return bc
}
return &BufferedConn{bufio.NewReader(c), c} return &BufferedConn{bufio.NewReader(c), c}
} }

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
}

24
common/net/relay.go Normal file
View File

@ -0,0 +1,24 @@
package net
import (
"io"
"net"
"time"
)
// Relay copies between left and right bidirectionally.
func Relay(leftConn, rightConn net.Conn) {
ch := make(chan error)
go func() {
// Wrapping to avoid using *net.TCPConn.(ReadFrom)
// See also https://github.com/Dreamacro/clash/pull/1209
_, err := io.Copy(WriteOnlyWriter{Writer: leftConn}, ReadOnlyReader{Reader: rightConn})
leftConn.SetReadDeadline(time.Now())
ch <- err
}()
io.Copy(WriteOnlyWriter{Writer: rightConn}, ReadOnlyReader{Reader: leftConn})
rightConn.SetReadDeadline(time.Now())
<-ch
}

View File

@ -1,3 +1,3 @@
package observable package observable
type Iterable <-chan interface{} type Iterable <-chan any

View File

@ -2,15 +2,15 @@ package observable
import ( import (
"sync" "sync"
"sync/atomic"
"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 []any) chan any {
ch := make(chan interface{}) ch := make(chan any)
go func() { go func() {
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
for _, elm := range item { for _, elm := range item {
@ -22,7 +22,7 @@ func iterator(item []interface{}) chan interface{} {
} }
func TestObservable(t *testing.T) { func TestObservable(t *testing.T) {
iter := iterator([]interface{}{1, 2, 3, 4, 5}) iter := iterator([]any{1, 2, 3, 4, 5})
src := NewObservable(iter) src := NewObservable(iter)
data, err := src.Subscribe() data, err := src.Subscribe()
assert.Nil(t, err) assert.Nil(t, err)
@ -33,29 +33,29 @@ 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([]any{1, 2, 3, 4, 5})
src := NewObservable(iter) src := NewObservable(iter)
ch1, _ := src.Subscribe() ch1, _ := src.Subscribe()
ch2, _ := src.Subscribe() ch2, _ := src.Subscribe()
var count int32 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 any) {
for range ch { for range ch {
atomic.AddInt32(&count, 1) 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, int32(10), count) assert.Equal(t, int32(10), count.Load())
} }
func TestObservable_UnSubscribe(t *testing.T) { func TestObservable_UnSubscribe(t *testing.T) {
iter := iterator([]interface{}{1, 2, 3, 4, 5}) iter := iterator([]any{1, 2, 3, 4, 5})
src := NewObservable(iter) src := NewObservable(iter)
data, err := src.Subscribe() data, err := src.Subscribe()
assert.Nil(t, err) assert.Nil(t, err)
@ -65,7 +65,7 @@ func TestObservable_UnSubscribe(t *testing.T) {
} }
func TestObservable_SubscribeClosedSource(t *testing.T) { func TestObservable_SubscribeClosedSource(t *testing.T) {
iter := iterator([]interface{}{1}) iter := iterator([]any{1})
src := NewObservable(iter) src := NewObservable(iter)
data, _ := src.Subscribe() data, _ := src.Subscribe()
<-data <-data
@ -75,14 +75,14 @@ func TestObservable_SubscribeClosedSource(t *testing.T) {
} }
func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) { func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {
sub := Subscription(make(chan interface{})) sub := Subscription(make(chan any))
iter := iterator([]interface{}{1}) iter := iterator([]any{1})
src := NewObservable(iter) src := NewObservable(iter)
src.UnSubscribe(sub) src.UnSubscribe(sub)
} }
func TestObservable_SubscribeGoroutineLeak(t *testing.T) { func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
iter := iterator([]interface{}{1, 2, 3, 4, 5}) iter := iterator([]any{1, 2, 3, 4, 5})
src := NewObservable(iter) src := NewObservable(iter)
max := 100 max := 100
@ -94,7 +94,7 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(max) wg.Add(max)
waitCh := func(ch <-chan interface{}) { waitCh := func(ch <-chan any) {
for range ch { for range ch {
} }
wg.Done() wg.Done()
@ -113,3 +113,34 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
_, more := <-list[0] _, more := <-list[0]
assert.False(t, more) assert.False(t, more)
} }
func Benchmark_Observable_1000(b *testing.B) {
ch := make(chan any)
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 any
type Subscriber struct { type Subscriber struct {
buffer *channels.InfiniteChannel buffer chan any
once sync.Once once sync.Once
} }
func (s *Subscriber) Emit(item interface{}) { func (s *Subscriber) Emit(item any) {
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 any, 200),
} }
return sub return sub
} }

View File

@ -17,7 +17,7 @@ type Picker struct {
once sync.Once once sync.Once
errOnce sync.Once errOnce sync.Once
result interface{} result any
err error err error
} }
@ -43,7 +43,7 @@ func WithTimeout(ctx context.Context, timeout time.Duration) (*Picker, context.C
// Wait blocks until all function calls from the Go method have returned, // Wait blocks until all function calls from the Go method have returned,
// then returns the first nil error result (if any) from them. // then returns the first nil error result (if any) from them.
func (p *Picker) Wait() interface{} { func (p *Picker) Wait() any {
p.wg.Wait() p.wg.Wait()
if p.cancel != nil { if p.cancel != nil {
p.cancel() p.cancel()
@ -58,7 +58,7 @@ func (p *Picker) Error() error {
// Go calls the given function in a new goroutine. // Go calls the given function in a new goroutine.
// The first call to return a nil error cancels the group; its result will be returned by Wait. // The first call to return a nil error cancels the group; its result will be returned by Wait.
func (p *Picker) Go(f func() (interface{}, error)) { func (p *Picker) Go(f func() (any, error)) {
p.wg.Add(1) p.wg.Add(1)
go func() { go func() {

View File

@ -8,8 +8,8 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func sleepAndSend(ctx context.Context, delay int, input interface{}) func() (interface{}, error) { func sleepAndSend(ctx context.Context, delay int, input any) func() (any, error) {
return func() (interface{}, error) { return func() (any, error) {
timer := time.NewTimer(time.Millisecond * time.Duration(delay)) timer := time.NewTimer(time.Millisecond * time.Duration(delay))
select { select {
case <-timer.C: case <-timer.C:

View File

@ -8,11 +8,7 @@ import (
"sync" "sync"
) )
var defaultAllocator *Allocator var defaultAllocator = NewAllocator()
func init() {
defaultAllocator = NewAllocator()
}
// Allocator for incoming frames, optimized to prevent overwriting after zeroing // Allocator for incoming frames, optimized to prevent overwriting after zeroing
type Allocator struct { type Allocator struct {
@ -27,7 +23,7 @@ func NewAllocator() *Allocator {
alloc.buffers = make([]sync.Pool, 17) // 1B -> 64K alloc.buffers = make([]sync.Pool, 17) // 1B -> 64K
for k := range alloc.buffers { for k := range alloc.buffers {
i := k i := k
alloc.buffers[k].New = func() interface{} { alloc.buffers[k].New = func() any {
return make([]byte, 1<<uint32(i)) return make([]byte, 1<<uint32(i))
} }
} }
@ -56,12 +52,13 @@ func (alloc *Allocator) Put(buf []byte) error {
return errors.New("allocator Put() incorrect buffer size") return errors.New("allocator Put() incorrect buffer size")
} }
//nolint
//lint:ignore SA6002 ignore temporarily //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)
} }

17
common/pool/buffer.go Normal file
View File

@ -0,0 +1,17 @@
package pool
import (
"bytes"
"sync"
)
var bufferPool = sync.Pool{New: func() any { return &bytes.Buffer{} }}
func GetBuffer() *bytes.Buffer {
return bufferPool.Get().(*bytes.Buffer)
}
func PutBuffer(buf *bytes.Buffer) {
buf.Reset()
bufferPool.Put(buf)
}

View File

@ -5,6 +5,11 @@ const (
// but the maximum packet size of vmess/shadowsocks is about 16 KiB // but the maximum packet size of vmess/shadowsocks is about 16 KiB
// so define a buffer of 20 KiB to reduce the memory of each TCP relay // so define a buffer of 20 KiB to reduce the memory of each TCP relay
RelayBufferSize = 20 * 1024 RelayBufferSize = 20 * 1024
// RelayBufferSize uses 20KiB, but due to the allocator it will actually
// request 32Kib. Most UDPs are smaller than the MTU, and the TUN's MTU
// set to 9000, so the UDP Buffer size set to 16Kib
UDPBufferSize = 16 * 1024
) )
func Get(size int) []byte { func Get(size int) []byte {

View File

@ -6,12 +6,12 @@ import (
// Queue is a simple concurrent safe queue // Queue is a simple concurrent safe queue
type Queue struct { type Queue struct {
items []interface{} items []any
lock sync.RWMutex lock sync.RWMutex
} }
// Put add the item to the queue. // Put add the item to the queue.
func (q *Queue) Put(items ...interface{}) { func (q *Queue) Put(items ...any) {
if len(items) == 0 { if len(items) == 0 {
return return
} }
@ -22,7 +22,7 @@ func (q *Queue) Put(items ...interface{}) {
} }
// Pop returns the head of items. // Pop returns the head of items.
func (q *Queue) Pop() interface{} { func (q *Queue) Pop() any {
if len(q.items) == 0 { if len(q.items) == 0 {
return nil return nil
} }
@ -35,7 +35,7 @@ func (q *Queue) Pop() interface{} {
} }
// Last returns the last of item. // Last returns the last of item.
func (q *Queue) Last() interface{} { func (q *Queue) Last() any {
if len(q.items) == 0 { if len(q.items) == 0 {
return nil return nil
} }
@ -47,8 +47,8 @@ func (q *Queue) Last() interface{} {
} }
// Copy get the copy of queue. // Copy get the copy of queue.
func (q *Queue) Copy() []interface{} { func (q *Queue) Copy() []any {
items := []interface{}{} items := []any{}
q.lock.RLock() q.lock.RLock()
items = append(items, q.items...) items = append(items, q.items...)
q.lock.RUnlock() q.lock.RUnlock()
@ -66,6 +66,6 @@ func (q *Queue) Len() int64 {
// New is a constructor for a new concurrent safe queue. // New is a constructor for a new concurrent safe queue.
func New(hint int64) *Queue { func New(hint int64) *Queue {
return &Queue{ return &Queue{
items: make([]interface{}, 0, hint), items: make([]any, 0, hint),
} }
} }

View File

@ -7,7 +7,7 @@ import (
type call struct { type call struct {
wg sync.WaitGroup wg sync.WaitGroup
val interface{} val any
err error err error
} }
@ -20,13 +20,12 @@ type Single struct {
} }
type Result struct { type Result struct {
Val interface{} Val any
Err error Err error
} }
// Do single.Do likes sync.singleFilght // Do single.Do likes sync.singleFlight
//lint:ignore ST1008 it likes sync.singleFilght func (s *Single) Do(fn func() (any, error)) (v any, 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()
if now.Before(s.last.Add(s.wait)) { if now.Before(s.last.Add(s.wait)) {

View File

@ -2,18 +2,18 @@ package singledo
import ( import (
"sync" "sync"
"sync/atomic"
"testing" "testing"
"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
var shardCount int32 = 0 shardCount := atomic.NewInt32(0)
call := func() (interface{}, error) { call := func() (any, error) {
foo++ foo++
time.Sleep(time.Millisecond * 5) time.Sleep(time.Millisecond * 5)
return nil, nil return nil, nil
@ -26,7 +26,7 @@ func TestBasic(t *testing.T) {
go func() { go func() {
_, _, shard := single.Do(call) _, _, shard := single.Do(call)
if shard { if shard {
atomic.AddInt32(&shardCount, 1) shardCount.Inc()
} }
wg.Done() wg.Done()
}() }()
@ -34,13 +34,13 @@ func TestBasic(t *testing.T) {
wg.Wait() wg.Wait()
assert.Equal(t, 1, foo) assert.Equal(t, 1, foo)
assert.Equal(t, int32(4), shardCount) assert.Equal(t, int32(4), shardCount.Load())
} }
func TestTimer(t *testing.T) { func TestTimer(t *testing.T) {
single := NewSingle(time.Millisecond * 30) single := NewSingle(time.Millisecond * 30)
foo := 0 foo := 0
call := func() (interface{}, error) { call := func() (any, error) {
foo++ foo++
return nil, nil return nil, nil
} }
@ -56,7 +56,7 @@ func TestTimer(t *testing.T) {
func TestReset(t *testing.T) { func TestReset(t *testing.T) {
single := NewSingle(time.Millisecond * 30) single := NewSingle(time.Millisecond * 30)
foo := 0 foo := 0
call := func() (interface{}, error) { call := func() (any, error) {
foo++ foo++
return nil, nil return nil, nil
} }

View File

@ -1,4 +1,4 @@
// +build !linux //go:build !linux
package sockopt package sockopt

View File

@ -28,8 +28,8 @@ func NewDecoder(option Option) *Decoder {
return &Decoder{option: &option} return &Decoder{option: &option}
} }
// Decode transform a map[string]interface{} to a struct // Decode transform a map[string]any to a struct
func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error { func (d *Decoder) Decode(src map[string]any, dst any) error {
if reflect.TypeOf(dst).Kind() != reflect.Ptr { if reflect.TypeOf(dst).Kind() != reflect.Ptr {
return fmt.Errorf("Decode must recive a ptr struct") return fmt.Errorf("Decode must recive a ptr struct")
} }
@ -37,14 +37,16 @@ func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
v := reflect.ValueOf(dst).Elem() v := reflect.ValueOf(dst).Elem()
for idx := 0; idx < v.NumField(); idx++ { for idx := 0; idx < v.NumField(); idx++ {
field := t.Field(idx) field := t.Field(idx)
if field.Anonymous {
if err := d.decodeStruct(field.Name, src, v.Field(idx)); err != nil {
return err
}
continue
}
tag := field.Tag.Get(d.option.TagName) tag := field.Tag.Get(d.option.TagName)
str := strings.SplitN(tag, ",", 2) key, omitKey, found := strings.Cut(tag, ",")
key := str[0] omitempty := found && omitKey == "omitempty"
omitempty := false
if len(str) > 1 {
omitempty = str[1] == "omitempty"
}
value, ok := src[key] value, ok := src[key]
if !ok || value == nil { if !ok || value == nil {
@ -62,7 +64,7 @@ func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
return nil return nil
} }
func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error { func (d *Decoder) decode(name string, data any, val reflect.Value) error {
switch val.Kind() { switch val.Kind() {
case reflect.Int: case reflect.Int:
return d.decodeInt(name, data, val) return d.decodeInt(name, data, val)
@ -83,12 +85,14 @@ func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error
} }
} }
func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) (err error) { func (d *Decoder) decodeInt(name string, data any, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data) dataVal := reflect.ValueOf(data)
kind := dataVal.Kind() kind := dataVal.Kind()
switch { switch {
case kind == reflect.Int: case kind == reflect.Int:
val.SetInt(dataVal.Int()) val.SetInt(dataVal.Int())
case kind == reflect.Float64 && d.option.WeaklyTypedInput:
val.SetInt(int64(dataVal.Float()))
case kind == reflect.String && d.option.WeaklyTypedInput: case kind == reflect.String && d.option.WeaklyTypedInput:
var i int64 var i int64
i, err = strconv.ParseInt(dataVal.String(), 0, val.Type().Bits()) i, err = strconv.ParseInt(dataVal.String(), 0, val.Type().Bits())
@ -106,7 +110,7 @@ func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) (e
return err return err
} }
func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) (err error) { func (d *Decoder) decodeString(name string, data any, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data) dataVal := reflect.ValueOf(data)
kind := dataVal.Kind() kind := dataVal.Kind()
switch { switch {
@ -123,7 +127,7 @@ func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value)
return err return err
} }
func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) (err error) { func (d *Decoder) decodeBool(name string, data any, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data) dataVal := reflect.ValueOf(data)
kind := dataVal.Kind() kind := dataVal.Kind()
switch { switch {
@ -140,7 +144,7 @@ func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) (
return err return err
} }
func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) error { func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error {
dataVal := reflect.Indirect(reflect.ValueOf(data)) dataVal := reflect.Indirect(reflect.ValueOf(data))
valType := val.Type() valType := val.Type()
valElemType := valType.Elem() valElemType := valType.Elem()
@ -155,9 +159,19 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value)
for valSlice.Len() <= i { for valSlice.Len() <= i {
valSlice = reflect.Append(valSlice, reflect.Zero(valElemType)) valSlice = reflect.Append(valSlice, reflect.Zero(valElemType))
} }
currentField := valSlice.Index(i)
fieldName := fmt.Sprintf("%s[%d]", name, i) fieldName := fmt.Sprintf("%s[%d]", name, i)
if currentData == nil {
// in weakly type mode, null will convert to zero value
if d.option.WeaklyTypedInput {
continue
}
// in non-weakly type mode, null will convert to nil if element's zero value is nil, otherwise return an error
if elemKind := valElemType.Kind(); elemKind == reflect.Map || elemKind == reflect.Slice {
continue
}
return fmt.Errorf("'%s' can not be null", fieldName)
}
currentField := valSlice.Index(i)
if err := d.decode(fieldName, currentData, currentField); err != nil { if err := d.decode(fieldName, currentData, currentField); err != nil {
return err return err
} }
@ -167,7 +181,7 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value)
return nil return nil
} }
func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) error { func (d *Decoder) decodeMap(name string, data any, val reflect.Value) error {
valType := val.Type() valType := val.Type()
valKeyType := valType.Key() valKeyType := valType.Key()
valElemType := valType.Elem() valElemType := valType.Elem()
@ -239,7 +253,7 @@ func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val refle
return nil return nil
} }
func (d *Decoder) decodeStruct(name string, data interface{}, val reflect.Value) error { func (d *Decoder) decodeStruct(name string, data any, val reflect.Value) error {
dataVal := reflect.Indirect(reflect.ValueOf(data)) dataVal := reflect.Indirect(reflect.ValueOf(data))
// If the type of the value to write to and the data match directly, // If the type of the value to write to and the data match directly,
@ -267,7 +281,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
} }
dataValKeys := make(map[reflect.Value]struct{}) dataValKeys := make(map[reflect.Value]struct{})
dataValKeysUnused := make(map[interface{}]struct{}) dataValKeysUnused := make(map[any]struct{})
for _, dataValKey := range dataVal.MapKeys() { for _, dataValKey := range dataVal.MapKeys() {
dataValKeys[dataValKey] = struct{}{} dataValKeys[dataValKey] = struct{}{}
dataValKeysUnused[dataValKey.Interface()] = struct{}{} dataValKeysUnused[dataValKey.Interface()] = struct{}{}
@ -392,7 +406,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
return nil return nil
} }
func (d *Decoder) setInterface(name string, data interface{}, val reflect.Value) (err error) { func (d *Decoder) setInterface(name string, data any, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data) dataVal := reflect.ValueOf(data)
val.Set(dataVal) val.Set(dataVal)
return nil return nil

View File

@ -1,12 +1,15 @@
package structure package structure
import ( import (
"reflect"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
var decoder = NewDecoder(Option{TagName: "test"}) var (
var weakTypeDecoder = NewDecoder(Option{TagName: "test", WeaklyTypedInput: true}) decoder = NewDecoder(Option{TagName: "test"})
weakTypeDecoder = NewDecoder(Option{TagName: "test", WeaklyTypedInput: true})
)
type Baz struct { type Baz struct {
Foo int `test:"foo"` Foo int `test:"foo"`
@ -24,7 +27,7 @@ type BazOptional struct {
} }
func TestStructure_Basic(t *testing.T) { func TestStructure_Basic(t *testing.T) {
rawMap := map[string]interface{}{ rawMap := map[string]any{
"foo": 1, "foo": 1,
"bar": "test", "bar": "test",
"extra": false, "extra": false,
@ -37,16 +40,12 @@ func TestStructure_Basic(t *testing.T) {
s := &Baz{} s := &Baz{}
err := decoder.Decode(rawMap, s) err := decoder.Decode(rawMap, s)
if err != nil { assert.Nil(t, err)
t.Fatal(err.Error()) assert.Equal(t, goal, s)
}
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s)
}
} }
func TestStructure_Slice(t *testing.T) { func TestStructure_Slice(t *testing.T) {
rawMap := map[string]interface{}{ rawMap := map[string]any{
"foo": 1, "foo": 1,
"bar": []string{"one", "two"}, "bar": []string{"one", "two"},
} }
@ -58,16 +57,12 @@ func TestStructure_Slice(t *testing.T) {
s := &BazSlice{} s := &BazSlice{}
err := decoder.Decode(rawMap, s) err := decoder.Decode(rawMap, s)
if err != nil { assert.Nil(t, err)
t.Fatal(err.Error()) assert.Equal(t, goal, s)
}
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s)
}
} }
func TestStructure_Optional(t *testing.T) { func TestStructure_Optional(t *testing.T) {
rawMap := map[string]interface{}{ rawMap := map[string]any{
"foo": 1, "foo": 1,
} }
@ -77,50 +72,40 @@ func TestStructure_Optional(t *testing.T) {
s := &BazOptional{} s := &BazOptional{}
err := decoder.Decode(rawMap, s) err := decoder.Decode(rawMap, s)
if err != nil { assert.Nil(t, err)
t.Fatal(err.Error()) assert.Equal(t, goal, s)
}
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s)
}
} }
func TestStructure_MissingKey(t *testing.T) { func TestStructure_MissingKey(t *testing.T) {
rawMap := map[string]interface{}{ rawMap := map[string]any{
"foo": 1, "foo": 1,
} }
s := &Baz{} s := &Baz{}
err := decoder.Decode(rawMap, s) err := decoder.Decode(rawMap, s)
if err == nil { assert.NotNilf(t, err, "should throw error: %#v", s)
t.Fatalf("should throw error: %#v", s)
}
} }
func TestStructure_ParamError(t *testing.T) { func TestStructure_ParamError(t *testing.T) {
rawMap := map[string]interface{}{} rawMap := map[string]any{}
s := Baz{} s := Baz{}
err := decoder.Decode(rawMap, s) err := decoder.Decode(rawMap, s)
if err == nil { assert.NotNilf(t, err, "should throw error: %#v", s)
t.Fatalf("should throw error: %#v", s)
}
} }
func TestStructure_SliceTypeError(t *testing.T) { func TestStructure_SliceTypeError(t *testing.T) {
rawMap := map[string]interface{}{ rawMap := map[string]any{
"foo": 1, "foo": 1,
"bar": []int{1, 2}, "bar": []int{1, 2},
} }
s := &BazSlice{} s := &BazSlice{}
err := decoder.Decode(rawMap, s) err := decoder.Decode(rawMap, s)
if err == nil { assert.NotNilf(t, err, "should throw error: %#v", s)
t.Fatalf("should throw error: %#v", s)
}
} }
func TestStructure_WeakType(t *testing.T) { func TestStructure_WeakType(t *testing.T) {
rawMap := map[string]interface{}{ rawMap := map[string]any{
"foo": "1", "foo": "1",
"bar": []int{1}, "bar": []int{1},
} }
@ -132,10 +117,65 @@ func TestStructure_WeakType(t *testing.T) {
s := &BazSlice{} s := &BazSlice{}
err := weakTypeDecoder.Decode(rawMap, s) err := weakTypeDecoder.Decode(rawMap, s)
if err != nil { assert.Nil(t, err)
t.Fatal(err.Error()) assert.Equal(t, goal, s)
} }
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s) func TestStructure_Nest(t *testing.T) {
} rawMap := map[string]any{
"foo": 1,
}
goal := BazOptional{
Foo: 1,
}
s := &struct {
BazOptional
}{}
err := decoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Equal(t, s.BazOptional, goal)
}
func TestStructure_SliceNilValue(t *testing.T) {
rawMap := map[string]any{
"foo": 1,
"bar": []any{"bar", nil},
}
goal := &BazSlice{
Foo: 1,
Bar: []string{"bar", ""},
}
s := &BazSlice{}
err := weakTypeDecoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Equal(t, goal.Bar, s.Bar)
s = &BazSlice{}
err = decoder.Decode(rawMap, s)
assert.NotNil(t, err)
}
func TestStructure_SliceNilValueComplex(t *testing.T) {
rawMap := map[string]any{
"bar": []any{map[string]any{"bar": "foo"}, nil},
}
s := &struct {
Bar []map[string]any `test:"bar"`
}{}
err := decoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Nil(t, s.Bar[1])
ss := &struct {
Bar []Baz `test:"bar"`
}{}
err = decoder.Decode(rawMap, ss)
assert.NotNil(t, err)
} }

View File

@ -36,7 +36,7 @@ func NewAuthenticator(users []AuthUser) Authenticator {
au.storage.Store(user.User, user.Pass) au.storage.Store(user.User, user.Pass)
} }
usernames := make([]string, 0, len(users)) usernames := make([]string, 0, len(users))
au.storage.Range(func(key, value interface{}) bool { au.storage.Range(func(key, value any) bool {
usernames = append(usernames, key.(string)) usernames = append(usernames, key.(string))
return true return true
}) })

18
component/dhcp/conn.go Normal file
View File

@ -0,0 +1,18 @@
package dhcp
import (
"context"
"net"
"runtime"
"github.com/Dreamacro/clash/component/dialer"
)
func ListenDHCPClient(ctx context.Context, ifaceName string) (net.PacketConn, error) {
listenAddr := "0.0.0.0:68"
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
listenAddr = "255.255.255.255:68"
}
return dialer.ListenPacket(ctx, "udp4", listenAddr, dialer.WithInterface(ifaceName), dialer.WithAddrReuse(true))
}

88
component/dhcp/dhcp.go Normal file
View File

@ -0,0 +1,88 @@
package dhcp
import (
"context"
"errors"
"net"
"github.com/Dreamacro/clash/component/iface"
"github.com/insomniacslk/dhcp/dhcpv4"
)
var (
ErrNotResponding = errors.New("DHCP not responding")
ErrNotFound = errors.New("DNS option not found")
)
func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]net.IP, error) {
conn, err := ListenDHCPClient(context, ifaceName)
if err != nil {
return nil, err
}
defer conn.Close()
result := make(chan []net.IP, 1)
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return nil, err
}
discovery, err := dhcpv4.NewDiscovery(ifaceObj.HardwareAddr, dhcpv4.WithBroadcast(true), dhcpv4.WithRequestedOptions(dhcpv4.OptionDomainNameServer))
if err != nil {
return nil, err
}
go receiveOffer(conn, discovery.TransactionID, result)
_, err = conn.WriteTo(discovery.ToBytes(), &net.UDPAddr{IP: net.IPv4bcast, Port: 67})
if err != nil {
return nil, err
}
select {
case r, ok := <-result:
if !ok {
return nil, ErrNotFound
}
return r, nil
case <-context.Done():
return nil, ErrNotResponding
}
}
func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []net.IP) {
defer close(result)
buf := make([]byte, dhcpv4.MaxMessageSize)
for {
n, _, err := conn.ReadFrom(buf)
if err != nil {
return
}
pkt, err := dhcpv4.FromBytes(buf[:n])
if err != nil {
continue
}
if pkt.MessageType() != dhcpv4.MessageTypeOffer {
continue
}
if pkt.TransactionID != id {
continue
}
dns := pkt.DNS()
if len(dns) == 0 {
return
}
result <- dns
return
}
}

View File

@ -0,0 +1,66 @@
package dialer
import (
"net"
"syscall"
"github.com/Dreamacro/clash/component/iface"
"golang.org/x/sys/unix"
)
type controlFn = func(network, address string, c syscall.RawConn) error
func bindControl(ifaceIdx int, chain controlFn) controlFn {
return func(network, address string, c syscall.RawConn) (err error) {
defer func() {
if err == nil && chain != nil {
err = chain(network, address, c)
}
}()
ipStr, _, err := net.SplitHostPort(address)
if err == nil {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return
}
}
var innerErr error
err = c.Control(func(fd uintptr) {
switch network {
case "tcp4", "udp4":
innerErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, ifaceIdx)
case "tcp6", "udp6":
innerErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, ifaceIdx)
}
})
if innerErr != nil {
err = innerErr
}
return
}
}
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error {
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return err
}
dialer.Control = bindControl(ifaceObj.Index, dialer.Control)
return nil
}
func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) {
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return "", err
}
lc.Control = bindControl(ifaceObj.Index, lc.Control)
return address, nil
}

View File

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

View File

@ -0,0 +1,92 @@
//go:build !linux && !darwin
package dialer
import (
"net"
"strconv"
"strings"
"github.com/Dreamacro/clash/component/iface"
)
func lookupLocalAddr(ifaceName string, network string, destination net.IP, port int) (net.Addr, error) {
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return nil, err
}
var addr *net.IPNet
switch network {
case "udp4", "tcp4":
addr, err = ifaceObj.PickIPv4Addr(destination)
case "tcp6", "udp6":
addr, err = ifaceObj.PickIPv6Addr(destination)
default:
if destination != nil {
if destination.To4() != nil {
addr, err = ifaceObj.PickIPv4Addr(destination)
} else {
addr, err = ifaceObj.PickIPv6Addr(destination)
}
} else {
addr, err = ifaceObj.PickIPv4Addr(destination)
}
}
if err != nil {
return nil, err
}
if strings.HasPrefix(network, "tcp") {
return &net.TCPAddr{
IP: addr.IP,
Port: port,
}, nil
} else if strings.HasPrefix(network, "udp") {
return &net.UDPAddr{
IP: addr.IP,
Port: port,
}, nil
}
return nil, iface.ErrAddrNotFound
}
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination net.IP) error {
if !destination.IsGlobalUnicast() {
return nil
}
local := uint64(0)
if dialer.LocalAddr != nil {
_, port, err := net.SplitHostPort(dialer.LocalAddr.String())
if err == nil {
local, _ = strconv.ParseUint(port, 10, 16)
}
}
addr, err := lookupLocalAddr(ifaceName, network, destination, int(local))
if err != nil {
return err
}
dialer.LocalAddr = addr
return nil
}
func bindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, address string) (string, error) {
_, port, err := net.SplitHostPort(address)
if err != nil {
port = "0"
}
local, _ := strconv.ParseUint(port, 10, 16)
addr, err := lookupLocalAddr(ifaceName, network, nil, int(local))
if err != nil {
return "", err
}
return addr.String(), nil
}

View File

@ -8,33 +8,7 @@ import (
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
) )
func Dialer() (*net.Dialer, error) { func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) {
dialer := &net.Dialer{}
if DialerHook != nil {
if err := DialerHook(dialer); err != nil {
return nil, err
}
}
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) {
return DialContext(context.Background(), network, address)
}
func DialContext(ctx context.Context, network, address string) (net.Conn, error) {
switch network { switch network {
case "tcp4", "tcp6", "udp4", "udp6": case "tcp4", "tcp6", "udp4", "udp6":
host, port, err := net.SplitHostPort(address) host, port, err := net.SplitHostPort(address)
@ -42,11 +16,6 @@ func DialContext(ctx context.Context, network, address string) (net.Conn, error)
return nil, err return nil, err
} }
dialer, err := Dialer()
if err != nil {
return nil, err
}
var ip net.IP var ip net.IP
switch network { switch network {
case "tcp4", "udp4": case "tcp4", "udp4":
@ -54,41 +23,78 @@ func DialContext(ctx context.Context, network, address string) (net.Conn, error)
default: default:
ip, err = resolver.ResolveIPv6(host) ip, err = resolver.ResolveIPv6(host)
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
if DialHook != nil { return dialContext(ctx, network, ip, port, options)
if err := DialHook(dialer, network, ip); err != nil {
return nil, err
}
}
return dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
case "tcp", "udp": case "tcp", "udp":
return dualStackDialContext(ctx, network, address) return dualStackDialContext(ctx, network, address, options)
default: default:
return nil, errors.New("network invalid") return nil, errors.New("network invalid")
} }
} }
func ListenPacket(network, address string) (net.PacketConn, error) { func ListenPacket(ctx context.Context, network, address string, options ...Option) (net.PacketConn, error) {
lc, err := ListenConfig() cfg := &option{
if err != nil { interfaceName: DefaultInterface.Load(),
return nil, err routingMark: int(DefaultRoutingMark.Load()),
} }
if ListenPacketHook != nil && address == "" { for _, o := range DefaultOptions {
ip, err := ListenPacketHook() o(cfg)
}
for _, o := range options {
o(cfg)
}
lc := &net.ListenConfig{}
if cfg.interfaceName != "" {
addr, err := bindIfaceToListenConfig(cfg.interfaceName, lc, network, address)
if err != nil { if err != nil {
return nil, err return nil, err
} }
address = net.JoinHostPort(ip.String(), "0") address = addr
} }
return lc.ListenPacket(context.Background(), network, address) if cfg.addrReuse {
addrReuseToListenConfig(lc)
}
if cfg.routingMark != 0 {
bindMarkToListenConfig(cfg.routingMark, lc, network, address)
}
return lc.ListenPacket(ctx, network, address)
} }
func dualStackDialContext(ctx context.Context, network, address string) (net.Conn, error) { func dialContext(ctx context.Context, network string, destination net.IP, port string, options []Option) (net.Conn, error) {
opt := &option{
interfaceName: DefaultInterface.Load(),
routingMark: int(DefaultRoutingMark.Load()),
}
for _, o := range DefaultOptions {
o(opt)
}
for _, o := range options {
o(opt)
}
dialer := &net.Dialer{}
if opt.interfaceName != "" {
if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
return nil, err
}
}
if opt.routingMark != 0 {
bindMarkToDialer(opt.routingMark, dialer, network, destination)
}
return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port))
}
func dualStackDialContext(ctx context.Context, network, address string, options []Option) (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
@ -119,12 +125,6 @@ func dualStackDialContext(ctx context.Context, network, address string) (net.Con
} }
}() }()
dialer, err := Dialer()
if err != nil {
result.error = err
return
}
var ip net.IP var ip net.IP
if ipv6 { if ipv6 {
ip, result.error = resolver.ResolveIPv6(host) ip, result.error = resolver.ResolveIPv6(host)
@ -136,12 +136,7 @@ func dualStackDialContext(ctx context.Context, network, address string) (net.Con
} }
result.resolved = true result.resolved = true
if DialHook != nil { result.Conn, result.error = dialContext(ctx, network, ip, port, options)
if result.error = DialHook(dialer, network, ip); result.error != nil {
return
}
}
result.Conn, result.error = dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
} }
go startRacer(ctx, network+"4", host, false) go startRacer(ctx, network+"4", host, false)

View File

@ -1,148 +0,0 @@
package dialer
import (
"errors"
"net"
"time"
"github.com/Dreamacro/clash/common/singledo"
)
type DialerHookFunc = func(dialer *net.Dialer) error
type DialHookFunc = func(dialer *net.Dialer, network string, ip net.IP) error
type ListenConfigHookFunc = func(*net.ListenConfig) error
type ListenPacketHookFunc = func() (net.IP, error)
var (
DialerHook DialerHookFunc
DialHook DialHookFunc
ListenConfigHook ListenConfigHookFunc
ListenPacketHook ListenPacketHookFunc
)
var (
ErrAddrNotFound = errors.New("addr not found")
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 {
single := singledo.NewSingle(5 * time.Second)
return func() (net.IP, error) {
elm, err, _ := single.Do(func() (interface{}, error) {
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)
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 {
single := singledo.NewSingle(5 * time.Second)
return func(dialer *net.Dialer, network string, ip net.IP) error {
elm, err, _ := single.Do(func() (interface{}, error) {
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 err
}
addrs := elm.([]net.Addr)
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

@ -0,0 +1,43 @@
//go:build linux
package dialer
import (
"net"
"syscall"
)
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) {
dialer.Control = bindMarkToControl(mark, dialer.Control)
}
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) {
lc.Control = bindMarkToControl(mark, lc.Control)
}
func bindMarkToControl(mark int, chain controlFn) controlFn {
return func(network, address string, c syscall.RawConn) (err error) {
defer func() {
if err == nil && chain != nil {
err = chain(network, address, c)
}
}()
ipStr, _, err := net.SplitHostPort(address)
if err == nil {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return
}
}
var innerErr error
err = c.Control(func(fd uintptr) {
innerErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
})
if innerErr != nil {
err = innerErr
}
return
}
}

View File

@ -0,0 +1,26 @@
//go:build !linux
package dialer
import (
"net"
"sync"
"github.com/Dreamacro/clash/log"
)
var printMarkWarnOnce sync.Once
func printMarkWarn() {
printMarkWarnOnce.Do(func() {
log.Warnln("Routing mark on socket is not supported on current platform")
})
}
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) {
printMarkWarn()
}
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) {
printMarkWarn()
}

View File

@ -0,0 +1,35 @@
package dialer
import "go.uber.org/atomic"
var (
DefaultOptions []Option
DefaultInterface = atomic.NewString("")
DefaultRoutingMark = atomic.NewInt32(0)
)
type option struct {
interfaceName string
addrReuse bool
routingMark int
}
type Option func(opt *option)
func WithInterface(name string) Option {
return func(opt *option) {
opt.interfaceName = name
}
}
func WithAddrReuse(reuse bool) Option {
return func(opt *option) {
opt.addrReuse = reuse
}
}
func WithRoutingMark(mark int) Option {
return func(opt *option) {
opt.routingMark = mark
}
}

View File

@ -0,0 +1,9 @@
//go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows
package dialer
import (
"net"
)
func addrReuseToListenConfig(*net.ListenConfig) {}

View File

@ -0,0 +1,27 @@
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
package dialer
import (
"net"
"syscall"
"golang.org/x/sys/unix"
)
func addrReuseToListenConfig(lc *net.ListenConfig) {
chain := lc.Control
lc.Control = func(network, address string, c syscall.RawConn) (err error) {
defer func() {
if err == nil && chain != nil {
err = chain(network, address, c)
}
}()
return c.Control(func(fd uintptr) {
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
})
}
}

View File

@ -0,0 +1,24 @@
package dialer
import (
"net"
"syscall"
"golang.org/x/sys/windows"
)
func addrReuseToListenConfig(lc *net.ListenConfig) {
chain := lc.Control
lc.Control = func(network, address string, c syscall.RawConn) (err error) {
defer func() {
if err == nil && chain != nil {
err = chain(network, address, c)
}
}()
return c.Control(func(fd uintptr) {
windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1)
})
}
}

View File

@ -0,0 +1,55 @@
package fakeip
import (
"net"
"github.com/Dreamacro/clash/component/profile/cachefile"
)
type cachefileStore struct {
cache *cachefile.CacheFile
}
// GetByHost implements store.GetByHost
func (c *cachefileStore) GetByHost(host string) (net.IP, bool) {
elm := c.cache.GetFakeip([]byte(host))
if elm == nil {
return nil, false
}
return net.IP(elm), true
}
// PutByHost implements store.PutByHost
func (c *cachefileStore) PutByHost(host string, ip net.IP) {
c.cache.PutFakeip([]byte(host), ip)
}
// GetByIP implements store.GetByIP
func (c *cachefileStore) GetByIP(ip net.IP) (string, bool) {
elm := c.cache.GetFakeip(ip.To4())
if elm == nil {
return "", false
}
return string(elm), true
}
// PutByIP implements store.PutByIP
func (c *cachefileStore) PutByIP(ip net.IP, host string) {
c.cache.PutFakeip(ip.To4(), []byte(host))
}
// DelByIP implements store.DelByIP
func (c *cachefileStore) DelByIP(ip net.IP) {
ip = ip.To4()
c.cache.DelFakeipPair(ip, c.cache.GetFakeip(ip.To4()))
}
// Exist implements store.Exist
func (c *cachefileStore) Exist(ip net.IP) bool {
_, exist := c.GetByIP(ip)
return exist
}
// CloneTo implements store.CloneTo
// already persistence
func (c *cachefileStore) CloneTo(store store) {}

View File

@ -0,0 +1,69 @@
package fakeip
import (
"net"
"github.com/Dreamacro/clash/common/cache"
)
type memoryStore struct {
cache *cache.LruCache
}
// GetByHost implements store.GetByHost
func (m *memoryStore) GetByHost(host string) (net.IP, bool) {
if elm, exist := m.cache.Get(host); exist {
ip := elm.(net.IP)
// ensure ip --> host on head of linked list
m.cache.Get(ipToUint(ip.To4()))
return ip, true
}
return nil, false
}
// PutByHost implements store.PutByHost
func (m *memoryStore) PutByHost(host string, ip net.IP) {
m.cache.Set(host, ip)
}
// GetByIP implements store.GetByIP
func (m *memoryStore) GetByIP(ip net.IP) (string, bool) {
if elm, exist := m.cache.Get(ipToUint(ip.To4())); exist {
host := elm.(string)
// ensure host --> ip on head of linked list
m.cache.Get(host)
return host, true
}
return "", false
}
// PutByIP implements store.PutByIP
func (m *memoryStore) PutByIP(ip net.IP, host string) {
m.cache.Set(ipToUint(ip.To4()), host)
}
// DelByIP implements store.DelByIP
func (m *memoryStore) DelByIP(ip net.IP) {
ipNum := ipToUint(ip.To4())
if elm, exist := m.cache.Get(ipNum); exist {
m.cache.Delete(elm.(string))
}
m.cache.Delete(ipNum)
}
// Exist implements store.Exist
func (m *memoryStore) Exist(ip net.IP) bool {
return m.cache.Exist(ipToUint(ip.To4()))
}
// CloneTo implements store.CloneTo
// only for memoryStore to memoryStore
func (m *memoryStore) CloneTo(store store) {
if ms, ok := store.(*memoryStore); ok {
m.cache.CloneTo(ms.cache)
}
}

View File

@ -6,9 +6,20 @@ import (
"sync" "sync"
"github.com/Dreamacro/clash/common/cache" "github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/component/profile/cachefile"
"github.com/Dreamacro/clash/component/trie" "github.com/Dreamacro/clash/component/trie"
) )
type store interface {
GetByHost(host string) (net.IP, bool)
PutByHost(host string, ip net.IP)
GetByIP(ip net.IP) (string, bool)
PutByIP(ip net.IP, host string)
DelByIP(ip net.IP)
Exist(ip net.IP) bool
CloneTo(store)
}
// Pool is a implementation about fake ip generator without storage // Pool is a implementation about fake ip generator without storage
type Pool struct { type Pool struct {
max uint32 max uint32
@ -17,25 +28,20 @@ type Pool struct {
offset uint32 offset uint32
mux sync.Mutex mux sync.Mutex
host *trie.DomainTrie host *trie.DomainTrie
cache *cache.LruCache ipnet *net.IPNet
store store
} }
// Lookup return a fake ip with host // Lookup return a fake ip with host
func (p *Pool) Lookup(host string) net.IP { func (p *Pool) Lookup(host string) net.IP {
p.mux.Lock() p.mux.Lock()
defer p.mux.Unlock() defer p.mux.Unlock()
if elm, exist := p.cache.Get(host); exist { if ip, exist := p.store.GetByHost(host); exist {
ip := elm.(net.IP)
// ensure ip --> host on head of linked list
n := ipToUint(ip.To4())
offset := n - p.min + 1
p.cache.Get(offset)
return ip return ip
} }
ip := p.get(host) ip := p.get(host)
p.cache.Set(host, ip) p.store.PutByHost(host, ip)
return ip return ip
} }
@ -48,22 +54,11 @@ func (p *Pool) LookBack(ip net.IP) (string, bool) {
return "", false return "", false
} }
n := ipToUint(ip.To4()) return p.store.GetByIP(ip)
offset := n - p.min + 1
if elm, exist := p.cache.Get(offset); exist {
host := elm.(string)
// ensure host --> ip on head of linked list
p.cache.Get(host)
return host, true
}
return "", false
} }
// LookupHost return if domain in host // ShouldSkipped return if domain should be skipped
func (p *Pool) LookupHost(domain string) bool { func (p *Pool) ShouldSkipped(domain string) bool {
if p.host == nil { if p.host == nil {
return false return false
} }
@ -79,9 +74,7 @@ func (p *Pool) Exist(ip net.IP) bool {
return false return false
} }
n := ipToUint(ip.To4()) return p.store.Exist(ip)
offset := n - p.min + 1
return p.cache.Exist(offset)
} }
// Gateway return gateway ip // Gateway return gateway ip
@ -89,9 +82,14 @@ func (p *Pool) Gateway() net.IP {
return uintToIP(p.gateway) return uintToIP(p.gateway)
} }
// PatchFrom clone cache from old pool // IPNet return raw ipnet
func (p *Pool) PatchFrom(o *Pool) { func (p *Pool) IPNet() *net.IPNet {
o.cache.CloneTo(p.cache) return p.ipnet
}
// CloneFrom clone cache from old pool
func (p *Pool) CloneFrom(o *Pool) {
o.store.CloneTo(p.store)
} }
func (p *Pool) get(host string) net.IP { func (p *Pool) get(host string) net.IP {
@ -100,15 +98,19 @@ func (p *Pool) get(host string) net.IP {
p.offset = (p.offset + 1) % (p.max - p.min) p.offset = (p.offset + 1) % (p.max - p.min)
// Avoid infinite loops // Avoid infinite loops
if p.offset == current { if p.offset == current {
p.offset = (p.offset + 1) % (p.max - p.min)
ip := uintToIP(p.min + p.offset - 1)
p.store.DelByIP(ip)
break break
} }
if !p.cache.Exist(p.offset) { ip := uintToIP(p.min + p.offset - 1)
if !p.store.Exist(ip) {
break break
} }
} }
ip := uintToIP(p.min + p.offset - 1) ip := uintToIP(p.min + p.offset - 1)
p.cache.Set(p.offset, host) p.store.PutByIP(ip, host)
return ip return ip
} }
@ -124,11 +126,24 @@ func uintToIP(v uint32) net.IP {
return net.IP{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 type Options struct {
func New(ipnet *net.IPNet, size int, host *trie.DomainTrie) (*Pool, error) { IPNet *net.IPNet
min := ipToUint(ipnet.IP) + 2 Host *trie.DomainTrie
ones, bits := ipnet.Mask.Size() // Size sets the maximum number of entries in memory
// and does not work if Persistence is true
Size int
// Persistence will save the data to disk.
// Size will not work and record will be fully stored.
Persistence bool
}
// New return Pool instance
func New(options Options) (*Pool, error) {
min := ipToUint(options.IPNet.IP) + 2
ones, bits := options.IPNet.Mask.Size()
total := 1<<uint(bits-ones) - 2 total := 1<<uint(bits-ones) - 2
if total <= 0 { if total <= 0 {
@ -136,11 +151,22 @@ func New(ipnet *net.IPNet, size int, host *trie.DomainTrie) (*Pool, error) {
} }
max := min + uint32(total) - 1 max := min + uint32(total) - 1
return &Pool{ pool := &Pool{
min: min, min: min,
max: max, max: max,
gateway: min - 1, gateway: min - 1,
host: host, host: options.Host,
cache: cache.NewLRUCache(cache.WithSize(size * 2)), ipnet: options.IPNet,
}, nil }
if options.Persistence {
pool.store = &cachefileStore{
cache: cachefile.Cache(),
}
} else {
pool.store = &memoryStore{
cache: cache.NewLRUCache(cache.WithSize(options.Size * 2)),
}
}
return pool, nil
} }

View File

@ -1,39 +1,126 @@
package fakeip package fakeip
import ( import (
"fmt"
"net" "net"
"os"
"testing" "testing"
"time"
"github.com/Dreamacro/clash/component/profile/cachefile"
"github.com/Dreamacro/clash/component/trie"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"go.etcd.io/bbolt"
) )
func createPools(options Options) ([]*Pool, string, error) {
pool, err := New(options)
if err != nil {
return nil, "", err
}
filePool, tempfile, err := createCachefileStore(options)
if err != nil {
return nil, "", err
}
return []*Pool{pool, filePool}, tempfile, nil
}
func createCachefileStore(options Options) (*Pool, string, error) {
pool, err := New(options)
if err != nil {
return nil, "", err
}
f, err := os.CreateTemp("", "clash")
if err != nil {
return nil, "", err
}
db, err := bbolt.Open(f.Name(), 0o666, &bbolt.Options{Timeout: time.Second})
if err != nil {
return nil, "", err
}
pool.store = &cachefileStore{
cache: &cachefile.CacheFile{DB: db},
}
return pool, f.Name(), nil
}
func TestPool_Basic(t *testing.T) { func TestPool_Basic(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29") _, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
pool, _ := New(ipnet, 10, nil) pools, tempfile, err := createPools(Options{
IPNet: ipnet,
Size: 10,
})
assert.Nil(t, err)
defer os.Remove(tempfile)
first := pool.Lookup("foo.com") for _, pool := range pools {
last := pool.Lookup("bar.com") first := pool.Lookup("foo.com")
bar, exist := pool.LookBack(last) last := pool.Lookup("bar.com")
bar, exist := pool.LookBack(last)
assert.True(t, first.Equal(net.IP{192, 168, 0, 2})) assert.True(t, first.Equal(net.IP{192, 168, 0, 2}))
assert.True(t, last.Equal(net.IP{192, 168, 0, 3})) assert.Equal(t, pool.Lookup("foo.com"), net.IP{192, 168, 0, 2})
assert.True(t, exist) assert.True(t, last.Equal(net.IP{192, 168, 0, 3}))
assert.Equal(t, bar, "bar.com") assert.True(t, exist)
assert.Equal(t, bar, "bar.com")
assert.Equal(t, pool.Gateway(), net.IP{192, 168, 0, 1})
assert.Equal(t, pool.IPNet().String(), ipnet.String())
assert.True(t, pool.Exist(net.IP{192, 168, 0, 3}))
assert.False(t, pool.Exist(net.IP{192, 168, 0, 4}))
assert.False(t, pool.Exist(net.ParseIP("::1")))
}
} }
func TestPool_Cycle(t *testing.T) { func TestPool_CycleUsed(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
Size: 10,
})
assert.Nil(t, err)
defer os.Remove(tempfile)
for _, pool := range pools {
foo := pool.Lookup("foo.com")
bar := pool.Lookup("bar.com")
for i := 0; i < 3; i++ {
pool.Lookup(fmt.Sprintf("%d.com", i))
}
baz := pool.Lookup("baz.com")
next := pool.Lookup("foo.com")
assert.True(t, foo.Equal(baz))
assert.True(t, next.Equal(bar))
}
}
func TestPool_Skip(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/30") _, ipnet, _ := net.ParseCIDR("192.168.0.1/30")
pool, _ := New(ipnet, 10, nil) tree := trie.New()
tree.Insert("example.com", tree)
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
Size: 10,
Host: tree,
})
assert.Nil(t, err)
defer os.Remove(tempfile)
first := pool.Lookup("foo.com") for _, pool := range pools {
same := pool.Lookup("baz.com") assert.True(t, pool.ShouldSkipped("example.com"))
assert.False(t, pool.ShouldSkipped("foo.com"))
assert.True(t, first.Equal(same)) }
} }
func TestPool_MaxCacheSize(t *testing.T) { func TestPool_MaxCacheSize(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24") _, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
pool, _ := New(ipnet, 2, nil) pool, _ := New(Options{
IPNet: ipnet,
Size: 2,
})
first := pool.Lookup("foo.com") first := pool.Lookup("foo.com")
pool.Lookup("bar.com") pool.Lookup("bar.com")
@ -45,7 +132,10 @@ func TestPool_MaxCacheSize(t *testing.T) {
func TestPool_DoubleMapping(t *testing.T) { func TestPool_DoubleMapping(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24") _, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
pool, _ := New(ipnet, 2, nil) pool, _ := New(Options{
IPNet: ipnet,
Size: 2,
})
// fill cache // fill cache
fooIP := pool.Lookup("foo.com") fooIP := pool.Lookup("foo.com")
@ -70,9 +160,35 @@ func TestPool_DoubleMapping(t *testing.T) {
assert.False(t, bazIP.Equal(newBazIP)) assert.False(t, bazIP.Equal(newBazIP))
} }
func TestPool_Clone(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
pool, _ := New(Options{
IPNet: ipnet,
Size: 2,
})
first := pool.Lookup("foo.com")
last := pool.Lookup("bar.com")
assert.True(t, first.Equal(net.IP{192, 168, 0, 2}))
assert.True(t, last.Equal(net.IP{192, 168, 0, 3}))
newPool, _ := New(Options{
IPNet: ipnet,
Size: 2,
})
newPool.CloneFrom(pool)
_, firstExist := newPool.LookBack(first)
_, lastExist := newPool.LookBack(last)
assert.True(t, firstExist)
assert.True(t, lastExist)
}
func TestPool_Error(t *testing.T) { func TestPool_Error(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/31") _, ipnet, _ := net.ParseCIDR("192.168.0.1/31")
_, err := New(ipnet, 10, nil) _, err := New(Options{
IPNet: ipnet,
Size: 10,
})
assert.Error(t, err) assert.Error(t, err)
} }

115
component/iface/iface.go Normal file
View File

@ -0,0 +1,115 @@
package iface
import (
"errors"
"net"
"time"
"github.com/Dreamacro/clash/common/singledo"
)
type Interface struct {
Index int
Name string
Addrs []*net.IPNet
HardwareAddr net.HardwareAddr
}
var (
ErrIfaceNotFound = errors.New("interface not found")
ErrAddrNotFound = errors.New("addr not found")
)
var interfaces = singledo.NewSingle(time.Second * 20)
func ResolveInterface(name string) (*Interface, error) {
value, err, _ := interfaces.Do(func() (any, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
r := map[string]*Interface{}
for _, iface := range ifaces {
addrs, err := iface.Addrs()
if err != nil {
continue
}
ipNets := make([]*net.IPNet, 0, len(addrs))
for _, addr := range addrs {
ipNet := addr.(*net.IPNet)
if v4 := ipNet.IP.To4(); v4 != nil {
ipNet.IP = v4
}
ipNets = append(ipNets, ipNet)
}
r[iface.Name] = &Interface{
Index: iface.Index,
Name: iface.Name,
Addrs: ipNets,
HardwareAddr: iface.HardwareAddr,
}
}
return r, nil
})
if err != nil {
return nil, err
}
ifaces := value.(map[string]*Interface)
iface, ok := ifaces[name]
if !ok {
return nil, ErrIfaceNotFound
}
return iface, nil
}
func FlushCache() {
interfaces.Reset()
}
func (iface *Interface) PickIPv4Addr(destination net.IP) (*net.IPNet, error) {
return iface.pickIPAddr(destination, func(addr *net.IPNet) bool {
return addr.IP.To4() != nil
})
}
func (iface *Interface) PickIPv6Addr(destination net.IP) (*net.IPNet, error) {
return iface.pickIPAddr(destination, func(addr *net.IPNet) bool {
return addr.IP.To4() == nil
})
}
func (iface *Interface) pickIPAddr(destination net.IP, accept func(addr *net.IPNet) bool) (*net.IPNet, error) {
var fallback *net.IPNet
for _, addr := range iface.Addrs {
if !accept(addr) {
continue
}
if fallback == nil && !addr.IP.IsLinkLocalUnicast() {
fallback = addr
if destination == nil {
break
}
}
if destination != nil && addr.Contains(destination) {
return addr, nil
}
}
if fallback == nil {
return nil, ErrAddrNotFound
}
return fallback, nil
}

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