diff --git a/adapters/direct.go b/adapters/direct.go index 3de2bea4..bb380880 100644 --- a/adapters/direct.go +++ b/adapters/direct.go @@ -30,6 +30,10 @@ func (d *DirectAdapter) Conn() net.Conn { type Direct struct { } +func (d *Direct) Name() string { + return "Direct" +} + func (d *Direct) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) { c, err := net.Dial("tcp", net.JoinHostPort(addr.String(), addr.Port)) if err != nil { diff --git a/adapters/reject.go b/adapters/reject.go index b8fad1ff..17ca08ed 100644 --- a/adapters/reject.go +++ b/adapters/reject.go @@ -27,6 +27,10 @@ func (r *RejectAdapter) Conn() net.Conn { type Reject struct { } +func (r *Reject) Name() string { + return "Reject" +} + func (r *Reject) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) { return &RejectAdapter{}, nil } diff --git a/adapters/shadowsocks.go b/adapters/shadowsocks.go index 2e3dd987..bef3ae05 100644 --- a/adapters/shadowsocks.go +++ b/adapters/shadowsocks.go @@ -35,9 +35,14 @@ func (ss *ShadowsocksAdapter) Conn() net.Conn { type ShadowSocks struct { server string + name string cipher core.Cipher } +func (ss *ShadowSocks) Name() string { + return ss.name +} + func (ss *ShadowSocks) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) { c, err := net.Dial("tcp", ss.server) if err != nil { @@ -49,7 +54,7 @@ func (ss *ShadowSocks) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err erro return &ShadowsocksAdapter{conn: c}, err } -func NewShadowSocks(ssURL string) (*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) @@ -58,6 +63,7 @@ func NewShadowSocks(ssURL string) (*ShadowSocks, error) { } return &ShadowSocks{ server: server, + name: name, cipher: ciph, }, nil } diff --git a/adapters/urltest.go b/adapters/urltest.go new file mode 100644 index 00000000..63aef470 --- /dev/null +++ b/adapters/urltest.go @@ -0,0 +1,148 @@ +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 +} + +func (u *URLTest) Name() string { + return u.name +} + +func (u *URLTest) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) { + return u.fast.Generator(addr) +} + +func (u *URLTest) loop() { + tick := time.Tick(u.delay) + go u.speedTest() + for range tick { + go u.speedTest() + } +} + +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, + } + go urlTest.loop() + return urlTest, nil +} diff --git a/constant/adapters.go b/constant/adapters.go index aab2db00..101115c5 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -18,5 +18,6 @@ type ServerAdapter interface { } type Proxy interface { + Name() string Generator(addr *Addr) (ProxyAdapter, error) } diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go index 69c90162..df073e82 100644 --- a/tunnel/tunnel.go +++ b/tunnel/tunnel.go @@ -2,8 +2,10 @@ package tunnel import ( "fmt" + "strconv" "strings" "sync" + "time" "github.com/Dreamacro/clash/adapters" C "github.com/Dreamacro/clash/constant" @@ -43,6 +45,7 @@ func (t *Tunnel) UpdateConfig() (err error) { proxysConfig := cfg.Section("Proxy") rulesConfig := cfg.Section("Rule") + groupsConfig := cfg.Section("Proxy Group") // parse proxy for _, key := range proxysConfig.Keys() { @@ -58,7 +61,7 @@ func (t *Tunnel) UpdateConfig() (err error) { continue } ssURL := fmt.Sprintf("ss://%s:%s@%s:%s", proxy[3], proxy[4], proxy[1], proxy[2]) - ss, err := adapters.NewShadowSocks(ssURL) + ss, err := adapters.NewShadowSocks(key.Name(), ssURL) if err != nil { return err } @@ -91,6 +94,33 @@ func (t *Tunnel) UpdateConfig() (err error) { } } + // parse proxy groups + for _, key := range groupsConfig.Keys() { + rule := strings.Split(key.Value(), ",") + if len(rule) < 4 { + continue + } + rule = trimArr(rule) + switch rule[0] { + case "url-test": + proxyNames := rule[1 : len(rule)-2] + delay, _ := strconv.Atoi(rule[len(rule)-1]) + url := rule[len(rule)-2] + var ps []C.Proxy + for _, name := range proxyNames { + if p, ok := proxys[name]; ok { + ps = append(ps, p) + } + } + + adapter, err := adapters.NewURLTest(key.Name(), ps, url, time.Duration(delay)*time.Second) + if err != nil { + return fmt.Errorf("Config error: %s", err.Error()) + } + proxys[key.Name()] = adapter + } + } + t.configLock.Lock() defer t.configLock.Unlock()