Compare commits

...

61 Commits

Author SHA1 Message Date
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
74 changed files with 3331 additions and 1463 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@
*.dll
*.so
*.dylib
bin/*
# Test binary, build with `go test -c`
*.test

View File

@ -2,22 +2,88 @@ NAME=clash
BINDIR=bin
GOBUILD=CGO_ENABLED=0 go build -ldflags '-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 \
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:
GOARCH=mipsle 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=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
windows-amd64:
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST))
$(gz_releases): %.gz : %
chmod +x $(BINDIR)/$(NAME)-$(basename $@)
gzip -f $(BINDIR)/$(NAME)-$(basename $@)
$(zip_releases): %.zip : %
zip -m -j $(BINDIR)/$(NAME)-$(basename $@).zip $(BINDIR)/$(NAME)-$(basename $@).exe
all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST)
releases: $(gz_releases) $(zip_releases)
clean:
rm $(BINDIR)/*

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,16 +20,12 @@
## Features
- HTTP/HTTPS and SOCKS proxy
- HTTP/HTTPS and SOCKS protocol
- Surge like configuration
- GeoIP rule support
- Support Vmess/Shadowsocks/Socks5
- Support for Netfilter TCP redirect
## Discussion
[Telegram Group](https://t.me/clash_discuss)
## Install
You can build from source:
@ -42,7 +36,7 @@ go get -u -v github.com/Dreamacro/clash
Pre-built binaries are available: [release](https://github.com/Dreamacro/clash/releases)
Requires Go >= 1.10.
Requires Go >= 1.11.
## Daemon
@ -62,8 +56,6 @@ 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 name of the configuration file is `config.yml`
@ -85,7 +77,7 @@ 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
@ -94,15 +86,30 @@ allow-lan: false
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
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: ""
# 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
# nameserver:
# - 114.114.114.114
# - tls://dns.rubyfish.cn:853 # dns over tls
# fallback: # concurrent request with nameserver, fallback used when GEOIP country isn't CN
# - 8.8.8.8
Proxy:
# shadowsocks
@ -110,7 +117,32 @@ Proxy:
# 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 }
# old obfs configuration remove after prerelease
- name: "ss2"
type: ss
server: server
port: 443
cipher: AEAD_CHACHA20_POLY1305
password: "password"
plugin: obfs
plugin-opts:
mode: tls # or http
# host: bing.com
- name: "ss3"
type: ss
server: server
port: 443
cipher: AEAD_CHACHA20_POLY1305
password: "password"
plugin: v2ray-plugin
plugin-opts:
mode: websocket # no QUIC now
# tls: true # wss
# skip-cert-verify: true
# host: bing.com
# path: "/"
# vmess
# cipher support auto/aes-128-gcm/chacha20-poly1305/none
@ -119,38 +151,54 @@ Proxy:
- { 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-path and ws-headers
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, network: ws, ws-path: /path, ws-headers: { Host: v2ray.com } }
# with ws + tls
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, network: ws, ws-path: /path, tls: true }
# socks5
- { name: "socks", type: socks5, server: server, port: 443 }
# socks5 with authentication
- { name: "socks", type: socks5, server: server, port: 443, username: "username", password: "password" }
# 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 }
# http
- { name: "http", type: http, server: server, port: 443 }
# http with authentication
- { name: "http", type: http, server: server, port: 443, username: "username", password: "password" }
# with tls (https)
- { name: "http", type: http, server: server, port: 443, tls: true }
# with tls (https) and skip-cert-verify
- { name: "http", type: http, server: server, port: 443, tls: true, skip-cert-verify: true }
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"] }
# 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"] }
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
- SOURCE-IP-CIDR,192.168.1.201/32,DIRECT
- GEOIP,CN,DIRECT
# note: there is two ","
- FINAL,,Proxy
# FINAL would remove after prerelease
# you also can use `FINAL,Proxy` or `FINAL,,Proxy` now
- MATCH,auto
```
## Thanks

View File

@ -32,8 +32,10 @@ func (h *HTTPAdapter) Conn() net.Conn {
// NewHTTP is HTTPAdapter generator
func NewHTTP(request *http.Request, conn net.Conn) *HTTPAdapter {
metadata := parseHTTPAddr(request)
metadata.SourceIP = parseSourceIP(conn)
return &HTTPAdapter{
metadata: parseHTTPAddr(request),
metadata: metadata,
R: request,
conn: conn,
}

View File

@ -7,8 +7,10 @@ import (
// NewHTTPS is HTTPAdapter generator
func NewHTTPS(request *http.Request, conn net.Conn) *SocketAdapter {
metadata := parseHTTPAddr(request)
metadata.SourceIP = parseSourceIP(conn)
return &SocketAdapter{
metadata: parseHTTPAddr(request),
metadata: metadata,
conn: conn,
}
}

View File

@ -29,9 +29,13 @@ func (s *SocketAdapter) Conn() net.Conn {
}
// NewSocket is SocketAdapter generator
func NewSocket(target socks.Addr, conn net.Conn) *SocketAdapter {
func NewSocket(target socks.Addr, conn net.Conn, source C.SourceType) *SocketAdapter {
metadata := parseSocksAddr(target)
metadata.Source = source
metadata.SourceIP = parseSourceIP(conn)
return &SocketAdapter{
conn: conn,
metadata: parseSocksAddr(target),
metadata: metadata,
}
}

View File

@ -10,32 +10,26 @@ import (
)
func parseSocksAddr(target socks.Addr) *C.Metadata {
var host, port string
var ip net.IP
metadata := &C.Metadata{
NetWork: C.TCP,
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
}
metadata.Host = string(target[2 : 2+target[1]])
metadata.Port = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
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]))
ip := net.IP(target[1 : 1+net.IPv4len])
metadata.IP = &ip
metadata.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]))
ip := net.IP(target[1 : 1+net.IPv6len])
metadata.IP = &ip
metadata.Port = 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 +38,33 @@ 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,
Source: C.HTTP,
AddrType: C.AtypDomainName,
Host: host,
IP: resolveIP,
IP: nil,
Port: port,
}
ip := net.ParseIP(host)
if ip != nil {
switch {
case ip.To4() == nil:
metadata.AddrType = C.AtypIPv6
default:
metadata.AddrType = C.AtypIPv4
}
metadata.IP = &ip
}
return metadata
}
func parseSourceIP(conn net.Conn) *net.IP {
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
return &addr.IP
}
return nil
}

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

@ -0,0 +1,26 @@
package adapters
import (
"encoding/json"
C "github.com/Dreamacro/clash/constant"
)
type Base struct {
name string
tp C.AdapterType
}
func (b *Base) Name() string {
return b.name
}
func (b *Base) Type() C.AdapterType {
return b.tp
}
func (b *Base) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": b.Type().String(),
})
}

View File

@ -6,40 +6,29 @@ import (
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) Generator(metadata *C.Metadata) (net.Conn, error) {
address := net.JoinHostPort(metadata.Host, metadata.Port)
if metadata.IP != nil {
address = net.JoinHostPort(metadata.IP.String(), metadata.Port)
}
// 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 := net.DialTimeout("tcp", address, tcpTimeout)
if err != nil {
return
return nil, err
}
tcpKeepAlive(c)
return &DirectAdapter{conn: c}, nil
return c, nil
}
func NewDirect() *Direct {
return &Direct{}
return &Direct{
Base: &Base{
name: "DIRECT",
tp: C.Direct,
},
}
}

View File

@ -1,7 +1,9 @@
package adapters
import (
"encoding/json"
"errors"
"net"
"sync"
"time"
@ -14,7 +16,7 @@ type proxy struct {
}
type Fallback struct {
name string
*Base
proxies []*proxy
rawURL string
interval time.Duration
@ -28,14 +30,6 @@ 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 {
@ -44,7 +38,7 @@ func (f *Fallback) Now() string {
return f.proxies[0].RawProxy.Name()
}
func (f *Fallback) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
func (f *Fallback) Generator(metadata *C.Metadata) (net.Conn, error) {
idx := 0
var proxy *proxy
for {
@ -52,17 +46,29 @@ func (f *Fallback) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err
if proxy == nil {
break
}
adapter, err = proxy.RawProxy.Generator(metadata)
adapter, err := proxy.RawProxy.Generator(metadata)
if err != nil {
proxy.Valid = false
idx++
continue
}
return
return adapter, err
}
return f.proxies[0].RawProxy.Generator(metadata)
}
func (f *Fallback) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range f.proxies {
all = append(all, proxy.RawProxy.Name())
}
return json.Marshal(map[string]interface{}{
"type": f.Type().String(),
"now": f.Now(),
"all": all,
})
}
func (f *Fallback) Close() {
f.done <- struct{}{}
}
@ -125,7 +131,10 @@ func NewFallback(option FallbackOption, proxies []C.Proxy) (*Fallback, error) {
}
Fallback := &Fallback{
name: option.Name,
Base: &Base{
name: option.Name,
tp: C.Fallback,
},
proxies: warpperProxies,
rawURL: option.URL,
interval: interval,

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

@ -0,0 +1,122 @@
package adapters
import (
"bufio"
"bytes"
"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) Generator(metadata *C.Metadata) (net.Conn, error) {
c, err := net.DialTimeout("tcp", h.addr, tcpTimeout)
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 c, nil
}
func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
var buf bytes.Buffer
var err error
addr := net.JoinHostPort(metadata.String(), metadata.Port)
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 == 200 {
return nil
}
if resp.StatusCode == 407 {
return errors.New("HTTP need auth")
}
if resp.StatusCode == 405 {
return errors.New("CONNECT method not allowed by proxy")
}
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(),
MinVersion: tls.VersionTLS11,
MaxVersion: tls.VersionTLS12,
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,94 @@
package adapters
import (
"encoding/json"
"errors"
"net"
"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
}
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.IP == nil {
return ""
}
return metadata.IP.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) Generator(metadata *C.Metadata) (net.Conn, error) {
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
buckets := int32(len(lb.proxies))
for i := 0; i < lb.maxRetry; i++ {
idx := jumpHash(key, buckets)
if proxy, err := lb.proxies[idx].Generator(metadata); err == nil {
return proxy, nil
}
key++
}
return lb.proxies[0].Generator(metadata)
}
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"`
}
func NewLoadBalance(name string, proxies []C.Proxy) (*LoadBalance, error) {
if len(proxies) == 0 {
return nil, errors.New("Provide at least one proxy")
}
return &LoadBalance{
Base: &Base{
name: name,
tp: C.LoadBalance,
},
proxies: proxies,
maxRetry: 3,
}, nil
}

View File

@ -8,42 +8,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) Generator(metadata *C.Metadata) (net.Conn, error) {
return &NopConn{}, 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,14 +1,16 @@
package adapters
import (
"encoding/json"
"errors"
"net"
"sort"
C "github.com/Dreamacro/clash/constant"
)
type Selector struct {
name string
*Base
selected C.Proxy
proxies map[string]C.Proxy
}
@ -18,29 +20,25 @@ type SelectorOption struct {
Proxies []string `proxy:"proxies"`
}
func (s *Selector) Name() string {
return s.name
}
func (s *Selector) Type() C.AdapterType {
return C.Selector
}
func (s *Selector) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
func (s *Selector) Generator(metadata *C.Metadata) (net.Conn, error) {
return s.selected.Generator(metadata)
}
func (s *Selector) Now() string {
return s.selected.Name()
}
func (s *Selector) All() []string {
func (s *Selector) MarshalJSON() ([]byte, error) {
var all []string
for k := range s.proxies {
all = append(all, k)
}
sort.Strings(all)
return all
return json.Marshal(map[string]interface{}{
"type": s.Type().String(),
"now": s.Now(),
"all": all,
})
}
func (s *Selector) Now() string {
return s.selected.Name()
}
func (s *Selector) Set(name string) error {
@ -63,7 +61,10 @@ func NewSelector(name string, proxies []C.Proxy) (*Selector, error) {
}
s := &Selector{
name: name,
Base: &Base{
name: name,
tp: C.Selector,
},
proxies: mapping,
selected: proxies[0],
}

View File

@ -2,73 +2,87 @@ package adapters
import (
"bytes"
"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"
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
wsOption *v2rayObfs.WebsocketOption
}
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"`
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"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
}
func (ss *ShadowSocks) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
func (ss *ShadowSocks) Generator(metadata *C.Metadata) (net.Conn, error) {
c, err := net.DialTimeout("tcp", ss.server, tcpTimeout)
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.NewWebsocketObfs(c, ss.wsOption)
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 c, err
}
func (ss *ShadowSocks) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": ss.Type().String(),
})
}
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
@ -80,18 +94,62 @@ 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 wsOption *v2rayObfs.WebsocketOption
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())
}
obfsMode = opts.Mode
obfsOption = &opts
} else if option.Plugin == "v2ray-plugin" {
opts := v2rayObfsOption{Host: "bing.com"}
if err := decoder.Decode(option.PluginOpts, &opts); err != nil {
return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %s", server, err.Error())
}
obfsMode = opts.Mode
var tlsConfig *tls.Config
if opts.TLS {
tlsConfig = &tls.Config{
ServerName: opts.Host,
InsecureSkipVerify: opts.SkipCertVerify,
ClientSessionCache: getClientSessionCache(),
}
}
wsOption = &v2rayObfs.WebsocketOption{
Host: opts.Host,
Path: opts.Path,
TLSConfig: tlsConfig,
}
}
return &ShadowSocks{
server: server,
name: option.Name,
cipher: ciph,
obfs: obfs,
obfsHost: obfsHost,
Base: &Base{
name: option.Name,
tp: C.Shadowsocks,
},
server: server,
cipher: ciph,
obfsMode: obfsMode,
wsOption: wsOption,
obfsOption: obfsOption,
}, nil
}

View File

@ -14,23 +14,11 @@ import (
"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
@ -40,19 +28,13 @@ 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"`
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) {
func (ss *Socks5) Generator(metadata *C.Metadata) (net.Conn, error) {
c, err := net.DialTimeout("tcp", ss.addr, tcpTimeout)
if err == nil && ss.tls {
@ -68,24 +50,52 @@ func (ss *Socks5) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err e
if err := ss.shakeHand(metadata, c); err != nil {
return nil, err
}
return &Socks5Adapter{conn: c}, nil
return c, nil
}
func (ss *Socks5) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
buf := make([]byte, socks.MaxAddrLen)
var err error
// VER, CMD, RSV
_, err := rw.Write([]byte{5, 1, 0})
// VER, NMETHODS, METHODS
if len(ss.user) > 0 {
_, err = rw.Write([]byte{5, 1, 2})
} else {
_, err = rw.Write([]byte{5, 1, 0})
}
if err != nil {
return err
}
// VER, METHOD
if _, err := io.ReadFull(rw, buf[:2]); err != nil {
return err
}
if buf[0] != 5 {
return errors.New("SOCKS version error")
}
if buf[1] == 2 {
// password protocol version
authMsg := &bytes.Buffer{}
authMsg.WriteByte(1)
authMsg.WriteByte(uint8(len(ss.user)))
authMsg.WriteString(ss.user)
authMsg.WriteByte(uint8(len(ss.pass)))
authMsg.WriteString(ss.pass)
if _, err := rw.Write(authMsg.Bytes()); err != nil {
return err
}
if _, err := io.ReadFull(rw, buf[:2]); err != nil {
return err
}
if buf[1] != 0 {
return errors.New("rejected username/password")
}
} else if buf[1] != 0 {
return errors.New("SOCKS need auth")
}
@ -115,8 +125,13 @@ func NewSocks5(option Socks5Option) *Socks5 {
}
return &Socks5{
Base: &Base{
name: option.Name,
tp: C.Socks5,
},
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
name: option.Name,
user: option.UserName,
pass: option.Password,
tls: option.TLS,
skipCertVerify: option.SkipCertVerify,
tlsConfig: tlsConfig,

View File

@ -1,7 +1,10 @@
package adapters
import (
"encoding/json"
"errors"
"net"
"sort"
"sync"
"sync/atomic"
"time"
@ -10,7 +13,7 @@ import (
)
type URLTest struct {
name string
*Base
proxies []C.Proxy
rawURL string
fast C.Proxy
@ -26,19 +29,11 @@ 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) {
func (u *URLTest) Generator(metadata *C.Metadata) (net.Conn, error) {
a, err := u.fast.Generator(metadata)
if err != nil {
go u.speedTest()
@ -46,6 +41,19 @@ func (u *URLTest) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err e
return a, err
}
func (u *URLTest) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range u.proxies {
all = append(all, proxy.Name())
}
sort.Strings(all)
return json.Marshal(map[string]interface{}{
"type": u.Type().String(),
"now": u.Now(),
"all": all,
})
}
func (u *URLTest) Close() {
u.done <- struct{}{}
}
@ -113,7 +121,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

@ -36,7 +36,7 @@ func DelayTest(proxy C.Proxy, url string) (t int16, err error) {
defer instance.Close()
transport := &http.Transport{
Dial: func(string, string) (net.Conn, error) {
return instance.Conn(), nil
return instance, nil
},
// from http.DefaultTransport
MaxIdleConns: 100,

View File

@ -10,76 +10,60 @@ 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"`
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) Generator(metadata *C.Metadata) (net.Conn, error) {
c, err := net.DialTimeout("tcp", v.server, tcpTimeout)
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 c, err
}
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: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
NetWork: option.Network,
WebSocketPath: option.WSPath,
SkipCertVerify: option.SkipCertVerify,
SessionCacahe: getClientSessionCache(),
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,
SessionCacahe: getClientSessionCache(),
})
if err != nil {
return nil, err
}
return &Vmess{
name: option.Name,
Base: &Base{
name: option.Name,
tp: C.Vmess,
},
server: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
client: client,
}, nil

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"
)
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")
if i.(int) != 1 {
t.Error("should recv 1")
}
s := c.Get("string")
if s.(string) != "a" {
t.Error("should recv 'a'")
}
}
func TestCache_TTL(t *testing.T) {
interval := 200 * time.Millisecond
ttl := 20 * time.Millisecond
c := New(interval)
c.Put("int", 1, ttl)
i := c.Get("int")
if i.(int) != 1 {
t.Error("should recv 1")
}
time.Sleep(ttl * 2)
i = c.Get("int")
if i != nil {
t.Error("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")
if i != nil {
t.Error("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()
}

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

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

@ -0,0 +1,149 @@
package murmur3
// https://github.com/spaolacci/murmur3/blob/master/murmur32.go
import (
"hash"
"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 = (k1 << 15) | (k1 >> 17) // rotl32(k1, 15)
k1 *= c2_32
h1 ^= k1
h1 = (h1 << 13) | (h1 >> 19) // rotl32(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 = (k1 << 15) | (k1 >> 17) // rotl32(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
var p uintptr
if len(data) > 0 {
p = uintptr(unsafe.Pointer(&data[0]))
}
p1 := p + uintptr(4*nblocks)
for ; p < p1; p += 4 {
k1 := *(*uint32)(unsafe.Pointer(p))
k1 *= c1_32
k1 = (k1 << 15) | (k1 >> 17) // rotl32(k1, 15)
k1 *= c2_32
h1 ^= k1
h1 = (h1 << 13) | (h1 >> 19) // rotl32(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 = (k1 << 15) | (k1 >> 17) // rotl32(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
}

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

@ -0,0 +1,22 @@
package picker
import "context"
func SelectFast(ctx context.Context, in <-chan interface{}) <-chan interface{} {
out := make(chan interface{})
go func() {
select {
case p, open := <-in:
if open {
out <- p
}
case <-ctx.Done():
}
close(out)
for range in {
}
}()
return out
}

View File

@ -0,0 +1,44 @@
package picker
import (
"context"
"testing"
"time"
)
func sleepAndSend(delay int, in chan<- interface{}, input interface{}) {
time.Sleep(time.Millisecond * time.Duration(delay))
in <- input
}
func sleepAndClose(delay int, in chan interface{}) {
time.Sleep(time.Millisecond * time.Duration(delay))
close(in)
}
func TestPicker_Basic(t *testing.T) {
in := make(chan interface{})
fast := SelectFast(context.Background(), in)
go sleepAndSend(20, in, 1)
go sleepAndSend(30, in, 2)
go sleepAndClose(40, in)
number, exist := <-fast
if !exist || number != 1 {
t.Error("should recv 1", exist, number)
}
}
func TestPicker_Timeout(t *testing.T) {
in := make(chan interface{})
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*5)
defer cancel()
fast := SelectFast(ctx, in)
go sleepAndSend(20, in, 1)
go sleepAndClose(30, in)
_, exist := <-fast
if exist {
t.Error("should recv false")
}
}

View File

@ -1,5 +1,7 @@
package structure
// references: https://github.com/mitchellh/mapstructure
import (
"fmt"
"reflect"
@ -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())
}
@ -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
}

View File

@ -14,6 +14,10 @@ func init() {
rand.Seed(time.Now().Unix())
}
const (
chunkSize = 1 << 14 // 2 ** 14 == 16 * 1024
)
var bufPool = sync.Pool{New: func() interface{} { return make([]byte, 2048) }}
// TLSObfs is shadowsocks tls simple-obfs implementation
@ -75,8 +79,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)
@ -107,9 +126,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 +142,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 +165,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

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,37 @@
package obfs
import (
"crypto/tls"
"net"
"github.com/Dreamacro/clash/component/vmess"
)
// WebsocketOption is options of websocket obfs
type WebsocketOption struct {
Host string
Path string
TLSConfig *tls.Config
}
// NewWebsocketObfs return a HTTPObfs
func NewWebsocketObfs(conn net.Conn, option *WebsocketOption) (net.Conn, error) {
config := &vmess.WebsocketConfig{
Host: option.Host,
Path: option.Path,
TLS: option.TLSConfig != nil,
TLSConfig: option.TLSConfig,
}
var err error
conn, err = vmess.NewWebsocketConn(conn, config)
if err != nil {
return nil, err
}
conn = NewMux(conn, MuxOption{
ID: [2]byte{0, 0},
Host: "127.0.0.1",
Port: 0,
})
return conn, nil
}

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)
}
@ -153,7 +143,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)
@ -196,7 +186,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,
@ -209,4 +199,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

@ -69,21 +69,23 @@ type Client struct {
security Security
tls bool
host string
wsConfig *websocketConfig
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
SessionCacahe tls.ClientSessionCache
UUID string
AlterID uint16
Security string
TLS bool
HostName string
Port string
NetWork string
WebSocketPath string
WebSocketHeaders map[string]string
SkipCertVerify bool
SessionCacahe tls.ClientSessionCache
}
// New return a Conn with net.Conn and DstAddr
@ -91,14 +93,14 @@ func (c *Client) New(conn net.Conn, dst *DstAddr) (net.Conn, error) {
var err error
r := rand.Intn(len(c.user))
if c.wsConfig != nil {
conn, err = newWebsocketConn(conn, c.wsConfig)
conn, err = NewWebsocketConn(conn, c.wsConfig)
if err != nil {
return nil, err
}
} else if c.tls {
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
@ -129,9 +131,12 @@ func NewClient(config Config) (*Client, error) {
return nil, fmt.Errorf("Unknown network type: %s", config.NetWork)
}
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.SessionCacahe,
}
@ -140,13 +145,14 @@ func NewClient(config Config) (*Client, error) {
}
}
var wsConfig *websocketConfig
var wsConfig *WebsocketConfig
if config.NetWork == "ws" {
wsConfig = &websocketConfig{
host: config.Host,
path: config.WebSocketPath,
tls: config.TLS,
tlsConfig: tlsConfig,
wsConfig = &WebsocketConfig{
Host: host,
Path: config.WebSocketPath,
Headers: config.WebSocketHeaders,
TLS: config.TLS,
TLSConfig: tlsConfig,
}
}
@ -155,7 +161,7 @@ func NewClient(config Config) (*Client, error) {
uuid: &uid,
security: security,
tls: config.TLS,
host: config.Host,
host: host,
wsConfig: wsConfig,
tlsConfig: tlsConfig,
}, nil

View File

@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
@ -18,11 +19,12 @@ type websocketConn struct {
remoteAddr net.Addr
}
type websocketConfig struct {
host string
path string
tls bool
tlsConfig *tls.Config
type WebsocketConfig struct {
Host string
Path string
Headers map[string]string
TLS bool
TLSConfig *tls.Config
}
// Read implements net.Conn.Read()
@ -100,7 +102,7 @@ func (wsc *websocketConn) SetWriteDeadline(t time.Time) error {
return wsc.conn.SetWriteDeadline(t)
}
func newWebsocketConn(conn net.Conn, c *websocketConfig) (net.Conn, error) {
func NewWebsocketConn(conn net.Conn, c *WebsocketConfig) (net.Conn, error) {
dialer := &websocket.Dialer{
NetDial: func(network, addr string) (net.Conn, error) {
return conn, nil
@ -111,23 +113,30 @@ func newWebsocketConn(conn net.Conn, c *websocketConfig) (net.Conn, error) {
}
scheme := "ws"
if c.tls {
if c.TLS {
scheme = "wss"
dialer.TLSClientConfig = c.tlsConfig
dialer.TLSClientConfig = c.TLSConfig
}
host, port, err := net.SplitHostPort(c.host)
host, port, _ := net.SplitHostPort(c.Host)
if (scheme == "ws" && port != "80") || (scheme == "wss" && port != "443") {
host = c.host
host = c.Host
}
uri := url.URL{
Scheme: scheme,
Host: host,
Path: c.path,
Path: c.Path,
}
wsConn, resp, err := dialer.Dial(uri.String(), nil)
headers := http.Header{}
if c.Headers != nil {
for k, v := range c.Headers {
headers.Set(k, v)
}
}
wsConn, resp, err := dialer.Dial(uri.String(), headers)
if err != nil {
var reason string
if resp != nil {

View File

@ -3,94 +3,85 @@ 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"
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"`
AllowLan bool `json:"allow-lan"`
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"`
Listen string `yaml:"listen"`
EnhancedMode dns.EnhancedMode `yaml:"enhanced-mode"`
}
// 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"`
// Config is clash config manager
type Config struct {
General *General
DNS *DNS
Rules []C.Rule
Proxies map[string]C.Proxy
}
type rawDNS struct {
Enable bool `yaml:"enable"`
IPv6 bool `yaml:"ipv6"`
NameServer []string `yaml:"nameserver"`
Fallback []string `yaml:"fallback"`
Listen string `yaml:"listen"`
EnhancedMode dns.EnhancedMode `yaml:"enhanced-mode"`
}
type rawConfig struct {
Port int `yaml:"port"`
SocksPort int `yaml:"socks-port"`
RedirPort int `yaml:"redir-port"`
AllowLan bool `yaml:"allow-lan"`
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"`
DNS rawDNS `yaml:"dns"`
Proxy []map[string]interface{} `yaml:"Proxy"`
ProxyGroup []map[string]interface{} `yaml:"Proxy Group"`
Rule []string `yaml:"Rule"`
}
// 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
}
// Event is event of clash config
type Event struct {
Type string
Payload interface{}
}
// Subscribe config stream
func (c *Config) Subscribe() observable.Subscription {
sub, _ := c.observable.Subscribe()
return sub
}
// Report return a channel for collecting report message
func (c *Config) Report() chan<- interface{} {
return c.reportCh
}
func (c *Config) readConfig() (*RawConfig, error) {
if _, err := os.Stat(C.Path.Config()); os.IsNotExist(err) {
func readConfig(path string) (*rawConfig, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err
}
data, err := ioutil.ReadFile(C.Path.Config())
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
@ -100,144 +91,93 @@ func (c *Config) readConfig() (*RawConfig, error) {
}
// config with some default value
rawConfig := &RawConfig{
rawConfig := &rawConfig{
AllowLan: false,
Mode: Rule.String(),
LogLevel: C.INFO.String(),
Mode: T.Rule,
LogLevel: log.INFO,
Rule: []string{},
Proxy: []map[string]interface{}{},
ProxyGroup: []map[string]interface{}{},
DNS: rawDNS{
Enable: false,
},
}
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
}
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)
if err != nil {
return nil, err
}
config.Rules = rules
dnsCfg, err := parseDNS(rawCfg.DNS)
if err != nil {
return nil, err
}
config.DNS = dnsCfg
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
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,
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)
proxiesConfig := cfg.Proxy
groupsConfig := cfg.ProxyGroup
@ -251,11 +191,11 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
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
err := fmt.Errorf("can't parse")
switch proxyType {
case "ss":
ssOption := &adapters.ShadowSocksOption{}
@ -271,6 +211,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)
@ -279,15 +226,15 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
}
proxy, err = adapters.NewVmess(*vmessOption)
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
}
@ -297,14 +244,16 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
groupType, existType := mapping["type"].(string)
groupName, existName := mapping["name"].(string)
if !existType && existName {
return fmt.Errorf("ProxyGroup %d: missing type or name", idx)
return nil, fmt.Errorf("ProxyGroup %d: missing type or name", idx)
}
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
ps := []C.Proxy{}
err := fmt.Errorf("can't parse")
switch groupType {
case "url-test":
urlTestOption := &adapters.URLTestOption{}
@ -313,9 +262,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 +274,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 +286,155 @@ 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.Name, 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
}
var ps []C.Proxy
ps := []C.Proxy{}
for _, v := range proxies {
ps = append(ps, 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
return proxies, nil
}
func (c *Config) parseRules(cfg *RawConfig) error {
func parseRules(cfg *rawConfig) ([]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)
}
rule = trimArr(rule)
switch rule[0] {
case "DOMAIN":
rules = append(rules, R.NewDomain(rule[1], rule[2]))
rules = append(rules, R.NewDomain(payload, target))
case "DOMAIN-SUFFIX":
rules = append(rules, R.NewDomainSuffix(rule[1], rule[2]))
rules = append(rules, R.NewDomainSuffix(payload, target))
case "DOMAIN-KEYWORD":
rules = append(rules, R.NewDomainKeyword(rule[1], rule[2]))
rules = append(rules, R.NewDomainKeyword(payload, target))
case "GEOIP":
rules = append(rules, R.NewGEOIP(rule[1], rule[2]))
rules = append(rules, R.NewGEOIP(payload, target))
case "IP-CIDR", "IP-CIDR6":
rules = append(rules, R.NewIPCIDR(rule[1], rule[2]))
rules = append(rules, R.NewIPCIDR(payload, target, false))
case "SOURCE-IP-CIDR":
rules = append(rules, R.NewIPCIDR(payload, target, true))
case "MATCH":
fallthrough
case "FINAL":
rules = append(rules, R.NewFinal(rule[2]))
rules = append(rules, R.NewFinal(target))
}
}
c.rules = rules
c.event <- &Event{Type: "rules", Payload: rules}
return nil
return rules, 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 host, err := hostWithDefaultPort(server, "53"); err == nil {
nameservers = append(
nameservers,
dns.NameServer{Addr: host},
)
continue
}
u, err := url.Parse(server)
if err != nil {
return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error())
}
if u.Scheme != "tls" {
return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme)
}
host, err := hostWithDefaultPort(u.Host, "853")
nameservers = append(
nameservers,
dns.NameServer{
Net: "tcp-tls",
Addr: host,
},
)
}
return nameservers, nil
}
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),
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")
}
go config.handleResponseMessage()
return config
}
// Instance return singleton instance of Config
func Instance() *Config {
once.Do(func() {
config = newConfig()
})
return config
dnsCfg := &DNS{
Enable: cfg.Enable,
Listen: cfg.Listen,
EnhancedMode: cfg.EnhancedMode,
}
if nameserver, err := parseNameServer(cfg.NameServer); err == nil {
dnsCfg.NameServer = nameserver
}
if fallback, err := parseNameServer(cfg.Fallback); err == nil {
dnsCfg.Fallback = fallback
}
return dnsCfg, nil
}

View File

@ -3,6 +3,7 @@ package config
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"net/http"
"os"
@ -54,15 +55,15 @@ 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.yml
if _, err := os.Stat(C.Path.Config()); os.IsNotExist(err) {
log.Info("Can't find config, create a 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

@ -14,13 +14,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 {

View File

@ -12,15 +12,12 @@ const (
Selector
Shadowsocks
Socks5
Http
URLTest
Vmess
LoadBalance
)
type ProxyAdapter interface {
Conn() net.Conn
Close()
}
type ServerAdapter interface {
Metadata() *Metadata
Close()
@ -29,7 +26,8 @@ type ServerAdapter interface {
type Proxy interface {
Name() string
Type() AdapterType
Generator(metadata *Metadata) (ProxyAdapter, error)
Generator(metadata *Metadata) (net.Conn, error)
MarshalJSON() ([]byte, error)
}
// AdapterType is enum of adapter type
@ -49,10 +47,14 @@ func (at AdapterType) String() string {
return "Shadowsocks"
case Socks5:
return "Socks5"
case Http:
return "Http"
case URLTest:
return "URLTest"
case Vmess:
return "Vmess"
case LoadBalance:
return "LoadBalance"
default:
return "Unknow"
}

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

@ -15,6 +15,7 @@ const (
HTTP SourceType = iota
SOCKS
REDIR
)
type NetWork int
@ -32,15 +33,20 @@ type SourceType int
type Metadata struct {
NetWork NetWork
Source SourceType
SourceIP *net.IP
AddrType int
Host string
IP *net.IP
Port string
}
func (addr *Metadata) String() string {
if addr.Host == "" {
return addr.IP.String()
func (m *Metadata) String() string {
if m.Host == "" {
return m.IP.String()
}
return addr.Host
return m.Host
}
func (m *Metadata) Valid() bool {
return m.Host != "" || m.IP != nil
}

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,6 +7,7 @@ const (
DomainKeyword
GEOIP
IPCIDR
SourceIPCIDR
FINAL
)
@ -24,6 +25,8 @@ func (rt RuleType) String() string {
return "GEOIP"
case IPCIDR:
return "IPCIDR"
case SourceIPCIDR:
return "SourceIPCIDR"
case FINAL:
return "FINAL"
default:

273
dns/client.go Normal file
View File

@ -0,0 +1,273 @@
package dns
import (
"context"
"crypto/tls"
"errors"
"net"
"strings"
"sync"
"time"
"github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/common/picker"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
D "github.com/miekg/dns"
geoip2 "github.com/oschwald/geoip2-golang"
)
var (
globalSessionCache = tls.NewLRUClientSessionCache(64)
mmdb *geoip2.Reader
once sync.Once
resolver *Resolver
)
type Resolver struct {
ipv6 bool
mapping bool
fallback []*nameserver
main []*nameserver
cache *cache.Cache
}
type result struct {
Msg *D.Msg
Error error
}
func isIPRequest(q D.Question) bool {
if q.Qclass == D.ClassINET && (q.Qtype == D.TypeA || q.Qtype == D.TypeAAAA) {
return true
}
return false
}
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()
if len(msg.Answer) > 0 {
ttl := uint32(expireTime.Sub(time.Now()).Seconds())
for _, answer := range msg.Answer {
answer.Header().Ttl = ttl
}
}
return
}
defer func() {
if msg != nil {
putMsgToCache(r.cache, q.String(), msg)
if r.mapping {
ips, err := r.msgToIP(msg)
if err != nil {
log.Debugln("[DNS] msg to ip error: %s", err.Error())
return
}
for _, ip := range ips {
putMsgToCache(r.cache, ip.String(), msg)
}
}
}
}()
isIPReq := isIPRequest(q)
if isIPReq {
msg, err = r.resolveIP(m)
return
}
msg, err = r.exchange(r.main, m)
return
}
func (r *Resolver) exchange(servers []*nameserver, m *D.Msg) (msg *D.Msg, err error) {
in := make(chan interface{})
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
fast := picker.SelectFast(ctx, in)
wg := sync.WaitGroup{}
wg.Add(len(servers))
for _, server := range servers {
go func(s *nameserver) {
defer wg.Done()
msg, _, err := s.Client.Exchange(m, s.Address)
if err != nil || msg.Rcode != D.RcodeSuccess {
return
}
in <- &result{Msg: msg, Error: err}
}(server)
}
// release in channel
go func() {
wg.Wait()
close(in)
}()
elm, exist := <-fast
if !exist {
return nil, errors.New("All DNS requests failed")
}
resp := elm.(*result)
msg, err = resp.Msg, resp.Error
return
}
func (r *Resolver) resolveIP(m *D.Msg) (msg *D.Msg, err error) {
msgCh := r.resolve(r.main, m)
if r.fallback == nil {
res := <-msgCh
msg, err = res.Msg, res.Error
return
}
fallbackMsg := r.resolve(r.fallback, m)
res := <-msgCh
if res.Error == nil {
if mmdb == nil {
return nil, errors.New("GeoIP can't use")
}
ips, err := r.msgToIP(res.Msg)
if err == nil {
if record, _ := mmdb.Country(ips[0]); record.Country.IsoCode == "CN" || record.Country.IsoCode == "" {
// release channel
go func() { <-fallbackMsg }()
msg = res.Msg
return msg, err
}
}
}
res = <-fallbackMsg
msg, err = res.Msg, res.Error
return
}
func (r *Resolver) ResolveIP(host string) (ip net.IP, err error) {
ip = net.ParseIP(host)
if ip != nil {
return ip, nil
}
query := &D.Msg{}
dnsType := D.TypeA
if r.ipv6 {
dnsType = D.TypeAAAA
}
query.SetQuestion(D.Fqdn(host), dnsType)
msg, err := r.Exchange(query)
if err != nil {
return nil, err
}
var ips []net.IP
ips, err = r.msgToIP(msg)
if err != nil {
return nil, err
}
ip = ips[0]
return
}
func (r *Resolver) msgToIP(msg *D.Msg) ([]net.IP, error) {
var 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)
}
}
if len(ips) == 0 {
return nil, errors.New("Can't parse msg")
}
return ips, nil
}
func (r *Resolver) IPToHost(ip net.IP) (string, bool) {
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) resolve(client []*nameserver, msg *D.Msg) <-chan *result {
ch := make(chan *result)
go func() {
res, err := r.exchange(client, msg)
ch <- &result{Msg: res, Error: err}
}()
return ch
}
func (r *Resolver) IsMapping() bool {
return r.mapping
}
type NameServer struct {
Net string
Addr string
}
type nameserver struct {
Client *D.Client
Address string
}
type Config struct {
Main, Fallback []NameServer
IPv6 bool
EnhancedMode EnhancedMode
}
func transform(servers []NameServer) []*nameserver {
var ret []*nameserver
for _, s := range servers {
ret = append(ret, &nameserver{
Client: &D.Client{
Net: s.Net,
TLSConfig: &tls.Config{
ClientSessionCache: globalSessionCache,
},
},
Address: s.Addr,
})
}
return ret
}
func New(config Config) *Resolver {
once.Do(func() {
mmdb, _ = geoip2.Open(C.Path.MMDB())
})
r := &Resolver{
main: transform(config.Main),
ipv6: config.IPv6,
cache: cache.New(time.Second * 60),
mapping: config.EnhancedMode == MAPPING,
}
if config.Fallback != nil {
r.fallback = transform(config.Fallback)
}
return r
}

62
dns/server.go Normal file
View File

@ -0,0 +1,62 @@
package dns
import (
"net"
D "github.com/miekg/dns"
)
var (
address string
server = &Server{}
)
type Server struct {
*D.Server
r *Resolver
}
func (s *Server) ServeDNS(w D.ResponseWriter, r *D.Msg) {
msg, err := s.r.Exchange(r)
if err != nil {
D.HandleFailed(w, r)
return
}
msg.SetReply(r)
w.WriteMsg(msg)
}
func ReCreateServer(addr string, resolver *Resolver) error {
if server.Server != nil {
server.Shutdown()
}
if addr == address {
return nil
}
_, 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
server = &Server{r: resolver}
server.Server = &D.Server{Addr: addr, PacketConn: p, Handler: server}
go func() {
server.ActivateAndServe()
}()
return nil
}

89
dns/util.go Normal file
View File

@ -0,0 +1,89 @@
package dns
import (
"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 "fakeip"
case MAPPING:
return "redir-host"
default:
return "unknown"
}
}
func putMsgToCache(c *cache.Cache, key string, msg *D.Msg) {
if len(msg.Answer) == 0 {
log.Debugln("[DNS] answer length is zero: %#v", msg)
return
}
ttl := time.Duration(msg.Answer[0].Header().Ttl) * time.Second
c.Put(key, msg, ttl)
}

15
go.mod
View File

@ -1,17 +1,20 @@
module github.com/Dreamacro/clash
require (
github.com/Dreamacro/go-shadowsocks2 v0.1.2-0.20181019110427-0a03f1a25270
github.com/Dreamacro/go-shadowsocks2 v0.1.3-0.20190202135136-da4602d8f112
github.com/eapache/queue v1.1.0 // indirect
github.com/go-chi/chi v3.3.3+incompatible
github.com/go-chi/chi v4.0.1+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/gofrs/uuid v3.2.0+incompatible
github.com/gorilla/websocket v1.4.0
github.com/miekg/dns v1.1.4
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/sirupsen/logrus v1.3.0
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613
golang.org/x/net v0.0.0-20181108082009-03003ca0c849
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
gopkg.in/eapache/channels.v1 v1.1.0
gopkg.in/yaml.v2 v2.2.1
gopkg.in/yaml.v2 v2.2.2
)

36
go.sum
View File

@ -1,37 +1,43 @@
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.3-0.20190202135136-da4602d8f112 h1:1axYxE0ZLJy40+ulq46XQt7MaJDJr4iGer1NQz7jmKw=
github.com/Dreamacro/go-shadowsocks2 v0.1.3-0.20190202135136-da4602d8f112/go.mod h1:giIuN+TuUudTxHc1jjTOyyQYiJ3VXp1pWOHdJbSCAPo=
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.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.1+incompatible h1:RSRC5qmFPtO90t7pTL0DBMNpZFsb/sHF3RXVlDgFisA=
github.com/go-chi/chi v4.0.1+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/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.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/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.4 h1:rCMZsU2ScVSYcAsOXgmC6+AKOK+6pmQTOcw03nfwYV0=
github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
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/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.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
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/crypto v0.0.0-20190131182504-b8fe1690c613 h1:MQ/ZZiDsUapFFiMS+vzwXkCTeEKaum+Do5rINYJDmxc=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20181108082009-03003ca0c849 h1:FSqE2GGG7wzsYUsWiQ8MZrvEd1EOyU3NCF0AW3Wtltg=
golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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=
@ -39,5 +45,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
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)
}

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

@ -0,0 +1,104 @@
package executor
import (
adapters "github.com/Dreamacro/clash/adapters/outbound"
"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"
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) {
if force {
updateGeneral(cfg.General)
}
updateProxies(cfg.Proxies)
updateRules(cfg.Rules)
updateDNS(cfg.DNS)
}
func GetGeneral() *config.General {
ports := P.GetPorts()
return &config.General{
Port: ports.Port,
SocksPort: ports.SocksPort,
RedirPort: ports.RedirPort,
AllowLan: P.AllowLan(),
Mode: T.Instance().Mode(),
LogLevel: log.Level(),
}
}
func updateDNS(c *config.DNS) {
if c.Enable == false {
T.Instance().SetResolver(nil)
dns.ReCreateServer("", nil)
return
}
r := dns.New(dns.Config{
Main: c.NameServer,
Fallback: c.Fallback,
IPv6: c.IPv6,
EnhancedMode: c.EnhancedMode,
})
T.Instance().SetResolver(r)
if err := dns.ReCreateServer(c.Listen, r); err != nil {
log.Errorln("Start DNS server error: %s", err.Error())
return
}
log.Infoln("DNS server listening at: %s", c.Listen)
}
func updateProxies(proxies map[string]C.Proxy) {
tunnel := T.Instance()
oldProxies := tunnel.Proxies()
// close old goroutine
for _, proxy := range oldProxies {
switch raw := proxy.(type) {
case *adapters.URLTest:
raw.Close()
case *adapters.Fallback:
raw.Close()
}
}
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)
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())
}
}

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

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

@ -0,0 +1,102 @@
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"`
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)
}
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}
}

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

@ -0,0 +1,137 @@
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).(C.Proxy)
selector, ok := proxy.(*A.Selector)
if !ok {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
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)
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)):
render.Status(r, http.StatusRequestTimeout)
render.JSON(w, r, ErrRequestTimeout)
case t := <-sigCh:
if t == 0 {
render.Status(r, http.StatusServiceUnavailable)
render.JSON(w, r, newError("An error occurred in the delay test"))
} else {
render.JSON(w, r, render.M{
"delay": t,
})
}
}
}

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

View File

@ -1,4 +1,4 @@
package hub
package route
import (
"encoding/json"
@ -6,65 +6,75 @@ import (
"strings"
"time"
"github.com/Dreamacro/clash/config"
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"
log "github.com/sirupsen/logrus"
)
var secret = ""
var (
serverSecret = ""
serverAddr = ""
uiPath = ""
)
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
}
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", "DELETE", "OPTIONS"},
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
MaxAge: 300,
})
r.Use(cors.Handler, authentication)
root := chi.NewRouter().With(jsonContentType)
root.Get("/traffic", traffic)
root.Get("/logs", getLogs)
r.With(jsonContentType).Get("/traffic", traffic)
r.With(jsonContentType).Get("/logs", getLogs)
r.Mount("/configs", configRouter())
r.Mount("/proxies", proxyRouter())
r.Mount("/rules", ruleRouter())
r.Get("/", hello)
r.Group(func(r chi.Router) {
r.Use(cors.Handler, authentication)
log.Infof("RESTful API listening at: %s", addr)
r.Mount("/", root)
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/", 301).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.Errorf("External controller error: %s", err.Error())
log.Errorln("External controller error: %s", err.Error())
}
}
@ -81,18 +91,16 @@ func authentication(next http.Handler) http.Handler {
header := r.Header.Get("Authorization")
text := strings.SplitN(header, " ", 2)
if secret == "" {
if serverSecret == "" {
next.ServeHTTP(w, r)
return
}
hasUnvalidHeader := text[0] != "Bearer"
hasUnvalidSecret := len(text) == 2 && text[1] != secret
hasUnvalidSecret := len(text) == 2 && text[1] != serverSecret
if hasUnvalidHeader || hasUnvalidSecret {
w.WriteHeader(http.StatusUnauthorized)
render.JSON(w, r, Error{
Error: "Authentication failed",
})
render.Status(r, http.StatusUnauthorized)
render.JSON(w, r, ErrUnauthorized)
return
}
next.ServeHTTP(w, r)
@ -100,17 +108,15 @@ func authentication(next http.Handler) http.Handler {
return http.HandlerFunc(fn)
}
type contextKey string
func (c contextKey) String() string {
return "clash context key " + string(c)
func hello(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, render.M{"hello": "clash"})
}
func traffic(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
render.Status(r, http.StatusOK)
tick := time.NewTicker(time.Second)
t := tunnel.Traffic()
t := T.Instance().Traffic()
for range tick.C {
up, down := t.Now()
if err := json.NewEncoder(w).Encode(Traffic{
@ -134,29 +140,18 @@ func getLogs(w http.ResponseWriter, r *http.Request) {
levelText = "info"
}
level, ok := C.LogLevelMapping[levelText]
level, ok := log.LogLevelMapping[levelText]
if !ok {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Level error",
})
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
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
}
sub := log.Subscribe()
render.Status(r, http.StatusOK)
for elm := range sub {
log := elm.(T.Log)
if log.LogLevel > level {
log := elm.(*log.Event)
if log.LogLevel < level {
continue
}
@ -169,10 +164,3 @@ func getLogs(w http.ResponseWriter, r *http.Request) {
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"
}
}

93
log/log.go Normal file
View File

@ -0,0 +1,93 @@
package log
import (
"fmt"
"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.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 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...),
}
}

26
main.go
View File

@ -1,17 +1,15 @@
package main
import (
"flag"
"os"
"os/signal"
"path/filepath"
"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"
)
@ -26,21 +24,19 @@ func init() {
}
func main() {
tunnel.Instance().Run()
proxy.Instance().Run()
hub.Run()
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())
}

View File

@ -6,35 +6,33 @@ import (
"net/http"
"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 NewHttpProxy(addr string) (*C.ProxySignal, error) {
type HttpListener struct {
net.Listener
address string
closed bool
}
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}
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
@ -43,14 +41,16 @@ func NewHttpProxy(addr string) (*C.ProxySignal, error) {
}
}()
go func() {
<-done
close(done)
l.Close()
closed <- struct{}{}
}()
return hl, nil
}
return signal, nil
func (l *HttpListener) Close() {
l.closed = true
l.Listener.Close()
}
func (l *HttpListener) Address() string {
return l.address
}
func handleConn(conn net.Conn) {

View File

@ -1,116 +1,150 @@
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
socksListener *socks.SockListener
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
}
signal, err := http.NewHttpProxy(addr)
if err != nil {
return err
}
l.httpSignal = signal
return nil
type Ports struct {
Port int `json:"port"`
SocksPort int `json:"socks-port"`
RedirPort int `json:"redir-port"`
}
func (l *Listener) updateSocks(addr string) error {
if l.socksSignal != nil {
signal := l.socksSignal
signal.Done <- struct{}{}
<-signal.Closed
l.socksSignal = nil
}
signal, err := socks.NewSocksProxy(addr)
if err != nil {
return err
}
l.socksSignal = signal
return nil
func AllowLan() bool {
return allowLan
}
func (l *Listener) updateRedir(addr string) error {
if l.redirSignal != nil {
signal := l.redirSignal
signal.Done <- struct{}{}
<-signal.Closed
l.redirSignal = nil
}
signal, err := redir.NewRedirProxy(addr)
if err != nil {
return err
}
l.redirSignal = signal
return nil
func SetAllowLan(al bool) {
allowLan = al
}
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 ReCreateHTTP(port int) error {
addr := genAddr(port, allowLan)
if httpListener != nil {
if httpListener.Address() == addr {
return nil
}
httpListener.Close()
httpListener = nil
}
if portIsZero(addr) {
return nil
}
var err error
httpListener, err = http.NewHttpProxy(addr)
if err != nil {
return err
}
return nil
}
// Run ensure config monitoring
func (l *Listener) Run() {
signal := make(chan struct{})
go l.process(signal)
<-signal
func ReCreateSocks(port int) error {
addr := genAddr(port, allowLan)
if socksListener != nil {
if socksListener.Address() == addr {
return nil
}
socksListener.Close()
socksListener = nil
}
if portIsZero(addr) {
return nil
}
var err error
socksListener, err = socks.NewSocksProxy(addr)
if err != nil {
return err
}
return nil
}
func newListener() *Listener {
return &Listener{}
func ReCreateRedir(port int) error {
addr := genAddr(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
}
// Instance return singleton instance of Listener
func Instance() *Listener {
once.Do(func() {
listener = newListener()
})
return listener
// 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(port int, allowLan bool) string {
if allowLan {
return fmt.Sprintf(":%d", port)
}
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))
}

View File

@ -0,0 +1,52 @@
package redir
import (
"errors"
"net"
"syscall"
"unsafe"
"github.com/Dreamacro/go-shadowsocks2/socks"
)
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) (socks.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 socks.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) (socks.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] = socks.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

@ -5,35 +5,35 @@ import (
"github.com/Dreamacro/clash/adapters/inbound"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"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,14 +42,16 @@ 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) {
@ -59,5 +61,5 @@ func handleSocks(conn net.Conn) {
return
}
conn.(*net.TCPConn).SetKeepAlive(true)
tun.Add(adapters.NewSocket(target, conn))
tun.Add(adapters.NewSocket(target, conn, C.SOCKS))
}

View File

@ -1,6 +1,8 @@
package rules
import (
"strings"
C "github.com/Dreamacro/clash/constant"
)
@ -30,7 +32,7 @@ func (d *Domain) Payload() string {
func NewDomain(domain string, adapter string) *Domain {
return &Domain{
domain: domain,
domain: strings.ToLower(domain),
adapter: adapter,
}
}

View File

@ -33,7 +33,7 @@ func (dk *DomainKeyword) Payload() string {
func NewDomainKeyword(keyword string, adapter string) *DomainKeyword {
return &DomainKeyword{
keyword: keyword,
keyword: strings.ToLower(keyword),
adapter: adapter,
}
}

View File

@ -33,7 +33,7 @@ func (ds *DomainSuffix) Payload() string {
func NewDomainSuffix(suffix string, adapter string) *DomainSuffix {
return &DomainSuffix{
suffix: suffix,
suffix: strings.ToLower(suffix),
adapter: adapter,
}
}

View File

@ -7,20 +7,24 @@ import (
)
type IPCIDR struct {
ipnet *net.IPNet
adapter string
ipnet *net.IPNet
adapter string
isSourceIP bool
}
func (i *IPCIDR) RuleType() C.RuleType {
if i.isSourceIP {
return C.SourceIPCIDR
}
return C.IPCIDR
}
func (i *IPCIDR) IsMatch(metadata *C.Metadata) bool {
if metadata.IP == nil {
return false
ip := metadata.IP
if i.isSourceIP {
ip = metadata.SourceIP
}
return i.ipnet.Contains(*metadata.IP)
return ip != nil && i.ipnet.Contains(*ip)
}
func (i *IPCIDR) Adapter() string {
@ -31,12 +35,13 @@ func (i *IPCIDR) Payload() string {
return i.ipnet.String()
}
func NewIPCIDR(s string, adapter string) *IPCIDR {
func NewIPCIDR(s string, adapter string, isSourceIP bool) *IPCIDR {
_, ipnet, err := net.ParseCIDR(s)
if err != nil {
}
return &IPCIDR{
ipnet: ipnet,
adapter: adapter,
ipnet: ipnet,
adapter: adapter,
isSourceIP: isSourceIP,
}
}

View File

@ -5,11 +5,11 @@ import (
"io"
"net"
"net/http"
"strings"
"sync"
"time"
"github.com/Dreamacro/clash/adapters/inbound"
C "github.com/Dreamacro/clash/constant"
adapters "github.com/Dreamacro/clash/adapters/inbound"
)
const (
@ -21,12 +21,17 @@ const (
var bufPool = sync.Pool{New: func() interface{} { return make([]byte, bufferSize) }}
func (t *Tunnel) handleHTTP(request *adapters.HTTPAdapter, proxy C.ProxyAdapter) {
conn := newTrafficTrack(proxy.Conn(), t.traffic)
func (t *Tunnel) handleHTTP(request *adapters.HTTPAdapter, outbound net.Conn) {
conn := newTrafficTrack(outbound, t.traffic)
req := request.R
host := req.Host
keepalive := true
for {
if strings.ToLower(req.Header.Get("Connection")) == "close" {
keepalive = false
}
req.Header.Set("Connection", "close")
req.RequestURI = ""
adapters.RemoveHopByHopHeaders(req.Header)
@ -53,6 +58,10 @@ func (t *Tunnel) handleHTTP(request *adapters.HTTPAdapter, proxy C.ProxyAdapter)
break
}
if !keepalive {
break
}
req, err = http.ReadRequest(bufio.NewReader(request.Conn()))
if err != nil {
break
@ -66,8 +75,8 @@ func (t *Tunnel) handleHTTP(request *adapters.HTTPAdapter, proxy C.ProxyAdapter)
}
}
func (t *Tunnel) handleSOCKS(request *adapters.SocketAdapter, proxy C.ProxyAdapter) {
conn := newTrafficTrack(proxy.Conn(), t.traffic)
func (t *Tunnel) handleSOCKS(request *adapters.SocketAdapter, outbound net.Conn) {
conn := newTrafficTrack(outbound, t.traffic)
relay(request.Conn(), conn)
}

View File

@ -1,51 +0,0 @@
package tunnel
import (
"fmt"
C "github.com/Dreamacro/clash/constant"
log "github.com/sirupsen/logrus"
)
type Log struct {
LogLevel C.LogLevel
Payload string
}
func (l *Log) Type() string {
return l.LogLevel.String()
}
func print(data Log) {
switch data.LogLevel {
case C.INFO:
log.Infoln(data.Payload)
case C.WARNING:
log.Warnln(data.Payload)
case C.ERROR:
log.Errorln(data.Payload)
case C.DEBUG:
log.Debugln(data.Payload)
}
}
func (t *Tunnel) subscribeLogs() {
sub, err := t.observable.Subscribe()
if err != nil {
log.Fatalf("Can't subscribe tunnel log: %s", err.Error())
}
for elm := range sub {
data := elm.(Log)
if data.LogLevel <= t.logLevel {
print(data)
}
}
}
func newLog(logLevel C.LogLevel, format string, v ...interface{}) Log {
return Log{
LogLevel: logLevel,
Payload: fmt.Sprintf(format, v...),
}
}

65
tunnel/mode.go Normal file
View File

@ -0,0 +1,65 @@
package tunnel
import (
"encoding/json"
"errors"
)
type Mode int
var (
// ModeMapping is a mapping for Mode enum
ModeMapping = map[string]Mode{
Global.String(): Global,
Rule.String(): Rule,
Direct.String(): Direct,
}
)
const (
Global Mode = iota
Rule
Direct
)
// UnmarshalJSON unserialize Mode
func (m *Mode) UnmarshalJSON(data []byte) error {
var tp string
json.Unmarshal(data, &tp)
mode, exist := ModeMapping[tp]
if !exist {
return errors.New("invalid mode")
}
*m = mode
return nil
}
// UnmarshalYAML unserialize Mode with yaml
func (m *Mode) UnmarshalYAML(unmarshal func(interface{}) error) error {
var tp string
unmarshal(&tp)
mode, exist := ModeMapping[tp]
if !exist {
return errors.New("invalid mode")
}
*m = mode
return nil
}
// MarshalJSON serialize Mode
func (m Mode) MarshalJSON() ([]byte, error) {
return json.Marshal(m.String())
}
func (m Mode) String() string {
switch m {
case Global:
return "Global"
case Rule:
return "Rule"
case Direct:
return "Direct"
default:
return "Unknow"
}
}

View File

@ -1,15 +1,17 @@
package tunnel
import (
"fmt"
"net"
"sync"
"time"
InboundAdapter "github.com/Dreamacro/clash/adapters/inbound"
"github.com/Dreamacro/clash/common/observable"
cfg "github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/dns"
"github.com/Dreamacro/clash/log"
"gopkg.in/eapache/channels.v1"
channels "gopkg.in/eapache/channels.v1"
)
var (
@ -24,14 +26,10 @@ type Tunnel struct {
proxies map[string]C.Proxy
configLock *sync.RWMutex
traffic *C.Traffic
resolver *dns.Resolver
// Outbound Rule
mode cfg.Mode
// Log
logCh chan interface{}
observable *observable.Observable
logLevel C.LogLevel
mode Mode
}
// Add request to queue
@ -44,33 +42,47 @@ func (t *Tunnel) Traffic() *C.Traffic {
return t.traffic
}
// Log return clash log stream
func (t *Tunnel) Log() *observable.Observable {
return t.observable
// Rules return all rules
func (t *Tunnel) Rules() []C.Rule {
return t.rules
}
func (t *Tunnel) configMonitor(signal chan<- struct{}) {
sub := cfg.Instance().Subscribe()
signal <- struct{}{}
for elm := range sub {
event := elm.(*cfg.Event)
switch event.Type {
case "proxies":
proxies := event.Payload.(map[string]C.Proxy)
t.configLock.Lock()
t.proxies = proxies
t.configLock.Unlock()
case "rules":
rules := event.Payload.([]C.Rule)
t.configLock.Lock()
t.rules = rules
t.configLock.Unlock()
case "mode":
t.mode = event.Payload.(cfg.Mode)
case "log-level":
t.logLevel = event.Payload.(C.LogLevel)
}
}
// UpdateRules handle update rules
func (t *Tunnel) UpdateRules(rules []C.Rule) {
t.configLock.Lock()
t.rules = rules
t.configLock.Unlock()
}
// Proxies return all proxies
func (t *Tunnel) Proxies() map[string]C.Proxy {
return t.proxies
}
// UpdateProxies handle update proxies
func (t *Tunnel) UpdateProxies(proxies map[string]C.Proxy) {
t.configLock.Lock()
t.proxies = proxies
t.configLock.Unlock()
}
// Mode return current mode
func (t *Tunnel) Mode() Mode {
return t.mode
}
// SetMode change the mode of tunnel
func (t *Tunnel) SetMode(mode Mode) {
t.mode = mode
}
// SetResolver change the resolver of tunnel
func (t *Tunnel) SetResolver(resolver *dns.Resolver) {
t.resolver = resolver
}
func (t *Tunnel) hasResolver() bool {
return t.resolver != nil
}
func (t *Tunnel) process() {
@ -82,23 +94,58 @@ func (t *Tunnel) process() {
}
}
func (t *Tunnel) resolveIP(host string) (net.IP, error) {
if t.resolver == nil {
ipAddr, err := net.ResolveIPAddr("ip", host)
if err != nil {
return nil, err
}
return ipAddr.IP, nil
}
return t.resolver.ResolveIP(host)
}
func (t *Tunnel) needLookupIP(metadata *C.Metadata) bool {
return t.hasResolver() && t.resolver.IsMapping() && metadata.Host == "" && metadata.IP != nil
}
func (t *Tunnel) handleConn(localConn C.ServerAdapter) {
defer localConn.Close()
metadata := localConn.Metadata()
if t.needLookupIP(metadata) {
host, exist := t.resolver.IPToHost(*metadata.IP)
if exist {
metadata.Host = host
metadata.AddrType = C.AtypDomainName
}
}
var proxy C.Proxy
switch t.mode {
case cfg.Direct:
case Direct:
proxy = t.proxies["DIRECT"]
case cfg.Global:
case Global:
proxy = t.proxies["GLOBAL"]
// Rule
default:
proxy = t.match(metadata)
var err error
proxy, err = t.match(metadata)
if err != nil {
return
}
}
if !metadata.Valid() {
log.Warnln("[Metadata] not valid: %#v", metadata)
return
}
remoConn, err := proxy.Generator(metadata)
if err != nil {
t.logCh <- newLog(C.WARNING, "Proxy connect error: %s", err.Error())
log.Warnln("Proxy[%s] connect [%s --> %s] error: %s", proxy.Name(), metadata.SourceIP.String(), metadata.String(), err.Error())
return
}
defer remoConn.Close()
@ -111,44 +158,42 @@ func (t *Tunnel) handleConn(localConn C.ServerAdapter) {
}
}
func (t *Tunnel) match(metadata *C.Metadata) C.Proxy {
func (t *Tunnel) shouldResolveIP(rule C.Rule, metadata *C.Metadata) bool {
return (rule.RuleType() == C.GEOIP || rule.RuleType() == C.IPCIDR) && metadata.Host != "" && metadata.IP == nil
}
func (t *Tunnel) match(metadata *C.Metadata) (C.Proxy, error) {
t.configLock.RLock()
defer t.configLock.RUnlock()
for _, rule := range t.rules {
if rule.IsMatch(metadata) {
a, ok := t.proxies[rule.Adapter()]
if !ok {
continue
if t.shouldResolveIP(rule, metadata) {
ip, err := t.resolveIP(metadata.Host)
if err != nil {
return nil, fmt.Errorf("[DNS] resolve %s error: %s", metadata.Host, err.Error())
}
log.Debugln("[DNS] %s --> %s", metadata.Host, ip.String())
metadata.IP = &ip
}
if rule.IsMatch(metadata) {
if a, ok := t.proxies[rule.Adapter()]; ok {
log.Infoln("%s --> %v match %s using %s", metadata.SourceIP.String(), metadata.String(), rule.RuleType().String(), rule.Adapter())
return a, nil
}
t.logCh <- newLog(C.INFO, "%v match %s using %s", metadata.String(), rule.RuleType().String(), rule.Adapter())
return a
}
}
t.logCh <- newLog(C.INFO, "%v doesn't match any rule using DIRECT", metadata.String())
return t.proxies["DIRECT"]
}
// Run initial task
func (t *Tunnel) Run() {
go t.process()
go t.subscribeLogs()
signal := make(chan struct{})
go t.configMonitor(signal)
<-signal
log.Infoln("%s --> %v doesn't match any rule using DIRECT", metadata.SourceIP.String(), metadata.String())
return t.proxies["DIRECT"], nil
}
func newTunnel() *Tunnel {
logCh := make(chan interface{})
return &Tunnel{
queue: channels.NewInfiniteChannel(),
proxies: make(map[string]C.Proxy),
observable: observable.NewObservable(logCh),
logCh: logCh,
configLock: &sync.RWMutex{},
traffic: C.NewTraffic(time.Second),
mode: cfg.Rule,
logLevel: C.INFO,
mode: Rule,
}
}
@ -156,6 +201,7 @@ func newTunnel() *Tunnel {
func Instance() *Tunnel {
once.Do(func() {
tunnel = newTunnel()
go tunnel.process()
})
return tunnel
}