diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ba7190 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# dep +vendor diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4714980 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,27 @@ +language: go +sudo: false +go: + - "1.10" +before_install: + - go get -u github.com/golang/dep/cmd/dep +install: + - "$GOPATH/bin/dep ensure" +env: + global: + - NAME=clash + - BINDIR=bin +script: + - go test ./... +before_deploy: make -j releases +deploy: + provider: releases + prerelease: true + skip_cleanup: true + api_key: + secure: dp1tc1h0er7aaAZ1hY0Xk/cUKwB0ifsAjg6e0M/Ad5NC87oucP6ESNFkDu0e9rUS1yB826/VnVGzNE/Z5zdjXVzPft+g5v5oRxzI4BKLhf07t9s+x8Z+3sApTxdsC5BvcN9x+5yRbpDLQ3biDPxSFu86j7m2pkEWw6XYNZO3/5y+RZXX7zu+d4MzTLUaA2kWl7KQAP0tEJNuw9ACDhpkw7LYbU/8q3E76prOTeme5/AT6Gxj7XhKUNP27lazhhqBSWM14ybPANqojNLEfMFHN/Eu2phYO07MuLTd4zuOIuw9y65kgvTFcHRlORjwUhnviXyA69obQejjgDI1WDOtU4PqpFaSLrxWtKI6k5VNWHARYggDm/wKl0WG7F0Kgio1KiGGhDg2yrbseXr/zBNaDhBtTFh6XJffqqwmgby1PXB6PWwfvWXooJMaQiFZczLWeMBl8v6XbSN6jtMTh/PQlKai6BcDd4LM8GQ7VHpSeff4qXEU4Vpnadjgs8VDPOHng6/HV+wDs8q2LrlMbnxLWxbCjOMUB6w7YnSrwH9owzKSoUs/531I4tTCRQIgipJtTK2b881/8osVjdMGS1mDXhBWO+OM0LCAdORJz+kN4PIkXXvKLt6jX74k6z4M3swFaqqtlTduN2Yy/ErsjguQO1VZfHmcpNssmJXI5QB9sxA= + file: bin/* + file_glob: true + on: + repo: Dreamacro/clash + branch: master + tags: true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3cada3f --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..a879f70 --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,97 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + branch = "master" + name = "github.com/Yawning/chacha20" + packages = ["."] + revision = "e3b1f968fc6397b51d963fee8ec8711a47bc0ce8" + +[[projects]] + name = "github.com/eapache/queue" + packages = ["."] + 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 = ["."] + revision = "7118115686e16b77967cdbf55d1b944fe14ad312" + version = "v1.2.1" + +[[projects]] + name = "github.com/oschwald/maxminddb-golang" + packages = ["."] + revision = "c5bec84d1963260297932a1b7a1753c8420717a7" + version = "v1.3.0" + +[[projects]] + name = "github.com/riobard/go-shadowsocks2" + packages = [ + "core", + "shadowaead", + "shadowstream", + "socks" + ] + revision = "8346403248229fc7e10d7a259de8e9352a9d8830" + version = "v0.1.0" + +[[projects]] + name = "github.com/sirupsen/logrus" + packages = ["."] + revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc" + version = "v1.0.5" + +[[projects]] + branch = "master" + name = "golang.org/x/crypto" + packages = [ + "chacha20poly1305", + "hkdf", + "internal/chacha20", + "internal/subtle", + "poly1305", + "ssh/terminal" + ] + revision = "027cca12c2d63e3d62b670d901e8a2c95854feec" + +[[projects]] + branch = "master" + name = "golang.org/x/sys" + packages = [ + "cpu", + "unix", + "windows" + ] + revision = "6c888cc515d3ed83fc103cf1d84468aad274b0a7" + +[[projects]] + name = "gopkg.in/eapache/channels.v1" + packages = ["."] + revision = "47238d5aae8c0fefd518ef2bee46290909cf8263" + version = "v1.1.0" + +[[projects]] + name = "gopkg.in/ini.v1" + packages = ["."] + revision = "06f5f3d67269ccec1fe5fe4134ba6e982984f7f5" + version = "v1.37.0" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "c3c901e4e393a2df9e421924d3a4d85ec73642e36dcbc1ddca5fc13159220e86" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..ffa7005 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,58 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# 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" + +[[constraint]] + name = "github.com/riobard/go-shadowsocks2" + version = "0.1.0" + +[[constraint]] + name = "github.com/sirupsen/logrus" + version = "1.0.5" + +[[constraint]] + name = "gopkg.in/eapache/channels.v1" + version = "1.1.0" + +[[constraint]] + name = "gopkg.in/ini.v1" + version = "1.37.0" + +[prune] + go-tests = true + unused-packages = true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ec13360 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Dreamacro + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..87ae04a --- /dev/null +++ b/Makefile @@ -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)/* diff --git a/adapters/direct.go b/adapters/direct.go new file mode 100644 index 0000000..f561ee9 --- /dev/null +++ b/adapters/direct.go @@ -0,0 +1,49 @@ +package adapters + +import ( + "io" + "net" + + C "github.com/Dreamacro/clash/constant" +) + +// DirectAdapter is a directly connected adapter +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() +} + +// 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) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) { + c, err := net.Dial("tcp", net.JoinHostPort(addr.String(), addr.Port)) + if err != nil { + return + } + c.(*net.TCPConn).SetKeepAlive(true) + return &DirectAdapter{conn: NewTrafficTrack(c, d.traffic)}, nil +} + +func NewDirect(traffic *C.Traffic) *Direct { + return &Direct{traffic: traffic} +} diff --git a/adapters/reject.go b/adapters/reject.go new file mode 100644 index 0000000..897e1c6 --- /dev/null +++ b/adapters/reject.go @@ -0,0 +1,50 @@ +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) 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 +} diff --git a/adapters/shadowsocks.go b/adapters/shadowsocks.go new file mode 100644 index 0000000..f4950c5 --- /dev/null +++ b/adapters/shadowsocks.go @@ -0,0 +1,105 @@ +package adapters + +import ( + "bytes" + "fmt" + "io" + "net" + "net/url" + "strconv" + + C "github.com/Dreamacro/clash/constant" + + "github.com/riobard/go-shadowsocks2/core" + "github.com/riobard/go-shadowsocks2/socks" +) + +// ShadowsocksAdapter is a shadowsocks adapter +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() +} + +func (ss *ShadowsocksAdapter) Conn() net.Conn { + return ss.conn +} + +type ShadowSocks struct { + server string + name 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) { + 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 = ss.cipher.StreamConn(c) + _, err = c.Write(serializesSocksAddr(addr)) + return &ShadowsocksAdapter{conn: NewTrafficTrack(c, ss.traffic)}, err +} + +func NewShadowSocks(name string, ssURL string, traffic *C.Traffic) (*ShadowSocks, error) { + var key []byte + server, cipher, password, _ := parseURL(ssURL) + 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) { + u, err := url.Parse(s) + if err != nil { + return + } + + addr = u.Host + if u.User != nil { + cipher = u.User.Username() + password, _ = u.User.Password() + } + return +} + +func serializesSocksAddr(addr *C.Addr) []byte { + var buf [][]byte + aType := uint8(addr.AddrType) + p, _ := strconv.Atoi(addr.Port) + port := []byte{uint8(p >> 8), uint8(p & 0xff)} + switch addr.AddrType { + case socks.AtypDomainName: + len := uint8(len(addr.Host)) + host := []byte(addr.Host) + buf = [][]byte{{aType, len}, host, port} + case socks.AtypIPv4: + host := addr.IP.To4() + buf = [][]byte{{aType}, host, port} + case socks.AtypIPv6: + host := addr.IP.To16() + buf = [][]byte{{aType}, host, port} + } + return bytes.Join(buf, []byte("")) +} diff --git a/adapters/urltest.go b/adapters/urltest.go new file mode 100644 index 0000000..7608024 --- /dev/null +++ b/adapters/urltest.go @@ -0,0 +1,160 @@ +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) 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 +} diff --git a/adapters/util.go b/adapters/util.go new file mode 100644 index 0000000..1f41c54 --- /dev/null +++ b/adapters/util.go @@ -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} +} diff --git a/constant/adapters.go b/constant/adapters.go new file mode 100644 index 0000000..101115c --- /dev/null +++ b/constant/adapters.go @@ -0,0 +1,23 @@ +package constant + +import ( + "io" + "net" +) + +type ProxyAdapter interface { + ReadWriter() io.ReadWriter + Conn() net.Conn + Close() +} + +type ServerAdapter interface { + Addr() *Addr + Connect(ProxyAdapter) + Close() +} + +type Proxy interface { + Name() string + Generator(addr *Addr) (ProxyAdapter, error) +} diff --git a/constant/addr.go b/constant/addr.go new file mode 100644 index 0000000..6f498f9 --- /dev/null +++ b/constant/addr.go @@ -0,0 +1,40 @@ +package constant + +import ( + "net" +) + +// Socks addr type +const ( + AtypIPv4 = 1 + AtypDomainName = 3 + AtypIPv6 = 4 + + TCP = iota + UDP +) + +type NetWork int + +func (n *NetWork) String() string { + if *n == TCP { + return "tcp" + } + return "udp" +} + +// Addr is used to store connection address +type Addr struct { + NetWork NetWork + AddrType int + Host string + IP *net.IP + Port string +} + +func (addr *Addr) String() string { + if addr.Host == "" { + return addr.IP.String() + } + return addr.Host +} diff --git a/constant/config.go b/constant/config.go new file mode 100644 index 0000000..63c8785 --- /dev/null +++ b/constant/config.go @@ -0,0 +1,112 @@ +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" +) + +var ( + HomeDir string + ConfigPath string + MMDBPath string +) + +func init() { + currentUser, err := user.Current() + if err != nil { + dir := os.Getenv("HOME") + if dir == "" { + log.Fatalf("Can't get current user: %s", err.Error()) + } + HomeDir = dir + } else { + HomeDir = currentUser.HomeDir + } + + dirPath := path.Join(HomeDir, ".config", Name) + if _, err := os.Stat(dirPath); os.IsNotExist(err) { + if err := os.MkdirAll(dirPath, 0777); err != nil { + log.Fatalf("Can't create config directory %s: %s", dirPath, err.Error()) + } + } + + 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, + ) +} diff --git a/constant/rule.go b/constant/rule.go new file mode 100644 index 0000000..0acdcc3 --- /dev/null +++ b/constant/rule.go @@ -0,0 +1,36 @@ +package constant + +// Rule Type +const ( + DomainSuffix RuleType = iota + DomainKeyword + GEOIP + IPCIDR + FINAL +) + +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 +} diff --git a/constant/traffic.go b/constant/traffic.go new file mode 100644 index 0000000..edf6736 --- /dev/null +++ b/constant/traffic.go @@ -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 +} diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 0000000..c624b94 Binary files /dev/null and b/docs/logo.png differ diff --git a/hub/configs.go b/hub/configs.go new file mode 100644 index 0000000..3cc46a3 --- /dev/null +++ b/hub/configs.go @@ -0,0 +1,67 @@ +package hub + +import ( + "net/http" + + "github.com/go-chi/chi" + "github.com/go-chi/render" +) + +type Configs struct { + Proxys []Proxy `json:"proxys"` + Rules []Rule `json:"rules"` +} + +type Proxy struct { + Name string `json:"name"` +} + +type Rule struct { + Name string `json:"name"` + Payload string `json:"type"` +} + +func configRouter() http.Handler { + r := chi.NewRouter() + r.Get("/", getConfig) + r.Put("/", updateConfig) + return r +} + +func getConfig(w http.ResponseWriter, r *http.Request) { + rulesCfg, proxysCfg := tun.Config() + + var ( + rules []Rule + proxys []Proxy + ) + + for _, rule := range rulesCfg { + rules = append(rules, Rule{ + Name: rule.RuleType().String(), + Payload: rule.Payload(), + }) + } + + for _, proxy := range proxysCfg { + proxys = append(proxys, Proxy{Name: proxy.Name()}) + } + + w.WriteHeader(http.StatusOK) + render.JSON(w, r, Configs{ + Rules: rules, + Proxys: proxys, + }) +} + +func updateConfig(w http.ResponseWriter, r *http.Request) { + err := tun.UpdateConfig() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + render.JSON(w, r, Error{ + Error: err.Error(), + }) + return + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/hub/server.go b/hub/server.go new file mode 100644 index 0000000..5c9f403 --- /dev/null +++ b/hub/server.go @@ -0,0 +1,115 @@ +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 Error struct { + Error string `json:"error"` +} + +func NewHub(addr string) { + r := chi.NewRouter() + + r.Get("/traffic", traffic) + r.Get("/logs", getLogs) + r.Mount("/configs", configRouter()) + + 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 := 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() + } +} + +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]tunnel.LogLevel{ + "info": tunnel.INFO, + "debug": tunnel.DEBUG, + "error": tunnel.ERROR, + "warning": tunnel.WARNING, + } + + level, ok := mapping[req.Level] + if !ok { + w.WriteHeader(http.StatusBadRequest) + render.JSON(w, r, Error{ + Error: "Level error", + }) + return + } + + src := tun.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.(tunnel.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() + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..8f409cb --- /dev/null +++ b/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "os" + "os/signal" + "syscall" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/hub" + "github.com/Dreamacro/clash/proxy/http" + "github.com/Dreamacro/clash/proxy/socks" + "github.com/Dreamacro/clash/tunnel" + + log "github.com/sirupsen/logrus" +) + +func main() { + 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("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) + + // Hub + if key, err := section.GetKey("external-controller"); err == nil { + go hub.NewHub(key.Value()) + } + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + <-sigCh +} diff --git a/observable/iterable.go b/observable/iterable.go new file mode 100644 index 0000000..7892994 --- /dev/null +++ b/observable/iterable.go @@ -0,0 +1,18 @@ +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") + } +} diff --git a/observable/observable.go b/observable/observable.go new file mode 100644 index 0000000..5ba292e --- /dev/null +++ b/observable/observable.go @@ -0,0 +1,68 @@ +package observable + +import ( + "errors" + "sync" +) + +type Observable struct { + iterable Iterable + listener *sync.Map + done bool + doneLock sync.RWMutex +} + +func (o *Observable) process() { + for item := range o.iterable { + o.listener.Range(func(key, value interface{}) bool { + elm := value.(*Subscriber) + elm.Emit(item) + return true + }) + } + o.close() +} + +func (o *Observable) close() { + o.doneLock.Lock() + o.done = true + o.doneLock.Unlock() + + o.listener.Range(func(key, value interface{}) bool { + elm := value.(*Subscriber) + elm.Close() + return true + }) +} + +func (o *Observable) Subscribe() (Subscription, error) { + o.doneLock.RLock() + done := o.done + o.doneLock.RUnlock() + if done == true { + return nil, errors.New("Observable is closed") + } + subscriber := newSubscriber() + o.listener.Store(subscriber.Out(), subscriber) + return subscriber.Out(), nil +} + +func (o *Observable) UnSubscribe(sub Subscription) { + elm, exist := o.listener.Load(sub) + if !exist { + println("not exist") + return + } + subscriber := elm.(*Subscriber) + o.listener.Delete(subscriber.Out()) + subscriber.Close() +} + +func NewObservable(any Iterable) *Observable { + observable := &Observable{ + iterable: any, + listener: &sync.Map{}, + } + go observable.process() + return observable +} diff --git a/observable/observable_test.go b/observable/observable_test.go new file mode 100644 index 0000000..10bef10 --- /dev/null +++ b/observable/observable_test.go @@ -0,0 +1,117 @@ +package observable + +import ( + "runtime" + "sync" + "testing" + "time" +) + +func iterator(item []interface{}) chan interface{} { + ch := make(chan interface{}) + go func() { + time.Sleep(100 * time.Millisecond) + for _, elm := range item { + ch <- elm + } + close(ch) + }() + return ch +} + +func TestObservable(t *testing.T) { + iter := iterator([]interface{}{1, 2, 3, 4, 5}) + src := NewObservable(iter) + data, err := src.Subscribe() + if err != nil { + t.Error(err) + } + count := 0 + for { + _, open := <-data + if !open { + break + } + count = count + 1 + } + if count != 5 { + t.Error("Revc number error") + } +} + +func TestObservable_MutilSubscribe(t *testing.T) { + iter := iterator([]interface{}{1, 2, 3, 4, 5}) + src := NewObservable(iter) + ch1, _ := src.Subscribe() + ch2, _ := src.Subscribe() + count := 0 + + var wg sync.WaitGroup + wg.Add(2) + waitCh := func(ch <-chan interface{}) { + for { + _, open := <-ch + if !open { + break + } + count = count + 1 + } + wg.Done() + } + go waitCh(ch1) + go waitCh(ch2) + wg.Wait() + if count != 10 { + t.Error("Revc number error") + } +} + +func TestObservable_UnSubscribe(t *testing.T) { + iter := iterator([]interface{}{1, 2, 3, 4, 5}) + src := NewObservable(iter) + data, err := src.Subscribe() + if err != nil { + t.Error(err) + } + src.UnSubscribe(data) + _, open := <-data + if open { + t.Error("Revc number error") + } +} + +func TestObservable_SubscribeGoroutineLeak(t *testing.T) { + // waiting for other goroutine recycle + time.Sleep(120 * time.Millisecond) + init := runtime.NumGoroutine() + iter := iterator([]interface{}{1, 2, 3, 4, 5}) + src := NewObservable(iter) + max := 100 + + var list []Subscription + for i := 0; i < max; i++ { + ch, _ := src.Subscribe() + list = append(list, ch) + } + + var wg sync.WaitGroup + wg.Add(max) + waitCh := func(ch <-chan interface{}) { + for { + _, open := <-ch + if !open { + break + } + } + wg.Done() + } + + for _, ch := range list { + go waitCh(ch) + } + wg.Wait() + now := runtime.NumGoroutine() + if init != now { + t.Errorf("Goroutine Leak: init %d now %d", init, now) + } +} diff --git a/observable/subscriber.go b/observable/subscriber.go new file mode 100644 index 0000000..3fb1e58 --- /dev/null +++ b/observable/subscriber.go @@ -0,0 +1,35 @@ +package observable + +import ( + "sync" + + "gopkg.in/eapache/channels.v1" +) + +type Subscription <-chan interface{} + +type Subscriber struct { + buffer *channels.InfiniteChannel + once sync.Once +} + +func (s *Subscriber) Emit(item interface{}) { + s.buffer.In() <- item +} + +func (s *Subscriber) Out() Subscription { + return s.buffer.Out() +} + +func (s *Subscriber) Close() { + s.once.Do(func() { + s.buffer.Close() + }) +} + +func newSubscriber() *Subscriber { + sub := &Subscriber{ + buffer: channels.NewInfiniteChannel(), + } + return sub +} diff --git a/observable/util.go b/observable/util.go new file mode 100644 index 0000000..d7d02b0 --- /dev/null +++ b/observable/util.go @@ -0,0 +1,15 @@ +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 +} diff --git a/proxy/http/http.go b/proxy/http/http.go new file mode 100644 index 0000000..8807c92 --- /dev/null +++ b/proxy/http/http.go @@ -0,0 +1,77 @@ +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 +} diff --git a/proxy/http/https.go b/proxy/http/https.go new file mode 100644 index 0000000..33eed17 --- /dev/null +++ b/proxy/http/https.go @@ -0,0 +1,35 @@ +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, + } +} diff --git a/proxy/http/server.go b/proxy/http/server.go new file mode 100644 index 0000000..9dcefd0 --- /dev/null +++ b/proxy/http/server.go @@ -0,0 +1,86 @@ +package http + +import ( + "fmt" + "net" + "net/http" + "strings" + + 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() +) + +func NewHttpProxy(port string) { + 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) + } else { + handleHTTP(w, r) + } + }), + } + log.Infof("HTTP proxy :%s", port) + server.ListenAndServe() +} + +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 { + 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, + } +} diff --git a/proxy/socks/tcp.go b/proxy/socks/tcp.go new file mode 100644 index 0000000..163fdc5 --- /dev/null +++ b/proxy/socks/tcp.go @@ -0,0 +1,98 @@ +package socks + +import ( + "fmt" + "io" + "net" + "strconv" + + 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() +) + +func NewSocksProxy(port string) { + l, err := net.Listen("tcp", fmt.Sprintf(":%s", port)) + defer l.Close() + if err != nil { + return + } + log.Infof("SOCKS proxy :%s", port) + for { + c, err := l.Accept() + if err != nil { + continue + } + go handleSocks(c) + } +} + +func handleSocks(conn net.Conn) { + target, err := socks.Handshake(conn) + if err != nil { + conn.Close() + 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), + } +} diff --git a/proxy/socks/udp.go b/proxy/socks/udp.go new file mode 100644 index 0000000..a266580 --- /dev/null +++ b/proxy/socks/udp.go @@ -0,0 +1 @@ +package socks diff --git a/rules/domain_keyword.go b/rules/domain_keyword.go new file mode 100644 index 0000000..19ca2e2 --- /dev/null +++ b/rules/domain_keyword.go @@ -0,0 +1,39 @@ +package rules + +import ( + "strings" + + C "github.com/Dreamacro/clash/constant" +) + +type DomainKeyword struct { + keyword string + adapter string +} + +func (dk *DomainKeyword) RuleType() C.RuleType { + return C.DomainKeyword +} + +func (dk *DomainKeyword) IsMatch(addr *C.Addr) bool { + if addr.AddrType != C.AtypDomainName { + return false + } + domain := addr.Host + return strings.Contains(domain, dk.keyword) +} + +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, + adapter: adapter, + } +} diff --git a/rules/domain_suffix.go b/rules/domain_suffix.go new file mode 100644 index 0000000..d6dd9f9 --- /dev/null +++ b/rules/domain_suffix.go @@ -0,0 +1,39 @@ +package rules + +import ( + "strings" + + C "github.com/Dreamacro/clash/constant" +) + +type DomainSuffix struct { + suffix string + adapter string +} + +func (ds *DomainSuffix) RuleType() C.RuleType { + return C.DomainSuffix +} + +func (ds *DomainSuffix) IsMatch(addr *C.Addr) bool { + if addr.AddrType != C.AtypDomainName { + return false + } + domain := addr.Host + return strings.HasSuffix(domain, "."+ds.suffix) || domain == ds.suffix +} + +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, + adapter: adapter, + } +} diff --git a/rules/final.go b/rules/final.go new file mode 100644 index 0000000..f07fbab --- /dev/null +++ b/rules/final.go @@ -0,0 +1,31 @@ +package rules + +import ( + C "github.com/Dreamacro/clash/constant" +) + +type Final struct { + adapter string +} + +func (f *Final) RuleType() C.RuleType { + return C.FINAL +} + +func (f *Final) IsMatch(addr *C.Addr) bool { + return true +} + +func (f *Final) Adapter() string { + return f.adapter +} + +func (f *Final) Payload() string { + return "" +} + +func NewFinal(adapter string) *Final { + return &Final{ + adapter: adapter, + } +} diff --git a/rules/geoip.go b/rules/geoip.go new file mode 100644 index 0000000..9ee8691 --- /dev/null +++ b/rules/geoip.go @@ -0,0 +1,50 @@ +package rules + +import ( + 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()) + } +} + +type GEOIP struct { + country string + adapter string +} + +func (g *GEOIP) RuleType() C.RuleType { + return C.GEOIP +} + +func (g *GEOIP) IsMatch(addr *C.Addr) bool { + if addr.IP == nil { + return false + } + record, _ := mmdb.Country(*addr.IP) + return record.Country.IsoCode == g.country +} + +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, + adapter: adapter, + } +} diff --git a/rules/ipcidr.go b/rules/ipcidr.go new file mode 100644 index 0000000..baf4475 --- /dev/null +++ b/rules/ipcidr.go @@ -0,0 +1,42 @@ +package rules + +import ( + "net" + + C "github.com/Dreamacro/clash/constant" +) + +type IPCIDR struct { + ipnet *net.IPNet + adapter string +} + +func (i *IPCIDR) RuleType() C.RuleType { + return C.IPCIDR +} + +func (i *IPCIDR) IsMatch(addr *C.Addr) bool { + if addr.IP == nil { + return false + } + + return i.ipnet.Contains(*addr.IP) +} + +func (i *IPCIDR) Adapter() string { + return i.adapter +} + +func (i *IPCIDR) Payload() string { + return i.ipnet.String() +} + +func NewIPCIDR(s string, adapter string) *IPCIDR { + _, ipnet, err := net.ParseCIDR(s) + if err != nil { + } + return &IPCIDR{ + ipnet: ipnet, + adapter: adapter, + } +} diff --git a/tunnel/log.go b/tunnel/log.go new file mode 100644 index 0000000..2021a35 --- /dev/null +++ b/tunnel/log.go @@ -0,0 +1,67 @@ +package tunnel + +import ( + "fmt" + + log "github.com/sirupsen/logrus" +) + +const ( + ERROR LogLevel = iota + WARNING + INFO + DEBUG +) + +type LogLevel int + +type Log struct { + 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.LogLevel { + case INFO: + log.Infoln(data.Payload) + case WARNING: + log.Warnln(data.Payload) + case ERROR: + log.Errorln(data.Payload) + case DEBUG: + log.Debugln(data.Payload) + } +} + +func (t *Tunnel) subscribeLogs() { + sub, err := t.observable.Subscribe() + if err != nil { + log.Fatalf("Can't subscribe tunnel log: %s", err.Error()) + } + for elm := range sub { + data := elm.(Log) + print(data) + } +} + +func newLog(logLevel LogLevel, format string, v ...interface{}) Log { + return Log{ + LogLevel: logLevel, + Payload: fmt.Sprintf(format, v...), + } +} diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go new file mode 100644 index 0000000..d1d581a --- /dev/null +++ b/tunnel/tunnel.go @@ -0,0 +1,215 @@ +package tunnel + +import ( + "fmt" + "strconv" + "strings" + "sync" + "time" + + "github.com/Dreamacro/clash/adapters" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/observable" + R "github.com/Dreamacro/clash/rules" + + "gopkg.in/eapache/channels.v1" +) + +var ( + tunnel *Tunnel + once sync.Once +) + +type Tunnel struct { + queue *channels.InfiniteChannel + rules []C.Rule + proxys map[string]C.Proxy + observable *observable.Observable + logCh chan interface{} + configLock *sync.RWMutex + traffic *C.Traffic +} + +func (t *Tunnel) Add(req C.ServerAdapter) { + t.queue.In() <- req +} + +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) 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 := strings.Split(key.Value(), ",") + if len(proxy) == 0 { + continue + } + proxy = trimArr(proxy) + 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 + } + } + + // 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(), ",") + 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 + } + } + + // 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() + } + } + + t.proxys = proxys + t.rules = rules + + return nil +} + +func (t *Tunnel) process() { + queue := t.queue.Out() + for { + elm := <-queue + conn := elm.(C.ServerAdapter) + go t.handleConn(conn) + } +} + +func (t *Tunnel) handleConn(localConn C.ServerAdapter) { + defer localConn.Close() + addr := localConn.Addr() + proxy := t.match(addr) + remoConn, err := proxy.Generator(addr) + if err != nil { + t.logCh <- newLog(WARNING, "Proxy connect error: %s", err.Error()) + return + } + defer remoConn.Close() + + localConn.Connect(remoConn) +} + +func (t *Tunnel) match(addr *C.Addr) C.Proxy { + t.configLock.RLock() + defer t.configLock.RUnlock() + + for _, rule := range t.rules { + if rule.IsMatch(addr) { + a, ok := t.proxys[rule.Adapter()] + if !ok { + continue + } + t.logCh <- newLog(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"] +} + +func newTunnel() *Tunnel { + logCh := make(chan interface{}) + tunnel := &Tunnel{ + queue: channels.NewInfiniteChannel(), + proxys: make(map[string]C.Proxy), + observable: observable.NewObservable(logCh), + logCh: logCh, + configLock: &sync.RWMutex{}, + traffic: C.NewTraffic(time.Second), + } + go tunnel.process() + go tunnel.subscribeLogs() + return tunnel +} + +func GetInstance() *Tunnel { + once.Do(func() { + tunnel = newTunnel() + }) + return tunnel +} diff --git a/tunnel/utils.go b/tunnel/utils.go new file mode 100644 index 0000000..4e72f18 --- /dev/null +++ b/tunnel/utils.go @@ -0,0 +1,12 @@ +package tunnel + +import ( + "strings" +) + +func trimArr(arr []string) (r []string) { + for _, e := range arr { + r = append(r, strings.Trim(e, " ")) + } + return +}