Compare commits

...

12 Commits

18 changed files with 324 additions and 72 deletions

View File

@ -1,17 +1,23 @@
FROM golang:latest as builder
RUN wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz -O /tmp/GeoLite2-Country.tar.gz && \
tar zxvf /tmp/GeoLite2-Country.tar.gz -C /tmp && \
cp /tmp/GeoLite2-Country_*/GeoLite2-Country.mmdb /Country.mmdb
mv /tmp/GeoLite2-Country_*/GeoLite2-Country.mmdb /Country.mmdb
WORKDIR /clash-src
COPY . /clash-src
RUN go mod download && \
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o /clash && \
chmod +x /clash
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o /clash
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 /clash .
COPY --from=builder /clash /
EXPOSE 7890 7891
ENTRYPOINT ["/clash"]

View File

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

View File

@ -32,7 +32,7 @@ func (d *Direct) Type() C.AdapterType {
}
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 {
return
}

View File

@ -54,7 +54,7 @@ func (ss *ShadowSocks) Type() C.AdapterType {
}
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 {
return nil, fmt.Errorf("%s connect error", ss.server)
}

View File

@ -2,6 +2,7 @@ package adapters
import (
"bytes"
"crypto/tls"
"errors"
"fmt"
"io"
@ -27,14 +28,18 @@ func (ss *Socks5Adapter) Conn() net.Conn {
}
type Socks5 struct {
addr string
name string
addr string
name string
tls bool
skipCertVerify bool
}
type Socks5Option struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
TLS bool `proxy:"tls,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
}
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) {
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 {
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 {
return &Socks5{
addr: fmt.Sprintf("%s:%d", option.Server, option.Port),
name: option.Name,
addr: fmt.Sprintf("%s:%d", option.Server, option.Port),
name: option.Name,
tls: option.TLS,
skipCertVerify: option.SkipCertVerify,
}
}

View File

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

View File

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

View File

@ -31,13 +31,16 @@ type Vmess struct {
}
type VmessOption struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UUID string `proxy:"uuid"`
AlterID int `proxy:"alterId"`
Cipher string `proxy:"cipher"`
TLS bool `proxy:"tls,omitempty"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UUID string `proxy:"uuid"`
AlterID int `proxy:"alterId"`
Cipher string `proxy:"cipher"`
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 {
@ -49,22 +52,26 @@ func (ss *Vmess) Type() C.AdapterType {
}
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 {
return nil, fmt.Errorf("%s connect error", ss.server)
}
tcpKeepAlive(c)
c = ss.client.New(c, parseVmessAddr(metadata))
c, err = ss.client.New(c, parseVmessAddr(metadata))
return &VmessAdapter{conn: c}, err
}
func NewVmess(option VmessOption) (*Vmess, error) {
security := strings.ToLower(option.Cipher)
client, err := vmess.NewClient(vmess.Config{
UUID: option.UUID,
AlterID: uint16(option.AlterID),
Security: security,
TLS: option.TLS,
UUID: option.UUID,
AlterID: uint16(option.AlterID),
Security: security,
TLS: option.TLS,
Host: fmt.Sprintf("%s:%d", option.Server, option.Port),
NetWork: option.Network,
WebSocketPath: option.WSPath,
SkipCertVerify: option.SkipCertVerify,
})
if err != nil {
return nil, err

View File

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

View File

@ -5,9 +5,12 @@ import (
"fmt"
"math/rand"
"net"
"net/url"
"runtime"
"time"
"github.com/gofrs/uuid"
"github.com/gorilla/websocket"
)
// Version of vmess
@ -36,10 +39,6 @@ var CipherMapping = map[string]byte{
"chacha20-poly1305": SecurityCHACHA20POLY1305,
}
var tlsConfig = &tls.Config{
InsecureSkipVerify: true,
}
// Command types
const (
CommandTCP byte = 1
@ -62,27 +61,76 @@ type DstAddr struct {
// Client is vmess connection generator
type Client struct {
user []*ID
uuid *uuid.UUID
security Security
tls bool
user []*ID
uuid *uuid.UUID
security Security
tls bool
host string
websocket bool
websocketPath string
skipCertVerify bool
}
// Config of vmess
type Config struct {
UUID string
AlterID uint16
Security string
TLS bool
UUID string
AlterID uint16
Security string
TLS bool
Host string
NetWork string
WebSocketPath string
SkipCertVerify bool
}
// 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))
if c.tls {
conn = tls.Client(conn, tlsConfig)
if c.websocket {
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
@ -108,10 +156,18 @@ func NewClient(config Config) (*Client, error) {
default:
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{
user: newAlterIDs(newID(&uid), config.AlterID),
uuid: &uid,
security: security,
tls: config.TLS,
user: newAlterIDs(newID(&uid), config.AlterID),
uuid: &uid,
security: security,
tls: config.TLS,
host: config.Host,
websocket: config.NetWork == "ws",
websocketPath: config.WebSocketPath,
}, 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
for idx, mapping := range proxiesConfig {
proxyType, existType := mapping["type"].(string)
proxyName, existName := mapping["name"].(string)
if !existType && existName {
return fmt.Errorf("Proxy %d missing type or name", idx)
if !existType {
return fmt.Errorf("Proxy %d missing type", idx)
}
if _, exist := proxies[proxyName]; exist {
return fmt.Errorf("Proxy %s is the duplicate name", proxyName)
}
var proxy C.Proxy
var err error
switch proxyType {
@ -285,10 +281,15 @@ func (c *Config) parseProxies(cfg *RawConfig) error {
default:
return fmt.Errorf("Unsupport proxy type: %s", proxyType)
}
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

1
go.mod
View File

@ -7,6 +7,7 @@ require (
github.com/go-chi/cors v1.0.0
github.com/go-chi/render v1.0.1
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/maxminddb-golang v1.3.0 // indirect
github.com/sirupsen/logrus v1.1.0

2
go.sum
View File

@ -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/gofrs/uuid v3.1.0+incompatible h1:q2rtkjaKT4YEr6E1kamy0Ha4RtepWlQBedyHx0uzKwA=
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/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/oschwald/geoip2-golang v1.2.1 h1:3iz+jmeJc6fuCyWeKgtXSXu7+zvkxJbHFXkMT5FVebU=

View File

@ -1,8 +1,10 @@
package hub
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
@ -16,12 +18,24 @@ import (
func proxyRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getProxies)
r.Get("/{name}", getProxy)
r.Get("/{name}/delay", getProxyDelay)
r.Put("/{name}", updateProxy)
r.With(parseProxyName).Get("/{name}", getProxy)
r.With(parseProxyName).Get("/{name}/delay", getProxyDelay)
r.With(parseProxyName).Put("/{name}", updateProxy)
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 string `json:"type"`
}
@ -83,7 +97,7 @@ func getProxies(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()
proxy, exist := proxies[name]
if !exist {
@ -110,7 +124,7 @@ func updateProxy(w http.ResponseWriter, r *http.Request) {
return
}
name := chi.URLParam(r, "name")
name := r.Context().Value(contextKey("proxy name")).(string)
proxies := cfg.Proxies()
proxy, exist := proxies[name]
if !exist {
@ -162,7 +176,7 @@ func getProxyDelay(w http.ResponseWriter, r *http.Request) {
return
}
name := chi.URLParam(r, "name")
name := r.Context().Value(contextKey("proxy name")).(string)
proxies := cfg.Proxies()
proxy, exist := proxies[name]
if !exist {

View File

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

View File

@ -100,6 +100,12 @@ func authentication(next http.Handler) http.Handler {
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) {
w.WriteHeader(http.StatusOK)

View File

@ -5,12 +5,22 @@ import (
"io"
"net"
"net/http"
"sync"
"time"
"github.com/Dreamacro/clash/adapters/inbound"
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) {
conn := newTrafficTrack(proxy.Conn(), t.traffic)
req := request.R
@ -66,12 +76,16 @@ func relay(leftConn, rightConn net.Conn) {
ch := make(chan error)
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())
ch <- err
}()
io.Copy(rightConn, leftConn)
buf := bufPool.Get().([]byte)
io.CopyBuffer(rightConn, leftConn, buf)
bufPool.Put(buf[:cap(buf)])
rightConn.SetReadDeadline(time.Now())
<-ch
}