Compare commits

..

15 Commits

21 changed files with 366 additions and 106 deletions

View File

@ -1,17 +1,23 @@
FROM golang:latest as builder FROM golang:latest as builder
RUN wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz -O /tmp/GeoLite2-Country.tar.gz && \ RUN wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz -O /tmp/GeoLite2-Country.tar.gz && \
tar zxvf /tmp/GeoLite2-Country.tar.gz -C /tmp && \ tar zxvf /tmp/GeoLite2-Country.tar.gz -C /tmp && \
cp /tmp/GeoLite2-Country_*/GeoLite2-Country.mmdb /Country.mmdb mv /tmp/GeoLite2-Country_*/GeoLite2-Country.mmdb /Country.mmdb
WORKDIR /clash-src WORKDIR /clash-src
COPY . /clash-src COPY . /clash-src
RUN go mod download && \ RUN go mod download && \
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o /clash && \ GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o /clash
chmod +x /clash
FROM alpine:latest FROM alpine:latest
RUN apk --no-cache add ca-certificates && \
mkdir -p /root/.config/clash RUN apk add --no-cache ca-certificates
COPY --from=builder /Country.mmdb /root/.config/clash/ COPY --from=builder /Country.mmdb /root/.config/clash/
COPY --from=builder /clash . COPY --from=builder /clash /
EXPOSE 7890 7891 EXPOSE 7890 7891
ENTRYPOINT ["/clash"] ENTRYPOINT ["/clash"]

View File

@ -101,7 +101,7 @@ log-level: info
external-controller: 127.0.0.1:9090 external-controller: 127.0.0.1:9090
# Secret for RESTful API (Optional) # Secret for RESTful API (Optional)
secret: "" # secret: ""
Proxy: Proxy:
@ -114,11 +114,22 @@ Proxy:
# vmess # vmess
# cipher support auto/aes-128-gcm/chacha20-poly1305/none # cipher support auto/aes-128-gcm/chacha20-poly1305/none
- { name: "vmess1", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto } - { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto }
- { name: "vmess2", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, tls: true } # with tls
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, tls: true }
# with tls and skip-cert-verify
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, tls: true, skip-cert-verify: true }
# with ws
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, network: ws, ws-path: /path }
# with ws + tls
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, network: ws, ws-path: /path, tls: true }
# socks5 # socks5
- { name: "socks", type: socks5, server: server, port: 443 } - { name: "socks", type: socks5, server: server, port: 443 }
# with tls
- { name: "socks", type: socks5, server: server, port: 443, tls: true }
# with tls and skip-cert-verify
- { name: "socks", type: socks5, server: server, port: 443, tls: true, skip-cert-verify: true }
Proxy Group: Proxy Group:
# url-test select which proxy will be used by benchmarking speed to a URL. # url-test select which proxy will be used by benchmarking speed to a URL.
@ -134,7 +145,9 @@ Proxy Group:
Rule: Rule:
- DOMAIN-SUFFIX,google.com,Proxy - DOMAIN-SUFFIX,google.com,Proxy
- DOMAIN-KEYWORD,google,Proxy - DOMAIN-KEYWORD,google,Proxy
- DOMAIN,google.com,Proxy
- DOMAIN-SUFFIX,ad.com,REJECT - DOMAIN-SUFFIX,ad.com,REJECT
- IP-CIDR,127.0.0.0/8,DIRECT
- GEOIP,CN,DIRECT - GEOIP,CN,DIRECT
# note: there is two "," # note: there is two ","
- FINAL,,Proxy - FINAL,,Proxy

View File

@ -24,7 +24,7 @@ func (d *DirectAdapter) Conn() net.Conn {
type Direct struct{} type Direct struct{}
func (d *Direct) Name() string { func (d *Direct) Name() string {
return "Direct" return "DIRECT"
} }
func (d *Direct) Type() C.AdapterType { func (d *Direct) Type() C.AdapterType {
@ -32,7 +32,7 @@ func (d *Direct) Type() C.AdapterType {
} }
func (d *Direct) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) { func (d *Direct) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
c, err := net.Dial("tcp", net.JoinHostPort(metadata.String(), metadata.Port)) c, err := net.DialTimeout("tcp", net.JoinHostPort(metadata.String(), metadata.Port), tcpTimeout)
if err != nil { if err != nil {
return return
} }

View File

@ -25,7 +25,7 @@ type Reject struct {
} }
func (r *Reject) Name() string { func (r *Reject) Name() string {
return "Reject" return "REJECT"
} }
func (r *Reject) Type() C.AdapterType { func (r *Reject) Type() C.AdapterType {

View File

@ -52,21 +52,20 @@ func (s *Selector) Set(name string) error {
return nil return nil
} }
func NewSelector(name string, proxies map[string]C.Proxy) (*Selector, error) { func NewSelector(name string, proxies []C.Proxy) (*Selector, error) {
if len(proxies) == 0 { if len(proxies) == 0 {
return nil, errors.New("Provide at least one proxy") return nil, errors.New("Provide at least one proxy")
} }
mapping := make(map[string]C.Proxy) mapping := make(map[string]C.Proxy)
var init string for _, proxy := range proxies {
for k, v := range proxies { mapping[proxy.Name()] = proxy
mapping[k] = v
init = k
} }
s := &Selector{ s := &Selector{
name: name, name: name,
proxies: mapping, proxies: mapping,
selected: proxies[init], selected: proxies[0],
} }
return s, nil return s, nil
} }

View File

@ -54,7 +54,7 @@ func (ss *ShadowSocks) Type() C.AdapterType {
} }
func (ss *ShadowSocks) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) { func (ss *ShadowSocks) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
c, err := net.Dial("tcp", ss.server) c, err := net.DialTimeout("tcp", ss.server, tcpTimeout)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error", ss.server) return nil, fmt.Errorf("%s connect error", ss.server)
} }

View File

@ -2,6 +2,7 @@ package adapters
import ( import (
"bytes" "bytes"
"crypto/tls"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -27,14 +28,18 @@ func (ss *Socks5Adapter) Conn() net.Conn {
} }
type Socks5 struct { type Socks5 struct {
addr string addr string
name string name string
tls bool
skipCertVerify bool
} }
type Socks5Option struct { type Socks5Option struct {
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
TLS bool `proxy:"tls,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
} }
func (ss *Socks5) Name() string { func (ss *Socks5) Name() string {
@ -46,7 +51,16 @@ func (ss *Socks5) Type() C.AdapterType {
} }
func (ss *Socks5) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) { func (ss *Socks5) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
c, err := net.Dial("tcp", ss.addr) c, err := net.DialTimeout("tcp", ss.addr, tcpTimeout)
if err == nil && ss.tls {
tlsConfig := tls.Config{
InsecureSkipVerify: ss.skipCertVerify,
MaxVersion: tls.VersionTLS12,
}
c = tls.Client(c, &tlsConfig)
}
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error", ss.addr) return nil, fmt.Errorf("%s connect error", ss.addr)
} }
@ -90,7 +104,9 @@ func (ss *Socks5) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
func NewSocks5(option Socks5Option) *Socks5 { func NewSocks5(option Socks5Option) *Socks5 {
return &Socks5{ return &Socks5{
addr: fmt.Sprintf("%s:%d", option.Server, option.Port), addr: fmt.Sprintf("%s:%d", option.Server, option.Port),
name: option.Name, name: option.Name,
tls: option.TLS,
skipCertVerify: option.SkipCertVerify,
} }
} }

View File

@ -3,6 +3,7 @@ package adapters
import ( import (
"errors" "errors"
"sync" "sync"
"sync/atomic"
"time" "time"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
@ -15,6 +16,7 @@ type URLTest struct {
fast C.Proxy fast C.Proxy
interval time.Duration interval time.Duration
done chan struct{} done chan struct{}
once int32
} }
type URLTestOption struct { type URLTestOption struct {
@ -37,7 +39,11 @@ func (u *URLTest) Now() string {
} }
func (u *URLTest) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) { func (u *URLTest) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
return u.fast.Generator(metadata) a, err := u.fast.Generator(metadata)
if err != nil {
go u.speedTest()
}
return a, err
} }
func (u *URLTest) Close() { func (u *URLTest) Close() {
@ -59,6 +65,11 @@ Loop:
} }
func (u *URLTest) speedTest() { func (u *URLTest) speedTest() {
if atomic.AddInt32(&u.once, 1) != 1 {
return
}
defer atomic.StoreInt32(&u.once, 0)
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
wg.Add(len(u.proxies)) wg.Add(len(u.proxies))
c := make(chan interface{}) c := make(chan interface{})
@ -108,6 +119,7 @@ func NewURLTest(option URLTestOption, proxies []C.Proxy) (*URLTest, error) {
fast: proxies[0], fast: proxies[0],
interval: interval, interval: interval,
done: make(chan struct{}), done: make(chan struct{}),
once: 0,
} }
go urlTest.loop() go urlTest.loop()
return urlTest, nil return urlTest, nil

View File

@ -10,6 +10,10 @@ import (
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
const (
tcpTimeout = 5 * time.Second
)
// DelayTest get the delay for the specified URL // DelayTest get the delay for the specified URL
func DelayTest(proxy C.Proxy, url string) (t int16, err error) { func DelayTest(proxy C.Proxy, url string) (t int16, err error) {
addr, err := urlToMetadata(url) addr, err := urlToMetadata(url)

View File

@ -31,13 +31,16 @@ type Vmess struct {
} }
type VmessOption struct { type VmessOption struct {
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
UUID string `proxy:"uuid"` UUID string `proxy:"uuid"`
AlterID int `proxy:"alterId"` AlterID int `proxy:"alterId"`
Cipher string `proxy:"cipher"` Cipher string `proxy:"cipher"`
TLS bool `proxy:"tls,omitempty"` TLS bool `proxy:"tls,omitempty"`
Network string `proxy:"network,omitempty"`
WSPath string `proxy:"ws-path,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
} }
func (ss *Vmess) Name() string { func (ss *Vmess) Name() string {
@ -49,22 +52,26 @@ func (ss *Vmess) Type() C.AdapterType {
} }
func (ss *Vmess) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) { func (ss *Vmess) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
c, err := net.Dial("tcp", ss.server) c, err := net.DialTimeout("tcp", ss.server, tcpTimeout)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error", ss.server) return nil, fmt.Errorf("%s connect error", ss.server)
} }
tcpKeepAlive(c) tcpKeepAlive(c)
c = ss.client.New(c, parseVmessAddr(metadata)) c, err = ss.client.New(c, parseVmessAddr(metadata))
return &VmessAdapter{conn: c}, err return &VmessAdapter{conn: c}, err
} }
func NewVmess(option VmessOption) (*Vmess, error) { func NewVmess(option VmessOption) (*Vmess, error) {
security := strings.ToLower(option.Cipher) security := strings.ToLower(option.Cipher)
client, err := vmess.NewClient(vmess.Config{ client, err := vmess.NewClient(vmess.Config{
UUID: option.UUID, UUID: option.UUID,
AlterID: uint16(option.AlterID), AlterID: uint16(option.AlterID),
Security: security, Security: security,
TLS: option.TLS, TLS: option.TLS,
Host: fmt.Sprintf("%s:%d", option.Server, option.Port),
NetWork: option.Network,
WebSocketPath: option.WSPath,
SkipCertVerify: option.SkipCertVerify,
}) })
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -50,5 +50,6 @@ func newAlterIDs(primary *ID, alterIDCount uint16) []*ID {
alterIDs[idx] = &ID{UUID: newid, CmdKey: primary.CmdKey[:]} alterIDs[idx] = &ID{UUID: newid, CmdKey: primary.CmdKey[:]}
prevID = newid prevID = newid
} }
alterIDs = append(alterIDs, primary)
return alterIDs return alterIDs
} }

View File

@ -5,9 +5,12 @@ import (
"fmt" "fmt"
"math/rand" "math/rand"
"net" "net"
"net/url"
"runtime" "runtime"
"time"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
"github.com/gorilla/websocket"
) )
// Version of vmess // Version of vmess
@ -36,10 +39,6 @@ var CipherMapping = map[string]byte{
"chacha20-poly1305": SecurityCHACHA20POLY1305, "chacha20-poly1305": SecurityCHACHA20POLY1305,
} }
var tlsConfig = &tls.Config{
InsecureSkipVerify: true,
}
// Command types // Command types
const ( const (
CommandTCP byte = 1 CommandTCP byte = 1
@ -62,27 +61,76 @@ type DstAddr struct {
// Client is vmess connection generator // Client is vmess connection generator
type Client struct { type Client struct {
user []*ID user []*ID
uuid *uuid.UUID uuid *uuid.UUID
security Security security Security
tls bool tls bool
host string
websocket bool
websocketPath string
skipCertVerify bool
} }
// Config of vmess // Config of vmess
type Config struct { type Config struct {
UUID string UUID string
AlterID uint16 AlterID uint16
Security string Security string
TLS bool TLS bool
Host string
NetWork string
WebSocketPath string
SkipCertVerify bool
} }
// New return a Conn with net.Conn and DstAddr // New return a Conn with net.Conn and DstAddr
func (c *Client) New(conn net.Conn, dst *DstAddr) net.Conn { func (c *Client) New(conn net.Conn, dst *DstAddr) (net.Conn, error) {
r := rand.Intn(len(c.user)) r := rand.Intn(len(c.user))
if c.tls { if c.websocket {
conn = tls.Client(conn, tlsConfig) dialer := &websocket.Dialer{
NetDial: func(network, addr string) (net.Conn, error) {
return conn, nil
},
ReadBufferSize: 4 * 1024,
WriteBufferSize: 4 * 1024,
HandshakeTimeout: time.Second * 8,
}
scheme := "ws"
if c.tls {
scheme = "wss"
dialer.TLSClientConfig = &tls.Config{
InsecureSkipVerify: c.skipCertVerify,
}
}
host, port, err := net.SplitHostPort(c.host)
if (scheme == "ws" && port != "80") || (scheme == "wss" && port != "443") {
host = c.host
}
uri := url.URL{
Scheme: scheme,
Host: host,
Path: c.websocketPath,
}
wsConn, resp, err := dialer.Dial(uri.String(), nil)
if err != nil {
var reason string
if resp != nil {
reason = resp.Status
}
println(uri.String(), err.Error())
return nil, fmt.Errorf("Dial %s error: %s", host, reason)
}
conn = newWebsocketConn(wsConn, conn.RemoteAddr())
} else if c.tls {
conn = tls.Client(conn, &tls.Config{
InsecureSkipVerify: c.skipCertVerify,
})
} }
return newConn(conn, c.user[r], dst, c.security) return newConn(conn, c.user[r], dst, c.security), nil
} }
// NewClient return Client instance // NewClient return Client instance
@ -108,10 +156,18 @@ func NewClient(config Config) (*Client, error) {
default: default:
return nil, fmt.Errorf("Unknown security type: %s", config.Security) return nil, fmt.Errorf("Unknown security type: %s", config.Security)
} }
if config.NetWork != "" && config.NetWork != "ws" {
return nil, fmt.Errorf("Unknown network type: %s", config.NetWork)
}
return &Client{ return &Client{
user: newAlterIDs(newID(&uid), config.AlterID), user: newAlterIDs(newID(&uid), config.AlterID),
uuid: &uid, uuid: &uid,
security: security, security: security,
tls: config.TLS, tls: config.TLS,
host: config.Host,
websocket: config.NetWork == "ws",
websocketPath: config.WebSocketPath,
}, nil }, nil
} }

View File

@ -0,0 +1,99 @@
package vmess
import (
"fmt"
"io"
"net"
"strings"
"time"
"github.com/gorilla/websocket"
)
type websocketConn struct {
conn *websocket.Conn
reader io.Reader
remoteAddr net.Addr
}
// Read implements net.Conn.Read()
func (wsc *websocketConn) Read(b []byte) (int, error) {
for {
reader, err := wsc.getReader()
if err != nil {
return 0, err
}
nBytes, err := reader.Read(b)
if err == io.EOF {
wsc.reader = nil
continue
}
return nBytes, err
}
}
// Write implements io.Writer.
func (wsc *websocketConn) Write(b []byte) (int, error) {
if err := wsc.conn.WriteMessage(websocket.BinaryMessage, b); err != nil {
return 0, err
}
return len(b), nil
}
func (wsc *websocketConn) Close() error {
var errors []string
if err := wsc.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(time.Second*5)); err != nil {
errors = append(errors, err.Error())
}
if err := wsc.conn.Close(); err != nil {
errors = append(errors, err.Error())
}
if len(errors) > 0 {
return fmt.Errorf("Failed to close connection: %s", strings.Join(errors, ","))
}
return nil
}
func (wsc *websocketConn) getReader() (io.Reader, error) {
if wsc.reader != nil {
return wsc.reader, nil
}
_, reader, err := wsc.conn.NextReader()
if err != nil {
return nil, err
}
wsc.reader = reader
return reader, nil
}
func (wsc *websocketConn) LocalAddr() net.Addr {
return wsc.conn.LocalAddr()
}
func (wsc *websocketConn) RemoteAddr() net.Addr {
return wsc.remoteAddr
}
func (wsc *websocketConn) SetDeadline(t time.Time) error {
if err := wsc.SetReadDeadline(t); err != nil {
return err
}
return wsc.SetWriteDeadline(t)
}
func (wsc *websocketConn) SetReadDeadline(t time.Time) error {
return wsc.conn.SetReadDeadline(t)
}
func (wsc *websocketConn) SetWriteDeadline(t time.Time) error {
return wsc.conn.SetWriteDeadline(t)
}
func newWebsocketConn(conn *websocket.Conn, remoteAddr net.Addr) net.Conn {
return &websocketConn{
conn: conn,
remoteAddr: remoteAddr,
}
}

View File

@ -250,14 +250,10 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
// parse proxy // parse proxy
for idx, mapping := range proxiesConfig { for idx, mapping := range proxiesConfig {
proxyType, existType := mapping["type"].(string) proxyType, existType := mapping["type"].(string)
proxyName, existName := mapping["name"].(string) if !existType {
if !existType && existName { return fmt.Errorf("Proxy %d missing type", idx)
return fmt.Errorf("Proxy %d missing type or name", idx)
} }
if _, exist := proxies[proxyName]; exist {
return fmt.Errorf("Proxy %s is the duplicate name", proxyName)
}
var proxy C.Proxy var proxy C.Proxy
var err error var err error
switch proxyType { switch proxyType {
@ -285,10 +281,15 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
default: default:
return fmt.Errorf("Unsupport proxy type: %s", proxyType) return fmt.Errorf("Unsupport proxy type: %s", proxyType)
} }
if err != nil { if err != nil {
return fmt.Errorf("Proxy %s: %s", proxyName, err.Error()) return fmt.Errorf("Proxy [%d]: %s", idx, err.Error())
} }
proxies[proxyName] = proxy
if _, exist := proxies[proxy.Name()]; exist {
return fmt.Errorf("Proxy %s is the duplicate name", proxy.Name())
}
proxies[proxy.Name()] = proxy
} }
// parse proxy group // parse proxy group
@ -312,13 +313,9 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
break break
} }
var ps []C.Proxy ps, err := getProxies(proxies, urlTestOption.Proxies)
for _, name := range urlTestOption.Proxies { if err != nil {
p, ok := proxies[name] return fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error())
if !ok {
return fmt.Errorf("ProxyGroup %s: proxy or proxy group '%s' not found", groupName, name)
}
ps = append(ps, p)
} }
group, err = adapters.NewURLTest(*urlTestOption, ps) group, err = adapters.NewURLTest(*urlTestOption, ps)
case "select": case "select":
@ -327,28 +324,22 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
if err != nil { if err != nil {
break break
} }
selectProxy := make(map[string]C.Proxy)
for _, name := range selectorOption.Proxies { ps, err := getProxies(proxies, selectorOption.Proxies)
proxy, exist := proxies[name] if err != nil {
if !exist { return fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error())
return fmt.Errorf("ProxyGroup %s: proxy or proxy group '%s' not found", groupName, name)
}
selectProxy[name] = proxy
} }
group, err = adapters.NewSelector(selectorOption.Name, selectProxy) group, err = adapters.NewSelector(selectorOption.Name, ps)
case "fallback": case "fallback":
fallbackOption := &adapters.FallbackOption{} fallbackOption := &adapters.FallbackOption{}
err = decoder.Decode(mapping, fallbackOption) err = decoder.Decode(mapping, fallbackOption)
if err != nil { if err != nil {
break break
} }
var ps []C.Proxy
for _, name := range fallbackOption.Proxies { ps, err := getProxies(proxies, fallbackOption.Proxies)
p, ok := proxies[name] if err != nil {
if !ok { return fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error())
return fmt.Errorf("ProxyGroup %s: proxy or proxy group '%s' not found", groupName, name)
}
ps = append(ps, p)
} }
group, err = adapters.NewFallback(*fallbackOption, ps) group, err = adapters.NewFallback(*fallbackOption, ps)
} }
@ -358,7 +349,12 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
proxies[groupName] = group proxies[groupName] = group
} }
proxies["GLOBAL"], _ = adapters.NewSelector("GLOBAL", proxies) var ps []C.Proxy
for _, v := range proxies {
ps = append(ps, v)
}
proxies["GLOBAL"], _ = adapters.NewSelector("GLOBAL", ps)
// close old goroutine // close old goroutine
for _, proxy := range c.proxies { for _, proxy := range c.proxies {

View File

@ -3,6 +3,8 @@ package config
import ( import (
"fmt" "fmt"
"strings" "strings"
C "github.com/Dreamacro/clash/constant"
) )
func trimArr(arr []string) (r []string) { func trimArr(arr []string) (r []string) {
@ -19,6 +21,18 @@ func genAddr(port int, allowLan bool) string {
return fmt.Sprintf("127.0.0.1:%d", port) return fmt.Sprintf("127.0.0.1:%d", port)
} }
func getProxies(mapping map[string]C.Proxy, list []string) ([]C.Proxy, error) {
var ps []C.Proxy
for _, name := range list {
p, ok := mapping[name]
if !ok {
return nil, fmt.Errorf("'%s' not found", name)
}
ps = append(ps, p)
}
return ps, nil
}
func or(pointers ...*int) *int { func or(pointers ...*int) *int {
for _, p := range pointers { for _, p := range pointers {
if p != nil { if p != nil {

3
go.mod
View File

@ -1,12 +1,13 @@
module github.com/Dreamacro/clash module github.com/Dreamacro/clash
require ( require (
github.com/Dreamacro/go-shadowsocks2 v0.1.2-0.20181016063207-89bf7cffdaf4 github.com/Dreamacro/go-shadowsocks2 v0.1.2-0.20181019110427-0a03f1a25270
github.com/eapache/queue v1.1.0 // indirect github.com/eapache/queue v1.1.0 // indirect
github.com/go-chi/chi v3.3.3+incompatible github.com/go-chi/chi v3.3.3+incompatible
github.com/go-chi/cors v1.0.0 github.com/go-chi/cors v1.0.0
github.com/go-chi/render v1.0.1 github.com/go-chi/render v1.0.1
github.com/gofrs/uuid v3.1.0+incompatible github.com/gofrs/uuid v3.1.0+incompatible
github.com/gorilla/websocket v1.4.0
github.com/oschwald/geoip2-golang v1.2.1 github.com/oschwald/geoip2-golang v1.2.1
github.com/oschwald/maxminddb-golang v1.3.0 // indirect github.com/oschwald/maxminddb-golang v1.3.0 // indirect
github.com/sirupsen/logrus v1.1.0 github.com/sirupsen/logrus v1.1.0

6
go.sum
View File

@ -1,5 +1,5 @@
github.com/Dreamacro/go-shadowsocks2 v0.1.2-0.20181016063207-89bf7cffdaf4 h1:n+F4LoHdFCwsGohVN+4dwfmmx0h2uvdPEeiwdhFeH8g= github.com/Dreamacro/go-shadowsocks2 v0.1.2-0.20181019110427-0a03f1a25270 h1:ugkI+Yw5ArFnhF8KTbJxyWIyvxMCa8jWyUF+wIAulhM=
github.com/Dreamacro/go-shadowsocks2 v0.1.2-0.20181016063207-89bf7cffdaf4/go.mod h1:DlkXRxmh5K+99aTPQaVjsZ1fAZNFw42vXGcOjR3Otps= github.com/Dreamacro/go-shadowsocks2 v0.1.2-0.20181019110427-0a03f1a25270/go.mod h1:DlkXRxmh5K+99aTPQaVjsZ1fAZNFw42vXGcOjR3Otps=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -14,6 +14,8 @@ github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/gofrs/uuid v3.1.0+incompatible h1:q2rtkjaKT4YEr6E1kamy0Ha4RtepWlQBedyHx0uzKwA= github.com/gofrs/uuid v3.1.0+incompatible h1:q2rtkjaKT4YEr6E1kamy0Ha4RtepWlQBedyHx0uzKwA=
github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe h1:CHRGQ8V7OlCYtwaKPJi3iA7J+YdNKdo8j7nG5IgDhjs= github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe h1:CHRGQ8V7OlCYtwaKPJi3iA7J+YdNKdo8j7nG5IgDhjs=
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/oschwald/geoip2-golang v1.2.1 h1:3iz+jmeJc6fuCyWeKgtXSXu7+zvkxJbHFXkMT5FVebU= github.com/oschwald/geoip2-golang v1.2.1 h1:3iz+jmeJc6fuCyWeKgtXSXu7+zvkxJbHFXkMT5FVebU=

View File

@ -1,8 +1,10 @@
package hub package hub
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"time" "time"
@ -16,12 +18,24 @@ import (
func proxyRouter() http.Handler { func proxyRouter() http.Handler {
r := chi.NewRouter() r := chi.NewRouter()
r.Get("/", getProxies) r.Get("/", getProxies)
r.Get("/{name}", getProxy) r.With(parseProxyName).Get("/{name}", getProxy)
r.Get("/{name}/delay", getProxyDelay) r.With(parseProxyName).Get("/{name}/delay", getProxyDelay)
r.Put("/{name}", updateProxy) r.With(parseProxyName).Put("/{name}", updateProxy)
return r return r
} }
// When name is composed of a partial escape string, Golang does not unescape it
func parseProxyName(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
if newName, err := url.PathUnescape(name); err == nil {
name = newName
}
ctx := context.WithValue(r.Context(), contextKey("proxy name"), name)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
type SampleProxy struct { type SampleProxy struct {
Type string `json:"type"` Type string `json:"type"`
} }
@ -83,7 +97,7 @@ func getProxies(w http.ResponseWriter, r *http.Request) {
} }
func getProxy(w http.ResponseWriter, r *http.Request) { func getProxy(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name") name := r.Context().Value(contextKey("proxy name")).(string)
proxies := cfg.Proxies() proxies := cfg.Proxies()
proxy, exist := proxies[name] proxy, exist := proxies[name]
if !exist { if !exist {
@ -110,7 +124,7 @@ func updateProxy(w http.ResponseWriter, r *http.Request) {
return return
} }
name := chi.URLParam(r, "name") name := r.Context().Value(contextKey("proxy name")).(string)
proxies := cfg.Proxies() proxies := cfg.Proxies()
proxy, exist := proxies[name] proxy, exist := proxies[name]
if !exist { if !exist {
@ -162,7 +176,7 @@ func getProxyDelay(w http.ResponseWriter, r *http.Request) {
return return
} }
name := chi.URLParam(r, "name") name := r.Context().Value(contextKey("proxy name")).(string)
proxies := cfg.Proxies() proxies := cfg.Proxies()
proxy, exist := proxies[name] proxy, exist := proxies[name]
if !exist { if !exist {

View File

@ -15,8 +15,8 @@ func ruleRouter() http.Handler {
} }
type Rule struct { type Rule struct {
Name string `json:"name"` Type string `json:"type"`
Payload string `json:"type"` Payload string `json:"payload"`
Proxy string `json:"proxy"` Proxy string `json:"proxy"`
} }
@ -30,7 +30,7 @@ func getRules(w http.ResponseWriter, r *http.Request) {
var rules []Rule var rules []Rule
for _, rule := range rawRules { for _, rule := range rawRules {
rules = append(rules, Rule{ rules = append(rules, Rule{
Name: rule.RuleType().String(), Type: rule.RuleType().String(),
Payload: rule.Payload(), Payload: rule.Payload(),
Proxy: rule.Adapter(), Proxy: rule.Adapter(),
}) })

View File

@ -100,6 +100,12 @@ func authentication(next http.Handler) http.Handler {
return http.HandlerFunc(fn) return http.HandlerFunc(fn)
} }
type contextKey string
func (c contextKey) String() string {
return "clash context key " + string(c)
}
func traffic(w http.ResponseWriter, r *http.Request) { func traffic(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)

View File

@ -5,12 +5,22 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"sync"
"time" "time"
"github.com/Dreamacro/clash/adapters/inbound" "github.com/Dreamacro/clash/adapters/inbound"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
const (
// io.Copy default buffer size is 32 KiB
// but the maximum packet size of vmess/shadowsocks is about 16 KiB
// so define a buffer of 20 KiB to reduce the memory of each TCP relay
bufferSize = 20 * 1024
)
var bufPool = sync.Pool{New: func() interface{} { return make([]byte, bufferSize) }}
func (t *Tunnel) handleHTTP(request *adapters.HTTPAdapter, proxy C.ProxyAdapter) { func (t *Tunnel) handleHTTP(request *adapters.HTTPAdapter, proxy C.ProxyAdapter) {
conn := newTrafficTrack(proxy.Conn(), t.traffic) conn := newTrafficTrack(proxy.Conn(), t.traffic)
req := request.R req := request.R
@ -66,12 +76,16 @@ func relay(leftConn, rightConn net.Conn) {
ch := make(chan error) ch := make(chan error)
go func() { go func() {
_, err := io.Copy(leftConn, rightConn) buf := bufPool.Get().([]byte)
_, err := io.CopyBuffer(leftConn, rightConn, buf)
bufPool.Put(buf[:cap(buf)])
leftConn.SetReadDeadline(time.Now()) leftConn.SetReadDeadline(time.Now())
ch <- err ch <- err
}() }()
io.Copy(rightConn, leftConn) buf := bufPool.Get().([]byte)
io.CopyBuffer(rightConn, leftConn, buf)
bufPool.Put(buf[:cap(buf)])
rightConn.SetReadDeadline(time.Now()) rightConn.SetReadDeadline(time.Now())
<-ch <-ch
} }