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/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..84d8285 --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,84 @@ +# 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/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", + "poly1305", + "ssh/terminal" + ] + revision = "8ac0e0d97ce45cd83d1d7243c060cb8461dda5e9" + +[[projects]] + branch = "master" + name = "golang.org/x/sys" + packages = [ + "cpu", + "unix", + "windows" + ] + revision = "9527bec2660bd847c050fda93a0f0c6dee0800bb" + +[[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 = "4297c505508c6cdd8c94fd4ef29bc6940c65fea81e125fcf871a316b6e671a71" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..7ce6475 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,50 @@ +# 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/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/README.md b/README.md new file mode 100644 index 0000000..54a10bd --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Clash + +A rule based proxy in Go. + +## Features + +- HTTP/HTTPS and SOCKS proxy +- Surge like configuration +- GeoIP rule support + +## Install + +You can build from source: + +```sh +go get -u -v github.com/Dreamacro/clash +``` + +Requires Go >= 1.10. + +## Config + +Configuration file at `$HOME/.config/clash/config.ini` + +Below is a simple demo configuration file: + +```ini +[General] +port = 7890 +socks-port = 7891 + +[Proxy] +# name = ss, server, port, cipter, password +Proxy = ss, server, port, AEAD_CHACHA20_POLY1305, password + +[Rule] +DOMAIN-SUFFIX,google.com,Proxy +DOMAIN-KEYWORD,google,Proxy +DOMAIN-SUFFIX,ad.com,REJECT +GEOIP,CN,DIRECT +FINAL,,Proxy +``` + +## TODO + +- [ ] Complementing the necessary rule operators diff --git a/adapters/direct.go b/adapters/direct.go new file mode 100644 index 0000000..6c12703 --- /dev/null +++ b/adapters/direct.go @@ -0,0 +1,44 @@ +package adapters + +import ( + "io" + "net" + + C "github.com/Dreamacro/clash/constant" +) + +// DirectAdapter is a directly connected adapter +type DirectAdapter struct { + conn net.Conn +} + +// Writer is used to output network traffic +func (d *DirectAdapter) Writer() io.Writer { + return d.conn +} + +// Reader is used to input network traffic +func (d *DirectAdapter) Reader() io.Reader { + return d.conn +} + +// Close is used to close connection +func (d *DirectAdapter) Close() { + d.conn.Close() +} + +type Direct struct { +} + +func (d *Direct) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) { + c, err := net.Dial("tcp", net.JoinHostPort(addr.Host, addr.Port)) + if err != nil { + return + } + c.(*net.TCPConn).SetKeepAlive(true) + return &DirectAdapter{conn: c}, nil +} + +func NewDirect() *Direct { + return &Direct{} +} diff --git a/adapters/reject.go b/adapters/reject.go new file mode 100644 index 0000000..bfceb81 --- /dev/null +++ b/adapters/reject.go @@ -0,0 +1,46 @@ +package adapters + +import ( + "io" + + C "github.com/Dreamacro/clash/constant" +) + +// RejectAdapter is a reject connected adapter +type RejectAdapter struct { +} + +// Writer is used to output network traffic +func (r *RejectAdapter) Writer() io.Writer { + return &NopRW{} +} + +// Reader is used to input network traffic +func (r *RejectAdapter) Reader() io.Reader { + return &NopRW{} +} + +// Close is used to close connection +func (r *RejectAdapter) Close() { +} + +type Reject struct { +} + +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..bc27d91 --- /dev/null +++ b/adapters/shadowsocks.go @@ -0,0 +1,97 @@ +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 +} + +// Writer is used to output network traffic +func (ss *ShadowsocksAdapter) Writer() io.Writer { + return ss.conn +} + +// Reader is used to input network traffic +func (ss *ShadowsocksAdapter) Reader() io.Reader { + return ss.conn +} + +// Close is used to close connection +func (ss *ShadowsocksAdapter) Close() { + ss.conn.Close() +} + +type ShadowSocks struct { + server string + cipher string + password string +} + +func (ss *ShadowSocks) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) { + var key []byte + ciph, _ := core.PickCipher(ss.cipher, key, ss.password) + c, err := net.Dial("tcp", ss.server) + if err != nil { + return nil, fmt.Errorf("%s connect error", ss.server) + } + c.(*net.TCPConn).SetKeepAlive(true) + c = ciph.StreamConn(c) + _, err = c.Write(serializesSocksAddr(addr)) + return &ShadowsocksAdapter{conn: c}, err +} + +func NewShadowSocks(ssURL string) *ShadowSocks { + server, cipher, password, _ := parseURL(ssURL) + return &ShadowSocks{ + server: server, + cipher: cipher, + password: password, + } +} + +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{[]byte{aType, len}, host, port} + case socks.AtypIPv4: + host := net.ParseIP(addr.Host).To4() + buf = [][]byte{[]byte{aType}, host, port} + case socks.AtypIPv6: + host := net.ParseIP(addr.Host).To16() + buf = [][]byte{[]byte{aType}, host, port} + } + return bytes.Join(buf, []byte("")) +} diff --git a/constant/adapters.go b/constant/adapters.go new file mode 100644 index 0000000..63b4908 --- /dev/null +++ b/constant/adapters.go @@ -0,0 +1,20 @@ +package constant + +import ( + "io" +) + +type ProxyAdapter interface { + Writer() io.Writer + Reader() io.Reader + Close() +} + +type ServerAdapter interface { + Addr() *Addr + ProxyAdapter +} + +type Proxy interface { + Generator(addr *Addr) (ProxyAdapter, error) +} diff --git a/constant/addr.go b/constant/addr.go new file mode 100644 index 0000000..6dcd9ad --- /dev/null +++ b/constant/addr.go @@ -0,0 +1,15 @@ +package constant + +// Socks addr type +const ( + AtypIPv4 = 1 + AtypDomainName = 3 + AtypIPv6 = 4 +) + +// Addr is used to store connection address +type Addr struct { + AddrType int + Host string + Port string +} diff --git a/constant/config.go b/constant/config.go new file mode 100644 index 0000000..caa8c2d --- /dev/null +++ b/constant/config.go @@ -0,0 +1,107 @@ +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 { + log.Fatalf("Can't get current user: %s", err.Error()) + } + 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..6a65feb --- /dev/null +++ b/constant/rule.go @@ -0,0 +1,18 @@ +package constant + +// Rule Type +const ( + DomainSuffix RuleType = iota + DomainKeyword + GEOIP + IPCIDR + FINAL +) + +type RuleType int + +type Rule interface { + RuleType() RuleType + IsMatch(addr *Addr) bool + Adapter() string +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..4bbaf6a --- /dev/null +++ b/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "os" + "os/signal" + "syscall" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/proxy" + "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 proxy.NewHttpProxy(port) + go proxy.NewSocksProxy(socksPort) + + 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.go b/proxy/http.go new file mode 100644 index 0000000..232e378 --- /dev/null +++ b/proxy/http.go @@ -0,0 +1,120 @@ +package proxy + +import ( + "bufio" + "bytes" + "crypto/tls" + "fmt" + "io" + "net" + "net/http" + "net/http/httputil" + "strings" + + "github.com/Dreamacro/clash/constant" + + "github.com/riobard/go-shadowsocks2/socks" + log "github.com/sirupsen/logrus" +) + +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) + } + }), + // Disable HTTP/2. + TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), + } + log.Infof("HTTP proxy :%s", port) + server.ListenAndServe() +} + +func handleHTTP(w http.ResponseWriter, r *http.Request) { + buf, _ := httputil.DumpRequestOut(r, true) + hijacker, ok := w.(http.Hijacker) + if !ok { + return + } + conn, rw, err := hijacker.Hijack() + if err != nil { + return + } + addr := r.Host + // padding default port + if !strings.Contains(addr, ":") { + addr += ":80" + } + tun.Add(NewHttp(addr, conn, rw, buf)) +} + +func handleTunneling(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + hijacker, ok := w.(http.Hijacker) + if !ok { + return + } + conn, rw, err := hijacker.Hijack() + if err != nil { + return + } + tun.Add(NewHttp(r.Host, conn, rw, []byte{})) +} + +type HttpAdapter struct { + addr *constant.Addr + conn net.Conn + rw *bufio.ReadWriter + r io.Reader +} + +func (h *HttpAdapter) Writer() io.Writer { + return h.conn +} + +func (h *HttpAdapter) Reader() io.Reader { + return h.r +} + +func (h *HttpAdapter) Close() { + h.conn.Close() +} + +func (h *HttpAdapter) Addr() *constant.Addr { + return h.addr +} + +func parseHttpAddr(target string) *constant.Addr { + host, port, _ := net.SplitHostPort(target) + + 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 &constant.Addr{ + AddrType: addType, + Host: host, + Port: port, + } +} + +func NewHttp(host string, conn net.Conn, rw *bufio.ReadWriter, payload []byte) *HttpAdapter { + r := io.MultiReader(bytes.NewReader(payload), rw) + return &HttpAdapter{ + conn: conn, + addr: parseHttpAddr(host), + rw: rw, + r: r, + } +} diff --git a/proxy/socks.go b/proxy/socks.go new file mode 100644 index 0000000..106d482 --- /dev/null +++ b/proxy/socks.go @@ -0,0 +1,93 @@ +package proxy + +import ( + "fmt" + "io" + "net" + "strconv" + + "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.(*net.TCPConn).SetKeepAlive(true) + tun.Add(NewSocks(target, conn)) +} + +type SocksAdapter struct { + conn net.Conn + addr *constant.Addr +} + +func (s *SocksAdapter) Writer() io.Writer { + return s.conn +} + +func (s *SocksAdapter) Reader() io.Reader { + return s.conn +} + +func (s *SocksAdapter) Close() { + s.conn.Close() +} + +func (s *SocksAdapter) Addr() *constant.Addr { + return s.addr +} + +func parseSocksAddr(target socks.Addr) *constant.Addr { + var host, port string + + 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])) + case socks.AtypIPv4: + host = net.IP(target[1 : 1+net.IPv4len]).String() + port = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1])) + case socks.AtypIPv6: + host = net.IP(target[1 : 1+net.IPv6len]).String() + port = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1])) + } + + return &constant.Addr{ + AddrType: int(target[0]), + Host: host, + Port: port, + } +} + +func NewSocks(target socks.Addr, conn net.Conn) *SocksAdapter { + return &SocksAdapter{ + conn: conn, + addr: parseSocksAddr(target), + } +} diff --git a/rules/domain_keyword.go b/rules/domain_keyword.go new file mode 100644 index 0000000..a235dc7 --- /dev/null +++ b/rules/domain_keyword.go @@ -0,0 +1,35 @@ +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 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..42f4fc8 --- /dev/null +++ b/rules/domain_suffix.go @@ -0,0 +1,35 @@ +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 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..c82768c --- /dev/null +++ b/rules/final.go @@ -0,0 +1,27 @@ +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 NewFinal(adapter string) *Final { + return &Final{ + adapter: adapter, + } +} diff --git a/rules/geoip.go b/rules/geoip.go new file mode 100644 index 0000000..6975eef --- /dev/null +++ b/rules/geoip.go @@ -0,0 +1,52 @@ +package rules + +import ( + "net" + + 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.AddrType == C.AtypDomainName { + return false + } + dstIP := net.ParseIP(addr.Host) + if dstIP == nil { + return false + } + record, _ := mmdb.Country(dstIP) + return record.Country.IsoCode == g.country +} + +func (g *GEOIP) Adapter() string { + return g.adapter +} + +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..16eb08b --- /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.AddrType == C.AtypDomainName { + return false + } + ip := net.ParseIP(addr.Host) + if ip == nil { + return false + } + + return i.ipnet.Contains(ip) +} + +func (g *IPCIDR) Adapter() string { + return g.adapter +} + +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..639b883 --- /dev/null +++ b/tunnel/log.go @@ -0,0 +1,52 @@ +package tunnel + +import ( + "fmt" + + log "github.com/sirupsen/logrus" +) + +const ( + INFO LogType = iota + WARNING + ERROR + DEBUG +) + +type LogType int + +type Log struct { + LogType LogType + Payload string +} + +func print(data Log) { + switch data.LogType { + 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(logType LogType, format string, v ...interface{}) Log { + return Log{ + LogType: logType, + Payload: fmt.Sprintf(format, v...), + } +} diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go new file mode 100644 index 0000000..53cd4de --- /dev/null +++ b/tunnel/tunnel.go @@ -0,0 +1,146 @@ +package tunnel + +import ( + "fmt" + "io" + "strings" + "sync" + + "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{} +} + +func (t *Tunnel) Add(req C.ServerAdapter) { + t.queue.In() <- req +} + +func (t *Tunnel) UpdateConfig() (err error) { + cfg, err := C.GetConfig() + if err != nil { + return + } + + proxys := cfg.Section("Proxy") + rules := cfg.Section("Rule") + + // parse proxy + for _, key := range proxys.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]) + t.proxys[key.Name()] = adapters.NewShadowSocks(ssURL) + } + } + + // init proxy + t.proxys["DIRECT"] = adapters.NewDirect() + t.proxys["REJECT"] = adapters.NewReject() + + // parse rules + for _, key := range rules.Keys() { + rule := strings.Split(key.Name(), ",") + if len(rule) < 3 { + continue + } + rule = trimArr(rule) + switch rule[0] { + case "DOMAIN-SUFFIX": + t.rules = append(t.rules, R.NewDomainSuffix(rule[1], rule[2])) + case "DOMAIN-KEYWORD": + t.rules = append(t.rules, R.NewDomainKeyword(rule[1], rule[2])) + case "GEOIP": + t.rules = append(t.rules, R.NewGEOIP(rule[1], rule[2])) + case "IP-CIDR", "IP-CIDR6": + t.rules = append(t.rules, R.NewIPCIDR(rule[1], rule[2])) + case "FINAL": + t.rules = append(t.rules, R.NewFinal(rule[2])) + } + } + + 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() + + go io.Copy(localConn.Writer(), remoConn.Reader()) + io.Copy(remoConn.Writer(), localConn.Reader()) +} + +func (t *Tunnel) match(addr *C.Addr) C.Proxy { + for _, rule := range t.rules { + if rule.IsMatch(addr) { + a, ok := t.proxys[rule.Adapter()] + if !ok { + continue + } + t.logCh <- newLog(INFO, "%v match %d using %s", addr.Host, rule.RuleType(), rule.Adapter()) + return a + } + } + t.logCh <- newLog(INFO, "don't find, direct") + 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, + } + 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 +}