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
+}