Compare commits

...

19 Commits

Author SHA1 Message Date
88c7ccf749 Add: license scan report and status
* Add license scan report and status

Signed-off-by: fossabot <badges@fossa.io>

* Improve: adjust indentation and position
2018-07-15 22:32:20 +08:00
3cacfb8a7f Update: add config route 2018-07-15 22:23:20 +08:00
0eef9bbf5d Add: selector and proxys & rules router 2018-07-12 23:28:38 +08:00
39b45513af Improve: delete useless code and code coverage is now 100% 2018-07-12 18:03:02 +08:00
283e4e1f4f Update: README.md logo and badges 2018-06-23 00:44:28 +08:00
bd0a1ae1a7 Improve: gofmt -s for shadowsocks.go 2018-06-22 23:56:41 +08:00
018a6ba041 Improve: add log level 2018-06-22 13:15:20 +08:00
9c2ace1f91 Fixed: handle chunked data 2018-06-22 10:26:56 +08:00
17224a32cb Add: add windows release 2018-06-20 23:00:16 +08:00
05ab653103 Add: config hub route 2018-06-20 22:41:02 +08:00
c2c8f82f96 Fixed: the configuration can now be updated correctly 2018-06-19 20:31:36 +08:00
330a3391e3 Improve: ci config and Dockerfile & update dependencies 2018-06-18 18:24:11 +08:00
79e5338113 Add: external controller 2018-06-18 11:31:49 +08:00
bc4ca2818b Add: Record network traffic 2018-06-17 22:41:32 +08:00
46b8e2eec1 Update: Dockerfile 2018-06-17 10:00:52 +08:00
cd9f496fc9 Add: Dockerfile and Makefile 2018-06-17 08:18:39 +08:00
ef301af04f Update: add url-test intro & add banner 2018-06-16 21:48:23 +08:00
6ffbb7c89e Add: url-test proxy group support 2018-06-16 21:34:13 +08:00
961250f998 Fixed: throw error when shadowsocks cipher type wrong 2018-06-16 15:41:26 +08:00
40 changed files with 1392 additions and 155 deletions

View File

@ -11,12 +11,8 @@ env:
- NAME=clash
- BINDIR=bin
script:
- go test
- GOARCH=amd64 GOOS=linux go build -o $BINDIR/$NAME-linux
- GOARCH=amd64 GOOS=darwin go build -o $BINDIR/$NAME-macos
- chmod +x $BINDIR/$NAME-*
- gzip $BINDIR/$NAME-linux
- gzip $BINDIR/$NAME-macos
- go test ./...
before_deploy: make -j releases
deploy:
provider: releases
prerelease: true

19
Dockerfile Normal file
View 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 /Country.mmdb /root/.config/clash/
COPY --from=builder /clash .
EXPOSE 7890 7891
ENTRYPOINT ["/clash"]

19
Gopkg.lock generated
View File

@ -13,6 +13,18 @@
revision = "44cc805cf13205b55f69e14bcb69867d1ae92f98"
version = "v1.1.0"
[[projects]]
name = "github.com/go-chi/chi"
packages = ["."]
revision = "e83ac2304db3c50cf03d96a2fcd39009d458bc35"
version = "v3.3.2"
[[projects]]
name = "github.com/go-chi/render"
packages = ["."]
revision = "3215478343fbc559bd3fc08f7031bb134d6bdad5"
version = "v1.0.1"
[[projects]]
name = "github.com/oschwald/geoip2-golang"
packages = ["."]
@ -49,10 +61,11 @@
"chacha20poly1305",
"hkdf",
"internal/chacha20",
"internal/subtle",
"poly1305",
"ssh/terminal"
]
revision = "8ac0e0d97ce45cd83d1d7243c060cb8461dda5e9"
revision = "027cca12c2d63e3d62b670d901e8a2c95854feec"
[[projects]]
branch = "master"
@ -62,7 +75,7 @@
"unix",
"windows"
]
revision = "9527bec2660bd847c050fda93a0f0c6dee0800bb"
revision = "6c888cc515d3ed83fc103cf1d84468aad274b0a7"
[[projects]]
name = "gopkg.in/eapache/channels.v1"
@ -79,6 +92,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "4297c505508c6cdd8c94fd4ef29bc6940c65fea81e125fcf871a316b6e671a71"
inputs-digest = "c3c901e4e393a2df9e421924d3a4d85ec73642e36dcbc1ddca5fc13159220e86"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -25,6 +25,14 @@
# unused-packages = true
[[constraint]]
name = "github.com/go-chi/chi"
version = "3.3.2"
[[constraint]]
name = "github.com/go-chi/render"
version = "1.0.1"
[[constraint]]
name = "github.com/oschwald/geoip2-golang"
version = "1.2.1"

23
Makefile Normal file
View File

@ -0,0 +1,23 @@
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)-$@
win64:
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
releases: linux macos win64
chmod +x $(BINDIR)/$(NAME)-*
gzip $(BINDIR)/$(NAME)-linux
gzip $(BINDIR)/$(NAME)-macos
zip -m -j $(BINDIR)/$(NAME)-win64.zip $(BINDIR)/$(NAME)-win64.exe
clean:
rm $(BINDIR)/*

View File

@ -1,6 +1,25 @@
# Clash
<h1 align="center">
<img src="https://github.com/Dreamacro/clash/raw/master/docs/logo.png" alt="Clash" width="200">
<br>
Clash
<br>
</h1>
A rule based proxy in Go.
<h4 align="center">A rule based proxy in Go.</h4>
<p align="center">
<a href="https://travis-ci.org/Dreamacro/clash">
<img src="https://img.shields.io/travis/Dreamacro/clash.svg?style=flat-square"
alt="Travis-CI">
</a>
<a href="https://goreportcard.com/report/github.com/Dreamacro/clash">
<img src="https://goreportcard.com/badge/github.com/Dreamacro/clash?style=flat-square">
</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>
</p>
## Features
@ -26,12 +45,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.
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
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
Configuration file at `$HOME/.config/clash/config.ini`
@ -43,11 +66,20 @@ Below is a simple demo configuration file:
port = 7890
socks-port = 7891
# A RESTful API for clash
external-controller = 127.0.0.1:8080
[Proxy]
# 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
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]
DOMAIN-SUFFIX,google.com,Proxy
@ -57,6 +89,9 @@ GEOIP,CN,DIRECT
FINAL,,Proxy # note: there is two ","
```
## 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

View File

@ -22,12 +22,21 @@ func (d *DirectAdapter) Close() {
d.conn.Close()
}
// Close is used to close connection
// Conn is used to http request
func (d *DirectAdapter) Conn() net.Conn {
return d.conn
}
type Direct struct {
traffic *C.Traffic
}
func (d *Direct) Name() string {
return "Direct"
}
func (d *Direct) Type() C.AdapterType {
return C.Direct
}
func (d *Direct) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
@ -36,9 +45,9 @@ func (d *Direct) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
return
}
c.(*net.TCPConn).SetKeepAlive(true)
return &DirectAdapter{conn: c}, nil
return &DirectAdapter{conn: NewTrafficTrack(c, d.traffic)}, nil
}
func NewDirect() *Direct {
return &Direct{}
func NewDirect(traffic *C.Traffic) *Direct {
return &Direct{traffic: traffic}
}

View File

@ -19,7 +19,7 @@ func (r *RejectAdapter) ReadWriter() io.ReadWriter {
// Close is used to close connection
func (r *RejectAdapter) Close() {}
// Close is used to close connection
// Conn is used to http request
func (r *RejectAdapter) Conn() net.Conn {
return nil
}
@ -27,6 +27,14 @@ func (r *RejectAdapter) Conn() net.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{}, nil
}

65
adapters/selector.go Normal file
View File

@ -0,0 +1,65 @@
package adapters
import (
"errors"
C "github.com/Dreamacro/clash/constant"
)
type Selector struct {
name string
selected C.Proxy
proxys map[string]C.Proxy
}
func (s *Selector) Name() string {
return s.name
}
func (s *Selector) Type() C.AdapterType {
return C.Selector
}
func (s *Selector) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
return s.selected.Generator(addr)
}
func (s *Selector) Now() string {
return s.selected.Name()
}
func (s *Selector) All() []string {
var all []string
for k, _ := range s.proxys {
all = append(all, k)
}
return all
}
func (s *Selector) Set(name string) error {
proxy, exist := s.proxys[name]
if !exist {
return errors.New("Proxy does not exist")
}
s.selected = proxy
return nil
}
func NewSelector(name string, proxys map[string]C.Proxy) (*Selector, error) {
if len(proxys) == 0 {
return nil, errors.New("Provide at least one proxy")
}
mapping := make(map[string]C.Proxy)
var init string
for k, v := range proxys {
mapping[k] = v
init = k
}
s := &Selector{
name: name,
proxys: mapping,
selected: proxys[init],
}
return s, nil
}

View File

@ -34,31 +34,44 @@ func (ss *ShadowsocksAdapter) Conn() net.Conn {
}
type ShadowSocks struct {
server string
cipher string
password string
server string
name string
cipher core.Cipher
traffic *C.Traffic
}
func (ss *ShadowSocks) Name() string {
return ss.name
}
func (ss *ShadowSocks) Type() C.AdapterType {
return C.Shadowsocks
}
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)
if err != nil {
return nil, fmt.Errorf("%s connect error", ss.server)
}
c.(*net.TCPConn).SetKeepAlive(true)
c = ciph.StreamConn(c)
c = ss.cipher.StreamConn(c)
_, 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)
return &ShadowSocks{
server: server,
cipher: cipher,
password: password,
ciph, err := core.PickCipher(cipher, key, password)
if err != nil {
return nil, fmt.Errorf("ss %s initialize error: %s", server, err.Error())
}
return &ShadowSocks{
server: server,
name: name,
cipher: ciph,
traffic: traffic,
}, nil
}
func parseURL(s string) (addr, cipher, password string, err error) {
@ -84,13 +97,13 @@ func serializesSocksAddr(addr *C.Addr) []byte {
case socks.AtypDomainName:
len := uint8(len(addr.Host))
host := []byte(addr.Host)
buf = [][]byte{[]byte{aType, len}, host, port}
buf = [][]byte{{aType, len}, host, port}
case socks.AtypIPv4:
host := addr.IP.To4()
buf = [][]byte{[]byte{aType}, host, port}
buf = [][]byte{{aType}, host, port}
case socks.AtypIPv6:
host := addr.IP.To16()
buf = [][]byte{[]byte{aType}, host, port}
buf = [][]byte{{aType}, host, port}
}
return bytes.Join(buf, []byte(""))
}

168
adapters/urltest.go Normal file
View File

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

28
adapters/util.go Normal file
View 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}
}

View File

@ -5,6 +5,15 @@ import (
"net"
)
// Adapter Type
const (
Direct AdapterType = iota
Reject
Selector
Shadowsocks
URLTest
)
type ProxyAdapter interface {
ReadWriter() io.ReadWriter
Conn() net.Conn
@ -18,5 +27,27 @@ type ServerAdapter interface {
}
type Proxy interface {
Name() string
Type() AdapterType
Generator(addr *Addr) (ProxyAdapter, error)
}
// AdapterType is enum of adapter type
type AdapterType int
func (at AdapterType) String() string {
switch at {
case Direct:
return "Direct"
case Reject:
return "Reject"
case Selector:
return "Selector"
case Shadowsocks:
return "Shadowsocks"
case URLTest:
return "URLTest"
default:
return "Unknow"
}
}

View File

@ -16,8 +16,8 @@ import (
const (
Name = "clash"
DefalutHTTPPort = "7890"
DefalutSOCKSPort = "7891"
DefalutHTTPPort = 7890
DefalutSOCKSPort = 7891
)
var (
@ -26,12 +26,24 @@ var (
MMDBPath string
)
type General struct {
Mode *string `json:"mode,omitempty"`
AllowLan *bool `json:"allow-lan,omitempty"`
Port *int `json:"port,omitempty"`
SocksPort *int `json:"socks-port,omitempty"`
}
func init() {
currentUser, err := user.Current()
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)
if _, err := os.Stat(dirPath); os.IsNotExist(err) {

7
constant/proxy.go Normal file
View File

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

View File

@ -11,8 +11,26 @@ const (
type RuleType int
func (rt RuleType) String() string {
switch rt {
case DomainSuffix:
return "DomainSuffix"
case DomainKeyword:
return "DomainKeyword"
case GEOIP:
return "GEOIP"
case IPCIDR:
return "IPCIDR"
case FINAL:
return "FINAL"
default:
return "Unknow"
}
}
type Rule interface {
RuleType() RuleType
IsMatch(addr *Addr) bool
Adapter() string
Payload() string
}

55
constant/traffic.go Normal file
View 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
}

BIN
docs/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

31
hub/common.go Normal file
View File

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

74
hub/configs.go Normal file
View File

@ -0,0 +1,74 @@
package hub
import (
"fmt"
"net/http"
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"
)
func configRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getConfigs)
r.Put("/", updateConfigs)
return r
}
var modeMapping = map[string]T.Mode{
"Global": T.Global,
"Rule": T.Rule,
"Direct": T.Direct,
}
func getConfigs(w http.ResponseWriter, r *http.Request) {
info := listener.Info()
mode := tunnel.GetMode().String()
info.Mode = &mode
render.JSON(w, r, info)
}
func updateConfigs(w http.ResponseWriter, r *http.Request) {
general := &C.General{}
err := render.DecodeJSON(r.Body, general)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Format error",
})
return
}
// update errors
var proxyErr, modeErr error
// update proxy
listener := proxy.Instance()
proxyErr = listener.Update(general.AllowLan, general.Port, general.SocksPort)
// update mode
if general.Mode != nil {
mode, ok := modeMapping[*general.Mode]
if !ok {
modeErr = fmt.Errorf("Mode error")
} else {
tunnel.SetMode(mode)
}
}
hasError, errors := formatErrors(map[string]error{
"proxy": proxyErr,
"mode": modeErr,
})
if hasError {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, errors)
return
}
w.WriteHeader(http.StatusNoContent)
}

129
hub/proxys.go Normal file
View File

@ -0,0 +1,129 @@
package hub
import (
"fmt"
"net/http"
A "github.com/Dreamacro/clash/adapters"
C "github.com/Dreamacro/clash/constant"
"github.com/go-chi/chi"
"github.com/go-chi/render"
)
func proxyRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getProxys)
r.Get("/{name}", getProxy)
r.Put("/{name}", updateProxy)
return r
}
type SampleProxy struct {
Type string `json:"type"`
}
type Selector struct {
Type string `json:"type"`
Now string `json:"now"`
All []string `json:"all"`
}
type URLTest struct {
Type string `json:"type"`
Now string `json:"now"`
}
func transformProxy(proxy C.Proxy) interface{} {
t := proxy.Type()
switch t {
case C.Selector:
selector := proxy.(*A.Selector)
return Selector{
Type: t.String(),
Now: selector.Now(),
All: selector.All(),
}
case C.URLTest:
return URLTest{
Type: t.String(),
Now: proxy.(*A.URLTest).Now(),
}
default:
return SampleProxy{
Type: proxy.Type().String(),
}
}
}
type GetProxysResponse struct {
Proxys map[string]interface{} `json:"proxys"`
}
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)
}
render.JSON(w, r, GetProxysResponse{Proxys: proxys})
}
func getProxy(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
_, proxys := tunnel.Config()
proxy, exist := proxys[name]
if !exist {
w.WriteHeader(http.StatusNotFound)
render.JSON(w, r, Error{
Error: "Proxy not found",
})
return
}
render.JSON(w, r, transformProxy(proxy))
}
type UpdateProxyRequest struct {
Name string `json:"name"`
}
func updateProxy(w http.ResponseWriter, r *http.Request) {
req := UpdateProxyRequest{}
if err := render.DecodeJSON(r.Body, &req); err != nil {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Format error",
})
return
}
name := chi.URLParam(r, "name")
_, proxys := tunnel.Config()
proxy, exist := proxys[name]
if !exist {
w.WriteHeader(http.StatusNotFound)
render.JSON(w, r, Error{
Error: "Proxy not found",
})
return
}
selector, ok := proxy.(*A.Selector)
if !ok {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Proxy can't update",
})
return
}
if err := selector.Set(req.Name); err != nil {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: fmt.Sprintf("Selector update error: %s", err.Error()),
})
return
}
w.WriteHeader(http.StatusNoContent)
}

53
hub/rules.go Normal file
View File

@ -0,0 +1,53 @@
package hub
import (
"net/http"
"github.com/go-chi/chi"
"github.com/go-chi/render"
)
func ruleRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getRules)
r.Put("/", updateRules)
return r
}
type Rule struct {
Name string `json:"name"`
Payload string `json:"type"`
}
type GetRulesResponse struct {
Rules []Rule `json:"rules"`
}
func getRules(w http.ResponseWriter, r *http.Request) {
rulesCfg, _ := tunnel.Config()
var rules []Rule
for _, rule := range rulesCfg {
rules = append(rules, Rule{
Name: rule.RuleType().String(),
Payload: rule.Payload(),
})
}
w.WriteHeader(http.StatusOK)
render.JSON(w, r, GetRulesResponse{
Rules: rules,
})
}
func updateRules(w http.ResponseWriter, r *http.Request) {
err := tunnel.UpdateConfig()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
render.JSON(w, r, Error{
Error: err.Error(),
})
return
}
w.WriteHeader(http.StatusNoContent)
}

109
hub/server.go Normal file
View File

@ -0,0 +1,109 @@
package hub
import (
"encoding/json"
"net/http"
"time"
T "github.com/Dreamacro/clash/tunnel"
"github.com/go-chi/chi"
"github.com/go-chi/render"
log "github.com/sirupsen/logrus"
)
type Traffic struct {
Up int64 `json:"up"`
Down int64 `json:"down"`
}
func NewHub(addr string) {
r := chi.NewRouter()
r.Get("/traffic", traffic)
r.Get("/logs", getLogs)
r.Mount("/configs", configRouter())
r.Mount("/proxys", proxyRouter())
r.Mount("/rules", ruleRouter())
err := http.ListenAndServe(addr, r)
if err != nil {
log.Fatalf("External controller error: %s", err.Error())
}
}
func traffic(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
tick := time.NewTicker(time.Second)
t := tunnel.Traffic()
for range tick.C {
up, down := t.Now()
if err := json.NewEncoder(w).Encode(Traffic{
Up: up,
Down: down,
}); err != nil {
break
}
w.(http.Flusher).Flush()
}
}
type GetLogs struct {
Level string `json:"level"`
}
type Log struct {
Type string `json:"type"`
Payload string `json:"payload"`
}
func getLogs(w http.ResponseWriter, r *http.Request) {
req := &GetLogs{}
render.DecodeJSON(r.Body, req)
if req.Level == "" {
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]
if !ok {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Level error",
})
return
}
src := tunnel.Log()
sub, err := src.Subscribe()
defer src.UnSubscribe(sub)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
render.JSON(w, r, Error{
Error: err.Error(),
})
return
}
render.Status(r, http.StatusOK)
for elm := range sub {
log := elm.(T.Log)
if log.LogLevel > level {
continue
}
if err := json.NewEncoder(w).Encode(Log{
Type: log.Type(),
Payload: log.Payload,
}); err != nil {
break
}
w.(http.Flusher).Flush()
}
}

30
main.go
View File

@ -6,37 +6,33 @@ import (
"syscall"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/proxy/http"
"github.com/Dreamacro/clash/proxy/socks"
"github.com/Dreamacro/clash/hub"
"github.com/Dreamacro/clash/proxy"
"github.com/Dreamacro/clash/tunnel"
log "github.com/sirupsen/logrus"
)
func main() {
if err := tunnel.GetInstance().UpdateConfig(); err != nil {
log.Fatalf("Parse config error: %s", err.Error())
}
if err := proxy.Instance().Run(); err != nil {
log.Fatalf("Proxy listen error: %s", err.Error())
}
// Hub
cfg, err := C.GetConfig()
if err != nil {
log.Fatalf("Read config error: %s", err.Error())
}
port, socksPort := C.DefalutHTTPPort, C.DefalutSOCKSPort
section := cfg.Section("General")
if key, err := section.GetKey("port"); err == nil {
port = key.Value()
if key, err := section.GetKey("external-controller"); err == nil {
go hub.NewHub(key.Value())
}
if key, err := section.GetKey("socks-port"); err == nil {
socksPort = key.Value()
}
err = tunnel.GetInstance().UpdateConfig()
if err != nil {
log.Fatalf("Parse config error: %s", err.Error())
}
go http.NewHttpProxy(port)
go socks.NewSocksProxy(socksPort)
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh

View File

@ -1,18 +1,3 @@
package observable
import (
"errors"
)
type Iterable <-chan interface{}
func NewIterable(any interface{}) (Iterable, error) {
switch any := any.(type) {
case chan interface{}:
return Iterable(any), nil
case <-chan interface{}:
return Iterable(any), nil
default:
return nil, errors.New("type error")
}
}

View File

@ -50,7 +50,6 @@ func (o *Observable) Subscribe() (Subscription, error) {
func (o *Observable) UnSubscribe(sub Subscription) {
elm, exist := o.listener.Load(sub)
if !exist {
println("not exist")
return
}
subscriber := elm.(*Subscriber)

View File

@ -27,11 +27,7 @@ func TestObservable(t *testing.T) {
t.Error(err)
}
count := 0
for {
_, open := <-data
if !open {
break
}
for range data {
count = count + 1
}
if count != 5 {
@ -49,11 +45,7 @@ func TestObservable_MutilSubscribe(t *testing.T) {
var wg sync.WaitGroup
wg.Add(2)
waitCh := func(ch <-chan interface{}) {
for {
_, open := <-ch
if !open {
break
}
for range ch {
count = count + 1
}
wg.Done()
@ -80,6 +72,25 @@ func TestObservable_UnSubscribe(t *testing.T) {
}
}
func TestObservable_SubscribeClosedSource(t *testing.T) {
iter := iterator([]interface{}{1})
src := NewObservable(iter)
data, _ := src.Subscribe()
<-data
_, closed := src.Subscribe()
if closed == nil {
t.Error("Observable should be closed")
}
}
func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {
sub := Subscription(make(chan interface{}))
iter := iterator([]interface{}{1})
src := NewObservable(iter)
src.UnSubscribe(sub)
}
func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
// waiting for other goroutine recycle
time.Sleep(120 * time.Millisecond)
@ -97,11 +108,7 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
var wg sync.WaitGroup
wg.Add(max)
waitCh := func(ch <-chan interface{}) {
for {
_, open := <-ch
if !open {
break
}
for range ch {
}
wg.Done()
}

View File

@ -1,15 +0,0 @@
package observable
func mergeWithBytes(ch <-chan interface{}, buf []byte) chan interface{} {
out := make(chan interface{})
go func() {
defer close(out)
if len(buf) != 0 {
out <- buf
}
for elm := range ch {
out <- elm
}
}()
return out
}

View File

@ -32,7 +32,6 @@ func (h *HttpAdapter) Connect(proxy C.ProxyAdapter) {
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
resp, err := req.RoundTrip(h.r)
@ -48,7 +47,23 @@ func (h *HttpAdapter) Connect(proxy C.ProxyAdapter) {
}
}
h.w.WriteHeader(resp.StatusCode)
io.Copy(h.w, resp.Body)
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{}) {

View File

@ -1,7 +1,7 @@
package http
import (
"fmt"
"context"
"net"
"net/http"
"strings"
@ -17,9 +17,20 @@ var (
tun = tunnel.GetInstance()
)
func NewHttpProxy(port string) {
func NewHttpProxy(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,
}
server := &http.Server{
Addr: fmt.Sprintf(":%s", port),
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
handleTunneling(w, r)
@ -28,8 +39,20 @@ func NewHttpProxy(port string) {
}
}),
}
log.Infof("HTTP proxy :%s", port)
server.ListenAndServe()
go func() {
log.Infof("HTTP proxy listening at: %s", addr)
server.Serve(l)
}()
go func() {
<-done
server.Shutdown(context.Background())
l.Close()
closed <- struct{}{}
}()
return signal, nil
}
func handleHTTP(w http.ResponseWriter, r *http.Request) {

143
proxy/listener.go Normal file
View File

@ -0,0 +1,143 @@
package proxy
import (
"fmt"
"sync"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/proxy/http"
"github.com/Dreamacro/clash/proxy/socks"
log "github.com/sirupsen/logrus"
)
var (
listener *Listener
once sync.Once
)
type Listener struct {
httpPort int
socksPort int
allowLan bool
// signal for update
httpSignal *C.ProxySignal
socksSignal *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 {
if l.httpSignal != nil {
signal := l.httpSignal
signal.Done <- struct{}{}
<-signal.Closed
l.httpSignal = nil
}
signal, err := http.NewHttpProxy(l.genAddr(port))
if err != nil {
return err
}
l.httpSignal = signal
l.httpPort = port
return nil
}
func (l *Listener) updateSocks(port int) error {
if l.socksSignal != nil {
signal := l.socksSignal
signal.Done <- struct{}{}
<-signal.Closed
l.socksSignal = nil
}
signal, err := socks.NewSocksProxy(l.genAddr(port))
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 = ""
}
return fmt.Sprintf("%s:%d", host, port)
}
func (l *Listener) Run() error {
return l.Update(&l.allowLan, &l.httpPort, &l.socksPort)
}
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,
}
}
func Instance() *Listener {
once.Do(func() {
listener = newListener()
})
return listener
}

View File

@ -1,7 +1,6 @@
package socks
import (
"fmt"
"io"
"net"
"strconv"
@ -17,20 +16,41 @@ var (
tun = tunnel.GetInstance()
)
func NewSocksProxy(port string) {
l, err := net.Listen("tcp", fmt.Sprintf(":%s", port))
defer l.Close()
func NewSocksProxy(addr string) (*C.ProxySignal, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return
return nil, err
}
log.Infof("SOCKS proxy :%s", port)
for {
c, err := l.Accept()
if err != nil {
continue
done := make(chan struct{})
closed := make(chan struct{})
signal := &C.ProxySignal{
Done: done,
Closed: closed,
}
go func() {
log.Infof("SOCKS proxy listening at: %s", addr)
for {
c, err := l.Accept()
if err != nil {
if _, open := <-done; !open {
break
}
continue
}
go handleSocks(c)
}
go handleSocks(c)
}
}()
go func() {
<-done
close(done)
l.Close()
closed <- struct{}{}
}()
return signal, nil
}
func handleSocks(conn net.Conn) {

View File

@ -27,6 +27,10 @@ func (dk *DomainKeyword) Adapter() string {
return dk.adapter
}
func (dk *DomainKeyword) Payload() string {
return dk.keyword
}
func NewDomainKeyword(keyword string, adapter string) *DomainKeyword {
return &DomainKeyword{
keyword: keyword,

View File

@ -27,6 +27,10 @@ func (ds *DomainSuffix) Adapter() string {
return ds.adapter
}
func (ds *DomainSuffix) Payload() string {
return ds.suffix
}
func NewDomainSuffix(suffix string, adapter string) *DomainSuffix {
return &DomainSuffix{
suffix: suffix,

View File

@ -20,6 +20,10 @@ func (f *Final) Adapter() string {
return f.adapter
}
func (f *Final) Payload() string {
return ""
}
func NewFinal(adapter string) *Final {
return &Final{
adapter: adapter,

View File

@ -38,6 +38,10 @@ func (g *GEOIP) Adapter() string {
return g.adapter
}
func (g *GEOIP) Payload() string {
return g.country
}
func NewGEOIP(country string, adapter string) *GEOIP {
return &GEOIP{
country: country,

View File

@ -23,8 +23,12 @@ func (i *IPCIDR) IsMatch(addr *C.Addr) bool {
return i.ipnet.Contains(*addr.IP)
}
func (g *IPCIDR) Adapter() string {
return g.adapter
func (i *IPCIDR) Adapter() string {
return i.adapter
}
func (i *IPCIDR) Payload() string {
return i.ipnet.String()
}
func NewIPCIDR(s string, adapter string) *IPCIDR {

View File

@ -7,21 +7,36 @@ import (
)
const (
INFO LogType = iota
ERROR LogLevel = iota
WARNING
ERROR
INFO
DEBUG
)
type LogType int
type LogLevel int
type Log struct {
LogType LogType
Payload string
LogLevel 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"
}
}
func print(data Log) {
switch data.LogType {
switch data.LogLevel {
case INFO:
log.Infoln(data.Payload)
case WARNING:
@ -44,9 +59,9 @@ func (t *Tunnel) subscribeLogs() {
}
}
func newLog(logType LogType, format string, v ...interface{}) Log {
func newLog(logLevel LogLevel, format string, v ...interface{}) Log {
return Log{
LogType: logType,
Payload: fmt.Sprintf(format, v...),
LogLevel: logLevel,
Payload: fmt.Sprintf(format, v...),
}
}

22
tunnel/mode.go Normal file
View File

@ -0,0 +1,22 @@
package tunnel
type Mode int
const (
Global Mode = iota
Rule
Direct
)
func (m Mode) String() string {
switch m {
case Global:
return "Global"
case Rule:
return "Rule"
case Direct:
return "Direct"
default:
return "Unknow"
}
}

View File

@ -2,8 +2,10 @@ package tunnel
import (
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/Dreamacro/clash/adapters"
C "github.com/Dreamacro/clash/constant"
@ -25,35 +27,55 @@ type Tunnel struct {
observable *observable.Observable
logCh chan interface{}
configLock *sync.RWMutex
traffic *C.Traffic
mode Mode
selector *adapters.Selector
}
func (t *Tunnel) Add(req C.ServerAdapter) {
t.queue.In() <- req
}
func (t *Tunnel) UpdateConfig() (err error) {
t.configLock.Lock()
defer t.configLock.Unlock()
func (t *Tunnel) Traffic() *C.Traffic {
return t.traffic
}
func (t *Tunnel) Config() ([]C.Rule, map[string]C.Proxy) {
return t.rules, t.proxys
}
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
}
// clear proxys and rules
t.proxys = make(map[string]C.Proxy)
t.rules = []C.Rule{}
// empty proxys and rules
proxys := make(map[string]C.Proxy)
rules := []C.Rule{}
proxys := cfg.Section("Proxy")
rules := cfg.Section("Rule")
proxysConfig := cfg.Section("Proxy")
rulesConfig := cfg.Section("Rule")
groupsConfig := cfg.Section("Proxy Group")
// parse proxy
for _, key := range proxys.Keys() {
proxy := strings.Split(key.Value(), ",")
for _, key := range proxysConfig.Keys() {
proxy := key.Strings(",")
if len(proxy) == 0 {
continue
}
proxy = trimArr(proxy)
switch proxy[0] {
// ss, server, port, cipter, password
case "ss":
@ -61,16 +83,16 @@ func (t *Tunnel) UpdateConfig() (err error) {
continue
}
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
t.proxys["DIRECT"] = adapters.NewDirect()
t.proxys["REJECT"] = adapters.NewReject()
// parse rules
for _, key := range rules.Keys() {
for _, key := range rulesConfig.Keys() {
rule := strings.Split(key.Name(), ",")
if len(rule) < 3 {
continue
@ -78,18 +100,87 @@ func (t *Tunnel) UpdateConfig() (err error) {
rule = trimArr(rule)
switch rule[0] {
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":
t.rules = append(t.rules, R.NewDomainKeyword(rule[1], rule[2]))
rules = append(rules, R.NewDomainKeyword(rule[1], rule[2]))
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":
t.rules = append(t.rules, R.NewIPCIDR(rule[1], rule[2]))
rules = append(rules, R.NewIPCIDR(rule[1], rule[2]))
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(), ",")
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
}
@ -105,7 +196,17 @@ func (t *Tunnel) process() {
func (t *Tunnel) handleConn(localConn C.ServerAdapter) {
defer localConn.Close()
addr := localConn.Addr()
proxy := t.match(addr)
var proxy C.Proxy
switch t.mode {
case Direct:
proxy = t.proxys["DIRECT"]
case Global:
proxy = t.selector
// Rule
default:
proxy = t.match(addr)
}
remoConn, err := proxy.Generator(addr)
if err != nil {
t.logCh <- newLog(WARNING, "Proxy connect error: %s", err.Error())
@ -126,11 +227,11 @@ func (t *Tunnel) match(addr *C.Addr) C.Proxy {
if !ok {
continue
}
t.logCh <- newLog(INFO, "%v match %d using %s", addr.String(), rule.RuleType(), rule.Adapter())
t.logCh <- newLog(INFO, "%v match %s using %s", addr.String(), rule.RuleType().String(), rule.Adapter())
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"]
}
@ -142,6 +243,8 @@ func newTunnel() *Tunnel {
observable: observable.NewObservable(logCh),
logCh: logCh,
configLock: &sync.RWMutex{},
traffic: C.NewTraffic(time.Second),
mode: Rule,
}
go tunnel.process()
go tunnel.subscribeLogs()