mirror of
https://github.com/Elegycloud/clash-backup.git
synced 2024-12-23 00:47:23 +08:00
Signed-off-by: Elegybackup <wangeyun@qq.com>
This commit is contained in:
parent
550e66133e
commit
a81e94cd4f
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
|
27
.travis.yml
Normal file
27
.travis.yml
Normal file
@ -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
|
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@ -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"]
|
97
Gopkg.lock
generated
Normal file
97
Gopkg.lock
generated
Normal file
@ -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
|
58
Gopkg.toml
Normal file
58
Gopkg.toml
Normal file
@ -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
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -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.
|
23
Makefile
Normal file
23
Makefile
Normal file
@ -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)/*
|
49
adapters/direct.go
Normal file
49
adapters/direct.go
Normal file
@ -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}
|
||||||
|
}
|
50
adapters/reject.go
Normal file
50
adapters/reject.go
Normal file
@ -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
|
||||||
|
}
|
105
adapters/shadowsocks.go
Normal file
105
adapters/shadowsocks.go
Normal file
@ -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(""))
|
||||||
|
}
|
160
adapters/urltest.go
Normal file
160
adapters/urltest.go
Normal file
@ -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
|
||||||
|
}
|
28
adapters/util.go
Normal file
28
adapters/util.go
Normal file
@ -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}
|
||||||
|
}
|
23
constant/adapters.go
Normal file
23
constant/adapters.go
Normal file
@ -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)
|
||||||
|
}
|
40
constant/addr.go
Normal file
40
constant/addr.go
Normal file
@ -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
|
||||||
|
}
|
112
constant/config.go
Normal file
112
constant/config.go
Normal file
@ -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,
|
||||||
|
)
|
||||||
|
}
|
36
constant/rule.go
Normal file
36
constant/rule.go
Normal file
@ -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
|
||||||
|
}
|
55
constant/traffic.go
Normal file
55
constant/traffic.go
Normal file
@ -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
|
||||||
|
}
|
BIN
docs/logo.png
Normal file
BIN
docs/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
67
hub/configs.go
Normal file
67
hub/configs.go
Normal file
@ -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)
|
||||||
|
}
|
115
hub/server.go
Normal file
115
hub/server.go
Normal file
@ -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()
|
||||||
|
}
|
||||||
|
}
|
49
main.go
Normal file
49
main.go
Normal file
@ -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
|
||||||
|
}
|
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
|
||||||
|
}
|
77
proxy/http/http.go
Normal file
77
proxy/http/http.go
Normal file
@ -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
|
||||||
|
}
|
35
proxy/http/https.go
Normal file
35
proxy/http/https.go
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
86
proxy/http/server.go
Normal file
86
proxy/http/server.go
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
98
proxy/socks/tcp.go
Normal file
98
proxy/socks/tcp.go
Normal file
@ -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),
|
||||||
|
}
|
||||||
|
}
|
1
proxy/socks/udp.go
Normal file
1
proxy/socks/udp.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package socks
|
39
rules/domain_keyword.go
Normal file
39
rules/domain_keyword.go
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
39
rules/domain_suffix.go
Normal file
39
rules/domain_suffix.go
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
31
rules/final.go
Normal file
31
rules/final.go
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
50
rules/geoip.go
Normal file
50
rules/geoip.go
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
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.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,
|
||||||
|
}
|
||||||
|
}
|
67
tunnel/log.go
Normal file
67
tunnel/log.go
Normal file
@ -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...),
|
||||||
|
}
|
||||||
|
}
|
215
tunnel/tunnel.go
Normal file
215
tunnel/tunnel.go
Normal file
@ -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
|
||||||
|
}
|
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 New Issue
Block a user