Compare commits

..

20 Commits

Author SHA1 Message Date
ebe1cee6dc Improve: clean code 2018-08-12 16:18:58 +08:00
fc4f119049 Fix: Makefile 2018-08-12 14:06:50 +08:00
2b87b907ae New: custom socks5 proxy support 2018-08-12 13:50:54 +08:00
35e572406b Fix: firefox one socket to process multiple domains 2018-08-12 12:14:59 +08:00
228d674d93 Fix: import package 2018-08-12 11:06:17 +08:00
752944d381 Update: README.md 2018-08-12 04:22:14 +08:00
63308472ad New: redir proxy 2018-08-12 04:00:34 +08:00
0208e32933 Fix: update proxy config api 2018-08-12 02:23:46 +08:00
410b272b50 Fix: issue #3 2018-08-11 22:51:30 +08:00
ea424a7694 Add: delay test api for proxies 2018-08-08 11:51:06 +08:00
36f4ceafa1 Fix: returns the sorted proxy name 2018-08-07 14:45:24 +08:00
674f4be24d Fix: add JSON Content-Type for stream api to avoid buffer in swift 2018-08-07 14:45:16 +08:00
63c967b2b3 Fixed: global mode and update log level 2018-08-04 23:04:16 +08:00
a1a58c31ae Improve: lazy load mmdb 2018-08-01 00:18:29 +08:00
295d649624 Update: Initialize the config file outside of the init function 2018-07-31 17:54:16 +08:00
7347c28f75 Fix: io copy REJECT error 2018-07-31 17:53:39 +08:00
8389150318 Improve: config convergent and add log-level 2018-07-26 00:04:59 +08:00
7357d2d0c2 Add: cors for external controller 2018-07-19 09:31:53 +08:00
d540a0d29f Fix: typo 2018-07-18 21:50:16 +08:00
d55e1b664b Lint: simplify code 2018-07-16 00:47:03 +08:00
47 changed files with 1792 additions and 938 deletions

16
Gopkg.lock generated
View File

@ -19,6 +19,12 @@
revision = "e83ac2304db3c50cf03d96a2fcd39009d458bc35"
version = "v3.3.2"
[[projects]]
name = "github.com/go-chi/cors"
packages = ["."]
revision = "dba6525398619dead495962a916728e7ee2ca322"
version = "v1.0.0"
[[projects]]
name = "github.com/go-chi/render"
packages = ["."]
@ -65,7 +71,7 @@
"poly1305",
"ssh/terminal"
]
revision = "027cca12c2d63e3d62b670d901e8a2c95854feec"
revision = "a2144134853fc9a27a7b1e3eb4f19f1a76df13c9"
[[projects]]
branch = "master"
@ -75,7 +81,7 @@
"unix",
"windows"
]
revision = "6c888cc515d3ed83fc103cf1d84468aad274b0a7"
revision = "ac767d655b305d4e9612f5f6e33120b9176c4ad4"
[[projects]]
name = "gopkg.in/eapache/channels.v1"
@ -86,12 +92,12 @@
[[projects]]
name = "gopkg.in/ini.v1"
packages = ["."]
revision = "06f5f3d67269ccec1fe5fe4134ba6e982984f7f5"
version = "v1.37.0"
revision = "358ee7663966325963d4e8b2e1fbd570c5195153"
version = "v1.38.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "c3c901e4e393a2df9e421924d3a4d85ec73642e36dcbc1ddca5fc13159220e86"
inputs-digest = "0ccb06b3617c87e75bd650f92adc99e55b93070e0b2a0bc71634270226e125fc"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -29,6 +29,10 @@
name = "github.com/go-chi/chi"
version = "3.3.2"
[[constraint]]
name = "github.com/go-chi/cors"
version = "1.0.0"
[[constraint]]
name = "github.com/go-chi/render"
version = "1.0.1"
@ -51,7 +55,7 @@
[[constraint]]
name = "gopkg.in/ini.v1"
version = "1.37.0"
version = "1.38.1"
[prune]
go-tests = true

View File

@ -2,7 +2,7 @@ NAME=clash
BINDIR=bin
GOBUILD=CGO_ENABLED=0 go build -ldflags '-w -s'
all: linux macos
all: linux macos win64
linux:
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@

View File

@ -15,7 +15,6 @@
<a href="https://goreportcard.com/report/github.com/Dreamacro/clash">
<img src="https://goreportcard.com/badge/github.com/Dreamacro/clash?style=flat-square">
</a>
<a href="https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_shield" alt="FOSSA Status"><img src="https://app.fossa.io/api/projects/git%2Bgithub.com%2FDreamacro%2Fclash.svg?type=shield"/></a>
<a href="https://github.com/Dreamacro/clash/releases">
<img src="https://img.shields.io/github/release/Dreamacro/clash/all.svg?style=flat-square">
</a>
@ -66,6 +65,9 @@ Below is a simple demo configuration file:
port = 7890
socks-port = 7891
# redir proxy for Linux and macOS
redir-port = 7892
# A RESTful API for clash
external-controller = 127.0.0.1:8080
@ -73,13 +75,21 @@ external-controller = 127.0.0.1:8080
# name = ss, server, port, cipher, password
# The types of cipher are consistent with go-shadowsocks2
# support AEAD_AES_128_GCM AEAD_AES_192_GCM AEAD_AES_256_GCM AEAD_CHACHA20_POLY1305 AES-128-CTR AES-192-CTR AES-256-CTR AES-128-CFB AES-192-CFB AES-256-CFB CHACHA20-IETF XCHACHA20
Proxy1 = ss, server1, port, AEAD_CHACHA20_POLY1305, password
Proxy2 = ss, server2, port, AEAD_CHACHA20_POLY1305, password
ss1 = ss, server1, port, AEAD_CHACHA20_POLY1305, password
ss2 = ss, server2, port, AEAD_CHACHA20_POLY1305, password
# name = socks5, server, port
socks = socks5, server1, port
[Proxy Group]
# url-test select which proxy will be used by benchmarking speed to a URL.
# name = url-test, [proxys], url, interval(second)
Proxy = url-test, Proxy1, Proxy2, http://www.google.com/generate_204, 300
# name = url-test, [proxies], url, interval(second)
auto = url-test, ss1, ss2, http://www.google.com/generate_204, 300
# select is used for selecting proxy or proxy group
# you can use RESTful API to switch proxy, is recommended for use in GUI.
# name = select, [proxies]
Proxy = select, ss1, ss2, auto
[Rule]
DOMAIN-SUFFIX,google.com,Proxy
@ -89,9 +99,19 @@ GEOIP,CN,DIRECT
FINAL,,Proxy # note: there is two ","
```
## Thanks
[riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
[google/tcpproxy](https://github.com/google/tcpproxy)
## License
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FDreamacro%2Fclash.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large)
## TODO
- [ ] Complementing the necessary rule operators
- [x] Complementing the necessary rule operators
- [x] Redir proxy
- [ ] UDP support
- [ ] Connection manager

82
adapters/local/http.go Normal file
View File

@ -0,0 +1,82 @@
package adapters
import (
"bufio"
"bytes"
"io"
"net"
"strings"
C "github.com/Dreamacro/clash/constant"
)
// PeekedConn handle http connection and buffed HTTP data
type PeekedConn struct {
net.Conn
Peeked []byte
host string
isHTTP bool
}
func (c *PeekedConn) Read(p []byte) (n int, err error) {
if len(c.Peeked) > 0 {
n = copy(p, c.Peeked)
c.Peeked = c.Peeked[n:]
if len(c.Peeked) == 0 {
c.Peeked = nil
}
return n, nil
}
// Sometimes firefox just open a socket to process multiple domains in HTTP
// The temporary solution is to return io.EOF when encountering different HOST
if c.isHTTP {
br := bufio.NewReader(bytes.NewReader(p))
_, hostName := ParserHTTPHostHeader(br)
if hostName != "" {
if !strings.Contains(hostName, ":") {
hostName += ":80"
}
if hostName != c.host {
return 0, io.EOF
}
}
}
return c.Conn.Read(p)
}
// HTTPAdapter is a adapter for HTTP connection
type HTTPAdapter struct {
addr *C.Addr
conn *PeekedConn
}
// Close HTTP connection
func (h *HTTPAdapter) Close() {
h.conn.Close()
}
// Addr return destination address
func (h *HTTPAdapter) Addr() *C.Addr {
return h.addr
}
// Conn return raw net.Conn of HTTP
func (h *HTTPAdapter) Conn() net.Conn {
return h.conn
}
// NewHTTP is HTTPAdapter generator
func NewHTTP(host string, peeked []byte, isHTTP bool, conn net.Conn) *HTTPAdapter {
return &HTTPAdapter{
addr: parseHTTPAddr(host),
conn: &PeekedConn{
Peeked: peeked,
Conn: conn,
host: host,
isHTTP: isHTTP,
},
}
}

37
adapters/local/socks.go Normal file
View File

@ -0,0 +1,37 @@
package adapters
import (
"net"
C "github.com/Dreamacro/clash/constant"
"github.com/riobard/go-shadowsocks2/socks"
)
// SocksAdapter is a adapter for socks and redir connection
type SocksAdapter struct {
conn net.Conn
addr *C.Addr
}
// Close socks and redir connection
func (s *SocksAdapter) Close() {
s.conn.Close()
}
// Addr return destination address
func (s *SocksAdapter) Addr() *C.Addr {
return s.addr
}
// Conn return raw net.Conn
func (s *SocksAdapter) Conn() net.Conn {
return s.conn
}
// NewSocks is SocksAdapter generator
func NewSocks(target socks.Addr, conn net.Conn) *SocksAdapter {
return &SocksAdapter{
conn: conn,
addr: parseSocksAddr(target),
}
}

140
adapters/local/util.go Normal file
View File

@ -0,0 +1,140 @@
package adapters
import (
"bufio"
"bytes"
"net"
"net/http"
"strconv"
C "github.com/Dreamacro/clash/constant"
"github.com/riobard/go-shadowsocks2/socks"
)
func parseSocksAddr(target socks.Addr) *C.Addr {
var host, port string
var ip net.IP
switch target[0] {
case socks.AtypDomainName:
host = string(target[2 : 2+target[1]])
port = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
ipAddr, err := net.ResolveIPAddr("ip", host)
if err == nil {
ip = ipAddr.IP
}
case socks.AtypIPv4:
ip = net.IP(target[1 : 1+net.IPv4len])
port = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
case socks.AtypIPv6:
ip = net.IP(target[1 : 1+net.IPv6len])
port = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
}
return &C.Addr{
NetWork: C.TCP,
AddrType: int(target[0]),
Host: host,
IP: &ip,
Port: port,
}
}
func parseHTTPAddr(target string) *C.Addr {
host, port, _ := net.SplitHostPort(target)
ipAddr, err := net.ResolveIPAddr("ip", host)
var resolveIP *net.IP
if err == nil {
resolveIP = &ipAddr.IP
}
var addType int
ip := net.ParseIP(host)
switch {
case ip == nil:
addType = socks.AtypDomainName
case ip.To4() == nil:
addType = socks.AtypIPv6
default:
addType = socks.AtypIPv4
}
return &C.Addr{
NetWork: C.TCP,
AddrType: addType,
Host: host,
IP: resolveIP,
Port: port,
}
}
// ParserHTTPHostHeader returns the HTTP Host header from br without
// consuming any of its bytes. It returns "" if it can't find one.
func ParserHTTPHostHeader(br *bufio.Reader) (method, host string) {
// br := bufio.NewReader(bytes.NewReader(data))
const maxPeek = 4 << 10
peekSize := 0
for {
peekSize++
if peekSize > maxPeek {
b, _ := br.Peek(br.Buffered())
return method, httpHostHeaderFromBytes(b)
}
b, err := br.Peek(peekSize)
if n := br.Buffered(); n > peekSize {
b, _ = br.Peek(n)
peekSize = n
}
if len(b) > 0 {
if b[0] < 'A' || b[0] > 'Z' {
// Doesn't look like an HTTP verb
// (GET, POST, etc).
return
}
if bytes.Index(b, crlfcrlf) != -1 || bytes.Index(b, lflf) != -1 {
req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(b)))
if err != nil {
return
}
if len(req.Header["Host"]) > 1 {
// TODO(bradfitz): what does
// ReadRequest do if there are
// multiple Host headers?
return
}
return req.Method, req.Host
}
}
if err != nil {
return method, httpHostHeaderFromBytes(b)
}
}
}
var (
lfHostColon = []byte("\nHost:")
lfhostColon = []byte("\nhost:")
crlf = []byte("\r\n")
lf = []byte("\n")
crlfcrlf = []byte("\r\n\r\n")
lflf = []byte("\n\n")
)
func httpHostHeaderFromBytes(b []byte) string {
if i := bytes.Index(b, lfHostColon); i != -1 {
return string(bytes.TrimSpace(untilEOL(b[i+len(lfHostColon):])))
}
if i := bytes.Index(b, lfhostColon); i != -1 {
return string(bytes.TrimSpace(untilEOL(b[i+len(lfhostColon):])))
}
return ""
}
// untilEOL returns v, truncated before the first '\n' byte, if any.
// The returned slice may include a '\r' at the end.
func untilEOL(v []byte) []byte {
if i := bytes.IndexByte(v, '\n'); i != -1 {
return v[:i]
}
return v
}

View File

@ -1,54 +0,0 @@
package adapters
import (
"io"
"net"
C "github.com/Dreamacro/clash/constant"
)
// RejectAdapter is a reject connected adapter
type RejectAdapter struct {
}
// ReadWriter is used to handle network traffic
func (r *RejectAdapter) ReadWriter() io.ReadWriter {
return &NopRW{}
}
// Close is used to close connection
func (r *RejectAdapter) Close() {}
// Conn is used to http request
func (r *RejectAdapter) Conn() net.Conn {
return nil
}
type Reject struct {
}
func (r *Reject) Name() string {
return "Reject"
}
func (r *Reject) Type() C.AdapterType {
return C.Reject
}
func (r *Reject) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
return &RejectAdapter{}, nil
}
func NewReject() *Reject {
return &Reject{}
}
type NopRW struct{}
func (rw *NopRW) Read(b []byte) (int, error) {
return len(b), nil
}
func (rw *NopRW) Write(b []byte) (int, error) {
return 0, io.EOF
}

View File

@ -1,7 +1,6 @@
package adapters
import (
"io"
"net"
C "github.com/Dreamacro/clash/constant"
@ -12,11 +11,6 @@ type DirectAdapter struct {
conn net.Conn
}
// ReadWriter is used to handle network traffic
func (d *DirectAdapter) ReadWriter() io.ReadWriter {
return d.conn
}
// Close is used to close connection
func (d *DirectAdapter) Close() {
d.conn.Close()
@ -27,9 +21,7 @@ func (d *DirectAdapter) Conn() net.Conn {
return d.conn
}
type Direct struct {
traffic *C.Traffic
}
type Direct struct{}
func (d *Direct) Name() string {
return "Direct"
@ -45,9 +37,9 @@ func (d *Direct) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
return
}
c.(*net.TCPConn).SetKeepAlive(true)
return &DirectAdapter{conn: NewTrafficTrack(c, d.traffic)}, nil
return &DirectAdapter{conn: c}, nil
}
func NewDirect(traffic *C.Traffic) *Direct {
return &Direct{traffic: traffic}
func NewDirect() *Direct {
return &Direct{}
}

69
adapters/remote/reject.go Normal file
View File

@ -0,0 +1,69 @@
package adapters
import (
"io"
"net"
"time"
C "github.com/Dreamacro/clash/constant"
)
// RejectAdapter is a reject connected adapter
type RejectAdapter struct {
conn net.Conn
}
// Close is used to close connection
func (r *RejectAdapter) Close() {}
// Conn is used to http request
func (r *RejectAdapter) Conn() net.Conn {
return r.conn
}
type Reject struct {
}
func (r *Reject) Name() string {
return "Reject"
}
func (r *Reject) Type() C.AdapterType {
return C.Reject
}
func (r *Reject) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
return &RejectAdapter{conn: &NopConn{}}, nil
}
func NewReject() *Reject {
return &Reject{}
}
type NopConn struct{}
func (rw *NopConn) Read(b []byte) (int, error) {
return len(b), nil
}
func (rw *NopConn) Write(b []byte) (int, error) {
return 0, io.EOF
}
// Close is fake function for net.Conn
func (rw *NopConn) Close() error { return nil }
// LocalAddr is fake function for net.Conn
func (rw *NopConn) LocalAddr() net.Addr { return nil }
// RemoteAddr is fake function for net.Conn
func (rw *NopConn) RemoteAddr() net.Addr { return nil }
// SetDeadline is fake function for net.Conn
func (rw *NopConn) SetDeadline(time.Time) error { return nil }
// SetReadDeadline is fake function for net.Conn
func (rw *NopConn) SetReadDeadline(time.Time) error { return nil }
// SetWriteDeadline is fake function for net.Conn
func (rw *NopConn) SetWriteDeadline(time.Time) error { return nil }

View File

@ -2,6 +2,7 @@ package adapters
import (
"errors"
"sort"
C "github.com/Dreamacro/clash/constant"
)
@ -9,7 +10,7 @@ import (
type Selector struct {
name string
selected C.Proxy
proxys map[string]C.Proxy
proxies map[string]C.Proxy
}
func (s *Selector) Name() string {
@ -30,14 +31,15 @@ func (s *Selector) Now() string {
func (s *Selector) All() []string {
var all []string
for k, _ := range s.proxys {
for k := range s.proxies {
all = append(all, k)
}
sort.Strings(all)
return all
}
func (s *Selector) Set(name string) error {
proxy, exist := s.proxys[name]
proxy, exist := s.proxies[name]
if !exist {
return errors.New("Proxy does not exist")
}
@ -45,21 +47,21 @@ func (s *Selector) Set(name string) error {
return nil
}
func NewSelector(name string, proxys map[string]C.Proxy) (*Selector, error) {
if len(proxys) == 0 {
func NewSelector(name string, proxies map[string]C.Proxy) (*Selector, error) {
if len(proxies) == 0 {
return nil, errors.New("Provide at least one proxy")
}
mapping := make(map[string]C.Proxy)
var init string
for k, v := range proxys {
for k, v := range proxies {
mapping[k] = v
init = k
}
s := &Selector{
name: name,
proxys: mapping,
selected: proxys[init],
proxies: mapping,
selected: proxies[init],
}
return s, nil
}

View File

@ -3,7 +3,6 @@ package adapters
import (
"bytes"
"fmt"
"io"
"net"
"net/url"
"strconv"
@ -19,11 +18,6 @@ type ShadowsocksAdapter struct {
conn net.Conn
}
// ReadWriter is used to handle network traffic
func (ss *ShadowsocksAdapter) ReadWriter() io.ReadWriter {
return ss.conn
}
// Close is used to close connection
func (ss *ShadowsocksAdapter) Close() {
ss.conn.Close()
@ -34,10 +28,9 @@ func (ss *ShadowsocksAdapter) Conn() net.Conn {
}
type ShadowSocks struct {
server string
name string
cipher core.Cipher
traffic *C.Traffic
server string
name string
cipher core.Cipher
}
func (ss *ShadowSocks) Name() string {
@ -56,10 +49,10 @@ func (ss *ShadowSocks) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err erro
c.(*net.TCPConn).SetKeepAlive(true)
c = ss.cipher.StreamConn(c)
_, err = c.Write(serializesSocksAddr(addr))
return &ShadowsocksAdapter{conn: NewTrafficTrack(c, ss.traffic)}, err
return &ShadowsocksAdapter{conn: c}, err
}
func NewShadowSocks(name string, ssURL string, traffic *C.Traffic) (*ShadowSocks, error) {
func NewShadowSocks(name string, ssURL string) (*ShadowSocks, error) {
var key []byte
server, cipher, password, _ := parseURL(ssURL)
ciph, err := core.PickCipher(cipher, key, password)
@ -67,10 +60,9 @@ func NewShadowSocks(name string, ssURL string, traffic *C.Traffic) (*ShadowSocks
return nil, fmt.Errorf("ss %s initialize error: %s", server, err.Error())
}
return &ShadowSocks{
server: server,
name: name,
cipher: ciph,
traffic: traffic,
server: server,
name: name,
cipher: ciph,
}, nil
}

91
adapters/remote/socks5.go Normal file
View File

@ -0,0 +1,91 @@
package adapters
import (
"bytes"
"errors"
"fmt"
"io"
"net"
C "github.com/Dreamacro/clash/constant"
"github.com/riobard/go-shadowsocks2/socks"
)
// Socks5Adapter is a shadowsocks adapter
type Socks5Adapter struct {
conn net.Conn
}
// Close is used to close connection
func (ss *Socks5Adapter) Close() {
ss.conn.Close()
}
func (ss *Socks5Adapter) Conn() net.Conn {
return ss.conn
}
type Socks5 struct {
addr string
name string
}
func (ss *Socks5) Name() string {
return ss.name
}
func (ss *Socks5) Type() C.AdapterType {
return C.Socks5
}
func (ss *Socks5) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
c, err := net.Dial("tcp", ss.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error", ss.addr)
}
c.(*net.TCPConn).SetKeepAlive(true)
if err := ss.sharkHand(addr, c); err != nil {
return nil, err
}
return &Socks5Adapter{conn: c}, nil
}
func (ss *Socks5) sharkHand(addr *C.Addr, rw io.ReadWriter) error {
buf := make([]byte, socks.MaxAddrLen)
// VER, CMD, RSV
_, err := rw.Write([]byte{5, 1, 0})
if err != nil {
return err
}
if _, err := io.ReadFull(rw, buf[:2]); err != nil {
return err
}
if buf[0] != 5 {
return errors.New("SOCKS version error")
} else if buf[1] != 0 {
return errors.New("SOCKS need auth")
}
// VER, CMD, RSV, ADDR
if _, err := rw.Write(bytes.Join([][]byte{[]byte{5, 1, 0}, serializesSocksAddr(addr)}, []byte(""))); err != nil {
return err
}
if _, err := io.ReadFull(rw, buf[:10]); err != nil {
return err
}
return nil
}
func NewSocks5(name, addr string) *Socks5 {
return &Socks5{
addr: addr,
name: name,
}
}

102
adapters/remote/urltest.go Normal file
View File

@ -0,0 +1,102 @@
package adapters
import (
"sync"
"time"
C "github.com/Dreamacro/clash/constant"
)
type URLTest struct {
name string
proxies []C.Proxy
rawURL string
fast C.Proxy
delay time.Duration
done chan struct{}
}
func (u *URLTest) Name() string {
return u.name
}
func (u *URLTest) Type() C.AdapterType {
return C.URLTest
}
func (u *URLTest) Now() string {
return u.fast.Name()
}
func (u *URLTest) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
return u.fast.Generator(addr)
}
func (u *URLTest) Close() {
u.done <- struct{}{}
}
func (u *URLTest) loop() {
tick := time.NewTicker(u.delay)
go u.speedTest()
Loop:
for {
select {
case <-tick.C:
go u.speedTest()
case <-u.done:
break Loop
}
}
}
func (u *URLTest) speedTest() {
wg := sync.WaitGroup{}
wg.Add(len(u.proxies))
c := make(chan interface{})
fast := selectFast(c)
timer := time.NewTimer(u.delay)
for _, p := range u.proxies {
go func(p C.Proxy) {
_, err := DelayTest(p, u.rawURL)
if err == nil {
c <- p
}
wg.Done()
}(p)
}
go func() {
wg.Wait()
close(c)
}()
select {
case <-timer.C:
// Wait for fast to return or close.
<-fast
case p, open := <-fast:
if open {
u.fast = p.(C.Proxy)
}
}
}
func NewURLTest(name string, proxies []C.Proxy, rawURL string, delay time.Duration) (*URLTest, error) {
_, err := urlToAddr(rawURL)
if err != nil {
return nil, err
}
urlTest := &URLTest{
name: name,
proxies: proxies[:],
rawURL: rawURL,
fast: proxies[0],
delay: delay,
done: make(chan struct{}),
}
go urlTest.loop()
return urlTest, nil
}

86
adapters/remote/util.go Normal file
View File

@ -0,0 +1,86 @@
package adapters
import (
"fmt"
"net"
"net/http"
"net/url"
"time"
C "github.com/Dreamacro/clash/constant"
)
// DelayTest get the delay for the specified URL
func DelayTest(proxy C.Proxy, url string) (t int16, err error) {
addr, err := urlToAddr(url)
if err != nil {
return
}
start := time.Now()
instance, err := proxy.Generator(&addr)
if err != nil {
return
}
defer instance.Close()
transport := &http.Transport{
Dial: func(string, string) (net.Conn, error) {
return instance.Conn(), nil
},
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
client := http.Client{Transport: transport}
req, err := client.Get(url)
if err != nil {
return
}
req.Body.Close()
t = int16(time.Since(start) / time.Millisecond)
return
}
func urlToAddr(rawURL string) (addr C.Addr, err error) {
u, err := url.Parse(rawURL)
if err != nil {
return
}
port := u.Port()
if port == "" {
if u.Scheme == "https" {
port = "443"
} else if u.Scheme == "http" {
port = "80"
} else {
err = fmt.Errorf("%s scheme not Support", rawURL)
return
}
}
addr = C.Addr{
AddrType: C.AtypDomainName,
Host: u.Hostname(),
IP: nil,
Port: port,
}
return
}
func selectFast(in chan interface{}) chan interface{} {
out := make(chan interface{})
go func() {
p, open := <-in
if open {
out <- p
}
close(out)
for range in {
}
}()
return out
}

View File

@ -1,168 +0,0 @@
package adapters
import (
"fmt"
"net"
"net/http"
"net/url"
"sync"
"time"
C "github.com/Dreamacro/clash/constant"
)
type URLTest struct {
name string
proxys []C.Proxy
url *url.URL
rawURL string
addr *C.Addr
fast C.Proxy
delay time.Duration
done chan struct{}
}
func (u *URLTest) Name() string {
return u.name
}
func (u *URLTest) Type() C.AdapterType {
return C.URLTest
}
func (u *URLTest) Now() string {
return u.fast.Name()
}
func (u *URLTest) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
return u.fast.Generator(addr)
}
func (u *URLTest) Close() {
u.done <- struct{}{}
}
func (u *URLTest) loop() {
tick := time.NewTicker(u.delay)
go u.speedTest()
Loop:
for {
select {
case <-tick.C:
go u.speedTest()
case <-u.done:
break Loop
}
}
}
func (u *URLTest) speedTest() {
wg := sync.WaitGroup{}
wg.Add(len(u.proxys))
c := make(chan interface{})
fast := selectFast(c)
timer := time.NewTimer(u.delay)
for _, p := range u.proxys {
go func(p C.Proxy) {
err := getUrl(p, u.addr, u.rawURL)
if err == nil {
c <- p
}
wg.Done()
}(p)
}
go func() {
wg.Wait()
close(c)
}()
select {
case <-timer.C:
// Wait for fast to return or close.
<-fast
case p, open := <-fast:
if open {
u.fast = p.(C.Proxy)
}
}
}
func getUrl(proxy C.Proxy, addr *C.Addr, rawURL string) (err error) {
instance, err := proxy.Generator(addr)
if err != nil {
return
}
defer instance.Close()
transport := &http.Transport{
Dial: func(string, string) (net.Conn, error) {
return instance.Conn(), nil
},
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
client := http.Client{Transport: transport}
req, err := client.Get(rawURL)
if err != nil {
return
}
req.Body.Close()
return nil
}
func selectFast(in chan interface{}) chan interface{} {
out := make(chan interface{})
go func() {
p, open := <-in
if open {
out <- p
}
close(out)
for range in {
}
}()
return out
}
func NewURLTest(name string, proxys []C.Proxy, rawURL string, delay time.Duration) (*URLTest, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
port := u.Port()
if port == "" {
if u.Scheme == "https" {
port = "443"
} else if u.Scheme == "http" {
port = "80"
} else {
return nil, fmt.Errorf("%s scheme not Support", rawURL)
}
}
addr := &C.Addr{
AddrType: C.AtypDomainName,
Host: u.Hostname(),
IP: nil,
Port: port,
}
urlTest := &URLTest{
name: name,
proxys: proxys[:],
rawURL: rawURL,
url: u,
addr: addr,
fast: proxys[0],
delay: delay,
done: make(chan struct{}),
}
go urlTest.loop()
return urlTest, nil
}

373
config/config.go Normal file
View File

@ -0,0 +1,373 @@
package config
import (
"fmt"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/Dreamacro/clash/adapters/remote"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/observable"
R "github.com/Dreamacro/clash/rules"
log "github.com/sirupsen/logrus"
"gopkg.in/ini.v1"
)
var (
config *Config
once sync.Once
)
// General config
type General struct {
Port int
SocksPort int
RedirPort int
AllowLan bool
Mode Mode
LogLevel C.LogLevel
}
// ProxyConfig is update proxy schema
type ProxyConfig struct {
Port *int
SocksPort *int
RedirPort *int
AllowLan *bool
}
// Config is clash config manager
type Config struct {
general *General
rules []C.Rule
proxies map[string]C.Proxy
lastUpdate time.Time
event chan<- interface{}
reportCh chan interface{}
observable *observable.Observable
}
// Event is event of clash config
type Event struct {
Type string
Payload interface{}
}
// Subscribe config stream
func (c *Config) Subscribe() observable.Subscription {
sub, _ := c.observable.Subscribe()
return sub
}
// Report return a channel for collecting report message
func (c *Config) Report() chan<- interface{} {
return c.reportCh
}
func (c *Config) readConfig() (*ini.File, error) {
if _, err := os.Stat(C.ConfigPath); os.IsNotExist(err) {
return nil, err
}
return ini.LoadSources(
ini.LoadOptions{AllowBooleanKeys: true},
C.ConfigPath,
)
}
// Parse config
func (c *Config) Parse() error {
cfg, err := c.readConfig()
if err != nil {
return err
}
if err := c.parseGeneral(cfg); err != nil {
return err
}
if err := c.parseProxies(cfg); err != nil {
return err
}
return c.parseRules(cfg)
}
// Proxies return proxies of clash
func (c *Config) Proxies() map[string]C.Proxy {
return c.proxies
}
// Rules return rules of clash
func (c *Config) Rules() []C.Rule {
return c.rules
}
// SetMode change mode of clash
func (c *Config) SetMode(mode Mode) {
c.general.Mode = mode
c.event <- &Event{Type: "mode", Payload: mode}
}
// SetLogLevel change log level of clash
func (c *Config) SetLogLevel(level C.LogLevel) {
c.general.LogLevel = level
c.event <- &Event{Type: "log-level", Payload: level}
}
// General return clash general config
func (c *Config) General() General {
return *c.general
}
// UpdateRules is a function for hot reload rules
func (c *Config) UpdateRules() error {
cfg, err := c.readConfig()
if err != nil {
return err
}
return c.parseRules(cfg)
}
func (c *Config) parseGeneral(cfg *ini.File) error {
general := cfg.Section("General")
port := general.Key("port").RangeInt(0, 1, 65535)
socksPort := general.Key("socks-port").RangeInt(0, 1, 65535)
redirPort := general.Key("redir-port").RangeInt(0, 1, 65535)
allowLan := general.Key("allow-lan").MustBool()
logLevelString := general.Key("log-level").MustString(C.INFO.String())
modeString := general.Key("mode").MustString(Rule.String())
mode, exist := ModeMapping[modeString]
if !exist {
return fmt.Errorf("General.mode value invalid")
}
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 := general.Key("external-controller").String(); restAddr != "" {
c.event <- &Event{Type: "external-controller", Payload: restAddr}
}
c.UpdateGeneral(*c.general)
return nil
}
// UpdateGeneral dispatch update event
func (c *Config) UpdateGeneral(general General) {
c.UpdateProxy(ProxyConfig{
Port: &general.Port,
SocksPort: &general.SocksPort,
RedirPort: &general.RedirPort,
AllowLan: &general.AllowLan,
})
c.event <- &Event{Type: "mode", Payload: general.Mode}
c.event <- &Event{Type: "log-level", Payload: general.LogLevel}
}
// UpdateProxy dispatch update proxy event
func (c *Config) UpdateProxy(pc ProxyConfig) {
if pc.AllowLan != nil {
c.general.AllowLan = *pc.AllowLan
}
if (pc.AllowLan != nil || pc.Port != nil) && *pc.Port != 0 {
c.general.Port = *pc.Port
c.event <- &Event{Type: "http-addr", Payload: genAddr(*pc.Port, c.general.AllowLan)}
}
if (pc.AllowLan != nil || pc.SocksPort != nil) && *pc.SocksPort != 0 {
c.general.SocksPort = *pc.SocksPort
c.event <- &Event{Type: "socks-addr", Payload: genAddr(*pc.SocksPort, c.general.AllowLan)}
}
if (pc.AllowLan != nil || pc.RedirPort != nil) && *pc.RedirPort != 0 {
c.general.RedirPort = *pc.RedirPort
c.event <- &Event{Type: "redir-addr", Payload: genAddr(*pc.RedirPort, c.general.AllowLan)}
}
}
func (c *Config) parseProxies(cfg *ini.File) error {
proxies := make(map[string]C.Proxy)
proxiesConfig := cfg.Section("Proxy")
groupsConfig := cfg.Section("Proxy Group")
// parse proxy
for _, key := range proxiesConfig.Keys() {
proxy := key.Strings(",")
if len(proxy) == 0 {
continue
}
switch proxy[0] {
// ss, server, port, cipter, password
case "ss":
if len(proxy) < 5 {
continue
}
ssURL := fmt.Sprintf("ss://%s:%s@%s:%s", proxy[3], proxy[4], proxy[1], proxy[2])
ss, err := adapters.NewShadowSocks(key.Name(), ssURL)
if err != nil {
return err
}
proxies[key.Name()] = ss
// socks5, server, port
case "socks5":
if len(proxy) < 3 {
continue
}
addr := fmt.Sprintf("%s:%s", proxy[1], proxy[2])
socks5 := adapters.NewSocks5(key.Name(), addr)
proxies[key.Name()] = socks5
}
}
// parse proxy group
for _, key := range groupsConfig.Keys() {
rule := strings.Split(key.Value(), ",")
rule = trimArr(rule)
switch rule[0] {
case "url-test":
if len(rule) < 4 {
return fmt.Errorf("URLTest need more than 4 param")
}
proxyNames := rule[1 : len(rule)-2]
delay, _ := strconv.Atoi(rule[len(rule)-1])
url := rule[len(rule)-2]
var ps []C.Proxy
for _, name := range proxyNames {
if p, ok := proxies[name]; ok {
ps = append(ps, p)
}
}
adapter, err := adapters.NewURLTest(key.Name(), ps, url, time.Duration(delay)*time.Second)
if err != nil {
return fmt.Errorf("Config error: %s", err.Error())
}
proxies[key.Name()] = adapter
case "select":
if len(rule) < 2 {
return fmt.Errorf("Selector need more than 2 param")
}
proxyNames := rule[1:]
selectProxy := make(map[string]C.Proxy)
for _, name := range proxyNames {
proxy, exist := proxies[name]
if !exist {
return fmt.Errorf("Proxy %s not exist", name)
}
selectProxy[name] = proxy
}
selector, err := adapters.NewSelector(key.Name(), selectProxy)
if err != nil {
return fmt.Errorf("Selector create error: %s", err.Error())
}
proxies[key.Name()] = selector
}
}
// init proxy
proxies["GLOBAL"], _ = adapters.NewSelector("GLOBAL", proxies)
proxies["DIRECT"] = adapters.NewDirect()
proxies["REJECT"] = adapters.NewReject()
c.proxies = proxies
c.event <- &Event{Type: "proxies", Payload: proxies}
return nil
}
func (c *Config) parseRules(cfg *ini.File) error {
rules := []C.Rule{}
rulesConfig := cfg.Section("Rule")
// parse rules
for _, key := range rulesConfig.Keys() {
rule := strings.Split(key.Name(), ",")
if len(rule) < 3 {
continue
}
rule = trimArr(rule)
switch rule[0] {
case "DOMAIN-SUFFIX":
rules = append(rules, R.NewDomainSuffix(rule[1], rule[2]))
case "DOMAIN-KEYWORD":
rules = append(rules, R.NewDomainKeyword(rule[1], rule[2]))
case "GEOIP":
rules = append(rules, R.NewGEOIP(rule[1], rule[2]))
case "IP-CIDR", "IP-CIDR6":
rules = append(rules, R.NewIPCIDR(rule[1], rule[2]))
case "FINAL":
rules = append(rules, R.NewFinal(rule[2]))
}
}
c.rules = rules
c.event <- &Event{Type: "rules", Payload: rules}
return nil
}
func (c *Config) handleResponseMessage() {
for elm := range c.reportCh {
event := elm.(*Event)
switch event.Type {
case "http-addr":
if event.Payload.(bool) == false {
log.Errorf("Listening HTTP proxy at %s error", c.general.Port)
c.general.Port = 0
}
case "socks-addr":
if event.Payload.(bool) == false {
log.Errorf("Listening SOCKS proxy at %s error", c.general.SocksPort)
c.general.SocksPort = 0
}
case "redir-addr":
if event.Payload.(bool) == false {
log.Errorf("Listening Redir proxy at %s error", c.general.RedirPort)
c.general.RedirPort = 0
}
}
}
}
func newConfig() *Config {
event := make(chan interface{})
reportCh := make(chan interface{})
config := &Config{
general: &General{},
proxies: make(map[string]C.Proxy),
rules: []C.Rule{},
lastUpdate: time.Now(),
event: event,
reportCh: reportCh,
observable: observable.NewObservable(event),
}
go config.handleResponseMessage()
return config
}
// Instance return singleton instance of Config
func Instance() *Config {
once.Do(func() {
config = newConfig()
})
return config
}

72
config/initial.go Normal file
View File

@ -0,0 +1,72 @@
package config
import (
"archive/tar"
"compress/gzip"
"io"
"net/http"
"os"
"strings"
C "github.com/Dreamacro/clash/constant"
log "github.com/sirupsen/logrus"
)
func downloadMMDB(path string) (err error) {
resp, err := http.Get("http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz")
if err != nil {
return
}
defer resp.Body.Close()
gr, err := gzip.NewReader(resp.Body)
if err != nil {
return
}
defer gr.Close()
tr := tar.NewReader(gr)
for {
h, err := tr.Next()
if err == io.EOF {
break
} else if err != nil {
return err
}
if !strings.HasSuffix(h.Name, "GeoLite2-Country.mmdb") {
continue
}
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, tr)
if err != nil {
return err
}
}
return nil
}
// Init prepare necessary files
func Init() {
// initial config.ini
if _, err := os.Stat(C.ConfigPath); os.IsNotExist(err) {
log.Info("Can't find config, create a empty file")
os.OpenFile(C.ConfigPath, os.O_CREATE|os.O_WRONLY, 0644)
}
// initial mmdb
if _, err := os.Stat(C.MMDBPath); os.IsNotExist(err) {
log.Info("Can't find MMDB, start download")
err := downloadMMDB(C.MMDBPath)
if err != nil {
log.Fatalf("Can't download MMDB: %s", err.Error())
}
}
}

View File

@ -1,7 +1,16 @@
package tunnel
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

20
config/utils.go Normal file
View File

@ -0,0 +1,20 @@
package config
import (
"fmt"
"strings"
)
func trimArr(arr []string) (r []string) {
for _, e := range arr {
r = append(r, strings.Trim(e, " "))
}
return
}
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

@ -1,7 +1,6 @@
package constant
import (
"io"
"net"
)
@ -11,18 +10,17 @@ const (
Reject
Selector
Shadowsocks
Socks5
URLTest
)
type ProxyAdapter interface {
ReadWriter() io.ReadWriter
Conn() net.Conn
Close()
}
type ServerAdapter interface {
Addr() *Addr
Connect(ProxyAdapter)
Close()
}
@ -45,6 +43,8 @@ func (at AdapterType) String() string {
return "Selector"
case Shadowsocks:
return "Shadowsocks"
case Socks5:
return "Socks5"
case URLTest:
return "URLTest"
default:

View File

@ -10,8 +10,11 @@ const (
AtypDomainName = 3
AtypIPv6 = 4
TCP = iota
TCP NetWork = iota
UDP
HTTP SourceType = iota
SOCKS
)
type NetWork int
@ -23,9 +26,12 @@ func (n *NetWork) String() string {
return "udp"
}
type SourceType int
// Addr is used to store connection address
type Addr struct {
NetWork NetWork
Source SourceType
AddrType int
Host string
IP *net.IP

View File

@ -1,23 +1,15 @@
package constant
import (
"archive/tar"
"compress/gzip"
"io"
"net/http"
"os"
"os/user"
"path"
"strings"
log "github.com/sirupsen/logrus"
"gopkg.in/ini.v1"
)
const (
Name = "clash"
DefalutHTTPPort = 7890
DefalutSOCKSPort = 7891
Name = "clash"
)
var (
@ -31,6 +23,7 @@ type General struct {
AllowLan *bool `json:"allow-lan,omitempty"`
Port *int `json:"port,omitempty"`
SocksPort *int `json:"socks-port,omitempty"`
LogLevel *string `json:"log-level,omitempty"`
}
func init() {
@ -53,67 +46,5 @@ func init() {
}
ConfigPath = path.Join(dirPath, "config.ini")
if _, err := os.Stat(ConfigPath); os.IsNotExist(err) {
log.Info("Can't find config, create a empty file")
os.OpenFile(ConfigPath, os.O_CREATE|os.O_WRONLY, 0644)
}
MMDBPath = path.Join(dirPath, "Country.mmdb")
if _, err := os.Stat(MMDBPath); os.IsNotExist(err) {
log.Info("Can't find MMDB, start download")
err := downloadMMDB(MMDBPath)
if err != nil {
log.Fatalf("Can't download MMDB: %s", err.Error())
}
}
}
func downloadMMDB(path string) (err error) {
resp, err := http.Get("http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz")
if err != nil {
return
}
defer resp.Body.Close()
gr, err := gzip.NewReader(resp.Body)
if err != nil {
return
}
defer gr.Close()
tr := tar.NewReader(gr)
for {
h, err := tr.Next()
if err == io.EOF {
break
} else if err != nil {
return err
}
if !strings.HasSuffix(h.Name, "GeoLite2-Country.mmdb") {
continue
}
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, tr)
if err != nil {
return err
}
}
return nil
}
func GetConfig() (*ini.File, error) {
if _, err := os.Stat(ConfigPath); os.IsNotExist(err) {
return nil, err
}
return ini.LoadSources(
ini.LoadOptions{AllowBooleanKeys: true},
ConfigPath,
)
}

35
constant/log.go Normal file
View File

@ -0,0 +1,35 @@
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

@ -1,12 +1,14 @@
package hub
import (
"github.com/Dreamacro/clash/config"
"github.com/Dreamacro/clash/proxy"
T "github.com/Dreamacro/clash/tunnel"
)
var (
tunnel = T.GetInstance()
tunnel = T.Instance()
cfg = config.Instance()
listener = proxy.Instance()
)

View File

@ -4,9 +4,8 @@ import (
"fmt"
"net/http"
"github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/proxy"
T "github.com/Dreamacro/clash/tunnel"
"github.com/go-chi/chi"
"github.com/go-chi/render"
@ -19,17 +18,23 @@ func configRouter() http.Handler {
return r
}
var modeMapping = map[string]T.Mode{
"Global": T.Global,
"Rule": T.Rule,
"Direct": T.Direct,
type configSchema struct {
Port int `json:"port"`
SocksPort int `json:"socket-port"`
AllowLan bool `json:"allow-lan"`
Mode string `json:"mode"`
LogLevel string `json:"log-level"`
}
func getConfigs(w http.ResponseWriter, r *http.Request) {
info := listener.Info()
mode := tunnel.GetMode().String()
info.Mode = &mode
render.JSON(w, r, info)
general := cfg.General()
render.JSON(w, r, configSchema{
Port: general.Port,
SocksPort: general.SocksPort,
AllowLan: general.AllowLan,
Mode: general.Mode.String(),
LogLevel: general.LogLevel.String(),
})
}
func updateConfigs(w http.ResponseWriter, r *http.Request) {
@ -44,25 +49,31 @@ func updateConfigs(w http.ResponseWriter, r *http.Request) {
}
// update errors
var proxyErr, modeErr error
// update proxy
listener := proxy.Instance()
proxyErr = listener.Update(general.AllowLan, general.Port, general.SocksPort)
var modeErr, logLevelErr error
// update mode
if general.Mode != nil {
mode, ok := modeMapping[*general.Mode]
mode, ok := config.ModeMapping[*general.Mode]
if !ok {
modeErr = fmt.Errorf("Mode error")
} else {
tunnel.SetMode(mode)
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{
"proxy": proxyErr,
"mode": modeErr,
"mode": modeErr,
"log-level": logLevelErr,
})
if hasError {
@ -70,5 +81,13 @@ func updateConfigs(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, errors)
return
}
// update proxy
cfg.UpdateProxy(config.ProxyConfig{
AllowLan: general.AllowLan,
Port: general.Port,
SocksPort: general.SocksPort,
})
w.WriteHeader(http.StatusNoContent)
}

View File

@ -3,8 +3,10 @@ package hub
import (
"fmt"
"net/http"
"strconv"
"time"
A "github.com/Dreamacro/clash/adapters"
A "github.com/Dreamacro/clash/adapters/remote"
C "github.com/Dreamacro/clash/constant"
"github.com/go-chi/chi"
@ -13,8 +15,9 @@ import (
func proxyRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getProxys)
r.Get("/", getProxies)
r.Get("/{name}", getProxy)
r.Get("/{name}/delay", getProxyDelay)
r.Put("/{name}", updateProxy)
return r
}
@ -56,23 +59,23 @@ func transformProxy(proxy C.Proxy) interface{} {
}
}
type GetProxysResponse struct {
Proxys map[string]interface{} `json:"proxys"`
type GetProxiesResponse struct {
Proxies map[string]interface{} `json:"proxies"`
}
func getProxys(w http.ResponseWriter, r *http.Request) {
_, rawProxys := tunnel.Config()
proxys := make(map[string]interface{})
for name, proxy := range rawProxys {
proxys[name] = transformProxy(proxy)
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, GetProxysResponse{Proxys: proxys})
render.JSON(w, r, GetProxiesResponse{Proxies: proxies})
}
func getProxy(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
_, proxys := tunnel.Config()
proxy, exist := proxys[name]
proxies := cfg.Proxies()
proxy, exist := proxies[name]
if !exist {
w.WriteHeader(http.StatusNotFound)
render.JSON(w, r, Error{
@ -98,8 +101,8 @@ func updateProxy(w http.ResponseWriter, r *http.Request) {
}
name := chi.URLParam(r, "name")
_, proxys := tunnel.Config()
proxy, exist := proxys[name]
proxies := cfg.Proxies()
proxy, exist := proxies[name]
if !exist {
w.WriteHeader(http.StatusNotFound)
render.JSON(w, r, Error{
@ -127,3 +130,64 @@ func updateProxy(w http.ResponseWriter, r *http.Request) {
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 := chi.URLParam(r, "name")
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,
})
}
}
}

View File

@ -24,10 +24,10 @@ type GetRulesResponse struct {
}
func getRules(w http.ResponseWriter, r *http.Request) {
rulesCfg, _ := tunnel.Config()
rawRules := cfg.Rules()
var rules []Rule
for _, rule := range rulesCfg {
for _, rule := range rawRules {
rules = append(rules, Rule{
Name: rule.RuleType().String(),
Payload: rule.Payload(),
@ -41,7 +41,7 @@ func getRules(w http.ResponseWriter, r *http.Request) {
}
func updateRules(w http.ResponseWriter, r *http.Request) {
err := tunnel.UpdateConfig()
err := cfg.UpdateRules()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
render.JSON(w, r, Error{

View File

@ -5,9 +5,12 @@ import (
"net/http"
"time"
"github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant"
T "github.com/Dreamacro/clash/tunnel"
"github.com/go-chi/chi"
"github.com/go-chi/cors"
"github.com/go-chi/render"
log "github.com/sirupsen/logrus"
)
@ -17,21 +20,51 @@ type Traffic struct {
Down int64 `json:"down"`
}
func NewHub(addr string) {
func newHub(signal chan struct{}) {
var addr string
ch := config.Instance().Subscribe()
signal <- struct{}{}
for {
elm := <-ch
event := elm.(*config.Event)
if event.Type == "external-controller" {
addr = event.Payload.(string)
break
}
}
r := chi.NewRouter()
r.Get("/traffic", traffic)
r.Get("/logs", getLogs)
cors := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Content-Type"},
MaxAge: 300,
})
r.Use(cors.Handler)
r.With(jsonContentType).Get("/traffic", traffic)
r.With(jsonContentType).Get("/logs", getLogs)
r.Mount("/configs", configRouter())
r.Mount("/proxys", proxyRouter())
r.Mount("/proxies", proxyRouter())
r.Mount("/rules", ruleRouter())
log.Infof("RESTful API listening at: %s", addr)
err := http.ListenAndServe(addr, r)
if err != nil {
log.Fatalf("External controller error: %s", err.Error())
log.Errorf("External controller error: %s", err.Error())
}
}
func jsonContentType(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func traffic(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
@ -65,14 +98,7 @@ func getLogs(w http.ResponseWriter, r *http.Request) {
req.Level = "info"
}
mapping := map[string]T.LogLevel{
"info": T.INFO,
"debug": T.DEBUG,
"error": T.ERROR,
"warning": T.WARNING,
}
level, ok := mapping[req.Level]
level, ok := C.LogLevelMapping[req.Level]
if !ok {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
@ -107,3 +133,10 @@ func getLogs(w http.ResponseWriter, r *http.Request) {
w.(http.Flusher).Flush()
}
}
// Run initial hub
func Run() {
signal := make(chan struct{})
go newHub(signal)
<-signal
}

23
main.go
View File

@ -5,7 +5,7 @@ import (
"os/signal"
"syscall"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/config"
"github.com/Dreamacro/clash/hub"
"github.com/Dreamacro/clash/proxy"
"github.com/Dreamacro/clash/tunnel"
@ -14,23 +14,14 @@ import (
)
func main() {
if err := tunnel.GetInstance().UpdateConfig(); err != nil {
log.Fatalf("Parse config error: %s", err.Error())
}
tunnel.Instance().Run()
proxy.Instance().Run()
hub.Run()
if err := proxy.Instance().Run(); err != nil {
log.Fatalf("Proxy listen error: %s", err.Error())
}
// Hub
cfg, err := C.GetConfig()
config.Init()
err := config.Instance().Parse()
if err != nil {
log.Fatalf("Read config error: %s", err.Error())
}
section := cfg.Section("General")
if key, err := section.GetKey("external-controller"); err == nil {
go hub.NewHub(key.Value())
log.Fatalf("Parse config error: %s", err.Error())
}
sigCh := make(chan os.Signal, 1)

View File

@ -1,77 +0,0 @@
package http
import (
"io"
"net"
"net/http"
"time"
C "github.com/Dreamacro/clash/constant"
)
type HttpAdapter struct {
addr *C.Addr
r *http.Request
w http.ResponseWriter
done chan struct{}
}
func (h *HttpAdapter) Close() {
h.done <- struct{}{}
}
func (h *HttpAdapter) Addr() *C.Addr {
return h.addr
}
func (h *HttpAdapter) Connect(proxy C.ProxyAdapter) {
req := http.Transport{
Dial: func(string, string) (net.Conn, error) {
return proxy.Conn(), nil
},
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
resp, err := req.RoundTrip(h.r)
if err != nil {
return
}
defer resp.Body.Close()
header := h.w.Header()
for k, vv := range resp.Header {
for _, v := range vv {
header.Add(k, v)
}
}
h.w.WriteHeader(resp.StatusCode)
var writer io.Writer = h.w
if len(resp.TransferEncoding) > 0 && resp.TransferEncoding[0] == "chunked" {
writer = ChunkWriter{Writer: h.w}
}
io.Copy(writer, resp.Body)
}
type ChunkWriter struct {
io.Writer
}
func (cw ChunkWriter) Write(b []byte) (int, error) {
n, err := cw.Writer.Write(b)
if err == nil {
cw.Writer.(http.Flusher).Flush()
}
return n, err
}
func NewHttp(host string, w http.ResponseWriter, r *http.Request) (*HttpAdapter, chan struct{}) {
done := make(chan struct{})
return &HttpAdapter{
addr: parseHttpAddr(host),
r: r,
w: w,
done: done,
}, done
}

View File

@ -1,35 +0,0 @@
package http
import (
"bufio"
"io"
"net"
C "github.com/Dreamacro/clash/constant"
)
type HttpsAdapter struct {
addr *C.Addr
conn net.Conn
rw *bufio.ReadWriter
}
func (h *HttpsAdapter) Close() {
h.conn.Close()
}
func (h *HttpsAdapter) Addr() *C.Addr {
return h.addr
}
func (h *HttpsAdapter) Connect(proxy C.ProxyAdapter) {
go io.Copy(h.conn, proxy.ReadWriter())
io.Copy(proxy.ReadWriter(), h.conn)
}
func NewHttps(host string, conn net.Conn) *HttpsAdapter {
return &HttpsAdapter{
addr: parseHttpAddr(host),
conn: conn,
}
}

View File

@ -1,20 +1,20 @@
package http
import (
"context"
"bufio"
"net"
"net/http"
"strings"
"github.com/Dreamacro/clash/adapters/local"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/tunnel"
"github.com/riobard/go-shadowsocks2/socks"
log "github.com/sirupsen/logrus"
)
var (
tun = tunnel.GetInstance()
tun = tunnel.Instance()
)
func NewHttpProxy(addr string) (*C.ProxySignal, error) {
@ -30,24 +30,23 @@ func NewHttpProxy(addr string) (*C.ProxySignal, error) {
Closed: closed,
}
server := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
handleTunneling(w, r)
} else {
handleHTTP(w, r)
}
}),
}
go func() {
log.Infof("HTTP proxy listening at: %s", addr)
server.Serve(l)
for {
c, err := l.Accept()
if err != nil {
if _, open := <-done; !open {
break
}
continue
}
go handleConn(c)
}
}()
go func() {
<-done
server.Shutdown(context.Background())
close(done)
l.Close()
closed <- struct{}{}
}()
@ -55,55 +54,26 @@ func NewHttpProxy(addr string) (*C.ProxySignal, error) {
return signal, nil
}
func handleHTTP(w http.ResponseWriter, r *http.Request) {
addr := r.Host
// padding default port
if !strings.Contains(addr, ":") {
addr += ":80"
}
req, done := NewHttp(addr, w, r)
tun.Add(req)
<-done
}
func handleTunneling(w http.ResponseWriter, r *http.Request) {
hijacker, ok := w.(http.Hijacker)
if !ok {
func handleConn(conn net.Conn) {
br := bufio.NewReader(conn)
method, hostName := adapters.ParserHTTPHostHeader(br)
if hostName == "" {
return
}
conn, _, err := hijacker.Hijack()
if err != nil {
return
}
// w.WriteHeader(http.StatusOK) doesn't works in Safari
conn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n"))
tun.Add(NewHttps(r.Host, conn))
}
func parseHttpAddr(target string) *C.Addr {
host, port, _ := net.SplitHostPort(target)
ipAddr, err := net.ResolveIPAddr("ip", host)
var resolveIP *net.IP
if err == nil {
resolveIP = &ipAddr.IP
}
var addType int
ip := net.ParseIP(host)
switch {
case ip == nil:
addType = socks.AtypDomainName
case ip.To4() == nil:
addType = socks.AtypIPv6
default:
addType = socks.AtypIPv4
}
return &C.Addr{
NetWork: C.TCP,
AddrType: addType,
Host: host,
IP: resolveIP,
Port: port,
if !strings.Contains(hostName, ":") {
hostName += ":80"
}
var peeked []byte
if method == http.MethodConnect {
_, err := conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
if err != nil {
return
}
} else if n := br.Buffered(); n > 0 {
peeked, _ = br.Peek(br.Buffered())
}
tun.Add(adapters.NewHTTP(hostName, peeked, method != http.MethodConnect, conn))
}

View File

@ -1,14 +1,13 @@
package proxy
import (
"fmt"
"sync"
"github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/proxy/http"
"github.com/Dreamacro/clash/proxy/redir"
"github.com/Dreamacro/clash/proxy/socks"
log "github.com/sirupsen/logrus"
)
var (
@ -17,58 +16,13 @@ var (
)
type Listener struct {
httpPort int
socksPort int
allowLan bool
// signal for update
httpSignal *C.ProxySignal
socksSignal *C.ProxySignal
redirSignal *C.ProxySignal
}
// Info returns the proxys's current configuration
func (l *Listener) Info() (info C.General) {
return C.General{
Port: &l.httpPort,
SocksPort: &l.socksPort,
AllowLan: &l.allowLan,
}
}
func (l *Listener) Update(allowLan *bool, httpPort *int, socksPort *int) error {
if allowLan != nil {
l.allowLan = *allowLan
}
var socksErr, httpErr error
if allowLan != nil || httpPort != nil {
newHTTPPort := l.httpPort
if httpPort != nil {
newHTTPPort = *httpPort
}
httpErr = l.updateHTTP(newHTTPPort)
}
if allowLan != nil || socksPort != nil {
newSocksPort := l.socksPort
if socksPort != nil {
newSocksPort = *socksPort
}
socksErr = l.updateSocks(newSocksPort)
}
if socksErr != nil && httpErr != nil {
return fmt.Errorf("%s\n%s", socksErr.Error(), httpErr.Error())
} else if socksErr != nil {
return socksErr
} else if httpErr != nil {
return httpErr
} else {
return nil
}
}
func (l *Listener) updateHTTP(port int) error {
func (l *Listener) updateHTTP(addr string) error {
if l.httpSignal != nil {
signal := l.httpSignal
signal.Done <- struct{}{}
@ -76,17 +30,16 @@ func (l *Listener) updateHTTP(port int) error {
l.httpSignal = nil
}
signal, err := http.NewHttpProxy(l.genAddr(port))
signal, err := http.NewHttpProxy(addr)
if err != nil {
return err
}
l.httpSignal = signal
l.httpPort = port
return nil
}
func (l *Listener) updateSocks(port int) error {
func (l *Listener) updateSocks(addr string) error {
if l.socksSignal != nil {
signal := l.socksSignal
signal.Done <- struct{}{}
@ -94,47 +47,67 @@ func (l *Listener) updateSocks(port int) error {
l.socksSignal = nil
}
signal, err := socks.NewSocksProxy(l.genAddr(port))
signal, err := socks.NewSocksProxy(addr)
if err != nil {
return err
}
l.socksSignal = signal
l.socksPort = port
return nil
}
func (l *Listener) genAddr(port int) string {
host := "127.0.0.1"
if l.allowLan {
host = ""
func (l *Listener) updateRedir(addr string) error {
if l.redirSignal != nil {
signal := l.redirSignal
signal.Done <- struct{}{}
<-signal.Closed
l.redirSignal = nil
}
return fmt.Sprintf("%s:%d", host, port)
signal, err := redir.NewRedirProxy(addr)
if err != nil {
return err
}
l.redirSignal = signal
return nil
}
func (l *Listener) Run() error {
return l.Update(&l.allowLan, &l.httpPort, &l.socksPort)
func (l *Listener) process(signal chan<- struct{}) {
sub := config.Instance().Subscribe()
signal <- struct{}{}
reportCH := config.Instance().Report()
for elm := range sub {
event := elm.(*config.Event)
switch event.Type {
case "http-addr":
addr := event.Payload.(string)
err := l.updateHTTP(addr)
reportCH <- &config.Event{Type: "http-addr", Payload: err == nil}
case "socks-addr":
addr := event.Payload.(string)
err := l.updateSocks(addr)
reportCH <- &config.Event{Type: "socks-addr", Payload: err == nil}
case "redir-addr":
addr := event.Payload.(string)
err := l.updateRedir(addr)
reportCH <- &config.Event{Type: "redir-addr", Payload: err == nil}
}
}
}
// Run ensure config monitoring
func (l *Listener) Run() {
signal := make(chan struct{})
go l.process(signal)
<-signal
}
func newListener() *Listener {
cfg, err := C.GetConfig()
if err != nil {
log.Fatalf("Read config error: %s", err.Error())
}
general := cfg.Section("General")
port := general.Key("port").RangeInt(C.DefalutHTTPPort, 1, 65535)
socksPort := general.Key("socks-port").RangeInt(C.DefalutSOCKSPort, 1, 65535)
allowLan := general.Key("allow-lan").MustBool()
return &Listener{
httpPort: port,
socksPort: socksPort,
allowLan: allowLan,
}
return &Listener{}
}
// Instance return singleton instance of Listener
func Instance() *Listener {
once.Do(func() {
listener = newListener()

62
proxy/redir/tcp.go Normal file
View File

@ -0,0 +1,62 @@
package redir
import (
"net"
"github.com/Dreamacro/clash/adapters/local"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/tunnel"
log "github.com/sirupsen/logrus"
)
var (
tun = tunnel.Instance()
)
func NewRedirProxy(addr string) (*C.ProxySignal, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
done := make(chan struct{})
closed := make(chan struct{})
signal := &C.ProxySignal{
Done: done,
Closed: closed,
}
go func() {
log.Infof("Redir proxy listening at: %s", addr)
for {
c, err := l.Accept()
if err != nil {
if _, open := <-done; !open {
break
}
continue
}
go handleRedir(c)
}
}()
go func() {
<-done
close(done)
l.Close()
closed <- struct{}{}
}()
return signal, nil
}
func handleRedir(conn net.Conn) {
target, err := parserPacket(conn)
if err != nil {
conn.Close()
return
}
conn.(*net.TCPConn).SetKeepAlive(true)
tun.Add(adapters.NewSocks(target, conn))
}

58
proxy/redir/tcp_darwin.go Normal file
View File

@ -0,0 +1,58 @@
package redir
import (
"net"
"syscall"
"unsafe"
"github.com/riobard/go-shadowsocks2/socks"
)
func parserPacket(c net.Conn) (socks.Addr, error) {
const (
PfInout = 0
PfIn = 1
PfOut = 2
IOCOut = 0x40000000
IOCIn = 0x80000000
IOCInOut = IOCIn | IOCOut
IOCPARMMask = 0x1FFF
LEN = 4*16 + 4*4 + 4*1
// #define _IOC(inout,group,num,len) (inout | ((len & IOCPARMMask) << 16) | ((group) << 8) | (num))
// #define _IOWR(g,n,t) _IOC(IOCInOut, (g), (n), sizeof(t))
// #define DIOCNATLOOK _IOWR('D', 23, struct pfioc_natlook)
DIOCNATLOOK = IOCInOut | ((LEN & IOCPARMMask) << 16) | ('D' << 8) | 23
)
fd, err := syscall.Open("/dev/pf", 0, syscall.O_RDONLY)
if err != nil {
return nil, err
}
defer syscall.Close(fd)
nl := struct { // struct pfioc_natlook
saddr, daddr, rsaddr, rdaddr [16]byte
sxport, dxport, rsxport, rdxport [4]byte
af, proto, protoVariant, direction uint8
}{
af: syscall.AF_INET,
proto: syscall.IPPROTO_TCP,
direction: PfOut,
}
saddr := c.RemoteAddr().(*net.TCPAddr)
daddr := c.LocalAddr().(*net.TCPAddr)
copy(nl.saddr[:], saddr.IP)
copy(nl.daddr[:], daddr.IP)
nl.sxport[0], nl.sxport[1] = byte(saddr.Port>>8), byte(saddr.Port)
nl.dxport[0], nl.dxport[1] = byte(daddr.Port>>8), byte(daddr.Port)
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), DIOCNATLOOK, uintptr(unsafe.Pointer(&nl))); errno != 0 {
return nil, errno
}
addr := make([]byte, 1+net.IPv4len+2)
addr[0] = socks.AtypIPv4
copy(addr[1:1+net.IPv4len], nl.rdaddr[:4])
copy(addr[1+net.IPv4len:], nl.rdxport[:2])
return addr, nil
}

51
proxy/redir/tcp_linux.go Normal file
View File

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

View File

@ -0,0 +1,17 @@
package redir
import (
"syscall"
"unsafe"
)
const GETSOCKOPT = 15 // https://golang.org/src/syscall/syscall_linux_386.go#L183
func socketcall(call, a0, a1, a2, a3, a4, a5 uintptr) error {
var a [6]uintptr
a[0], a[1], a[2], a[3], a[4], a[5] = a0, a1, a2, a3, a4, a5
if _, _, errno := syscall.Syscall6(syscall.SYS_SOCKETCALL, call, uintptr(unsafe.Pointer(&a)), 0, 0, 0, 0); errno != 0 {
return errno
}
return nil
}

View File

@ -0,0 +1,14 @@
// +build linux,!386
package redir
import "syscall"
const GETSOCKOPT = syscall.SYS_GETSOCKOPT
func socketcall(call, a0, a1, a2, a3, a4, a5 uintptr) error {
if _, _, errno := syscall.Syscall6(call, a0, a1, a2, a3, a4, a5); errno != 0 {
return errno
}
return nil
}

View File

@ -0,0 +1,12 @@
package redir
import (
"errors"
"net"
"github.com/riobard/go-shadowsocks2/socks"
)
func parserPacket(conn net.Conn) (socks.Addr, error) {
return nil, errors.New("Windows not support yet")
}

View File

@ -1,10 +1,9 @@
package socks
import (
"io"
"net"
"strconv"
"github.com/Dreamacro/clash/adapters/local"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/tunnel"
@ -13,7 +12,7 @@ import (
)
var (
tun = tunnel.GetInstance()
tun = tunnel.Instance()
)
func NewSocksProxy(addr string) (*C.ProxySignal, error) {
@ -60,59 +59,5 @@ func handleSocks(conn net.Conn) {
return
}
conn.(*net.TCPConn).SetKeepAlive(true)
tun.Add(NewSocks(target, conn))
}
type SocksAdapter struct {
conn net.Conn
addr *C.Addr
}
func (s *SocksAdapter) Close() {
s.conn.Close()
}
func (s *SocksAdapter) Addr() *C.Addr {
return s.addr
}
func (s *SocksAdapter) Connect(proxy C.ProxyAdapter) {
go io.Copy(s.conn, proxy.ReadWriter())
io.Copy(proxy.ReadWriter(), s.conn)
}
func parseSocksAddr(target socks.Addr) *C.Addr {
var host, port string
var ip net.IP
switch target[0] {
case socks.AtypDomainName:
host = string(target[2 : 2+target[1]])
port = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
ipAddr, err := net.ResolveIPAddr("ip", host)
if err == nil {
ip = ipAddr.IP
}
case socks.AtypIPv4:
ip = net.IP(target[1 : 1+net.IPv4len])
port = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
case socks.AtypIPv6:
ip = net.IP(target[1 : 1+net.IPv6len])
port = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
}
return &C.Addr{
NetWork: C.TCP,
AddrType: int(target[0]),
Host: host,
IP: &ip,
Port: port,
}
}
func NewSocks(target socks.Addr, conn net.Conn) *SocksAdapter {
return &SocksAdapter{
conn: conn,
addr: parseSocksAddr(target),
}
tun.Add(adapters.NewSocks(target, conn))
}

View File

@ -1,21 +1,18 @@
package rules
import (
"sync"
C "github.com/Dreamacro/clash/constant"
"github.com/oschwald/geoip2-golang"
log "github.com/sirupsen/logrus"
)
var mmdb *geoip2.Reader
func init() {
var err error
mmdb, err = geoip2.Open(C.MMDBPath)
if err != nil {
log.Fatalf("Can't load mmdb: %s", err.Error())
}
}
var (
mmdb *geoip2.Reader
once sync.Once
)
type GEOIP struct {
country string
@ -43,6 +40,13 @@ func (g *GEOIP) Payload() string {
}
func NewGEOIP(country string, adapter string) *GEOIP {
once.Do(func() {
var err error
mmdb, err = geoip2.Open(C.MMDBPath)
if err != nil {
log.Fatalf("Can't load mmdb: %s", err.Error())
}
})
return &GEOIP{
country: country,
adapter: adapter,

29
tunnel/connection.go Normal file
View File

@ -0,0 +1,29 @@
package tunnel
import (
"io"
"github.com/Dreamacro/clash/adapters/local"
C "github.com/Dreamacro/clash/constant"
)
func (t *Tunnel) handleHTTP(request *adapters.HTTPAdapter, proxy C.ProxyAdapter) {
conn := newTrafficTrack(proxy.Conn(), t.traffic)
// Before we unwrap src and/or dst, copy any buffered data.
if wc, ok := request.Conn().(*adapters.PeekedConn); ok && len(wc.Peeked) > 0 {
if _, err := conn.Write(wc.Peeked); err != nil {
return
}
wc.Peeked = nil
}
go io.Copy(request.Conn(), conn)
io.Copy(conn, request.Conn())
}
func (t *Tunnel) handleSOCKS(request *adapters.SocksAdapter, proxy C.ProxyAdapter) {
conn := newTrafficTrack(proxy.Conn(), t.traffic)
go io.Copy(request.Conn(), conn)
io.Copy(conn, request.Conn())
}

View File

@ -3,47 +3,29 @@ package tunnel
import (
"fmt"
C "github.com/Dreamacro/clash/constant"
log "github.com/sirupsen/logrus"
)
const (
ERROR LogLevel = iota
WARNING
INFO
DEBUG
)
type LogLevel int
type Log struct {
LogLevel LogLevel
LogLevel C.LogLevel
Payload string
}
func (l *Log) Type() string {
switch l.LogLevel {
case INFO:
return "Info"
case WARNING:
return "Warning"
case ERROR:
return "Error"
case DEBUG:
return "Debug"
default:
return "Unknow"
}
return l.LogLevel.String()
}
func print(data Log) {
switch data.LogLevel {
case INFO:
case C.INFO:
log.Infoln(data.Payload)
case WARNING:
case C.WARNING:
log.Warnln(data.Payload)
case ERROR:
case C.ERROR:
log.Errorln(data.Payload)
case DEBUG:
case C.DEBUG:
log.Debugln(data.Payload)
}
}
@ -55,11 +37,13 @@ func (t *Tunnel) subscribeLogs() {
}
for elm := range sub {
data := elm.(Log)
print(data)
if data.LogLevel <= t.logLevel {
print(data)
}
}
}
func newLog(logLevel LogLevel, format string, v ...interface{}) Log {
func newLog(logLevel C.LogLevel, format string, v ...interface{}) Log {
return Log{
LogLevel: logLevel,
Payload: fmt.Sprintf(format, v...),

View File

@ -1,16 +1,13 @@
package tunnel
import (
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/Dreamacro/clash/adapters"
LocalAdapter "github.com/Dreamacro/clash/adapters/local"
cfg "github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/observable"
R "github.com/Dreamacro/clash/rules"
"gopkg.in/eapache/channels.v1"
)
@ -20,168 +17,60 @@ var (
once sync.Once
)
// Tunnel handle proxy socket and HTTP/SOCKS socket
type Tunnel struct {
queue *channels.InfiniteChannel
rules []C.Rule
proxys map[string]C.Proxy
observable *observable.Observable
logCh chan interface{}
proxies map[string]C.Proxy
configLock *sync.RWMutex
traffic *C.Traffic
mode Mode
selector *adapters.Selector
// Outbound Rule
mode cfg.Mode
// Log
logCh chan interface{}
observable *observable.Observable
logLevel C.LogLevel
}
// Add request to queue
func (t *Tunnel) Add(req C.ServerAdapter) {
t.queue.In() <- req
}
// Traffic return traffic of all connections
func (t *Tunnel) Traffic() *C.Traffic {
return t.traffic
}
func (t *Tunnel) Config() ([]C.Rule, map[string]C.Proxy) {
return t.rules, t.proxys
}
// Log return clash log stream
func (t *Tunnel) Log() *observable.Observable {
return t.observable
}
func (t *Tunnel) SetMode(mode Mode) {
t.mode = mode
}
func (t *Tunnel) GetMode() Mode {
return t.mode
}
func (t *Tunnel) UpdateConfig() (err error) {
cfg, err := C.GetConfig()
if err != nil {
return
}
// empty proxys and rules
proxys := make(map[string]C.Proxy)
rules := []C.Rule{}
proxysConfig := cfg.Section("Proxy")
rulesConfig := cfg.Section("Rule")
groupsConfig := cfg.Section("Proxy Group")
// parse proxy
for _, key := range proxysConfig.Keys() {
proxy := key.Strings(",")
if len(proxy) == 0 {
continue
}
switch proxy[0] {
// ss, server, port, cipter, password
case "ss":
if len(proxy) < 5 {
continue
}
ssURL := fmt.Sprintf("ss://%s:%s@%s:%s", proxy[3], proxy[4], proxy[1], proxy[2])
ss, err := adapters.NewShadowSocks(key.Name(), ssURL, t.traffic)
if err != nil {
return err
}
proxys[key.Name()] = ss
func (t *Tunnel) configMonitor(signal chan<- struct{}) {
sub := cfg.Instance().Subscribe()
signal <- struct{}{}
for elm := range sub {
event := elm.(*cfg.Event)
switch event.Type {
case "proxies":
proxies := event.Payload.(map[string]C.Proxy)
t.configLock.Lock()
t.proxies = proxies
t.configLock.Unlock()
case "rules":
rules := event.Payload.([]C.Rule)
t.configLock.Lock()
t.rules = rules
t.configLock.Unlock()
case "mode":
t.mode = event.Payload.(cfg.Mode)
case "log-level":
t.logLevel = event.Payload.(C.LogLevel)
}
}
// parse rules
for _, key := range rulesConfig.Keys() {
rule := strings.Split(key.Name(), ",")
if len(rule) < 3 {
continue
}
rule = trimArr(rule)
switch rule[0] {
case "DOMAIN-SUFFIX":
rules = append(rules, R.NewDomainSuffix(rule[1], rule[2]))
case "DOMAIN-KEYWORD":
rules = append(rules, R.NewDomainKeyword(rule[1], rule[2]))
case "GEOIP":
rules = append(rules, R.NewGEOIP(rule[1], rule[2]))
case "IP-CIDR", "IP-CIDR6":
rules = append(rules, R.NewIPCIDR(rule[1], rule[2]))
case "FINAL":
rules = append(rules, R.NewFinal(rule[2]))
}
}
// parse proxy groups
for _, key := range groupsConfig.Keys() {
rule := strings.Split(key.Value(), ",")
rule = trimArr(rule)
switch rule[0] {
case "url-test":
if len(rule) < 4 {
return fmt.Errorf("URLTest need more than 4 param")
}
proxyNames := rule[1 : len(rule)-2]
delay, _ := strconv.Atoi(rule[len(rule)-1])
url := rule[len(rule)-2]
var ps []C.Proxy
for _, name := range proxyNames {
if p, ok := proxys[name]; ok {
ps = append(ps, p)
}
}
adapter, err := adapters.NewURLTest(key.Name(), ps, url, time.Duration(delay)*time.Second)
if err != nil {
return fmt.Errorf("Config error: %s", err.Error())
}
proxys[key.Name()] = adapter
case "select":
if len(rule) < 3 {
return fmt.Errorf("Selector need more than 3 param")
}
proxyNames := rule[1:]
selectProxy := make(map[string]C.Proxy)
for _, name := range proxyNames {
proxy, exist := proxys[name]
if !exist {
return fmt.Errorf("Proxy %s not exist", name)
}
selectProxy[name] = proxy
}
selector, err := adapters.NewSelector(key.Name(), selectProxy)
if err != nil {
return fmt.Errorf("Selector create error: %s", err.Error())
}
proxys[key.Name()] = selector
}
}
// init proxy
proxys["DIRECT"] = adapters.NewDirect(t.traffic)
proxys["REJECT"] = adapters.NewReject()
t.configLock.Lock()
defer t.configLock.Unlock()
// stop url-test
for _, elm := range t.proxys {
urlTest, ok := elm.(*adapters.URLTest)
if ok {
urlTest.Close()
}
}
s, err := adapters.NewSelector("Proxy", proxys)
if err != nil {
return err
}
t.proxys = proxys
t.rules = rules
t.selector = s
return nil
}
func (t *Tunnel) process() {
@ -199,22 +88,27 @@ func (t *Tunnel) handleConn(localConn C.ServerAdapter) {
var proxy C.Proxy
switch t.mode {
case Direct:
proxy = t.proxys["DIRECT"]
case Global:
proxy = t.selector
case cfg.Direct:
proxy = t.proxies["DIRECT"]
case cfg.Global:
proxy = t.proxies["GLOBAL"]
// Rule
default:
proxy = t.match(addr)
}
remoConn, err := proxy.Generator(addr)
if err != nil {
t.logCh <- newLog(WARNING, "Proxy connect error: %s", err.Error())
t.logCh <- newLog(C.WARNING, "Proxy connect error: %s", err.Error())
return
}
defer remoConn.Close()
localConn.Connect(remoConn)
switch adapter := localConn.(type) {
case *LocalAdapter.HTTPAdapter:
t.handleHTTP(adapter, remoConn)
case *LocalAdapter.SocksAdapter:
t.handleSOCKS(adapter, remoConn)
}
}
func (t *Tunnel) match(addr *C.Addr) C.Proxy {
@ -223,35 +117,43 @@ func (t *Tunnel) match(addr *C.Addr) C.Proxy {
for _, rule := range t.rules {
if rule.IsMatch(addr) {
a, ok := t.proxys[rule.Adapter()]
a, ok := t.proxies[rule.Adapter()]
if !ok {
continue
}
t.logCh <- newLog(INFO, "%v match %s using %s", addr.String(), rule.RuleType().String(), rule.Adapter())
t.logCh <- newLog(C.INFO, "%v match %s using %s", addr.String(), rule.RuleType().String(), rule.Adapter())
return a
}
}
t.logCh <- newLog(INFO, "%v doesn't match any rule using DIRECT", addr.String())
return t.proxys["DIRECT"]
t.logCh <- newLog(C.INFO, "%v doesn't match any rule using DIRECT", addr.String())
return t.proxies["DIRECT"]
}
// Run initial task
func (t *Tunnel) Run() {
go t.process()
go t.subscribeLogs()
signal := make(chan struct{})
go t.configMonitor(signal)
<-signal
}
func newTunnel() *Tunnel {
logCh := make(chan interface{})
tunnel := &Tunnel{
return &Tunnel{
queue: channels.NewInfiniteChannel(),
proxys: make(map[string]C.Proxy),
proxies: make(map[string]C.Proxy),
observable: observable.NewObservable(logCh),
logCh: logCh,
configLock: &sync.RWMutex{},
traffic: C.NewTraffic(time.Second),
mode: Rule,
mode: cfg.Rule,
logLevel: C.INFO,
}
go tunnel.process()
go tunnel.subscribeLogs()
return tunnel
}
func GetInstance() *Tunnel {
// Instance return singleton instance of Tunnel
func Instance() *Tunnel {
once.Do(func() {
tunnel = newTunnel()
})

View File

@ -1,4 +1,4 @@
package adapters
package tunnel
import (
"net"
@ -6,6 +6,7 @@ import (
C "github.com/Dreamacro/clash/constant"
)
// TrafficTrack record traffic of net.Conn
type TrafficTrack struct {
net.Conn
traffic *C.Traffic
@ -23,6 +24,6 @@ func (tt *TrafficTrack) Write(b []byte) (int, error) {
return n, err
}
func NewTrafficTrack(conn net.Conn, traffic *C.Traffic) *TrafficTrack {
func newTrafficTrack(conn net.Conn, traffic *C.Traffic) *TrafficTrack {
return &TrafficTrack{traffic: traffic, Conn: conn}
}

View File

@ -1,12 +0,0 @@
package tunnel
import (
"strings"
)
func trimArr(arr []string) (r []string) {
for _, e := range arr {
r = append(r, strings.Trim(e, " "))
}
return
}