Feature: add experimental provider

This commit is contained in:
Dreamacro
2019-12-08 12:17:24 +08:00
parent 4d7096f451
commit c427bc89ef
33 changed files with 1247 additions and 620 deletions

View File

@ -1,4 +1,4 @@
package adapters
package inbound
import (
"net"

View File

@ -1,4 +1,4 @@
package adapters
package inbound
import (
"net"

View File

@ -1,4 +1,4 @@
package adapters
package inbound
import (
"net"

View File

@ -1,4 +1,4 @@
package adapters
package inbound
import (
"net"

View File

@ -1,4 +1,4 @@
package adapters
package outbound
import (
"context"
@ -38,14 +38,16 @@ func (b *Base) SupportUDP() bool {
return b.udp
}
func (b *Base) Destroy() {}
func (b *Base) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": b.Type().String(),
})
}
func NewBase(name string, tp C.AdapterType, udp bool) *Base {
return &Base{name, tp, udp}
}
type conn struct {
net.Conn
chain C.Chain
@ -199,9 +201,3 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
func NewProxy(adapter C.ProxyAdapter) *Proxy {
return &Proxy{adapter, queue.New(10), true}
}
// ProxyGroupOption contain the common options for all kind of ProxyGroup
type ProxyGroupOption struct {
Name string `proxy:"name"`
Proxies []string `proxy:"proxies"`
}

View File

@ -1,4 +1,4 @@
package adapters
package outbound
import (
"context"

View File

@ -1,146 +0,0 @@
package adapters
import (
"context"
"encoding/json"
"errors"
"net"
"sync/atomic"
"time"
"github.com/Dreamacro/clash/common/picker"
C "github.com/Dreamacro/clash/constant"
)
type Fallback struct {
*Base
proxies []C.Proxy
rawURL string
interval time.Duration
done chan struct{}
once int32
}
type FallbackOption struct {
Name string `proxy:"name"`
Proxies []string `proxy:"proxies"`
URL string `proxy:"url"`
Interval int `proxy:"interval"`
}
func (f *Fallback) Now() string {
proxy := f.findAliveProxy()
return proxy.Name()
}
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
proxy := f.findAliveProxy()
c, err := proxy.DialContext(ctx, metadata)
if err == nil {
c.AppendToChains(f)
}
return c, err
}
func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
proxy := f.findAliveProxy()
pc, addr, err := proxy.DialUDP(metadata)
if err == nil {
pc.AppendToChains(f)
}
return pc, addr, err
}
func (f *Fallback) SupportUDP() bool {
proxy := f.findAliveProxy()
return proxy.SupportUDP()
}
func (f *Fallback) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range f.proxies {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
"type": f.Type().String(),
"now": f.Now(),
"all": all,
})
}
func (f *Fallback) Destroy() {
f.done <- struct{}{}
}
func (f *Fallback) loop() {
tick := time.NewTicker(f.interval)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go f.validTest(ctx)
Loop:
for {
select {
case <-tick.C:
go f.validTest(ctx)
case <-f.done:
break Loop
}
}
}
func (f *Fallback) findAliveProxy() C.Proxy {
for _, proxy := range f.proxies {
if proxy.Alive() {
return proxy
}
}
return f.proxies[0]
}
func (f *Fallback) validTest(ctx context.Context) {
if !atomic.CompareAndSwapInt32(&f.once, 0, 1) {
return
}
defer atomic.StoreInt32(&f.once, 0)
ctx, cancel := context.WithTimeout(ctx, defaultURLTestTimeout)
defer cancel()
picker := picker.WithoutAutoCancel(ctx)
for _, p := range f.proxies {
proxy := p
picker.Go(func() (interface{}, error) {
return proxy.URLTest(ctx, f.rawURL)
})
}
picker.Wait()
}
func NewFallback(option FallbackOption, proxies []C.Proxy) (*Fallback, error) {
_, err := urlToMetadata(option.URL)
if err != nil {
return nil, err
}
if len(proxies) < 1 {
return nil, errors.New("The number of proxies cannot be 0")
}
interval := time.Duration(option.Interval) * time.Second
Fallback := &Fallback{
Base: &Base{
name: option.Name,
tp: C.Fallback,
},
proxies: proxies,
rawURL: option.URL,
interval: interval,
done: make(chan struct{}),
once: 0,
}
go Fallback.loop()
return Fallback, nil
}

View File

@ -1,4 +1,4 @@
package adapters
package outbound
import (
"bufio"

View File

@ -0,0 +1,64 @@
package outbound
import (
"fmt"
"github.com/Dreamacro/clash/common/structure"
C "github.com/Dreamacro/clash/constant"
)
func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true})
proxyType, existType := mapping["type"].(string)
if !existType {
return nil, fmt.Errorf("Missing type")
}
var proxy C.ProxyAdapter
err := fmt.Errorf("Cannot parse")
switch proxyType {
case "ss":
ssOption := &ShadowSocksOption{}
err = decoder.Decode(mapping, ssOption)
if err != nil {
break
}
proxy, err = NewShadowSocks(*ssOption)
case "socks5":
socksOption := &Socks5Option{}
err = decoder.Decode(mapping, socksOption)
if err != nil {
break
}
proxy = NewSocks5(*socksOption)
case "http":
httpOption := &HttpOption{}
err = decoder.Decode(mapping, httpOption)
if err != nil {
break
}
proxy = NewHttp(*httpOption)
case "vmess":
vmessOption := &VmessOption{}
err = decoder.Decode(mapping, vmessOption)
if err != nil {
break
}
proxy, err = NewVmess(*vmessOption)
case "snell":
snellOption := &SnellOption{}
err = decoder.Decode(mapping, snellOption)
if err != nil {
break
}
proxy, err = NewSnell(*snellOption)
default:
return nil, fmt.Errorf("Unsupport proxy type: %s", proxyType)
}
if err != nil {
return nil, err
}
return NewProxy(proxy), nil
}

View File

@ -1,4 +1,4 @@
package adapters
package outbound
import (
"context"

View File

@ -1,4 +1,4 @@
package adapters
package outbound
import (
"context"

View File

@ -1,4 +1,4 @@
package adapters
package outbound
import (
"context"

View File

@ -1,4 +1,4 @@
package adapters
package outbound
import (
"context"

View File

@ -1,162 +0,0 @@
package adapters
import (
"context"
"encoding/json"
"errors"
"net"
"sync/atomic"
"time"
"github.com/Dreamacro/clash/common/picker"
C "github.com/Dreamacro/clash/constant"
)
type URLTest struct {
*Base
proxies []C.Proxy
rawURL string
fast C.Proxy
interval time.Duration
done chan struct{}
once int32
}
type URLTestOption struct {
Name string `proxy:"name"`
Proxies []string `proxy:"proxies"`
URL string `proxy:"url"`
Interval int `proxy:"interval"`
}
func (u *URLTest) Now() string {
return u.fast.Name()
}
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
for i := 0; i < 3; i++ {
c, err = u.fast.DialContext(ctx, metadata)
if err == nil {
c.AppendToChains(u)
return
}
u.fallback()
}
return
}
func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
pc, addr, err := u.fast.DialUDP(metadata)
if err == nil {
pc.AppendToChains(u)
}
return pc, addr, err
}
func (u *URLTest) SupportUDP() bool {
return u.fast.SupportUDP()
}
func (u *URLTest) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range u.proxies {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
"type": u.Type().String(),
"now": u.Now(),
"all": all,
})
}
func (u *URLTest) Destroy() {
u.done <- struct{}{}
}
func (u *URLTest) loop() {
tick := time.NewTicker(u.interval)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go u.speedTest(ctx)
Loop:
for {
select {
case <-tick.C:
go u.speedTest(ctx)
case <-u.done:
break Loop
}
}
}
func (u *URLTest) fallback() {
fast := u.proxies[0]
min := fast.LastDelay()
for _, proxy := range u.proxies[1:] {
if !proxy.Alive() {
continue
}
delay := proxy.LastDelay()
if delay < min {
fast = proxy
min = delay
}
}
u.fast = fast
}
func (u *URLTest) speedTest(ctx context.Context) {
if !atomic.CompareAndSwapInt32(&u.once, 0, 1) {
return
}
defer atomic.StoreInt32(&u.once, 0)
ctx, cancel := context.WithTimeout(ctx, defaultURLTestTimeout)
defer cancel()
picker := picker.WithoutAutoCancel(ctx)
for _, p := range u.proxies {
proxy := p
picker.Go(func() (interface{}, error) {
_, err := proxy.URLTest(ctx, u.rawURL)
if err != nil {
return nil, err
}
return proxy, nil
})
}
fast := picker.WaitWithoutCancel()
if fast != nil {
u.fast = fast.(C.Proxy)
}
picker.Wait()
}
func NewURLTest(option URLTestOption, proxies []C.Proxy) (*URLTest, error) {
_, err := urlToMetadata(option.URL)
if err != nil {
return nil, err
}
if len(proxies) < 1 {
return nil, errors.New("The number of proxies cannot be 0")
}
interval := time.Duration(option.Interval) * time.Second
urlTest := &URLTest{
Base: &Base{
name: option.Name,
tp: C.URLTest,
},
proxies: proxies[:],
rawURL: option.URL,
fast: proxies[0],
interval: interval,
done: make(chan struct{}),
once: 0,
}
go urlTest.loop()
return urlTest, nil
}

View File

@ -1,4 +1,4 @@
package adapters
package outbound
import (
"bytes"

View File

@ -1,4 +1,4 @@
package adapters
package outbound
import (
"context"

View File

@ -0,0 +1,84 @@
package outboundgroup
import (
"context"
"encoding/json"
"net"
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/provider"
C "github.com/Dreamacro/clash/constant"
)
type Fallback struct {
*outbound.Base
providers []provider.ProxyProvider
}
func (f *Fallback) Now() string {
proxy := f.findAliveProxy()
return proxy.Name()
}
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
proxy := f.findAliveProxy()
c, err := proxy.DialContext(ctx, metadata)
if err == nil {
c.AppendToChains(f)
}
return c, err
}
func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
proxy := f.findAliveProxy()
pc, addr, err := proxy.DialUDP(metadata)
if err == nil {
pc.AppendToChains(f)
}
return pc, addr, err
}
func (f *Fallback) SupportUDP() bool {
proxy := f.findAliveProxy()
return proxy.SupportUDP()
}
func (f *Fallback) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range f.proxies() {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
"type": f.Type().String(),
"now": f.Now(),
"all": all,
})
}
func (f *Fallback) proxies() []C.Proxy {
proxies := []C.Proxy{}
for _, provider := range f.providers {
proxies = append(proxies, provider.Proxies()...)
}
return proxies
}
func (f *Fallback) findAliveProxy() C.Proxy {
for _, provider := range f.providers {
proxies := provider.Proxies()
for _, proxy := range proxies {
if proxy.Alive() {
return proxy
}
}
}
return f.providers[0].Proxies()[0]
}
func NewFallback(name string, providers []provider.ProxyProvider) *Fallback {
return &Fallback{
Base: outbound.NewBase(name, C.Fallback, false),
providers: providers,
}
}

View File

@ -1,13 +1,12 @@
package adapters
package outboundgroup
import (
"context"
"encoding/json"
"errors"
"net"
"sync"
"time"
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/common/murmur3"
C "github.com/Dreamacro/clash/constant"
@ -15,12 +14,9 @@ import (
)
type LoadBalance struct {
*Base
proxies []C.Proxy
maxRetry int
rawURL string
interval time.Duration
done chan struct{}
*outbound.Base
maxRetry int
providers []provider.ProxyProvider
}
func getKey(metadata *C.Metadata) string {
@ -62,16 +58,17 @@ func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c
}()
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
buckets := int32(len(lb.proxies))
proxies := lb.proxies()
buckets := int32(len(proxies))
for i := 0; i < lb.maxRetry; i, key = i+1, key+1 {
idx := jumpHash(key, buckets)
proxy := lb.proxies[idx]
proxy := proxies[idx]
if proxy.Alive() {
c, err = proxy.DialContext(ctx, metadata)
return
}
}
c, err = lb.proxies[0].DialContext(ctx, metadata)
c, err = proxies[0].DialContext(ctx, metadata)
return
}
@ -83,57 +80,34 @@ func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, addr net.
}()
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
buckets := int32(len(lb.proxies))
proxies := lb.proxies()
buckets := int32(len(proxies))
for i := 0; i < lb.maxRetry; i, key = i+1, key+1 {
idx := jumpHash(key, buckets)
proxy := lb.proxies[idx]
proxy := proxies[idx]
if proxy.Alive() {
return proxy.DialUDP(metadata)
}
}
return lb.proxies[0].DialUDP(metadata)
return proxies[0].DialUDP(metadata)
}
func (lb *LoadBalance) SupportUDP() bool {
return true
}
func (lb *LoadBalance) Destroy() {
lb.done <- struct{}{}
}
func (lb *LoadBalance) validTest() {
wg := sync.WaitGroup{}
wg.Add(len(lb.proxies))
for _, p := range lb.proxies {
go func(p C.Proxy) {
p.URLTest(context.Background(), lb.rawURL)
wg.Done()
}(p)
}
wg.Wait()
}
func (lb *LoadBalance) loop() {
tick := time.NewTicker(lb.interval)
go lb.validTest()
Loop:
for {
select {
case <-tick.C:
go lb.validTest()
case <-lb.done:
break Loop
}
func (lb *LoadBalance) proxies() []C.Proxy {
proxies := []C.Proxy{}
for _, provider := range lb.providers {
proxies = append(proxies, provider.Proxies()...)
}
return proxies
}
func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range lb.proxies {
for _, proxy := range lb.proxies() {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
@ -142,31 +116,10 @@ func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
})
}
type LoadBalanceOption struct {
Name string `proxy:"name"`
Proxies []string `proxy:"proxies"`
URL string `proxy:"url"`
Interval int `proxy:"interval"`
}
func NewLoadBalance(option LoadBalanceOption, proxies []C.Proxy) (*LoadBalance, error) {
if len(proxies) == 0 {
return nil, errors.New("Provide at least one proxy")
func NewLoadBalance(name string, providers []provider.ProxyProvider) *LoadBalance {
return &LoadBalance{
Base: outbound.NewBase(name, C.LoadBalance, false),
maxRetry: 3,
providers: providers,
}
interval := time.Duration(option.Interval) * time.Second
lb := &LoadBalance{
Base: &Base{
name: option.Name,
tp: C.LoadBalance,
},
proxies: proxies,
maxRetry: 3,
rawURL: option.URL,
interval: interval,
done: make(chan struct{}),
}
go lb.loop()
return lb, nil
}

View File

@ -0,0 +1,139 @@
package outboundgroup
import (
"errors"
"fmt"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/common/structure"
C "github.com/Dreamacro/clash/constant"
)
var (
errFormat = errors.New("format error")
errType = errors.New("unsupport type")
errMissUse = errors.New("`use` field should not be empty")
errMissHealthCheck = errors.New("`url` or `interval` missing")
errDuplicateProvider = errors.New("`duplicate provider name")
)
type GroupCommonOption struct {
Name string `group:"name"`
Type string `group:"type"`
Proxies []string `group:"proxies,omitempty"`
Use []string `group:"use,omitempty"`
URL string `group:"url,omitempty"`
Interval int `group:"interval,omitempty"`
}
func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, providersMap map[string]provider.ProxyProvider) (C.ProxyAdapter, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true})
groupOption := &GroupCommonOption{}
if err := decoder.Decode(config, groupOption); err != nil {
return nil, errFormat
}
if groupOption.Type == "" || groupOption.Name == "" {
return nil, errFormat
}
groupName := groupOption.Name
providers := []provider.ProxyProvider{}
if len(groupOption.Proxies) != 0 {
ps, err := getProxies(proxyMap, groupOption.Proxies)
if err != nil {
return nil, err
}
// if Use not empty, drop health check options
if len(groupOption.Use) != 0 {
pd, err := provider.NewCompatibleProvier(groupName, ps, nil)
if err != nil {
return nil, err
}
providers = append(providers, pd)
} else {
// select don't need health check
if groupOption.Type == "select" {
pd, err := provider.NewCompatibleProvier(groupName, ps, nil)
if err != nil {
return nil, err
}
providers = append(providers, pd)
providersMap[groupName] = pd
} else {
if groupOption.URL == "" || groupOption.Interval == 0 {
return nil, errMissHealthCheck
}
healthOption := &provider.HealthCheckOption{
URL: groupOption.URL,
Interval: uint(groupOption.Interval),
}
pd, err := provider.NewCompatibleProvier(groupName, ps, healthOption)
if err != nil {
return nil, err
}
providers = append(providers, pd)
providersMap[groupName] = pd
}
}
}
if len(groupOption.Use) != 0 {
list, err := getProviders(providersMap, groupOption.Use)
if err != nil {
return nil, err
}
providers = append(providers, list...)
}
var group C.ProxyAdapter
switch groupOption.Type {
case "url-test":
group = NewURLTest(groupName, providers)
case "select":
group = NewSelector(groupName, providers)
case "fallback":
group = NewFallback(groupName, providers)
case "load-balance":
group = NewLoadBalance(groupName, providers)
default:
return nil, fmt.Errorf("%w: %s", errType, groupOption.Type)
}
return group, nil
}
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 getProviders(mapping map[string]provider.ProxyProvider, list []string) ([]provider.ProxyProvider, error) {
var ps []provider.ProxyProvider
for _, name := range list {
p, ok := mapping[name]
if !ok {
return nil, fmt.Errorf("'%s' not found", name)
}
if p.VehicleType() == provider.Compatible {
return nil, fmt.Errorf("proxy group %s can't contains in `use`", name)
}
ps = append(ps, p)
}
return ps, nil
}

View File

@ -1,4 +1,4 @@
package adapters
package outboundgroup
import (
"context"
@ -6,19 +6,15 @@ import (
"errors"
"net"
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/provider"
C "github.com/Dreamacro/clash/constant"
)
type Selector struct {
*Base
*outbound.Base
selected C.Proxy
proxies map[string]C.Proxy
proxyList []string
}
type SelectorOption struct {
Name string `proxy:"name"`
Proxies []string `proxy:"proxies"`
providers []provider.ProxyProvider
}
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
@ -42,10 +38,15 @@ func (s *Selector) SupportUDP() bool {
}
func (s *Selector) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range s.proxies() {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
"type": s.Type().String(),
"now": s.Now(),
"all": s.proxyList,
"all": all,
})
}
@ -54,34 +55,29 @@ func (s *Selector) Now() string {
}
func (s *Selector) Set(name string) error {
proxy, exist := s.proxies[name]
if !exist {
return errors.New("Proxy does not exist")
for _, proxy := range s.proxies() {
if proxy.Name() == name {
s.selected = proxy
return nil
}
}
s.selected = proxy
return nil
return errors.New("Proxy does not exist")
}
func NewSelector(name string, proxies []C.Proxy) (*Selector, error) {
if len(proxies) == 0 {
return nil, errors.New("Provide at least one proxy")
func (s *Selector) proxies() []C.Proxy {
proxies := []C.Proxy{}
for _, provider := range s.providers {
proxies = append(proxies, provider.Proxies()...)
}
return proxies
}
func NewSelector(name string, providers []provider.ProxyProvider) *Selector {
selected := providers[0].Proxies()[0]
return &Selector{
Base: outbound.NewBase(name, C.Selector, false),
providers: providers,
selected: selected,
}
mapping := make(map[string]C.Proxy)
proxyList := make([]string, len(proxies))
for idx, proxy := range proxies {
mapping[proxy.Name()] = proxy
proxyList[idx] = proxy.Name()
}
s := &Selector{
Base: &Base{
name: name,
tp: C.Selector,
},
proxies: mapping,
selected: proxies[0],
proxyList: proxyList,
}
return s, nil
}

View File

@ -0,0 +1,93 @@
package outboundgroup
import (
"context"
"encoding/json"
"net"
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/provider"
C "github.com/Dreamacro/clash/constant"
)
type URLTest struct {
*outbound.Base
fast C.Proxy
providers []provider.ProxyProvider
}
func (u *URLTest) Now() string {
return u.fast.Name()
}
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
for i := 0; i < 3; i++ {
c, err = u.fast.DialContext(ctx, metadata)
if err == nil {
c.AppendToChains(u)
return
}
u.fallback()
}
return
}
func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
pc, addr, err := u.fast.DialUDP(metadata)
if err == nil {
pc.AppendToChains(u)
}
return pc, addr, err
}
func (u *URLTest) proxies() []C.Proxy {
proxies := []C.Proxy{}
for _, provider := range u.providers {
proxies = append(proxies, provider.Proxies()...)
}
return proxies
}
func (u *URLTest) SupportUDP() bool {
return u.fast.SupportUDP()
}
func (u *URLTest) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range u.proxies() {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
"type": u.Type().String(),
"now": u.Now(),
"all": all,
})
}
func (u *URLTest) fallback() {
proxies := u.proxies()
fast := proxies[0]
min := fast.LastDelay()
for _, proxy := range proxies[1:] {
if !proxy.Alive() {
continue
}
delay := proxy.LastDelay()
if delay < min {
fast = proxy
min = delay
}
}
u.fast = fast
}
func NewURLTest(name string, providers []provider.ProxyProvider) *URLTest {
fast := providers[0].Proxies()[0]
return &URLTest{
Base: outbound.NewBase(name, C.URLTest, false),
fast: fast,
providers: providers,
}
}

View File

@ -0,0 +1,53 @@
package provider
import (
"context"
"time"
C "github.com/Dreamacro/clash/constant"
)
const (
defaultURLTestTimeout = time.Second * 5
)
type HealthCheckOption struct {
URL string
Interval uint
}
type healthCheck struct {
url string
proxies []C.Proxy
ticker *time.Ticker
}
func (hc *healthCheck) process() {
go hc.check()
for range hc.ticker.C {
hc.check()
}
}
func (hc *healthCheck) check() {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
for _, proxy := range hc.proxies {
go proxy.URLTest(ctx, hc.url)
}
<-ctx.Done()
cancel()
}
func (hc *healthCheck) close() {
hc.ticker.Stop()
}
func newHealthCheck(proxies []C.Proxy, url string, interval uint) *healthCheck {
ticker := time.NewTicker(time.Duration(interval) * time.Second)
return &healthCheck{
proxies: proxies,
url: url,
ticker: ticker,
}
}

View File

@ -0,0 +1,60 @@
package provider
import (
"errors"
"fmt"
"time"
"github.com/Dreamacro/clash/common/structure"
C "github.com/Dreamacro/clash/constant"
)
var (
errVehicleType = errors.New("unsupport vehicle type")
)
type healthCheckSchema struct {
Enable bool `provider:"enable"`
URL string `provider:"url"`
Interval int `provider:"interval"`
}
type proxyProviderSchema struct {
Type string `provider:"type"`
Path string `provider:"path"`
URL string `provider:"url,omitempty"`
Interval int `provider:"interval,omitempty"`
HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
}
func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvider, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
schema := &proxyProviderSchema{}
if err := decoder.Decode(mapping, schema); err != nil {
return nil, err
}
var healthCheckOption *HealthCheckOption
if schema.HealthCheck.Enable {
healthCheckOption = &HealthCheckOption{
URL: schema.HealthCheck.URL,
Interval: uint(schema.HealthCheck.Interval),
}
}
path := C.Path.Reslove(schema.Path)
var vehicle Vehicle
switch schema.Type {
case "file":
vehicle = NewFileVehicle(path)
case "http":
vehicle = NewHTTPVehicle(schema.URL, path)
default:
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
}
interval := time.Duration(uint(schema.Interval)) * time.Second
return NewProxySetProvider(name, interval, vehicle, healthCheckOption), nil
}

View File

@ -0,0 +1,293 @@
package provider
import (
"bytes"
"crypto/md5"
"errors"
"fmt"
"io/ioutil"
"net/url"
"os"
"sync"
"time"
"github.com/Dreamacro/clash/adapters/outbound"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"gopkg.in/yaml.v2"
)
const (
ReservedName = "default"
fileMode = 0666
)
// Provider Type
const (
Proxy ProviderType = iota
Rule
)
// ProviderType defined
type ProviderType int
func (pt ProviderType) String() string {
switch pt {
case Proxy:
return "Proxy"
case Rule:
return "Rule"
default:
return "Unknown"
}
}
// Provider interface
type Provider interface {
Name() string
VehicleType() VehicleType
Type() ProviderType
Initial() error
Reload() error
Destroy() error
}
// ProxyProvider interface
type ProxyProvider interface {
Provider
Proxies() []C.Proxy
}
type ProxySchema struct {
Proxies []map[string]interface{} `yaml:"proxies"`
}
type ProxySetProvider struct {
name string
vehicle Vehicle
hash [16]byte
proxies []C.Proxy
healthCheck *healthCheck
healthCheckOption *HealthCheckOption
ticker *time.Ticker
// mux for avoiding creating new goroutines when pulling
mux sync.Mutex
}
func (pp *ProxySetProvider) Name() string {
return pp.name
}
func (pp *ProxySetProvider) Reload() error {
return nil
}
func (pp *ProxySetProvider) Destroy() error {
pp.mux.Lock()
defer pp.mux.Unlock()
if pp.healthCheck != nil {
pp.healthCheck.close()
pp.healthCheck = nil
}
if pp.ticker != nil {
pp.ticker.Stop()
}
return nil
}
func (pp *ProxySetProvider) Initial() error {
var buf []byte
var err error
if _, err := os.Stat(pp.vehicle.Path()); err == nil {
buf, err = ioutil.ReadFile(pp.vehicle.Path())
} else {
buf, err = pp.vehicle.Read()
}
if err != nil {
return err
}
proxies, err := pp.parse(buf)
if err != nil {
return err
}
if err := ioutil.WriteFile(pp.vehicle.Path(), buf, fileMode); err != nil {
return err
}
pp.hash = md5.Sum(buf)
pp.setProxies(proxies)
// pull proxies automatically
if pp.ticker != nil {
go pp.pullLoop()
}
return nil
}
func (pp *ProxySetProvider) VehicleType() VehicleType {
return pp.vehicle.Type()
}
func (pp *ProxySetProvider) Type() ProviderType {
return Proxy
}
func (pp *ProxySetProvider) Proxies() []C.Proxy {
return pp.proxies
}
func (pp *ProxySetProvider) pullLoop() {
for range pp.ticker.C {
if err := pp.pull(); err != nil {
log.Warnln("[Provider] %s pull error: %s", pp.Name(), err.Error())
}
}
}
func (pp *ProxySetProvider) pull() error {
buf, err := pp.vehicle.Read()
if err != nil {
return err
}
hash := md5.Sum(buf)
if bytes.Equal(pp.hash[:], hash[:]) {
log.Debugln("[Provider] %s's proxies doesn't change", pp.Name())
return nil
}
proxies, err := pp.parse(buf)
if err != nil {
return err
}
log.Infoln("[Provider] %s's proxies update", pp.Name())
if err := ioutil.WriteFile(pp.vehicle.Path(), buf, fileMode); err != nil {
return err
}
pp.hash = hash
pp.setProxies(proxies)
return nil
}
func (pp *ProxySetProvider) parse(buf []byte) ([]C.Proxy, error) {
schema := &ProxySchema{}
if err := yaml.Unmarshal(buf, schema); err != nil {
return nil, err
}
if schema.Proxies == nil {
return nil, errors.New("File must have a `proxies` field")
}
proxies := []C.Proxy{}
for idx, mapping := range schema.Proxies {
proxy, err := outbound.ParseProxy(mapping)
if err != nil {
return nil, fmt.Errorf("Proxy %d error: %w", idx, err)
}
proxies = append(proxies, proxy)
}
return proxies, nil
}
func (pp *ProxySetProvider) setProxies(proxies []C.Proxy) {
pp.proxies = proxies
if pp.healthCheckOption != nil {
pp.mux.Lock()
if pp.healthCheck != nil {
pp.healthCheck.close()
pp.healthCheck = newHealthCheck(proxies, pp.healthCheckOption.URL, pp.healthCheckOption.Interval)
go pp.healthCheck.process()
}
pp.mux.Unlock()
}
}
func NewProxySetProvider(name string, interval time.Duration, vehicle Vehicle, option *HealthCheckOption) *ProxySetProvider {
var ticker *time.Ticker
if interval != 0 {
ticker = time.NewTicker(interval)
}
return &ProxySetProvider{
name: name,
vehicle: vehicle,
proxies: []C.Proxy{},
healthCheckOption: option,
ticker: ticker,
}
}
type CompatibleProvier struct {
name string
healthCheck *healthCheck
proxies []C.Proxy
}
func (cp *CompatibleProvier) Name() string {
return cp.name
}
func (cp *CompatibleProvier) Reload() error {
return nil
}
func (cp *CompatibleProvier) Destroy() error {
if cp.healthCheck != nil {
cp.healthCheck.close()
}
return nil
}
func (cp *CompatibleProvier) Initial() error {
if cp.healthCheck != nil {
go cp.healthCheck.process()
}
return nil
}
func (cp *CompatibleProvier) VehicleType() VehicleType {
return Compatible
}
func (cp *CompatibleProvier) Type() ProviderType {
return Proxy
}
func (cp *CompatibleProvier) Proxies() []C.Proxy {
return cp.proxies
}
func NewCompatibleProvier(name string, proxies []C.Proxy, option *HealthCheckOption) (*CompatibleProvier, error) {
if len(proxies) == 0 {
return nil, errors.New("Provider need one proxy at least")
}
var hc *healthCheck
if option != nil {
if _, err := url.Parse(option.URL); err != nil {
return nil, fmt.Errorf("URL format error: %w", err)
}
hc = newHealthCheck(proxies, option.URL, option.Interval)
}
return &CompatibleProvier{
name: name,
proxies: proxies,
healthCheck: hc,
}, nil
}

View File

@ -0,0 +1,109 @@
package provider
import (
"context"
"io/ioutil"
"net/http"
"time"
)
// Vehicle Type
const (
File VehicleType = iota
HTTP
Compatible
)
// VehicleType defined
type VehicleType int
func (v VehicleType) String() string {
switch v {
case File:
return "File"
case HTTP:
return "HTTP"
case Compatible:
return "Compatible"
default:
return "Unknown"
}
}
type Vehicle interface {
Read() ([]byte, error)
Path() string
Type() VehicleType
}
type FileVehicle struct {
path string
}
func (f *FileVehicle) Type() VehicleType {
return File
}
func (f *FileVehicle) Path() string {
return f.path
}
func (f *FileVehicle) Read() ([]byte, error) {
return ioutil.ReadFile(f.path)
}
func NewFileVehicle(path string) *FileVehicle {
return &FileVehicle{path: path}
}
type HTTPVehicle struct {
url string
path string
}
func (h *HTTPVehicle) Type() VehicleType {
return HTTP
}
func (h *HTTPVehicle) Path() string {
return h.path
}
func (h *HTTPVehicle) Read() ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()
req, err := http.NewRequest(http.MethodGet, h.url, nil)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
transport := &http.Transport{
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
client := http.Client{Transport: transport}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
buf, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if err := ioutil.WriteFile(h.path, buf, fileMode); err != nil {
return nil, err
}
return buf, nil
}
func NewHTTPVehicle(url string, path string) *HTTPVehicle {
return &HTTPVehicle{url, path}
}