Compare commits

...

24 Commits

Author SHA1 Message Date
f93d6aa294 Fix: crash when dns not set 2018-12-05 21:52:31 +08:00
f192d591c7 Chore: bump to 0.10.0 2018-12-05 21:26:04 +08:00
03c249ecb1 Feature: add custom DNS support (#56) 2018-12-05 21:13:29 +08:00
da5db36ccf Fix: policy group unexpectedly closed 2018-12-05 18:19:30 +08:00
ca6e67a384 Feature: add silent info level 2018-12-03 23:41:40 +08:00
6636db242b Feature: add http/https [connect] proxy (#52) 2018-12-03 23:27:00 +08:00
f5715c4f42 Fix: chunk size limit in tls obfs (#54)
* Fix: add chunkSize limit in TLSObfs

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

* Chore: improve proxy architecture

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

* Chore: make code unified

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

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

View File

@ -5,7 +5,7 @@
<br> <br>
</h1> </h1>
<h4 align="center">A rule based proxy in Go.</h4> <h4 align="center">A rule based tunnel in Go.</h4>
<p align="center"> <p align="center">
<a href="https://travis-ci.org/Dreamacro/clash"> <a href="https://travis-ci.org/Dreamacro/clash">
@ -22,16 +22,12 @@
## Features ## Features
- HTTP/HTTPS and SOCKS proxy - HTTP/HTTPS and SOCKS protocol
- Surge like configuration - Surge like configuration
- GeoIP rule support - GeoIP rule support
- Support Vmess/Shadowsocks/Socks5 - Support Vmess/Shadowsocks/Socks5
- Support for Netfilter TCP redirect - Support for Netfilter TCP redirect
## Discussion
[Telegram Group](https://t.me/clash_discuss)
## Install ## Install
You can build from source: You can build from source:
@ -42,7 +38,7 @@ go get -u -v github.com/Dreamacro/clash
Pre-built binaries are available: [release](https://github.com/Dreamacro/clash/releases) Pre-built binaries are available: [release](https://github.com/Dreamacro/clash/releases)
Requires Go >= 1.10. Requires Go >= 1.11.
## Daemon ## Daemon
@ -85,7 +81,7 @@ port: 7890
# port of SOCKS5 # port of SOCKS5
socks-port: 7891 socks-port: 7891
# redir proxy for Linux and macOS # redir port for Linux and macOS
# redir-port: 7892 # redir-port: 7892
allow-lan: false allow-lan: false
@ -94,7 +90,7 @@ allow-lan: false
mode: Rule mode: Rule
# set log level to stdout (default is info) # set log level to stdout (default is info)
# info / warning / error / debug # info / warning / error / debug / silent
log-level: info log-level: info
# A RESTful API for clash # A RESTful API for clash
@ -103,6 +99,17 @@ external-controller: 127.0.0.1:9090
# Secret for RESTful API (Optional) # Secret for RESTful API (Optional)
# secret: "" # 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: Proxy:
# shadowsocks # shadowsocks
@ -126,11 +133,22 @@ Proxy:
# socks5 # socks5
- { name: "socks", type: socks5, server: server, port: 443 } - { 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 # with tls
- { name: "socks", type: socks5, server: server, port: 443, tls: true } - { name: "socks", type: socks5, server: server, port: 443, tls: true }
# with tls and skip-cert-verify # with tls and skip-cert-verify
- { name: "socks", type: socks5, server: server, port: 443, tls: true, skip-cert-verify: true } - { name: "socks", type: socks5, server: server, port: 443, 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: Proxy Group:
# url-test select which proxy will be used by benchmarking speed to a URL. # 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 }
@ -149,8 +167,9 @@ Rule:
- DOMAIN-SUFFIX,ad.com,REJECT - DOMAIN-SUFFIX,ad.com,REJECT
- IP-CIDR,127.0.0.0/8,DIRECT - IP-CIDR,127.0.0.0/8,DIRECT
- GEOIP,CN,DIRECT - GEOIP,CN,DIRECT
# note: there is two "," # FINAL would remove after prerelease
- FINAL,,Proxy # you also can use `FINAL,Proxy` or `FINAL,,Proxy` now
- MATCH,Proxy
``` ```
## Thanks ## Thanks

View File

@ -29,9 +29,12 @@ func (s *SocketAdapter) Conn() net.Conn {
} }
// NewSocket is SocketAdapter generator // 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
return &SocketAdapter{ return &SocketAdapter{
conn: conn, conn: conn,
metadata: parseSocksAddr(target), metadata: metadata,
} }
} }

View File

@ -10,32 +10,26 @@ import (
) )
func parseSocksAddr(target socks.Addr) *C.Metadata { func parseSocksAddr(target socks.Addr) *C.Metadata {
var host, port string metadata := &C.Metadata{
var ip net.IP NetWork: C.TCP,
AddrType: int(target[0]),
}
switch target[0] { switch target[0] {
case socks.AtypDomainName: case socks.AtypDomainName:
host = string(target[2 : 2+target[1]]) metadata.Host = string(target[2 : 2+target[1]])
port = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1])) metadata.Port = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
ipAddr, err := net.ResolveIPAddr("ip", host)
if err == nil {
ip = ipAddr.IP
}
case socks.AtypIPv4: case socks.AtypIPv4:
ip = net.IP(target[1 : 1+net.IPv4len]) ip := net.IP(target[1 : 1+net.IPv4len])
port = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1])) metadata.IP = &ip
metadata.Port = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
case socks.AtypIPv6: case socks.AtypIPv6:
ip = net.IP(target[1 : 1+net.IPv6len]) ip := net.IP(target[1 : 1+net.IPv6len])
port = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1])) metadata.IP = &ip
metadata.Port = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
} }
return &C.Metadata{ return metadata
NetWork: C.TCP,
AddrType: int(target[0]),
Host: host,
IP: &ip,
Port: port,
}
} }
func parseHTTPAddr(request *http.Request) *C.Metadata { func parseHTTPAddr(request *http.Request) *C.Metadata {
@ -44,28 +38,26 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
if port == "" { if port == "" {
port = "80" port = "80"
} }
ipAddr, err := net.ResolveIPAddr("ip", host)
var resolveIP *net.IP
if err == nil {
resolveIP = &ipAddr.IP
}
var addType int metadata := &C.Metadata{
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{
NetWork: C.TCP, NetWork: C.TCP,
AddrType: addType, Source: C.HTTP,
AddrType: C.AtypDomainName,
Host: host, Host: host,
IP: resolveIP, IP: nil,
Port: port, 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
} }

View File

@ -1,6 +1,7 @@
package adapters package adapters
import ( import (
"encoding/json"
"net" "net"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
@ -32,7 +33,12 @@ func (d *Direct) Type() C.AdapterType {
} }
func (d *Direct) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) { func (d *Direct) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
c, err := net.DialTimeout("tcp", net.JoinHostPort(metadata.String(), metadata.Port), tcpTimeout) address := net.JoinHostPort(metadata.Host, metadata.Port)
if metadata.IP != nil {
address = net.JoinHostPort(metadata.IP.String(), metadata.Port)
}
c, err := net.DialTimeout("tcp", address, tcpTimeout)
if err != nil { if err != nil {
return return
} }
@ -40,6 +46,12 @@ func (d *Direct) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err er
return &DirectAdapter{conn: c}, nil return &DirectAdapter{conn: c}, nil
} }
func (d *Direct) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": d.Type().String(),
})
}
func NewDirect() *Direct { func NewDirect() *Direct {
return &Direct{} return &Direct{}
} }

View File

@ -1,6 +1,7 @@
package adapters package adapters
import ( import (
"encoding/json"
"errors" "errors"
"sync" "sync"
"time" "time"
@ -63,6 +64,18 @@ func (f *Fallback) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err
return f.proxies[0].RawProxy.Generator(metadata) 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() { func (f *Fallback) Close() {
f.done <- struct{}{} f.done <- struct{}{}
} }

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

@ -0,0 +1,148 @@
package adapters
import (
"bufio"
"bytes"
"crypto/tls"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"strconv"
C "github.com/Dreamacro/clash/constant"
)
// HTTPAdapter is a proxy adapter
type HTTPAdapter struct {
conn net.Conn
}
// Close is used to close connection
func (ha *HTTPAdapter) Close() {
ha.conn.Close()
}
func (ha *HTTPAdapter) Conn() net.Conn {
return ha.conn
}
type Http struct {
addr string
name 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) Name() string {
return h.name
}
func (h *Http) Type() C.AdapterType {
return C.Http
}
func (h *Http) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err 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 &HTTPAdapter{conn: c}, nil
}
func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
var buf bytes.Buffer
var err error
buf.WriteString("CONNECT ")
buf.WriteString(net.JoinHostPort(metadata.Host, metadata.Port))
buf.WriteString(" HTTP/1.1\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 (h *Http) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": h.Type().String(),
})
}
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{
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,6 +1,7 @@
package adapters package adapters
import ( import (
"encoding/json"
"io" "io"
"net" "net"
"time" "time"
@ -36,6 +37,12 @@ func (r *Reject) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err er
return &RejectAdapter{conn: &NopConn{}}, nil return &RejectAdapter{conn: &NopConn{}}, nil
} }
func (r *Reject) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": r.Type().String(),
})
}
func NewReject() *Reject { func NewReject() *Reject {
return &Reject{} return &Reject{}
} }
@ -43,7 +50,7 @@ func NewReject() *Reject {
type NopConn struct{} type NopConn struct{}
func (rw *NopConn) Read(b []byte) (int, error) { func (rw *NopConn) Read(b []byte) (int, error) {
return len(b), nil return 0, io.EOF
} }
func (rw *NopConn) Write(b []byte) (int, error) { func (rw *NopConn) Write(b []byte) (int, error) {

View File

@ -1,6 +1,7 @@
package adapters package adapters
import ( import (
"encoding/json"
"errors" "errors"
"sort" "sort"
@ -30,17 +31,21 @@ func (s *Selector) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err
return s.selected.Generator(metadata) return s.selected.Generator(metadata)
} }
func (s *Selector) Now() string { func (s *Selector) MarshalJSON() ([]byte, error) {
return s.selected.Name()
}
func (s *Selector) All() []string {
var all []string var all []string
for k := range s.proxies { for k := range s.proxies {
all = append(all, k) all = append(all, k)
} }
sort.Strings(all) 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 { func (s *Selector) Set(name string) error {

View File

@ -2,6 +2,7 @@ package adapters
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"net" "net"
"strconv" "strconv"
@ -71,8 +72,14 @@ func (ss *ShadowSocks) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter,
return &ShadowsocksAdapter{conn: c}, err return &ShadowsocksAdapter{conn: c}, err
} }
func (ss *ShadowSocks) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": ss.Type().String(),
})
}
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
server := fmt.Sprintf("%s:%d", option.Server, option.Port) server := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
cipher := option.Cipher cipher := option.Cipher
password := option.Password password := option.Password
ciph, err := core.PickCipher(cipher, nil, password) ciph, err := core.PickCipher(cipher, nil, password)

View File

@ -3,10 +3,12 @@ package adapters
import ( import (
"bytes" "bytes"
"crypto/tls" "crypto/tls"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"net" "net"
"strconv"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
@ -30,14 +32,19 @@ func (ss *Socks5Adapter) Conn() net.Conn {
type Socks5 struct { type Socks5 struct {
addr string addr string
name string name string
user string
pass string
tls bool tls bool
skipCertVerify bool skipCertVerify bool
tlsConfig *tls.Config
} }
type Socks5Option struct { type Socks5Option struct {
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
UserName string `proxy:"username,omitempty"`
Password string `proxy:"password,omitempty"`
TLS bool `proxy:"tls,omitempty"` TLS bool `proxy:"tls,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
} }
@ -54,11 +61,9 @@ func (ss *Socks5) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err e
c, err := net.DialTimeout("tcp", ss.addr, tcpTimeout) c, err := net.DialTimeout("tcp", ss.addr, tcpTimeout)
if err == nil && ss.tls { if err == nil && ss.tls {
tlsConfig := tls.Config{ cc := tls.Client(c, ss.tlsConfig)
InsecureSkipVerify: ss.skipCertVerify, err = cc.Handshake()
MaxVersion: tls.VersionTLS12, c = cc
}
c = tls.Client(c, &tlsConfig)
} }
if err != nil { if err != nil {
@ -71,21 +76,55 @@ func (ss *Socks5) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err e
return &Socks5Adapter{conn: c}, nil return &Socks5Adapter{conn: c}, nil
} }
func (ss *Socks5) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": ss.Type().String(),
})
}
func (ss *Socks5) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error { func (ss *Socks5) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
buf := make([]byte, socks.MaxAddrLen) buf := make([]byte, socks.MaxAddrLen)
var err error
// VER, CMD, RSV // VER, NMETHODS, METHODS
_, err := rw.Write([]byte{5, 1, 0}) if len(ss.user) > 0 {
_, err = rw.Write([]byte{5, 1, 2})
} else {
_, err = rw.Write([]byte{5, 1, 0})
}
if err != nil { if err != nil {
return err return err
} }
// VER, METHOD
if _, err := io.ReadFull(rw, buf[:2]); err != nil { if _, err := io.ReadFull(rw, buf[:2]); err != nil {
return err return err
} }
if buf[0] != 5 { if buf[0] != 5 {
return errors.New("SOCKS version error") 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 { } else if buf[1] != 0 {
return errors.New("SOCKS need auth") return errors.New("SOCKS need auth")
} }
@ -103,10 +142,24 @@ func (ss *Socks5) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
} }
func NewSocks5(option Socks5Option) *Socks5 { func NewSocks5(option Socks5Option) *Socks5 {
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 &Socks5{ return &Socks5{
addr: fmt.Sprintf("%s:%d", option.Server, option.Port), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
name: option.Name, name: option.Name,
user: option.UserName,
pass: option.Password,
tls: option.TLS, tls: option.TLS,
skipCertVerify: option.SkipCertVerify, skipCertVerify: option.SkipCertVerify,
tlsConfig: tlsConfig,
} }
} }

View File

@ -1,7 +1,9 @@
package adapters package adapters
import ( import (
"encoding/json"
"errors" "errors"
"sort"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -46,6 +48,19 @@ func (u *URLTest) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err e
return a, err 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() { func (u *URLTest) Close() {
u.done <- struct{}{} u.done <- struct{}{}
} }

View File

@ -1,10 +1,12 @@
package adapters package adapters
import ( import (
"crypto/tls"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"sync"
"time" "time"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
@ -14,6 +16,11 @@ const (
tcpTimeout = 5 * time.Second tcpTimeout = 5 * time.Second
) )
var (
globalClientSessionCache tls.ClientSessionCache
once sync.Once
)
// DelayTest get the delay for the specified URL // DelayTest get the delay for the specified URL
func DelayTest(proxy C.Proxy, url string) (t int16, err error) { func DelayTest(proxy C.Proxy, url string) (t int16, err error) {
addr, err := urlToMetadata(url) addr, err := urlToMetadata(url)
@ -95,3 +102,10 @@ func tcpKeepAlive(c net.Conn) {
tcp.SetKeepAlivePeriod(30 * time.Second) tcp.SetKeepAlivePeriod(30 * time.Second)
} }
} }
func getClientSessionCache() tls.ClientSessionCache {
once.Do(func() {
globalClientSessionCache = tls.NewLRUClientSessionCache(128)
})
return globalClientSessionCache
}

View File

@ -1,6 +1,7 @@
package adapters package adapters
import ( import (
"encoding/json"
"fmt" "fmt"
"net" "net"
"strconv" "strconv"
@ -43,24 +44,30 @@ type VmessOption struct {
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
} }
func (ss *Vmess) Name() string { func (v *Vmess) Name() string {
return ss.name return v.name
} }
func (ss *Vmess) Type() C.AdapterType { func (v *Vmess) Type() C.AdapterType {
return C.Vmess return C.Vmess
} }
func (ss *Vmess) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) { func (v *Vmess) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
c, err := net.DialTimeout("tcp", ss.server, tcpTimeout) c, err := net.DialTimeout("tcp", v.server, tcpTimeout)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error", ss.server) return nil, fmt.Errorf("%s connect error", v.server)
} }
tcpKeepAlive(c) tcpKeepAlive(c)
c, err = ss.client.New(c, parseVmessAddr(metadata)) c, err = v.client.New(c, parseVmessAddr(metadata))
return &VmessAdapter{conn: c}, err return &VmessAdapter{conn: c}, err
} }
func (v *Vmess) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"type": v.Type().String(),
})
}
func NewVmess(option VmessOption) (*Vmess, error) { func NewVmess(option VmessOption) (*Vmess, error) {
security := strings.ToLower(option.Cipher) security := strings.ToLower(option.Cipher)
client, err := vmess.NewClient(vmess.Config{ client, err := vmess.NewClient(vmess.Config{
@ -68,10 +75,12 @@ func NewVmess(option VmessOption) (*Vmess, error) {
AlterID: uint16(option.AlterID), AlterID: uint16(option.AlterID),
Security: security, Security: security,
TLS: option.TLS, TLS: option.TLS,
Host: fmt.Sprintf("%s:%d", option.Server, option.Port), HostName: option.Server,
Port: strconv.Itoa(option.Port),
NetWork: option.Network, NetWork: option.Network,
WebSocketPath: option.WSPath, WebSocketPath: option.WSPath,
SkipCertVerify: option.SkipCertVerify, SkipCertVerify: option.SkipCertVerify,
SessionCacahe: getClientSessionCache(),
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -79,7 +88,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
return &Vmess{ return &Vmess{
name: option.Name, name: option.Name,
server: fmt.Sprintf("%s:%d", option.Server, option.Port), server: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
client: client, client: client,
}, nil }, nil
} }

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

@ -0,0 +1,91 @@
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
}
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()
}

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

@ -14,6 +14,10 @@ func init() {
rand.Seed(time.Now().Unix()) rand.Seed(time.Now().Unix())
} }
const (
chunkSize = 1 << 14 // 2 ** 14 == 16 * 1024
)
var bufPool = sync.Pool{New: func() interface{} { return make([]byte, 2048) }} var bufPool = sync.Pool{New: func() interface{} { return make([]byte, 2048) }}
// TLSObfs is shadowsocks tls simple-obfs implementation // TLSObfs is shadowsocks tls simple-obfs implementation
@ -75,8 +79,23 @@ func (to *TLSObfs) Read(b []byte) (int, error) {
// type + ver = 3 // type + ver = 3
return to.read(b, 3) return to.read(b, 3)
} }
func (to *TLSObfs) Write(b []byte) (int, error) { 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 { if to.firstRequest {
helloMsg := makeClientHelloMsg(b, to.server) helloMsg := makeClientHelloMsg(b, to.server)
_, err := to.Conn.Write(helloMsg) _, 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 { func makeClientHelloMsg(data []byte, server string) []byte {
random := make([]byte, 32) random := make([]byte, 28)
sessionID := make([]byte, 32) sessionID := make([]byte, 32)
size := make([]byte, 2)
rand.Read(random) rand.Read(random)
rand.Read(sessionID) rand.Read(sessionID)
@ -124,12 +142,12 @@ func makeClientHelloMsg(data []byte, server string) []byte {
// clientHello, length, TLS 1.2 version // clientHello, length, TLS 1.2 version
buf.WriteByte(1) buf.WriteByte(1)
binary.BigEndian.PutUint16(size, uint16(208+len(data)+len(server)))
buf.WriteByte(0) buf.WriteByte(0)
buf.Write(size) binary.Write(buf, binary.BigEndian, uint16(208+len(data)+len(server)))
buf.Write([]byte{0x03, 0x03}) 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.Write(random)
buf.WriteByte(32) buf.WriteByte(32)
buf.Write(sessionID) buf.Write(sessionID)
@ -147,24 +165,19 @@ func makeClientHelloMsg(data []byte, server string) []byte {
buf.Write([]byte{0x01, 0x00}) buf.Write([]byte{0x01, 0x00})
// extension length // extension length
binary.BigEndian.PutUint16(size, uint16(79+len(data)+len(server))) binary.Write(buf, binary.BigEndian, uint16(79+len(data)+len(server)))
buf.Write(size)
// session ticket // session ticket
buf.Write([]byte{0x00, 0x23}) buf.Write([]byte{0x00, 0x23})
binary.BigEndian.PutUint16(size, uint16(len(data))) binary.Write(buf, binary.BigEndian, uint16(len(data)))
buf.Write(size)
buf.Write(data) buf.Write(data)
// server name // server name
buf.Write([]byte{0x00, 0x00}) buf.Write([]byte{0x00, 0x00})
binary.BigEndian.PutUint16(size, uint16(len(server)+5)) binary.Write(buf, binary.BigEndian, uint16(len(server)+5))
buf.Write(size) binary.Write(buf, binary.BigEndian, uint16(len(server)+3))
binary.BigEndian.PutUint16(size, uint16(len(server)+3))
buf.Write(size)
buf.WriteByte(0) buf.WriteByte(0)
binary.BigEndian.PutUint16(size, uint16(len(server))) binary.Write(buf, binary.BigEndian, uint16(len(server)))
buf.Write(size)
buf.Write([]byte(server)) buf.Write([]byte(server))
// ec_point // ec_point

View File

@ -65,11 +65,10 @@ func (vc *Conn) Read(b []byte) (int, error) {
} }
func (vc *Conn) sendRequest() error { func (vc *Conn) sendRequest() error {
timestamp := make([]byte, 8) timestamp := time.Now()
binary.BigEndian.PutUint64(timestamp, uint64(time.Now().UTC().Unix()))
h := hmac.New(md5.New, vc.id.UUID.Bytes()) h := hmac.New(md5.New, vc.id.UUID.Bytes())
h.Write(timestamp) binary.Write(h, binary.BigEndian, uint64(timestamp.Unix()))
_, err := vc.Conn.Write(h.Sum(nil)) _, err := vc.Conn.Write(h.Sum(nil))
if err != nil { if err != nil {
return err return err
@ -111,7 +110,7 @@ func (vc *Conn) sendRequest() error {
return err return err
} }
stream := cipher.NewCFBEncrypter(block, hashTimestamp(time.Now().UTC())) stream := cipher.NewCFBEncrypter(block, hashTimestamp(timestamp))
stream.XORKeyStream(buf.Bytes(), buf.Bytes()) stream.XORKeyStream(buf.Bytes(), buf.Bytes())
_, err = vc.Conn.Write(buf.Bytes()) _, err = vc.Conn.Write(buf.Bytes())
return err return err
@ -145,7 +144,7 @@ func (vc *Conn) recvResponse() error {
func hashTimestamp(t time.Time) []byte { func hashTimestamp(t time.Time) []byte {
md5hash := md5.New() md5hash := md5.New()
ts := make([]byte, 8) ts := make([]byte, 8)
binary.BigEndian.PutUint64(ts, uint64(t.UTC().Unix())) binary.BigEndian.PutUint64(ts, uint64(t.Unix()))
md5hash.Write(ts) md5hash.Write(ts)
md5hash.Write(ts) md5hash.Write(ts)
md5hash.Write(ts) md5hash.Write(ts)

View File

@ -5,12 +5,10 @@ import (
"fmt" "fmt"
"math/rand" "math/rand"
"net" "net"
"net/url"
"runtime" "runtime"
"time" "sync"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
"github.com/gorilla/websocket"
) )
// Version of vmess // Version of vmess
@ -39,6 +37,11 @@ var CipherMapping = map[string]byte{
"chacha20-poly1305": SecurityCHACHA20POLY1305, "chacha20-poly1305": SecurityCHACHA20POLY1305,
} }
var (
clientSessionCache tls.ClientSessionCache
once sync.Once
)
// Command types // Command types
const ( const (
CommandTCP byte = 1 CommandTCP byte = 1
@ -61,14 +64,13 @@ type DstAddr struct {
// Client is vmess connection generator // Client is vmess connection generator
type Client struct { type Client struct {
user []*ID user []*ID
uuid *uuid.UUID uuid *uuid.UUID
security Security security Security
tls bool tls bool
host string host string
websocket bool wsConfig *websocketConfig
websocketPath string tlsConfig *tls.Config
skipCertVerify bool
} }
// Config of vmess // Config of vmess
@ -77,58 +79,25 @@ type Config struct {
AlterID uint16 AlterID uint16
Security string Security string
TLS bool TLS bool
Host string HostName string
Port string
NetWork string NetWork string
WebSocketPath string WebSocketPath string
SkipCertVerify bool SkipCertVerify bool
SessionCacahe tls.ClientSessionCache
} }
// New return a Conn with net.Conn and DstAddr // New return a Conn with net.Conn and DstAddr
func (c *Client) New(conn net.Conn, dst *DstAddr) (net.Conn, error) { func (c *Client) New(conn net.Conn, dst *DstAddr) (net.Conn, error) {
var err error
r := rand.Intn(len(c.user)) r := rand.Intn(len(c.user))
if c.websocket { if c.wsConfig != nil {
dialer := &websocket.Dialer{ conn, err = newWebsocketConn(conn, c.wsConfig)
NetDial: func(network, addr string) (net.Conn, error) {
return conn, nil
},
ReadBufferSize: 4 * 1024,
WriteBufferSize: 4 * 1024,
HandshakeTimeout: time.Second * 8,
}
scheme := "ws"
if c.tls {
scheme = "wss"
dialer.TLSClientConfig = &tls.Config{
InsecureSkipVerify: c.skipCertVerify,
}
}
host, port, err := net.SplitHostPort(c.host)
if (scheme == "ws" && port != "80") || (scheme == "wss" && port != "443") {
host = c.host
}
uri := url.URL{
Scheme: scheme,
Host: host,
Path: c.websocketPath,
}
wsConn, resp, err := dialer.Dial(uri.String(), nil)
if err != nil { if err != nil {
var reason string return nil, err
if resp != nil {
reason = resp.Status
}
println(uri.String(), err.Error())
return nil, fmt.Errorf("Dial %s error: %s", host, reason)
} }
conn = newWebsocketConn(wsConn, conn.RemoteAddr())
} else if c.tls { } else if c.tls {
conn = tls.Client(conn, &tls.Config{ conn = tls.Client(conn, c.tlsConfig)
InsecureSkipVerify: c.skipCertVerify,
})
} }
return newConn(conn, c.user[r], dst, c.security), nil return newConn(conn, c.user[r], dst, c.security), nil
} }
@ -161,13 +130,44 @@ func NewClient(config Config) (*Client, error) {
return nil, fmt.Errorf("Unknown network type: %s", config.NetWork) 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,
}
if tlsConfig.ClientSessionCache == nil {
tlsConfig.ClientSessionCache = getClientSessionCache()
}
}
var wsConfig *websocketConfig
if config.NetWork == "ws" {
wsConfig = &websocketConfig{
host: host,
path: config.WebSocketPath,
tls: config.TLS,
tlsConfig: tlsConfig,
}
}
return &Client{ return &Client{
user: newAlterIDs(newID(&uid), config.AlterID), user: newAlterIDs(newID(&uid), config.AlterID),
uuid: &uid, uuid: &uid,
security: security, security: security,
tls: config.TLS, tls: config.TLS,
host: config.Host, host: host,
websocket: config.NetWork == "ws", wsConfig: wsConfig,
websocketPath: config.WebSocketPath, tlsConfig: tlsConfig,
}, nil }, nil
} }
func getClientSessionCache() tls.ClientSessionCache {
once.Do(func() {
clientSessionCache = tls.NewLRUClientSessionCache(128)
})
return clientSessionCache
}

View File

@ -1,9 +1,11 @@
package vmess package vmess
import ( import (
"crypto/tls"
"fmt" "fmt"
"io" "io"
"net" "net"
"net/url"
"strings" "strings"
"time" "time"
@ -16,6 +18,13 @@ type websocketConn struct {
remoteAddr net.Addr remoteAddr net.Addr
} }
type websocketConfig struct {
host string
path string
tls bool
tlsConfig *tls.Config
}
// Read implements net.Conn.Read() // Read implements net.Conn.Read()
func (wsc *websocketConn) Read(b []byte) (int, error) { func (wsc *websocketConn) Read(b []byte) (int, error) {
for { for {
@ -91,9 +100,44 @@ func (wsc *websocketConn) SetWriteDeadline(t time.Time) error {
return wsc.conn.SetWriteDeadline(t) return wsc.conn.SetWriteDeadline(t)
} }
func newWebsocketConn(conn *websocket.Conn, remoteAddr net.Addr) net.Conn { func newWebsocketConn(conn net.Conn, c *websocketConfig) (net.Conn, error) {
return &websocketConn{ dialer := &websocket.Dialer{
conn: conn, NetDial: func(network, addr string) (net.Conn, error) {
remoteAddr: remoteAddr, return conn, nil
},
ReadBufferSize: 4 * 1024,
WriteBufferSize: 4 * 1024,
HandshakeTimeout: time.Second * 8,
} }
scheme := "ws"
if c.tls {
scheme = "wss"
dialer.TLSClientConfig = c.tlsConfig
}
host, port, err := net.SplitHostPort(c.host)
if (scheme == "ws" && port != "80") || (scheme == "wss" && port != "443") {
host = c.host
}
uri := url.URL{
Scheme: scheme,
Host: host,
Path: c.path,
}
wsConn, resp, err := dialer.Dial(uri.String(), nil)
if err != nil {
var reason string
if resp != nil {
reason = resp.Status
}
return nil, fmt.Errorf("Dial %s error: %s", host, reason)
}
return &websocketConn{
conn: wsConn,
remoteAddr: conn.RemoteAddr(),
}, nil
} }

View File

@ -3,94 +3,82 @@ package config
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net"
"net/url"
"os" "os"
"strings" "strings"
"sync"
"time"
adapters "github.com/Dreamacro/clash/adapters/outbound" adapters "github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/common/observable"
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/dns"
"github.com/Dreamacro/clash/log"
R "github.com/Dreamacro/clash/rules" R "github.com/Dreamacro/clash/rules"
T "github.com/Dreamacro/clash/tunnel"
log "github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v2"
) )
var (
config *Config
once sync.Once
)
// General config // General config
type General struct { type General struct {
Port int Port int `json:"port"`
SocksPort int SocksPort int `json:"socks-port"`
RedirPort int RedirPort int `json:"redir-port"`
AllowLan bool AllowLan bool `json:"allow-lan"`
Mode Mode Mode T.Mode `json:"mode"`
LogLevel C.LogLevel LogLevel log.LogLevel `json:"log-level"`
ExternalController string `json:"external-controller,omitempty"`
Secret string `json:"secret,omitempty"`
} }
// ProxyConfig is update proxy schema // DNS config
type ProxyConfig struct { type DNS struct {
Port *int Enable bool `yaml:"enable"`
SocksPort *int IPv6 bool `yaml:"ipv6"`
RedirPort *int NameServer []dns.NameServer `yaml:"nameserver"`
AllowLan *bool Fallback []dns.NameServer `yaml:"fallback"`
Listen string `yaml:"listen"`
EnhancedMode dns.EnhancedMode `yaml:"enhanced-mode"`
} }
// RawConfig is raw config struct // Config is clash config manager
type RawConfig struct { type Config struct {
Port int `yaml:"port"` General *General
SocksPort int `yaml:"socks-port"` DNS *DNS
RedirPort int `yaml:"redir-port"` Rules []C.Rule
AllowLan bool `yaml:"allow-lan"` Proxies map[string]C.Proxy
Mode string `yaml:"mode"` }
LogLevel string `yaml:"log-level"`
ExternalController string `yaml:"external-controller"`
Secret string `yaml:"secret"`
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"`
Secret string `yaml:"secret"`
DNS *rawDNS `yaml:"dns"`
Proxy []map[string]interface{} `yaml:"Proxy"` Proxy []map[string]interface{} `yaml:"Proxy"`
ProxyGroup []map[string]interface{} `yaml:"Proxy Group"` ProxyGroup []map[string]interface{} `yaml:"Proxy Group"`
Rule []string `yaml:"Rule"` Rule []string `yaml:"Rule"`
} }
// Config is clash config manager func readConfig(path string) (*rawConfig, error) {
type Config struct { if _, err := os.Stat(path); os.IsNotExist(err) {
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) {
return nil, err return nil, err
} }
data, err := ioutil.ReadFile(C.Path.Config()) data, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -100,144 +88,81 @@ func (c *Config) readConfig() (*RawConfig, error) {
} }
// config with some default value // config with some default value
rawConfig := &RawConfig{ rawConfig := &rawConfig{
AllowLan: false, AllowLan: false,
Mode: Rule.String(), Mode: T.Rule,
LogLevel: C.INFO.String(), LogLevel: log.INFO,
Rule: []string{}, Rule: []string{},
Proxy: []map[string]interface{}{}, Proxy: []map[string]interface{}{},
ProxyGroup: []map[string]interface{}{}, ProxyGroup: []map[string]interface{}{},
DNS: &rawDNS{
Enable: false,
},
} }
err = yaml.Unmarshal([]byte(data), &rawConfig) err = yaml.Unmarshal([]byte(data), &rawConfig)
return rawConfig, err return rawConfig, err
} }
// Parse config // Parse config
func (c *Config) Parse() error { func Parse(path string) (*Config, error) {
cfg, err := c.readConfig() config := &Config{}
rawCfg, err := readConfig(path)
if err != nil { if err != nil {
return err return nil, err
} }
if err := c.parseGeneral(cfg); err != nil { general, err := parseGeneral(rawCfg)
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()
if err != nil { 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 port := cfg.Port
socksPort := cfg.SocksPort socksPort := cfg.SocksPort
redirPort := cfg.RedirPort redirPort := cfg.RedirPort
allowLan := cfg.AllowLan allowLan := cfg.AllowLan
logLevelString := cfg.LogLevel externalController := cfg.ExternalController
modeString := cfg.Mode secret := cfg.Secret
mode := cfg.Mode
logLevel := cfg.LogLevel
mode, exist := ModeMapping[modeString] general := &General{
if !exist { Port: port,
return fmt.Errorf("General.mode value invalid") SocksPort: socksPort,
RedirPort: redirPort,
AllowLan: allowLan,
Mode: mode,
LogLevel: logLevel,
ExternalController: externalController,
Secret: secret,
} }
return general, nil
logLevel, exist := C.LogLevelMapping[logLevelString]
if !exist {
return fmt.Errorf("General.log-level value invalid")
}
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
} }
// UpdateGeneral dispatch update event func parseProxies(cfg *rawConfig) (map[string]C.Proxy, error) {
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 {
proxies := make(map[string]C.Proxy) proxies := make(map[string]C.Proxy)
proxiesConfig := cfg.Proxy proxiesConfig := cfg.Proxy
groupsConfig := cfg.ProxyGroup groupsConfig := cfg.ProxyGroup
@ -251,7 +176,7 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
for idx, mapping := range proxiesConfig { for idx, mapping := range proxiesConfig {
proxyType, existType := mapping["type"].(string) proxyType, existType := mapping["type"].(string)
if !existType { if !existType {
return fmt.Errorf("Proxy %d missing type", idx) return nil, fmt.Errorf("Proxy %d missing type", idx)
} }
var proxy C.Proxy var proxy C.Proxy
@ -271,6 +196,13 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
break break
} }
proxy = adapters.NewSocks5(*socksOption) proxy = adapters.NewSocks5(*socksOption)
case "http":
httpOption := &adapters.HttpOption{}
err = decoder.Decode(mapping, httpOption)
if err != nil {
break
}
proxy = adapters.NewHttp(*httpOption)
case "vmess": case "vmess":
vmessOption := &adapters.VmessOption{} vmessOption := &adapters.VmessOption{}
err = decoder.Decode(mapping, vmessOption) err = decoder.Decode(mapping, vmessOption)
@ -279,15 +211,15 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
} }
proxy, err = adapters.NewVmess(*vmessOption) proxy, err = adapters.NewVmess(*vmessOption)
default: default:
return fmt.Errorf("Unsupport proxy type: %s", proxyType) return nil, fmt.Errorf("Unsupport proxy type: %s", proxyType)
} }
if err != nil { 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 { if _, exist := proxies[proxy.Name()]; exist {
return fmt.Errorf("Proxy %s is the duplicate name", proxy.Name()) return nil, fmt.Errorf("Proxy %s is the duplicate name", proxy.Name())
} }
proxies[proxy.Name()] = proxy proxies[proxy.Name()] = proxy
} }
@ -297,11 +229,11 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
groupType, existType := mapping["type"].(string) groupType, existType := mapping["type"].(string)
groupName, existName := mapping["name"].(string) groupName, existName := mapping["name"].(string)
if !existType && existName { 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 { 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 group C.Proxy
var err error var err error
@ -315,7 +247,7 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
ps, err := getProxies(proxies, urlTestOption.Proxies) ps, err := getProxies(proxies, urlTestOption.Proxies)
if err != nil { 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) group, err = adapters.NewURLTest(*urlTestOption, ps)
case "select": case "select":
@ -327,7 +259,7 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
ps, err := getProxies(proxies, selectorOption.Proxies) ps, err := getProxies(proxies, selectorOption.Proxies)
if err != nil { 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) group, err = adapters.NewSelector(selectorOption.Name, ps)
case "fallback": case "fallback":
@ -339,12 +271,12 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
ps, err := getProxies(proxies, fallbackOption.Proxies) ps, err := getProxies(proxies, fallbackOption.Proxies)
if err != nil { 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) group, err = adapters.NewFallback(*fallbackOption, ps)
} }
if err != nil { if err != nil {
return fmt.Errorf("Proxy %s: %s", groupName, err.Error()) return nil, fmt.Errorf("Proxy %s: %s", groupName, err.Error())
} }
proxies[groupName] = group proxies[groupName] = group
} }
@ -355,97 +287,124 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
} }
proxies["GLOBAL"], _ = adapters.NewSelector("GLOBAL", ps) proxies["GLOBAL"], _ = adapters.NewSelector("GLOBAL", ps)
return proxies, nil
// 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
} }
func (c *Config) parseRules(cfg *RawConfig) error { func parseRules(cfg *rawConfig) ([]C.Rule, error) {
rules := []C.Rule{} rules := []C.Rule{}
rulesConfig := cfg.Rule rulesConfig := cfg.Rule
// parse rules // parse rules
for _, line := range rulesConfig { for idx, line := range rulesConfig {
rule := strings.Split(line, ",") rule := trimArr(strings.Split(line, ","))
if len(rule) < 3 { var (
continue 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] error: format invalid", idx)
} }
rule = trimArr(rule) rule = trimArr(rule)
switch rule[0] { switch rule[0] {
case "DOMAIN": case "DOMAIN":
rules = append(rules, R.NewDomain(rule[1], rule[2])) rules = append(rules, R.NewDomain(payload, target))
case "DOMAIN-SUFFIX": case "DOMAIN-SUFFIX":
rules = append(rules, R.NewDomainSuffix(rule[1], rule[2])) rules = append(rules, R.NewDomainSuffix(payload, target))
case "DOMAIN-KEYWORD": case "DOMAIN-KEYWORD":
rules = append(rules, R.NewDomainKeyword(rule[1], rule[2])) rules = append(rules, R.NewDomainKeyword(payload, target))
case "GEOIP": case "GEOIP":
rules = append(rules, R.NewGEOIP(rule[1], rule[2])) rules = append(rules, R.NewGEOIP(payload, target))
case "IP-CIDR", "IP-CIDR6": case "IP-CIDR", "IP-CIDR6":
rules = append(rules, R.NewIPCIDR(rule[1], rule[2])) rules = append(rules, R.NewIPCIDR(payload, target))
case "MATCH":
fallthrough
case "FINAL": case "FINAL":
rules = append(rules, R.NewFinal(rule[2])) rules = append(rules, R.NewFinal(target))
} }
} }
c.rules = rules return rules, nil
c.event <- &Event{Type: "rules", Payload: rules}
return nil
} }
func (c *Config) handleResponseMessage() { func hostWithDefaultPort(host string, defPort string) (string, error) {
for elm := range c.reportCh { if !strings.Contains(host, ":") {
event := elm.(*Event) host += ":"
switch event.Type { }
case "http-addr":
if event.Payload.(bool) == false { hostname, port, err := net.SplitHostPort(host)
log.Errorf("Listening HTTP proxy at %d error", c.general.Port) if err != nil {
c.general.Port = 0 return "", err
} }
case "socks-addr":
if event.Payload.(bool) == false { if port == "" {
log.Errorf("Listening SOCKS proxy at %d error", c.general.SocksPort) port = defPort
c.general.SocksPort = 0 }
}
case "redir-addr": return net.JoinHostPort(hostname, port), nil
if event.Payload.(bool) == false { }
log.Errorf("Listening Redir proxy at %d error", c.general.RedirPort)
c.general.RedirPort = 0 func parseNameServer(servers []string) ([]dns.NameServer, error) {
} nameservers := []dns.NameServer{}
log.Debugln("%#v", servers)
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 { func parseDNS(cfg *rawDNS) (*DNS, error) {
event := make(chan interface{}) if cfg.Enable && len(cfg.NameServer) == 0 {
reportCh := make(chan interface{}) return nil, fmt.Errorf("If DNS configuration is turned on, NameServer cannot be empty")
config := &Config{
general: &General{},
proxies: make(map[string]C.Proxy),
rules: []C.Rule{},
lastUpdate: time.Now(),
event: event,
reportCh: reportCh,
observable: observable.NewObservable(event),
} }
go config.handleResponseMessage()
return config
}
// Instance return singleton instance of Config dnsCfg := &DNS{
func Instance() *Config { Enable: cfg.Enable,
once.Do(func() { Listen: cfg.Listen,
config = newConfig() EnhancedMode: cfg.EnhancedMode,
}) }
return config
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 ( import (
"archive/tar" "archive/tar"
"compress/gzip" "compress/gzip"
"fmt"
"io" "io"
"net/http" "net/http"
"os" "os"
@ -54,15 +55,15 @@ func downloadMMDB(path string) (err error) {
} }
// Init prepare necessary files // Init prepare necessary files
func Init() { func Init(dir string) error {
// initial homedir // initial homedir
if _, err := os.Stat(C.Path.HomeDir()); os.IsNotExist(err) { if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(C.Path.HomeDir(), 0777); err != nil { if err := os.MkdirAll(dir, 0777); err != nil {
log.Fatalf("Can't create config directory %s: %s", C.Path.HomeDir(), err.Error()) 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) { if _, err := os.Stat(C.Path.Config()); os.IsNotExist(err) {
log.Info("Can't find config, create a empty file") log.Info("Can't find config, create a empty file")
os.OpenFile(C.Path.Config(), os.O_CREATE|os.O_WRONLY, 0644) 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") log.Info("Can't find MMDB, start download")
err := downloadMMDB(C.Path.MMDB()) err := downloadMMDB(C.Path.MMDB())
if err != nil { 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 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) { func getProxies(mapping map[string]C.Proxy, list []string) ([]C.Proxy, error) {
var ps []C.Proxy var ps []C.Proxy
for _, name := range list { for _, name := range list {

View File

@ -12,6 +12,7 @@ const (
Selector Selector
Shadowsocks Shadowsocks
Socks5 Socks5
Http
URLTest URLTest
Vmess Vmess
) )
@ -30,6 +31,7 @@ type Proxy interface {
Name() string Name() string
Type() AdapterType Type() AdapterType
Generator(metadata *Metadata) (ProxyAdapter, error) Generator(metadata *Metadata) (ProxyAdapter, error)
MarshalJSON() ([]byte, error)
} }
// AdapterType is enum of adapter type // AdapterType is enum of adapter type
@ -49,6 +51,8 @@ func (at AdapterType) String() string {
return "Shadowsocks" return "Shadowsocks"
case Socks5: case Socks5:
return "Socks5" return "Socks5"
case Http:
return "Http"
case URLTest: case URLTest:
return "URLTest" return "URLTest"
case Vmess: case Vmess:

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 HTTP SourceType = iota
SOCKS SOCKS
REDIR
) )
type NetWork int type NetWork int

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

265
dns/client.go Normal file
View File

@ -0,0 +1,265 @@
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 := r.cache.Get(q.String())
if cache != nil {
return cache.(*D.Msg).Copy(), nil
}
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, _ := r.msgToIP(res.Msg)
if record, _ := mmdb.Country(ips[0]); record.Country.IsoCode == "CN" || record.Country.IsoCode == "" {
// release channel
go func() { <-fallbackMsg }()
msg = res.Msg
return
}
}
res = <-fallbackMsg
msg, err = res.Msg, res.Error
return
}
func (r *Resolver) ResolveIP(host string) (ip net.IP, err error) {
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 {
if r.ipv6 {
ans, ok := answer.(*D.AAAA)
if !ok {
continue
}
ips = append(ips, ans.AAAA)
continue
}
ans, ok := answer.(*D.A)
if !ok {
continue
}
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
}
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())
})
println(config.EnhancedMode)
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)
}

11
go.mod
View File

@ -1,17 +1,20 @@
module github.com/Dreamacro/clash module github.com/Dreamacro/clash
require ( require (
github.com/Dreamacro/go-shadowsocks2 v0.1.2-0.20181019110427-0a03f1a25270 github.com/Dreamacro/go-shadowsocks2 v0.1.2
github.com/eapache/queue v1.1.0 // indirect github.com/eapache/queue v1.1.0 // indirect
github.com/go-chi/chi v3.3.3+incompatible github.com/go-chi/chi v3.3.3+incompatible
github.com/go-chi/cors v1.0.0 github.com/go-chi/cors v1.0.0
github.com/go-chi/render v1.0.1 github.com/go-chi/render v1.0.1
github.com/gofrs/uuid v3.1.0+incompatible github.com/gofrs/uuid v3.1.0+incompatible
github.com/gorilla/websocket v1.4.0 github.com/gorilla/websocket v1.4.0
github.com/miekg/dns v1.1.0
github.com/oschwald/geoip2-golang v1.2.1 github.com/oschwald/geoip2-golang v1.2.1
github.com/oschwald/maxminddb-golang v1.3.0 // indirect github.com/oschwald/maxminddb-golang v1.3.0 // indirect
github.com/sirupsen/logrus v1.1.0 github.com/sirupsen/logrus v1.2.0
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941 golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85
golang.org/x/net v0.0.0-20181108082009-03003ca0c849 // indirect
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
gopkg.in/eapache/channels.v1 v1.1.0 gopkg.in/eapache/channels.v1 v1.1.0
gopkg.in/yaml.v2 v2.2.1 gopkg.in/yaml.v2 v2.2.2
) )

28
go.sum
View File

@ -1,5 +1,5 @@
github.com/Dreamacro/go-shadowsocks2 v0.1.2-0.20181019110427-0a03f1a25270 h1:ugkI+Yw5ArFnhF8KTbJxyWIyvxMCa8jWyUF+wIAulhM= github.com/Dreamacro/go-shadowsocks2 v0.1.2 h1:8KgWbwAw5PJF+i6F3tI2iW/Em9WDtAuDG4obot8bGLM=
github.com/Dreamacro/go-shadowsocks2 v0.1.2-0.20181019110427-0a03f1a25270/go.mod h1:DlkXRxmh5K+99aTPQaVjsZ1fAZNFw42vXGcOjR3Otps= github.com/Dreamacro/go-shadowsocks2 v0.1.2/go.mod h1:J5YbNUiKtaD7EJmQ4O9ruUTY9+IgrflPgm63K1nUE0I=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= 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/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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -16,22 +16,28 @@ github.com/gofrs/uuid v3.1.0+incompatible h1:q2rtkjaKT4YEr6E1kamy0Ha4RtepWlQBedy
github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 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 v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
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/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/miekg/dns v1.1.0 h1:yv9O9RJbvVFkvW8PKYqp4x7HQkc5RWwmUY/L8MdUaIg=
github.com/miekg/dns v1.1.0/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 h1:3iz+jmeJc6fuCyWeKgtXSXu7+zvkxJbHFXkMT5FVebU=
github.com/oschwald/geoip2-golang v1.2.1/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE= 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 h1:oTh8IBSj10S5JNlUDg5WjJ1QdBMdeaZIkPEVfESSWgE=
github.com/oschwald/maxminddb-golang v1.3.0/go.mod h1:3jhIUymTJ5VREKyIhWm66LJiQt04F0UCDdodShpjWsY= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.1.0 h1:65VZabgUiV9ktjGM5nTq0+YurgTyX+YI2lSSfDjI+qU= github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.1.0/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A= github.com/sirupsen/logrus v1.2.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 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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-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-20181127143415-eb0de9b17e85 h1:et7+NAX3lLIk5qUCTA9QelBjGE/NkhzYw/mhnr0s7nI=
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= 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-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 h1:EfdBzeKbFSvOjoIqSZcfS8wp0FBLokGBEs9lz1OtSg0=
golang.org/x/sys v0.0.0-20181005133103-4497e2df6f9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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/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 h1:5bGAyKKvyCTWjSj7mhefG6Lc68VyN4MH1v8/7OoeeB4=
gopkg.in/eapache/channels.v1 v1.1.0/go.mod h1:BHIBujSvu9yMTrTYbTCjDD43gUhtmaOtTWDe7sTv1js= 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.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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)
}

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

@ -0,0 +1,101 @@
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)
updateGeneral(cfg.General)
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)
dns.ReCreateServer(c.Listen, r)
}
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())
}
}

21
hub/hub.go Normal file
View File

@ -0,0 +1,21 @@
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.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:"socket-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.Respond(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 {
w.WriteHeader(http.StatusBadRequest)
render.Respond(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)
}
w.WriteHeader(http.StatusNoContent)
}
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 {
w.WriteHeader(http.StatusBadRequest)
render.Respond(w, r, ErrBadRequest)
return
}
if !filepath.IsAbs(req.Path) {
w.WriteHeader(http.StatusBadRequest)
render.Respond(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 {
w.WriteHeader(http.StatusBadRequest)
render.Respond(w, r, newError(err.Error()))
return
}
executor.ApplyConfig(cfg, force)
w.WriteHeader(http.StatusNoContent)
}

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 {
w.WriteHeader(http.StatusNotFound)
render.Respond(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.Respond(w, r, map[string]map[string]C.Proxy{
"proxies": proxies,
})
}
func getProxy(w http.ResponseWriter, r *http.Request) {
proxy := r.Context().Value(CtxKeyProxy).(C.Proxy)
render.Respond(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 {
w.WriteHeader(http.StatusBadRequest)
render.Respond(w, r, ErrBadRequest)
return
}
proxy := r.Context().Value(CtxKeyProxy).(C.Proxy)
selector, ok := proxy.(*A.Selector)
if !ok {
w.WriteHeader(http.StatusBadRequest)
render.Respond(w, r, ErrBadRequest)
return
}
if err := selector.Set(req.Name); err != nil {
w.WriteHeader(http.StatusBadRequest)
render.Respond(w, r, newError(fmt.Sprintf("Selector update error: %s", err.Error())))
return
}
w.WriteHeader(http.StatusNoContent)
}
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.Respond(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)):
w.WriteHeader(http.StatusRequestTimeout)
render.Respond(w, r, ErrRequestTimeout)
case t := <-sigCh:
if t == 0 {
w.WriteHeader(http.StatusServiceUnavailable)
render.Respond(w, r, newError("An error occurred in the delay test"))
} else {
render.Respond(w, r, map[string]int16{
"delay": t,
})
}
}
}

View File

@ -1,8 +1,10 @@
package hub package route
import ( import (
"net/http" "net/http"
T "github.com/Dreamacro/clash/tunnel"
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/go-chi/render" "github.com/go-chi/render"
) )
@ -10,7 +12,6 @@ import (
func ruleRouter() http.Handler { func ruleRouter() http.Handler {
r := chi.NewRouter() r := chi.NewRouter()
r.Get("/", getRules) r.Get("/", getRules)
r.Put("/", updateRules)
return r return r
} }
@ -20,12 +21,8 @@ type Rule struct {
Proxy string `json:"proxy"` Proxy string `json:"proxy"`
} }
type GetRulesResponse struct {
Rules []Rule `json:"rules"`
}
func getRules(w http.ResponseWriter, r *http.Request) { func getRules(w http.ResponseWriter, r *http.Request) {
rawRules := cfg.Rules() rawRules := T.Instance().Rules()
var rules []Rule var rules []Rule
for _, rule := range rawRules { for _, rule := range rawRules {
@ -37,19 +34,7 @@ func getRules(w http.ResponseWriter, r *http.Request) {
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
render.JSON(w, r, GetRulesResponse{ render.Respond(w, r, map[string][]Rule{
Rules: rules, "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 ( import (
"encoding/json" "encoding/json"
@ -6,44 +6,32 @@ import (
"strings" "strings"
"time" "time"
"github.com/Dreamacro/clash/config" "github.com/Dreamacro/clash/log"
C "github.com/Dreamacro/clash/constant"
T "github.com/Dreamacro/clash/tunnel" T "github.com/Dreamacro/clash/tunnel"
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/go-chi/cors" "github.com/go-chi/cors"
"github.com/go-chi/render" "github.com/go-chi/render"
log "github.com/sirupsen/logrus"
) )
var secret = "" var (
serverSecret = ""
serverAddr = ""
)
type Traffic struct { type Traffic struct {
Up int64 `json:"up"` Up int64 `json:"up"`
Down int64 `json:"down"` Down int64 `json:"down"`
} }
func newHub(signal chan struct{}) { func Start(addr string, secret string) {
var addr string if serverAddr != "" {
ch := config.Instance().Subscribe() return
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
}
} }
serverAddr = addr
serverSecret = secret
r := chi.NewRouter() r := chi.NewRouter()
cors := cors.New(cors.Options{ cors := cors.New(cors.Options{
@ -61,10 +49,10 @@ func newHub(signal chan struct{}) {
r.Mount("/proxies", proxyRouter()) r.Mount("/proxies", proxyRouter())
r.Mount("/rules", ruleRouter()) r.Mount("/rules", ruleRouter())
log.Infof("RESTful API listening at: %s", addr) log.Infoln("RESTful API listening at: %s", addr)
err := http.ListenAndServe(addr, r) err := http.ListenAndServe(addr, r)
if err != nil { if err != nil {
log.Errorf("External controller error: %s", err.Error()) log.Errorln("External controller error: %s", err.Error())
} }
} }
@ -81,18 +69,16 @@ func authentication(next http.Handler) http.Handler {
header := r.Header.Get("Authorization") header := r.Header.Get("Authorization")
text := strings.SplitN(header, " ", 2) text := strings.SplitN(header, " ", 2)
if secret == "" { if serverSecret == "" {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
hasUnvalidHeader := text[0] != "Bearer" hasUnvalidHeader := text[0] != "Bearer"
hasUnvalidSecret := len(text) == 2 && text[1] != secret hasUnvalidSecret := len(text) == 2 && text[1] != serverSecret
if hasUnvalidHeader || hasUnvalidSecret { if hasUnvalidHeader || hasUnvalidSecret {
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
render.JSON(w, r, Error{ render.Respond(w, r, ErrUnauthorized)
Error: "Authentication failed",
})
return return
} }
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
@ -100,17 +86,11 @@ func authentication(next http.Handler) http.Handler {
return http.HandlerFunc(fn) return http.HandlerFunc(fn)
} }
type contextKey string
func (c contextKey) String() string {
return "clash context key " + string(c)
}
func traffic(w http.ResponseWriter, r *http.Request) { func traffic(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
tick := time.NewTicker(time.Second) tick := time.NewTicker(time.Second)
t := tunnel.Traffic() t := T.Instance().Traffic()
for range tick.C { for range tick.C {
up, down := t.Now() up, down := t.Now()
if err := json.NewEncoder(w).Encode(Traffic{ if err := json.NewEncoder(w).Encode(Traffic{
@ -134,29 +114,18 @@ func getLogs(w http.ResponseWriter, r *http.Request) {
levelText = "info" levelText = "info"
} }
level, ok := C.LogLevelMapping[levelText] level, ok := log.LogLevelMapping[levelText]
if !ok { if !ok {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{ render.Respond(w, r, ErrBadRequest)
Error: "Level error",
})
return return
} }
src := tunnel.Log() sub := log.Subscribe()
sub, err := src.Subscribe()
defer src.UnSubscribe(sub)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
render.JSON(w, r, Error{
Error: err.Error(),
})
return
}
render.Status(r, http.StatusOK) render.Status(r, http.StatusOK)
for elm := range sub { for elm := range sub {
log := elm.(T.Log) log := elm.(*log.Event)
if log.LogLevel > level { if log.LogLevel < level {
continue continue
} }
@ -169,10 +138,3 @@ func getLogs(w http.ResponseWriter, r *http.Request) {
w.(http.Flusher).Flush() 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 package main
import ( import (
"flag"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"syscall" "syscall"
"flag"
"path"
"github.com/Dreamacro/clash/config" "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" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/hub"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -26,21 +24,19 @@ func init() {
} }
func main() { func main() {
tunnel.Instance().Run() if homedir != "" {
proxy.Instance().Run() if !filepath.IsAbs(homedir) {
hub.Run()
if (homedir != "") {
if !path.IsAbs(homedir) {
currentDir, _ := os.Getwd() currentDir, _ := os.Getwd()
homedir = path.Join(currentDir, homedir) homedir = filepath.Join(currentDir, homedir)
} }
C.SetHomeDir(homedir) C.SetHomeDir(homedir)
} }
config.Init() if err := config.Init(C.Path.HomeDir()); err != nil {
err := config.Instance().Parse() log.Fatalf("Initial configuration directory error: %s", err.Error())
if err != nil { }
if err := hub.Parse(); err != nil {
log.Fatalf("Parse config error: %s", err.Error()) log.Fatalf("Parse config error: %s", err.Error())
} }

View File

@ -6,35 +6,33 @@ import (
"net/http" "net/http"
"github.com/Dreamacro/clash/adapters/inbound" "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/clash/tunnel"
log "github.com/sirupsen/logrus"
) )
var ( var (
tun = tunnel.Instance() 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) l, err := net.Listen("tcp", addr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
hl := &HttpListener{l, addr, false}
done := make(chan struct{})
closed := make(chan struct{})
signal := &C.ProxySignal{
Done: done,
Closed: closed,
}
go func() { go func() {
log.Infof("HTTP proxy listening at: %s", addr) log.Infoln("HTTP proxy listening at: %s", addr)
for { for {
c, err := l.Accept() c, err := hl.Accept()
if err != nil { if err != nil {
if _, open := <-done; !open { if hl.closed {
break break
} }
continue continue
@ -43,14 +41,16 @@ func NewHttpProxy(addr string) (*C.ProxySignal, error) {
} }
}() }()
go func() { return hl, nil
<-done }
close(done)
l.Close()
closed <- struct{}{}
}()
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) { func handleConn(conn net.Conn) {

View File

@ -1,116 +1,150 @@
package proxy package proxy
import ( 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/http"
"github.com/Dreamacro/clash/proxy/redir" "github.com/Dreamacro/clash/proxy/redir"
"github.com/Dreamacro/clash/proxy/socks" "github.com/Dreamacro/clash/proxy/socks"
) )
var ( var (
listener *Listener allowLan = false
once sync.Once
socksListener *socks.SockListener
httpListener *http.HttpListener
redirListener *redir.RedirListener
) )
type Listener struct { type listener interface {
// signal for update Close()
httpSignal *C.ProxySignal Address() string
socksSignal *C.ProxySignal
redirSignal *C.ProxySignal
} }
func (l *Listener) updateHTTP(addr string) error { type Ports struct {
if l.httpSignal != nil { Port int `json:"port"`
signal := l.httpSignal SocksPort int `json:"socks-port"`
signal.Done <- struct{}{} RedirPort int `json:"redir-port"`
<-signal.Closed
l.httpSignal = nil
}
signal, err := http.NewHttpProxy(addr)
if err != nil {
return err
}
l.httpSignal = signal
return nil
} }
func (l *Listener) updateSocks(addr string) error { func AllowLan() bool {
if l.socksSignal != nil { return allowLan
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 (l *Listener) updateRedir(addr string) error { func SetAllowLan(al bool) {
if l.redirSignal != nil { allowLan = al
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 (l *Listener) process(signal chan<- struct{}) { func ReCreateHTTP(port int) error {
sub := config.Instance().Subscribe() addr := genAddr(port, allowLan)
signal <- struct{}{}
reportCH := config.Instance().Report() if httpListener != nil {
for elm := range sub { if httpListener.Address() == addr {
event := elm.(*config.Event) return nil
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}
} }
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 ReCreateSocks(port int) error {
func (l *Listener) Run() { addr := genAddr(port, allowLan)
signal := make(chan struct{})
go l.process(signal) if socksListener != nil {
<-signal 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 { func ReCreateRedir(port int) error {
return &Listener{} 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 // GetPorts return the ports of proxy servers
func Instance() *Listener { func GetPorts() *Ports {
once.Do(func() { ports := &Ports{}
listener = newListener()
}) if httpListener != nil {
return listener _, 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" "github.com/Dreamacro/clash/adapters/inbound"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel" "github.com/Dreamacro/clash/tunnel"
log "github.com/sirupsen/logrus"
) )
var ( var (
tun = tunnel.Instance() 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) l, err := net.Listen("tcp", addr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
rl := &RedirListener{l, addr, false}
done := make(chan struct{})
closed := make(chan struct{})
signal := &C.ProxySignal{
Done: done,
Closed: closed,
}
go func() { go func() {
log.Infof("Redir proxy listening at: %s", addr) log.Infoln("Redir proxy listening at: %s", addr)
for { for {
c, err := l.Accept() c, err := l.Accept()
if err != nil { if err != nil {
if _, open := <-done; !open { if rl.closed {
break break
} }
continue continue
@ -41,14 +40,16 @@ func NewRedirProxy(addr string) (*C.ProxySignal, error) {
} }
}() }()
go func() { return rl, nil
<-done }
close(done)
l.Close()
closed <- struct{}{}
}()
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) { func handleRedir(conn net.Conn) {
@ -58,5 +59,5 @@ func handleRedir(conn net.Conn) {
return return
} }
conn.(*net.TCPConn).SetKeepAlive(true) conn.(*net.TCPConn).SetKeepAlive(true)
tun.Add(adapters.NewSocket(target, conn)) tun.Add(adapters.NewSocket(target, conn, C.REDIR))
} }

View File

@ -5,35 +5,35 @@ import (
"github.com/Dreamacro/clash/adapters/inbound" "github.com/Dreamacro/clash/adapters/inbound"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel" "github.com/Dreamacro/clash/tunnel"
"github.com/Dreamacro/go-shadowsocks2/socks" "github.com/Dreamacro/go-shadowsocks2/socks"
log "github.com/sirupsen/logrus"
) )
var ( var (
tun = tunnel.Instance() 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) l, err := net.Listen("tcp", addr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
done := make(chan struct{}) sl := &SockListener{l, addr, false}
closed := make(chan struct{})
signal := &C.ProxySignal{
Done: done,
Closed: closed,
}
go func() { go func() {
log.Infof("SOCKS proxy listening at: %s", addr) log.Infoln("SOCKS proxy listening at: %s", addr)
for { for {
c, err := l.Accept() c, err := l.Accept()
if err != nil { if err != nil {
if _, open := <-done; !open { if sl.closed {
break break
} }
continue continue
@ -42,14 +42,16 @@ func NewSocksProxy(addr string) (*C.ProxySignal, error) {
} }
}() }()
go func() { return sl, nil
<-done }
close(done)
l.Close()
closed <- struct{}{}
}()
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) { func handleSocks(conn net.Conn) {
@ -59,5 +61,5 @@ func handleSocks(conn net.Conn) {
return return
} }
conn.(*net.TCPConn).SetKeepAlive(true) 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 package rules
import ( import (
"strings"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
@ -30,7 +32,7 @@ func (d *Domain) Payload() string {
func NewDomain(domain string, adapter string) *Domain { func NewDomain(domain string, adapter string) *Domain {
return &Domain{ return &Domain{
domain: domain, domain: strings.ToLower(domain),
adapter: adapter, adapter: adapter,
} }
} }

View File

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

View File

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

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,13 +1,14 @@
package tunnel package tunnel
import ( import (
"net"
"sync" "sync"
"time" "time"
InboundAdapter "github.com/Dreamacro/clash/adapters/inbound" 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" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/dns"
"github.com/Dreamacro/clash/log"
"gopkg.in/eapache/channels.v1" "gopkg.in/eapache/channels.v1"
) )
@ -24,14 +25,10 @@ type Tunnel struct {
proxies map[string]C.Proxy proxies map[string]C.Proxy
configLock *sync.RWMutex configLock *sync.RWMutex
traffic *C.Traffic traffic *C.Traffic
resolver *dns.Resolver
// Outbound Rule // Outbound Rule
mode cfg.Mode mode Mode
// Log
logCh chan interface{}
observable *observable.Observable
logLevel C.LogLevel
} }
// Add request to queue // Add request to queue
@ -44,33 +41,43 @@ func (t *Tunnel) Traffic() *C.Traffic {
return t.traffic return t.traffic
} }
// Log return clash log stream // Rules return all rules
func (t *Tunnel) Log() *observable.Observable { func (t *Tunnel) Rules() []C.Rule {
return t.observable return t.rules
} }
func (t *Tunnel) configMonitor(signal chan<- struct{}) { // UpdateRules handle update rules
sub := cfg.Instance().Subscribe() func (t *Tunnel) UpdateRules(rules []C.Rule) {
signal <- struct{}{} t.configLock.Lock()
for elm := range sub { t.rules = rules
event := elm.(*cfg.Event) t.configLock.Unlock()
switch event.Type { }
case "proxies":
proxies := event.Payload.(map[string]C.Proxy) // Proxies return all proxies
t.configLock.Lock() func (t *Tunnel) Proxies() map[string]C.Proxy {
t.proxies = proxies return t.proxies
t.configLock.Unlock() }
case "rules":
rules := event.Payload.([]C.Rule) // UpdateProxies handle update proxies
t.configLock.Lock() func (t *Tunnel) UpdateProxies(proxies map[string]C.Proxy) {
t.rules = rules t.configLock.Lock()
t.configLock.Unlock() t.proxies = proxies
case "mode": t.configLock.Unlock()
t.mode = event.Payload.(cfg.Mode) }
case "log-level":
t.logLevel = event.Payload.(C.LogLevel) // 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) process() { func (t *Tunnel) process() {
@ -82,15 +89,46 @@ 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) handleConn(localConn C.ServerAdapter) { func (t *Tunnel) handleConn(localConn C.ServerAdapter) {
defer localConn.Close() defer localConn.Close()
metadata := localConn.Metadata() metadata := localConn.Metadata()
if metadata.Source == C.REDIR && t.resolver != nil {
host, exist := t.resolver.IPToHost(*metadata.IP)
if exist {
metadata.Host = host
metadata.AddrType = C.AtypDomainName
}
} else if metadata.IP == nil && metadata.AddrType == C.AtypDomainName {
ip, err := t.resolveIP(metadata.Host)
if err != nil {
log.Debugln("[DNS] resolve %s error: %s", metadata.Host, err.Error())
} else {
log.Debugln("[DNS] %s --> %s", metadata.Host, ip.String())
metadata.IP = &ip
}
} else {
log.Debugln("[DNS] unknown%#v", metadata)
}
var proxy C.Proxy var proxy C.Proxy
switch t.mode { switch t.mode {
case cfg.Direct: case Direct:
proxy = t.proxies["DIRECT"] proxy = t.proxies["DIRECT"]
case cfg.Global: case Global:
proxy = t.proxies["GLOBAL"] proxy = t.proxies["GLOBAL"]
// Rule // Rule
default: default:
@ -98,7 +136,7 @@ func (t *Tunnel) handleConn(localConn C.ServerAdapter) {
} }
remoConn, err := proxy.Generator(metadata) remoConn, err := proxy.Generator(metadata)
if err != nil { if err != nil {
t.logCh <- newLog(C.WARNING, "Proxy connect error: %s", err.Error()) log.Warnln("Proxy[%s] connect [%s] error: %s", proxy.Name(), metadata.String(), err.Error())
return return
} }
defer remoConn.Close() defer remoConn.Close()
@ -121,34 +159,21 @@ func (t *Tunnel) match(metadata *C.Metadata) C.Proxy {
if !ok { if !ok {
continue continue
} }
t.logCh <- newLog(C.INFO, "%v match %s using %s", metadata.String(), rule.RuleType().String(), rule.Adapter()) log.Infoln("%v match %s using %s", metadata.String(), rule.RuleType().String(), rule.Adapter())
return a return a
} }
} }
t.logCh <- newLog(C.INFO, "%v doesn't match any rule using DIRECT", metadata.String()) log.Infoln("%v doesn't match any rule using DIRECT", metadata.String())
return t.proxies["DIRECT"] 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
}
func newTunnel() *Tunnel { func newTunnel() *Tunnel {
logCh := make(chan interface{})
return &Tunnel{ return &Tunnel{
queue: channels.NewInfiniteChannel(), queue: channels.NewInfiniteChannel(),
proxies: make(map[string]C.Proxy), proxies: make(map[string]C.Proxy),
observable: observable.NewObservable(logCh),
logCh: logCh,
configLock: &sync.RWMutex{}, configLock: &sync.RWMutex{},
traffic: C.NewTraffic(time.Second), traffic: C.NewTraffic(time.Second),
mode: cfg.Rule, mode: Rule,
logLevel: C.INFO,
} }
} }
@ -156,6 +181,7 @@ func newTunnel() *Tunnel {
func Instance() *Tunnel { func Instance() *Tunnel {
once.Do(func() { once.Do(func() {
tunnel = newTunnel() tunnel = newTunnel()
go tunnel.process()
}) })
return tunnel return tunnel
} }