Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
79e5338113 | |||
bc4ca2818b | |||
46b8e2eec1 | |||
cd9f496fc9 | |||
ef301af04f | |||
6ffbb7c89e | |||
961250f998 |
@ -12,11 +12,7 @@ env:
|
|||||||
- BINDIR=bin
|
- BINDIR=bin
|
||||||
script:
|
script:
|
||||||
- go test
|
- go test
|
||||||
- GOARCH=amd64 GOOS=linux go build -o $BINDIR/$NAME-linux
|
before_deploy: make -j releases
|
||||||
- GOARCH=amd64 GOOS=darwin go build -o $BINDIR/$NAME-macos
|
|
||||||
- chmod +x $BINDIR/$NAME-*
|
|
||||||
- gzip $BINDIR/$NAME-linux
|
|
||||||
- gzip $BINDIR/$NAME-macos
|
|
||||||
deploy:
|
deploy:
|
||||||
provider: releases
|
provider: releases
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
FROM golang:latest as builder
|
||||||
|
RUN wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz -O /tmp/GeoLite2-Country.tar.gz && \
|
||||||
|
tar zxvf /tmp/GeoLite2-Country.tar.gz -C /tmp && \
|
||||||
|
cp /tmp/GeoLite2-Country_*/GeoLite2-Country.mmdb /Country.mmdb
|
||||||
|
RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh && \
|
||||||
|
mkdir -p /go/src/github.com/Dreamacro/clash
|
||||||
|
WORKDIR /go/src/github.com/Dreamacro/clash
|
||||||
|
COPY . /go/src/github.com/Dreamacro/clash
|
||||||
|
RUN dep ensure && \
|
||||||
|
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o /clash && \
|
||||||
|
chmod +x /clash
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
RUN apk --no-cache add ca-certificates && \
|
||||||
|
mkdir -p /root/.config/clash
|
||||||
|
COPY --from=builder /clash .
|
||||||
|
COPY --from=builder /Country.mmdb /root/.config/clash/
|
||||||
|
EXPOSE 7890 7891
|
||||||
|
ENTRYPOINT ["/clash"]
|
5
Gopkg.lock
generated
5
Gopkg.lock
generated
@ -49,10 +49,11 @@
|
|||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"hkdf",
|
"hkdf",
|
||||||
"internal/chacha20",
|
"internal/chacha20",
|
||||||
|
"internal/subtle",
|
||||||
"poly1305",
|
"poly1305",
|
||||||
"ssh/terminal"
|
"ssh/terminal"
|
||||||
]
|
]
|
||||||
revision = "8ac0e0d97ce45cd83d1d7243c060cb8461dda5e9"
|
revision = "a8fb68e7206f8c78be19b432c58eb52a6aa34462"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
@ -62,7 +63,7 @@
|
|||||||
"unix",
|
"unix",
|
||||||
"windows"
|
"windows"
|
||||||
]
|
]
|
||||||
revision = "9527bec2660bd847c050fda93a0f0c6dee0800bb"
|
revision = "6c888cc515d3ed83fc103cf1d84468aad274b0a7"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "gopkg.in/eapache/channels.v1"
|
name = "gopkg.in/eapache/channels.v1"
|
||||||
|
19
Makefile
Normal file
19
Makefile
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
NAME=clash
|
||||||
|
BINDIR=bin
|
||||||
|
GOBUILD=CGO_ENABLED=0 go build -ldflags '-w -s'
|
||||||
|
|
||||||
|
all: linux macos
|
||||||
|
|
||||||
|
linux:
|
||||||
|
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
|
macos:
|
||||||
|
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
|
releases: linux macos
|
||||||
|
chmod +x $(BINDIR)/$(NAME)-*
|
||||||
|
gzip $(BINDIR)/$(NAME)-linux
|
||||||
|
gzip $(BINDIR)/$(NAME)-macos
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm $(BINDIR)/*
|
19
README.md
19
README.md
@ -1,5 +1,7 @@
|
|||||||
# Clash
|
# Clash
|
||||||
|
|
||||||
|
[](https://travis-ci.org/Dreamacro/clash)
|
||||||
|
|
||||||
A rule based proxy in Go.
|
A rule based proxy in Go.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@ -26,12 +28,16 @@ Unfortunately, there is no native elegant way to implement golang's daemon.
|
|||||||
|
|
||||||
So we can use third-party daemon tools like pm2, supervisor, and so on.
|
So we can use third-party daemon tools like pm2, supervisor, and so on.
|
||||||
|
|
||||||
In the case of pm2, we can start the daemon this way:
|
In the case of [pm2](https://github.com/Unitech/pm2), we can start the daemon this way:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pm2 start clash
|
pm2 start clash
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you have Docker installed, you can run clash directly using `docker-compose`.
|
||||||
|
|
||||||
|
[Run clash in docker](https://github.com/Dreamacro/clash/wiki/Run-clash-in-docker)
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|
||||||
Configuration file at `$HOME/.config/clash/config.ini`
|
Configuration file at `$HOME/.config/clash/config.ini`
|
||||||
@ -43,11 +49,20 @@ Below is a simple demo configuration file:
|
|||||||
port = 7890
|
port = 7890
|
||||||
socks-port = 7891
|
socks-port = 7891
|
||||||
|
|
||||||
|
# A RESTful API for clash
|
||||||
|
external-controller = 127.0.0.1:8080
|
||||||
|
|
||||||
[Proxy]
|
[Proxy]
|
||||||
# name = ss, server, port, cipher, password
|
# name = ss, server, port, cipher, password
|
||||||
# The types of cipher are consistent with go-shadowsocks2
|
# 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
|
# 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
|
||||||
Proxy = ss, server, port, AEAD_CHACHA20_POLY1305, password
|
Proxy1 = ss, server1, port, AEAD_CHACHA20_POLY1305, password
|
||||||
|
Proxy2 = ss, server2, port, AEAD_CHACHA20_POLY1305, password
|
||||||
|
|
||||||
|
[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
|
||||||
|
|
||||||
[Rule]
|
[Rule]
|
||||||
DOMAIN-SUFFIX,google.com,Proxy
|
DOMAIN-SUFFIX,google.com,Proxy
|
||||||
|
@ -22,12 +22,17 @@ func (d *DirectAdapter) Close() {
|
|||||||
d.conn.Close()
|
d.conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close is used to close connection
|
// Conn is used to http request
|
||||||
func (d *DirectAdapter) Conn() net.Conn {
|
func (d *DirectAdapter) Conn() net.Conn {
|
||||||
return d.conn
|
return d.conn
|
||||||
}
|
}
|
||||||
|
|
||||||
type Direct struct {
|
type Direct struct {
|
||||||
|
traffic *C.Traffic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Direct) Name() string {
|
||||||
|
return "Direct"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Direct) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
|
func (d *Direct) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
|
||||||
@ -36,9 +41,9 @@ func (d *Direct) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.(*net.TCPConn).SetKeepAlive(true)
|
c.(*net.TCPConn).SetKeepAlive(true)
|
||||||
return &DirectAdapter{conn: c}, nil
|
return &DirectAdapter{conn: NewTrafficTrack(c, d.traffic)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDirect() *Direct {
|
func NewDirect(traffic *C.Traffic) *Direct {
|
||||||
return &Direct{}
|
return &Direct{traffic: traffic}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ func (r *RejectAdapter) ReadWriter() io.ReadWriter {
|
|||||||
// Close is used to close connection
|
// Close is used to close connection
|
||||||
func (r *RejectAdapter) Close() {}
|
func (r *RejectAdapter) Close() {}
|
||||||
|
|
||||||
// Close is used to close connection
|
// Conn is used to http request
|
||||||
func (r *RejectAdapter) Conn() net.Conn {
|
func (r *RejectAdapter) Conn() net.Conn {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -27,6 +27,10 @@ func (r *RejectAdapter) Conn() net.Conn {
|
|||||||
type Reject struct {
|
type Reject struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Reject) Name() string {
|
||||||
|
return "Reject"
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Reject) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
|
func (r *Reject) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
|
||||||
return &RejectAdapter{}, nil
|
return &RejectAdapter{}, nil
|
||||||
}
|
}
|
||||||
|
@ -34,31 +34,40 @@ func (ss *ShadowsocksAdapter) Conn() net.Conn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ShadowSocks struct {
|
type ShadowSocks struct {
|
||||||
server string
|
server string
|
||||||
cipher string
|
name string
|
||||||
password string
|
cipher core.Cipher
|
||||||
|
traffic *C.Traffic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *ShadowSocks) Name() string {
|
||||||
|
return ss.name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *ShadowSocks) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
|
func (ss *ShadowSocks) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
|
||||||
var key []byte
|
|
||||||
ciph, _ := core.PickCipher(ss.cipher, key, ss.password)
|
|
||||||
c, err := net.Dial("tcp", ss.server)
|
c, err := net.Dial("tcp", ss.server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error", ss.server)
|
return nil, fmt.Errorf("%s connect error", ss.server)
|
||||||
}
|
}
|
||||||
c.(*net.TCPConn).SetKeepAlive(true)
|
c.(*net.TCPConn).SetKeepAlive(true)
|
||||||
c = ciph.StreamConn(c)
|
c = ss.cipher.StreamConn(c)
|
||||||
_, err = c.Write(serializesSocksAddr(addr))
|
_, err = c.Write(serializesSocksAddr(addr))
|
||||||
return &ShadowsocksAdapter{conn: c}, err
|
return &ShadowsocksAdapter{conn: NewTrafficTrack(c, ss.traffic)}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewShadowSocks(ssURL string) *ShadowSocks {
|
func NewShadowSocks(name string, ssURL string, traffic *C.Traffic) (*ShadowSocks, error) {
|
||||||
|
var key []byte
|
||||||
server, cipher, password, _ := parseURL(ssURL)
|
server, cipher, password, _ := parseURL(ssURL)
|
||||||
return &ShadowSocks{
|
ciph, err := core.PickCipher(cipher, key, password)
|
||||||
server: server,
|
if err != nil {
|
||||||
cipher: cipher,
|
return nil, fmt.Errorf("ss %s initialize error: %s", server, err.Error())
|
||||||
password: password,
|
|
||||||
}
|
}
|
||||||
|
return &ShadowSocks{
|
||||||
|
server: server,
|
||||||
|
name: name,
|
||||||
|
cipher: ciph,
|
||||||
|
traffic: traffic,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseURL(s string) (addr, cipher, password string, err error) {
|
func parseURL(s string) (addr, cipher, password string, err error) {
|
||||||
|
148
adapters/urltest.go
Normal file
148
adapters/urltest.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *URLTest) Name() string {
|
||||||
|
return u.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *URLTest) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
|
||||||
|
return u.fast.Generator(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *URLTest) loop() {
|
||||||
|
tick := time.Tick(u.delay)
|
||||||
|
go u.speedTest()
|
||||||
|
for range tick {
|
||||||
|
go u.speedTest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
go urlTest.loop()
|
||||||
|
return urlTest, nil
|
||||||
|
}
|
28
adapters/util.go
Normal file
28
adapters/util.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package adapters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TrafficTrack struct {
|
||||||
|
net.Conn
|
||||||
|
traffic *C.Traffic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tt *TrafficTrack) Read(b []byte) (int, error) {
|
||||||
|
n, err := tt.Conn.Read(b)
|
||||||
|
tt.traffic.Down() <- int64(n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tt *TrafficTrack) Write(b []byte) (int, error) {
|
||||||
|
n, err := tt.Conn.Write(b)
|
||||||
|
tt.traffic.Up() <- int64(n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTrafficTrack(conn net.Conn, traffic *C.Traffic) *TrafficTrack {
|
||||||
|
return &TrafficTrack{traffic: traffic, Conn: conn}
|
||||||
|
}
|
@ -18,5 +18,6 @@ type ServerAdapter interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Proxy interface {
|
type Proxy interface {
|
||||||
|
Name() string
|
||||||
Generator(addr *Addr) (ProxyAdapter, error)
|
Generator(addr *Addr) (ProxyAdapter, error)
|
||||||
}
|
}
|
||||||
|
@ -29,9 +29,14 @@ var (
|
|||||||
func init() {
|
func init() {
|
||||||
currentUser, err := user.Current()
|
currentUser, err := user.Current()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Can't get current user: %s", err.Error())
|
dir := os.Getenv("HOME")
|
||||||
|
if dir == "" {
|
||||||
|
log.Fatalf("Can't get current user: %s", err.Error())
|
||||||
|
}
|
||||||
|
HomeDir = dir
|
||||||
|
} else {
|
||||||
|
HomeDir = currentUser.HomeDir
|
||||||
}
|
}
|
||||||
HomeDir = currentUser.HomeDir
|
|
||||||
|
|
||||||
dirPath := path.Join(HomeDir, ".config", Name)
|
dirPath := path.Join(HomeDir, ".config", Name)
|
||||||
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
|
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
|
||||||
|
55
constant/traffic.go
Normal file
55
constant/traffic.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Traffic struct {
|
||||||
|
up chan int64
|
||||||
|
down chan int64
|
||||||
|
upCount int64
|
||||||
|
downCount int64
|
||||||
|
upTotal int64
|
||||||
|
downTotal int64
|
||||||
|
interval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Traffic) Up() chan<- int64 {
|
||||||
|
return t.up
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Traffic) Down() chan<- int64 {
|
||||||
|
return t.down
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Traffic) Now() (up int64, down int64) {
|
||||||
|
return t.upTotal, t.downTotal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Traffic) handle() {
|
||||||
|
go t.handleCh(t.up, &t.upCount, &t.upTotal)
|
||||||
|
go t.handleCh(t.down, &t.downCount, &t.downTotal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Traffic) handleCh(ch <-chan int64, count *int64, total *int64) {
|
||||||
|
ticker := time.NewTicker(t.interval)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case n := <-ch:
|
||||||
|
*count += n
|
||||||
|
case <-ticker.C:
|
||||||
|
*total = *count
|
||||||
|
*count = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTraffic(interval time.Duration) *Traffic {
|
||||||
|
t := &Traffic{
|
||||||
|
up: make(chan int64),
|
||||||
|
down: make(chan int64),
|
||||||
|
interval: interval,
|
||||||
|
}
|
||||||
|
go t.handle()
|
||||||
|
return t
|
||||||
|
}
|
83
hub/server.go
Normal file
83
hub/server.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package hub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/tunnel"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
"github.com/go-chi/render"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
tun = tunnel.GetInstance()
|
||||||
|
)
|
||||||
|
|
||||||
|
type Traffic struct {
|
||||||
|
Up int64 `json:"up"`
|
||||||
|
Down int64 `json:"down"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Log struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Payload string `json:"payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHub(addr string) {
|
||||||
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
r.Get("/traffic", traffic)
|
||||||
|
r.Get("/logs", getLogs)
|
||||||
|
|
||||||
|
err := http.ListenAndServe(addr, r)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("External controller error: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func traffic(w http.ResponseWriter, r *http.Request) {
|
||||||
|
render.Status(r, http.StatusOK)
|
||||||
|
|
||||||
|
tick := time.NewTicker(time.Second)
|
||||||
|
t := tun.Traffic()
|
||||||
|
for range tick.C {
|
||||||
|
up, down := t.Now()
|
||||||
|
if err := json.NewEncoder(w).Encode(Traffic{
|
||||||
|
Up: up,
|
||||||
|
Down: down,
|
||||||
|
}); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
src := tun.Log()
|
||||||
|
sub, err := src.Subscribe()
|
||||||
|
defer src.UnSubscribe(sub)
|
||||||
|
if err != nil {
|
||||||
|
render.Status(r, http.StatusInternalServerError)
|
||||||
|
render.JSON(w, r, Error{
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
render.Status(r, http.StatusOK)
|
||||||
|
for elm := range sub {
|
||||||
|
log := elm.(tunnel.Log)
|
||||||
|
if err := json.NewEncoder(w).Encode(Log{
|
||||||
|
Type: log.Type(),
|
||||||
|
Payload: log.Payload,
|
||||||
|
}); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
}
|
||||||
|
}
|
6
main.go
6
main.go
@ -6,6 +6,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/hub"
|
||||||
"github.com/Dreamacro/clash/proxy/http"
|
"github.com/Dreamacro/clash/proxy/http"
|
||||||
"github.com/Dreamacro/clash/proxy/socks"
|
"github.com/Dreamacro/clash/proxy/socks"
|
||||||
"github.com/Dreamacro/clash/tunnel"
|
"github.com/Dreamacro/clash/tunnel"
|
||||||
@ -37,6 +38,11 @@ func main() {
|
|||||||
go http.NewHttpProxy(port)
|
go http.NewHttpProxy(port)
|
||||||
go socks.NewSocksProxy(socksPort)
|
go socks.NewSocksProxy(socksPort)
|
||||||
|
|
||||||
|
// Hub
|
||||||
|
if key, err := section.GetKey("external-controller"); err == nil {
|
||||||
|
go hub.NewHub(key.Value())
|
||||||
|
}
|
||||||
|
|
||||||
sigCh := make(chan os.Signal, 1)
|
sigCh := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-sigCh
|
<-sigCh
|
||||||
|
@ -20,6 +20,21 @@ type Log struct {
|
|||||||
Payload string
|
Payload string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Log) Type() string {
|
||||||
|
switch l.LogType {
|
||||||
|
case INFO:
|
||||||
|
return "Info"
|
||||||
|
case WARNING:
|
||||||
|
return "Warning"
|
||||||
|
case ERROR:
|
||||||
|
return "Error"
|
||||||
|
case DEBUG:
|
||||||
|
return "Debug"
|
||||||
|
default:
|
||||||
|
return "Unknow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func print(data Log) {
|
func print(data Log) {
|
||||||
switch data.LogType {
|
switch data.LogType {
|
||||||
case INFO:
|
case INFO:
|
||||||
|
@ -2,8 +2,10 @@ package tunnel
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapters"
|
"github.com/Dreamacro/clash/adapters"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
@ -25,30 +27,37 @@ type Tunnel struct {
|
|||||||
observable *observable.Observable
|
observable *observable.Observable
|
||||||
logCh chan interface{}
|
logCh chan interface{}
|
||||||
configLock *sync.RWMutex
|
configLock *sync.RWMutex
|
||||||
|
traffic *C.Traffic
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tunnel) Add(req C.ServerAdapter) {
|
func (t *Tunnel) Add(req C.ServerAdapter) {
|
||||||
t.queue.In() <- req
|
t.queue.In() <- req
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tunnel) UpdateConfig() (err error) {
|
func (t *Tunnel) Traffic() *C.Traffic {
|
||||||
t.configLock.Lock()
|
return t.traffic
|
||||||
defer t.configLock.Unlock()
|
}
|
||||||
|
|
||||||
|
func (t *Tunnel) Log() *observable.Observable {
|
||||||
|
return t.observable
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tunnel) UpdateConfig() (err error) {
|
||||||
cfg, err := C.GetConfig()
|
cfg, err := C.GetConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear proxys and rules
|
// clear proxys and rules
|
||||||
t.proxys = make(map[string]C.Proxy)
|
proxys := make(map[string]C.Proxy)
|
||||||
t.rules = []C.Rule{}
|
rules := []C.Rule{}
|
||||||
|
|
||||||
proxys := cfg.Section("Proxy")
|
proxysConfig := cfg.Section("Proxy")
|
||||||
rules := cfg.Section("Rule")
|
rulesConfig := cfg.Section("Rule")
|
||||||
|
groupsConfig := cfg.Section("Proxy Group")
|
||||||
|
|
||||||
// parse proxy
|
// parse proxy
|
||||||
for _, key := range proxys.Keys() {
|
for _, key := range proxysConfig.Keys() {
|
||||||
proxy := strings.Split(key.Value(), ",")
|
proxy := strings.Split(key.Value(), ",")
|
||||||
if len(proxy) == 0 {
|
if len(proxy) == 0 {
|
||||||
continue
|
continue
|
||||||
@ -61,16 +70,20 @@ func (t *Tunnel) UpdateConfig() (err error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ssURL := fmt.Sprintf("ss://%s:%s@%s:%s", proxy[3], proxy[4], proxy[1], proxy[2])
|
ssURL := fmt.Sprintf("ss://%s:%s@%s:%s", proxy[3], proxy[4], proxy[1], proxy[2])
|
||||||
t.proxys[key.Name()] = adapters.NewShadowSocks(ssURL)
|
ss, err := adapters.NewShadowSocks(key.Name(), ssURL, t.traffic)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
proxys[key.Name()] = ss
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// init proxy
|
// init proxy
|
||||||
t.proxys["DIRECT"] = adapters.NewDirect()
|
proxys["DIRECT"] = adapters.NewDirect(t.traffic)
|
||||||
t.proxys["REJECT"] = adapters.NewReject()
|
proxys["REJECT"] = adapters.NewReject()
|
||||||
|
|
||||||
// parse rules
|
// parse rules
|
||||||
for _, key := range rules.Keys() {
|
for _, key := range rulesConfig.Keys() {
|
||||||
rule := strings.Split(key.Name(), ",")
|
rule := strings.Split(key.Name(), ",")
|
||||||
if len(rule) < 3 {
|
if len(rule) < 3 {
|
||||||
continue
|
continue
|
||||||
@ -78,18 +91,51 @@ func (t *Tunnel) UpdateConfig() (err error) {
|
|||||||
rule = trimArr(rule)
|
rule = trimArr(rule)
|
||||||
switch rule[0] {
|
switch rule[0] {
|
||||||
case "DOMAIN-SUFFIX":
|
case "DOMAIN-SUFFIX":
|
||||||
t.rules = append(t.rules, R.NewDomainSuffix(rule[1], rule[2]))
|
rules = append(rules, R.NewDomainSuffix(rule[1], rule[2]))
|
||||||
case "DOMAIN-KEYWORD":
|
case "DOMAIN-KEYWORD":
|
||||||
t.rules = append(t.rules, R.NewDomainKeyword(rule[1], rule[2]))
|
rules = append(rules, R.NewDomainKeyword(rule[1], rule[2]))
|
||||||
case "GEOIP":
|
case "GEOIP":
|
||||||
t.rules = append(t.rules, R.NewGEOIP(rule[1], rule[2]))
|
rules = append(rules, R.NewGEOIP(rule[1], rule[2]))
|
||||||
case "IP-CIDR", "IP-CIDR6":
|
case "IP-CIDR", "IP-CIDR6":
|
||||||
t.rules = append(t.rules, R.NewIPCIDR(rule[1], rule[2]))
|
rules = append(rules, R.NewIPCIDR(rule[1], rule[2]))
|
||||||
case "FINAL":
|
case "FINAL":
|
||||||
t.rules = append(t.rules, R.NewFinal(rule[2]))
|
rules = append(rules, R.NewFinal(rule[2]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parse proxy groups
|
||||||
|
for _, key := range groupsConfig.Keys() {
|
||||||
|
rule := strings.Split(key.Value(), ",")
|
||||||
|
if len(rule) < 4 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rule = trimArr(rule)
|
||||||
|
switch rule[0] {
|
||||||
|
case "url-test":
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.configLock.Lock()
|
||||||
|
defer t.configLock.Unlock()
|
||||||
|
|
||||||
|
t.proxys = proxys
|
||||||
|
t.rules = rules
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,7 +176,7 @@ func (t *Tunnel) match(addr *C.Addr) C.Proxy {
|
|||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.logCh <- newLog(INFO, "don't find, direct")
|
t.logCh <- newLog(INFO, "%v doesn't match any rule using DIRECT", addr.String())
|
||||||
return t.proxys["DIRECT"]
|
return t.proxys["DIRECT"]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,6 +188,7 @@ func newTunnel() *Tunnel {
|
|||||||
observable: observable.NewObservable(logCh),
|
observable: observable.NewObservable(logCh),
|
||||||
logCh: logCh,
|
logCh: logCh,
|
||||||
configLock: &sync.RWMutex{},
|
configLock: &sync.RWMutex{},
|
||||||
|
traffic: C.NewTraffic(time.Second),
|
||||||
}
|
}
|
||||||
go tunnel.process()
|
go tunnel.process()
|
||||||
go tunnel.subscribeLogs()
|
go tunnel.subscribeLogs()
|
||||||
|
Reference in New Issue
Block a user