Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
ebe1cee6dc | |||
fc4f119049 | |||
2b87b907ae | |||
35e572406b | |||
228d674d93 | |||
752944d381 | |||
63308472ad | |||
0208e32933 | |||
410b272b50 | |||
ea424a7694 | |||
36f4ceafa1 | |||
674f4be24d | |||
63c967b2b3 | |||
a1a58c31ae | |||
295d649624 | |||
7347c28f75 | |||
8389150318 | |||
7357d2d0c2 | |||
d540a0d29f | |||
d55e1b664b |
16
Gopkg.lock
generated
16
Gopkg.lock
generated
@ -19,6 +19,12 @@
|
||||
revision = "e83ac2304db3c50cf03d96a2fcd39009d458bc35"
|
||||
version = "v3.3.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-chi/cors"
|
||||
packages = ["."]
|
||||
revision = "dba6525398619dead495962a916728e7ee2ca322"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-chi/render"
|
||||
packages = ["."]
|
||||
@ -65,7 +71,7 @@
|
||||
"poly1305",
|
||||
"ssh/terminal"
|
||||
]
|
||||
revision = "027cca12c2d63e3d62b670d901e8a2c95854feec"
|
||||
revision = "a2144134853fc9a27a7b1e3eb4f19f1a76df13c9"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@ -75,7 +81,7 @@
|
||||
"unix",
|
||||
"windows"
|
||||
]
|
||||
revision = "6c888cc515d3ed83fc103cf1d84468aad274b0a7"
|
||||
revision = "ac767d655b305d4e9612f5f6e33120b9176c4ad4"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/eapache/channels.v1"
|
||||
@ -86,12 +92,12 @@
|
||||
[[projects]]
|
||||
name = "gopkg.in/ini.v1"
|
||||
packages = ["."]
|
||||
revision = "06f5f3d67269ccec1fe5fe4134ba6e982984f7f5"
|
||||
version = "v1.37.0"
|
||||
revision = "358ee7663966325963d4e8b2e1fbd570c5195153"
|
||||
version = "v1.38.1"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "c3c901e4e393a2df9e421924d3a4d85ec73642e36dcbc1ddca5fc13159220e86"
|
||||
inputs-digest = "0ccb06b3617c87e75bd650f92adc99e55b93070e0b2a0bc71634270226e125fc"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
@ -29,6 +29,10 @@
|
||||
name = "github.com/go-chi/chi"
|
||||
version = "3.3.2"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-chi/cors"
|
||||
version = "1.0.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-chi/render"
|
||||
version = "1.0.1"
|
||||
@ -51,7 +55,7 @@
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/ini.v1"
|
||||
version = "1.37.0"
|
||||
version = "1.38.1"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
|
2
Makefile
2
Makefile
@ -2,7 +2,7 @@ NAME=clash
|
||||
BINDIR=bin
|
||||
GOBUILD=CGO_ENABLED=0 go build -ldflags '-w -s'
|
||||
|
||||
all: linux macos
|
||||
all: linux macos win64
|
||||
|
||||
linux:
|
||||
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
32
README.md
32
README.md
@ -15,7 +15,6 @@
|
||||
<a href="https://goreportcard.com/report/github.com/Dreamacro/clash">
|
||||
<img src="https://goreportcard.com/badge/github.com/Dreamacro/clash?style=flat-square">
|
||||
</a>
|
||||
<a href="https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_shield" alt="FOSSA Status"><img src="https://app.fossa.io/api/projects/git%2Bgithub.com%2FDreamacro%2Fclash.svg?type=shield"/></a>
|
||||
<a href="https://github.com/Dreamacro/clash/releases">
|
||||
<img src="https://img.shields.io/github/release/Dreamacro/clash/all.svg?style=flat-square">
|
||||
</a>
|
||||
@ -66,6 +65,9 @@ Below is a simple demo configuration file:
|
||||
port = 7890
|
||||
socks-port = 7891
|
||||
|
||||
# redir proxy for Linux and macOS
|
||||
redir-port = 7892
|
||||
|
||||
# A RESTful API for clash
|
||||
external-controller = 127.0.0.1:8080
|
||||
|
||||
@ -73,13 +75,21 @@ external-controller = 127.0.0.1:8080
|
||||
# name = ss, server, port, cipher, password
|
||||
# The types of cipher are consistent with go-shadowsocks2
|
||||
# support AEAD_AES_128_GCM AEAD_AES_192_GCM AEAD_AES_256_GCM AEAD_CHACHA20_POLY1305 AES-128-CTR AES-192-CTR AES-256-CTR AES-128-CFB AES-192-CFB AES-256-CFB CHACHA20-IETF XCHACHA20
|
||||
Proxy1 = ss, server1, port, AEAD_CHACHA20_POLY1305, password
|
||||
Proxy2 = ss, server2, port, AEAD_CHACHA20_POLY1305, password
|
||||
ss1 = ss, server1, port, AEAD_CHACHA20_POLY1305, password
|
||||
ss2 = ss, server2, port, AEAD_CHACHA20_POLY1305, password
|
||||
|
||||
# name = socks5, server, port
|
||||
socks = socks5, server1, port
|
||||
|
||||
[Proxy Group]
|
||||
# url-test select which proxy will be used by benchmarking speed to a URL.
|
||||
# name = url-test, [proxys], url, interval(second)
|
||||
Proxy = url-test, Proxy1, Proxy2, http://www.google.com/generate_204, 300
|
||||
# name = url-test, [proxies], url, interval(second)
|
||||
auto = url-test, ss1, ss2, http://www.google.com/generate_204, 300
|
||||
|
||||
# select is used for selecting proxy or proxy group
|
||||
# you can use RESTful API to switch proxy, is recommended for use in GUI.
|
||||
# name = select, [proxies]
|
||||
Proxy = select, ss1, ss2, auto
|
||||
|
||||
[Rule]
|
||||
DOMAIN-SUFFIX,google.com,Proxy
|
||||
@ -89,9 +99,19 @@ GEOIP,CN,DIRECT
|
||||
FINAL,,Proxy # note: there is two ","
|
||||
```
|
||||
|
||||
## Thanks
|
||||
|
||||
[riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
|
||||
|
||||
[google/tcpproxy](https://github.com/google/tcpproxy)
|
||||
|
||||
## License
|
||||
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large)
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Complementing the necessary rule operators
|
||||
- [x] Complementing the necessary rule operators
|
||||
- [x] Redir proxy
|
||||
- [ ] UDP support
|
||||
- [ ] Connection manager
|
||||
|
82
adapters/local/http.go
Normal file
82
adapters/local/http.go
Normal file
@ -0,0 +1,82 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
// PeekedConn handle http connection and buffed HTTP data
|
||||
type PeekedConn struct {
|
||||
net.Conn
|
||||
Peeked []byte
|
||||
host string
|
||||
isHTTP bool
|
||||
}
|
||||
|
||||
func (c *PeekedConn) Read(p []byte) (n int, err error) {
|
||||
if len(c.Peeked) > 0 {
|
||||
n = copy(p, c.Peeked)
|
||||
c.Peeked = c.Peeked[n:]
|
||||
if len(c.Peeked) == 0 {
|
||||
c.Peeked = nil
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Sometimes firefox just open a socket to process multiple domains in HTTP
|
||||
// The temporary solution is to return io.EOF when encountering different HOST
|
||||
if c.isHTTP {
|
||||
br := bufio.NewReader(bytes.NewReader(p))
|
||||
_, hostName := ParserHTTPHostHeader(br)
|
||||
if hostName != "" {
|
||||
if !strings.Contains(hostName, ":") {
|
||||
hostName += ":80"
|
||||
}
|
||||
|
||||
if hostName != c.host {
|
||||
return 0, io.EOF
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c.Conn.Read(p)
|
||||
}
|
||||
|
||||
// HTTPAdapter is a adapter for HTTP connection
|
||||
type HTTPAdapter struct {
|
||||
addr *C.Addr
|
||||
conn *PeekedConn
|
||||
}
|
||||
|
||||
// Close HTTP connection
|
||||
func (h *HTTPAdapter) Close() {
|
||||
h.conn.Close()
|
||||
}
|
||||
|
||||
// Addr return destination address
|
||||
func (h *HTTPAdapter) Addr() *C.Addr {
|
||||
return h.addr
|
||||
}
|
||||
|
||||
// Conn return raw net.Conn of HTTP
|
||||
func (h *HTTPAdapter) Conn() net.Conn {
|
||||
return h.conn
|
||||
}
|
||||
|
||||
// NewHTTP is HTTPAdapter generator
|
||||
func NewHTTP(host string, peeked []byte, isHTTP bool, conn net.Conn) *HTTPAdapter {
|
||||
return &HTTPAdapter{
|
||||
addr: parseHTTPAddr(host),
|
||||
conn: &PeekedConn{
|
||||
Peeked: peeked,
|
||||
Conn: conn,
|
||||
host: host,
|
||||
isHTTP: isHTTP,
|
||||
},
|
||||
}
|
||||
}
|
37
adapters/local/socks.go
Normal file
37
adapters/local/socks.go
Normal file
@ -0,0 +1,37 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/riobard/go-shadowsocks2/socks"
|
||||
)
|
||||
|
||||
// SocksAdapter is a adapter for socks and redir connection
|
||||
type SocksAdapter struct {
|
||||
conn net.Conn
|
||||
addr *C.Addr
|
||||
}
|
||||
|
||||
// Close socks and redir connection
|
||||
func (s *SocksAdapter) Close() {
|
||||
s.conn.Close()
|
||||
}
|
||||
|
||||
// Addr return destination address
|
||||
func (s *SocksAdapter) Addr() *C.Addr {
|
||||
return s.addr
|
||||
}
|
||||
|
||||
// Conn return raw net.Conn
|
||||
func (s *SocksAdapter) Conn() net.Conn {
|
||||
return s.conn
|
||||
}
|
||||
|
||||
// NewSocks is SocksAdapter generator
|
||||
func NewSocks(target socks.Addr, conn net.Conn) *SocksAdapter {
|
||||
return &SocksAdapter{
|
||||
conn: conn,
|
||||
addr: parseSocksAddr(target),
|
||||
}
|
||||
}
|
140
adapters/local/util.go
Normal file
140
adapters/local/util.go
Normal file
@ -0,0 +1,140 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/riobard/go-shadowsocks2/socks"
|
||||
)
|
||||
|
||||
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 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,
|
||||
}
|
||||
}
|
||||
|
||||
// ParserHTTPHostHeader returns the HTTP Host header from br without
|
||||
// consuming any of its bytes. It returns "" if it can't find one.
|
||||
func ParserHTTPHostHeader(br *bufio.Reader) (method, host string) {
|
||||
// br := bufio.NewReader(bytes.NewReader(data))
|
||||
const maxPeek = 4 << 10
|
||||
peekSize := 0
|
||||
for {
|
||||
peekSize++
|
||||
if peekSize > maxPeek {
|
||||
b, _ := br.Peek(br.Buffered())
|
||||
return method, httpHostHeaderFromBytes(b)
|
||||
}
|
||||
b, err := br.Peek(peekSize)
|
||||
if n := br.Buffered(); n > peekSize {
|
||||
b, _ = br.Peek(n)
|
||||
peekSize = n
|
||||
}
|
||||
if len(b) > 0 {
|
||||
if b[0] < 'A' || b[0] > 'Z' {
|
||||
// Doesn't look like an HTTP verb
|
||||
// (GET, POST, etc).
|
||||
return
|
||||
}
|
||||
if bytes.Index(b, crlfcrlf) != -1 || bytes.Index(b, lflf) != -1 {
|
||||
req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(b)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(req.Header["Host"]) > 1 {
|
||||
// TODO(bradfitz): what does
|
||||
// ReadRequest do if there are
|
||||
// multiple Host headers?
|
||||
return
|
||||
}
|
||||
return req.Method, req.Host
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return method, httpHostHeaderFromBytes(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
lfHostColon = []byte("\nHost:")
|
||||
lfhostColon = []byte("\nhost:")
|
||||
crlf = []byte("\r\n")
|
||||
lf = []byte("\n")
|
||||
crlfcrlf = []byte("\r\n\r\n")
|
||||
lflf = []byte("\n\n")
|
||||
)
|
||||
|
||||
func httpHostHeaderFromBytes(b []byte) string {
|
||||
if i := bytes.Index(b, lfHostColon); i != -1 {
|
||||
return string(bytes.TrimSpace(untilEOL(b[i+len(lfHostColon):])))
|
||||
}
|
||||
if i := bytes.Index(b, lfhostColon); i != -1 {
|
||||
return string(bytes.TrimSpace(untilEOL(b[i+len(lfhostColon):])))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// untilEOL returns v, truncated before the first '\n' byte, if any.
|
||||
// The returned slice may include a '\r' at the end.
|
||||
func untilEOL(v []byte) []byte {
|
||||
if i := bytes.IndexByte(v, '\n'); i != -1 {
|
||||
return v[:i]
|
||||
}
|
||||
return v
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
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) Type() C.AdapterType {
|
||||
return C.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
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
@ -12,11 +11,6 @@ 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()
|
||||
@ -27,9 +21,7 @@ func (d *DirectAdapter) Conn() net.Conn {
|
||||
return d.conn
|
||||
}
|
||||
|
||||
type Direct struct {
|
||||
traffic *C.Traffic
|
||||
}
|
||||
type Direct struct{}
|
||||
|
||||
func (d *Direct) Name() string {
|
||||
return "Direct"
|
||||
@ -45,9 +37,9 @@ func (d *Direct) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
|
||||
return
|
||||
}
|
||||
c.(*net.TCPConn).SetKeepAlive(true)
|
||||
return &DirectAdapter{conn: NewTrafficTrack(c, d.traffic)}, nil
|
||||
return &DirectAdapter{conn: c}, nil
|
||||
}
|
||||
|
||||
func NewDirect(traffic *C.Traffic) *Direct {
|
||||
return &Direct{traffic: traffic}
|
||||
func NewDirect() *Direct {
|
||||
return &Direct{}
|
||||
}
|
69
adapters/remote/reject.go
Normal file
69
adapters/remote/reject.go
Normal file
@ -0,0 +1,69 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
// RejectAdapter is a reject connected adapter
|
||||
type RejectAdapter struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
// Close is used to close connection
|
||||
func (r *RejectAdapter) Close() {}
|
||||
|
||||
// Conn is used to http request
|
||||
func (r *RejectAdapter) Conn() net.Conn {
|
||||
return r.conn
|
||||
}
|
||||
|
||||
type Reject struct {
|
||||
}
|
||||
|
||||
func (r *Reject) Name() string {
|
||||
return "Reject"
|
||||
}
|
||||
|
||||
func (r *Reject) Type() C.AdapterType {
|
||||
return C.Reject
|
||||
}
|
||||
|
||||
func (r *Reject) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
|
||||
return &RejectAdapter{conn: &NopConn{}}, nil
|
||||
}
|
||||
|
||||
func NewReject() *Reject {
|
||||
return &Reject{}
|
||||
}
|
||||
|
||||
type NopConn struct{}
|
||||
|
||||
func (rw *NopConn) Read(b []byte) (int, error) {
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (rw *NopConn) Write(b []byte) (int, error) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
// Close is fake function for net.Conn
|
||||
func (rw *NopConn) Close() error { return nil }
|
||||
|
||||
// LocalAddr is fake function for net.Conn
|
||||
func (rw *NopConn) LocalAddr() net.Addr { return nil }
|
||||
|
||||
// RemoteAddr is fake function for net.Conn
|
||||
func (rw *NopConn) RemoteAddr() net.Addr { return nil }
|
||||
|
||||
// SetDeadline is fake function for net.Conn
|
||||
func (rw *NopConn) SetDeadline(time.Time) error { return nil }
|
||||
|
||||
// SetReadDeadline is fake function for net.Conn
|
||||
func (rw *NopConn) SetReadDeadline(time.Time) error { return nil }
|
||||
|
||||
// SetWriteDeadline is fake function for net.Conn
|
||||
func (rw *NopConn) SetWriteDeadline(time.Time) error { return nil }
|
@ -2,6 +2,7 @@ package adapters
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
@ -9,7 +10,7 @@ import (
|
||||
type Selector struct {
|
||||
name string
|
||||
selected C.Proxy
|
||||
proxys map[string]C.Proxy
|
||||
proxies map[string]C.Proxy
|
||||
}
|
||||
|
||||
func (s *Selector) Name() string {
|
||||
@ -30,14 +31,15 @@ func (s *Selector) Now() string {
|
||||
|
||||
func (s *Selector) All() []string {
|
||||
var all []string
|
||||
for k, _ := range s.proxys {
|
||||
for k := range s.proxies {
|
||||
all = append(all, k)
|
||||
}
|
||||
sort.Strings(all)
|
||||
return all
|
||||
}
|
||||
|
||||
func (s *Selector) Set(name string) error {
|
||||
proxy, exist := s.proxys[name]
|
||||
proxy, exist := s.proxies[name]
|
||||
if !exist {
|
||||
return errors.New("Proxy does not exist")
|
||||
}
|
||||
@ -45,21 +47,21 @@ func (s *Selector) Set(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewSelector(name string, proxys map[string]C.Proxy) (*Selector, error) {
|
||||
if len(proxys) == 0 {
|
||||
func NewSelector(name string, proxies map[string]C.Proxy) (*Selector, error) {
|
||||
if len(proxies) == 0 {
|
||||
return nil, errors.New("Provide at least one proxy")
|
||||
}
|
||||
|
||||
mapping := make(map[string]C.Proxy)
|
||||
var init string
|
||||
for k, v := range proxys {
|
||||
for k, v := range proxies {
|
||||
mapping[k] = v
|
||||
init = k
|
||||
}
|
||||
s := &Selector{
|
||||
name: name,
|
||||
proxys: mapping,
|
||||
selected: proxys[init],
|
||||
proxies: mapping,
|
||||
selected: proxies[init],
|
||||
}
|
||||
return s, nil
|
||||
}
|
@ -3,7 +3,6 @@ package adapters
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
@ -19,11 +18,6 @@ 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()
|
||||
@ -34,10 +28,9 @@ func (ss *ShadowsocksAdapter) Conn() net.Conn {
|
||||
}
|
||||
|
||||
type ShadowSocks struct {
|
||||
server string
|
||||
name string
|
||||
cipher core.Cipher
|
||||
traffic *C.Traffic
|
||||
server string
|
||||
name string
|
||||
cipher core.Cipher
|
||||
}
|
||||
|
||||
func (ss *ShadowSocks) Name() string {
|
||||
@ -56,10 +49,10 @@ func (ss *ShadowSocks) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err erro
|
||||
c.(*net.TCPConn).SetKeepAlive(true)
|
||||
c = ss.cipher.StreamConn(c)
|
||||
_, err = c.Write(serializesSocksAddr(addr))
|
||||
return &ShadowsocksAdapter{conn: NewTrafficTrack(c, ss.traffic)}, err
|
||||
return &ShadowsocksAdapter{conn: c}, err
|
||||
}
|
||||
|
||||
func NewShadowSocks(name string, ssURL string, traffic *C.Traffic) (*ShadowSocks, error) {
|
||||
func NewShadowSocks(name string, ssURL string) (*ShadowSocks, error) {
|
||||
var key []byte
|
||||
server, cipher, password, _ := parseURL(ssURL)
|
||||
ciph, err := core.PickCipher(cipher, key, password)
|
||||
@ -67,10 +60,9 @@ func NewShadowSocks(name string, ssURL string, traffic *C.Traffic) (*ShadowSocks
|
||||
return nil, fmt.Errorf("ss %s initialize error: %s", server, err.Error())
|
||||
}
|
||||
return &ShadowSocks{
|
||||
server: server,
|
||||
name: name,
|
||||
cipher: ciph,
|
||||
traffic: traffic,
|
||||
server: server,
|
||||
name: name,
|
||||
cipher: ciph,
|
||||
}, nil
|
||||
}
|
||||
|
91
adapters/remote/socks5.go
Normal file
91
adapters/remote/socks5.go
Normal file
@ -0,0 +1,91 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
"github.com/riobard/go-shadowsocks2/socks"
|
||||
)
|
||||
|
||||
// Socks5Adapter is a shadowsocks adapter
|
||||
type Socks5Adapter struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
// Close is used to close connection
|
||||
func (ss *Socks5Adapter) Close() {
|
||||
ss.conn.Close()
|
||||
}
|
||||
|
||||
func (ss *Socks5Adapter) Conn() net.Conn {
|
||||
return ss.conn
|
||||
}
|
||||
|
||||
type Socks5 struct {
|
||||
addr string
|
||||
name string
|
||||
}
|
||||
|
||||
func (ss *Socks5) Name() string {
|
||||
return ss.name
|
||||
}
|
||||
|
||||
func (ss *Socks5) Type() C.AdapterType {
|
||||
return C.Socks5
|
||||
}
|
||||
|
||||
func (ss *Socks5) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
|
||||
c, err := net.Dial("tcp", ss.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error", ss.addr)
|
||||
}
|
||||
c.(*net.TCPConn).SetKeepAlive(true)
|
||||
|
||||
if err := ss.sharkHand(addr, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Socks5Adapter{conn: c}, nil
|
||||
}
|
||||
|
||||
func (ss *Socks5) sharkHand(addr *C.Addr, rw io.ReadWriter) error {
|
||||
buf := make([]byte, socks.MaxAddrLen)
|
||||
|
||||
// VER, CMD, RSV
|
||||
_, err := rw.Write([]byte{5, 1, 0})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(rw, buf[:2]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if buf[0] != 5 {
|
||||
return errors.New("SOCKS version error")
|
||||
} else if buf[1] != 0 {
|
||||
return errors.New("SOCKS need auth")
|
||||
}
|
||||
|
||||
// VER, CMD, RSV, ADDR
|
||||
if _, err := rw.Write(bytes.Join([][]byte{[]byte{5, 1, 0}, serializesSocksAddr(addr)}, []byte(""))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(rw, buf[:10]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewSocks5(name, addr string) *Socks5 {
|
||||
return &Socks5{
|
||||
addr: addr,
|
||||
name: name,
|
||||
}
|
||||
}
|
102
adapters/remote/urltest.go
Normal file
102
adapters/remote/urltest.go
Normal file
@ -0,0 +1,102 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type URLTest struct {
|
||||
name string
|
||||
proxies []C.Proxy
|
||||
rawURL string
|
||||
fast C.Proxy
|
||||
delay time.Duration
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func (u *URLTest) Name() string {
|
||||
return u.name
|
||||
}
|
||||
|
||||
func (u *URLTest) Type() C.AdapterType {
|
||||
return C.URLTest
|
||||
}
|
||||
|
||||
func (u *URLTest) Now() string {
|
||||
return u.fast.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.proxies))
|
||||
c := make(chan interface{})
|
||||
fast := selectFast(c)
|
||||
timer := time.NewTimer(u.delay)
|
||||
|
||||
for _, p := range u.proxies {
|
||||
go func(p C.Proxy) {
|
||||
_, err := DelayTest(p, 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 NewURLTest(name string, proxies []C.Proxy, rawURL string, delay time.Duration) (*URLTest, error) {
|
||||
_, err := urlToAddr(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
urlTest := &URLTest{
|
||||
name: name,
|
||||
proxies: proxies[:],
|
||||
rawURL: rawURL,
|
||||
fast: proxies[0],
|
||||
delay: delay,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go urlTest.loop()
|
||||
return urlTest, nil
|
||||
}
|
86
adapters/remote/util.go
Normal file
86
adapters/remote/util.go
Normal file
@ -0,0 +1,86 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
// DelayTest get the delay for the specified URL
|
||||
func DelayTest(proxy C.Proxy, url string) (t int16, err error) {
|
||||
addr, err := urlToAddr(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
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(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req.Body.Close()
|
||||
t = int16(time.Since(start) / time.Millisecond)
|
||||
return
|
||||
}
|
||||
|
||||
func urlToAddr(rawURL string) (addr C.Addr, err error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
port := u.Port()
|
||||
if port == "" {
|
||||
if u.Scheme == "https" {
|
||||
port = "443"
|
||||
} else if u.Scheme == "http" {
|
||||
port = "80"
|
||||
} else {
|
||||
err = fmt.Errorf("%s scheme not Support", rawURL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
addr = C.Addr{
|
||||
AddrType: C.AtypDomainName,
|
||||
Host: u.Hostname(),
|
||||
IP: nil,
|
||||
Port: port,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
@ -1,168 +0,0 @@
|
||||
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) Type() C.AdapterType {
|
||||
return C.URLTest
|
||||
}
|
||||
|
||||
func (u *URLTest) Now() string {
|
||||
return u.fast.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
|
||||
}
|
373
config/config.go
Normal file
373
config/config.go
Normal file
@ -0,0 +1,373 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters/remote"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/observable"
|
||||
R "github.com/Dreamacro/clash/rules"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
config *Config
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// General config
|
||||
type General struct {
|
||||
Port int
|
||||
SocksPort int
|
||||
RedirPort int
|
||||
AllowLan bool
|
||||
Mode Mode
|
||||
LogLevel C.LogLevel
|
||||
}
|
||||
|
||||
// ProxyConfig is update proxy schema
|
||||
type ProxyConfig struct {
|
||||
Port *int
|
||||
SocksPort *int
|
||||
RedirPort *int
|
||||
AllowLan *bool
|
||||
}
|
||||
|
||||
// Config is clash config manager
|
||||
type Config struct {
|
||||
general *General
|
||||
rules []C.Rule
|
||||
proxies map[string]C.Proxy
|
||||
lastUpdate time.Time
|
||||
|
||||
event chan<- interface{}
|
||||
reportCh chan interface{}
|
||||
observable *observable.Observable
|
||||
}
|
||||
|
||||
// Event is event of clash config
|
||||
type Event struct {
|
||||
Type string
|
||||
Payload interface{}
|
||||
}
|
||||
|
||||
// Subscribe config stream
|
||||
func (c *Config) Subscribe() observable.Subscription {
|
||||
sub, _ := c.observable.Subscribe()
|
||||
return sub
|
||||
}
|
||||
|
||||
// Report return a channel for collecting report message
|
||||
func (c *Config) Report() chan<- interface{} {
|
||||
return c.reportCh
|
||||
}
|
||||
|
||||
func (c *Config) readConfig() (*ini.File, error) {
|
||||
if _, err := os.Stat(C.ConfigPath); os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
return ini.LoadSources(
|
||||
ini.LoadOptions{AllowBooleanKeys: true},
|
||||
C.ConfigPath,
|
||||
)
|
||||
}
|
||||
|
||||
// Parse config
|
||||
func (c *Config) Parse() error {
|
||||
cfg, err := c.readConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.parseGeneral(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.parseProxies(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.parseRules(cfg)
|
||||
}
|
||||
|
||||
// Proxies return proxies of clash
|
||||
func (c *Config) Proxies() map[string]C.Proxy {
|
||||
return c.proxies
|
||||
}
|
||||
|
||||
// Rules return rules of clash
|
||||
func (c *Config) Rules() []C.Rule {
|
||||
return c.rules
|
||||
}
|
||||
|
||||
// SetMode change mode of clash
|
||||
func (c *Config) SetMode(mode Mode) {
|
||||
c.general.Mode = mode
|
||||
c.event <- &Event{Type: "mode", Payload: mode}
|
||||
}
|
||||
|
||||
// SetLogLevel change log level of clash
|
||||
func (c *Config) SetLogLevel(level C.LogLevel) {
|
||||
c.general.LogLevel = level
|
||||
c.event <- &Event{Type: "log-level", Payload: level}
|
||||
}
|
||||
|
||||
// General return clash general config
|
||||
func (c *Config) General() General {
|
||||
return *c.general
|
||||
}
|
||||
|
||||
// UpdateRules is a function for hot reload rules
|
||||
func (c *Config) UpdateRules() error {
|
||||
cfg, err := c.readConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.parseRules(cfg)
|
||||
}
|
||||
|
||||
func (c *Config) parseGeneral(cfg *ini.File) error {
|
||||
general := cfg.Section("General")
|
||||
|
||||
port := general.Key("port").RangeInt(0, 1, 65535)
|
||||
socksPort := general.Key("socks-port").RangeInt(0, 1, 65535)
|
||||
redirPort := general.Key("redir-port").RangeInt(0, 1, 65535)
|
||||
allowLan := general.Key("allow-lan").MustBool()
|
||||
logLevelString := general.Key("log-level").MustString(C.INFO.String())
|
||||
modeString := general.Key("mode").MustString(Rule.String())
|
||||
|
||||
mode, exist := ModeMapping[modeString]
|
||||
if !exist {
|
||||
return fmt.Errorf("General.mode value invalid")
|
||||
}
|
||||
|
||||
logLevel, exist := C.LogLevelMapping[logLevelString]
|
||||
if !exist {
|
||||
return fmt.Errorf("General.log-level value invalid")
|
||||
}
|
||||
|
||||
c.general = &General{
|
||||
Port: port,
|
||||
SocksPort: socksPort,
|
||||
RedirPort: redirPort,
|
||||
AllowLan: allowLan,
|
||||
Mode: mode,
|
||||
LogLevel: logLevel,
|
||||
}
|
||||
|
||||
if restAddr := general.Key("external-controller").String(); restAddr != "" {
|
||||
c.event <- &Event{Type: "external-controller", Payload: restAddr}
|
||||
}
|
||||
|
||||
c.UpdateGeneral(*c.general)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateGeneral dispatch update event
|
||||
func (c *Config) UpdateGeneral(general General) {
|
||||
c.UpdateProxy(ProxyConfig{
|
||||
Port: &general.Port,
|
||||
SocksPort: &general.SocksPort,
|
||||
RedirPort: &general.RedirPort,
|
||||
AllowLan: &general.AllowLan,
|
||||
})
|
||||
c.event <- &Event{Type: "mode", Payload: general.Mode}
|
||||
c.event <- &Event{Type: "log-level", Payload: general.LogLevel}
|
||||
}
|
||||
|
||||
// UpdateProxy dispatch update proxy event
|
||||
func (c *Config) UpdateProxy(pc ProxyConfig) {
|
||||
if pc.AllowLan != nil {
|
||||
c.general.AllowLan = *pc.AllowLan
|
||||
}
|
||||
|
||||
if (pc.AllowLan != nil || pc.Port != nil) && *pc.Port != 0 {
|
||||
c.general.Port = *pc.Port
|
||||
c.event <- &Event{Type: "http-addr", Payload: genAddr(*pc.Port, c.general.AllowLan)}
|
||||
}
|
||||
|
||||
if (pc.AllowLan != nil || pc.SocksPort != nil) && *pc.SocksPort != 0 {
|
||||
c.general.SocksPort = *pc.SocksPort
|
||||
c.event <- &Event{Type: "socks-addr", Payload: genAddr(*pc.SocksPort, c.general.AllowLan)}
|
||||
}
|
||||
|
||||
if (pc.AllowLan != nil || pc.RedirPort != nil) && *pc.RedirPort != 0 {
|
||||
c.general.RedirPort = *pc.RedirPort
|
||||
c.event <- &Event{Type: "redir-addr", Payload: genAddr(*pc.RedirPort, c.general.AllowLan)}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) parseProxies(cfg *ini.File) error {
|
||||
proxies := make(map[string]C.Proxy)
|
||||
proxiesConfig := cfg.Section("Proxy")
|
||||
groupsConfig := cfg.Section("Proxy Group")
|
||||
|
||||
// parse proxy
|
||||
for _, key := range proxiesConfig.Keys() {
|
||||
proxy := key.Strings(",")
|
||||
if len(proxy) == 0 {
|
||||
continue
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
proxies[key.Name()] = ss
|
||||
// socks5, server, port
|
||||
case "socks5":
|
||||
if len(proxy) < 3 {
|
||||
continue
|
||||
}
|
||||
addr := fmt.Sprintf("%s:%s", proxy[1], proxy[2])
|
||||
socks5 := adapters.NewSocks5(key.Name(), addr)
|
||||
proxies[key.Name()] = socks5
|
||||
}
|
||||
}
|
||||
|
||||
// parse proxy group
|
||||
for _, key := range groupsConfig.Keys() {
|
||||
rule := strings.Split(key.Value(), ",")
|
||||
rule = trimArr(rule)
|
||||
switch rule[0] {
|
||||
case "url-test":
|
||||
if len(rule) < 4 {
|
||||
return fmt.Errorf("URLTest need more than 4 param")
|
||||
}
|
||||
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 := proxies[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())
|
||||
}
|
||||
proxies[key.Name()] = adapter
|
||||
case "select":
|
||||
if len(rule) < 2 {
|
||||
return fmt.Errorf("Selector need more than 2 param")
|
||||
}
|
||||
proxyNames := rule[1:]
|
||||
selectProxy := make(map[string]C.Proxy)
|
||||
for _, name := range proxyNames {
|
||||
proxy, exist := proxies[name]
|
||||
if !exist {
|
||||
return fmt.Errorf("Proxy %s not exist", name)
|
||||
}
|
||||
selectProxy[name] = proxy
|
||||
}
|
||||
selector, err := adapters.NewSelector(key.Name(), selectProxy)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Selector create error: %s", err.Error())
|
||||
}
|
||||
proxies[key.Name()] = selector
|
||||
}
|
||||
}
|
||||
|
||||
// init proxy
|
||||
proxies["GLOBAL"], _ = adapters.NewSelector("GLOBAL", proxies)
|
||||
proxies["DIRECT"] = adapters.NewDirect()
|
||||
proxies["REJECT"] = adapters.NewReject()
|
||||
|
||||
c.proxies = proxies
|
||||
c.event <- &Event{Type: "proxies", Payload: proxies}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) parseRules(cfg *ini.File) error {
|
||||
rules := []C.Rule{}
|
||||
|
||||
rulesConfig := cfg.Section("Rule")
|
||||
// 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]))
|
||||
}
|
||||
}
|
||||
|
||||
c.rules = rules
|
||||
c.event <- &Event{Type: "rules", Payload: rules}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) handleResponseMessage() {
|
||||
for elm := range c.reportCh {
|
||||
event := elm.(*Event)
|
||||
switch event.Type {
|
||||
case "http-addr":
|
||||
if event.Payload.(bool) == false {
|
||||
log.Errorf("Listening HTTP proxy at %s error", c.general.Port)
|
||||
c.general.Port = 0
|
||||
}
|
||||
case "socks-addr":
|
||||
if event.Payload.(bool) == false {
|
||||
log.Errorf("Listening SOCKS proxy at %s error", c.general.SocksPort)
|
||||
c.general.SocksPort = 0
|
||||
}
|
||||
case "redir-addr":
|
||||
if event.Payload.(bool) == false {
|
||||
log.Errorf("Listening Redir proxy at %s error", c.general.RedirPort)
|
||||
c.general.RedirPort = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newConfig() *Config {
|
||||
event := make(chan interface{})
|
||||
reportCh := make(chan interface{})
|
||||
config := &Config{
|
||||
general: &General{},
|
||||
proxies: make(map[string]C.Proxy),
|
||||
rules: []C.Rule{},
|
||||
lastUpdate: time.Now(),
|
||||
|
||||
event: event,
|
||||
reportCh: reportCh,
|
||||
observable: observable.NewObservable(event),
|
||||
}
|
||||
go config.handleResponseMessage()
|
||||
return config
|
||||
}
|
||||
|
||||
// Instance return singleton instance of Config
|
||||
func Instance() *Config {
|
||||
once.Do(func() {
|
||||
config = newConfig()
|
||||
})
|
||||
return config
|
||||
}
|
72
config/initial.go
Normal file
72
config/initial.go
Normal file
@ -0,0 +1,72 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Init prepare necessary files
|
||||
func Init() {
|
||||
// initial config.ini
|
||||
if _, err := os.Stat(C.ConfigPath); os.IsNotExist(err) {
|
||||
log.Info("Can't find config, create a empty file")
|
||||
os.OpenFile(C.ConfigPath, os.O_CREATE|os.O_WRONLY, 0644)
|
||||
}
|
||||
|
||||
// initial mmdb
|
||||
if _, err := os.Stat(C.MMDBPath); os.IsNotExist(err) {
|
||||
log.Info("Can't find MMDB, start download")
|
||||
err := downloadMMDB(C.MMDBPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Can't download MMDB: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,16 @@
|
||||
package tunnel
|
||||
package config
|
||||
|
||||
type Mode int
|
||||
|
||||
var (
|
||||
// ModeMapping is a mapping for Mode enum
|
||||
ModeMapping = map[string]Mode{
|
||||
"Global": Global,
|
||||
"Rule": Rule,
|
||||
"Direct": Direct,
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
Global Mode = iota
|
||||
Rule
|
20
config/utils.go
Normal file
20
config/utils.go
Normal file
@ -0,0 +1,20 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func trimArr(arr []string) (r []string) {
|
||||
for _, e := range arr {
|
||||
r = append(r, strings.Trim(e, " "))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func genAddr(port int, allowLan bool) string {
|
||||
if allowLan {
|
||||
return fmt.Sprintf(":%d", port)
|
||||
}
|
||||
return fmt.Sprintf("127.0.0.1:%d", port)
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package constant
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
@ -11,18 +10,17 @@ const (
|
||||
Reject
|
||||
Selector
|
||||
Shadowsocks
|
||||
Socks5
|
||||
URLTest
|
||||
)
|
||||
|
||||
type ProxyAdapter interface {
|
||||
ReadWriter() io.ReadWriter
|
||||
Conn() net.Conn
|
||||
Close()
|
||||
}
|
||||
|
||||
type ServerAdapter interface {
|
||||
Addr() *Addr
|
||||
Connect(ProxyAdapter)
|
||||
Close()
|
||||
}
|
||||
|
||||
@ -45,6 +43,8 @@ func (at AdapterType) String() string {
|
||||
return "Selector"
|
||||
case Shadowsocks:
|
||||
return "Shadowsocks"
|
||||
case Socks5:
|
||||
return "Socks5"
|
||||
case URLTest:
|
||||
return "URLTest"
|
||||
default:
|
||||
|
@ -10,8 +10,11 @@ const (
|
||||
AtypDomainName = 3
|
||||
AtypIPv6 = 4
|
||||
|
||||
TCP = iota
|
||||
TCP NetWork = iota
|
||||
UDP
|
||||
|
||||
HTTP SourceType = iota
|
||||
SOCKS
|
||||
)
|
||||
|
||||
type NetWork int
|
||||
@ -23,9 +26,12 @@ func (n *NetWork) String() string {
|
||||
return "udp"
|
||||
}
|
||||
|
||||
type SourceType int
|
||||
|
||||
// Addr is used to store connection address
|
||||
type Addr struct {
|
||||
NetWork NetWork
|
||||
Source SourceType
|
||||
AddrType int
|
||||
Host string
|
||||
IP *net.IP
|
||||
|
@ -1,23 +1,15 @@
|
||||
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
|
||||
Name = "clash"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -31,6 +23,7 @@ type General struct {
|
||||
AllowLan *bool `json:"allow-lan,omitempty"`
|
||||
Port *int `json:"port,omitempty"`
|
||||
SocksPort *int `json:"socks-port,omitempty"`
|
||||
LogLevel *string `json:"log-level,omitempty"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
@ -53,67 +46,5 @@ func init() {
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
35
constant/log.go
Normal file
35
constant/log.go
Normal file
@ -0,0 +1,35 @@
|
||||
package constant
|
||||
|
||||
var (
|
||||
// LogLevelMapping is a mapping for LogLevel enum
|
||||
LogLevelMapping = map[string]LogLevel{
|
||||
"error": ERROR,
|
||||
"warning": WARNING,
|
||||
"info": INFO,
|
||||
"debug": DEBUG,
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
ERROR LogLevel = iota
|
||||
WARNING
|
||||
INFO
|
||||
DEBUG
|
||||
)
|
||||
|
||||
type LogLevel int
|
||||
|
||||
func (l LogLevel) String() string {
|
||||
switch l {
|
||||
case INFO:
|
||||
return "info"
|
||||
case WARNING:
|
||||
return "warning"
|
||||
case ERROR:
|
||||
return "error"
|
||||
case DEBUG:
|
||||
return "debug"
|
||||
default:
|
||||
return "unknow"
|
||||
}
|
||||
}
|
@ -1,12 +1,14 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"github.com/Dreamacro/clash/config"
|
||||
"github.com/Dreamacro/clash/proxy"
|
||||
T "github.com/Dreamacro/clash/tunnel"
|
||||
)
|
||||
|
||||
var (
|
||||
tunnel = T.GetInstance()
|
||||
tunnel = T.Instance()
|
||||
cfg = config.Instance()
|
||||
listener = proxy.Instance()
|
||||
)
|
||||
|
||||
|
@ -4,9 +4,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/Dreamacro/clash/config"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/proxy"
|
||||
T "github.com/Dreamacro/clash/tunnel"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/render"
|
||||
@ -19,17 +18,23 @@ func configRouter() http.Handler {
|
||||
return r
|
||||
}
|
||||
|
||||
var modeMapping = map[string]T.Mode{
|
||||
"Global": T.Global,
|
||||
"Rule": T.Rule,
|
||||
"Direct": T.Direct,
|
||||
type configSchema struct {
|
||||
Port int `json:"port"`
|
||||
SocksPort int `json:"socket-port"`
|
||||
AllowLan bool `json:"allow-lan"`
|
||||
Mode string `json:"mode"`
|
||||
LogLevel string `json:"log-level"`
|
||||
}
|
||||
|
||||
func getConfigs(w http.ResponseWriter, r *http.Request) {
|
||||
info := listener.Info()
|
||||
mode := tunnel.GetMode().String()
|
||||
info.Mode = &mode
|
||||
render.JSON(w, r, info)
|
||||
general := cfg.General()
|
||||
render.JSON(w, r, configSchema{
|
||||
Port: general.Port,
|
||||
SocksPort: general.SocksPort,
|
||||
AllowLan: general.AllowLan,
|
||||
Mode: general.Mode.String(),
|
||||
LogLevel: general.LogLevel.String(),
|
||||
})
|
||||
}
|
||||
|
||||
func updateConfigs(w http.ResponseWriter, r *http.Request) {
|
||||
@ -44,25 +49,31 @@ func updateConfigs(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// update errors
|
||||
var proxyErr, modeErr error
|
||||
|
||||
// update proxy
|
||||
listener := proxy.Instance()
|
||||
proxyErr = listener.Update(general.AllowLan, general.Port, general.SocksPort)
|
||||
var modeErr, logLevelErr error
|
||||
|
||||
// update mode
|
||||
if general.Mode != nil {
|
||||
mode, ok := modeMapping[*general.Mode]
|
||||
mode, ok := config.ModeMapping[*general.Mode]
|
||||
if !ok {
|
||||
modeErr = fmt.Errorf("Mode error")
|
||||
} else {
|
||||
tunnel.SetMode(mode)
|
||||
cfg.SetMode(mode)
|
||||
}
|
||||
}
|
||||
|
||||
// update log-level
|
||||
if general.LogLevel != nil {
|
||||
level, ok := C.LogLevelMapping[*general.LogLevel]
|
||||
if !ok {
|
||||
logLevelErr = fmt.Errorf("Log Level error")
|
||||
} else {
|
||||
cfg.SetLogLevel(level)
|
||||
}
|
||||
}
|
||||
|
||||
hasError, errors := formatErrors(map[string]error{
|
||||
"proxy": proxyErr,
|
||||
"mode": modeErr,
|
||||
"mode": modeErr,
|
||||
"log-level": logLevelErr,
|
||||
})
|
||||
|
||||
if hasError {
|
||||
@ -70,5 +81,13 @@ func updateConfigs(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, errors)
|
||||
return
|
||||
}
|
||||
|
||||
// update proxy
|
||||
cfg.UpdateProxy(config.ProxyConfig{
|
||||
AllowLan: general.AllowLan,
|
||||
Port: general.Port,
|
||||
SocksPort: general.SocksPort,
|
||||
})
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
@ -3,8 +3,10 @@ package hub
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
A "github.com/Dreamacro/clash/adapters"
|
||||
A "github.com/Dreamacro/clash/adapters/remote"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
@ -13,8 +15,9 @@ import (
|
||||
|
||||
func proxyRouter() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/", getProxys)
|
||||
r.Get("/", getProxies)
|
||||
r.Get("/{name}", getProxy)
|
||||
r.Get("/{name}/delay", getProxyDelay)
|
||||
r.Put("/{name}", updateProxy)
|
||||
return r
|
||||
}
|
||||
@ -56,23 +59,23 @@ func transformProxy(proxy C.Proxy) interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
type GetProxysResponse struct {
|
||||
Proxys map[string]interface{} `json:"proxys"`
|
||||
type GetProxiesResponse struct {
|
||||
Proxies map[string]interface{} `json:"proxies"`
|
||||
}
|
||||
|
||||
func getProxys(w http.ResponseWriter, r *http.Request) {
|
||||
_, rawProxys := tunnel.Config()
|
||||
proxys := make(map[string]interface{})
|
||||
for name, proxy := range rawProxys {
|
||||
proxys[name] = transformProxy(proxy)
|
||||
func getProxies(w http.ResponseWriter, r *http.Request) {
|
||||
rawProxies := cfg.Proxies()
|
||||
proxies := make(map[string]interface{})
|
||||
for name, proxy := range rawProxies {
|
||||
proxies[name] = transformProxy(proxy)
|
||||
}
|
||||
render.JSON(w, r, GetProxysResponse{Proxys: proxys})
|
||||
render.JSON(w, r, GetProxiesResponse{Proxies: proxies})
|
||||
}
|
||||
|
||||
func getProxy(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
_, proxys := tunnel.Config()
|
||||
proxy, exist := proxys[name]
|
||||
proxies := cfg.Proxies()
|
||||
proxy, exist := proxies[name]
|
||||
if !exist {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
render.JSON(w, r, Error{
|
||||
@ -98,8 +101,8 @@ func updateProxy(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
name := chi.URLParam(r, "name")
|
||||
_, proxys := tunnel.Config()
|
||||
proxy, exist := proxys[name]
|
||||
proxies := cfg.Proxies()
|
||||
proxy, exist := proxies[name]
|
||||
if !exist {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
render.JSON(w, r, Error{
|
||||
@ -127,3 +130,64 @@ func updateProxy(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
type GetProxyDelayRequest struct {
|
||||
URL string `json:"url"`
|
||||
Timeout int16 `json:"timeout"`
|
||||
}
|
||||
|
||||
type GetProxyDelayResponse struct {
|
||||
Delay int16 `json:"delay"`
|
||||
}
|
||||
|
||||
func getProxyDelay(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
url := query.Get("url")
|
||||
timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
render.JSON(w, r, Error{
|
||||
Error: "Format error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
name := chi.URLParam(r, "name")
|
||||
proxies := cfg.Proxies()
|
||||
proxy, exist := proxies[name]
|
||||
if !exist {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
render.JSON(w, r, Error{
|
||||
Error: "Proxy not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
sigCh := make(chan int16)
|
||||
go func() {
|
||||
t, err := A.DelayTest(proxy, url)
|
||||
if err != nil {
|
||||
sigCh <- 0
|
||||
}
|
||||
sigCh <- t
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(time.Millisecond * time.Duration(timeout)):
|
||||
w.WriteHeader(http.StatusRequestTimeout)
|
||||
render.JSON(w, r, Error{
|
||||
Error: "Proxy delay test timeout",
|
||||
})
|
||||
case t := <-sigCh:
|
||||
if t == 0 {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
render.JSON(w, r, Error{
|
||||
Error: "An error occurred in the delay test",
|
||||
})
|
||||
} else {
|
||||
render.JSON(w, r, GetProxyDelayResponse{
|
||||
Delay: t,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -24,10 +24,10 @@ type GetRulesResponse struct {
|
||||
}
|
||||
|
||||
func getRules(w http.ResponseWriter, r *http.Request) {
|
||||
rulesCfg, _ := tunnel.Config()
|
||||
rawRules := cfg.Rules()
|
||||
|
||||
var rules []Rule
|
||||
for _, rule := range rulesCfg {
|
||||
for _, rule := range rawRules {
|
||||
rules = append(rules, Rule{
|
||||
Name: rule.RuleType().String(),
|
||||
Payload: rule.Payload(),
|
||||
@ -41,7 +41,7 @@ func getRules(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func updateRules(w http.ResponseWriter, r *http.Request) {
|
||||
err := tunnel.UpdateConfig()
|
||||
err := cfg.UpdateRules()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
render.JSON(w, r, Error{
|
||||
|
@ -5,9 +5,12 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/config"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
T "github.com/Dreamacro/clash/tunnel"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/go-chi/render"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -17,21 +20,51 @@ type Traffic struct {
|
||||
Down int64 `json:"down"`
|
||||
}
|
||||
|
||||
func NewHub(addr string) {
|
||||
func newHub(signal chan struct{}) {
|
||||
var addr string
|
||||
ch := config.Instance().Subscribe()
|
||||
signal <- struct{}{}
|
||||
for {
|
||||
elm := <-ch
|
||||
event := elm.(*config.Event)
|
||||
if event.Type == "external-controller" {
|
||||
addr = event.Payload.(string)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Get("/traffic", traffic)
|
||||
r.Get("/logs", getLogs)
|
||||
cors := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Content-Type"},
|
||||
MaxAge: 300,
|
||||
})
|
||||
|
||||
r.Use(cors.Handler)
|
||||
|
||||
r.With(jsonContentType).Get("/traffic", traffic)
|
||||
r.With(jsonContentType).Get("/logs", getLogs)
|
||||
r.Mount("/configs", configRouter())
|
||||
r.Mount("/proxys", proxyRouter())
|
||||
r.Mount("/proxies", proxyRouter())
|
||||
r.Mount("/rules", ruleRouter())
|
||||
|
||||
log.Infof("RESTful API listening at: %s", addr)
|
||||
err := http.ListenAndServe(addr, r)
|
||||
if err != nil {
|
||||
log.Fatalf("External controller error: %s", err.Error())
|
||||
log.Errorf("External controller error: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func jsonContentType(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
func traffic(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
@ -65,14 +98,7 @@ func getLogs(w http.ResponseWriter, r *http.Request) {
|
||||
req.Level = "info"
|
||||
}
|
||||
|
||||
mapping := map[string]T.LogLevel{
|
||||
"info": T.INFO,
|
||||
"debug": T.DEBUG,
|
||||
"error": T.ERROR,
|
||||
"warning": T.WARNING,
|
||||
}
|
||||
|
||||
level, ok := mapping[req.Level]
|
||||
level, ok := C.LogLevelMapping[req.Level]
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
render.JSON(w, r, Error{
|
||||
@ -107,3 +133,10 @@ func getLogs(w http.ResponseWriter, r *http.Request) {
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// Run initial hub
|
||||
func Run() {
|
||||
signal := make(chan struct{})
|
||||
go newHub(signal)
|
||||
<-signal
|
||||
}
|
||||
|
23
main.go
23
main.go
@ -5,7 +5,7 @@ import (
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/config"
|
||||
"github.com/Dreamacro/clash/hub"
|
||||
"github.com/Dreamacro/clash/proxy"
|
||||
"github.com/Dreamacro/clash/tunnel"
|
||||
@ -14,23 +14,14 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := tunnel.GetInstance().UpdateConfig(); err != nil {
|
||||
log.Fatalf("Parse config error: %s", err.Error())
|
||||
}
|
||||
tunnel.Instance().Run()
|
||||
proxy.Instance().Run()
|
||||
hub.Run()
|
||||
|
||||
if err := proxy.Instance().Run(); err != nil {
|
||||
log.Fatalf("Proxy listen error: %s", err.Error())
|
||||
}
|
||||
|
||||
// Hub
|
||||
cfg, err := C.GetConfig()
|
||||
config.Init()
|
||||
err := config.Instance().Parse()
|
||||
if err != nil {
|
||||
log.Fatalf("Read config error: %s", err.Error())
|
||||
}
|
||||
|
||||
section := cfg.Section("General")
|
||||
if key, err := section.GetKey("external-controller"); err == nil {
|
||||
go hub.NewHub(key.Value())
|
||||
log.Fatalf("Parse config error: %s", err.Error())
|
||||
}
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
|
@ -1,77 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
@ -1,20 +1,20 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters/local"
|
||||
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()
|
||||
tun = tunnel.Instance()
|
||||
)
|
||||
|
||||
func NewHttpProxy(addr string) (*C.ProxySignal, error) {
|
||||
@ -30,24 +30,23 @@ func NewHttpProxy(addr string) (*C.ProxySignal, error) {
|
||||
Closed: closed,
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodConnect {
|
||||
handleTunneling(w, r)
|
||||
} else {
|
||||
handleHTTP(w, r)
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Infof("HTTP proxy listening at: %s", addr)
|
||||
server.Serve(l)
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
if _, open := <-done; !open {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
go handleConn(c)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
<-done
|
||||
server.Shutdown(context.Background())
|
||||
close(done)
|
||||
l.Close()
|
||||
closed <- struct{}{}
|
||||
}()
|
||||
@ -55,55 +54,26 @@ func NewHttpProxy(addr string) (*C.ProxySignal, error) {
|
||||
return signal, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
func handleConn(conn net.Conn) {
|
||||
br := bufio.NewReader(conn)
|
||||
method, hostName := adapters.ParserHTTPHostHeader(br)
|
||||
if hostName == "" {
|
||||
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,
|
||||
|
||||
if !strings.Contains(hostName, ":") {
|
||||
hostName += ":80"
|
||||
}
|
||||
|
||||
var peeked []byte
|
||||
if method == http.MethodConnect {
|
||||
_, err := conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else if n := br.Buffered(); n > 0 {
|
||||
peeked, _ = br.Peek(br.Buffered())
|
||||
}
|
||||
|
||||
tun.Add(adapters.NewHTTP(hostName, peeked, method != http.MethodConnect, conn))
|
||||
}
|
||||
|
@ -1,14 +1,13 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/Dreamacro/clash/config"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/proxy/http"
|
||||
"github.com/Dreamacro/clash/proxy/redir"
|
||||
"github.com/Dreamacro/clash/proxy/socks"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -17,58 +16,13 @@ var (
|
||||
)
|
||||
|
||||
type Listener struct {
|
||||
httpPort int
|
||||
socksPort int
|
||||
allowLan bool
|
||||
|
||||
// signal for update
|
||||
httpSignal *C.ProxySignal
|
||||
socksSignal *C.ProxySignal
|
||||
redirSignal *C.ProxySignal
|
||||
}
|
||||
|
||||
// Info returns the proxys's current configuration
|
||||
func (l *Listener) Info() (info C.General) {
|
||||
return C.General{
|
||||
Port: &l.httpPort,
|
||||
SocksPort: &l.socksPort,
|
||||
AllowLan: &l.allowLan,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Listener) Update(allowLan *bool, httpPort *int, socksPort *int) error {
|
||||
if allowLan != nil {
|
||||
l.allowLan = *allowLan
|
||||
}
|
||||
|
||||
var socksErr, httpErr error
|
||||
if allowLan != nil || httpPort != nil {
|
||||
newHTTPPort := l.httpPort
|
||||
if httpPort != nil {
|
||||
newHTTPPort = *httpPort
|
||||
}
|
||||
httpErr = l.updateHTTP(newHTTPPort)
|
||||
}
|
||||
|
||||
if allowLan != nil || socksPort != nil {
|
||||
newSocksPort := l.socksPort
|
||||
if socksPort != nil {
|
||||
newSocksPort = *socksPort
|
||||
}
|
||||
socksErr = l.updateSocks(newSocksPort)
|
||||
}
|
||||
|
||||
if socksErr != nil && httpErr != nil {
|
||||
return fmt.Errorf("%s\n%s", socksErr.Error(), httpErr.Error())
|
||||
} else if socksErr != nil {
|
||||
return socksErr
|
||||
} else if httpErr != nil {
|
||||
return httpErr
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Listener) updateHTTP(port int) error {
|
||||
func (l *Listener) updateHTTP(addr string) error {
|
||||
if l.httpSignal != nil {
|
||||
signal := l.httpSignal
|
||||
signal.Done <- struct{}{}
|
||||
@ -76,17 +30,16 @@ func (l *Listener) updateHTTP(port int) error {
|
||||
l.httpSignal = nil
|
||||
}
|
||||
|
||||
signal, err := http.NewHttpProxy(l.genAddr(port))
|
||||
signal, err := http.NewHttpProxy(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.httpSignal = signal
|
||||
l.httpPort = port
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) updateSocks(port int) error {
|
||||
func (l *Listener) updateSocks(addr string) error {
|
||||
if l.socksSignal != nil {
|
||||
signal := l.socksSignal
|
||||
signal.Done <- struct{}{}
|
||||
@ -94,47 +47,67 @@ func (l *Listener) updateSocks(port int) error {
|
||||
l.socksSignal = nil
|
||||
}
|
||||
|
||||
signal, err := socks.NewSocksProxy(l.genAddr(port))
|
||||
signal, err := socks.NewSocksProxy(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.socksSignal = signal
|
||||
l.socksPort = port
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) genAddr(port int) string {
|
||||
host := "127.0.0.1"
|
||||
if l.allowLan {
|
||||
host = ""
|
||||
func (l *Listener) updateRedir(addr string) error {
|
||||
if l.redirSignal != nil {
|
||||
signal := l.redirSignal
|
||||
signal.Done <- struct{}{}
|
||||
<-signal.Closed
|
||||
l.redirSignal = nil
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", host, port)
|
||||
|
||||
signal, err := redir.NewRedirProxy(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.redirSignal = signal
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) Run() error {
|
||||
return l.Update(&l.allowLan, &l.httpPort, &l.socksPort)
|
||||
func (l *Listener) process(signal chan<- struct{}) {
|
||||
sub := config.Instance().Subscribe()
|
||||
signal <- struct{}{}
|
||||
reportCH := config.Instance().Report()
|
||||
for elm := range sub {
|
||||
event := elm.(*config.Event)
|
||||
switch event.Type {
|
||||
case "http-addr":
|
||||
addr := event.Payload.(string)
|
||||
err := l.updateHTTP(addr)
|
||||
reportCH <- &config.Event{Type: "http-addr", Payload: err == nil}
|
||||
case "socks-addr":
|
||||
addr := event.Payload.(string)
|
||||
err := l.updateSocks(addr)
|
||||
reportCH <- &config.Event{Type: "socks-addr", Payload: err == nil}
|
||||
case "redir-addr":
|
||||
addr := event.Payload.(string)
|
||||
err := l.updateRedir(addr)
|
||||
reportCH <- &config.Event{Type: "redir-addr", Payload: err == nil}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run ensure config monitoring
|
||||
func (l *Listener) Run() {
|
||||
signal := make(chan struct{})
|
||||
go l.process(signal)
|
||||
<-signal
|
||||
}
|
||||
|
||||
func newListener() *Listener {
|
||||
cfg, err := C.GetConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("Read config error: %s", err.Error())
|
||||
}
|
||||
|
||||
general := cfg.Section("General")
|
||||
|
||||
port := general.Key("port").RangeInt(C.DefalutHTTPPort, 1, 65535)
|
||||
socksPort := general.Key("socks-port").RangeInt(C.DefalutSOCKSPort, 1, 65535)
|
||||
allowLan := general.Key("allow-lan").MustBool()
|
||||
|
||||
return &Listener{
|
||||
httpPort: port,
|
||||
socksPort: socksPort,
|
||||
allowLan: allowLan,
|
||||
}
|
||||
return &Listener{}
|
||||
}
|
||||
|
||||
// Instance return singleton instance of Listener
|
||||
func Instance() *Listener {
|
||||
once.Do(func() {
|
||||
listener = newListener()
|
||||
|
62
proxy/redir/tcp.go
Normal file
62
proxy/redir/tcp.go
Normal file
@ -0,0 +1,62 @@
|
||||
package redir
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters/local"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/tunnel"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
tun = tunnel.Instance()
|
||||
)
|
||||
|
||||
func NewRedirProxy(addr string) (*C.ProxySignal, error) {
|
||||
l, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
closed := make(chan struct{})
|
||||
signal := &C.ProxySignal{
|
||||
Done: done,
|
||||
Closed: closed,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Infof("Redir proxy listening at: %s", addr)
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
if _, open := <-done; !open {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
go handleRedir(c)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
<-done
|
||||
close(done)
|
||||
l.Close()
|
||||
closed <- struct{}{}
|
||||
}()
|
||||
|
||||
return signal, nil
|
||||
}
|
||||
|
||||
func handleRedir(conn net.Conn) {
|
||||
target, err := parserPacket(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
conn.(*net.TCPConn).SetKeepAlive(true)
|
||||
tun.Add(adapters.NewSocks(target, conn))
|
||||
}
|
58
proxy/redir/tcp_darwin.go
Normal file
58
proxy/redir/tcp_darwin.go
Normal file
@ -0,0 +1,58 @@
|
||||
package redir
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/riobard/go-shadowsocks2/socks"
|
||||
)
|
||||
|
||||
func parserPacket(c net.Conn) (socks.Addr, error) {
|
||||
const (
|
||||
PfInout = 0
|
||||
PfIn = 1
|
||||
PfOut = 2
|
||||
IOCOut = 0x40000000
|
||||
IOCIn = 0x80000000
|
||||
IOCInOut = IOCIn | IOCOut
|
||||
IOCPARMMask = 0x1FFF
|
||||
LEN = 4*16 + 4*4 + 4*1
|
||||
// #define _IOC(inout,group,num,len) (inout | ((len & IOCPARMMask) << 16) | ((group) << 8) | (num))
|
||||
// #define _IOWR(g,n,t) _IOC(IOCInOut, (g), (n), sizeof(t))
|
||||
// #define DIOCNATLOOK _IOWR('D', 23, struct pfioc_natlook)
|
||||
DIOCNATLOOK = IOCInOut | ((LEN & IOCPARMMask) << 16) | ('D' << 8) | 23
|
||||
)
|
||||
|
||||
fd, err := syscall.Open("/dev/pf", 0, syscall.O_RDONLY)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
|
||||
nl := struct { // struct pfioc_natlook
|
||||
saddr, daddr, rsaddr, rdaddr [16]byte
|
||||
sxport, dxport, rsxport, rdxport [4]byte
|
||||
af, proto, protoVariant, direction uint8
|
||||
}{
|
||||
af: syscall.AF_INET,
|
||||
proto: syscall.IPPROTO_TCP,
|
||||
direction: PfOut,
|
||||
}
|
||||
saddr := c.RemoteAddr().(*net.TCPAddr)
|
||||
daddr := c.LocalAddr().(*net.TCPAddr)
|
||||
copy(nl.saddr[:], saddr.IP)
|
||||
copy(nl.daddr[:], daddr.IP)
|
||||
nl.sxport[0], nl.sxport[1] = byte(saddr.Port>>8), byte(saddr.Port)
|
||||
nl.dxport[0], nl.dxport[1] = byte(daddr.Port>>8), byte(daddr.Port)
|
||||
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), DIOCNATLOOK, uintptr(unsafe.Pointer(&nl))); errno != 0 {
|
||||
return nil, errno
|
||||
}
|
||||
|
||||
addr := make([]byte, 1+net.IPv4len+2)
|
||||
addr[0] = socks.AtypIPv4
|
||||
copy(addr[1:1+net.IPv4len], nl.rdaddr[:4])
|
||||
copy(addr[1+net.IPv4len:], nl.rdxport[:2])
|
||||
return addr, nil
|
||||
}
|
51
proxy/redir/tcp_linux.go
Normal file
51
proxy/redir/tcp_linux.go
Normal file
@ -0,0 +1,51 @@
|
||||
package redir
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/riobard/go-shadowsocks2/socks"
|
||||
)
|
||||
|
||||
const (
|
||||
SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv4.h
|
||||
IP6T_SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv6/ip6_tables.h
|
||||
)
|
||||
|
||||
func parserPacket(conn net.Conn) (socks.Addr, error) {
|
||||
c, ok := conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return nil, errors.New("only work with TCP connection")
|
||||
}
|
||||
|
||||
rc, err := c.SyscallConn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var addr socks.Addr
|
||||
|
||||
rc.Control(func(fd uintptr) {
|
||||
addr, err = getorigdst(fd)
|
||||
})
|
||||
|
||||
return addr, err
|
||||
}
|
||||
|
||||
// Call getorigdst() from linux/net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c
|
||||
func getorigdst(fd uintptr) (socks.Addr, error) {
|
||||
raw := syscall.RawSockaddrInet4{}
|
||||
siz := unsafe.Sizeof(raw)
|
||||
if err := socketcall(GETSOCKOPT, fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&raw)), uintptr(unsafe.Pointer(&siz)), 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr := make([]byte, 1+net.IPv4len+2)
|
||||
addr[0] = socks.AtypIPv4
|
||||
copy(addr[1:1+net.IPv4len], raw.Addr[:])
|
||||
port := (*[2]byte)(unsafe.Pointer(&raw.Port)) // big-endian
|
||||
addr[1+net.IPv4len], addr[1+net.IPv4len+1] = port[0], port[1]
|
||||
return addr, nil
|
||||
}
|
17
proxy/redir/tcp_linux_386.go
Normal file
17
proxy/redir/tcp_linux_386.go
Normal file
@ -0,0 +1,17 @@
|
||||
package redir
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const GETSOCKOPT = 15 // https://golang.org/src/syscall/syscall_linux_386.go#L183
|
||||
|
||||
func socketcall(call, a0, a1, a2, a3, a4, a5 uintptr) error {
|
||||
var a [6]uintptr
|
||||
a[0], a[1], a[2], a[3], a[4], a[5] = a0, a1, a2, a3, a4, a5
|
||||
if _, _, errno := syscall.Syscall6(syscall.SYS_SOCKETCALL, call, uintptr(unsafe.Pointer(&a)), 0, 0, 0, 0); errno != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
14
proxy/redir/tcp_linux_other.go
Normal file
14
proxy/redir/tcp_linux_other.go
Normal file
@ -0,0 +1,14 @@
|
||||
// +build linux,!386
|
||||
|
||||
package redir
|
||||
|
||||
import "syscall"
|
||||
|
||||
const GETSOCKOPT = syscall.SYS_GETSOCKOPT
|
||||
|
||||
func socketcall(call, a0, a1, a2, a3, a4, a5 uintptr) error {
|
||||
if _, _, errno := syscall.Syscall6(call, a0, a1, a2, a3, a4, a5); errno != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
12
proxy/redir/tcp_windows.go
Normal file
12
proxy/redir/tcp_windows.go
Normal file
@ -0,0 +1,12 @@
|
||||
package redir
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/riobard/go-shadowsocks2/socks"
|
||||
)
|
||||
|
||||
func parserPacket(conn net.Conn) (socks.Addr, error) {
|
||||
return nil, errors.New("Windows not support yet")
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
package socks
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters/local"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/tunnel"
|
||||
|
||||
@ -13,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
tun = tunnel.GetInstance()
|
||||
tun = tunnel.Instance()
|
||||
)
|
||||
|
||||
func NewSocksProxy(addr string) (*C.ProxySignal, error) {
|
||||
@ -60,59 +59,5 @@ func handleSocks(conn net.Conn) {
|
||||
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),
|
||||
}
|
||||
tun.Add(adapters.NewSocks(target, conn))
|
||||
}
|
||||
|
@ -1,21 +1,18 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
var (
|
||||
mmdb *geoip2.Reader
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
type GEOIP struct {
|
||||
country string
|
||||
@ -43,6 +40,13 @@ func (g *GEOIP) Payload() string {
|
||||
}
|
||||
|
||||
func NewGEOIP(country string, adapter string) *GEOIP {
|
||||
once.Do(func() {
|
||||
var err error
|
||||
mmdb, err = geoip2.Open(C.MMDBPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Can't load mmdb: %s", err.Error())
|
||||
}
|
||||
})
|
||||
return &GEOIP{
|
||||
country: country,
|
||||
adapter: adapter,
|
||||
|
29
tunnel/connection.go
Normal file
29
tunnel/connection.go
Normal file
@ -0,0 +1,29 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters/local"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
func (t *Tunnel) handleHTTP(request *adapters.HTTPAdapter, proxy C.ProxyAdapter) {
|
||||
conn := newTrafficTrack(proxy.Conn(), t.traffic)
|
||||
|
||||
// Before we unwrap src and/or dst, copy any buffered data.
|
||||
if wc, ok := request.Conn().(*adapters.PeekedConn); ok && len(wc.Peeked) > 0 {
|
||||
if _, err := conn.Write(wc.Peeked); err != nil {
|
||||
return
|
||||
}
|
||||
wc.Peeked = nil
|
||||
}
|
||||
|
||||
go io.Copy(request.Conn(), conn)
|
||||
io.Copy(conn, request.Conn())
|
||||
}
|
||||
|
||||
func (t *Tunnel) handleSOCKS(request *adapters.SocksAdapter, proxy C.ProxyAdapter) {
|
||||
conn := newTrafficTrack(proxy.Conn(), t.traffic)
|
||||
go io.Copy(request.Conn(), conn)
|
||||
io.Copy(conn, request.Conn())
|
||||
}
|
@ -3,47 +3,29 @@ package tunnel
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
ERROR LogLevel = iota
|
||||
WARNING
|
||||
INFO
|
||||
DEBUG
|
||||
)
|
||||
|
||||
type LogLevel int
|
||||
|
||||
type Log struct {
|
||||
LogLevel LogLevel
|
||||
LogLevel C.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"
|
||||
}
|
||||
return l.LogLevel.String()
|
||||
}
|
||||
|
||||
func print(data Log) {
|
||||
switch data.LogLevel {
|
||||
case INFO:
|
||||
case C.INFO:
|
||||
log.Infoln(data.Payload)
|
||||
case WARNING:
|
||||
case C.WARNING:
|
||||
log.Warnln(data.Payload)
|
||||
case ERROR:
|
||||
case C.ERROR:
|
||||
log.Errorln(data.Payload)
|
||||
case DEBUG:
|
||||
case C.DEBUG:
|
||||
log.Debugln(data.Payload)
|
||||
}
|
||||
}
|
||||
@ -55,11 +37,13 @@ func (t *Tunnel) subscribeLogs() {
|
||||
}
|
||||
for elm := range sub {
|
||||
data := elm.(Log)
|
||||
print(data)
|
||||
if data.LogLevel <= t.logLevel {
|
||||
print(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newLog(logLevel LogLevel, format string, v ...interface{}) Log {
|
||||
func newLog(logLevel C.LogLevel, format string, v ...interface{}) Log {
|
||||
return Log{
|
||||
LogLevel: logLevel,
|
||||
Payload: fmt.Sprintf(format, v...),
|
||||
|
228
tunnel/tunnel.go
228
tunnel/tunnel.go
@ -1,16 +1,13 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters"
|
||||
LocalAdapter "github.com/Dreamacro/clash/adapters/local"
|
||||
cfg "github.com/Dreamacro/clash/config"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/observable"
|
||||
R "github.com/Dreamacro/clash/rules"
|
||||
|
||||
"gopkg.in/eapache/channels.v1"
|
||||
)
|
||||
@ -20,168 +17,60 @@ var (
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// Tunnel handle proxy socket and HTTP/SOCKS socket
|
||||
type Tunnel struct {
|
||||
queue *channels.InfiniteChannel
|
||||
rules []C.Rule
|
||||
proxys map[string]C.Proxy
|
||||
observable *observable.Observable
|
||||
logCh chan interface{}
|
||||
proxies map[string]C.Proxy
|
||||
configLock *sync.RWMutex
|
||||
traffic *C.Traffic
|
||||
mode Mode
|
||||
selector *adapters.Selector
|
||||
|
||||
// Outbound Rule
|
||||
mode cfg.Mode
|
||||
|
||||
// Log
|
||||
logCh chan interface{}
|
||||
observable *observable.Observable
|
||||
logLevel C.LogLevel
|
||||
}
|
||||
|
||||
// Add request to queue
|
||||
func (t *Tunnel) Add(req C.ServerAdapter) {
|
||||
t.queue.In() <- req
|
||||
}
|
||||
|
||||
// Traffic return traffic of all connections
|
||||
func (t *Tunnel) Traffic() *C.Traffic {
|
||||
return t.traffic
|
||||
}
|
||||
|
||||
func (t *Tunnel) Config() ([]C.Rule, map[string]C.Proxy) {
|
||||
return t.rules, t.proxys
|
||||
}
|
||||
|
||||
// Log return clash log stream
|
||||
func (t *Tunnel) Log() *observable.Observable {
|
||||
return t.observable
|
||||
}
|
||||
|
||||
func (t *Tunnel) SetMode(mode Mode) {
|
||||
t.mode = mode
|
||||
}
|
||||
|
||||
func (t *Tunnel) GetMode() Mode {
|
||||
return t.mode
|
||||
}
|
||||
|
||||
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 := key.Strings(",")
|
||||
if len(proxy) == 0 {
|
||||
continue
|
||||
}
|
||||
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
|
||||
func (t *Tunnel) configMonitor(signal chan<- struct{}) {
|
||||
sub := cfg.Instance().Subscribe()
|
||||
signal <- struct{}{}
|
||||
for elm := range sub {
|
||||
event := elm.(*cfg.Event)
|
||||
switch event.Type {
|
||||
case "proxies":
|
||||
proxies := event.Payload.(map[string]C.Proxy)
|
||||
t.configLock.Lock()
|
||||
t.proxies = proxies
|
||||
t.configLock.Unlock()
|
||||
case "rules":
|
||||
rules := event.Payload.([]C.Rule)
|
||||
t.configLock.Lock()
|
||||
t.rules = rules
|
||||
t.configLock.Unlock()
|
||||
case "mode":
|
||||
t.mode = event.Payload.(cfg.Mode)
|
||||
case "log-level":
|
||||
t.logLevel = event.Payload.(C.LogLevel)
|
||||
}
|
||||
}
|
||||
|
||||
// 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(), ",")
|
||||
rule = trimArr(rule)
|
||||
switch rule[0] {
|
||||
case "url-test":
|
||||
if len(rule) < 4 {
|
||||
return fmt.Errorf("URLTest need more than 4 param")
|
||||
}
|
||||
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
|
||||
case "select":
|
||||
if len(rule) < 3 {
|
||||
return fmt.Errorf("Selector need more than 3 param")
|
||||
}
|
||||
proxyNames := rule[1:]
|
||||
selectProxy := make(map[string]C.Proxy)
|
||||
for _, name := range proxyNames {
|
||||
proxy, exist := proxys[name]
|
||||
if !exist {
|
||||
return fmt.Errorf("Proxy %s not exist", name)
|
||||
}
|
||||
selectProxy[name] = proxy
|
||||
}
|
||||
selector, err := adapters.NewSelector(key.Name(), selectProxy)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Selector create error: %s", err.Error())
|
||||
}
|
||||
proxys[key.Name()] = selector
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
s, err := adapters.NewSelector("Proxy", proxys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.proxys = proxys
|
||||
t.rules = rules
|
||||
t.selector = s
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tunnel) process() {
|
||||
@ -199,22 +88,27 @@ func (t *Tunnel) handleConn(localConn C.ServerAdapter) {
|
||||
|
||||
var proxy C.Proxy
|
||||
switch t.mode {
|
||||
case Direct:
|
||||
proxy = t.proxys["DIRECT"]
|
||||
case Global:
|
||||
proxy = t.selector
|
||||
case cfg.Direct:
|
||||
proxy = t.proxies["DIRECT"]
|
||||
case cfg.Global:
|
||||
proxy = t.proxies["GLOBAL"]
|
||||
// Rule
|
||||
default:
|
||||
proxy = t.match(addr)
|
||||
}
|
||||
remoConn, err := proxy.Generator(addr)
|
||||
if err != nil {
|
||||
t.logCh <- newLog(WARNING, "Proxy connect error: %s", err.Error())
|
||||
t.logCh <- newLog(C.WARNING, "Proxy connect error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
defer remoConn.Close()
|
||||
|
||||
localConn.Connect(remoConn)
|
||||
switch adapter := localConn.(type) {
|
||||
case *LocalAdapter.HTTPAdapter:
|
||||
t.handleHTTP(adapter, remoConn)
|
||||
case *LocalAdapter.SocksAdapter:
|
||||
t.handleSOCKS(adapter, remoConn)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tunnel) match(addr *C.Addr) C.Proxy {
|
||||
@ -223,35 +117,43 @@ func (t *Tunnel) match(addr *C.Addr) C.Proxy {
|
||||
|
||||
for _, rule := range t.rules {
|
||||
if rule.IsMatch(addr) {
|
||||
a, ok := t.proxys[rule.Adapter()]
|
||||
a, ok := t.proxies[rule.Adapter()]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
t.logCh <- newLog(INFO, "%v match %s using %s", addr.String(), rule.RuleType().String(), rule.Adapter())
|
||||
t.logCh <- newLog(C.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"]
|
||||
t.logCh <- newLog(C.INFO, "%v doesn't match any rule using DIRECT", addr.String())
|
||||
return t.proxies["DIRECT"]
|
||||
}
|
||||
|
||||
// Run initial task
|
||||
func (t *Tunnel) Run() {
|
||||
go t.process()
|
||||
go t.subscribeLogs()
|
||||
signal := make(chan struct{})
|
||||
go t.configMonitor(signal)
|
||||
<-signal
|
||||
}
|
||||
|
||||
func newTunnel() *Tunnel {
|
||||
logCh := make(chan interface{})
|
||||
tunnel := &Tunnel{
|
||||
return &Tunnel{
|
||||
queue: channels.NewInfiniteChannel(),
|
||||
proxys: make(map[string]C.Proxy),
|
||||
proxies: make(map[string]C.Proxy),
|
||||
observable: observable.NewObservable(logCh),
|
||||
logCh: logCh,
|
||||
configLock: &sync.RWMutex{},
|
||||
traffic: C.NewTraffic(time.Second),
|
||||
mode: Rule,
|
||||
mode: cfg.Rule,
|
||||
logLevel: C.INFO,
|
||||
}
|
||||
go tunnel.process()
|
||||
go tunnel.subscribeLogs()
|
||||
return tunnel
|
||||
}
|
||||
|
||||
func GetInstance() *Tunnel {
|
||||
// Instance return singleton instance of Tunnel
|
||||
func Instance() *Tunnel {
|
||||
once.Do(func() {
|
||||
tunnel = newTunnel()
|
||||
})
|
||||
|
@ -1,4 +1,4 @@
|
||||
package adapters
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"net"
|
||||
@ -6,6 +6,7 @@ import (
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
// TrafficTrack record traffic of net.Conn
|
||||
type TrafficTrack struct {
|
||||
net.Conn
|
||||
traffic *C.Traffic
|
||||
@ -23,6 +24,6 @@ func (tt *TrafficTrack) Write(b []byte) (int, error) {
|
||||
return n, err
|
||||
}
|
||||
|
||||
func NewTrafficTrack(conn net.Conn, traffic *C.Traffic) *TrafficTrack {
|
||||
func newTrafficTrack(conn net.Conn, traffic *C.Traffic) *TrafficTrack {
|
||||
return &TrafficTrack{traffic: traffic, Conn: conn}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func trimArr(arr []string) (r []string) {
|
||||
for _, e := range arr {
|
||||
r = append(r, strings.Trim(e, " "))
|
||||
}
|
||||
return
|
||||
}
|
Reference in New Issue
Block a user