From a81e94cd4f6f978084db27ae3a8b549f0d28ef06 Mon Sep 17 00:00:00 2001 From: Elegybackup Date: Sat, 11 Nov 2023 16:07:53 +0800 Subject: [PATCH] Signed-off-by: Elegybackup --- .gitignore | 15 +++ .travis.yml | 27 +++++ Dockerfile | 19 +++ Gopkg.lock | 97 +++++++++++++++ Gopkg.toml | 58 +++++++++ LICENSE | 21 ++++ Makefile | 23 ++++ adapters/direct.go | 49 ++++++++ adapters/reject.go | 50 ++++++++ adapters/shadowsocks.go | 105 +++++++++++++++++ adapters/urltest.go | 160 +++++++++++++++++++++++++ adapters/util.go | 28 +++++ constant/adapters.go | 23 ++++ constant/addr.go | 40 +++++++ constant/config.go | 112 ++++++++++++++++++ constant/rule.go | 36 ++++++ constant/traffic.go | 55 +++++++++ docs/logo.png | Bin 0 -> 14428 bytes hub/configs.go | 67 +++++++++++ hub/server.go | 115 ++++++++++++++++++ main.go | 49 ++++++++ observable/iterable.go | 18 +++ observable/observable.go | 68 +++++++++++ observable/observable_test.go | 117 ++++++++++++++++++ observable/subscriber.go | 35 ++++++ observable/util.go | 15 +++ proxy/http/http.go | 77 ++++++++++++ proxy/http/https.go | 35 ++++++ proxy/http/server.go | 86 ++++++++++++++ proxy/socks/tcp.go | 98 ++++++++++++++++ proxy/socks/udp.go | 1 + rules/domain_keyword.go | 39 ++++++ rules/domain_suffix.go | 39 ++++++ rules/final.go | 31 +++++ rules/geoip.go | 50 ++++++++ rules/ipcidr.go | 42 +++++++ tunnel/log.go | 67 +++++++++++ tunnel/tunnel.go | 215 ++++++++++++++++++++++++++++++++++ tunnel/utils.go | 12 ++ 39 files changed, 2194 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Dockerfile create mode 100644 Gopkg.lock create mode 100644 Gopkg.toml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 adapters/direct.go create mode 100644 adapters/reject.go create mode 100644 adapters/shadowsocks.go create mode 100644 adapters/urltest.go create mode 100644 adapters/util.go create mode 100644 constant/adapters.go create mode 100644 constant/addr.go create mode 100644 constant/config.go create mode 100644 constant/rule.go create mode 100644 constant/traffic.go create mode 100644 docs/logo.png create mode 100644 hub/configs.go create mode 100644 hub/server.go create mode 100644 main.go create mode 100644 observable/iterable.go create mode 100644 observable/observable.go create mode 100644 observable/observable_test.go create mode 100644 observable/subscriber.go create mode 100644 observable/util.go create mode 100644 proxy/http/http.go create mode 100644 proxy/http/https.go create mode 100644 proxy/http/server.go create mode 100644 proxy/socks/tcp.go create mode 100644 proxy/socks/udp.go create mode 100644 rules/domain_keyword.go create mode 100644 rules/domain_suffix.go create mode 100644 rules/final.go create mode 100644 rules/geoip.go create mode 100644 rules/ipcidr.go create mode 100644 tunnel/log.go create mode 100644 tunnel/tunnel.go create mode 100644 tunnel/utils.go 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 0000000000000000000000000000000000000000..c624b94d23b333e4cb2406ccc04c9599552e0bac GIT binary patch literal 14428 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 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 +}