Init: first commit 🎉
This commit is contained in:
parent
8532718345
commit
4f192ef575
27 changed files with 1451 additions and 0 deletions
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
|
@ -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
|
84
Gopkg.lock
generated
Normal file
84
Gopkg.lock
generated
Normal file
|
@ -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
|
50
Gopkg.toml
Normal file
50
Gopkg.toml
Normal file
|
@ -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
|
46
README.md
Normal file
46
README.md
Normal file
|
@ -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
|
44
adapters/direct.go
Normal file
44
adapters/direct.go
Normal file
|
@ -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{}
|
||||||
|
}
|
46
adapters/reject.go
Normal file
46
adapters/reject.go
Normal file
|
@ -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
|
||||||
|
}
|
97
adapters/shadowsocks.go
Normal file
97
adapters/shadowsocks.go
Normal file
|
@ -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(""))
|
||||||
|
}
|
20
constant/adapters.go
Normal file
20
constant/adapters.go
Normal file
|
@ -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)
|
||||||
|
}
|
15
constant/addr.go
Normal file
15
constant/addr.go
Normal file
|
@ -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
|
||||||
|
}
|
107
constant/config.go
Normal file
107
constant/config.go
Normal file
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
18
constant/rule.go
Normal file
18
constant/rule.go
Normal file
|
@ -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
|
||||||
|
}
|
42
main.go
Normal file
42
main.go
Normal file
|
@ -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
|
||||||
|
}
|
18
observable/iterable.go
Normal file
18
observable/iterable.go
Normal file
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
68
observable/observable.go
Normal file
68
observable/observable.go
Normal file
|
@ -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
|
||||||
|
}
|
117
observable/observable_test.go
Normal file
117
observable/observable_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
35
observable/subscriber.go
Normal file
35
observable/subscriber.go
Normal file
|
@ -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
|
||||||
|
}
|
15
observable/util.go
Normal file
15
observable/util.go
Normal file
|
@ -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
|
||||||
|
}
|
120
proxy/http.go
Normal file
120
proxy/http.go
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
93
proxy/socks.go
Normal file
93
proxy/socks.go
Normal file
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
35
rules/domain_keyword.go
Normal file
35
rules/domain_keyword.go
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
35
rules/domain_suffix.go
Normal file
35
rules/domain_suffix.go
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
27
rules/final.go
Normal file
27
rules/final.go
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
52
rules/geoip.go
Normal file
52
rules/geoip.go
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
42
rules/ipcidr.go
Normal file
42
rules/ipcidr.go
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
52
tunnel/log.go
Normal file
52
tunnel/log.go
Normal file
|
@ -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...),
|
||||||
|
}
|
||||||
|
}
|
146
tunnel/tunnel.go
Normal file
146
tunnel/tunnel.go
Normal file
|
@ -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
|
||||||
|
}
|
12
tunnel/utils.go
Normal file
12
tunnel/utils.go
Normal file
|
@ -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
|
||||||
|
}
|
Loading…
Reference in a new issue