Compare commits

...

187 Commits

Author SHA1 Message Date
d1fb442bd5 Chore: update dependencies 2019-10-13 00:22:39 +08:00
7c4a359a2b Fix: dial tcp with context to avoid margin of error 2019-10-12 23:55:39 +08:00
0cdc40beb3 Fix: urltest get fastest node ehavior (#326) 2019-10-12 23:29:00 +08:00
4cd8b6f24f Fix: some UDP issues (#265) 2019-10-11 20:11:18 +08:00
0f63682bdf Chore: update README.md (#353) 2019-10-11 20:10:19 +08:00
52125a3992 Fix: fakeip missing host 2019-10-11 14:01:16 +08:00
06c9dfdb80 Feature: experimental support snell 2019-10-09 18:46:23 +08:00
54386ccda3 Chore: Improve grammar and wording (#337) 2019-10-08 10:59:24 +08:00
d3c50cf89f Fix: throw error when CONNECT return 5xx 2019-10-05 09:33:40 +08:00
50d2e082d5 Feature: websocket api support browser 2019-09-30 14:13:29 +08:00
c38469330d Fix: ip version check 2019-09-27 15:26:07 +08:00
045c3a3ad4 Fix: clearer error and ipv6 string parse (#325) 2019-09-27 10:33:37 +08:00
904c354ee4 Fix: use correctly last record 2019-09-26 10:08:50 +08:00
1a8a6d0b5d Feature: v2ray-plugin support disable mux 2019-09-21 23:49:00 +08:00
e0c8aed5c7 Fix(API): cors middleware hoisting because it doesn't work with r.Group 2019-09-21 21:28:02 +08:00
8f60d61ff9 Fix(fake-ip): return failed when type is AAAA 2019-09-21 10:30:43 +08:00
5e6ab99403 Fix: dial should return when dns failed (#311) 2019-09-20 16:04:06 +08:00
8adcc4d83b Fix: TCP dial error should not return early (#307) 2019-09-17 20:11:49 +08:00
b76737bdbb Feature: add fallback filters (#105) 2019-09-15 13:36:45 +08:00
09f435d928 Chore: update README.md 2019-09-14 21:45:11 +08:00
3dd9ea52d8 Fix(domain-trie): crash when insert 2019-09-14 21:42:40 +08:00
09917a2a95 Fix: tcp dual stack dial 2019-09-14 20:01:12 +08:00
96a4abf46c Feature: move hosts to the top 2019-09-14 20:01:08 +08:00
16e3090ee8 Feature: add version api 2019-09-13 17:44:30 +08:00
b3e10c05e6 Fix: parse error in proxyGroupsDagSort (#298) 2019-09-13 15:04:51 +08:00
112b3e5a6c Fix: don't close connection on status 100 Continue and header Proxy-Connection: Keep-Alive (#294) 2019-09-12 10:22:08 +08:00
60fdd82e2b Fix(API): use right status code 2019-09-08 22:33:52 +08:00
9815010131 Fix: HTTP status code 100 Continue support" (#288)
* Fix: HTTP Status Code `100 Continue` support

* Style: code style adjustment
2019-09-08 12:21:28 +09:00
9875f8ea6e Fix: typo (#287) 2019-09-07 17:23:43 +09:00
9e0bd62790 Migration: go 1.13 2019-09-04 23:26:20 +09:00
0d51877fcd Fix: should keep the original order of proxy groups (#284) 2019-08-28 23:44:32 +08:00
e34090c39a Improve: url-test retry dial when failed (#283) 2019-08-28 22:28:01 +08:00
5cc66e51db Chore: remove unused code (#282) 2019-08-27 16:23:44 +08:00
71f0a4e35e Fix: typo (#281) 2019-08-26 12:26:14 +08:00
48a2013d9c Fix: socks address stringify buffer overflow 2019-08-16 21:38:27 +08:00
6f3a654d6c Chore: ship github action 2019-08-15 11:40:40 +08:00
0f7f0a9b1a Optimization: socks UDP & fix typo (#261) 2019-08-12 14:01:32 +08:00
d59e98dc83 Feature: allow arbitrary order in proxy group (#89) 2019-08-12 10:11:44 +08:00
b137a50d85 Fix: crash in handleConn 2019-08-10 20:14:24 +08:00
f75cd04181 Change: speedtest with HEAD instead of GET (#259) 2019-08-09 15:39:13 +08:00
b926f4cf09 Feature: trace adapters when dialing (#170) 2019-08-09 01:28:37 +08:00
5829c3d5be Feature: support customizing bind-address when allow-lan is true (#255) 2019-08-08 13:45:07 +08:00
288afd1308 Fix: don't read yml if not exist (#253) 2019-08-07 14:21:39 +08:00
528fbd10e4 Fix(dns): use closure client (#251) 2019-08-02 09:22:09 +08:00
85128a634d Fix(vmess): set current server name in tls 2019-07-31 11:13:49 +08:00
f6acbaac7b Fix(vmess): typo (#248) 2019-07-29 12:25:29 +08:00
b75da2c6d8 Fix: typo (#246) 2019-07-29 10:12:10 +08:00
271ed2b9c1 Improve(fakeip): use lru cache to avoid outdate 2019-07-26 19:09:13 +08:00
1702e7ddb4 Chore(build): add mipsle-softfloat (#240) 2019-07-26 18:45:50 +08:00
1fd8f690fe Fix(socks5): fully udp associate support (#233) 2019-07-25 17:47:39 +08:00
183e776970 Chore: use lower case cipher format (#238) 2019-07-25 17:06:26 +08:00
f00dfdd34d Fix(picker): add WithTimeout for some situation 2019-07-18 00:12:01 +08:00
0670275533 Fix(url-test): incorrect result 2019-07-17 22:24:26 +08:00
9e77c650d9 Fix(domain-trie): domain could without dot 2019-07-16 00:57:08 +08:00
3497fdaf45 Fix(domain-trie): Incorrect result 2019-07-15 18:00:51 +08:00
6077e825c5 Fix(dns): miss response 2019-07-15 17:44:55 +08:00
0dd2a6dee5 Fix(dns): set handler when resolver not nil 2019-07-15 10:18:42 +08:00
c1b5e4f561 Fix(dns): Incorrect variable name 2019-07-14 23:16:52 +08:00
1a21c8ebfd Feature(dns): support custom hosts 2019-07-14 19:29:58 +08:00
f867f02546 Feature(API): logs and traffic support websocket 2019-07-12 15:44:12 +08:00
7c6c147a18 Optimization: refactor picker 2019-07-02 19:18:03 +08:00
0eff8516c0 Chore: improve Dockerfile 2019-06-30 15:10:56 +08:00
34338e7107 Chore: update dependencies & fix typo 2019-06-29 16:48:48 +08:00
57fdd223f1 Feature: custom dns ipv4/ipv6 dual stack 2019-06-29 00:58:59 +08:00
bc3fc0c840 Feature: support DoH 2019-06-28 12:29:08 +08:00
662038e40e Fix: log correctly path 2019-06-27 22:56:24 +08:00
53528f8275 Fix: crash when authenticator is nil 2019-06-27 20:45:12 +08:00
1c792b46c9 Feature: local socks5/http(s) auth (#216) 2019-06-27 17:04:25 +08:00
2417cfda12 Chore: set log output to stdout 2019-06-26 20:30:57 +08:00
aa3516ca24 Chore: use 'dns' for ALPN in tcp-tls nameserver (#209) 2019-06-20 15:50:01 +08:00
bcf5b21208 Fix: check target is valid in rules (#210) 2019-06-20 11:03:50 +08:00
ba5eefb6dd Chore: clean up Dockfile 2019-06-19 22:12:25 +08:00
407de7388c Standardized: use recommend extension & forward compatibility before 1.0 2019-06-18 20:55:26 +08:00
6adafde9a0 Fix: strict ss obfs check 2019-06-18 20:37:53 +08:00
cba548114f Fix: use original sequence for url-test group (#201) 2019-06-13 20:18:07 +08:00
016e7bd0b4 Style: fix go vet warning 2019-05-26 13:55:13 +08:00
ad13ad8dba Fix: add mutex for fake ip pool 2019-05-23 23:27:29 +08:00
89168e6c96 Fix: DNS server not recreate correctly (#186) 2019-05-18 17:52:42 +08:00
a4b8e286db Fix: incorrect fake ip dns ttl (#187) 2019-05-18 17:44:12 +08:00
e837470a6a Fix: udp crash in tunnel 2019-05-16 18:40:20 +08:00
b589754485 Chore: fix typo (#182) 2019-05-16 14:19:37 +08:00
0eccbb023c Feature: make the selector proxies order as same as the order in the config file (#180)
* make the proxies order the same as the order in config file

* formatting & rename variable
2019-05-15 14:40:14 +08:00
71a08ad8e2 Chore: clean up code 2019-05-14 21:35:34 +08:00
0d4a999707 Chore: adjust fake-ip ttl 2019-05-12 10:48:07 +08:00
243d8a2844 Chore: update README.md 2019-05-09 21:05:47 +08:00
225c530d13 Feature: add DST-PORT and SRC-PORT 2019-05-09 21:00:29 +08:00
cff4841f3e Chore: fix socks reader overflow & update dependencies 2019-05-06 21:00:29 +08:00
f352f4479e Feature: support fakeip 2019-05-03 00:05:14 +08:00
762f227512 Feature: support vmess udp 2019-04-25 16:32:15 +08:00
936ea3aa55 Feature: support outbound socks5 udp 2019-04-25 13:48:47 +08:00
cec2206774 Feature: add experimental config for resolving ip fail behavior 2019-04-24 12:02:52 +08:00
90e3dccacd Fix: add missing error check 2019-04-24 10:29:29 +08:00
c92cda6980 Feature: socks5 udp associate 2019-04-23 23:30:13 +08:00
49f8902961 Fix: typo in initial config file (#166) 2019-04-22 09:57:08 +08:00
7770e18430 Chore: add GitBook link to README.md (#155) 2019-04-15 19:07:18 +08:00
593a63c228 Fix: make releases script 2019-03-30 15:34:24 +08:00
744728cb84 Chore: update README.md 2019-03-30 14:20:04 +08:00
2036f8cb7a Fix: IP-CIDR invalid payload crash 2019-03-30 14:11:59 +08:00
531f487629 Fix: incorrect mutex in speedTest (#153) 2019-03-29 10:27:26 +08:00
18f885a92a Feature: add interval url test for load-balance 2019-03-28 19:00:41 +08:00
d3b280a7e5 Fix: reuse Current.HomeDir until go 1.13 release 2019-03-28 18:20:19 +08:00
d1f6886558 Style: use atomic CompareAndSwap (#151) 2019-03-26 23:48:03 +08:00
791d72e05b Fix: crash when key value is nil 2019-03-25 20:42:20 +08:00
14600a8170 Fix: dns hot reload no effect 2019-03-23 19:41:41 +08:00
bb267e4a1f Feature: add version command (#148) 2019-03-23 19:24:26 +08:00
f99da37168 Fix: fallback & url-test lose efficacy 2019-03-23 16:29:27 +08:00
7a9d986ff3 Feature: add delay history and improve url-test behavior 2019-03-17 14:52:39 +08:00
63446da5fa Fix: expand UDPSize to avoid resolving error (#139) 2019-03-17 14:08:15 +08:00
acf55a7f64 Improve: Dial would reset proxy alive status 2019-03-16 00:43:16 +08:00
8c608f5d7a Feature: add custom headers support in v2ray-plugin (#137) 2019-03-15 09:43:46 +08:00
7f0c7d7802 Fix: should not return extra ip in msgToIP 2019-03-03 17:23:59 +08:00
7683271fe6 Style: rename Generator with Dial 2019-03-03 11:59:07 +08:00
23bb01a4df Fix: http request keepAlive with right http header 2019-03-03 11:51:15 +08:00
0011c7acfe Improve: support tcp dns server & return an error when parsing nameserver (#127) 2019-03-01 00:52:30 +08:00
d75f9ff783 Migration: go 1.12 2019-02-27 01:02:43 +08:00
815e80f720 Fix: dns use Extra records 2019-02-24 01:26:51 +08:00
ca5399a16e Fix: dns cache behavior 2019-02-23 20:31:59 +08:00
04927229ff Fix: disconnect normal proxy request 2019-02-21 16:16:49 +08:00
c0bd82d62b Chore: rename final 2019-02-18 21:53:57 +08:00
5c8bb24121 Fix: crash when directly request proxy server 2019-02-18 20:14:18 +08:00
575720e0cc Fix: windows 386 build 2019-02-15 23:43:28 +08:00
e7997a035b Chore: update README.md 2019-02-15 22:01:11 +08:00
287ad5bc53 Fix: vmess handshake block (#117) 2019-02-15 21:55:15 +08:00
c295c5e412 Feature: add load-balance group 2019-02-15 14:25:20 +08:00
8636a4f589 Fix: return 502 in http outbound (#116) 2019-02-14 11:37:47 +08:00
7a0717830c Fix: api invalid returning 2019-02-13 23:45:43 +08:00
5920b05752 Fix: crash when ip is nil 2019-02-12 12:29:33 +08:00
26a87f9d34 Fix: redir-host mode crash 2019-02-11 17:20:42 +08:00
8da19e81a4 Update: README.md 2019-02-11 15:55:17 +08:00
1339487ce4 Fix: tun2socks not lookup IP 2019-02-11 15:44:42 +08:00
2383cca2ce Feature: add v2ray-plugin 2019-02-11 15:25:39 +08:00
53b5ef199f Fix: parse proxies shadow variable 2019-02-04 09:39:17 +08:00
754df5ba9b Chore: update dependencies 2019-02-02 21:53:30 +08:00
1016355ef6 Chore: log dns server address when success 2019-02-02 21:37:36 +08:00
b594cbc68d Fix: parse ip string when use socks proxy (#100) 2019-02-02 21:11:27 +08:00
42d33fe629 Feature: SOURCE-IP-CIDR rule type (#96) 2019-02-02 21:03:13 +08:00
bfe51e46b0 Improve: lazy resolve ip 2019-02-02 20:47:38 +08:00
e30a628702 Fix: shadow variable 2019-01-30 20:22:52 +08:00
5581698908 Chore: improve programming style (#109) 2019-01-29 23:46:18 +08:00
36b5d1f18f Fix: DNS server returns the correct TTL 2019-01-25 15:38:14 +08:00
bd6c6a9ad1 Chore: print origin rule when format error (#92) 2019-01-14 10:35:11 +08:00
83fac44010 Fix: nghttpx return 400 error (#84) 2019-01-07 10:47:25 +08:00
15a77fa71b Fix: print log when start dns server failed (#87) 2019-01-06 14:31:42 +08:00
7768c5b933 Chore: add more platform release (#83) 2019-01-03 10:49:09 +08:00
4e91118a05 Feature: add freebsd release (#80)
add freebsd support
2018-12-31 20:57:21 +08:00
7b5e1f759c Fix: authentication with stream api 2018-12-29 14:11:54 +08:00
3f6c707aa9 Fix: patch config field 2018-12-23 20:25:49 +08:00
532ec88964 Chore: make a consistent code style 2018-12-23 00:42:08 +08:00
cb118d4371 Chore: improve outbound architecture 2018-12-22 23:56:42 +08:00
a7cfc81885 Fix: ignore some general configuration 2018-12-21 22:51:37 +08:00
551ab68c1e Fix: allow access to external-ui without authentication (#75) 2018-12-21 17:48:29 +08:00
ef6260282f Fix: parse external-ui 2018-12-21 10:55:21 +08:00
49635eab6c Chore: update external-ui explanation 2018-12-20 22:34:38 +08:00
a46041b81c Fix: force param make no sense 2018-12-20 22:23:31 +08:00
a6bbc67afb Feature: add custom ui support in API 2018-12-20 01:29:13 +08:00
afc4644dd1 Feature: FreeBSD compatibility patch (#63) 2018-12-18 10:37:00 +08:00
1607d3253f Feature: add websocket headers support in vmess 2018-12-11 00:25:05 +08:00
34c8655974 Fix: don't keepalive when connection is close (#65)
fixed #60
2018-12-10 11:48:57 +08:00
5e4b35e03a Chore: standardize API returns 2018-12-10 11:33:37 +08:00
fa9077969c Fix: dns crash & remove unused debug log 2018-12-10 11:00:52 +08:00
fcb1a7813a Fix: dns msg to ip 2018-12-06 13:29:43 +08:00
6f1bc3d65b Fix: add PATCH for CORS 2018-12-06 10:54:45 +08:00
2b93c9d4c9 Fix: resolve ip crash 2018-12-06 10:51:37 +08:00
f93d6aa294 Fix: crash when dns not set 2018-12-05 21:52:31 +08:00
f192d591c7 Chore: bump to 0.10.0 2018-12-05 21:26:04 +08:00
03c249ecb1 Feature: add custom DNS support (#56) 2018-12-05 21:13:29 +08:00
da5db36ccf Fix: policy group unexpectedly closed 2018-12-05 18:19:30 +08:00
ca6e67a384 Feature: add silent info level 2018-12-03 23:41:40 +08:00
6636db242b Feature: add http/https [connect] proxy (#52) 2018-12-03 23:27:00 +08:00
f5715c4f42 Fix: chunk size limit in tls obfs (#54)
* Fix: add chunkSize limit in TLSObfs

* Chore: add length var for len(b)
2018-12-01 09:32:02 +08:00
9cfd26d440 Feat: add switch config file API 2018-11-30 17:42:40 +08:00
dc24dd4d89 Fix: tls server name missing in vmess 2018-11-28 23:24:57 +08:00
a64cea5011 Fix: patch config API 2018-11-28 10:38:30 +08:00
f6743d4d21 Fix: chrome crash when using SwitchyOmega by reject rule (#47)
* Fix: chrome crash when using SwitchyOmega by reject rule
2018-11-25 17:00:11 +08:00
970643b144 Fix: goroutine leak while closing proxy (#43)
* Fix: goroutine leak while closing proxy

* Chore: improve proxy architecture

* Fix: stack overflow
2018-11-22 11:54:01 +08:00
05bf4d44ab Change: replace FINAL with MATCH in a progressive way 2018-11-21 18:21:24 +08:00
c7a349e1fe Improve: auto change payload to lowercase 2018-11-21 13:59:39 +08:00
01a477bd3d Chore: improve code architecture 2018-11-21 13:47:46 +08:00
91e35f2f6a Fix: resolve path in windows 2018-11-14 20:58:10 +08:00
b0e062dc7c Feature: SOCKS5 authentication support (#34)
* Feature: socks5 auth support

* Chore: make code unified

* Fix: auth buffer length
2018-11-09 17:36:30 +08:00
09cd34ec07 Chore: update README.md 2018-11-08 20:14:57 +08:00
da391356dd Fix: simple-obfs tls 2018-11-07 16:57:21 +08:00
502aa61c0e Fix: vmess small probability invalid auth 2018-11-06 17:34:19 +08:00
cc6d496143 Chore: optimize code structure in vmess websocket (#28)
* Chore: move conn process of ws to websocket.go

* Chore: some routine adjustment
2018-11-04 21:36:20 +08:00
10e0231bc1 Fix: dial IPv6 host (#29) 2018-11-04 21:12:16 +08:00
fd63707399 Optimization: use client session cache for TLS connection (#26) 2018-11-01 11:54:45 +08:00
c5757a9b11 Chore: delete redundant print 2018-10-30 10:50:57 +08:00
112 changed files with 7402 additions and 2013 deletions

21
.github/workflows/go.yml vendored Normal file
View File

@ -0,0 +1,21 @@
name: Go
on: [push, pull_request]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v1
with:
go-version: 1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: Get dependencies
run: |
go test ./...

7
.gitignore vendored
View File

@ -4,6 +4,7 @@
*.dll
*.so
*.dylib
bin/*
# Test binary, build with `go test -c`
*.test
@ -13,3 +14,9 @@
# dep
vendor
# GoLand
.idea/*
# macOS file
.DS_Store

View File

@ -1,7 +1,7 @@
language: go
sudo: false
go:
- "1.11"
- '1.13'
install:
- "go mod download"
env:

View File

@ -1,23 +1,18 @@
FROM golang:latest as builder
FROM golang:alpine as builder
RUN wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz -O /tmp/GeoLite2-Country.tar.gz && \
RUN apk add --no-cache make git && \
wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz -O /tmp/GeoLite2-Country.tar.gz && \
tar zxvf /tmp/GeoLite2-Country.tar.gz -C /tmp && \
mv /tmp/GeoLite2-Country_*/GeoLite2-Country.mmdb /Country.mmdb
WORKDIR /clash-src
COPY . /clash-src
RUN go mod download && \
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o /clash
make linux-amd64 && \
mv ./bin/clash-linux-amd64 /clash
FROM alpine:latest
RUN apk add --no-cache ca-certificates
COPY --from=builder /Country.mmdb /root/.config/clash/
COPY --from=builder /clash /
EXPOSE 7890 7891
ENTRYPOINT ["/clash"]

View File

@ -1,23 +1,97 @@
NAME=clash
BINDIR=bin
GOBUILD=CGO_ENABLED=0 go build -ldflags '-w -s'
VERSION=$(shell git describe --tags || echo "unknown version")
BUILDTIME=$(shell date -u)
GOBUILD=CGO_ENABLED=0 go build -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \
-X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \
-w -s'
all: linux macos win64
PLATFORM_LIST = \
darwin-amd64 \
linux-386 \
linux-amd64 \
linux-armv5 \
linux-armv6 \
linux-armv7 \
linux-armv8 \
linux-mips-softfloat \
linux-mips-hardfloat \
linux-mipsle-softfloat \
linux-mipsle-hardfloat \
linux-mips64 \
linux-mips64le \
freebsd-386 \
freebsd-amd64
linux:
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
WINDOWS_ARCH_LIST = \
windows-386 \
windows-amd64
macos:
all: linux-amd64 darwin-amd64 windows-amd64 # Most used
darwin-amd64:
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
win64:
linux-386:
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64:
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv5:
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv6:
GOARCH=arm GOOS=linux GOARM=6 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv7:
GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv8:
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-mips-softfloat:
GOARCH=mips GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-mips-hardfloat:
GOARCH=mips GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-mipsle-softfloat:
GOARCH=mipsle GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-mipsle-hardfloat:
GOARCH=mipsle GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-mips64:
GOARCH=mips64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-mips64le:
GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-386:
GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-amd64:
GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
windows-386:
GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-amd64:
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
releases: linux macos win64
chmod +x $(BINDIR)/$(NAME)-*
gzip $(BINDIR)/$(NAME)-linux
gzip $(BINDIR)/$(NAME)-macos
zip -m -j $(BINDIR)/$(NAME)-win64.zip $(BINDIR)/$(NAME)-win64.exe
gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST))
$(gz_releases): %.gz : %
chmod +x $(BINDIR)/$(NAME)-$(basename $@)
gzip -f -S -$(VERSION).gz $(BINDIR)/$(NAME)-$(basename $@)
$(zip_releases): %.zip : %
zip -m -j $(BINDIR)/$(NAME)-$(basename $@)-$(VERSION).zip $(BINDIR)/$(NAME)-$(basename $@).exe
all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST)
releases: $(gz_releases) $(zip_releases)
clean:
rm $(BINDIR)/*

263
README.md
View File

@ -1,11 +1,9 @@
<h1 align="center">
<img src="https://github.com/Dreamacro/clash/raw/master/docs/logo.png" alt="Clash" width="200">
<br>
Clash
<br>
<br>Clash<br>
</h1>
<h4 align="center">A rule based proxy in Go.</h4>
<h4 align="center">A rule-based tunnel in Go.</h4>
<p align="center">
<a href="https://travis-ci.org/Dreamacro/clash">
@ -13,7 +11,7 @@
alt="Travis-CI">
</a>
<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 href="https://github.com/Dreamacro/clash/releases">
<img src="https://img.shields.io/github/release/Dreamacro/clash/all.svg?style=flat-square">
@ -22,38 +20,39 @@
## Features
- HTTP/HTTPS and SOCKS proxy
- Surge like configuration
- Local HTTP/HTTPS/SOCKS server
- Surge-like configuration format
- GeoIP rule support
- Support Vmess/Shadowsocks/Socks5
- Support for Netfilter TCP redirect
## Discussion
[Telegram Group](https://t.me/clash_discuss)
- Supports Vmess, Shadowsocks, Snell and SOCKS5 protocol
- Supports Netfilter TCP redirecting
- Comprehensive HTTP API
## Install
You can build from source:
Clash Requires Go >= 1.13. You can build it from source:
```sh
go get -u -v github.com/Dreamacro/clash
$ go get -u -v github.com/Dreamacro/clash
```
Pre-built binaries are available: [release](https://github.com/Dreamacro/clash/releases)
Pre-built binaries are available here: [release](https://github.com/Dreamacro/clash/releases)
Requires Go >= 1.10.
Check Clash version with:
```sh
$ clash -v
```
## Daemon
Unfortunately, there is no native elegant way to implement golang's daemon.
Unfortunately, there is no native and elegant way to implement daemons on Golang.
So we can use third-party daemon tools like pm2, supervisor, and so on.
So we can use third-party daemon tools like PM2, Supervisor or the like.
In the case of [pm2](https://github.com/Unitech/pm2), we can start the daemon this way:
```sh
pm2 start clash
$ pm2 start clash
```
If you have Docker installed, you can run clash directly using `docker-compose`.
@ -62,21 +61,20 @@ If you have Docker installed, you can run clash directly using `docker-compose`.
## Config
**NOTE: after v0.8.0, clash using yaml as configuration file**
The default configuration directory is `$HOME/.config/clash`.
The default configuration directory is `$HOME/.config/clash`
The name of the configuration file is `config.yaml`.
The name of the configuration file is `config.yml`
If you want to use another directory, use `-d` to control the configuration directory.
If you want to use another directory, you can use `-d` to control the configuration directory
For example, you can use the current directory as the configuration directory
For example, you can use the current directory as the configuration directory:
```sh
clash -d .
$ clash -d .
```
Below is a simple demo configuration file:
<details>
<summary>This is an example configuration file</summary>
```yml
# port of HTTP
@ -85,73 +83,220 @@ port: 7890
# port of SOCKS5
socks-port: 7891
# redir proxy for Linux and macOS
# redir port for Linux and macOS
# redir-port: 7892
allow-lan: false
# Only applicable when setting allow-lan to true
# "*": bind all IP addresses
# 192.168.122.11: bind a single IPv4 address
# "[aaaa::a8aa:ff:fe09:57d8]": bind a single IPv6 address
# bind-address: "*"
# Rule / Global/ Direct (default is Rule)
mode: Rule
# set log level to stdout (default is info)
# info / warning / error / debug
# info / warning / error / debug / silent
log-level: info
# A RESTful API for clash
# RESTful API for clash
external-controller: 127.0.0.1:9090
# you can put the static web resource (such as clash-dashboard) to a directory, and clash would serve in `${API}/ui`
# input is a relative path to the configuration directory or an absolute path
# external-ui: folder
# Secret for RESTful API (Optional)
# secret: ""
# experimental feature
experimental:
ignore-resolve-fail: true # ignore dns resolve fail, default value is true
# authentication of local SOCKS5/HTTP(S) server
# authentication:
# - "user1:pass1"
# - "user2:pass2"
# # experimental hosts, support wildcard (e.g. *.clash.dev Even *.foo.*.example.com)
# # static domain has a higher priority than wildcard domain (foo.example.com > *.example.com)
# hosts:
# '*.clash.dev': 127.0.0.1
# 'alpha.clash.dev': '::1'
# dns:
# enable: true # set true to enable dns (default is false)
# ipv6: false # default is false
# listen: 0.0.0.0:53
# enhanced-mode: redir-host # or fake-ip
# # fake-ip-range: 198.18.0.1/16 # if you don't know what it is, don't change it
# nameserver:
# - 114.114.114.114
# - tls://dns.rubyfish.cn:853 # dns over tls
# - https://1.1.1.1/dns-query # dns over https
# fallback: # concurrent request with nameserver, fallback used when GEOIP country isn't CN
# - tcp://1.1.1.1
# fallback-filter:
# geoip: true # default
# ipcidr: # ips in these subnets will be considered polluted
# - 240.0.0.0/4
Proxy:
# shadowsocks
# The types of cipher are consistent with go-shadowsocks2
# support AEAD_AES_128_GCM AEAD_AES_192_GCM AEAD_AES_256_GCM AEAD_CHACHA20_POLY1305 AES-128-CTR AES-192-CTR AES-256-CTR AES-128-CFB AES-192-CFB AES-256-CFB CHACHA20-IETF XCHACHA20
# In addition to what go-shadowsocks2 supports, it also supports chacha20 rc4-md5 xchacha20-ietf-poly1305
- { name: "ss1", type: ss, server: server, port: 443, cipher: AEAD_CHACHA20_POLY1305, password: "password" }
- { name: "ss2", type: ss, server: server, port: 443, cipher: AEAD_CHACHA20_POLY1305, password: "password", obfs: tls, obfs-host: bing.com }
# The supported ciphers(encrypt methods):
# aes-128-gcm aes-192-gcm aes-256-gcm
# aes-128-cfb aes-192-cfb aes-256-cfb
# aes-128-ctr aes-192-ctr aes-256-ctr
# rc4-md5 chacha20 chacha20-ietf xchacha20
# chacha20-ietf-poly1305 xchacha20-ietf-poly1305
- name: "ss1"
type: ss
server: server
port: 443
cipher: chacha20-ietf-poly1305
password: "password"
# udp: true
# old obfs configuration format remove after prerelease
- name: "ss2"
type: ss
server: server
port: 443
cipher: chacha20-ietf-poly1305
password: "password"
plugin: obfs
plugin-opts:
mode: tls # or http
# host: bing.com
- name: "ss3"
type: ss
server: server
port: 443
cipher: chacha20-ietf-poly1305
password: "password"
plugin: v2ray-plugin
plugin-opts:
mode: websocket # no QUIC now
# tls: true # wss
# skip-cert-verify: true
# host: bing.com
# path: "/"
# mux: true
# headers:
# custom: value
# vmess
# cipher support auto/aes-128-gcm/chacha20-poly1305/none
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto }
# with tls
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, tls: true }
# with tls and skip-cert-verify
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, tls: true, skip-cert-verify: true }
# with ws
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, network: ws, ws-path: /path }
# with ws + tls
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, network: ws, ws-path: /path, tls: true }
- name: "vmess"
type: vmess
server: server
port: 443
uuid: uuid
alterId: 32
cipher: auto
# udp: true
# tls: true
# skip-cert-verify: true
# network: ws
# ws-path: /path
# ws-headers:
# Host: v2ray.com
# socks5
- { name: "socks", type: socks5, server: server, port: 443 }
# with tls
- { name: "socks", type: socks5, server: server, port: 443, tls: true }
# with tls and skip-cert-verify
- { name: "socks", type: socks5, server: server, port: 443, tls: true, skip-cert-verify: true }
- name: "socks"
type: socks5
server: server
port: 443
# username: username
# password: password
# tls: true
# skip-cert-verify: true
# udp: true
# http
- name: "http"
type: http
server: server
port: 443
# username: username
# password: password
# tls: true # https
# skip-cert-verify: true
# snell
- name: "snell"
type: snell
server: server
port: 44046
psk: yourpsk
# obfs-opts:
# mode: http # or tls
# host: bing.com
Proxy Group:
# url-test select which proxy will be used by benchmarking speed to a URL.
- { name: "auto", type: url-test, proxies: ["ss1", "ss2", "vmess1"], url: http://www.gstatic.com/generate_204, interval: 300 }
- name: "auto"
type: url-test
proxies:
- ss1
- ss2
- vmess1
url: 'http://www.gstatic.com/generate_204'
interval: 300
# fallback select an available policy by priority. The availability is tested by accessing an URL, just like an auto url-test group.
- { name: "fallback-auto", type: fallback, proxies: ["ss1", "ss2", "vmess1"], url: http://www.gstatic.com/generate_204, interval: 300 }
- name: "fallback-auto"
type: fallback
proxies:
- ss1
- ss2
- vmess1
url: 'http://www.gstatic.com/generate_204'
interval: 300
# load-balance: The request of the same eTLD will be dial on the same proxy.
- name: "load-balance"
type: load-balance
proxies:
- ss1
- ss2
- vmess1
url: 'http://www.gstatic.com/generate_204'
interval: 300
# select is used for selecting proxy or proxy group
# you can use RESTful API to switch proxy, is recommended for use in GUI.
- { name: "Proxy", type: select, proxies: ["ss1", "ss2", "vmess1", "auto"] }
- name: Proxy
type: select
proxies:
- ss1
- ss2
- vmess1
- auto
Rule:
- DOMAIN-SUFFIX,google.com,Proxy
- DOMAIN-KEYWORD,google,Proxy
- DOMAIN,google.com,Proxy
- DOMAIN-SUFFIX,google.com,auto
- DOMAIN-KEYWORD,google,auto
- DOMAIN,google.com,auto
- DOMAIN-SUFFIX,ad.com,REJECT
- IP-CIDR,127.0.0.0/8,DIRECT
# rename SOURCE-IP-CIDR and would remove after prerelease
- SRC-IP-CIDR,192.168.1.201/32,DIRECT
- GEOIP,CN,DIRECT
# note: there is two ","
- FINAL,,Proxy
- DST-PORT,80,DIRECT
- SRC-PORT,7777,DIRECT
# FINAL would remove after prerelease
# you also can use `FINAL,Proxy` or `FINAL,,Proxy` now
- MATCH,auto
```
</details>
## Documentations
https://clash.gitbook.io/
## Thanks
@ -167,5 +312,5 @@ Rule:
- [x] Complementing the necessary rule operators
- [x] Redir proxy
- [ ] UDP support
- [x] UDP support
- [ ] Connection manager

View File

@ -10,32 +10,27 @@ import (
// HTTPAdapter is a adapter for HTTP connection
type HTTPAdapter struct {
net.Conn
metadata *C.Metadata
conn net.Conn
R *http.Request
}
// Close HTTP connection
func (h *HTTPAdapter) Close() {
h.conn.Close()
}
// Metadata return destination metadata
func (h *HTTPAdapter) Metadata() *C.Metadata {
return h.metadata
}
// Conn return raw net.Conn of HTTP
func (h *HTTPAdapter) Conn() net.Conn {
return h.conn
}
// NewHTTP is HTTPAdapter generator
func NewHTTP(request *http.Request, conn net.Conn) *HTTPAdapter {
metadata := parseHTTPAddr(request)
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
return &HTTPAdapter{
metadata: parseHTTPAddr(request),
metadata: metadata,
R: request,
conn: conn,
Conn: conn,
}
}

View File

@ -7,8 +7,13 @@ import (
// NewHTTPS is HTTPAdapter generator
func NewHTTPS(request *http.Request, conn net.Conn) *SocketAdapter {
metadata := parseHTTPAddr(request)
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
return &SocketAdapter{
metadata: parseHTTPAddr(request),
conn: conn,
metadata: metadata,
Conn: conn,
}
}

View File

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

View File

@ -5,37 +5,30 @@ import (
"net/http"
"strconv"
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/go-shadowsocks2/socks"
)
func parseSocksAddr(target socks.Addr) *C.Metadata {
var host, port string
var ip net.IP
func parseSocksAddr(target socks5.Addr) *C.Metadata {
metadata := &C.Metadata{
AddrType: int(target[0]),
}
switch target[0] {
case socks.AtypDomainName:
host = string(target[2 : 2+target[1]])
port = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
ipAddr, err := net.ResolveIPAddr("ip", host)
if err == nil {
ip = ipAddr.IP
}
case socks.AtypIPv4:
ip = net.IP(target[1 : 1+net.IPv4len])
port = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
case socks.AtypIPv6:
ip = net.IP(target[1 : 1+net.IPv6len])
port = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
case socks5.AtypDomainName:
metadata.Host = string(target[2 : 2+target[1]])
metadata.DstPort = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
case socks5.AtypIPv4:
ip := net.IP(target[1 : 1+net.IPv4len])
metadata.DstIP = &ip
metadata.DstPort = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
case socks5.AtypIPv6:
ip := net.IP(target[1 : 1+net.IPv6len])
metadata.DstIP = &ip
metadata.DstPort = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
}
return &C.Metadata{
NetWork: C.TCP,
AddrType: int(target[0]),
Host: host,
IP: &ip,
Port: port,
}
return metadata
}
func parseHTTPAddr(request *http.Request) *C.Metadata {
@ -44,28 +37,36 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
if port == "" {
port = "80"
}
ipAddr, err := net.ResolveIPAddr("ip", host)
var resolveIP *net.IP
if err == nil {
resolveIP = &ipAddr.IP
}
var addType int
ip := net.ParseIP(host)
switch {
case ip == nil:
addType = socks.AtypDomainName
case ip.To4() == nil:
addType = socks.AtypIPv6
default:
addType = socks.AtypIPv4
}
return &C.Metadata{
metadata := &C.Metadata{
NetWork: C.TCP,
AddrType: addType,
Type: C.HTTP,
AddrType: C.AtypDomainName,
Host: host,
IP: resolveIP,
Port: port,
DstIP: nil,
DstPort: port,
}
ip := net.ParseIP(host)
if ip != nil {
switch {
case ip.To4() == nil:
metadata.AddrType = C.AtypIPv6
default:
metadata.AddrType = C.AtypIPv4
}
metadata.DstIP = &ip
}
return metadata
}
func parseAddr(addr string) (*net.IP, string, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, "", err
}
ip := net.ParseIP(host)
return &ip, port, nil
}

207
adapters/outbound/base.go Normal file
View File

@ -0,0 +1,207 @@
package adapters
import (
"context"
"encoding/json"
"errors"
"net"
"net/http"
"time"
"github.com/Dreamacro/clash/common/queue"
C "github.com/Dreamacro/clash/constant"
)
var (
defaultURLTestTimeout = time.Second * 5
)
type Base struct {
name 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) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
return nil, nil, errors.New("no support")
}
func (b *Base) SupportUDP() bool {
return b.udp
}
func (b *Base) Destroy() {}
func (b *Base) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": b.Type().String(),
})
}
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(c net.PacketConn, a C.ProxyAdapter) C.PacketConn {
return &packetConn{c, []string{a.Name()}}
}
type Proxy struct {
C.ProxyAdapter
history *queue.Queue
alive bool
}
func (p *Proxy) Alive() bool {
return p.alive
}
func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
defer cancel()
return p.DialContext(ctx, metadata)
}
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
conn, err := p.ProxyAdapter.DialContext(ctx, metadata)
if err != nil {
p.alive = false
}
return conn, err
}
func (p *Proxy) DelayHistory() []C.DelayHistory {
queue := p.history.Copy()
histories := []C.DelayHistory{}
for _, item := range queue {
histories = append(histories, item.(C.DelayHistory))
}
return histories
}
// LastDelay return last history record. if proxy is not alive, return the max value of uint16.
func (p *Proxy) LastDelay() (delay uint16) {
var max uint16 = 0xffff
if !p.alive {
return max
}
last := p.history.Last()
if last == nil {
return max
}
history := last.(C.DelayHistory)
if history.Delay == 0 {
return max
}
return history.Delay
}
func (p *Proxy) MarshalJSON() ([]byte, error) {
inner, err := p.ProxyAdapter.MarshalJSON()
if err != nil {
return inner, err
}
mapping := map[string]interface{}{}
json.Unmarshal(inner, &mapping)
mapping["history"] = p.DelayHistory()
return json.Marshal(mapping)
}
// URLTest get the delay for the specified URL
func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
defer func() {
p.alive = err == nil
record := C.DelayHistory{Time: time.Now()}
if err == nil {
record.Delay = t
}
p.history.Put(record)
if p.history.Len() > 10 {
p.history.Pop()
}
}()
addr, err := urlToMetadata(url)
if err != nil {
return
}
start := time.Now()
instance, err := p.DialContext(ctx, &addr)
if err != nil {
return
}
defer instance.Close()
req, err := http.NewRequest(http.MethodHead, url, nil)
if err != nil {
return
}
req = req.WithContext(ctx)
transport := &http.Transport{
Dial: func(string, string) (net.Conn, error) {
return instance, nil
},
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
client := http.Client{Transport: transport}
resp, err := client.Do(req)
if err != nil {
return
}
resp.Body.Close()
t = uint16(time.Since(start) / time.Millisecond)
return
}
func NewProxy(adapter C.ProxyAdapter) *Proxy {
return &Proxy{adapter, queue.New(10), true}
}
// ProxyGroupOption contain the common options for all kind of ProxyGroup
type ProxyGroupOption struct {
Name string `proxy:"name"`
Proxies []string `proxy:"proxies"`
}

View File

@ -1,45 +1,49 @@
package adapters
import (
"context"
"net"
C "github.com/Dreamacro/clash/constant"
)
// DirectAdapter is a directly connected adapter
type DirectAdapter struct {
conn net.Conn
type Direct struct {
*Base
}
// Close is used to close connection
func (d *DirectAdapter) Close() {
d.conn.Close()
}
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
address := net.JoinHostPort(metadata.Host, metadata.DstPort)
if metadata.DstIP != nil {
address = net.JoinHostPort(metadata.DstIP.String(), metadata.DstPort)
}
// Conn is used to http request
func (d *DirectAdapter) Conn() net.Conn {
return d.conn
}
type Direct struct{}
func (d *Direct) Name() string {
return "DIRECT"
}
func (d *Direct) Type() C.AdapterType {
return C.Direct
}
func (d *Direct) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
c, err := net.DialTimeout("tcp", net.JoinHostPort(metadata.String(), metadata.Port), tcpTimeout)
c, err := dialContext(ctx, "tcp", address)
if err != nil {
return
return nil, err
}
tcpKeepAlive(c)
return &DirectAdapter{conn: c}, nil
return newConn(c, d), nil
}
func (d *Direct) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
pc, err := net.ListenPacket("udp", "")
if err != nil {
return nil, nil, err
}
addr, err := resolveUDPAddr("udp", metadata.RemoteAddress())
if err != nil {
return nil, nil, err
}
return newPacketConn(pc, d), addr, nil
}
func NewDirect() *Direct {
return &Direct{}
return &Direct{
Base: &Base{
name: "DIRECT",
tp: C.Direct,
udp: true,
},
}
}

View File

@ -1,21 +1,19 @@
package adapters
import (
"context"
"encoding/json"
"errors"
"net"
"sync"
"time"
C "github.com/Dreamacro/clash/constant"
)
type proxy struct {
RawProxy C.Proxy
Valid bool
}
type Fallback struct {
name string
proxies []*proxy
*Base
proxies []C.Proxy
rawURL string
interval time.Duration
done chan struct{}
@ -28,42 +26,47 @@ type FallbackOption struct {
Interval int `proxy:"interval"`
}
func (f *Fallback) Name() string {
return f.name
}
func (f *Fallback) Type() C.AdapterType {
return C.Fallback
}
func (f *Fallback) Now() string {
_, proxy := f.findNextValidProxy(0)
if proxy != nil {
return proxy.RawProxy.Name()
}
return f.proxies[0].RawProxy.Name()
proxy := f.findAliveProxy()
return proxy.Name()
}
func (f *Fallback) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
idx := 0
var proxy *proxy
for {
idx, proxy = f.findNextValidProxy(idx)
if proxy == nil {
break
}
adapter, err = proxy.RawProxy.Generator(metadata)
if err != nil {
proxy.Valid = false
idx++
continue
}
return
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 f.proxies[0].RawProxy.Generator(metadata)
return c, err
}
func (f *Fallback) Close() {
func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
proxy := f.findAliveProxy()
pc, addr, err := proxy.DialUDP(metadata)
if err == nil {
pc.AppendToChains(f)
}
return pc, addr, 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) Destroy() {
f.done <- struct{}{}
}
@ -81,13 +84,13 @@ Loop:
}
}
func (f *Fallback) findNextValidProxy(start int) (int, *proxy) {
for i := start; i < len(f.proxies); i++ {
if f.proxies[i].Valid {
return i, f.proxies[i]
func (f *Fallback) findAliveProxy() C.Proxy {
for _, proxy := range f.proxies {
if proxy.Alive() {
return proxy
}
}
return -1, nil
return f.proxies[0]
}
func (f *Fallback) validTest() {
@ -95,9 +98,8 @@ func (f *Fallback) validTest() {
wg.Add(len(f.proxies))
for _, p := range f.proxies {
go func(p *proxy) {
_, err := DelayTest(p.RawProxy, f.rawURL)
p.Valid = err == nil
go func(p C.Proxy) {
p.URLTest(context.Background(), f.rawURL)
wg.Done()
}(p)
}
@ -116,17 +118,13 @@ func NewFallback(option FallbackOption, proxies []C.Proxy) (*Fallback, error) {
}
interval := time.Duration(option.Interval) * time.Second
warpperProxies := make([]*proxy, len(proxies))
for idx := range proxies {
warpperProxies[idx] = &proxy{
RawProxy: proxies[idx],
Valid: true,
}
}
Fallback := &Fallback{
name: option.Name,
proxies: warpperProxies,
Base: &Base{
name: option.Name,
tp: C.Fallback,
},
proxies: proxies,
rawURL: option.URL,
interval: interval,
done: make(chan struct{}),

125
adapters/outbound/http.go Normal file
View File

@ -0,0 +1,125 @@
package adapters
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/http"
"strconv"
C "github.com/Dreamacro/clash/constant"
)
type Http struct {
*Base
addr string
user string
pass string
tls bool
skipCertVerify bool
tlsConfig *tls.Config
}
type HttpOption struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UserName string `proxy:"username,omitempty"`
Password string `proxy:"password,omitempty"`
TLS bool `proxy:"tls,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
}
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
c, err := dialContext(ctx, "tcp", h.addr)
if err == nil && h.tls {
cc := tls.Client(c, h.tlsConfig)
err = cc.Handshake()
c = cc
}
if err != nil {
return nil, fmt.Errorf("%s connect error", h.addr)
}
tcpKeepAlive(c)
if err := h.shakeHand(metadata, c); err != nil {
return nil, err
}
return newConn(c, h), nil
}
func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
var buf bytes.Buffer
var err error
addr := metadata.RemoteAddress()
buf.WriteString("CONNECT " + addr + " HTTP/1.1\r\n")
buf.WriteString("Host: " + metadata.String() + "\r\n")
buf.WriteString("Proxy-Connection: Keep-Alive\r\n")
if h.user != "" && h.pass != "" {
auth := h.user + ":" + h.pass
buf.WriteString("Proxy-Authorization: Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) + "\r\n")
}
// header ended
buf.WriteString("\r\n")
_, err = rw.Write(buf.Bytes())
if err != nil {
return err
}
var req http.Request
resp, err := http.ReadResponse(bufio.NewReader(rw), &req)
if err != nil {
return err
}
if resp.StatusCode == http.StatusOK {
return nil
}
if resp.StatusCode == http.StatusProxyAuthRequired {
return errors.New("HTTP need auth")
}
if resp.StatusCode == http.StatusMethodNotAllowed {
return errors.New("CONNECT method not allowed by proxy")
}
if resp.StatusCode >= http.StatusInternalServerError {
return errors.New(resp.Status)
}
return fmt.Errorf("can not connect remote err code: %d", resp.StatusCode)
}
func NewHttp(option HttpOption) *Http {
var tlsConfig *tls.Config
if option.TLS {
tlsConfig = &tls.Config{
InsecureSkipVerify: option.SkipCertVerify,
ClientSessionCache: getClientSessionCache(),
ServerName: option.Server,
}
}
return &Http{
Base: &Base{
name: option.Name,
tp: C.Http,
},
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
user: option.UserName,
pass: option.Password,
tls: option.TLS,
skipCertVerify: option.SkipCertVerify,
tlsConfig: tlsConfig,
}
}

View File

@ -0,0 +1,172 @@
package adapters
import (
"context"
"encoding/json"
"errors"
"net"
"sync"
"time"
"github.com/Dreamacro/clash/common/murmur3"
C "github.com/Dreamacro/clash/constant"
"golang.org/x/net/publicsuffix"
)
type LoadBalance struct {
*Base
proxies []C.Proxy
maxRetry int
rawURL string
interval time.Duration
done chan struct{}
}
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)
}
}()
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
buckets := int32(len(lb.proxies))
for i := 0; i < lb.maxRetry; i, key = i+1, key+1 {
idx := jumpHash(key, buckets)
proxy := lb.proxies[idx]
if proxy.Alive() {
c, err = proxy.DialContext(ctx, metadata)
return
}
}
c, err = lb.proxies[0].DialContext(ctx, metadata)
return
}
func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, addr net.Addr, err error) {
defer func() {
if err == nil {
pc.AppendToChains(lb)
}
}()
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
buckets := int32(len(lb.proxies))
for i := 0; i < lb.maxRetry; i, key = i+1, key+1 {
idx := jumpHash(key, buckets)
proxy := lb.proxies[idx]
if proxy.Alive() {
return proxy.DialUDP(metadata)
}
}
return lb.proxies[0].DialUDP(metadata)
}
func (lb *LoadBalance) SupportUDP() bool {
return true
}
func (lb *LoadBalance) Destroy() {
lb.done <- struct{}{}
}
func (lb *LoadBalance) validTest() {
wg := sync.WaitGroup{}
wg.Add(len(lb.proxies))
for _, p := range lb.proxies {
go func(p C.Proxy) {
p.URLTest(context.Background(), lb.rawURL)
wg.Done()
}(p)
}
wg.Wait()
}
func (lb *LoadBalance) loop() {
tick := time.NewTicker(lb.interval)
go lb.validTest()
Loop:
for {
select {
case <-tick.C:
go lb.validTest()
case <-lb.done:
break Loop
}
}
}
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,
})
}
type LoadBalanceOption struct {
Name string `proxy:"name"`
Proxies []string `proxy:"proxies"`
URL string `proxy:"url"`
Interval int `proxy:"interval"`
}
func NewLoadBalance(option LoadBalanceOption, proxies []C.Proxy) (*LoadBalance, error) {
if len(proxies) == 0 {
return nil, errors.New("Provide at least one proxy")
}
interval := time.Duration(option.Interval) * time.Second
lb := &LoadBalance{
Base: &Base{
name: option.Name,
tp: C.LoadBalance,
},
proxies: proxies,
maxRetry: 3,
rawURL: option.URL,
interval: interval,
done: make(chan struct{}),
}
go lb.loop()
return lb, nil
}

View File

@ -1,6 +1,7 @@
package adapters
import (
"context"
"io"
"net"
"time"
@ -8,42 +9,27 @@ import (
C "github.com/Dreamacro/clash/constant"
)
// RejectAdapter is a reject connected adapter
type RejectAdapter struct {
conn net.Conn
}
// Close is used to close connection
func (r *RejectAdapter) Close() {}
// Conn is used to http request
func (r *RejectAdapter) Conn() net.Conn {
return r.conn
}
type Reject struct {
*Base
}
func (r *Reject) Name() string {
return "REJECT"
}
func (r *Reject) Type() C.AdapterType {
return C.Reject
}
func (r *Reject) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
return &RejectAdapter{conn: &NopConn{}}, nil
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
return newConn(&NopConn{}, r), nil
}
func NewReject() *Reject {
return &Reject{}
return &Reject{
Base: &Base{
name: "REJECT",
tp: C.Reject,
},
}
}
type NopConn struct{}
func (rw *NopConn) Read(b []byte) (int, error) {
return len(b), nil
return 0, io.EOF
}
func (rw *NopConn) Write(b []byte) (int, error) {

View File

@ -1,16 +1,19 @@
package adapters
import (
"context"
"encoding/json"
"errors"
"sort"
"net"
C "github.com/Dreamacro/clash/constant"
)
type Selector struct {
name string
selected C.Proxy
proxies map[string]C.Proxy
*Base
selected C.Proxy
proxies map[string]C.Proxy
proxyList []string
}
type SelectorOption struct {
@ -18,31 +21,38 @@ type SelectorOption struct {
Proxies []string `proxy:"proxies"`
}
func (s *Selector) Name() string {
return s.name
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
c, err := s.selected.DialContext(ctx, metadata)
if err == nil {
c.AppendToChains(s)
}
return c, err
}
func (s *Selector) Type() C.AdapterType {
return C.Selector
func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
pc, addr, err := s.selected.DialUDP(metadata)
if err == nil {
pc.AppendToChains(s)
}
return pc, addr, err
}
func (s *Selector) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
return s.selected.Generator(metadata)
func (s *Selector) SupportUDP() bool {
return s.selected.SupportUDP()
}
func (s *Selector) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"type": s.Type().String(),
"now": s.Now(),
"all": s.proxyList,
})
}
func (s *Selector) Now() string {
return s.selected.Name()
}
func (s *Selector) All() []string {
var all []string
for k := range s.proxies {
all = append(all, k)
}
sort.Strings(all)
return all
}
func (s *Selector) Set(name string) error {
proxy, exist := s.proxies[name]
if !exist {
@ -58,14 +68,20 @@ func NewSelector(name string, proxies []C.Proxy) (*Selector, error) {
}
mapping := make(map[string]C.Proxy)
for _, proxy := range proxies {
proxyList := make([]string, len(proxies))
for idx, proxy := range proxies {
mapping[proxy.Name()] = proxy
proxyList[idx] = proxy.Name()
}
s := &Selector{
name: name,
proxies: mapping,
selected: proxies[0],
Base: &Base{
name: name,
tp: C.Selector,
},
proxies: mapping,
selected: proxies[0],
proxyList: proxyList,
}
return s, nil
}

View File

@ -1,78 +1,115 @@
package adapters
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net"
"strconv"
"github.com/Dreamacro/clash/component/simple-obfs"
"github.com/Dreamacro/clash/common/structure"
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"
"github.com/Dreamacro/go-shadowsocks2/core"
"github.com/Dreamacro/go-shadowsocks2/socks"
)
// ShadowsocksAdapter is a shadowsocks adapter
type ShadowsocksAdapter struct {
conn net.Conn
}
// Close is used to close connection
func (ss *ShadowsocksAdapter) Close() {
ss.conn.Close()
}
func (ss *ShadowsocksAdapter) Conn() net.Conn {
return ss.conn
}
type ShadowSocks struct {
server string
name string
obfs string
obfsHost string
cipher core.Cipher
*Base
server string
cipher core.Cipher
// obfs
obfsMode string
obfsOption *simpleObfsOption
v2rayOption *v2rayObfs.Option
}
type ShadowSocksOption struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Password string `proxy:"password"`
Cipher string `proxy:"cipher"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Password string `proxy:"password"`
Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"`
Plugin string `proxy:"plugin,omitempty"`
PluginOpts map[string]interface{} `proxy:"plugin-opts,omitempty"`
// deprecated when bump to 1.0
Obfs string `proxy:"obfs,omitempty"`
ObfsHost string `proxy:"obfs-host,omitempty"`
}
func (ss *ShadowSocks) Name() string {
return ss.name
type simpleObfsOption struct {
Mode string `obfs:"mode"`
Host string `obfs:"host,omitempty"`
}
func (ss *ShadowSocks) Type() C.AdapterType {
return C.Shadowsocks
type v2rayObfsOption struct {
Mode string `obfs:"mode"`
Host string `obfs:"host,omitempty"`
Path string `obfs:"path,omitempty"`
TLS bool `obfs:"tls,omitempty"`
Headers map[string]string `obfs:"headers,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
Mux bool `obfs:"mux,omitempty"`
}
func (ss *ShadowSocks) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
c, err := net.DialTimeout("tcp", ss.server, tcpTimeout)
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
c, err := dialContext(ctx, "tcp", ss.server)
if err != nil {
return nil, fmt.Errorf("%s connect error", ss.server)
return nil, fmt.Errorf("%s connect error: %s", ss.server, err.Error())
}
tcpKeepAlive(c)
switch ss.obfs {
switch ss.obfsMode {
case "tls":
c = obfs.NewTLSObfs(c, ss.obfsHost)
c = obfs.NewTLSObfs(c, ss.obfsOption.Host)
case "http":
_, port, _ := net.SplitHostPort(ss.server)
c = obfs.NewHTTPObfs(c, ss.obfsHost, port)
c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port)
case "websocket":
var err error
c, err = v2rayObfs.NewV2rayObfs(c, ss.v2rayOption)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", ss.server, err.Error())
}
}
c = ss.cipher.StreamConn(c)
_, err = c.Write(serializesSocksAddr(metadata))
return &ShadowsocksAdapter{conn: c}, err
return newConn(c, ss), err
}
func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
pc, err := net.ListenPacket("udp", "")
if err != nil {
return nil, nil, err
}
addr, err := resolveUDPAddr("udp", ss.server)
if err != nil {
return nil, nil, err
}
targetAddr := socks5.ParseAddr(metadata.RemoteAddress())
if targetAddr == nil {
return nil, nil, fmt.Errorf("parse address error: %v:%v", metadata.String(), metadata.DstPort)
}
pc = ss.cipher.PacketConn(pc)
return newPacketConn(&ssUDPConn{PacketConn: pc, rAddr: targetAddr}, ss), addr, nil
}
func (ss *ShadowSocks) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": ss.Type().String(),
})
}
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
server := fmt.Sprintf("%s:%d", option.Server, option.Port)
server := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
cipher := option.Cipher
password := option.Password
ciph, err := core.PickCipher(cipher, nil, password)
@ -80,37 +117,92 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
return nil, fmt.Errorf("ss %s initialize error: %s", server, err.Error())
}
obfs := option.Obfs
obfsHost := "bing.com"
if option.ObfsHost != "" {
obfsHost = option.ObfsHost
var v2rayOption *v2rayObfs.Option
var obfsOption *simpleObfsOption
obfsMode := ""
// forward compatibility before 1.0
if option.Obfs != "" {
obfsMode = option.Obfs
obfsOption = &simpleObfsOption{
Host: "bing.com",
}
if option.ObfsHost != "" {
obfsOption.Host = option.ObfsHost
}
}
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
if option.Plugin == "obfs" {
opts := simpleObfsOption{Host: "bing.com"}
if err := decoder.Decode(option.PluginOpts, &opts); err != nil {
return nil, fmt.Errorf("ss %s initialize obfs error: %s", server, err.Error())
}
if opts.Mode != "tls" && opts.Mode != "http" {
return nil, fmt.Errorf("ss %s obfs mode error: %s", server, opts.Mode)
}
obfsMode = opts.Mode
obfsOption = &opts
} else if option.Plugin == "v2ray-plugin" {
opts := v2rayObfsOption{Host: "bing.com", Mux: true}
if err := decoder.Decode(option.PluginOpts, &opts); err != nil {
return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %s", server, err.Error())
}
if opts.Mode != "websocket" {
return nil, fmt.Errorf("ss %s obfs mode error: %s", server, opts.Mode)
}
obfsMode = opts.Mode
var tlsConfig *tls.Config
if opts.TLS {
tlsConfig = &tls.Config{
ServerName: opts.Host,
InsecureSkipVerify: opts.SkipCertVerify,
ClientSessionCache: getClientSessionCache(),
}
}
v2rayOption = &v2rayObfs.Option{
Host: opts.Host,
Path: opts.Path,
Headers: opts.Headers,
TLSConfig: tlsConfig,
Mux: opts.Mux,
}
}
return &ShadowSocks{
server: server,
name: option.Name,
cipher: ciph,
obfs: obfs,
obfsHost: obfsHost,
Base: &Base{
name: option.Name,
tp: C.Shadowsocks,
udp: option.UDP,
},
server: server,
cipher: ciph,
obfsMode: obfsMode,
v2rayOption: v2rayOption,
obfsOption: obfsOption,
}, nil
}
func serializesSocksAddr(metadata *C.Metadata) []byte {
var buf [][]byte
aType := uint8(metadata.AddrType)
p, _ := strconv.Atoi(metadata.Port)
port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch metadata.AddrType {
case socks.AtypDomainName:
len := uint8(len(metadata.Host))
host := []byte(metadata.Host)
buf = [][]byte{{aType, len}, host, port}
case socks.AtypIPv4:
host := metadata.IP.To4()
buf = [][]byte{{aType}, host, port}
case socks.AtypIPv6:
host := metadata.IP.To16()
buf = [][]byte{{aType}, host, port}
}
return bytes.Join(buf, nil)
type ssUDPConn struct {
net.PacketConn
rAddr socks5.Addr
}
func (uc *ssUDPConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
packet, err := socks5.EncodeUDPPacket(uc.rAddr, b)
if err != nil {
return
}
return uc.PacketConn.WriteTo(packet[3:], addr)
}
func (uc *ssUDPConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, a, e := uc.PacketConn.ReadFrom(b)
addr := socks5.SplitAddr(b[:n])
copy(b, b[len(addr):])
return n - len(addr), a, e
}

View File

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

View File

@ -1,112 +1,182 @@
package adapters
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"strconv"
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/go-shadowsocks2/socks"
)
// Socks5Adapter is a shadowsocks adapter
type Socks5Adapter struct {
conn net.Conn
}
// Close is used to close connection
func (ss *Socks5Adapter) Close() {
ss.conn.Close()
}
func (ss *Socks5Adapter) Conn() net.Conn {
return ss.conn
}
type Socks5 struct {
*Base
addr string
name string
user string
pass string
tls bool
skipCertVerify bool
tlsConfig *tls.Config
}
type Socks5Option struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UserName string `proxy:"username,omitempty"`
Password string `proxy:"password,omitempty"`
TLS bool `proxy:"tls,omitempty"`
UDP bool `proxy:"udp,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
}
func (ss *Socks5) Name() string {
return ss.name
}
func (ss *Socks5) Type() C.AdapterType {
return C.Socks5
}
func (ss *Socks5) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
c, err := net.DialTimeout("tcp", ss.addr, tcpTimeout)
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
c, err := dialContext(ctx, "tcp", ss.addr)
if err == nil && ss.tls {
tlsConfig := tls.Config{
InsecureSkipVerify: ss.skipCertVerify,
MaxVersion: tls.VersionTLS12,
}
c = tls.Client(c, &tlsConfig)
cc := tls.Client(c, ss.tlsConfig)
err = cc.Handshake()
c = cc
}
if err != nil {
return nil, fmt.Errorf("%s connect error", ss.addr)
}
tcpKeepAlive(c)
if err := ss.shakeHand(metadata, c); err != nil {
var user *socks5.User
if ss.user != "" {
user = &socks5.User{
Username: ss.user,
Password: ss.pass,
}
}
if _, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdConnect, user); err != nil {
return nil, err
}
return &Socks5Adapter{conn: c}, nil
return newConn(c, ss), nil
}
func (ss *Socks5) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
buf := make([]byte, socks.MaxAddrLen)
// VER, CMD, RSV
_, err := rw.Write([]byte{5, 1, 0})
func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, _ net.Addr, err error) {
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
defer cancel()
c, err := dialContext(ctx, "tcp", ss.addr)
if err != nil {
return err
err = fmt.Errorf("%s connect error", ss.addr)
return
}
if _, err := io.ReadFull(rw, buf[:2]); err != nil {
return err
if ss.tls {
cc := tls.Client(c, ss.tlsConfig)
err = cc.Handshake()
c = cc
}
if buf[0] != 5 {
return errors.New("SOCKS version error")
} else if buf[1] != 0 {
return errors.New("SOCKS need auth")
defer func() {
if err != nil {
c.Close()
}
}()
tcpKeepAlive(c)
var user *socks5.User
if ss.user != "" {
user = &socks5.User{
Username: ss.user,
Password: ss.pass,
}
}
// VER, CMD, RSV, ADDR
if _, err := rw.Write(bytes.Join([][]byte{{5, 1, 0}, serializesSocksAddr(metadata)}, []byte(""))); err != nil {
return err
bindAddr, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdUDPAssociate, user)
if err != nil {
err = fmt.Errorf("%v client hanshake error", err)
return
}
if _, err := io.ReadFull(rw, buf[:10]); err != nil {
return err
addr, err := net.ResolveUDPAddr("udp", bindAddr.String())
if err != nil {
return
}
return nil
targetAddr := socks5.ParseAddr(metadata.RemoteAddress())
if targetAddr == nil {
return nil, nil, fmt.Errorf("parse address error: %v:%v", metadata.String(), metadata.DstPort)
}
pc, err := net.ListenPacket("udp", "")
if err != nil {
return
}
go func() {
io.Copy(ioutil.Discard, c)
c.Close()
// A UDP association terminates when the TCP connection that the UDP
// ASSOCIATE request arrived on terminates. RFC1928
pc.Close()
}()
return newPacketConn(&socksUDPConn{PacketConn: pc, rAddr: targetAddr, tcpConn: c}, ss), addr, nil
}
func NewSocks5(option Socks5Option) *Socks5 {
var tlsConfig *tls.Config
if option.TLS {
tlsConfig = &tls.Config{
InsecureSkipVerify: option.SkipCertVerify,
ClientSessionCache: getClientSessionCache(),
ServerName: option.Server,
}
}
return &Socks5{
addr: fmt.Sprintf("%s:%d", option.Server, option.Port),
name: option.Name,
Base: &Base{
name: option.Name,
tp: C.Socks5,
udp: option.UDP,
},
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
user: option.UserName,
pass: option.Password,
tls: option.TLS,
skipCertVerify: option.SkipCertVerify,
tlsConfig: tlsConfig,
}
}
type socksUDPConn struct {
net.PacketConn
rAddr socks5.Addr
tcpConn net.Conn
}
func (uc *socksUDPConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
packet, err := socks5.EncodeUDPPacket(uc.rAddr, b)
if err != nil {
return
}
return uc.PacketConn.WriteTo(packet, addr)
}
func (uc *socksUDPConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, a, e := uc.PacketConn.ReadFrom(b)
if e != nil {
return 0, nil, e
}
addr, payload, err := socks5.DecodeUDPPacket(b)
if err != nil {
return 0, nil, err
}
// due to DecodeUDPPacket is mutable, record addr length
addrLength := len(addr)
copy(b, payload)
return n - addrLength - 3, a, nil
}
func (uc *socksUDPConn) Close() error {
uc.tcpConn.Close()
return uc.PacketConn.Close()
}

View File

@ -1,16 +1,19 @@
package adapters
import (
"context"
"encoding/json"
"errors"
"sync"
"net"
"sync/atomic"
"time"
"github.com/Dreamacro/clash/common/picker"
C "github.com/Dreamacro/clash/constant"
)
type URLTest struct {
name string
*Base
proxies []C.Proxy
rawURL string
fast C.Proxy
@ -26,27 +29,47 @@ type URLTestOption struct {
Interval int `proxy:"interval"`
}
func (u *URLTest) Name() string {
return u.name
}
func (u *URLTest) Type() C.AdapterType {
return C.URLTest
}
func (u *URLTest) Now() string {
return u.fast.Name()
}
func (u *URLTest) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
a, err := u.fast.Generator(metadata)
if err != nil {
go u.speedTest()
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
for i := 0; i < 3; i++ {
c, err = u.fast.DialContext(ctx, metadata)
if err == nil {
c.AppendToChains(u)
return
}
u.fallback()
}
return a, err
return
}
func (u *URLTest) Close() {
func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
pc, addr, err := u.fast.DialUDP(metadata)
if err == nil {
pc.AppendToChains(u)
}
return pc, addr, err
}
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 (u *URLTest) Destroy() {
u.done <- struct{}{}
}
@ -64,42 +87,49 @@ Loop:
}
}
func (u *URLTest) fallback() {
fast := u.proxies[0]
min := fast.LastDelay()
for _, proxy := range u.proxies[1:] {
if !proxy.Alive() {
continue
}
delay := proxy.LastDelay()
if delay < min {
fast = proxy
min = delay
}
}
u.fast = fast
}
func (u *URLTest) speedTest() {
if atomic.AddInt32(&u.once, 1) != 1 {
if !atomic.CompareAndSwapInt32(&u.once, 0, 1) {
return
}
defer atomic.StoreInt32(&u.once, 0)
wg := sync.WaitGroup{}
wg.Add(len(u.proxies))
c := make(chan interface{})
fast := selectFast(c)
timer := time.NewTimer(u.interval)
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
defer cancel()
picker := picker.WithoutAutoCancel(ctx)
for _, p := range u.proxies {
go func(p C.Proxy) {
_, err := DelayTest(p, u.rawURL)
if err == nil {
c <- p
proxy := p
picker.Go(func() (interface{}, error) {
_, err := proxy.URLTest(ctx, u.rawURL)
if err != nil {
return nil, err
}
wg.Done()
}(p)
return proxy, nil
})
}
go func() {
wg.Wait()
close(c)
}()
select {
case <-timer.C:
// Wait for fast to return or close.
<-fast
case p, open := <-fast:
if open {
u.fast = p.(C.Proxy)
}
fast := picker.WaitWithoutCancel()
if fast != nil {
u.fast = fast.(C.Proxy)
}
picker.Wait()
}
func NewURLTest(option URLTestOption, proxies []C.Proxy) (*URLTest, error) {
@ -113,7 +143,10 @@ func NewURLTest(option URLTestOption, proxies []C.Proxy) (*URLTest, error) {
interval := time.Duration(option.Interval) * time.Second
urlTest := &URLTest{
name: option.Name,
Base: &Base{
name: option.Name,
tp: C.URLTest,
},
proxies: proxies[:],
rawURL: option.URL,
fast: proxies[0],

View File

@ -1,51 +1,29 @@
package adapters
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"net/url"
"strconv"
"sync"
"time"
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/dns"
)
const (
tcpTimeout = 5 * time.Second
)
// DelayTest get the delay for the specified URL
func DelayTest(proxy C.Proxy, url string) (t int16, err error) {
addr, err := urlToMetadata(url)
if err != nil {
return
}
start := time.Now()
instance, err := proxy.Generator(&addr)
if err != nil {
return
}
defer instance.Close()
transport := &http.Transport{
Dial: func(string, string) (net.Conn, error) {
return instance.Conn(), nil
},
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
client := http.Client{Transport: transport}
resp, err := client.Get(url)
if err != nil {
return
}
resp.Body.Close()
t = int16(time.Since(start) / time.Millisecond)
return
}
var (
globalClientSessionCache tls.ClientSessionCache
once sync.Once
)
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
u, err := url.Parse(rawURL)
@ -68,30 +46,135 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
addr = C.Metadata{
AddrType: C.AtypDomainName,
Host: u.Hostname(),
IP: nil,
Port: port,
DstIP: nil,
DstPort: port,
}
return
}
func selectFast(in chan interface{}) chan interface{} {
out := make(chan interface{})
go func() {
p, open := <-in
if open {
out <- p
}
close(out)
for range in {
}
}()
return out
}
func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
tcp.SetKeepAlive(true)
tcp.SetKeepAlivePeriod(30 * time.Second)
}
}
func getClientSessionCache() tls.ClientSessionCache {
once.Do(func() {
globalClientSessionCache = tls.NewLRUClientSessionCache(128)
})
return globalClientSessionCache
}
func serializesSocksAddr(metadata *C.Metadata) []byte {
var buf [][]byte
aType := uint8(metadata.AddrType)
p, _ := strconv.Atoi(metadata.DstPort)
port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch metadata.AddrType {
case socks5.AtypDomainName:
len := uint8(len(metadata.Host))
host := []byte(metadata.Host)
buf = [][]byte{{aType, len}, host, port}
case socks5.AtypIPv4:
host := metadata.DstIP.To4()
buf = [][]byte{{aType}, host, port}
case socks5.AtypIPv6:
host := metadata.DstIP.To16()
buf = [][]byte{{aType}, host, port}
}
return bytes.Join(buf, nil)
}
func dialContext(ctx context.Context, network, address string) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
dialer := net.Dialer{}
returned := make(chan struct{})
defer close(returned)
type dialResult struct {
net.Conn
error
resolved bool
ipv6 bool
done bool
}
results := make(chan dialResult)
var primary, fallback dialResult
startRacer := func(ctx context.Context, host string, ipv6 bool) {
result := dialResult{ipv6: ipv6, done: true}
defer func() {
select {
case results <- result:
case <-returned:
if result.Conn != nil {
result.Conn.Close()
}
}
}()
var ip net.IP
if ipv6 {
ip, result.error = dns.ResolveIPv6(host)
} else {
ip, result.error = dns.ResolveIPv4(host)
}
if result.error != nil {
return
}
result.resolved = true
if ipv6 {
result.Conn, result.error = dialer.DialContext(ctx, "tcp6", net.JoinHostPort(ip.String(), port))
} else {
result.Conn, result.error = dialer.DialContext(ctx, "tcp4", net.JoinHostPort(ip.String(), port))
}
}
go startRacer(ctx, host, false)
go startRacer(ctx, host, true)
for {
select {
case res := <-results:
if res.error == nil {
return res.Conn, nil
}
if !res.ipv6 {
primary = res
} else {
fallback = res
}
if primary.done && fallback.done {
if primary.resolved {
return nil, primary.error
} else if fallback.resolved {
return nil, fallback.error
} else {
return nil, primary.error
}
}
}
}
}
func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
ip, err := dns.ResolveIP(host)
if err != nil {
return nil, err
}
return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port))
}

View File

@ -1,6 +1,7 @@
package adapters
import (
"context"
"fmt"
"net"
"strconv"
@ -10,76 +11,78 @@ import (
C "github.com/Dreamacro/clash/constant"
)
// VmessAdapter is a vmess adapter
type VmessAdapter struct {
conn net.Conn
}
// Close is used to close connection
func (v *VmessAdapter) Close() {
v.conn.Close()
}
func (v *VmessAdapter) Conn() net.Conn {
return v.conn
}
type Vmess struct {
name string
*Base
server string
client *vmess.Client
}
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"`
Network string `proxy:"network,omitempty"`
WSPath string `proxy:"ws-path,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
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"`
WSPath string `proxy:"ws-path,omitempty"`
WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
}
func (ss *Vmess) Name() string {
return ss.name
}
func (ss *Vmess) Type() C.AdapterType {
return C.Vmess
}
func (ss *Vmess) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
c, err := net.DialTimeout("tcp", ss.server, tcpTimeout)
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
c, err := dialContext(ctx, "tcp", v.server)
if err != nil {
return nil, fmt.Errorf("%s connect error", ss.server)
return nil, fmt.Errorf("%s connect error", v.server)
}
tcpKeepAlive(c)
c, err = ss.client.New(c, parseVmessAddr(metadata))
return &VmessAdapter{conn: c}, err
c, err = v.client.New(c, parseVmessAddr(metadata))
return newConn(c, v), err
}
func (v *Vmess) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
defer cancel()
c, err := dialContext(ctx, "tcp", v.server)
if err != nil {
return nil, nil, fmt.Errorf("%s connect error", v.server)
}
tcpKeepAlive(c)
c, err = v.client.New(c, parseVmessAddr(metadata))
if err != nil {
return nil, nil, fmt.Errorf("new vmess client error: %v", err)
}
return newPacketConn(&vmessUDPConn{Conn: c}, v), c.RemoteAddr(), 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,
TLS: option.TLS,
Host: fmt.Sprintf("%s:%d", option.Server, option.Port),
NetWork: option.Network,
WebSocketPath: option.WSPath,
SkipCertVerify: option.SkipCertVerify,
UUID: option.UUID,
AlterID: uint16(option.AlterID),
Security: security,
TLS: option.TLS,
HostName: option.Server,
Port: strconv.Itoa(option.Port),
NetWork: option.Network,
WebSocketPath: option.WSPath,
WebSocketHeaders: option.WSHeaders,
SkipCertVerify: option.SkipCertVerify,
SessionCache: getClientSessionCache(),
})
if err != nil {
return nil, err
}
return &Vmess{
name: option.Name,
server: fmt.Sprintf("%s:%d", option.Server, option.Port),
Base: &Base{
name: option.Name,
tp: C.Vmess,
udp: true,
},
server: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
client: client,
}, nil
}
@ -91,11 +94,11 @@ func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
case C.AtypIPv4:
addrType = byte(vmess.AtypIPv4)
addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.IP.To4())
copy(addr[:], metadata.DstIP.To4())
case C.AtypIPv6:
addrType = byte(vmess.AtypIPv6)
addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.IP.To16())
copy(addr[:], metadata.DstIP.To16())
case C.AtypDomainName:
addrType = byte(vmess.AtypDomainName)
addr = make([]byte, len(metadata.Host)+1)
@ -103,10 +106,24 @@ func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
copy(addr[1:], []byte(metadata.Host))
}
port, _ := strconv.Atoi(metadata.Port)
port, _ := strconv.Atoi(metadata.DstPort)
return &vmess.DstAddr{
UDP: metadata.NetWork == C.UDP,
AddrType: addrType,
Addr: addr,
Port: uint(port),
}
}
type vmessUDPConn struct {
net.Conn
}
func (uc *vmessUDPConn) WriteTo(b []byte, addr net.Addr) (int, error) {
return uc.Conn.Write(b)
}
func (uc *vmessUDPConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, err := uc.Conn.Read(b)
return n, uc.RemoteAddr(), err
}

106
common/cache/cache.go vendored Normal file
View File

@ -0,0 +1,106 @@
package cache
import (
"runtime"
"sync"
"time"
)
// Cache store element with a expired time
type Cache struct {
*cache
}
type cache struct {
mapping sync.Map
janitor *janitor
}
type element struct {
Expired time.Time
Payload interface{}
}
// Put element in Cache with its ttl
func (c *cache) Put(key interface{}, payload interface{}, ttl time.Duration) {
c.mapping.Store(key, &element{
Payload: payload,
Expired: time.Now().Add(ttl),
})
}
// Get element in Cache, and drop when it expired
func (c *cache) Get(key interface{}) interface{} {
item, exist := c.mapping.Load(key)
if !exist {
return nil
}
elm := item.(*element)
// expired
if time.Since(elm.Expired) > 0 {
c.mapping.Delete(key)
return nil
}
return elm.Payload
}
// GetWithExpire element in Cache with Expire Time
func (c *cache) GetWithExpire(key interface{}) (payload interface{}, expired time.Time) {
item, exist := c.mapping.Load(key)
if !exist {
return
}
elm := item.(*element)
// expired
if time.Since(elm.Expired) > 0 {
c.mapping.Delete(key)
return
}
return elm.Payload, elm.Expired
}
func (c *cache) cleanup() {
c.mapping.Range(func(k, v interface{}) bool {
key := k.(string)
elm := v.(*element)
if time.Since(elm.Expired) > 0 {
c.mapping.Delete(key)
}
return true
})
}
type janitor struct {
interval time.Duration
stop chan struct{}
}
func (j *janitor) process(c *cache) {
ticker := time.NewTicker(j.interval)
for {
select {
case <-ticker.C:
c.cleanup()
case <-j.stop:
ticker.Stop()
return
}
}
}
func stopJanitor(c *Cache) {
c.janitor.stop <- struct{}{}
}
// New return *Cache
func New(interval time.Duration) *Cache {
j := &janitor{
interval: interval,
stop: make(chan struct{}),
}
c := &cache{janitor: j}
go j.process(c)
C := &Cache{c}
runtime.SetFinalizer(C, stopJanitor)
return C
}

70
common/cache/cache_test.go vendored Normal file
View File

@ -0,0 +1,70 @@
package cache
import (
"runtime"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCache_Basic(t *testing.T) {
interval := 200 * time.Millisecond
ttl := 20 * time.Millisecond
c := New(interval)
c.Put("int", 1, ttl)
c.Put("string", "a", ttl)
i := c.Get("int")
assert.Equal(t, i.(int), 1, "should recv 1")
s := c.Get("string")
assert.Equal(t, s.(string), "a", "should recv 'a'")
}
func TestCache_TTL(t *testing.T) {
interval := 200 * time.Millisecond
ttl := 20 * time.Millisecond
now := time.Now()
c := New(interval)
c.Put("int", 1, ttl)
c.Put("int2", 2, ttl)
i := c.Get("int")
_, expired := c.GetWithExpire("int2")
assert.Equal(t, i.(int), 1, "should recv 1")
assert.True(t, now.Before(expired))
time.Sleep(ttl * 2)
i = c.Get("int")
j, _ := c.GetWithExpire("int2")
assert.Nil(t, i, "should recv nil")
assert.Nil(t, j, "should recv nil")
}
func TestCache_AutoCleanup(t *testing.T) {
interval := 10 * time.Millisecond
ttl := 15 * time.Millisecond
c := New(interval)
c.Put("int", 1, ttl)
time.Sleep(ttl * 2)
i := c.Get("int")
j, _ := c.GetWithExpire("int")
assert.Nil(t, i, "should recv nil")
assert.Nil(t, j, "should recv nil")
}
func TestCache_AutoGC(t *testing.T) {
sign := make(chan struct{})
go func() {
interval := 10 * time.Millisecond
ttl := 15 * time.Millisecond
c := New(interval)
c.Put("int", 1, ttl)
sign <- struct{}{}
}()
<-sign
runtime.GC()
}

157
common/cache/lrucache.go vendored Normal file
View File

@ -0,0 +1,157 @@
package cache
// Modified by https://github.com/die-net/lrucache
import (
"container/list"
"sync"
"time"
)
// Option is part of Functional Options Pattern
type Option func(*LruCache)
// WithUpdateAgeOnGet update expires when Get element
func WithUpdateAgeOnGet() Option {
return func(l *LruCache) {
l.updateAgeOnGet = true
}
}
// WithAge defined element max age (second)
func WithAge(maxAge int64) Option {
return func(l *LruCache) {
l.maxAge = maxAge
}
}
// WithSize defined max length of LruCache
func WithSize(maxSize int) Option {
return func(l *LruCache) {
l.maxSize = maxSize
}
}
// LruCache is a thread-safe, in-memory lru-cache that evicts the
// least recently used entries from memory when (if set) the entries are
// older than maxAge (in seconds). Use the New constructor to create one.
type LruCache struct {
maxAge int64
maxSize int
mu sync.Mutex
cache map[interface{}]*list.Element
lru *list.List // Front is least-recent
updateAgeOnGet bool
}
// NewLRUCache creates an LruCache
func NewLRUCache(options ...Option) *LruCache {
lc := &LruCache{
lru: list.New(),
cache: make(map[interface{}]*list.Element),
}
for _, option := range options {
option(lc)
}
return lc
}
// Get returns the interface{} representation of a cached response and a bool
// set to true if the key was found.
func (c *LruCache) Get(key interface{}) (interface{}, bool) {
c.mu.Lock()
defer c.mu.Unlock()
le, ok := c.cache[key]
if !ok {
return nil, false
}
if c.maxAge > 0 && le.Value.(*entry).expires <= time.Now().Unix() {
c.deleteElement(le)
c.maybeDeleteOldest()
return nil, false
}
c.lru.MoveToBack(le)
entry := le.Value.(*entry)
if c.maxAge > 0 && c.updateAgeOnGet {
entry.expires = time.Now().Unix() + c.maxAge
}
value := entry.value
return value, true
}
// Exist returns if key exist in cache but not put item to the head of linked list
func (c *LruCache) Exist(key interface{}) bool {
c.mu.Lock()
defer c.mu.Unlock()
_, ok := c.cache[key]
return ok
}
// Set stores the interface{} representation of a response for a given key.
func (c *LruCache) Set(key interface{}, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
expires := int64(0)
if c.maxAge > 0 {
expires = time.Now().Unix() + c.maxAge
}
if le, ok := c.cache[key]; ok {
c.lru.MoveToBack(le)
e := le.Value.(*entry)
e.value = value
e.expires = expires
} else {
e := &entry{key: key, value: value, expires: expires}
c.cache[key] = c.lru.PushBack(e)
if c.maxSize > 0 {
if len := c.lru.Len(); len > c.maxSize {
c.deleteElement(c.lru.Front())
}
}
}
c.maybeDeleteOldest()
}
// Delete removes the value associated with a key.
func (c *LruCache) Delete(key string) {
c.mu.Lock()
if le, ok := c.cache[key]; ok {
c.deleteElement(le)
}
c.mu.Unlock()
}
func (c *LruCache) maybeDeleteOldest() {
if c.maxAge > 0 {
now := time.Now().Unix()
for le := c.lru.Front(); le != nil && le.Value.(*entry).expires <= now; le = c.lru.Front() {
c.deleteElement(le)
}
}
}
func (c *LruCache) deleteElement(le *list.Element) {
c.lru.Remove(le)
e := le.Value.(*entry)
delete(c.cache, e.key)
}
type entry struct {
key interface{}
value interface{}
expires int64
}

117
common/cache/lrucache_test.go vendored Normal file
View File

@ -0,0 +1,117 @@
package cache
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
var entries = []struct {
key string
value string
}{
{"1", "one"},
{"2", "two"},
{"3", "three"},
{"4", "four"},
{"5", "five"},
}
func TestLRUCache(t *testing.T) {
c := NewLRUCache()
for _, e := range entries {
c.Set(e.key, e.value)
}
c.Delete("missing")
_, ok := c.Get("missing")
assert.False(t, ok)
for _, e := range entries {
value, ok := c.Get(e.key)
if assert.True(t, ok) {
assert.Equal(t, e.value, value.(string))
}
}
for _, e := range entries {
c.Delete(e.key)
_, ok := c.Get(e.key)
assert.False(t, ok)
}
}
func TestLRUMaxAge(t *testing.T) {
c := NewLRUCache(WithAge(86400))
now := time.Now().Unix()
expected := now + 86400
// Add one expired entry
c.Set("foo", "bar")
c.lru.Back().Value.(*entry).expires = now
// Reset
c.Set("foo", "bar")
e := c.lru.Back().Value.(*entry)
assert.True(t, e.expires >= now)
c.lru.Back().Value.(*entry).expires = now
// Set a few and verify expiration times
for _, s := range entries {
c.Set(s.key, s.value)
e := c.lru.Back().Value.(*entry)
assert.True(t, e.expires >= expected && e.expires <= expected+10)
}
// Make sure we can get them all
for _, s := range entries {
_, ok := c.Get(s.key)
assert.True(t, ok)
}
// Expire all entries
for _, s := range entries {
le, ok := c.cache[s.key]
if assert.True(t, ok) {
le.Value.(*entry).expires = now
}
}
// Get one expired entry, which should clear all expired entries
_, ok := c.Get("3")
assert.False(t, ok)
assert.Equal(t, c.lru.Len(), 0)
}
func TestLRUpdateOnGet(t *testing.T) {
c := NewLRUCache(WithAge(86400), WithUpdateAgeOnGet())
now := time.Now().Unix()
expires := now + 86400/2
// Add one expired entry
c.Set("foo", "bar")
c.lru.Back().Value.(*entry).expires = expires
_, ok := c.Get("foo")
assert.True(t, ok)
assert.True(t, c.lru.Back().Value.(*entry).expires > expires)
}
func TestMaxSize(t *testing.T) {
c := NewLRUCache(WithSize(2))
// Add one expired entry
c.Set("foo", "bar")
_, ok := c.Get("foo")
assert.True(t, ok)
c.Set("bar", "foo")
c.Set("baz", "foo")
_, ok = c.Get("foo")
assert.False(t, ok)
}

50
common/murmur3/murmur.go Normal file
View File

@ -0,0 +1,50 @@
package murmur3
type bmixer interface {
bmix(p []byte) (tail []byte)
Size() (n int)
reset()
}
type digest struct {
clen int // Digested input cumulative length.
tail []byte // 0 to Size()-1 bytes view of `buf'.
buf [16]byte // Expected (but not required) to be Size() large.
seed uint32 // Seed for initializing the hash.
bmixer
}
func (d *digest) BlockSize() int { return 1 }
func (d *digest) Write(p []byte) (n int, err error) {
n = len(p)
d.clen += n
if len(d.tail) > 0 {
// Stick back pending bytes.
nfree := d.Size() - len(d.tail) // nfree ∈ [1, d.Size()-1].
if nfree < len(p) {
// One full block can be formed.
block := append(d.tail, p[:nfree]...)
p = p[nfree:]
_ = d.bmix(block) // No tail.
} else {
// Tail's buf is large enough to prevent reallocs.
p = append(d.tail, p...)
}
}
d.tail = d.bmix(p)
// Keep own copy of the 0 to Size()-1 pending bytes.
nn := copy(d.buf[:], d.tail)
d.tail = d.buf[:nn]
return n, nil
}
func (d *digest) Reset() {
d.clen = 0
d.tail = nil
d.bmixer.reset()
}

145
common/murmur3/murmur32.go Normal file
View File

@ -0,0 +1,145 @@
package murmur3
// https://github.com/spaolacci/murmur3/blob/master/murmur32.go
import (
"hash"
"math/bits"
"unsafe"
)
// Make sure interfaces are correctly implemented.
var (
_ hash.Hash32 = new(digest32)
_ bmixer = new(digest32)
)
const (
c1_32 uint32 = 0xcc9e2d51
c2_32 uint32 = 0x1b873593
)
// digest32 represents a partial evaluation of a 32 bites hash.
type digest32 struct {
digest
h1 uint32 // Unfinalized running hash.
}
// New32 returns new 32-bit hasher
func New32() hash.Hash32 { return New32WithSeed(0) }
// New32WithSeed returns new 32-bit hasher set with explicit seed value
func New32WithSeed(seed uint32) hash.Hash32 {
d := new(digest32)
d.seed = seed
d.bmixer = d
d.Reset()
return d
}
func (d *digest32) Size() int { return 4 }
func (d *digest32) reset() { d.h1 = d.seed }
func (d *digest32) Sum(b []byte) []byte {
h := d.Sum32()
return append(b, byte(h>>24), byte(h>>16), byte(h>>8), byte(h))
}
// Digest as many blocks as possible.
func (d *digest32) bmix(p []byte) (tail []byte) {
h1 := d.h1
nblocks := len(p) / 4
for i := 0; i < nblocks; i++ {
k1 := *(*uint32)(unsafe.Pointer(&p[i*4]))
k1 *= c1_32
k1 = bits.RotateLeft32(k1, 15)
k1 *= c2_32
h1 ^= k1
h1 = bits.RotateLeft32(h1, 13)
h1 = h1*4 + h1 + 0xe6546b64
}
d.h1 = h1
return p[nblocks*d.Size():]
}
func (d *digest32) Sum32() (h1 uint32) {
h1 = d.h1
var k1 uint32
switch len(d.tail) & 3 {
case 3:
k1 ^= uint32(d.tail[2]) << 16
fallthrough
case 2:
k1 ^= uint32(d.tail[1]) << 8
fallthrough
case 1:
k1 ^= uint32(d.tail[0])
k1 *= c1_32
k1 = bits.RotateLeft32(k1, 15)
k1 *= c2_32
h1 ^= k1
}
h1 ^= uint32(d.clen)
h1 ^= h1 >> 16
h1 *= 0x85ebca6b
h1 ^= h1 >> 13
h1 *= 0xc2b2ae35
h1 ^= h1 >> 16
return h1
}
func Sum32(data []byte) uint32 { return Sum32WithSeed(data, 0) }
func Sum32WithSeed(data []byte, seed uint32) uint32 {
h1 := seed
nblocks := len(data) / 4
for i := 0; i < nblocks; i++ {
k1 := *(*uint32)(unsafe.Pointer(&data[i*4]))
k1 *= c1_32
k1 = bits.RotateLeft32(k1, 15)
k1 *= c2_32
h1 ^= k1
h1 = bits.RotateLeft32(h1, 13)
h1 = h1*4 + h1 + 0xe6546b64
}
tail := data[nblocks*4:]
var k1 uint32
switch len(tail) & 3 {
case 3:
k1 ^= uint32(tail[2]) << 16
fallthrough
case 2:
k1 ^= uint32(tail[1]) << 8
fallthrough
case 1:
k1 ^= uint32(tail[0])
k1 *= c1_32
k1 = bits.RotateLeft32(k1, 15)
k1 *= c2_32
h1 ^= k1
}
h1 ^= uint32(len(data))
h1 ^= h1 >> 16
h1 *= 0x85ebca6b
h1 ^= h1 >> 13
h1 *= 0xc2b2ae35
h1 ^= h1 >> 16
return h1
}

89
common/picker/picker.go Normal file
View File

@ -0,0 +1,89 @@
package picker
import (
"context"
"sync"
"time"
)
// Picker provides synchronization, and Context cancelation
// for groups of goroutines working on subtasks of a common task.
// Inspired by errGroup
type Picker struct {
ctx context.Context
cancel func()
wg sync.WaitGroup
once sync.Once
result interface{}
firstDone chan struct{}
}
func newPicker(ctx context.Context, cancel func()) *Picker {
return &Picker{
ctx: ctx,
cancel: cancel,
firstDone: make(chan struct{}, 1),
}
}
// WithContext returns a new Picker and an associated Context derived from ctx.
// and cancel when first element return.
func WithContext(ctx context.Context) (*Picker, context.Context) {
ctx, cancel := context.WithCancel(ctx)
return newPicker(ctx, cancel), ctx
}
// WithTimeout returns a new Picker and an associated Context derived from ctx with timeout.
func WithTimeout(ctx context.Context, timeout time.Duration) (*Picker, context.Context) {
ctx, cancel := context.WithTimeout(ctx, timeout)
return newPicker(ctx, cancel), ctx
}
// WithoutAutoCancel returns a new Picker and an associated Context derived from ctx,
// but it wouldn't cancel context when the first element return.
func WithoutAutoCancel(ctx context.Context) *Picker {
return newPicker(ctx, nil)
}
// Wait blocks until all function calls from the Go method have returned,
// then returns the first nil error result (if any) from them.
func (p *Picker) Wait() interface{} {
p.wg.Wait()
if p.cancel != nil {
p.cancel()
}
return p.result
}
// WaitWithoutCancel blocks until the first result return, if timeout will return nil.
func (p *Picker) WaitWithoutCancel() interface{} {
select {
case <-p.firstDone:
return p.result
case <-p.ctx.Done():
return p.result
}
}
// 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.
func (p *Picker) Go(f func() (interface{}, error)) {
p.wg.Add(1)
go func() {
defer p.wg.Done()
if ret, err := f(); err == nil {
p.once.Do(func() {
p.result = ret
p.firstDone <- struct{}{}
if p.cancel != nil {
p.cancel()
}
})
}
}()
}

View File

@ -0,0 +1,66 @@
package picker
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func sleepAndSend(ctx context.Context, delay int, input interface{}) func() (interface{}, error) {
return func() (interface{}, error) {
timer := time.NewTimer(time.Millisecond * time.Duration(delay))
select {
case <-timer.C:
return input, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
func TestPicker_Basic(t *testing.T) {
picker, ctx := WithContext(context.Background())
picker.Go(sleepAndSend(ctx, 30, 2))
picker.Go(sleepAndSend(ctx, 20, 1))
number := picker.Wait()
assert.NotNil(t, number)
assert.Equal(t, number.(int), 1)
}
func TestPicker_Timeout(t *testing.T) {
picker, ctx := WithTimeout(context.Background(), time.Millisecond*5)
picker.Go(sleepAndSend(ctx, 20, 1))
number := picker.Wait()
assert.Nil(t, number)
}
func TestPicker_WaitWithoutAutoCancel(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*60)
defer cancel()
picker := WithoutAutoCancel(ctx)
trigger := false
picker.Go(sleepAndSend(ctx, 10, 1))
picker.Go(func() (interface{}, error) {
timer := time.NewTimer(time.Millisecond * time.Duration(30))
select {
case <-timer.C:
trigger = true
return 2, nil
case <-ctx.Done():
return nil, ctx.Err()
}
})
elm := picker.WaitWithoutCancel()
assert.NotNil(t, elm)
assert.Equal(t, elm.(int), 1)
elm = picker.Wait()
assert.True(t, trigger)
assert.Equal(t, elm.(int), 1)
}

15
common/pool/pool.go Normal file
View File

@ -0,0 +1,15 @@
package pool
import (
"sync"
)
const (
// io.Copy default buffer size is 32 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
bufferSize = 20 * 1024
)
// BufPool provide buffer for relay
var BufPool = sync.Pool{New: func() interface{} { return make([]byte, bufferSize) }}

71
common/queue/queue.go Normal file
View File

@ -0,0 +1,71 @@
package queue
import (
"sync"
)
// Queue is a simple concurrent safe queue
type Queue struct {
items []interface{}
lock sync.RWMutex
}
// Put add the item to the queue.
func (q *Queue) Put(items ...interface{}) {
if len(items) == 0 {
return
}
q.lock.Lock()
q.items = append(q.items, items...)
q.lock.Unlock()
}
// Pop returns the head of items.
func (q *Queue) Pop() interface{} {
if len(q.items) == 0 {
return nil
}
q.lock.Lock()
head := q.items[0]
q.items = q.items[1:]
q.lock.Unlock()
return head
}
// Last returns the last of item.
func (q *Queue) Last() interface{} {
if len(q.items) == 0 {
return nil
}
q.lock.RLock()
last := q.items[len(q.items)-1]
q.lock.RUnlock()
return last
}
// Copy get the copy of queue.
func (q *Queue) Copy() []interface{} {
items := []interface{}{}
q.lock.RLock()
items = append(items, q.items...)
q.lock.RUnlock()
return items
}
// Len returns the number of items in this queue.
func (q *Queue) Len() int64 {
q.lock.Lock()
defer q.lock.Unlock()
return int64(len(q.items))
}
// New is a constructor for a new concurrent safe queue.
func New(hint int64) *Queue {
return &Queue{
items: make([]interface{}, 0, hint),
}
}

View File

@ -1,5 +1,7 @@
package structure
// references: https://github.com/mitchellh/mapstructure
import (
"fmt"
"reflect"
@ -45,11 +47,11 @@ func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
}
value, ok := src[key]
if !ok {
if !ok || value == nil {
if omitempty {
continue
}
return fmt.Errorf("key %s missing", key)
return fmt.Errorf("key '%s' missing", key)
}
err := d.decode(key, value, v.Field(idx))
@ -70,6 +72,10 @@ func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error
return d.decodeBool(name, data, val)
case reflect.Slice:
return d.decodeSlice(name, data, val)
case reflect.Map:
return d.decodeMap(name, data, val)
case reflect.Interface:
return d.setInterface(name, data, val)
default:
return fmt.Errorf("type %s not support", val.Kind().String())
}
@ -108,7 +114,7 @@ func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value)
val.SetString(strconv.FormatInt(dataVal.Int(), 10))
default:
err = fmt.Errorf(
"'%s' expected type'%s', got unconvertible type '%s'",
"'%s' expected type '%s', got unconvertible type '%s'",
name, val.Type(), dataVal.Type(),
)
}
@ -125,7 +131,7 @@ func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) (
val.SetBool(dataVal.Int() != 0)
default:
err = fmt.Errorf(
"'%s' expected type'%s', got unconvertible type '%s'",
"'%s' expected type '%s', got unconvertible type '%s'",
name, val.Type(), dataVal.Type(),
)
}
@ -158,3 +164,76 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value)
val.Set(valSlice)
return nil
}
func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) error {
valType := val.Type()
valKeyType := valType.Key()
valElemType := valType.Elem()
valMap := val
if valMap.IsNil() {
mapType := reflect.MapOf(valKeyType, valElemType)
valMap = reflect.MakeMap(mapType)
}
dataVal := reflect.Indirect(reflect.ValueOf(data))
if dataVal.Kind() != reflect.Map {
return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind())
}
return d.decodeMapFromMap(name, dataVal, val, valMap)
}
func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val reflect.Value, valMap reflect.Value) error {
valType := val.Type()
valKeyType := valType.Key()
valElemType := valType.Elem()
errors := make([]string, 0)
if dataVal.Len() == 0 {
if dataVal.IsNil() {
if !val.IsNil() {
val.Set(dataVal)
}
} else {
val.Set(valMap)
}
return nil
}
for _, k := range dataVal.MapKeys() {
fieldName := fmt.Sprintf("%s[%s]", name, k)
currentKey := reflect.Indirect(reflect.New(valKeyType))
if err := d.decode(fieldName, k.Interface(), currentKey); err != nil {
errors = append(errors, err.Error())
continue
}
v := dataVal.MapIndex(k).Interface()
currentVal := reflect.Indirect(reflect.New(valElemType))
if err := d.decode(fieldName, v, currentVal); err != nil {
errors = append(errors, err.Error())
continue
}
valMap.SetMapIndex(currentKey, currentVal)
}
val.Set(valMap)
if len(errors) > 0 {
return fmt.Errorf(strings.Join(errors, ","))
}
return nil
}
func (d *Decoder) setInterface(name string, data interface{}, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data)
val.Set(dataVal)
return nil
}

46
component/auth/auth.go Normal file
View File

@ -0,0 +1,46 @@
package auth
import (
"sync"
)
type Authenticator interface {
Verify(user string, pass string) bool
Users() []string
}
type AuthUser struct {
User string
Pass string
}
type inMemoryAuthenticator struct {
storage *sync.Map
usernames []string
}
func (au *inMemoryAuthenticator) Verify(user string, pass string) bool {
realPass, ok := au.storage.Load(user)
return ok && realPass == pass
}
func (au *inMemoryAuthenticator) Users() []string { return au.usernames }
func NewAuthenticator(users []AuthUser) Authenticator {
if len(users) == 0 {
return nil
}
au := &inMemoryAuthenticator{storage: &sync.Map{}}
for _, user := range users {
au.storage.Store(user.User, user.Pass)
}
usernames := make([]string, 0, len(users))
au.storage.Range(func(key, value interface{}) bool {
usernames = append(usernames, key.(string))
return true
})
au.usernames = usernames
return au
}

View File

@ -0,0 +1,26 @@
package trie
// Node is the trie's node
type Node struct {
Data interface{}
children map[string]*Node
}
func (n *Node) getChild(s string) *Node {
return n.children[s]
}
func (n *Node) hasChild(s string) bool {
return n.getChild(s) != nil
}
func (n *Node) addChild(s string, child *Node) {
n.children[s] = child
}
func newNode(data interface{}) *Node {
return &Node{
Data: data,
children: map[string]*Node{},
}
}

View File

@ -0,0 +1,92 @@
package trie
import (
"errors"
"strings"
)
const (
wildcard = "*"
domainStep = "."
)
var (
// ErrInvalidDomain means insert domain is invalid
ErrInvalidDomain = errors.New("invalid domain")
)
// Trie contains the main logic for adding and searching nodes for domain segments.
// support wildcard domain (e.g *.google.com)
type Trie struct {
root *Node
}
func isValidDomain(domain string) bool {
return domain != "" && domain[0] != '.' && domain[len(domain)-1] != '.'
}
// Insert adds a node to the trie.
// Support
// 1. www.example.com
// 2. *.example.com
// 3. subdomain.*.example.com
func (t *Trie) Insert(domain string, data interface{}) error {
if !isValidDomain(domain) {
return ErrInvalidDomain
}
parts := strings.Split(domain, domainStep)
node := t.root
// reverse storage domain part to save space
for i := len(parts) - 1; i >= 0; i-- {
part := parts[i]
if !node.hasChild(part) {
node.addChild(part, newNode(nil))
}
node = node.getChild(part)
}
node.Data = data
return nil
}
// Search is the most important part of the Trie.
// Priority as:
// 1. static part
// 2. wildcard domain
func (t *Trie) Search(domain string) *Node {
if !isValidDomain(domain) {
return nil
}
parts := strings.Split(domain, domainStep)
n := t.root
for i := len(parts) - 1; i >= 0; i-- {
part := parts[i]
var child *Node
if !n.hasChild(part) {
if !n.hasChild(wildcard) {
return nil
}
child = n.getChild(wildcard)
} else {
child = n.getChild(part)
}
n = child
}
if n.Data == nil {
return nil
}
return n
}
// New returns a new, empty Trie.
func New() *Trie {
return &Trie{root: newNode(nil)}
}

View File

@ -0,0 +1,87 @@
package trie
import (
"net"
"testing"
)
var localIP = net.IP{127, 0, 0, 1}
func TestTrie_Basic(t *testing.T) {
tree := New()
domains := []string{
"example.com",
"google.com",
}
for _, domain := range domains {
tree.Insert(domain, localIP)
}
node := tree.Search("example.com")
if node == nil {
t.Error("should not recv nil")
}
if !node.Data.(net.IP).Equal(localIP) {
t.Error("should equal 127.0.0.1")
}
if tree.Insert("", localIP) == nil {
t.Error("should return error")
}
}
func TestTrie_Wildcard(t *testing.T) {
tree := New()
domains := []string{
"*.example.com",
"sub.*.example.com",
"*.dev",
}
for _, domain := range domains {
tree.Insert(domain, localIP)
}
if tree.Search("sub.example.com") == nil {
t.Error("should not recv nil")
}
if tree.Search("sub.foo.example.com") == nil {
t.Error("should not recv nil")
}
if tree.Search("foo.sub.example.com") != nil {
t.Error("should recv nil")
}
if tree.Search("foo.example.dev") != nil {
t.Error("should recv nil")
}
if tree.Search("example.com") != nil {
t.Error("should recv nil")
}
}
func TestTrie_Boundary(t *testing.T) {
tree := New()
tree.Insert("*.dev", localIP)
if err := tree.Insert(".", localIP); err == nil {
t.Error("should recv err")
}
if err := tree.Insert(".com", localIP); err == nil {
t.Error("should recv err")
}
if tree.Search("dev") != nil {
t.Error("should recv nil")
}
if tree.Search(".dev") != nil {
t.Error("should recv nil")
}
}

117
component/fakeip/pool.go Normal file
View File

@ -0,0 +1,117 @@
package fakeip
import (
"errors"
"net"
"sync"
"github.com/Dreamacro/clash/common/cache"
)
// Pool is a implementation about fake ip generator without storage
type Pool struct {
max uint32
min uint32
gateway uint32
offset uint32
mux *sync.Mutex
cache *cache.LruCache
}
// Lookup return a fake ip with host
func (p *Pool) Lookup(host string) net.IP {
p.mux.Lock()
defer p.mux.Unlock()
if elm, exist := p.cache.Get(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
}
ip := p.get(host)
p.cache.Set(host, ip)
return ip
}
// LookBack return host with the fake ip
func (p *Pool) LookBack(ip net.IP) (string, bool) {
p.mux.Lock()
defer p.mux.Unlock()
if ip = ip.To4(); ip == nil {
return "", false
}
n := ipToUint(ip.To4())
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
}
// Gateway return gateway ip
func (p *Pool) Gateway() net.IP {
return uintToIP(p.gateway)
}
func (p *Pool) get(host string) net.IP {
current := p.offset
for {
p.offset = (p.offset + 1) % (p.max - p.min)
// Avoid infinite loops
if p.offset == current {
break
}
if !p.cache.Exist(p.offset) {
break
}
}
ip := uintToIP(p.min + p.offset - 1)
p.cache.Set(p.offset, host)
return ip
}
func ipToUint(ip net.IP) uint32 {
v := uint32(ip[0]) << 24
v += uint32(ip[1]) << 16
v += uint32(ip[2]) << 8
v += uint32(ip[3])
return v
}
func uintToIP(v uint32) net.IP {
return net.IPv4(byte(v>>24), byte(v>>16), byte(v>>8), byte(v))
}
// New return Pool instance
func New(ipnet *net.IPNet, size int) (*Pool, error) {
min := ipToUint(ipnet.IP) + 2
ones, bits := ipnet.Mask.Size()
total := 1<<uint(bits-ones) - 2
if total <= 0 {
return nil, errors.New("ipnet don't have valid ip")
}
max := min + uint32(total) - 1
return &Pool{
min: min,
max: max,
gateway: min - 1,
mux: &sync.Mutex{},
cache: cache.NewLRUCache(cache.WithSize(size * 2)),
}, nil
}

View File

@ -0,0 +1,78 @@
package fakeip
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPool_Basic(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
pool, _ := New(ipnet, 10)
first := pool.Lookup("foo.com")
last := pool.Lookup("bar.com")
bar, exist := pool.LookBack(last)
assert.True(t, first.Equal(net.IP{192, 168, 0, 2}))
assert.True(t, last.Equal(net.IP{192, 168, 0, 3}))
assert.True(t, exist)
assert.Equal(t, bar, "bar.com")
}
func TestPool_Cycle(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/30")
pool, _ := New(ipnet, 10)
first := pool.Lookup("foo.com")
same := pool.Lookup("baz.com")
assert.True(t, first.Equal(same))
}
func TestPool_MaxCacheSize(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
pool, _ := New(ipnet, 2)
first := pool.Lookup("foo.com")
pool.Lookup("bar.com")
pool.Lookup("baz.com")
next := pool.Lookup("foo.com")
assert.False(t, first.Equal(next))
}
func TestPool_DoubleMapping(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
pool, _ := New(ipnet, 2)
// fill cache
fooIP := pool.Lookup("foo.com")
bazIP := pool.Lookup("baz.com")
// make foo.com hot
pool.Lookup("foo.com")
// should drop baz.com
barIP := pool.Lookup("bar.com")
_, fooExist := pool.LookBack(fooIP)
_, bazExist := pool.LookBack(bazIP)
_, barExist := pool.LookBack(barIP)
newBazIP := pool.Lookup("baz.com")
assert.True(t, fooExist)
assert.False(t, bazExist)
assert.True(t, barExist)
assert.False(t, bazIP.Equal(newBazIP))
}
func TestPool_Error(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/31")
_, err := New(ipnet, 10)
assert.Error(t, err)
}

46
component/nat/table.go Normal file
View File

@ -0,0 +1,46 @@
package nat
import (
"net"
"sync"
)
type Table struct {
mapping sync.Map
}
type element struct {
RemoteAddr net.Addr
RemoteConn net.PacketConn
}
func (t *Table) Set(key string, pc net.PacketConn, addr net.Addr) {
// set conn read timeout
t.mapping.Store(key, &element{
RemoteConn: pc,
RemoteAddr: addr,
})
}
func (t *Table) Get(key string) (net.PacketConn, net.Addr) {
item, exist := t.mapping.Load(key)
if !exist {
return nil, nil
}
elm := item.(*element)
return elm.RemoteConn, elm.RemoteAddr
}
func (t *Table) GetOrCreateLock(key string) (*sync.WaitGroup, bool) {
item, loaded := t.mapping.LoadOrStore(key, &sync.WaitGroup{})
return item.(*sync.WaitGroup), loaded
}
func (t *Table) Delete(key string) {
t.mapping.Delete(key)
}
// New return *Cache
func New() *Table {
return &Table{}
}

View File

@ -8,6 +8,8 @@ import (
"math/rand"
"net"
"net/http"
"github.com/Dreamacro/clash/common/pool"
)
// HTTPObfs is shadowsocks http simple-obfs implementation
@ -32,15 +34,15 @@ func (ho *HTTPObfs) Read(b []byte) (int, error) {
}
if ho.firstResponse {
buf := bufPool.Get().([]byte)
buf := pool.BufPool.Get().([]byte)
n, err := ho.Conn.Read(buf)
if err != nil {
bufPool.Put(buf[:cap(buf)])
pool.BufPool.Put(buf[:cap(buf)])
return 0, err
}
idx := bytes.Index(buf[:n], []byte("\r\n\r\n"))
if idx == -1 {
bufPool.Put(buf[:cap(buf)])
pool.BufPool.Put(buf[:cap(buf)])
return 0, io.EOF
}
ho.firstResponse = false
@ -50,7 +52,7 @@ func (ho *HTTPObfs) Read(b []byte) (int, error) {
ho.buf = buf[:idx+4+length]
ho.offset = idx + 4 + n
} else {
bufPool.Put(buf[:cap(buf)])
pool.BufPool.Put(buf[:cap(buf)])
}
return n, nil
}

View File

@ -6,15 +6,18 @@ import (
"io"
"math/rand"
"net"
"sync"
"time"
"github.com/Dreamacro/clash/common/pool"
)
func init() {
rand.Seed(time.Now().Unix())
}
var bufPool = sync.Pool{New: func() interface{} { return make([]byte, 2048) }}
const (
chunkSize = 1 << 14 // 2 ** 14 == 16 * 1024
)
// TLSObfs is shadowsocks tls simple-obfs implementation
type TLSObfs struct {
@ -26,12 +29,12 @@ type TLSObfs struct {
}
func (to *TLSObfs) read(b []byte, discardN int) (int, error) {
buf := bufPool.Get().([]byte)
buf := pool.BufPool.Get().([]byte)
_, err := io.ReadFull(to.Conn, buf[:discardN])
if err != nil {
return 0, err
}
bufPool.Put(buf[:cap(buf)])
pool.BufPool.Put(buf[:cap(buf)])
sizeBuf := make([]byte, 2)
_, err = io.ReadFull(to.Conn, sizeBuf)
@ -75,8 +78,23 @@ func (to *TLSObfs) Read(b []byte) (int, error) {
// type + ver = 3
return to.read(b, 3)
}
func (to *TLSObfs) Write(b []byte) (int, error) {
length := len(b)
for i := 0; i < length; i += chunkSize {
end := i + chunkSize
if end > length {
end = length
}
n, err := to.write(b[i:end])
if err != nil {
return n, err
}
}
return length, nil
}
func (to *TLSObfs) write(b []byte) (int, error) {
if to.firstRequest {
helloMsg := makeClientHelloMsg(b, to.server)
_, err := to.Conn.Write(helloMsg)
@ -84,7 +102,7 @@ func (to *TLSObfs) Write(b []byte) (int, error) {
return len(b), err
}
size := bufPool.Get().([]byte)
size := pool.BufPool.Get().([]byte)
binary.BigEndian.PutUint16(size[:2], uint16(len(b)))
buf := &bytes.Buffer{}
@ -92,7 +110,7 @@ func (to *TLSObfs) Write(b []byte) (int, error) {
buf.Write(size[:2])
buf.Write(b)
_, err := to.Conn.Write(buf.Bytes())
bufPool.Put(size[:cap(size)])
pool.BufPool.Put(size[:cap(size)])
return len(b), err
}
@ -107,9 +125,8 @@ func NewTLSObfs(conn net.Conn, server string) net.Conn {
}
func makeClientHelloMsg(data []byte, server string) []byte {
random := make([]byte, 32)
random := make([]byte, 28)
sessionID := make([]byte, 32)
size := make([]byte, 2)
rand.Read(random)
rand.Read(sessionID)
@ -124,12 +141,12 @@ func makeClientHelloMsg(data []byte, server string) []byte {
// clientHello, length, TLS 1.2 version
buf.WriteByte(1)
binary.BigEndian.PutUint16(size, uint16(208+len(data)+len(server)))
buf.WriteByte(0)
buf.Write(size)
binary.Write(buf, binary.BigEndian, uint16(208+len(data)+len(server)))
buf.Write([]byte{0x03, 0x03})
// random, sid len, sid
// random with timestamp, sid len, sid
binary.Write(buf, binary.BigEndian, uint32(time.Now().Unix()))
buf.Write(random)
buf.WriteByte(32)
buf.Write(sessionID)
@ -147,24 +164,19 @@ func makeClientHelloMsg(data []byte, server string) []byte {
buf.Write([]byte{0x01, 0x00})
// extension length
binary.BigEndian.PutUint16(size, uint16(79+len(data)+len(server)))
buf.Write(size)
binary.Write(buf, binary.BigEndian, uint16(79+len(data)+len(server)))
// session ticket
buf.Write([]byte{0x00, 0x23})
binary.BigEndian.PutUint16(size, uint16(len(data)))
buf.Write(size)
binary.Write(buf, binary.BigEndian, uint16(len(data)))
buf.Write(data)
// server name
buf.Write([]byte{0x00, 0x00})
binary.BigEndian.PutUint16(size, uint16(len(server)+5))
buf.Write(size)
binary.BigEndian.PutUint16(size, uint16(len(server)+3))
buf.Write(size)
binary.Write(buf, binary.BigEndian, uint16(len(server)+5))
binary.Write(buf, binary.BigEndian, uint16(len(server)+3))
buf.WriteByte(0)
binary.BigEndian.PutUint16(size, uint16(len(server)))
buf.Write(size)
binary.Write(buf, binary.BigEndian, uint16(len(server)))
buf.Write([]byte(server))
// ec_point

21
component/snell/cipher.go Normal file
View File

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

91
component/snell/snell.go Normal file
View File

@ -0,0 +1,91 @@
package snell
import (
"bytes"
"encoding/binary"
"errors"
"io"
"net"
"sync"
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
"golang.org/x/crypto/chacha20poly1305"
)
const (
CommandPing byte = 0
CommandConnect byte = 1
CommandTunnel byte = 0
CommandError byte = 2
Version byte = 1
)
var (
bufferPool = sync.Pool{New: func() interface{} { return &bytes.Buffer{} }}
)
type Snell struct {
net.Conn
buffer [1]byte
reply bool
}
func (s *Snell) Read(b []byte) (int, error) {
if s.reply {
return s.Conn.Read(b)
}
s.reply = true
if _, err := io.ReadFull(s.Conn, s.buffer[:]); err != nil {
return 0, err
}
if s.buffer[0] == CommandTunnel {
return s.Conn.Read(b)
} else if s.buffer[0] != CommandError {
return 0, errors.New("Command not support")
}
// CommandError
if _, err := io.ReadFull(s.Conn, s.buffer[:]); err != nil {
return 0, err
}
length := int(s.buffer[0])
msg := make([]byte, length)
if _, err := io.ReadFull(s.Conn, msg); err != nil {
return 0, err
}
return 0, errors.New(string(msg))
}
func WriteHeader(conn net.Conn, host string, port uint) error {
buf := bufferPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufferPool.Put(buf)
buf.WriteByte(Version)
buf.WriteByte(CommandConnect)
// clientID length & id
buf.WriteByte(0)
// host & port
buf.WriteByte(uint8(len(host)))
buf.WriteString(host)
binary.Write(buf, binary.BigEndian, uint16(port))
if _, err := conn.Write(buf.Bytes()); err != nil {
return err
}
return nil
}
func StreamConn(conn net.Conn, psk []byte) net.Conn {
cipher := &snellCipher{psk, chacha20poly1305.New}
return &Snell{Conn: shadowaead.NewConn(conn, cipher)}
}

375
component/socks5/socks5.go Normal file
View File

@ -0,0 +1,375 @@
package socks5
import (
"bytes"
"errors"
"io"
"net"
"strconv"
"github.com/Dreamacro/clash/component/auth"
)
// Error represents a SOCKS error
type Error byte
func (err Error) Error() string {
return "SOCKS error: " + strconv.Itoa(int(err))
}
// Command is request commands as defined in RFC 1928 section 4.
type Command = uint8
// SOCKS request commands as defined in RFC 1928 section 4.
const (
CmdConnect Command = 1
CmdBind Command = 2
CmdUDPAssociate Command = 3
)
// SOCKS address types as defined in RFC 1928 section 5.
const (
AtypIPv4 = 1
AtypDomainName = 3
AtypIPv6 = 4
)
// MaxAddrLen is the maximum size of SOCKS address in bytes.
const MaxAddrLen = 1 + 1 + 255 + 2
// MaxAuthLen is the maximum size of user/password field in SOCKS5 Auth
const MaxAuthLen = 255
// Addr represents a SOCKS address as defined in RFC 1928 section 5.
type Addr []byte
func (a Addr) String() string {
var host, port string
switch a[0] {
case AtypDomainName:
hostLen := uint16(a[1])
host = string(a[2 : 2+hostLen])
port = strconv.Itoa((int(a[2+hostLen]) << 8) | int(a[2+hostLen+1]))
case AtypIPv4:
host = net.IP(a[1 : 1+net.IPv4len]).String()
port = strconv.Itoa((int(a[1+net.IPv4len]) << 8) | int(a[1+net.IPv4len+1]))
case AtypIPv6:
host = net.IP(a[1 : 1+net.IPv6len]).String()
port = strconv.Itoa((int(a[1+net.IPv6len]) << 8) | int(a[1+net.IPv6len+1]))
}
return net.JoinHostPort(host, port)
}
// SOCKS errors as defined in RFC 1928 section 6.
const (
ErrGeneralFailure = Error(1)
ErrConnectionNotAllowed = Error(2)
ErrNetworkUnreachable = Error(3)
ErrHostUnreachable = Error(4)
ErrConnectionRefused = Error(5)
ErrTTLExpired = Error(6)
ErrCommandNotSupported = Error(7)
ErrAddressNotSupported = Error(8)
)
// Auth errors used to return a specific "Auth failed" error
var ErrAuth = errors.New("auth failed")
type User struct {
Username string
Password string
}
// ServerHandshake fast-tracks SOCKS initialization to get target address to connect on server side.
func ServerHandshake(rw net.Conn, authenticator auth.Authenticator) (addr Addr, command Command, err error) {
// Read RFC 1928 for request and reply structure and sizes.
buf := make([]byte, MaxAddrLen)
// read VER, NMETHODS, METHODS
if _, err = io.ReadFull(rw, buf[:2]); err != nil {
return
}
nmethods := buf[1]
if _, err = io.ReadFull(rw, buf[:nmethods]); err != nil {
return
}
// write VER METHOD
if authenticator != nil {
if _, err = rw.Write([]byte{5, 2}); err != nil {
return
}
// Get header
header := make([]byte, 2)
if _, err = io.ReadFull(rw, header); err != nil {
return
}
authBuf := make([]byte, MaxAuthLen)
// Get username
userLen := int(header[1])
if userLen <= 0 {
rw.Write([]byte{1, 1})
err = ErrAuth
return
}
if _, err = io.ReadFull(rw, authBuf[:userLen]); err != nil {
return
}
user := string(authBuf[:userLen])
// Get password
if _, err = rw.Read(header[:1]); err != nil {
return
}
passLen := int(header[0])
if passLen <= 0 {
rw.Write([]byte{1, 1})
err = ErrAuth
return
}
if _, err = io.ReadFull(rw, authBuf[:passLen]); err != nil {
return
}
pass := string(authBuf[:passLen])
// Verify
if ok := authenticator.Verify(string(user), string(pass)); !ok {
rw.Write([]byte{1, 1})
err = ErrAuth
return
}
// Response auth state
if _, err = rw.Write([]byte{1, 0}); err != nil {
return
}
} else {
if _, err = rw.Write([]byte{5, 0}); err != nil {
return
}
}
// read VER CMD RSV ATYP DST.ADDR DST.PORT
if _, err = io.ReadFull(rw, buf[:3]); err != nil {
return
}
command = buf[1]
addr, err = readAddr(rw, buf)
if err != nil {
return
}
switch command {
case CmdConnect, CmdUDPAssociate:
// Acquire server listened address info
localAddr := ParseAddr(rw.LocalAddr().String())
if localAddr == nil {
err = ErrAddressNotSupported
} else {
// write VER REP RSV ATYP BND.ADDR BND.PORT
_, err = rw.Write(bytes.Join([][]byte{{5, 0, 0}, localAddr}, []byte{}))
}
case CmdBind:
fallthrough
default:
err = ErrCommandNotSupported
}
return
}
// ClientHandshake fast-tracks SOCKS initialization to get target address to connect on client side.
func ClientHandshake(rw io.ReadWriter, addr Addr, command Command, user *User) (Addr, error) {
buf := make([]byte, MaxAddrLen)
var err error
// VER, NMETHODS, METHODS
if user != nil {
_, err = rw.Write([]byte{5, 1, 2})
} else {
_, err = rw.Write([]byte{5, 1, 0})
}
if err != nil {
return nil, err
}
// VER, METHOD
if _, err := io.ReadFull(rw, buf[:2]); err != nil {
return nil, err
}
if buf[0] != 5 {
return nil, errors.New("SOCKS version error")
}
if buf[1] == 2 {
// password protocol version
authMsg := &bytes.Buffer{}
authMsg.WriteByte(1)
authMsg.WriteByte(uint8(len(user.Username)))
authMsg.WriteString(user.Username)
authMsg.WriteByte(uint8(len(user.Password)))
authMsg.WriteString(user.Password)
if _, err := rw.Write(authMsg.Bytes()); err != nil {
return nil, err
}
if _, err := io.ReadFull(rw, buf[:2]); err != nil {
return nil, err
}
if buf[1] != 0 {
return nil, errors.New("rejected username/password")
}
} else if buf[1] != 0 {
return nil, errors.New("SOCKS need auth")
}
// VER, CMD, RSV, ADDR
if _, err := rw.Write(bytes.Join([][]byte{{5, command, 0}, addr}, []byte{})); err != nil {
return nil, err
}
// VER, REP, RSV
if _, err := io.ReadFull(rw, buf[:3]); err != nil {
return nil, err
}
return readAddr(rw, buf)
}
func readAddr(r io.Reader, b []byte) (Addr, error) {
if len(b) < MaxAddrLen {
return nil, io.ErrShortBuffer
}
_, err := io.ReadFull(r, b[:1]) // read 1st byte for address type
if err != nil {
return nil, err
}
switch b[0] {
case AtypDomainName:
_, err = io.ReadFull(r, b[1:2]) // read 2nd byte for domain length
if err != nil {
return nil, err
}
domainLength := uint16(b[1])
_, err = io.ReadFull(r, b[2:2+domainLength+2])
return b[:1+1+domainLength+2], err
case AtypIPv4:
_, err = io.ReadFull(r, b[1:1+net.IPv4len+2])
return b[:1+net.IPv4len+2], err
case AtypIPv6:
_, err = io.ReadFull(r, b[1:1+net.IPv6len+2])
return b[:1+net.IPv6len+2], err
}
return nil, ErrAddressNotSupported
}
// SplitAddr slices a SOCKS address from beginning of b. Returns nil if failed.
func SplitAddr(b []byte) Addr {
addrLen := 1
if len(b) < addrLen {
return nil
}
switch b[0] {
case AtypDomainName:
if len(b) < 2 {
return nil
}
addrLen = 1 + 1 + int(b[1]) + 2
case AtypIPv4:
addrLen = 1 + net.IPv4len + 2
case AtypIPv6:
addrLen = 1 + net.IPv6len + 2
default:
return nil
}
if len(b) < addrLen {
return nil
}
return b[:addrLen]
}
// ParseAddr parses the address in string s. Returns nil if failed.
func ParseAddr(s string) Addr {
var addr Addr
host, port, err := net.SplitHostPort(s)
if err != nil {
return nil
}
if ip := net.ParseIP(host); ip != nil {
if ip4 := ip.To4(); ip4 != nil {
addr = make([]byte, 1+net.IPv4len+2)
addr[0] = AtypIPv4
copy(addr[1:], ip4)
} else {
addr = make([]byte, 1+net.IPv6len+2)
addr[0] = AtypIPv6
copy(addr[1:], ip)
}
} else {
if len(host) > 255 {
return nil
}
addr = make([]byte, 1+1+len(host)+2)
addr[0] = AtypDomainName
addr[1] = byte(len(host))
copy(addr[2:], host)
}
portnum, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return nil
}
addr[len(addr)-2], addr[len(addr)-1] = byte(portnum>>8), byte(portnum)
return addr
}
// DecodeUDPPacket split `packet` to addr payload, and this function is mutable with `packet`
func DecodeUDPPacket(packet []byte) (addr Addr, payload []byte, err error) {
if len(packet) < 5 {
err = errors.New("insufficient length of packet")
return
}
// packet[0] and packet[1] are reserved
if !bytes.Equal(packet[:2], []byte{0, 0}) {
err = errors.New("reserved fields should be zero")
return
}
if packet[2] != 0 /* fragments */ {
err = errors.New("discarding fragmented payload")
return
}
addr = SplitAddr(packet[3:])
if addr == nil {
err = errors.New("failed to read UDP header")
}
payload = packet[3+len(addr):]
return
}
func EncodeUDPPacket(addr Addr, payload []byte) (packet []byte, err error) {
if addr == nil {
err = errors.New("address is invalid")
return
}
packet = bytes.Join([][]byte{{0, 0, 0}, addr, payload}, []byte{})
return
}

View File

@ -0,0 +1,173 @@
package obfs
import (
"bytes"
"encoding/binary"
"errors"
"io"
"net"
)
type SessionStatus = byte
const (
SessionStatusNew SessionStatus = 0x01
SessionStatusKeep SessionStatus = 0x02
SessionStatusEnd SessionStatus = 0x03
SessionStatusKeepAlive SessionStatus = 0x04
)
const (
OptionNone = byte(0x00)
OptionData = byte(0x01)
OptionError = byte(0x02)
)
type MuxOption struct {
ID [2]byte
Port uint16
Host string
Type string
}
// Mux is an mux-compatible client for v2ray-plugin, not a complete implementation
type Mux struct {
net.Conn
buf bytes.Buffer
id [2]byte
length [2]byte
status [2]byte
otb []byte
remain int
}
func (m *Mux) Read(b []byte) (int, error) {
if m.remain != 0 {
length := m.remain
if len(b) < m.remain {
length = len(b)
}
n, err := m.Conn.Read(b[:length])
if err != nil {
return 0, err
}
m.remain = m.remain - n
return n, nil
}
for {
_, err := io.ReadFull(m.Conn, m.length[:])
if err != nil {
return 0, err
}
length := binary.BigEndian.Uint16(m.length[:])
if length > 512 {
return 0, errors.New("invalid metalen")
}
_, err = io.ReadFull(m.Conn, m.id[:])
if err != nil {
return 0, err
}
_, err = m.Conn.Read(m.status[:])
if err != nil {
return 0, err
}
opcode := m.status[0]
if opcode == SessionStatusKeepAlive {
continue
}
opts := m.status[1]
if opts != OptionData {
continue
}
_, err = io.ReadFull(m.Conn, m.length[:])
if err != nil {
return 0, err
}
dataLen := int(binary.BigEndian.Uint16(m.length[:]))
m.remain = dataLen
if dataLen > len(b) {
dataLen = len(b)
}
n, err := m.Conn.Read(b[:dataLen])
m.remain -= n
return n, err
}
}
func (m *Mux) Write(b []byte) (int, error) {
if m.otb != nil {
// create a sub connection
if _, err := m.Conn.Write(m.otb); err != nil {
return 0, err
}
m.otb = nil
}
m.buf.Reset()
binary.Write(&m.buf, binary.BigEndian, uint16(4))
m.buf.Write(m.id[:])
m.buf.WriteByte(SessionStatusKeep)
m.buf.WriteByte(OptionData)
binary.Write(&m.buf, binary.BigEndian, uint16(len(b)))
m.buf.Write(b)
return m.Conn.Write(m.buf.Bytes())
}
func (m *Mux) Close() error {
_, err := m.Conn.Write([]byte{0x0, 0x4, m.id[0], m.id[1], SessionStatusEnd, OptionNone})
if err != nil {
return err
}
return m.Conn.Close()
}
func NewMux(conn net.Conn, option MuxOption) *Mux {
buf := &bytes.Buffer{}
// fill empty length
buf.Write([]byte{0x0, 0x0})
buf.Write(option.ID[:])
buf.WriteByte(SessionStatusNew)
buf.WriteByte(OptionNone)
// tcp
netType := byte(0x1)
if option.Type == "udp" {
netType = byte(0x2)
}
buf.WriteByte(netType)
// port
binary.Write(buf, binary.BigEndian, option.Port)
// address
ip := net.ParseIP(option.Host)
if ip == nil {
buf.WriteByte(0x2)
buf.WriteString(option.Host)
} else if ipv4 := ip.To4(); ipv4 != nil {
buf.WriteByte(0x1)
buf.Write(ipv4)
} else {
buf.WriteByte(0x3)
buf.Write(ip.To16())
}
metadata := buf.Bytes()
binary.BigEndian.PutUint16(metadata[:2], uint16(len(metadata)-2))
return &Mux{
Conn: conn,
id: option.ID,
otb: metadata,
}
}

View File

@ -0,0 +1,49 @@
package obfs
import (
"crypto/tls"
"net"
"net/http"
"github.com/Dreamacro/clash/component/vmess"
)
// Option is options of websocket obfs
type Option struct {
Host string
Path string
Headers map[string]string
TLSConfig *tls.Config
Mux bool
}
// NewV2rayObfs return a HTTPObfs
func NewV2rayObfs(conn net.Conn, option *Option) (net.Conn, error) {
header := http.Header{}
for k, v := range option.Headers {
header.Add(k, v)
}
config := &vmess.WebsocketConfig{
Host: option.Host,
Path: option.Path,
TLS: option.TLSConfig != nil,
Headers: header,
TLSConfig: option.TLSConfig,
}
var err error
conn, err = vmess.NewWebsocketConn(conn, config)
if err != nil {
return nil, err
}
if option.Mux {
conn = NewMux(conn, MuxOption{
ID: [2]byte{0, 0},
Host: "127.0.0.1",
Port: 0,
})
}
return conn, nil
}

View File

@ -5,6 +5,8 @@ import (
"encoding/binary"
"errors"
"io"
"github.com/Dreamacro/clash/common/pool"
)
type aeadWriter struct {
@ -20,8 +22,8 @@ func newAEADWriter(w io.Writer, aead cipher.AEAD, iv []byte) *aeadWriter {
}
func (w *aeadWriter) Write(b []byte) (n int, err error) {
buf := bufPool.Get().([]byte)
defer bufPool.Put(buf[:cap(buf)])
buf := pool.BufPool.Get().([]byte)
defer pool.BufPool.Put(buf[:cap(buf)])
length := len(b)
for {
if length == 0 {
@ -71,7 +73,7 @@ func (r *aeadReader) Read(b []byte) (int, error) {
n := copy(b, r.buf[r.offset:])
r.offset += n
if r.offset == len(r.buf) {
bufPool.Put(r.buf[:cap(r.buf)])
pool.BufPool.Put(r.buf[:cap(r.buf)])
r.buf = nil
}
return n, nil
@ -87,10 +89,10 @@ func (r *aeadReader) Read(b []byte) (int, error) {
return 0, errors.New("Buffer is larger than standard")
}
buf := bufPool.Get().([]byte)
buf := pool.BufPool.Get().([]byte)
_, err = io.ReadFull(r.Reader, buf[:size])
if err != nil {
bufPool.Put(buf[:cap(buf)])
pool.BufPool.Put(buf[:cap(buf)])
return 0, err
}
@ -105,7 +107,7 @@ func (r *aeadReader) Read(b []byte) (int, error) {
realLen := size - r.Overhead()
n := copy(b, buf[:realLen])
if len(b) >= realLen {
bufPool.Put(buf[:cap(buf)])
pool.BufPool.Put(buf[:cap(buf)])
return n, nil
}

View File

@ -4,7 +4,8 @@ import (
"encoding/binary"
"errors"
"io"
"sync"
"github.com/Dreamacro/clash/common/pool"
)
const (
@ -13,8 +14,6 @@ const (
maxSize = 17 * 1024 // 2 + chunkSize + aead.Overhead()
)
var bufPool = sync.Pool{New: func() interface{} { return make([]byte, maxSize) }}
type chunkReader struct {
io.Reader
buf []byte
@ -35,7 +34,7 @@ func (cr *chunkReader) Read(b []byte) (int, error) {
n := copy(b, cr.buf[cr.offset:])
cr.offset += n
if cr.offset == len(cr.buf) {
bufPool.Put(cr.buf[:cap(cr.buf)])
pool.BufPool.Put(cr.buf[:cap(cr.buf)])
cr.buf = nil
}
return n, nil
@ -60,10 +59,10 @@ func (cr *chunkReader) Read(b []byte) (int, error) {
return size, nil
}
buf := bufPool.Get().([]byte)
buf := pool.BufPool.Get().([]byte)
_, err = io.ReadFull(cr.Reader, buf[:size])
if err != nil {
bufPool.Put(buf[:cap(buf)])
pool.BufPool.Put(buf[:cap(buf)])
return 0, err
}
n := copy(b, cr.buf[:])
@ -77,8 +76,8 @@ type chunkWriter struct {
}
func (cw *chunkWriter) Write(b []byte) (n int, err error) {
buf := bufPool.Get().([]byte)
defer bufPool.Put(buf[:cap(buf)])
buf := pool.BufPool.Get().([]byte)
defer pool.BufPool.Put(buf[:cap(buf)])
length := len(b)
for {
if length == 0 {

View File

@ -35,20 +35,10 @@ type Conn struct {
respV byte
security byte
sent bool
received bool
}
func (vc *Conn) Write(b []byte) (int, error) {
if vc.sent {
return vc.writer.Write(b)
}
if err := vc.sendRequest(); err != nil {
return 0, err
}
vc.sent = true
return vc.writer.Write(b)
}
@ -65,11 +55,10 @@ func (vc *Conn) Read(b []byte) (int, error) {
}
func (vc *Conn) sendRequest() error {
timestamp := make([]byte, 8)
binary.BigEndian.PutUint64(timestamp, uint64(time.Now().UTC().Unix()))
timestamp := time.Now()
h := hmac.New(md5.New, vc.id.UUID.Bytes())
h.Write(timestamp)
binary.Write(h, binary.BigEndian, uint64(timestamp.Unix()))
_, err := vc.Conn.Write(h.Sum(nil))
if err != nil {
return err
@ -88,7 +77,11 @@ func (vc *Conn) sendRequest() error {
// P Sec Reserve Cmd
buf.WriteByte(byte(p<<4) | byte(vc.security))
buf.WriteByte(0)
buf.WriteByte(CommandTCP)
if vc.dst.UDP {
buf.WriteByte(CommandUDP)
} else {
buf.WriteByte(CommandTCP)
}
// Port AddrType Addr
binary.Write(buf, binary.BigEndian, uint16(vc.dst.Port))
@ -111,7 +104,7 @@ func (vc *Conn) sendRequest() error {
return err
}
stream := cipher.NewCFBEncrypter(block, hashTimestamp(time.Now().UTC()))
stream := cipher.NewCFBEncrypter(block, hashTimestamp(timestamp))
stream.XORKeyStream(buf.Bytes(), buf.Bytes())
_, err = vc.Conn.Write(buf.Bytes())
return err
@ -145,7 +138,7 @@ func (vc *Conn) recvResponse() error {
func hashTimestamp(t time.Time) []byte {
md5hash := md5.New()
ts := make([]byte, 8)
binary.BigEndian.PutUint64(ts, uint64(t.UTC().Unix()))
binary.BigEndian.PutUint64(ts, uint64(t.Unix()))
md5hash.Write(ts)
md5hash.Write(ts)
md5hash.Write(ts)
@ -154,7 +147,7 @@ func hashTimestamp(t time.Time) []byte {
}
// newConn return a Conn instance
func newConn(conn net.Conn, id *ID, dst *DstAddr, security Security) *Conn {
func newConn(conn net.Conn, id *ID, dst *DstAddr, security Security) (*Conn, error) {
randBytes := make([]byte, 33)
rand.Read(randBytes)
reqBodyIV := make([]byte, 16)
@ -197,7 +190,7 @@ func newConn(conn net.Conn, id *ID, dst *DstAddr, security Security) *Conn {
reader = newAEADReader(conn, aead, respBodyIV[:])
}
return &Conn{
c := &Conn{
Conn: conn,
id: id,
dst: dst,
@ -210,4 +203,8 @@ func newConn(conn net.Conn, id *ID, dst *DstAddr, security Security) *Conn {
writer: writer,
security: security,
}
if err := c.sendRequest(); err != nil {
return nil, err
}
return c, nil
}

View File

@ -5,12 +5,11 @@ import (
"fmt"
"math/rand"
"net"
"net/url"
"net/http"
"runtime"
"time"
"sync"
"github.com/gofrs/uuid"
"github.com/gorilla/websocket"
)
// Version of vmess
@ -39,6 +38,11 @@ var CipherMapping = map[string]byte{
"chacha20-poly1305": SecurityCHACHA20POLY1305,
}
var (
clientSessionCache tls.ClientSessionCache
once sync.Once
)
// Command types
const (
CommandTCP byte = 1
@ -54,6 +58,7 @@ const (
// DstAddr store destination address
type DstAddr struct {
UDP bool
AddrType byte
Addr []byte
Port uint
@ -61,76 +66,43 @@ type DstAddr struct {
// Client is vmess connection generator
type Client struct {
user []*ID
uuid *uuid.UUID
security Security
tls bool
host string
websocket bool
websocketPath string
skipCertVerify bool
user []*ID
uuid *uuid.UUID
security Security
tls bool
host string
wsConfig *WebsocketConfig
tlsConfig *tls.Config
}
// Config of vmess
type Config struct {
UUID string
AlterID uint16
Security string
TLS bool
Host string
NetWork string
WebSocketPath string
SkipCertVerify bool
UUID string
AlterID uint16
Security string
TLS bool
HostName string
Port string
NetWork string
WebSocketPath string
WebSocketHeaders map[string]string
SkipCertVerify bool
SessionCache tls.ClientSessionCache
}
// New return a Conn with net.Conn and DstAddr
func (c *Client) New(conn net.Conn, dst *DstAddr) (net.Conn, error) {
var err error
r := rand.Intn(len(c.user))
if c.websocket {
dialer := &websocket.Dialer{
NetDial: func(network, addr string) (net.Conn, error) {
return conn, nil
},
ReadBufferSize: 4 * 1024,
WriteBufferSize: 4 * 1024,
HandshakeTimeout: time.Second * 8,
}
scheme := "ws"
if c.tls {
scheme = "wss"
dialer.TLSClientConfig = &tls.Config{
InsecureSkipVerify: c.skipCertVerify,
}
}
host, port, err := net.SplitHostPort(c.host)
if (scheme == "ws" && port != "80") || (scheme == "wss" && port != "443") {
host = c.host
}
uri := url.URL{
Scheme: scheme,
Host: host,
Path: c.websocketPath,
}
wsConn, resp, err := dialer.Dial(uri.String(), nil)
if c.wsConfig != nil {
conn, err = NewWebsocketConn(conn, c.wsConfig)
if err != nil {
var reason string
if resp != nil {
reason = resp.Status
}
println(uri.String(), err.Error())
return nil, fmt.Errorf("Dial %s error: %s", host, reason)
return nil, err
}
conn = newWebsocketConn(wsConn, conn.RemoteAddr())
} else if c.tls {
conn = tls.Client(conn, &tls.Config{
InsecureSkipVerify: c.skipCertVerify,
})
conn = tls.Client(conn, c.tlsConfig)
}
return newConn(conn, c.user[r], dst, c.security), nil
return newConn(conn, c.user[r], dst, c.security)
}
// NewClient return Client instance
@ -161,13 +133,53 @@ func NewClient(config Config) (*Client, error) {
return nil, fmt.Errorf("Unknown network type: %s", config.NetWork)
}
header := http.Header{}
for k, v := range config.WebSocketHeaders {
header.Add(k, v)
}
host := net.JoinHostPort(config.HostName, config.Port)
var tlsConfig *tls.Config
if config.TLS {
tlsConfig = &tls.Config{
ServerName: config.HostName,
InsecureSkipVerify: config.SkipCertVerify,
ClientSessionCache: config.SessionCache,
}
if tlsConfig.ClientSessionCache == nil {
tlsConfig.ClientSessionCache = getClientSessionCache()
}
if host := header.Get("Host"); host != "" {
tlsConfig.ServerName = host
}
}
var wsConfig *WebsocketConfig
if config.NetWork == "ws" {
wsConfig = &WebsocketConfig{
Host: host,
Path: config.WebSocketPath,
Headers: header,
TLS: config.TLS,
TLSConfig: tlsConfig,
}
}
return &Client{
user: newAlterIDs(newID(&uid), config.AlterID),
uuid: &uid,
security: security,
tls: config.TLS,
host: config.Host,
websocket: config.NetWork == "ws",
websocketPath: config.WebSocketPath,
user: newAlterIDs(newID(&uid), config.AlterID),
uuid: &uid,
security: security,
tls: config.TLS,
host: host,
wsConfig: wsConfig,
tlsConfig: tlsConfig,
}, nil
}
func getClientSessionCache() tls.ClientSessionCache {
once.Do(func() {
clientSessionCache = tls.NewLRUClientSessionCache(128)
})
return clientSessionCache
}

View File

@ -1,9 +1,12 @@
package vmess
import (
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
@ -16,6 +19,14 @@ type websocketConn struct {
remoteAddr net.Addr
}
type WebsocketConfig struct {
Host string
Path string
Headers http.Header
TLS bool
TLSConfig *tls.Config
}
// Read implements net.Conn.Read()
func (wsc *websocketConn) Read(b []byte) (int, error) {
for {
@ -91,9 +102,51 @@ func (wsc *websocketConn) SetWriteDeadline(t time.Time) error {
return wsc.conn.SetWriteDeadline(t)
}
func newWebsocketConn(conn *websocket.Conn, remoteAddr net.Addr) net.Conn {
return &websocketConn{
conn: conn,
remoteAddr: remoteAddr,
func NewWebsocketConn(conn net.Conn, c *WebsocketConfig) (net.Conn, error) {
dialer := &websocket.Dialer{
NetDial: func(network, addr string) (net.Conn, error) {
return conn, nil
},
ReadBufferSize: 4 * 1024,
WriteBufferSize: 4 * 1024,
HandshakeTimeout: time.Second * 8,
}
scheme := "ws"
if c.TLS {
scheme = "wss"
dialer.TLSClientConfig = c.TLSConfig
}
host, port, _ := net.SplitHostPort(c.Host)
if (scheme == "ws" && port != "80") || (scheme == "wss" && port != "443") {
host = c.Host
}
uri := url.URL{
Scheme: scheme,
Host: host,
Path: c.Path,
}
headers := http.Header{}
if c.Headers != nil {
for k := range c.Headers {
headers.Add(k, c.Headers.Get(k))
}
}
wsConn, resp, err := dialer.Dial(uri.String(), headers)
if err != nil {
reason := err.Error()
if resp != nil {
reason = resp.Status
}
return nil, fmt.Errorf("Dial %s error: %s", host, reason)
}
return &websocketConn{
conn: wsConn,
remoteAddr: conn.RemoteAddr(),
}, nil
}

View File

@ -3,259 +3,274 @@ package config
import (
"fmt"
"io/ioutil"
"net"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"time"
adapters "github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/common/observable"
"github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/component/auth"
trie "github.com/Dreamacro/clash/component/domain-trie"
"github.com/Dreamacro/clash/component/fakeip"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/dns"
"github.com/Dreamacro/clash/log"
R "github.com/Dreamacro/clash/rules"
T "github.com/Dreamacro/clash/tunnel"
log "github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2"
)
var (
config *Config
once sync.Once
)
// General config
type General struct {
Port int
SocksPort int
RedirPort int
AllowLan bool
Mode Mode
LogLevel C.LogLevel
Port int `json:"port"`
SocksPort int `json:"socks-port"`
RedirPort int `json:"redir-port"`
Authentication []string `json:"authentication"`
AllowLan bool `json:"allow-lan"`
BindAddress string `json:"bind-address"`
Mode T.Mode `json:"mode"`
LogLevel log.LogLevel `json:"log-level"`
ExternalController string `json:"-"`
ExternalUI string `json:"-"`
Secret string `json:"-"`
}
// ProxyConfig is update proxy schema
type ProxyConfig struct {
Port *int
SocksPort *int
RedirPort *int
AllowLan *bool
// DNS config
type DNS struct {
Enable bool `yaml:"enable"`
IPv6 bool `yaml:"ipv6"`
NameServer []dns.NameServer `yaml:"nameserver"`
Fallback []dns.NameServer `yaml:"fallback"`
FallbackFilter FallbackFilter `yaml:"fallback-filter"`
Listen string `yaml:"listen"`
EnhancedMode dns.EnhancedMode `yaml:"enhanced-mode"`
FakeIPRange *fakeip.Pool
}
// RawConfig is raw config struct
type RawConfig struct {
Port int `yaml:"port"`
SocksPort int `yaml:"socks-port"`
RedirPort int `yaml:"redir-port"`
AllowLan bool `yaml:"allow-lan"`
Mode string `yaml:"mode"`
LogLevel string `yaml:"log-level"`
ExternalController string `yaml:"external-controller"`
Secret string `yaml:"secret"`
// FallbackFilter config
type FallbackFilter struct {
GeoIP bool `yaml:"geoip"`
IPCIDR []*net.IPNet `yaml:"ipcidr"`
}
Proxy []map[string]interface{} `yaml:"Proxy"`
ProxyGroup []map[string]interface{} `yaml:"Proxy Group"`
Rule []string `yaml:"Rule"`
// Experimental config
type Experimental struct {
IgnoreResolveFail bool `yaml:"ignore-resolve-fail"`
}
// Config is clash config manager
type Config struct {
general *General
rules []C.Rule
proxies map[string]C.Proxy
lastUpdate time.Time
event chan<- interface{}
reportCh chan interface{}
observable *observable.Observable
General *General
DNS *DNS
Experimental *Experimental
Hosts *trie.Trie
Rules []C.Rule
Users []auth.AuthUser
Proxies map[string]C.Proxy
}
// Event is event of clash config
type Event struct {
Type string
Payload interface{}
type rawDNS struct {
Enable bool `yaml:"enable"`
IPv6 bool `yaml:"ipv6"`
NameServer []string `yaml:"nameserver"`
Fallback []string `yaml:"fallback"`
FallbackFilter rawFallbackFilter `yaml:"fallback-filter"`
Listen string `yaml:"listen"`
EnhancedMode dns.EnhancedMode `yaml:"enhanced-mode"`
FakeIPRange string `yaml:"fake-ip-range"`
}
// Subscribe config stream
func (c *Config) Subscribe() observable.Subscription {
sub, _ := c.observable.Subscribe()
return sub
type rawFallbackFilter struct {
GeoIP bool `yaml:"geoip"`
IPCIDR []string `yaml:"ipcidr"`
}
// Report return a channel for collecting report message
func (c *Config) Report() chan<- interface{} {
return c.reportCh
type rawConfig struct {
Port int `yaml:"port"`
SocksPort int `yaml:"socks-port"`
RedirPort int `yaml:"redir-port"`
Authentication []string `yaml:"authentication"`
AllowLan bool `yaml:"allow-lan"`
BindAddress string `yaml:"bind-address"`
Mode T.Mode `yaml:"mode"`
LogLevel log.LogLevel `yaml:"log-level"`
ExternalController string `yaml:"external-controller"`
ExternalUI string `yaml:"external-ui"`
Secret string `yaml:"secret"`
Hosts map[string]string `yaml:"hosts"`
DNS rawDNS `yaml:"dns"`
Experimental Experimental `yaml:"experimental"`
Proxy []map[string]interface{} `yaml:"Proxy"`
ProxyGroup []map[string]interface{} `yaml:"Proxy Group"`
Rule []string `yaml:"Rule"`
}
func (c *Config) readConfig() (*RawConfig, error) {
if _, err := os.Stat(C.Path.Config()); os.IsNotExist(err) {
// forward compatibility before 1.0
func readRawConfig(path string) ([]byte, error) {
data, err := ioutil.ReadFile(path)
if err == nil && len(data) != 0 {
return data, nil
}
if filepath.Ext(path) != ".yaml" {
return nil, err
}
data, err := ioutil.ReadFile(C.Path.Config())
path = path[:len(path)-5] + ".yml"
if _, err = os.Stat(path); err == nil {
return ioutil.ReadFile(path)
}
return data, nil
}
func readConfig(path string) (*rawConfig, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err
}
data, err := readRawConfig(path)
if err != nil {
return nil, err
}
if len(data) == 0 {
return nil, fmt.Errorf("Configuration file %s is empty", C.Path.Config())
return nil, fmt.Errorf("Configuration file %s is empty", path)
}
// config with some default value
rawConfig := &RawConfig{
AllowLan: false,
Mode: Rule.String(),
LogLevel: C.INFO.String(),
Rule: []string{},
Proxy: []map[string]interface{}{},
ProxyGroup: []map[string]interface{}{},
rawConfig := &rawConfig{
AllowLan: false,
BindAddress: "*",
Mode: T.Rule,
Authentication: []string{},
LogLevel: log.INFO,
Hosts: map[string]string{},
Rule: []string{},
Proxy: []map[string]interface{}{},
ProxyGroup: []map[string]interface{}{},
Experimental: Experimental{
IgnoreResolveFail: true,
},
DNS: rawDNS{
Enable: false,
FakeIPRange: "198.18.0.1/16",
FallbackFilter: rawFallbackFilter{
GeoIP: true,
IPCIDR: []string{},
},
},
}
err = yaml.Unmarshal([]byte(data), &rawConfig)
return rawConfig, err
}
// Parse config
func (c *Config) Parse() error {
cfg, err := c.readConfig()
func Parse(path string) (*Config, error) {
config := &Config{}
rawCfg, err := readConfig(path)
if err != nil {
return err
return nil, err
}
config.Experimental = &rawCfg.Experimental
if err := c.parseGeneral(cfg); err != nil {
return err
}
if err := c.parseProxies(cfg); err != nil {
return err
}
return c.parseRules(cfg)
}
// Proxies return proxies of clash
func (c *Config) Proxies() map[string]C.Proxy {
return c.proxies
}
// Rules return rules of clash
func (c *Config) Rules() []C.Rule {
return c.rules
}
// SetMode change mode of clash
func (c *Config) SetMode(mode Mode) {
c.general.Mode = mode
c.event <- &Event{Type: "mode", Payload: mode}
}
// SetLogLevel change log level of clash
func (c *Config) SetLogLevel(level C.LogLevel) {
c.general.LogLevel = level
c.event <- &Event{Type: "log-level", Payload: level}
}
// General return clash general config
func (c *Config) General() General {
return *c.general
}
// UpdateRules is a function for hot reload rules
func (c *Config) UpdateRules() error {
cfg, err := c.readConfig()
general, err := parseGeneral(rawCfg)
if err != nil {
return err
return nil, err
}
config.General = general
return c.parseRules(cfg)
proxies, err := parseProxies(rawCfg)
if err != nil {
return nil, err
}
config.Proxies = proxies
rules, err := parseRules(rawCfg, proxies)
if err != nil {
return nil, err
}
config.Rules = rules
dnsCfg, err := parseDNS(rawCfg.DNS)
if err != nil {
return nil, err
}
config.DNS = dnsCfg
hosts, err := parseHosts(rawCfg)
if err != nil {
return nil, err
}
config.Hosts = hosts
config.Users = parseAuthentication(rawCfg.Authentication)
return config, nil
}
func (c *Config) parseGeneral(cfg *RawConfig) error {
func parseGeneral(cfg *rawConfig) (*General, error) {
port := cfg.Port
socksPort := cfg.SocksPort
redirPort := cfg.RedirPort
allowLan := cfg.AllowLan
logLevelString := cfg.LogLevel
modeString := cfg.Mode
bindAddress := cfg.BindAddress
externalController := cfg.ExternalController
externalUI := cfg.ExternalUI
secret := cfg.Secret
mode := cfg.Mode
logLevel := cfg.LogLevel
mode, exist := ModeMapping[modeString]
if !exist {
return fmt.Errorf("General.mode value invalid")
if externalUI != "" {
if !filepath.IsAbs(externalUI) {
externalUI = filepath.Join(C.Path.HomeDir(), externalUI)
}
if _, err := os.Stat(externalUI); os.IsNotExist(err) {
return nil, fmt.Errorf("external-ui: %s not exist", externalUI)
}
}
logLevel, exist := C.LogLevelMapping[logLevelString]
if !exist {
return fmt.Errorf("General.log-level value invalid")
general := &General{
Port: port,
SocksPort: socksPort,
RedirPort: redirPort,
AllowLan: allowLan,
BindAddress: bindAddress,
Mode: mode,
LogLevel: logLevel,
ExternalController: externalController,
ExternalUI: externalUI,
Secret: secret,
}
c.general = &General{
Port: port,
SocksPort: socksPort,
RedirPort: redirPort,
AllowLan: allowLan,
Mode: mode,
LogLevel: logLevel,
}
if restAddr := cfg.ExternalController; restAddr != "" {
c.event <- &Event{Type: "external-controller", Payload: restAddr}
c.event <- &Event{Type: "secret", Payload: cfg.Secret}
}
c.UpdateGeneral(*c.general)
return nil
return general, nil
}
// UpdateGeneral dispatch update event
func (c *Config) UpdateGeneral(general General) {
c.UpdateProxy(ProxyConfig{
Port: &general.Port,
SocksPort: &general.SocksPort,
RedirPort: &general.RedirPort,
AllowLan: &general.AllowLan,
})
c.event <- &Event{Type: "mode", Payload: general.Mode}
c.event <- &Event{Type: "log-level", Payload: general.LogLevel}
}
// UpdateProxy dispatch update proxy event
func (c *Config) UpdateProxy(pc ProxyConfig) {
if pc.AllowLan != nil {
c.general.AllowLan = *pc.AllowLan
}
c.general.Port = *or(pc.Port, &c.general.Port)
if c.general.Port != 0 && (pc.AllowLan != nil || pc.Port != nil) {
c.event <- &Event{Type: "http-addr", Payload: genAddr(c.general.Port, c.general.AllowLan)}
}
c.general.SocksPort = *or(pc.SocksPort, &c.general.SocksPort)
if c.general.SocksPort != 0 && (pc.AllowLan != nil || pc.SocksPort != nil) {
c.event <- &Event{Type: "socks-addr", Payload: genAddr(c.general.SocksPort, c.general.AllowLan)}
}
c.general.RedirPort = *or(pc.RedirPort, &c.general.RedirPort)
if c.general.RedirPort != 0 && (pc.AllowLan != nil || pc.RedirPort != nil) {
c.event <- &Event{Type: "redir-addr", Payload: genAddr(c.general.RedirPort, c.general.AllowLan)}
}
}
func (c *Config) parseProxies(cfg *RawConfig) error {
func parseProxies(cfg *rawConfig) (map[string]C.Proxy, error) {
proxies := make(map[string]C.Proxy)
proxyList := []string{}
proxiesConfig := cfg.Proxy
groupsConfig := cfg.ProxyGroup
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true})
proxies["DIRECT"] = adapters.NewDirect()
proxies["REJECT"] = adapters.NewReject()
proxies["DIRECT"] = adapters.NewProxy(adapters.NewDirect())
proxies["REJECT"] = adapters.NewProxy(adapters.NewReject())
proxyList = append(proxyList, "DIRECT", "REJECT")
// parse proxy
for idx, mapping := range proxiesConfig {
proxyType, existType := mapping["type"].(string)
if !existType {
return fmt.Errorf("Proxy %d missing type", idx)
return nil, fmt.Errorf("Proxy %d missing type", idx)
}
var proxy C.Proxy
var err error
var proxy C.ProxyAdapter
err := fmt.Errorf("cannot parse")
switch proxyType {
case "ss":
ssOption := &adapters.ShadowSocksOption{}
@ -271,6 +286,13 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
break
}
proxy = adapters.NewSocks5(*socksOption)
case "http":
httpOption := &adapters.HttpOption{}
err = decoder.Decode(mapping, httpOption)
if err != nil {
break
}
proxy = adapters.NewHttp(*httpOption)
case "vmess":
vmessOption := &adapters.VmessOption{}
err = decoder.Decode(mapping, vmessOption)
@ -278,33 +300,57 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
break
}
proxy, err = adapters.NewVmess(*vmessOption)
case "snell":
snellOption := &adapters.SnellOption{}
err = decoder.Decode(mapping, snellOption)
if err != nil {
break
}
proxy, err = adapters.NewSnell(*snellOption)
default:
return fmt.Errorf("Unsupport proxy type: %s", proxyType)
return nil, fmt.Errorf("Unsupport proxy type: %s", proxyType)
}
if err != nil {
return fmt.Errorf("Proxy [%d]: %s", idx, err.Error())
return nil, fmt.Errorf("Proxy [%d]: %s", idx, err.Error())
}
if _, exist := proxies[proxy.Name()]; exist {
return fmt.Errorf("Proxy %s is the duplicate name", proxy.Name())
return nil, fmt.Errorf("Proxy %s is the duplicate name", proxy.Name())
}
proxies[proxy.Name()] = proxy
proxies[proxy.Name()] = adapters.NewProxy(proxy)
proxyList = append(proxyList, proxy.Name())
}
// keep the origional order of ProxyGroups in config file
for idx, mapping := range groupsConfig {
groupName, existName := mapping["name"].(string)
if !existName {
return nil, fmt.Errorf("ProxyGroup %d: missing name", idx)
}
proxyList = append(proxyList, groupName)
}
// check if any loop exists and sort the ProxyGroups
if err := proxyGroupsDagSort(groupsConfig, decoder); err != nil {
return nil, err
}
// parse proxy group
for idx, mapping := range groupsConfig {
for _, mapping := range groupsConfig {
groupType, existType := mapping["type"].(string)
groupName, existName := mapping["name"].(string)
if !existType && existName {
return fmt.Errorf("ProxyGroup %d: missing type or name", idx)
groupName, _ := mapping["name"].(string)
if !existType {
return nil, fmt.Errorf("ProxyGroup %s: missing type", groupName)
}
if _, exist := proxies[groupName]; exist {
return fmt.Errorf("ProxyGroup %s: the duplicate name", groupName)
return nil, fmt.Errorf("ProxyGroup %s: the duplicate name", groupName)
}
var group C.Proxy
var err error
var group C.ProxyAdapter
ps := []C.Proxy{}
err := fmt.Errorf("cannot parse")
switch groupType {
case "url-test":
urlTestOption := &adapters.URLTestOption{}
@ -313,9 +359,9 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
break
}
ps, err := getProxies(proxies, urlTestOption.Proxies)
ps, err = getProxies(proxies, urlTestOption.Proxies)
if err != nil {
return fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error())
return nil, fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error())
}
group, err = adapters.NewURLTest(*urlTestOption, ps)
case "select":
@ -325,9 +371,9 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
break
}
ps, err := getProxies(proxies, selectorOption.Proxies)
ps, err = getProxies(proxies, selectorOption.Proxies)
if err != nil {
return fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error())
return nil, fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error())
}
group, err = adapters.NewSelector(selectorOption.Name, ps)
case "fallback":
@ -337,115 +383,257 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
break
}
ps, err := getProxies(proxies, fallbackOption.Proxies)
ps, err = getProxies(proxies, fallbackOption.Proxies)
if err != nil {
return fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error())
return nil, fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error())
}
group, err = adapters.NewFallback(*fallbackOption, ps)
case "load-balance":
loadBalanceOption := &adapters.LoadBalanceOption{}
err = decoder.Decode(mapping, loadBalanceOption)
if err != nil {
break
}
ps, err = getProxies(proxies, loadBalanceOption.Proxies)
if err != nil {
return nil, fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error())
}
group, err = adapters.NewLoadBalance(*loadBalanceOption, ps)
}
if err != nil {
return fmt.Errorf("Proxy %s: %s", groupName, err.Error())
return nil, fmt.Errorf("Proxy %s: %s", groupName, err.Error())
}
proxies[groupName] = group
proxies[groupName] = adapters.NewProxy(group)
}
var ps []C.Proxy
for _, v := range proxies {
ps = append(ps, v)
ps := []C.Proxy{}
for _, v := range proxyList {
ps = append(ps, proxies[v])
}
proxies["GLOBAL"], _ = adapters.NewSelector("GLOBAL", ps)
// close old goroutine
for _, proxy := range c.proxies {
switch raw := proxy.(type) {
case *adapters.URLTest:
raw.Close()
case *adapters.Fallback:
raw.Close()
}
}
c.proxies = proxies
c.event <- &Event{Type: "proxies", Payload: proxies}
return nil
global, _ := adapters.NewSelector("GLOBAL", ps)
proxies["GLOBAL"] = adapters.NewProxy(global)
return proxies, nil
}
func (c *Config) parseRules(cfg *RawConfig) error {
func parseRules(cfg *rawConfig, proxies map[string]C.Proxy) ([]C.Rule, error) {
rules := []C.Rule{}
rulesConfig := cfg.Rule
// parse rules
for _, line := range rulesConfig {
rule := strings.Split(line, ",")
if len(rule) < 3 {
continue
for idx, line := range rulesConfig {
rule := trimArr(strings.Split(line, ","))
var (
payload string
target string
)
switch len(rule) {
case 2:
target = rule[1]
case 3:
payload = rule[1]
target = rule[2]
default:
return nil, fmt.Errorf("Rules[%d] [%s] error: format invalid", idx, line)
}
if _, ok := proxies[target]; !ok {
return nil, fmt.Errorf("Rules[%d] [%s] error: proxy [%s] not found", idx, line, target)
}
rule = trimArr(rule)
var parsed C.Rule
switch rule[0] {
case "DOMAIN":
rules = append(rules, R.NewDomain(rule[1], rule[2]))
parsed = R.NewDomain(payload, target)
case "DOMAIN-SUFFIX":
rules = append(rules, R.NewDomainSuffix(rule[1], rule[2]))
parsed = R.NewDomainSuffix(payload, target)
case "DOMAIN-KEYWORD":
rules = append(rules, R.NewDomainKeyword(rule[1], rule[2]))
parsed = R.NewDomainKeyword(payload, target)
case "GEOIP":
rules = append(rules, R.NewGEOIP(rule[1], rule[2]))
parsed = R.NewGEOIP(payload, target)
case "IP-CIDR", "IP-CIDR6":
rules = append(rules, R.NewIPCIDR(rule[1], rule[2]))
if rule := R.NewIPCIDR(payload, target, false); rule != nil {
parsed = rule
}
// deprecated when bump to 1.0
case "SOURCE-IP-CIDR":
fallthrough
case "SRC-IP-CIDR":
if rule := R.NewIPCIDR(payload, target, true); rule != nil {
parsed = rule
}
case "SRC-PORT":
if rule := R.NewPort(payload, target, true); rule != nil {
parsed = rule
}
case "DST-PORT":
if rule := R.NewPort(payload, target, false); rule != nil {
parsed = rule
}
case "MATCH":
fallthrough
// deprecated when bump to 1.0
case "FINAL":
rules = append(rules, R.NewFinal(rule[2]))
parsed = R.NewMatch(target)
}
if parsed == nil {
return nil, fmt.Errorf("Rules[%d] [%s] error: payload invalid", idx, line)
}
rules = append(rules, parsed)
}
return rules, nil
}
func parseHosts(cfg *rawConfig) (*trie.Trie, error) {
tree := trie.New()
if len(cfg.Hosts) != 0 {
for domain, ipStr := range cfg.Hosts {
ip := net.ParseIP(ipStr)
if ip == nil {
return nil, fmt.Errorf("%s is not a valid IP", ipStr)
}
tree.Insert(domain, ip)
}
}
c.rules = rules
c.event <- &Event{Type: "rules", Payload: rules}
return nil
return tree, nil
}
func (c *Config) handleResponseMessage() {
for elm := range c.reportCh {
event := elm.(*Event)
switch event.Type {
case "http-addr":
if event.Payload.(bool) == false {
log.Errorf("Listening HTTP proxy at %d error", c.general.Port)
c.general.Port = 0
}
case "socks-addr":
if event.Payload.(bool) == false {
log.Errorf("Listening SOCKS proxy at %d error", c.general.SocksPort)
c.general.SocksPort = 0
}
case "redir-addr":
if event.Payload.(bool) == false {
log.Errorf("Listening Redir proxy at %d error", c.general.RedirPort)
c.general.RedirPort = 0
}
func hostWithDefaultPort(host string, defPort string) (string, error) {
if !strings.Contains(host, ":") {
host += ":"
}
hostname, port, err := net.SplitHostPort(host)
if err != nil {
return "", err
}
if port == "" {
port = defPort
}
return net.JoinHostPort(hostname, port), nil
}
func parseNameServer(servers []string) ([]dns.NameServer, error) {
nameservers := []dns.NameServer{}
for idx, server := range servers {
// parse without scheme .e.g 8.8.8.8:53
if !strings.Contains(server, "://") {
server = "udp://" + server
}
u, err := url.Parse(server)
if err != nil {
return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error())
}
var host, dnsNetType string
switch u.Scheme {
case "udp":
host, err = hostWithDefaultPort(u.Host, "53")
dnsNetType = "" // UDP
case "tcp":
host, err = hostWithDefaultPort(u.Host, "53")
dnsNetType = "tcp" // TCP
case "tls":
host, err = hostWithDefaultPort(u.Host, "853")
dnsNetType = "tcp-tls" // DNS over TLS
case "https":
clearURL := url.URL{Scheme: "https", Host: u.Host, Path: u.Path}
host = clearURL.String()
dnsNetType = "https" // DNS over HTTPS
default:
return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme)
}
if err != nil {
return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error())
}
nameservers = append(
nameservers,
dns.NameServer{
Net: dnsNetType,
Addr: host,
},
)
}
return nameservers, nil
}
func parseFallbackIPCIDR(ips []string) ([]*net.IPNet, error) {
ipNets := []*net.IPNet{}
for idx, ip := range ips {
_, ipnet, err := net.ParseCIDR(ip)
if err != nil {
return nil, fmt.Errorf("DNS FallbackIP[%d] format error: %s", idx, err.Error())
}
ipNets = append(ipNets, ipnet)
}
return ipNets, nil
}
func parseDNS(cfg rawDNS) (*DNS, error) {
if cfg.Enable && len(cfg.NameServer) == 0 {
return nil, fmt.Errorf("If DNS configuration is turned on, NameServer cannot be empty")
}
dnsCfg := &DNS{
Enable: cfg.Enable,
Listen: cfg.Listen,
IPv6: cfg.IPv6,
EnhancedMode: cfg.EnhancedMode,
FallbackFilter: FallbackFilter{
IPCIDR: []*net.IPNet{},
},
}
var err error
if dnsCfg.NameServer, err = parseNameServer(cfg.NameServer); err != nil {
return nil, err
}
if dnsCfg.Fallback, err = parseNameServer(cfg.Fallback); err != nil {
return nil, err
}
if cfg.EnhancedMode == dns.FAKEIP {
_, ipnet, err := net.ParseCIDR(cfg.FakeIPRange)
if err != nil {
return nil, err
}
pool, err := fakeip.New(ipnet, 1000)
if err != nil {
return nil, err
}
dnsCfg.FakeIPRange = pool
}
dnsCfg.FallbackFilter.GeoIP = cfg.FallbackFilter.GeoIP
if fallbackip, err := parseFallbackIPCIDR(cfg.FallbackFilter.IPCIDR); err == nil {
dnsCfg.FallbackFilter.IPCIDR = fallbackip
}
return dnsCfg, nil
}
func parseAuthentication(rawRecords []string) []auth.AuthUser {
users := make([]auth.AuthUser, 0)
for _, line := range rawRecords {
userData := strings.SplitN(line, ":", 2)
if len(userData) == 2 {
users = append(users, auth.AuthUser{User: userData[0], Pass: userData[1]})
}
}
}
func newConfig() *Config {
event := make(chan interface{})
reportCh := make(chan interface{})
config := &Config{
general: &General{},
proxies: make(map[string]C.Proxy),
rules: []C.Rule{},
lastUpdate: time.Now(),
event: event,
reportCh: reportCh,
observable: observable.NewObservable(event),
}
go config.handleResponseMessage()
return config
}
// Instance return singleton instance of Config
func Instance() *Config {
once.Do(func() {
config = newConfig()
})
return config
return users
}

View File

@ -3,6 +3,7 @@ package config
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"net/http"
"os"
@ -54,17 +55,17 @@ func downloadMMDB(path string) (err error) {
}
// Init prepare necessary files
func Init() {
func Init(dir string) error {
// initial homedir
if _, err := os.Stat(C.Path.HomeDir()); os.IsNotExist(err) {
if err := os.MkdirAll(C.Path.HomeDir(), 0777); err != nil {
log.Fatalf("Can't create config directory %s: %s", C.Path.HomeDir(), err.Error())
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0777); err != nil {
return fmt.Errorf("Can't create config directory %s: %s", dir, err.Error())
}
}
// initial config.ini
// initial config.yaml
if _, err := os.Stat(C.Path.Config()); os.IsNotExist(err) {
log.Info("Can't find config, create a empty file")
log.Info("Can't find config, create an empty file")
os.OpenFile(C.Path.Config(), os.O_CREATE|os.O_WRONLY, 0644)
}
@ -73,7 +74,8 @@ func Init() {
log.Info("Can't find MMDB, start download")
err := downloadMMDB(C.Path.MMDB())
if err != nil {
log.Fatalf("Can't download MMDB: %s", err.Error())
return fmt.Errorf("Can't download MMDB: %s", err.Error())
}
}
return nil
}

View File

@ -1,31 +0,0 @@
package config
type Mode int
var (
// ModeMapping is a mapping for Mode enum
ModeMapping = map[string]Mode{
"Global": Global,
"Rule": Rule,
"Direct": Direct,
}
)
const (
Global Mode = iota
Rule
Direct
)
func (m Mode) String() string {
switch m {
case Global:
return "Global"
case Rule:
return "Rule"
case Direct:
return "Direct"
default:
return "Unknow"
}
}

View File

@ -4,6 +4,8 @@ import (
"fmt"
"strings"
adapters "github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/common/structure"
C "github.com/Dreamacro/clash/constant"
)
@ -14,13 +16,6 @@ func trimArr(arr []string) (r []string) {
return
}
func genAddr(port int, allowLan bool) string {
if allowLan {
return fmt.Sprintf(":%d", port)
}
return fmt.Sprintf("127.0.0.1:%d", port)
}
func getProxies(mapping map[string]C.Proxy, list []string) ([]C.Proxy, error) {
var ps []C.Proxy
for _, name := range list {
@ -41,3 +36,124 @@ func or(pointers ...*int) *int {
}
return pointers[len(pointers)-1]
}
// Check if ProxyGroups form DAG(Directed Acyclic Graph), and sort all ProxyGroups by dependency order.
// Meanwhile, record the original index in the config file.
// If loop is detected, return an error with location of loop.
func proxyGroupsDagSort(groupsConfig []map[string]interface{}, decoder *structure.Decoder) error {
type graphNode struct {
indegree int
// topological order
topo int
// the origional data in `groupsConfig`
data map[string]interface{}
// `outdegree` and `from` are used in loop locating
outdegree int
from []string
}
graph := make(map[string]*graphNode)
// Step 1.1 build dependency graph
for _, mapping := range groupsConfig {
option := &adapters.ProxyGroupOption{}
err := decoder.Decode(mapping, option)
groupName := option.Name
if err != nil {
return fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error())
}
if node, ok := graph[groupName]; ok {
if node.data != nil {
return fmt.Errorf("ProxyGroup %s: duplicate group name", groupName)
}
node.data = mapping
} else {
graph[groupName] = &graphNode{0, -1, mapping, 0, nil}
}
for _, proxy := range option.Proxies {
if node, ex := graph[proxy]; ex {
node.indegree++
} else {
graph[proxy] = &graphNode{1, -1, nil, 0, nil}
}
}
}
// Step 1.2 Topological Sort
// topological index of **ProxyGroup**
index := 0
queue := make([]string, 0)
for name, node := range graph {
// in the begning, put nodes that have `node.indegree == 0` into queue.
if node.indegree == 0 {
queue = append(queue, name)
}
}
// every element in queue have indegree == 0
for ; len(queue) > 0; queue = queue[1:] {
name := queue[0]
node := graph[name]
if node.data != nil {
index++
groupsConfig[len(groupsConfig)-index] = node.data
for _, proxy := range node.data["proxies"].([]interface{}) {
child := graph[proxy.(string)]
child.indegree--
if child.indegree == 0 {
queue = append(queue, proxy.(string))
}
}
}
delete(graph, name)
}
// no loop is detected, return sorted ProxyGroup
if len(graph) == 0 {
return nil
}
// if loop is detected, locate the loop and throw an error
// Step 2.1 rebuild the graph, fill `outdegree` and `from` filed
for name, node := range graph {
if node.data == nil {
continue
}
for _, proxy := range node.data["proxies"].([]interface{}) {
node.outdegree++
child := graph[proxy.(string)]
if child.from == nil {
child.from = make([]string, 0, child.indegree)
}
child.from = append(child.from, name)
}
}
// Step 2.2 remove nodes outside the loop. so that we have only the loops remain in `graph`
queue = make([]string, 0)
// initialize queue with node have outdegree == 0
for name, node := range graph {
if node.outdegree == 0 {
queue = append(queue, name)
}
}
// every element in queue have outdegree == 0
for ; len(queue) > 0; queue = queue[1:] {
name := queue[0]
node := graph[name]
for _, f := range node.from {
graph[f].outdegree--
if graph[f].outdegree == 0 {
queue = append(queue, f)
}
}
delete(graph, name)
}
// Step 2.3 report the elements in loop
loopElements := make([]string, 0, len(graph))
for name := range graph {
loopElements = append(loopElements, name)
delete(graph, name)
}
return fmt.Errorf("Loop is detected in ProxyGroup, please check following ProxyGroups: %v", loopElements)
}

View File

@ -1,7 +1,10 @@
package constant
import (
"context"
"fmt"
"net"
"time"
)
// Adapter Type
@ -11,25 +14,69 @@ const (
Reject
Selector
Shadowsocks
Snell
Socks5
Http
URLTest
Vmess
LoadBalance
)
type ProxyAdapter interface {
Conn() net.Conn
Close()
type ServerAdapter interface {
net.Conn
Metadata() *Metadata
}
type ServerAdapter interface {
Metadata() *Metadata
Close()
type Connection interface {
Chains() Chain
AppendToChains(adapter ProxyAdapter)
}
type Chain []string
func (c Chain) String() string {
switch len(c) {
case 0:
return ""
case 1:
return c[0]
default:
return fmt.Sprintf("%s[%s]", c[len(c)-1], c[0])
}
}
type Conn interface {
net.Conn
Connection
}
type PacketConn interface {
net.PacketConn
Connection
}
type ProxyAdapter interface {
Name() string
Type() AdapterType
DialContext(ctx context.Context, metadata *Metadata) (Conn, error)
DialUDP(metadata *Metadata) (PacketConn, net.Addr, error)
SupportUDP() bool
Destroy()
MarshalJSON() ([]byte, error)
}
type DelayHistory struct {
Time time.Time `json:"time"`
Delay uint16 `json:"delay"`
}
type Proxy interface {
Name() string
Type() AdapterType
Generator(metadata *Metadata) (ProxyAdapter, error)
ProxyAdapter
Alive() bool
DelayHistory() []DelayHistory
Dial(metadata *Metadata) (Conn, error)
LastDelay() uint16
URLTest(ctx context.Context, url string) (uint16, error)
}
// AdapterType is enum of adapter type
@ -47,13 +94,19 @@ func (at AdapterType) String() string {
return "Selector"
case Shadowsocks:
return "Shadowsocks"
case Snell:
return "Snell"
case Socks5:
return "Socks5"
case Http:
return "Http"
case URLTest:
return "URLTest"
case Vmess:
return "Vmess"
case LoadBalance:
return "LoadBalance"
default:
return "Unknow"
return "Unknown"
}
}

View File

@ -1,10 +0,0 @@
package constant
type General struct {
Mode *string `json:"mode,omitempty"`
AllowLan *bool `json:"allow-lan,omitempty"`
Port *int `json:"port,omitempty"`
SocksPort *int `json:"socks-port,omitempty"`
RedirPort *int `json:"redir-port,omitempty"`
LogLevel *string `json:"log-level,omitempty"`
}

View File

@ -1,35 +0,0 @@
package constant
var (
// LogLevelMapping is a mapping for LogLevel enum
LogLevelMapping = map[string]LogLevel{
"error": ERROR,
"warning": WARNING,
"info": INFO,
"debug": DEBUG,
}
)
const (
ERROR LogLevel = iota
WARNING
INFO
DEBUG
)
type LogLevel int
func (l LogLevel) String() string {
switch l {
case INFO:
return "info"
case WARNING:
return "warning"
case ERROR:
return "error"
case DEBUG:
return "debug"
default:
return "unknow"
}
}

View File

@ -13,8 +13,9 @@ const (
TCP NetWork = iota
UDP
HTTP SourceType = iota
HTTP Type = iota
SOCKS
REDIR
)
type NetWork int
@ -26,21 +27,34 @@ func (n *NetWork) String() string {
return "udp"
}
type SourceType int
type Type int
// Metadata is used to store connection address
type Metadata struct {
NetWork NetWork
Source SourceType
Type Type
SrcIP *net.IP
DstIP *net.IP
SrcPort string
DstPort string
AddrType int
Host string
IP *net.IP
Port string
}
func (addr *Metadata) String() string {
if addr.Host == "" {
return addr.IP.String()
}
return addr.Host
func (m *Metadata) RemoteAddress() string {
return net.JoinHostPort(m.String(), m.DstPort)
}
func (m *Metadata) String() string {
if m.Host != "" {
return m.Host
} else if m.DstIP != nil {
return m.DstIP.String()
} else {
return "<nil>"
}
}
func (m *Metadata) Valid() bool {
return m.Host != "" || m.DstIP != nil
}

View File

@ -2,7 +2,6 @@ package constant
import (
"os"
"os/user"
P "path"
)
@ -16,17 +15,11 @@ type path struct {
}
func init() {
currentUser, err := user.Current()
var homedir string
homedir, err := os.UserHomeDir()
if err != nil {
dir := os.Getenv("HOME")
if dir == "" {
dir, _ = os.Getwd()
}
homedir = dir
} else {
homedir = currentUser.HomeDir
homedir, _ = os.Getwd()
}
homedir = P.Join(homedir, ".config", Name)
Path = &path{homedir: homedir}
}
@ -41,7 +34,7 @@ func (p *path) HomeDir() string {
}
func (p *path) Config() string {
return P.Join(p.homedir, "config.yml")
return P.Join(p.homedir, "config.yaml")
}
func (p *path) MMDB() string {

View File

@ -1,7 +0,0 @@
package constant
// ProxySignal is used to handle graceful shutdown of proxy
type ProxySignal struct {
Done chan<- struct{}
Closed <-chan struct{}
}

View File

@ -7,7 +7,10 @@ const (
DomainKeyword
GEOIP
IPCIDR
FINAL
SrcIPCIDR
SrcPort
DstPort
MATCH
)
type RuleType int
@ -24,10 +27,16 @@ func (rt RuleType) String() string {
return "GEOIP"
case IPCIDR:
return "IPCIDR"
case FINAL:
return "FINAL"
case SrcIPCIDR:
return "SrcIPCIDR"
case SrcPort:
return "SrcPort"
case DstPort:
return "DstPort"
case MATCH:
return "MATCH"
default:
return "Unknow"
return "Unknown"
}
}

6
constant/version.go Normal file
View File

@ -0,0 +1,6 @@
package constant
var (
Version = "unknown version"
BuildTime = "unknown time"
)

21
dns/client.go Normal file
View File

@ -0,0 +1,21 @@
package dns
import (
"context"
D "github.com/miekg/dns"
)
type client struct {
*D.Client
Address string
}
func (c *client) Exchange(m *D.Msg) (msg *D.Msg, err error) {
return c.ExchangeContext(context.Background(), m)
}
func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
msg, _, err = c.Client.ExchangeContext(ctx, m, c.Address)
return
}

75
dns/doh.go Normal file
View File

@ -0,0 +1,75 @@
package dns
import (
"bytes"
"context"
"crypto/tls"
"io/ioutil"
"net/http"
D "github.com/miekg/dns"
)
const (
// dotMimeType is the DoH mimetype that should be used.
dotMimeType = "application/dns-message"
// dotPath is the URL path that should be used.
dotPath = "/dns-query"
)
var dohTransport = &http.Transport{
TLSClientConfig: &tls.Config{ClientSessionCache: globalSessionCache},
}
type dohClient struct {
url string
}
func (dc *dohClient) Exchange(m *D.Msg) (msg *D.Msg, err error) {
return dc.ExchangeContext(context.Background(), m)
}
func (dc *dohClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
req, err := dc.newRequest(m)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
return dc.doRequest(req)
}
// newRequest returns a new DoH request given a dns.Msg.
func (dc *dohClient) newRequest(m *D.Msg) (*http.Request, error) {
buf, err := m.Pack()
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, dc.url+"?bla=foo:443", bytes.NewReader(buf))
if err != nil {
return req, err
}
req.Header.Set("content-type", dotMimeType)
req.Header.Set("accept", dotMimeType)
return req, nil
}
func (dc *dohClient) doRequest(req *http.Request) (msg *D.Msg, err error) {
client := &http.Client{Transport: dohTransport}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
buf, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
msg = &D.Msg{}
err = msg.Unpack(buf)
return msg, err
}

26
dns/filters.go Normal file
View File

@ -0,0 +1,26 @@
package dns
import "net"
type fallbackFilter interface {
Match(net.IP) bool
}
type geoipFilter struct{}
func (gf *geoipFilter) Match(ip net.IP) bool {
if mmdb == nil {
return false
}
record, _ := mmdb.Country(ip)
return record.Country.IsoCode == "CN" || record.Country.IsoCode == ""
}
type ipnetFilter struct {
ipnet *net.IPNet
}
func (inf *ipnetFilter) Match(ip net.IP) bool {
return inf.ipnet.Contains(ip)
}

106
dns/iputil.go Normal file
View File

@ -0,0 +1,106 @@
package dns
import (
"errors"
"net"
"strings"
)
var (
errIPNotFound = errors.New("cannot found ip")
errIPVersion = errors.New("ip version error")
)
// ResolveIPv4 with a host, return ipv4
func ResolveIPv4(host string) (net.IP, error) {
if node := DefaultHosts.Search(host); node != nil {
if ip := node.Data.(net.IP).To4(); ip != nil {
return ip, nil
}
}
ip := net.ParseIP(host)
if ip != nil {
if !strings.Contains(host, ":") {
return ip, nil
}
return nil, errIPVersion
}
if DefaultResolver != nil {
return DefaultResolver.ResolveIPv4(host)
}
ipAddrs, err := net.LookupIP(host)
if err != nil {
return nil, err
}
for _, ip := range ipAddrs {
if ip4 := ip.To4(); ip4 != nil {
return ip4, nil
}
}
return nil, errIPNotFound
}
// ResolveIPv6 with a host, return ipv6
func ResolveIPv6(host string) (net.IP, error) {
if node := DefaultHosts.Search(host); node != nil {
if ip := node.Data.(net.IP).To16(); ip != nil {
return ip, nil
}
}
ip := net.ParseIP(host)
if ip != nil {
if strings.Contains(host, ":") {
return ip, nil
}
return nil, errIPVersion
}
if DefaultResolver != nil {
return DefaultResolver.ResolveIPv6(host)
}
ipAddrs, err := net.LookupIP(host)
if err != nil {
return nil, err
}
for _, ip := range ipAddrs {
if ip.To4() == nil {
return ip, nil
}
}
return nil, errIPNotFound
}
// ResolveIP with a host, return ip
func ResolveIP(host string) (net.IP, error) {
if node := DefaultHosts.Search(host); node != nil {
return node.Data.(net.IP), nil
}
if DefaultResolver != nil {
if DefaultResolver.ipv6 {
return DefaultResolver.ResolveIP(host)
}
return DefaultResolver.ResolveIPv4(host)
}
ip := net.ParseIP(host)
if ip != nil {
return ip, nil
}
ipAddr, err := net.ResolveIPAddr("ip", host)
if err != nil {
return nil, err
}
return ipAddr.IP, nil
}

79
dns/middleware.go Normal file
View File

@ -0,0 +1,79 @@
package dns
import (
"strings"
"github.com/Dreamacro/clash/component/fakeip"
"github.com/Dreamacro/clash/log"
D "github.com/miekg/dns"
)
type handler func(w D.ResponseWriter, r *D.Msg)
type middleware func(next handler) handler
func withFakeIP(fakePool *fakeip.Pool) middleware {
return func(next handler) handler {
return func(w D.ResponseWriter, r *D.Msg) {
q := r.Question[0]
if q.Qtype == D.TypeAAAA {
D.HandleFailed(w, r)
return
} else if q.Qtype != D.TypeA {
next(w, r)
return
}
host := strings.TrimRight(q.Name, ".")
rr := &D.A{}
rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: dnsDefaultTTL}
ip := fakePool.Lookup(host)
rr.A = ip
msg := r.Copy()
msg.Answer = []D.RR{rr}
setMsgTTL(msg, 1)
msg.SetReply(r)
w.WriteMsg(msg)
return
}
}
}
func withResolver(resolver *Resolver) handler {
return func(w D.ResponseWriter, r *D.Msg) {
msg, err := resolver.Exchange(r)
if err != nil {
q := r.Question[0]
log.Debugln("[DNS Server] Exchange %s failed: %v", q.String(), err)
D.HandleFailed(w, r)
return
}
msg.SetReply(r)
w.WriteMsg(msg)
return
}
}
func compose(middlewares []middleware, endpoint handler) handler {
length := len(middlewares)
h := endpoint
for i := length - 1; i >= 0; i-- {
middleware := middlewares[i]
h = middleware(h)
}
return h
}
func newHandler(resolver *Resolver) handler {
middlewares := []middleware{}
if resolver.IsFakeIP() {
middlewares = append(middlewares, withFakeIP(resolver.pool))
}
return compose(middlewares, withResolver(resolver))
}

317
dns/resolver.go Normal file
View File

@ -0,0 +1,317 @@
package dns
import (
"context"
"crypto/tls"
"errors"
"net"
"strings"
"sync"
"time"
"github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/common/picker"
trie "github.com/Dreamacro/clash/component/domain-trie"
"github.com/Dreamacro/clash/component/fakeip"
C "github.com/Dreamacro/clash/constant"
D "github.com/miekg/dns"
geoip2 "github.com/oschwald/geoip2-golang"
"golang.org/x/sync/singleflight"
)
var (
// DefaultResolver aim to resolve ip
DefaultResolver *Resolver
// DefaultHosts aim to resolve hosts
DefaultHosts = trie.New()
)
var (
globalSessionCache = tls.NewLRUClientSessionCache(64)
mmdb *geoip2.Reader
once sync.Once
)
type resolver interface {
Exchange(m *D.Msg) (msg *D.Msg, err error)
ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error)
}
type result struct {
Msg *D.Msg
Error error
}
type Resolver struct {
ipv6 bool
mapping bool
fakeip bool
pool *fakeip.Pool
main []resolver
fallback []resolver
fallbackFilters []fallbackFilter
group singleflight.Group
cache *cache.Cache
}
// ResolveIP request with TypeA and TypeAAAA, priority return TypeAAAA
func (r *Resolver) ResolveIP(host string) (ip net.IP, err error) {
ch := make(chan net.IP)
go func() {
defer close(ch)
ip, err := r.resolveIP(host, D.TypeA)
if err != nil {
return
}
ch <- ip
}()
ip, err = r.resolveIP(host, D.TypeAAAA)
if err == nil {
go func() {
<-ch
}()
return
}
ip, open := <-ch
if !open {
return nil, errIPNotFound
}
return ip, nil
}
// ResolveIPv4 request with TypeA
func (r *Resolver) ResolveIPv4(host string) (ip net.IP, err error) {
return r.resolveIP(host, D.TypeA)
}
// ResolveIPv6 request with TypeAAAA
func (r *Resolver) ResolveIPv6(host string) (ip net.IP, err error) {
return r.resolveIP(host, D.TypeAAAA)
}
func (r *Resolver) shouldFallback(ip net.IP) bool {
for _, filter := range r.fallbackFilters {
if filter.Match(ip) {
return true
}
}
return false
}
// Exchange a batch of dns request, and it use cache
func (r *Resolver) Exchange(m *D.Msg) (msg *D.Msg, err error) {
if len(m.Question) == 0 {
return nil, errors.New("should have one question at least")
}
q := m.Question[0]
cache, expireTime := r.cache.GetWithExpire(q.String())
if cache != nil {
msg = cache.(*D.Msg).Copy()
setMsgTTL(msg, uint32(expireTime.Sub(time.Now()).Seconds()))
return
}
defer func() {
if msg == nil {
return
}
putMsgToCache(r.cache, q.String(), msg)
if r.mapping {
ips := r.msgToIP(msg)
for _, ip := range ips {
putMsgToCache(r.cache, ip.String(), msg)
}
}
}()
ret, err, _ := r.group.Do(q.String(), func() (interface{}, error) {
isIPReq := isIPRequest(q)
if isIPReq {
msg, err := r.fallbackExchange(m)
return msg, err
}
return r.batchExchange(r.main, m)
})
if err == nil {
msg = ret.(*D.Msg)
}
return
}
// IPToHost return fake-ip or redir-host mapping host
func (r *Resolver) IPToHost(ip net.IP) (string, bool) {
if r.fakeip {
return r.pool.LookBack(ip)
}
cache := r.cache.Get(ip.String())
if cache == nil {
return "", false
}
fqdn := cache.(*D.Msg).Question[0].Name
return strings.TrimRight(fqdn, "."), true
}
func (r *Resolver) IsMapping() bool {
return r.mapping
}
func (r *Resolver) IsFakeIP() bool {
return r.fakeip
}
func (r *Resolver) batchExchange(clients []resolver, m *D.Msg) (msg *D.Msg, err error) {
fast, ctx := picker.WithTimeout(context.Background(), time.Second)
for _, client := range clients {
r := client
fast.Go(func() (interface{}, error) {
msg, err := r.ExchangeContext(ctx, m)
if err != nil || msg.Rcode != D.RcodeSuccess {
return nil, errors.New("resolve error")
}
return msg, nil
})
}
elm := fast.Wait()
if elm == nil {
return nil, errors.New("All DNS requests failed")
}
msg = elm.(*D.Msg)
return
}
func (r *Resolver) fallbackExchange(m *D.Msg) (msg *D.Msg, err error) {
msgCh := r.asyncExchange(r.main, m)
if r.fallback == nil {
res := <-msgCh
msg, err = res.Msg, res.Error
return
}
fallbackMsg := r.asyncExchange(r.fallback, m)
res := <-msgCh
if res.Error == nil {
if ips := r.msgToIP(res.Msg); len(ips) != 0 {
if r.shouldFallback(ips[0]) {
go func() { <-fallbackMsg }()
msg = res.Msg
return msg, err
}
}
}
res = <-fallbackMsg
msg, err = res.Msg, res.Error
return
}
func (r *Resolver) resolveIP(host string, dnsType uint16) (ip net.IP, err error) {
ip = net.ParseIP(host)
if ip != nil {
isIPv4 := ip.To4() != nil
if dnsType == D.TypeAAAA && !isIPv4 {
return ip, nil
} else if dnsType == D.TypeA && isIPv4 {
return ip, nil
}
}
query := &D.Msg{}
query.SetQuestion(D.Fqdn(host), dnsType)
msg, err := r.Exchange(query)
if err != nil {
return nil, err
}
ips := r.msgToIP(msg)
if len(ips) == 0 {
return nil, errIPNotFound
}
ip = ips[0]
return
}
func (r *Resolver) msgToIP(msg *D.Msg) []net.IP {
ips := []net.IP{}
for _, answer := range msg.Answer {
switch ans := answer.(type) {
case *D.AAAA:
ips = append(ips, ans.AAAA)
case *D.A:
ips = append(ips, ans.A)
}
}
return ips
}
func (r *Resolver) asyncExchange(client []resolver, msg *D.Msg) <-chan *result {
ch := make(chan *result)
go func() {
res, err := r.batchExchange(client, msg)
ch <- &result{Msg: res, Error: err}
}()
return ch
}
type NameServer struct {
Net string
Addr string
}
type FallbackFilter struct {
GeoIP bool
IPCIDR []*net.IPNet
}
type Config struct {
Main, Fallback []NameServer
IPv6 bool
EnhancedMode EnhancedMode
FallbackFilter FallbackFilter
Pool *fakeip.Pool
}
func New(config Config) *Resolver {
r := &Resolver{
ipv6: config.IPv6,
main: transform(config.Main),
cache: cache.New(time.Second * 60),
mapping: config.EnhancedMode == MAPPING,
fakeip: config.EnhancedMode == FAKEIP,
pool: config.Pool,
}
if len(config.Fallback) != 0 {
r.fallback = transform(config.Fallback)
}
fallbackFilters := []fallbackFilter{}
if config.FallbackFilter.GeoIP {
once.Do(func() {
mmdb, _ = geoip2.Open(C.Path.MMDB())
})
fallbackFilters = append(fallbackFilters, &geoipFilter{})
}
for _, ipnet := range config.FallbackFilter.IPCIDR {
fallbackFilters = append(fallbackFilters, &ipnetFilter{ipnet: ipnet})
}
r.fallbackFilters = fallbackFilters
return r
}

70
dns/server.go Normal file
View File

@ -0,0 +1,70 @@
package dns
import (
"net"
D "github.com/miekg/dns"
)
var (
address string
server = &Server{}
dnsDefaultTTL uint32 = 600
)
type Server struct {
*D.Server
handler handler
}
func (s *Server) ServeDNS(w D.ResponseWriter, r *D.Msg) {
if len(r.Question) == 0 {
D.HandleFailed(w, r)
return
}
s.handler(w, r)
}
func (s *Server) setHandler(handler handler) {
s.handler = handler
}
func ReCreateServer(addr string, resolver *Resolver) error {
if addr == address && resolver != nil {
handler := newHandler(resolver)
server.setHandler(handler)
return nil
}
if server.Server != nil {
server.Shutdown()
address = ""
}
_, port, err := net.SplitHostPort(addr)
if port == "0" || port == "" || err != nil {
return nil
}
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return err
}
p, err := net.ListenUDP("udp", udpAddr)
if err != nil {
return err
}
address = addr
handler := newHandler(resolver)
server = &Server{handler: handler}
server.Server = &D.Server{Addr: addr, PacketConn: p, Handler: server}
go func() {
server.ActivateAndServe()
}()
return nil
}

141
dns/util.go Normal file
View File

@ -0,0 +1,141 @@
package dns
import (
"crypto/tls"
"encoding/json"
"errors"
"time"
"github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/log"
yaml "gopkg.in/yaml.v2"
D "github.com/miekg/dns"
)
var (
// EnhancedModeMapping is a mapping for EnhancedMode enum
EnhancedModeMapping = map[string]EnhancedMode{
NORMAL.String(): NORMAL,
FAKEIP.String(): FAKEIP,
MAPPING.String(): MAPPING,
}
)
const (
NORMAL EnhancedMode = iota
FAKEIP
MAPPING
)
type EnhancedMode int
// UnmarshalYAML unserialize EnhancedMode with yaml
func (e *EnhancedMode) UnmarshalYAML(unmarshal func(interface{}) error) error {
var tp string
if err := unmarshal(&tp); err != nil {
return err
}
mode, exist := EnhancedModeMapping[tp]
if !exist {
return errors.New("invalid mode")
}
*e = mode
return nil
}
// MarshalYAML serialize EnhancedMode with yaml
func (e EnhancedMode) MarshalYAML() ([]byte, error) {
return yaml.Marshal(e.String())
}
// UnmarshalJSON unserialize EnhancedMode with json
func (e *EnhancedMode) UnmarshalJSON(data []byte) error {
var tp string
json.Unmarshal(data, &tp)
mode, exist := EnhancedModeMapping[tp]
if !exist {
return errors.New("invalid mode")
}
*e = mode
return nil
}
// MarshalJSON serialize EnhancedMode with json
func (e EnhancedMode) MarshalJSON() ([]byte, error) {
return json.Marshal(e.String())
}
func (e EnhancedMode) String() string {
switch e {
case NORMAL:
return "normal"
case FAKEIP:
return "fake-ip"
case MAPPING:
return "redir-host"
default:
return "unknown"
}
}
func putMsgToCache(c *cache.Cache, key string, msg *D.Msg) {
var ttl time.Duration
if len(msg.Answer) != 0 {
ttl = time.Duration(msg.Answer[0].Header().Ttl) * time.Second
} else if len(msg.Ns) != 0 {
ttl = time.Duration(msg.Ns[0].Header().Ttl) * time.Second
} else if len(msg.Extra) != 0 {
ttl = time.Duration(msg.Extra[0].Header().Ttl) * time.Second
} else {
log.Debugln("[DNS] response msg error: %#v", msg)
return
}
c.Put(key, msg.Copy(), ttl)
}
func setMsgTTL(msg *D.Msg, ttl uint32) {
for _, answer := range msg.Answer {
answer.Header().Ttl = ttl
}
for _, ns := range msg.Ns {
ns.Header().Ttl = ttl
}
for _, extra := range msg.Extra {
extra.Header().Ttl = ttl
}
}
func isIPRequest(q D.Question) bool {
if q.Qclass == D.ClassINET && (q.Qtype == D.TypeA || q.Qtype == D.TypeAAAA) {
return true
}
return false
}
func transform(servers []NameServer) []resolver {
ret := []resolver{}
for _, s := range servers {
if s.Net == "https" {
ret = append(ret, &dohClient{url: s.Addr})
continue
}
ret = append(ret, &client{
Client: &D.Client{
Net: s.Net,
TLSConfig: &tls.Config{
ClientSessionCache: globalSessionCache,
// alpn identifier, see https://tools.ietf.org/html/draft-hoffman-dprive-dns-tls-alpn-00#page-6
NextProtos: []string{"dns"},
},
UDPSize: 4096,
},
Address: s.Addr,
})
}
return ret
}

24
go.mod
View File

@ -1,17 +1,23 @@
module github.com/Dreamacro/clash
go 1.13
require (
github.com/Dreamacro/go-shadowsocks2 v0.1.2-0.20181019110427-0a03f1a25270
github.com/Dreamacro/go-shadowsocks2 v0.1.5-0.20191012162057-46254afc8b68
github.com/eapache/queue v1.1.0 // indirect
github.com/go-chi/chi v3.3.3+incompatible
github.com/go-chi/chi v4.0.2+incompatible
github.com/go-chi/cors v1.0.0
github.com/go-chi/render v1.0.1
github.com/gofrs/uuid v3.1.0+incompatible
github.com/gorilla/websocket v1.4.0
github.com/oschwald/geoip2-golang v1.2.1
github.com/oschwald/maxminddb-golang v1.3.0 // indirect
github.com/sirupsen/logrus v1.1.0
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941
github.com/gofrs/uuid v3.2.0+incompatible
github.com/gorilla/websocket v1.4.1
github.com/miekg/dns v1.1.22
github.com/oschwald/geoip2-golang v1.3.0
github.com/oschwald/maxminddb-golang v1.5.0 // indirect
github.com/sirupsen/logrus v1.4.2
github.com/stretchr/testify v1.4.0
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
golang.org/x/net v0.0.0-20191011234655-491137f69257
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
gopkg.in/eapache/channels.v1 v1.1.0
gopkg.in/yaml.v2 v2.2.1
gopkg.in/yaml.v2 v2.2.2
)

80
go.sum
View File

@ -1,43 +1,73 @@
github.com/Dreamacro/go-shadowsocks2 v0.1.2-0.20181019110427-0a03f1a25270 h1:ugkI+Yw5ArFnhF8KTbJxyWIyvxMCa8jWyUF+wIAulhM=
github.com/Dreamacro/go-shadowsocks2 v0.1.2-0.20181019110427-0a03f1a25270/go.mod h1:DlkXRxmh5K+99aTPQaVjsZ1fAZNFw42vXGcOjR3Otps=
github.com/Dreamacro/go-shadowsocks2 v0.1.5-0.20191012162057-46254afc8b68 h1:UBDLpj1IGVkUcUBuZWE6DmZApPTZcnmcV6AfyDN/yhg=
github.com/Dreamacro/go-shadowsocks2 v0.1.5-0.20191012162057-46254afc8b68/go.mod h1:Y8obOtHDOqxMGHjPglfCiXZBKExOA9VL6I6sJagOwYM=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/go-chi/chi v3.3.3+incompatible h1:KHkmBEMNkwKuK4FdQL7N2wOeB9jnIx7jR5wsuSBEFI8=
github.com/go-chi/chi v3.3.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/cors v1.0.0 h1:e6x8k7uWbUwYs+aXDoiUzeQFT6l0cygBYyNhD7/1Tg0=
github.com/go-chi/cors v1.0.0/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/gofrs/uuid v3.1.0+incompatible h1:q2rtkjaKT4YEr6E1kamy0Ha4RtepWlQBedyHx0uzKwA=
github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe h1:CHRGQ8V7OlCYtwaKPJi3iA7J+YdNKdo8j7nG5IgDhjs=
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/oschwald/geoip2-golang v1.2.1 h1:3iz+jmeJc6fuCyWeKgtXSXu7+zvkxJbHFXkMT5FVebU=
github.com/oschwald/geoip2-golang v1.2.1/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE=
github.com/oschwald/maxminddb-golang v1.3.0 h1:oTh8IBSj10S5JNlUDg5WjJ1QdBMdeaZIkPEVfESSWgE=
github.com/oschwald/maxminddb-golang v1.3.0/go.mod h1:3jhIUymTJ5VREKyIhWm66LJiQt04F0UCDdodShpjWsY=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/miekg/dns v1.1.22 h1:Jm64b3bO9kP43ddLjL2EY3Io6bmy1qGb9Xxz6TqS6rc=
github.com/miekg/dns v1.1.22/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/oschwald/geoip2-golang v1.3.0 h1:D+Hsdos1NARPbzZ2aInUHZL+dApIzo8E0ErJVsWcku8=
github.com/oschwald/geoip2-golang v1.3.0/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE=
github.com/oschwald/maxminddb-golang v1.5.0 h1:rmyoIV6z2/s9TCJedUuDiKht2RN12LWJ1L7iRGtWY64=
github.com/oschwald/maxminddb-golang v1.5.0/go.mod h1:3jhIUymTJ5VREKyIhWm66LJiQt04F0UCDdodShpjWsY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.1.0 h1:65VZabgUiV9ktjGM5nTq0+YurgTyX+YI2lSSfDjI+qU=
github.com/sirupsen/logrus v1.1.0/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941 h1:qBTHLajHecfu+xzRI9PqVDcqx7SdHj9d4B+EzSn3tAc=
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181005133103-4497e2df6f9e h1:EfdBzeKbFSvOjoIqSZcfS8wp0FBLokGBEs9lz1OtSg0=
golang.org/x/sys v0.0.0-20181005133103-4497e2df6f9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191011234655-491137f69257 h1:ry8e2D+cwaV6hk7lb3aRTjjZo24shrbK0e11QEOkTIg=
golang.org/x/net v0.0.0-20191011234655-491137f69257/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/eapache/channels.v1 v1.1.0 h1:5bGAyKKvyCTWjSj7mhefG6Lc68VyN4MH1v8/7OoeeB4=
gopkg.in/eapache/channels.v1 v1.1.0/go.mod h1:BHIBujSvu9yMTrTYbTCjDD43gUhtmaOtTWDe7sTv1js=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -1,33 +0,0 @@
package hub
import (
"github.com/Dreamacro/clash/config"
"github.com/Dreamacro/clash/proxy"
T "github.com/Dreamacro/clash/tunnel"
)
var (
tunnel = T.Instance()
cfg = config.Instance()
listener = proxy.Instance()
)
type Error struct {
Error string `json:"error"`
}
type Errors struct {
Errors map[string]string `json:"errors"`
}
func formatErrors(errorsMap map[string]error) (bool, Errors) {
errors := make(map[string]string)
hasError := false
for key, err := range errorsMap {
if err != nil {
errors[key] = err.Error()
hasError = true
}
}
return hasError, Errors{Errors: errors}
}

View File

@ -1,96 +0,0 @@
package hub
import (
"fmt"
"net/http"
"github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant"
"github.com/go-chi/chi"
"github.com/go-chi/render"
)
func configRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getConfigs)
r.Put("/", updateConfigs)
return r
}
type configSchema struct {
Port int `json:"port"`
SocksPort int `json:"socket-port"`
RedirPort int `json:"redir-port"`
AllowLan bool `json:"allow-lan"`
Mode string `json:"mode"`
LogLevel string `json:"log-level"`
}
func getConfigs(w http.ResponseWriter, r *http.Request) {
general := cfg.General()
render.JSON(w, r, configSchema{
Port: general.Port,
SocksPort: general.SocksPort,
RedirPort: general.RedirPort,
AllowLan: general.AllowLan,
Mode: general.Mode.String(),
LogLevel: general.LogLevel.String(),
})
}
func updateConfigs(w http.ResponseWriter, r *http.Request) {
general := &C.General{}
err := render.DecodeJSON(r.Body, general)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Format error",
})
return
}
// update errors
var modeErr, logLevelErr error
// update mode
if general.Mode != nil {
mode, ok := config.ModeMapping[*general.Mode]
if !ok {
modeErr = fmt.Errorf("Mode error")
} else {
cfg.SetMode(mode)
}
}
// update log-level
if general.LogLevel != nil {
level, ok := C.LogLevelMapping[*general.LogLevel]
if !ok {
logLevelErr = fmt.Errorf("Log Level error")
} else {
cfg.SetLogLevel(level)
}
}
hasError, errors := formatErrors(map[string]error{
"mode": modeErr,
"log-level": logLevelErr,
})
if hasError {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, errors)
return
}
// update proxy
cfg.UpdateProxy(config.ProxyConfig{
AllowLan: general.AllowLan,
Port: general.Port,
SocksPort: general.SocksPort,
RedirPort: general.RedirPort,
})
w.WriteHeader(http.StatusNoContent)
}

140
hub/executor/executor.go Normal file
View File

@ -0,0 +1,140 @@
package executor
import (
"github.com/Dreamacro/clash/component/auth"
trie "github.com/Dreamacro/clash/component/domain-trie"
"github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/dns"
"github.com/Dreamacro/clash/log"
P "github.com/Dreamacro/clash/proxy"
authStore "github.com/Dreamacro/clash/proxy/auth"
T "github.com/Dreamacro/clash/tunnel"
)
// Parse config with default config path
func Parse() (*config.Config, error) {
return ParseWithPath(C.Path.Config())
}
// ParseWithPath parse config with custom config path
func ParseWithPath(path string) (*config.Config, error) {
return config.Parse(path)
}
// ApplyConfig dispatch configure to all parts
func ApplyConfig(cfg *config.Config, force bool) {
updateUsers(cfg.Users)
if force {
updateGeneral(cfg.General)
}
updateProxies(cfg.Proxies)
updateRules(cfg.Rules)
updateDNS(cfg.DNS)
updateHosts(cfg.Hosts)
updateExperimental(cfg.Experimental)
}
func GetGeneral() *config.General {
ports := P.GetPorts()
authenticator := []string{}
if auth := authStore.Authenticator(); auth != nil {
authenticator = auth.Users()
}
general := &config.General{
Port: ports.Port,
SocksPort: ports.SocksPort,
RedirPort: ports.RedirPort,
Authentication: authenticator,
AllowLan: P.AllowLan(),
BindAddress: P.BindAddress(),
Mode: T.Instance().Mode(),
LogLevel: log.Level(),
}
return general
}
func updateExperimental(c *config.Experimental) {
T.Instance().UpdateExperimental(c.IgnoreResolveFail)
}
func updateDNS(c *config.DNS) {
if c.Enable == false {
dns.DefaultResolver = nil
dns.ReCreateServer("", nil)
return
}
r := dns.New(dns.Config{
Main: c.NameServer,
Fallback: c.Fallback,
IPv6: c.IPv6,
EnhancedMode: c.EnhancedMode,
Pool: c.FakeIPRange,
FallbackFilter: dns.FallbackFilter{
GeoIP: c.FallbackFilter.GeoIP,
IPCIDR: c.FallbackFilter.IPCIDR,
},
})
dns.DefaultResolver = r
if err := dns.ReCreateServer(c.Listen, r); err != nil {
log.Errorln("Start DNS server error: %s", err.Error())
return
}
if c.Listen != "" {
log.Infoln("DNS server listening at: %s", c.Listen)
}
}
func updateHosts(tree *trie.Trie) {
dns.DefaultHosts = tree
}
func updateProxies(proxies map[string]C.Proxy) {
tunnel := T.Instance()
oldProxies := tunnel.Proxies()
// close proxy group goroutine
for _, proxy := range oldProxies {
proxy.Destroy()
}
tunnel.UpdateProxies(proxies)
}
func updateRules(rules []C.Rule) {
T.Instance().UpdateRules(rules)
}
func updateGeneral(general *config.General) {
log.SetLevel(general.LogLevel)
T.Instance().SetMode(general.Mode)
allowLan := general.AllowLan
P.SetAllowLan(allowLan)
bindAddress := general.BindAddress
P.SetBindAddress(bindAddress)
if err := P.ReCreateHTTP(general.Port); err != nil {
log.Errorln("Start HTTP server error: %s", err.Error())
}
if err := P.ReCreateSocks(general.SocksPort); err != nil {
log.Errorln("Start SOCKS5 server error: %s", err.Error())
}
if err := P.ReCreateRedir(general.RedirPort); err != nil {
log.Errorln("Start Redir server error: %s", err.Error())
}
}
func updateUsers(users []auth.AuthUser) {
authenticator := auth.NewAuthenticator(users)
authStore.SetAuthenticator(authenticator)
if authenticator != nil {
log.Infoln("Authentication of local server updated")
}
}

25
hub/hub.go Normal file
View File

@ -0,0 +1,25 @@
package hub
import (
"github.com/Dreamacro/clash/hub/executor"
"github.com/Dreamacro/clash/hub/route"
)
// Parse call at the beginning of clash
func Parse() error {
cfg, err := executor.Parse()
if err != nil {
return err
}
if cfg.General.ExternalUI != "" {
route.SetUIPath(cfg.General.ExternalUI)
}
if cfg.General.ExternalController != "" {
go route.Start(cfg.General.ExternalController, cfg.General.Secret)
}
executor.ApplyConfig(cfg, true)
return nil
}

View File

@ -1,217 +0,0 @@
package hub
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
A "github.com/Dreamacro/clash/adapters/outbound"
C "github.com/Dreamacro/clash/constant"
"github.com/go-chi/chi"
"github.com/go-chi/render"
)
func proxyRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getProxies)
r.With(parseProxyName).Get("/{name}", getProxy)
r.With(parseProxyName).Get("/{name}/delay", getProxyDelay)
r.With(parseProxyName).Put("/{name}", updateProxy)
return r
}
// When name is composed of a partial escape string, Golang does not unescape it
func parseProxyName(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
if newName, err := url.PathUnescape(name); err == nil {
name = newName
}
ctx := context.WithValue(r.Context(), contextKey("proxy name"), name)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
type SampleProxy struct {
Type string `json:"type"`
}
type Selector struct {
Type string `json:"type"`
Now string `json:"now"`
All []string `json:"all"`
}
type URLTest struct {
Type string `json:"type"`
Now string `json:"now"`
}
type Fallback struct {
Type string `json:"type"`
Now string `json:"now"`
}
func transformProxy(proxy C.Proxy) interface{} {
t := proxy.Type()
switch t {
case C.Selector:
selector := proxy.(*A.Selector)
return Selector{
Type: t.String(),
Now: selector.Now(),
All: selector.All(),
}
case C.URLTest:
return URLTest{
Type: t.String(),
Now: proxy.(*A.URLTest).Now(),
}
case C.Fallback:
return Fallback{
Type: t.String(),
Now: proxy.(*A.Fallback).Now(),
}
default:
return SampleProxy{
Type: proxy.Type().String(),
}
}
}
type GetProxiesResponse struct {
Proxies map[string]interface{} `json:"proxies"`
}
func getProxies(w http.ResponseWriter, r *http.Request) {
rawProxies := cfg.Proxies()
proxies := make(map[string]interface{})
for name, proxy := range rawProxies {
proxies[name] = transformProxy(proxy)
}
render.JSON(w, r, GetProxiesResponse{Proxies: proxies})
}
func getProxy(w http.ResponseWriter, r *http.Request) {
name := r.Context().Value(contextKey("proxy name")).(string)
proxies := cfg.Proxies()
proxy, exist := proxies[name]
if !exist {
w.WriteHeader(http.StatusNotFound)
render.JSON(w, r, Error{
Error: "Proxy not found",
})
return
}
render.JSON(w, r, transformProxy(proxy))
}
type UpdateProxyRequest struct {
Name string `json:"name"`
}
func updateProxy(w http.ResponseWriter, r *http.Request) {
req := UpdateProxyRequest{}
if err := render.DecodeJSON(r.Body, &req); err != nil {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Format error",
})
return
}
name := r.Context().Value(contextKey("proxy name")).(string)
proxies := cfg.Proxies()
proxy, exist := proxies[name]
if !exist {
w.WriteHeader(http.StatusNotFound)
render.JSON(w, r, Error{
Error: "Proxy not found",
})
return
}
selector, ok := proxy.(*A.Selector)
if !ok {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Proxy can't update",
})
return
}
if err := selector.Set(req.Name); err != nil {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: fmt.Sprintf("Selector update error: %s", err.Error()),
})
return
}
w.WriteHeader(http.StatusNoContent)
}
type GetProxyDelayRequest struct {
URL string `json:"url"`
Timeout int16 `json:"timeout"`
}
type GetProxyDelayResponse struct {
Delay int16 `json:"delay"`
}
func getProxyDelay(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
url := query.Get("url")
timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Format error",
})
return
}
name := r.Context().Value(contextKey("proxy name")).(string)
proxies := cfg.Proxies()
proxy, exist := proxies[name]
if !exist {
w.WriteHeader(http.StatusNotFound)
render.JSON(w, r, Error{
Error: "Proxy not found",
})
return
}
sigCh := make(chan int16)
go func() {
t, err := A.DelayTest(proxy, url)
if err != nil {
sigCh <- 0
}
sigCh <- t
}()
select {
case <-time.After(time.Millisecond * time.Duration(timeout)):
w.WriteHeader(http.StatusRequestTimeout)
render.JSON(w, r, Error{
Error: "Proxy delay test timeout",
})
case t := <-sigCh:
if t == 0 {
w.WriteHeader(http.StatusServiceUnavailable)
render.JSON(w, r, Error{
Error: "An error occurred in the delay test",
})
} else {
render.JSON(w, r, GetProxyDelayResponse{
Delay: t,
})
}
}
}

107
hub/route/configs.go Normal file
View File

@ -0,0 +1,107 @@
package route
import (
"net/http"
"path/filepath"
"github.com/Dreamacro/clash/hub/executor"
"github.com/Dreamacro/clash/log"
P "github.com/Dreamacro/clash/proxy"
T "github.com/Dreamacro/clash/tunnel"
"github.com/go-chi/chi"
"github.com/go-chi/render"
)
func configRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getConfigs)
r.Put("/", updateConfigs)
r.Patch("/", patchConfigs)
return r
}
type configSchema struct {
Port *int `json:"port"`
SocksPort *int `json:"socks-port"`
RedirPort *int `json:"redir-port"`
AllowLan *bool `json:"allow-lan"`
BindAddress *string `json:"bind-address"`
Mode *T.Mode `json:"mode"`
LogLevel *log.LogLevel `json:"log-level"`
}
func getConfigs(w http.ResponseWriter, r *http.Request) {
general := executor.GetGeneral()
render.JSON(w, r, general)
}
func pointerOrDefault(p *int, def int) int {
if p != nil {
return *p
}
return def
}
func patchConfigs(w http.ResponseWriter, r *http.Request) {
general := &configSchema{}
if err := render.DecodeJSON(r.Body, general); err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
if general.AllowLan != nil {
P.SetAllowLan(*general.AllowLan)
}
if general.BindAddress != nil {
P.SetBindAddress(*general.BindAddress)
}
ports := P.GetPorts()
P.ReCreateHTTP(pointerOrDefault(general.Port, ports.Port))
P.ReCreateSocks(pointerOrDefault(general.SocksPort, ports.SocksPort))
P.ReCreateRedir(pointerOrDefault(general.RedirPort, ports.RedirPort))
if general.Mode != nil {
T.Instance().SetMode(*general.Mode)
}
if general.LogLevel != nil {
log.SetLevel(*general.LogLevel)
}
render.NoContent(w, r)
}
type updateConfigRequest struct {
Path string `json:"path"`
}
func updateConfigs(w http.ResponseWriter, r *http.Request) {
req := updateConfigRequest{}
if err := render.DecodeJSON(r.Body, &req); err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
if !filepath.IsAbs(req.Path) {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError("path is not a absoluted path"))
return
}
force := r.URL.Query().Get("force") == "true"
cfg, err := executor.ParseWithPath(req.Path)
if err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError(err.Error()))
return
}
executor.ApplyConfig(cfg, force)
render.NoContent(w, r)
}

12
hub/route/ctxkeys.go Normal file
View File

@ -0,0 +1,12 @@
package route
var (
CtxKeyProxyName = contextKey("proxy name")
CtxKeyProxy = contextKey("proxy")
)
type contextKey string
func (c contextKey) String() string {
return "clash context key " + string(c)
}

22
hub/route/errors.go Normal file
View File

@ -0,0 +1,22 @@
package route
var (
ErrUnauthorized = newError("Unauthorized")
ErrBadRequest = newError("Body invalid")
ErrForbidden = newError("Forbidden")
ErrNotFound = newError("Resource not found")
ErrRequestTimeout = newError("Timeout")
)
// HTTPError is custom HTTP error for API
type HTTPError struct {
Message string `json:"message"`
}
func (e *HTTPError) Error() string {
return e.Message
}
func newError(msg string) *HTTPError {
return &HTTPError{Message: msg}
}

132
hub/route/proxies.go Normal file
View File

@ -0,0 +1,132 @@
package route
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
A "github.com/Dreamacro/clash/adapters/outbound"
C "github.com/Dreamacro/clash/constant"
T "github.com/Dreamacro/clash/tunnel"
"github.com/go-chi/chi"
"github.com/go-chi/render"
)
func proxyRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getProxies)
r.Route("/{name}", func(r chi.Router) {
r.Use(parseProxyName, findProxyByName)
r.Get("/", getProxy)
r.Get("/delay", getProxyDelay)
r.Put("/", updateProxy)
})
return r
}
// When name is composed of a partial escape string, Golang does not unescape it
func parseProxyName(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
if newName, err := url.PathUnescape(name); err == nil {
name = newName
}
ctx := context.WithValue(r.Context(), CtxKeyProxyName, name)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func findProxyByName(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
name := r.Context().Value(CtxKeyProxyName).(string)
proxies := T.Instance().Proxies()
proxy, exist := proxies[name]
if !exist {
render.Status(r, http.StatusNotFound)
render.JSON(w, r, ErrNotFound)
return
}
ctx := context.WithValue(r.Context(), CtxKeyProxy, proxy)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func getProxies(w http.ResponseWriter, r *http.Request) {
proxies := T.Instance().Proxies()
render.JSON(w, r, render.M{
"proxies": proxies,
})
}
func getProxy(w http.ResponseWriter, r *http.Request) {
proxy := r.Context().Value(CtxKeyProxy).(C.Proxy)
render.JSON(w, r, proxy)
}
type UpdateProxyRequest struct {
Name string `json:"name"`
}
func updateProxy(w http.ResponseWriter, r *http.Request) {
req := UpdateProxyRequest{}
if err := render.DecodeJSON(r.Body, &req); err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
proxy := r.Context().Value(CtxKeyProxy).(*A.Proxy)
selector, ok := proxy.ProxyAdapter.(*A.Selector)
if !ok {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError("Must be a Selector"))
return
}
if err := selector.Set(req.Name); err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError(fmt.Sprintf("Selector update error: %s", err.Error())))
return
}
render.NoContent(w, r)
}
func getProxyDelay(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
url := query.Get("url")
timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16)
if err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
proxy := r.Context().Value(CtxKeyProxy).(C.Proxy)
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout))
defer cancel()
delay, err := proxy.URLTest(ctx, url)
if ctx.Err() != nil {
render.Status(r, http.StatusGatewayTimeout)
render.JSON(w, r, ErrRequestTimeout)
return
}
if err != nil || delay == 0 {
render.Status(r, http.StatusServiceUnavailable)
render.JSON(w, r, newError("An error occurred in the delay test"))
return
}
render.JSON(w, r, render.M{
"delay": delay,
})
}

View File

@ -1,8 +1,10 @@
package hub
package route
import (
"net/http"
T "github.com/Dreamacro/clash/tunnel"
"github.com/go-chi/chi"
"github.com/go-chi/render"
)
@ -10,7 +12,6 @@ import (
func ruleRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getRules)
r.Put("/", updateRules)
return r
}
@ -20,14 +21,10 @@ type Rule struct {
Proxy string `json:"proxy"`
}
type GetRulesResponse struct {
Rules []Rule `json:"rules"`
}
func getRules(w http.ResponseWriter, r *http.Request) {
rawRules := cfg.Rules()
rawRules := T.Instance().Rules()
var rules []Rule
rules := []Rule{}
for _, rule := range rawRules {
rules = append(rules, Rule{
Type: rule.RuleType().String(),
@ -36,20 +33,7 @@ func getRules(w http.ResponseWriter, r *http.Request) {
})
}
w.WriteHeader(http.StatusOK)
render.JSON(w, r, GetRulesResponse{
Rules: rules,
render.JSON(w, r, render.M{
"rules": rules,
})
}
func updateRules(w http.ResponseWriter, r *http.Request) {
err := cfg.UpdateRules()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
render.JSON(w, r, Error{
Error: err.Error(),
})
return
}
w.WriteHeader(http.StatusNoContent)
}

234
hub/route/server.go Normal file
View File

@ -0,0 +1,234 @@
package route
import (
"bytes"
"encoding/json"
"net/http"
"strings"
"time"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
T "github.com/Dreamacro/clash/tunnel"
"github.com/go-chi/chi"
"github.com/go-chi/cors"
"github.com/go-chi/render"
"github.com/gorilla/websocket"
)
var (
serverSecret = ""
serverAddr = ""
uiPath = ""
upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
)
type Traffic struct {
Up int64 `json:"up"`
Down int64 `json:"down"`
}
func SetUIPath(path string) {
uiPath = path
}
func Start(addr string, secret string) {
if serverAddr != "" {
return
}
serverAddr = addr
serverSecret = secret
r := chi.NewRouter()
cors := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
MaxAge: 300,
})
r.Use(cors.Handler)
r.Get("/", hello)
r.Group(func(r chi.Router) {
r.Use(authentication)
r.Get("/logs", getLogs)
r.Get("/traffic", traffic)
r.Get("/version", version)
r.Mount("/configs", configRouter())
r.Mount("/proxies", proxyRouter())
r.Mount("/rules", ruleRouter())
})
if uiPath != "" {
r.Group(func(r chi.Router) {
fs := http.StripPrefix("/ui", http.FileServer(http.Dir(uiPath)))
r.Get("/ui", http.RedirectHandler("/ui/", http.StatusTemporaryRedirect).ServeHTTP)
r.Get("/ui/*", func(w http.ResponseWriter, r *http.Request) {
fs.ServeHTTP(w, r)
})
})
}
log.Infoln("RESTful API listening at: %s", addr)
err := http.ListenAndServe(addr, r)
if err != nil {
log.Errorln("External controller error: %s", err.Error())
}
}
func authentication(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if serverSecret == "" {
next.ServeHTTP(w, r)
return
}
// Browser websocket not support custom header
if websocket.IsWebSocketUpgrade(r) && r.URL.Query().Get("token") != "" {
token := r.URL.Query().Get("token")
if token != serverSecret {
render.Status(r, http.StatusUnauthorized)
render.JSON(w, r, ErrUnauthorized)
return
}
next.ServeHTTP(w, r)
return
}
header := r.Header.Get("Authorization")
text := strings.SplitN(header, " ", 2)
hasUnvalidHeader := text[0] != "Bearer"
hasUnvalidSecret := len(text) == 2 && text[1] != serverSecret
if hasUnvalidHeader || hasUnvalidSecret {
render.Status(r, http.StatusUnauthorized)
render.JSON(w, r, ErrUnauthorized)
return
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func hello(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, render.M{"hello": "clash"})
}
func traffic(w http.ResponseWriter, r *http.Request) {
var wsConn *websocket.Conn
if websocket.IsWebSocketUpgrade(r) {
var err error
wsConn, err = upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
}
if wsConn == nil {
w.Header().Set("Content-Type", "application/json")
render.Status(r, http.StatusOK)
}
tick := time.NewTicker(time.Second)
t := T.Instance().Traffic()
buf := &bytes.Buffer{}
var err error
for range tick.C {
buf.Reset()
up, down := t.Now()
if err := json.NewEncoder(buf).Encode(Traffic{
Up: up,
Down: down,
}); err != nil {
break
}
if wsConn == nil {
_, err = w.Write(buf.Bytes())
w.(http.Flusher).Flush()
} else {
err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes())
}
if err != nil {
break
}
}
}
type Log struct {
Type string `json:"type"`
Payload string `json:"payload"`
}
func getLogs(w http.ResponseWriter, r *http.Request) {
levelText := r.URL.Query().Get("level")
if levelText == "" {
levelText = "info"
}
level, ok := log.LogLevelMapping[levelText]
if !ok {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
var wsConn *websocket.Conn
if websocket.IsWebSocketUpgrade(r) {
var err error
wsConn, err = upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
}
if wsConn == nil {
w.Header().Set("Content-Type", "application/json")
render.Status(r, http.StatusOK)
}
sub := log.Subscribe()
defer log.UnSubscribe(sub)
buf := &bytes.Buffer{}
var err error
for elm := range sub {
buf.Reset()
log := elm.(*log.Event)
if log.LogLevel < level {
continue
}
if err := json.NewEncoder(buf).Encode(Log{
Type: log.Type(),
Payload: log.Payload,
}); err != nil {
break
}
if wsConn == nil {
_, err = w.Write(buf.Bytes())
w.(http.Flusher).Flush()
} else {
err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes())
}
if err != nil {
break
}
}
}
func version(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, render.M{"version": C.Version})
}

View File

@ -1,178 +0,0 @@
package hub
import (
"encoding/json"
"net/http"
"strings"
"time"
"github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant"
T "github.com/Dreamacro/clash/tunnel"
"github.com/go-chi/chi"
"github.com/go-chi/cors"
"github.com/go-chi/render"
log "github.com/sirupsen/logrus"
)
var secret = ""
type Traffic struct {
Up int64 `json:"up"`
Down int64 `json:"down"`
}
func newHub(signal chan struct{}) {
var addr string
ch := config.Instance().Subscribe()
signal <- struct{}{}
count := 0
for {
elm := <-ch
event := elm.(*config.Event)
switch event.Type {
case "external-controller":
addr = event.Payload.(string)
count++
case "secret":
secret = event.Payload.(string)
count++
}
if count == 2 {
break
}
}
r := chi.NewRouter()
cors := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
MaxAge: 300,
})
r.Use(cors.Handler, authentication)
r.With(jsonContentType).Get("/traffic", traffic)
r.With(jsonContentType).Get("/logs", getLogs)
r.Mount("/configs", configRouter())
r.Mount("/proxies", proxyRouter())
r.Mount("/rules", ruleRouter())
log.Infof("RESTful API listening at: %s", addr)
err := http.ListenAndServe(addr, r)
if err != nil {
log.Errorf("External controller error: %s", err.Error())
}
}
func jsonContentType(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func authentication(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
header := r.Header.Get("Authorization")
text := strings.SplitN(header, " ", 2)
if secret == "" {
next.ServeHTTP(w, r)
return
}
hasUnvalidHeader := text[0] != "Bearer"
hasUnvalidSecret := len(text) == 2 && text[1] != secret
if hasUnvalidHeader || hasUnvalidSecret {
w.WriteHeader(http.StatusUnauthorized)
render.JSON(w, r, Error{
Error: "Authentication failed",
})
return
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
type contextKey string
func (c contextKey) String() string {
return "clash context key " + string(c)
}
func traffic(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
tick := time.NewTicker(time.Second)
t := tunnel.Traffic()
for range tick.C {
up, down := t.Now()
if err := json.NewEncoder(w).Encode(Traffic{
Up: up,
Down: down,
}); err != nil {
break
}
w.(http.Flusher).Flush()
}
}
type Log struct {
Type string `json:"type"`
Payload string `json:"payload"`
}
func getLogs(w http.ResponseWriter, r *http.Request) {
levelText := r.URL.Query().Get("level")
if levelText == "" {
levelText = "info"
}
level, ok := C.LogLevelMapping[levelText]
if !ok {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Level error",
})
return
}
src := tunnel.Log()
sub, err := src.Subscribe()
defer src.UnSubscribe(sub)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
render.JSON(w, r, Error{
Error: err.Error(),
})
return
}
render.Status(r, http.StatusOK)
for elm := range sub {
log := elm.(T.Log)
if log.LogLevel > level {
continue
}
if err := json.NewEncoder(w).Encode(Log{
Type: log.Type(),
Payload: log.Payload,
}); err != nil {
break
}
w.(http.Flusher).Flush()
}
}
// Run initial hub
func Run() {
signal := make(chan struct{})
go newHub(signal)
<-signal
}

73
log/level.go Normal file
View File

@ -0,0 +1,73 @@
package log
import (
"encoding/json"
"errors"
)
var (
// LogLevelMapping is a mapping for LogLevel enum
LogLevelMapping = map[string]LogLevel{
ERROR.String(): ERROR,
WARNING.String(): WARNING,
INFO.String(): INFO,
DEBUG.String(): DEBUG,
SILENT.String(): SILENT,
}
)
const (
DEBUG LogLevel = iota
INFO
WARNING
ERROR
SILENT
)
type LogLevel int
// UnmarshalYAML unserialize LogLevel with yaml
func (l *LogLevel) UnmarshalYAML(unmarshal func(interface{}) error) error {
var tp string
unmarshal(&tp)
level, exist := LogLevelMapping[tp]
if !exist {
return errors.New("invalid mode")
}
*l = level
return nil
}
// UnmarshalJSON unserialize LogLevel with json
func (l *LogLevel) UnmarshalJSON(data []byte) error {
var tp string
json.Unmarshal(data, &tp)
level, exist := LogLevelMapping[tp]
if !exist {
return errors.New("invalid mode")
}
*l = level
return nil
}
// MarshalJSON serialize LogLevel with json
func (l LogLevel) MarshalJSON() ([]byte, error) {
return json.Marshal(l.String())
}
func (l LogLevel) String() string {
switch l {
case INFO:
return "info"
case WARNING:
return "warning"
case ERROR:
return "error"
case DEBUG:
return "debug"
case SILENT:
return "silent"
default:
return "unknown"
}
}

100
log/log.go Normal file
View File

@ -0,0 +1,100 @@
package log
import (
"fmt"
"os"
"github.com/Dreamacro/clash/common/observable"
log "github.com/sirupsen/logrus"
)
var (
logCh = make(chan interface{})
source = observable.NewObservable(logCh)
level = INFO
)
func init() {
log.SetOutput(os.Stdout)
log.SetLevel(log.DebugLevel)
}
type Event struct {
LogLevel LogLevel
Payload string
}
func (e *Event) Type() string {
return e.LogLevel.String()
}
func Infoln(format string, v ...interface{}) {
event := newLog(INFO, format, v...)
logCh <- event
print(event)
}
func Warnln(format string, v ...interface{}) {
event := newLog(WARNING, format, v...)
logCh <- event
print(event)
}
func Errorln(format string, v ...interface{}) {
event := newLog(ERROR, format, v...)
logCh <- event
print(event)
}
func Debugln(format string, v ...interface{}) {
event := newLog(DEBUG, format, v...)
logCh <- event
print(event)
}
func Fatalln(format string, v ...interface{}) {
log.Fatalf(format, v...)
}
func Subscribe() observable.Subscription {
sub, _ := source.Subscribe()
return sub
}
func UnSubscribe(sub observable.Subscription) {
source.UnSubscribe(sub)
return
}
func Level() LogLevel {
return level
}
func SetLevel(newLevel LogLevel) {
level = newLevel
}
func print(data *Event) {
if data.LogLevel < level {
return
}
switch data.LogLevel {
case INFO:
log.Infoln(data.Payload)
case WARNING:
log.Warnln(data.Payload)
case ERROR:
log.Errorln(data.Payload)
case DEBUG:
log.Debugln(data.Payload)
}
}
func newLog(logLevel LogLevel, format string, v ...interface{}) *Event {
return &Event{
LogLevel: logLevel,
Payload: fmt.Sprintf(format, v...),
}
}

33
main.go
View File

@ -1,46 +1,51 @@
package main
import (
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"runtime"
"syscall"
"flag"
"path"
"github.com/Dreamacro/clash/config"
"github.com/Dreamacro/clash/hub"
"github.com/Dreamacro/clash/proxy"
"github.com/Dreamacro/clash/tunnel"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/hub"
log "github.com/sirupsen/logrus"
)
var (
version bool
homedir string
)
func init() {
flag.StringVar(&homedir, "d", "", "set configuration directory")
flag.BoolVar(&version, "v", false, "show current version of clash")
flag.Parse()
}
func main() {
tunnel.Instance().Run()
proxy.Instance().Run()
hub.Run()
if version {
fmt.Printf("Clash %s %s %s %s\n", C.Version, runtime.GOOS, runtime.GOARCH, C.BuildTime)
return
}
if (homedir != "") {
if !path.IsAbs(homedir) {
if homedir != "" {
if !filepath.IsAbs(homedir) {
currentDir, _ := os.Getwd()
homedir = path.Join(currentDir, homedir)
homedir = filepath.Join(currentDir, homedir)
}
C.SetHomeDir(homedir)
}
config.Init()
err := config.Instance().Parse()
if err != nil {
if err := config.Init(C.Path.HomeDir()); err != nil {
log.Fatalf("Initial configuration directory error: %s", err.Error())
}
if err := hub.Parse(); err != nil {
log.Fatalf("Parse config error: %s", err.Error())
}

17
proxy/auth/auth.go Normal file
View File

@ -0,0 +1,17 @@
package auth
import (
"github.com/Dreamacro/clash/component/auth"
)
var (
authenticator auth.Authenticator
)
func Authenticator() auth.Authenticator {
return authenticator
}
func SetAuthenticator(au auth.Authenticator) {
authenticator = au
}

View File

@ -2,65 +2,99 @@ package http
import (
"bufio"
"encoding/base64"
"net"
"net/http"
"strings"
"time"
"github.com/Dreamacro/clash/adapters/inbound"
C "github.com/Dreamacro/clash/constant"
adapters "github.com/Dreamacro/clash/adapters/inbound"
"github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/component/auth"
"github.com/Dreamacro/clash/log"
authStore "github.com/Dreamacro/clash/proxy/auth"
"github.com/Dreamacro/clash/tunnel"
log "github.com/sirupsen/logrus"
)
var (
tun = tunnel.Instance()
)
func NewHttpProxy(addr string) (*C.ProxySignal, error) {
type HttpListener struct {
net.Listener
address string
closed bool
cache *cache.Cache
}
func NewHttpProxy(addr string) (*HttpListener, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
done := make(chan struct{})
closed := make(chan struct{})
signal := &C.ProxySignal{
Done: done,
Closed: closed,
}
hl := &HttpListener{l, addr, false, cache.New(30 * time.Second)}
go func() {
log.Infof("HTTP proxy listening at: %s", addr)
log.Infoln("HTTP proxy listening at: %s", addr)
for {
c, err := l.Accept()
c, err := hl.Accept()
if err != nil {
if _, open := <-done; !open {
if hl.closed {
break
}
continue
}
go handleConn(c)
go handleConn(c, hl.cache)
}
}()
go func() {
<-done
close(done)
l.Close()
closed <- struct{}{}
}()
return signal, nil
return hl, nil
}
func handleConn(conn net.Conn) {
func (l *HttpListener) Close() {
l.closed = true
l.Listener.Close()
}
func (l *HttpListener) Address() string {
return l.address
}
func canActivate(loginStr string, authenticator auth.Authenticator, cache *cache.Cache) (ret bool) {
if result := cache.Get(loginStr); result != nil {
ret = result.(bool)
}
loginData, err := base64.StdEncoding.DecodeString(loginStr)
login := strings.Split(string(loginData), ":")
ret = err == nil && len(login) == 2 && authenticator.Verify(login[0], login[1])
cache.Put(loginStr, ret, time.Minute)
return
}
func handleConn(conn net.Conn, cache *cache.Cache) {
br := bufio.NewReader(conn)
request, err := http.ReadRequest(br)
if err != nil {
if err != nil || request.URL.Host == "" {
conn.Close()
return
}
authenticator := authStore.Authenticator()
if authenticator != nil {
if authStrings := strings.Split(request.Header.Get("Proxy-Authorization"), " "); len(authStrings) != 2 {
_, err = conn.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic\r\n\r\n"))
conn.Close()
return
} else if !canActivate(authStrings[1], authenticator, cache) {
conn.Write([]byte("HTTP/1.1 403 Forbidden\r\n\r\n"))
log.Infoln("Auth failed from %s", conn.RemoteAddr().String())
conn.Close()
return
}
}
if request.Method == http.MethodConnect {
_, err := conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
if err != nil {

View File

@ -1,116 +1,183 @@
package proxy
import (
"sync"
"fmt"
"net"
"strconv"
"github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/proxy/http"
"github.com/Dreamacro/clash/proxy/redir"
"github.com/Dreamacro/clash/proxy/socks"
)
var (
listener *Listener
once sync.Once
allowLan = false
bindAddress = "*"
socksListener *socks.SockListener
socksUDPListener *socks.SockUDPListener
httpListener *http.HttpListener
redirListener *redir.RedirListener
)
type Listener struct {
// signal for update
httpSignal *C.ProxySignal
socksSignal *C.ProxySignal
redirSignal *C.ProxySignal
type listener interface {
Close()
Address() string
}
func (l *Listener) updateHTTP(addr string) error {
if l.httpSignal != nil {
signal := l.httpSignal
signal.Done <- struct{}{}
<-signal.Closed
l.httpSignal = nil
type Ports struct {
Port int `json:"port"`
SocksPort int `json:"socks-port"`
RedirPort int `json:"redir-port"`
}
func AllowLan() bool {
return allowLan
}
func BindAddress() string {
return bindAddress
}
func SetAllowLan(al bool) {
allowLan = al
}
func SetBindAddress(host string) {
bindAddress = host
}
func ReCreateHTTP(port int) error {
addr := genAddr(bindAddress, port, allowLan)
if httpListener != nil {
if httpListener.Address() == addr {
return nil
}
httpListener.Close()
httpListener = nil
}
signal, err := http.NewHttpProxy(addr)
if portIsZero(addr) {
return nil
}
var err error
httpListener, err = http.NewHttpProxy(addr)
if err != nil {
return err
}
l.httpSignal = signal
return nil
}
func (l *Listener) updateSocks(addr string) error {
if l.socksSignal != nil {
signal := l.socksSignal
signal.Done <- struct{}{}
<-signal.Closed
l.socksSignal = nil
func ReCreateSocks(port int) error {
addr := genAddr(bindAddress, port, allowLan)
if socksListener != nil {
if socksListener.Address() == addr {
return nil
}
socksListener.Close()
socksListener = nil
}
signal, err := socks.NewSocksProxy(addr)
if portIsZero(addr) {
return nil
}
var err error
socksListener, err = socks.NewSocksProxy(addr)
if err != nil {
return err
}
l.socksSignal = signal
return nil
return reCreateSocksUDP(addr)
}
func (l *Listener) updateRedir(addr string) error {
if l.redirSignal != nil {
signal := l.redirSignal
signal.Done <- struct{}{}
<-signal.Closed
l.redirSignal = nil
func reCreateSocksUDP(addr string) error {
if socksUDPListener != nil {
if socksUDPListener.Address() == addr {
return nil
}
socksUDPListener.Close()
socksUDPListener = nil
}
signal, err := redir.NewRedirProxy(addr)
var err error
socksUDPListener, err = socks.NewSocksUDPProxy(addr)
if err != nil {
return err
}
l.redirSignal = signal
return nil
}
func (l *Listener) process(signal chan<- struct{}) {
sub := config.Instance().Subscribe()
signal <- struct{}{}
reportCH := config.Instance().Report()
for elm := range sub {
event := elm.(*config.Event)
switch event.Type {
case "http-addr":
addr := event.Payload.(string)
err := l.updateHTTP(addr)
reportCH <- &config.Event{Type: "http-addr", Payload: err == nil}
case "socks-addr":
addr := event.Payload.(string)
err := l.updateSocks(addr)
reportCH <- &config.Event{Type: "socks-addr", Payload: err == nil}
case "redir-addr":
addr := event.Payload.(string)
err := l.updateRedir(addr)
reportCH <- &config.Event{Type: "redir-addr", Payload: err == nil}
func ReCreateRedir(port int) error {
addr := genAddr(bindAddress, port, allowLan)
if redirListener != nil {
if redirListener.Address() == addr {
return nil
}
redirListener.Close()
redirListener = nil
}
if portIsZero(addr) {
return nil
}
var err error
redirListener, err = redir.NewRedirProxy(addr)
if err != nil {
return err
}
return nil
}
// GetPorts return the ports of proxy servers
func GetPorts() *Ports {
ports := &Ports{}
if httpListener != nil {
_, portStr, _ := net.SplitHostPort(httpListener.Address())
port, _ := strconv.Atoi(portStr)
ports.Port = port
}
if socksListener != nil {
_, portStr, _ := net.SplitHostPort(socksListener.Address())
port, _ := strconv.Atoi(portStr)
ports.SocksPort = port
}
if redirListener != nil {
_, portStr, _ := net.SplitHostPort(redirListener.Address())
port, _ := strconv.Atoi(portStr)
ports.RedirPort = port
}
return ports
}
func portIsZero(addr string) bool {
_, port, err := net.SplitHostPort(addr)
if port == "0" || port == "" || err != nil {
return true
}
return false
}
func genAddr(host string, port int, allowLan bool) string {
if allowLan {
if host == "*" {
return fmt.Sprintf(":%d", port)
} else {
return fmt.Sprintf("%s:%d", host, port)
}
}
}
// Run ensure config monitoring
func (l *Listener) Run() {
signal := make(chan struct{})
go l.process(signal)
<-signal
}
func newListener() *Listener {
return &Listener{}
}
// Instance return singleton instance of Listener
func Instance() *Listener {
once.Do(func() {
listener = newListener()
})
return listener
return fmt.Sprintf("127.0.0.1:%d", port)
}

View File

@ -5,34 +5,33 @@ import (
"github.com/Dreamacro/clash/adapters/inbound"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel"
log "github.com/sirupsen/logrus"
)
var (
tun = tunnel.Instance()
)
func NewRedirProxy(addr string) (*C.ProxySignal, error) {
type RedirListener struct {
net.Listener
address string
closed bool
}
func NewRedirProxy(addr string) (*RedirListener, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
done := make(chan struct{})
closed := make(chan struct{})
signal := &C.ProxySignal{
Done: done,
Closed: closed,
}
rl := &RedirListener{l, addr, false}
go func() {
log.Infof("Redir proxy listening at: %s", addr)
log.Infoln("Redir proxy listening at: %s", addr)
for {
c, err := l.Accept()
if err != nil {
if _, open := <-done; !open {
if rl.closed {
break
}
continue
@ -41,14 +40,16 @@ func NewRedirProxy(addr string) (*C.ProxySignal, error) {
}
}()
go func() {
<-done
close(done)
l.Close()
closed <- struct{}{}
}()
return rl, nil
}
return signal, nil
func (l *RedirListener) Close() {
l.closed = true
l.Listener.Close()
}
func (l *RedirListener) Address() string {
return l.address
}
func handleRedir(conn net.Conn) {
@ -58,5 +59,5 @@ func handleRedir(conn net.Conn) {
return
}
conn.(*net.TCPConn).SetKeepAlive(true)
tun.Add(adapters.NewSocket(target, conn))
tun.Add(adapters.NewSocket(target, conn, C.REDIR, C.TCP))
}

View File

@ -5,10 +5,10 @@ import (
"syscall"
"unsafe"
"github.com/Dreamacro/go-shadowsocks2/socks"
"github.com/Dreamacro/clash/component/socks5"
)
func parserPacket(c net.Conn) (socks.Addr, error) {
func parserPacket(c net.Conn) (socks5.Addr, error) {
const (
PfInout = 0
PfIn = 1
@ -51,7 +51,7 @@ func parserPacket(c net.Conn) (socks.Addr, error) {
}
addr := make([]byte, 1+net.IPv4len+2)
addr[0] = socks.AtypIPv4
addr[0] = socks5.AtypIPv4
copy(addr[1:1+net.IPv4len], nl.rdaddr[:4])
copy(addr[1+net.IPv4len:], nl.rdxport[:2])
return addr, nil

View File

@ -0,0 +1,52 @@
package redir
import (
"errors"
"net"
"syscall"
"unsafe"
"github.com/Dreamacro/clash/component/socks5"
)
const (
SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv4.h
IP6T_SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv6/ip6_tables.h
)
func parserPacket(conn net.Conn) (socks5.Addr, error) {
c, ok := conn.(*net.TCPConn)
if !ok {
return nil, errors.New("only work with TCP connection")
}
rc, err := c.SyscallConn()
if err != nil {
return nil, err
}
var addr socks5.Addr
rc.Control(func(fd uintptr) {
addr, err = getorigdst(fd)
})
return addr, err
}
// Call getorigdst() from linux/net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c
func getorigdst(fd uintptr) (socks5.Addr, error) {
raw := syscall.RawSockaddrInet4{}
siz := unsafe.Sizeof(raw)
_, _, err := syscall.Syscall6(syscall.SYS_GETSOCKOPT, fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&raw)), uintptr(unsafe.Pointer(&siz)), 0)
if err != 0 {
return nil, err
}
addr := make([]byte, 1+net.IPv4len+2)
addr[0] = socks5.AtypIPv4
copy(addr[1:1+net.IPv4len], raw.Addr[:])
port := (*[2]byte)(unsafe.Pointer(&raw.Port)) // big-endian
addr[1+net.IPv4len], addr[1+net.IPv4len+1] = port[0], port[1]
return addr, nil
}

View File

@ -6,7 +6,7 @@ import (
"syscall"
"unsafe"
"github.com/Dreamacro/go-shadowsocks2/socks"
"github.com/Dreamacro/clash/component/socks5"
)
const (
@ -14,7 +14,7 @@ const (
IP6T_SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv6/ip6_tables.h
)
func parserPacket(conn net.Conn) (socks.Addr, error) {
func parserPacket(conn net.Conn) (socks5.Addr, error) {
c, ok := conn.(*net.TCPConn)
if !ok {
return nil, errors.New("only work with TCP connection")
@ -25,7 +25,7 @@ func parserPacket(conn net.Conn) (socks.Addr, error) {
return nil, err
}
var addr socks.Addr
var addr socks5.Addr
rc.Control(func(fd uintptr) {
addr, err = getorigdst(fd)
@ -35,7 +35,7 @@ func parserPacket(conn net.Conn) (socks.Addr, error) {
}
// Call getorigdst() from linux/net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c
func getorigdst(fd uintptr) (socks.Addr, error) {
func getorigdst(fd uintptr) (socks5.Addr, error) {
raw := syscall.RawSockaddrInet4{}
siz := unsafe.Sizeof(raw)
if err := socketcall(GETSOCKOPT, fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&raw)), uintptr(unsafe.Pointer(&siz)), 0); err != nil {
@ -43,7 +43,7 @@ func getorigdst(fd uintptr) (socks.Addr, error) {
}
addr := make([]byte, 1+net.IPv4len+2)
addr[0] = socks.AtypIPv4
addr[0] = socks5.AtypIPv4
copy(addr[1:1+net.IPv4len], raw.Addr[:])
port := (*[2]byte)(unsafe.Pointer(&raw.Port)) // big-endian
addr[1+net.IPv4len], addr[1+net.IPv4len+1] = port[0], port[1]

View File

@ -4,9 +4,9 @@ import (
"errors"
"net"
"github.com/Dreamacro/go-shadowsocks2/socks"
"github.com/Dreamacro/clash/component/socks5"
)
func parserPacket(conn net.Conn) (socks.Addr, error) {
func parserPacket(conn net.Conn) (socks5.Addr, error) {
return nil, errors.New("Windows not support yet")
}

View File

@ -1,39 +1,41 @@
package socks
import (
"io"
"io/ioutil"
"net"
"github.com/Dreamacro/clash/adapters/inbound"
adapters "github.com/Dreamacro/clash/adapters/inbound"
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
authStore "github.com/Dreamacro/clash/proxy/auth"
"github.com/Dreamacro/clash/tunnel"
"github.com/Dreamacro/go-shadowsocks2/socks"
log "github.com/sirupsen/logrus"
)
var (
tun = tunnel.Instance()
)
func NewSocksProxy(addr string) (*C.ProxySignal, error) {
type SockListener struct {
net.Listener
address string
closed bool
}
func NewSocksProxy(addr string) (*SockListener, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
done := make(chan struct{})
closed := make(chan struct{})
signal := &C.ProxySignal{
Done: done,
Closed: closed,
}
sl := &SockListener{l, addr, false}
go func() {
log.Infof("SOCKS proxy listening at: %s", addr)
log.Infoln("SOCKS proxy listening at: %s", addr)
for {
c, err := l.Accept()
if err != nil {
if _, open := <-done; !open {
if sl.closed {
break
}
continue
@ -42,22 +44,29 @@ func NewSocksProxy(addr string) (*C.ProxySignal, error) {
}
}()
go func() {
<-done
close(done)
l.Close()
closed <- struct{}{}
}()
return sl, nil
}
return signal, nil
func (l *SockListener) Close() {
l.closed = true
l.Listener.Close()
}
func (l *SockListener) Address() string {
return l.address
}
func handleSocks(conn net.Conn) {
target, err := socks.Handshake(conn)
target, command, err := socks5.ServerHandshake(conn, authStore.Authenticator())
if err != nil {
conn.Close()
return
}
conn.(*net.TCPConn).SetKeepAlive(true)
tun.Add(adapters.NewSocket(target, conn))
if command == socks5.CmdUDPAssociate {
defer conn.Close()
io.Copy(ioutil.Discard, conn)
return
}
tun.Add(adapters.NewSocket(target, conn, C.SOCKS, C.TCP))
}

View File

@ -1 +1,68 @@
package socks
import (
"bytes"
"net"
adapters "github.com/Dreamacro/clash/adapters/inbound"
"github.com/Dreamacro/clash/common/pool"
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant"
)
type SockUDPListener struct {
net.PacketConn
address string
closed bool
}
func NewSocksUDPProxy(addr string) (*SockUDPListener, error) {
l, err := net.ListenPacket("udp", addr)
if err != nil {
return nil, err
}
sl := &SockUDPListener{l, addr, false}
go func() {
for {
buf := pool.BufPool.Get().([]byte)
n, remoteAddr, err := l.ReadFrom(buf)
if err != nil {
pool.BufPool.Put(buf[:cap(buf)])
if sl.closed {
break
}
continue
}
handleSocksUDP(l, buf[:n], remoteAddr)
}
}()
return sl, nil
}
func (l *SockUDPListener) Close() error {
l.closed = true
return l.PacketConn.Close()
}
func (l *SockUDPListener) Address() string {
return l.address
}
func handleSocksUDP(pc net.PacketConn, buf []byte, addr net.Addr) {
target, payload, err := socks5.DecodeUDPPacket(buf)
if err != nil {
// Unresolved UDP packet, return buffer to the pool
pool.BufPool.Put(buf[:cap(buf)])
return
}
conn := &fakeConn{
PacketConn: pc,
remoteAddr: addr,
targetAddr: target,
buffer: bytes.NewBuffer(payload),
bufRef: buf,
}
tun.Add(adapters.NewSocket(target, conn, C.SOCKS, C.UDP))
}

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