diff --git a/adapters/inbound/http.go b/adapters/inbound/http.go index 7aeb4c65..867dab22 100644 --- a/adapters/inbound/http.go +++ b/adapters/inbound/http.go @@ -1,4 +1,4 @@ -package adapters +package inbound import ( "net" diff --git a/adapters/inbound/https.go b/adapters/inbound/https.go index 124b80c2..7a219980 100644 --- a/adapters/inbound/https.go +++ b/adapters/inbound/https.go @@ -1,4 +1,4 @@ -package adapters +package inbound import ( "net" diff --git a/adapters/inbound/socket.go b/adapters/inbound/socket.go index 017f7424..43fbd0f5 100644 --- a/adapters/inbound/socket.go +++ b/adapters/inbound/socket.go @@ -1,4 +1,4 @@ -package adapters +package inbound import ( "net" diff --git a/adapters/inbound/util.go b/adapters/inbound/util.go index 9a16a6cf..16b440e9 100644 --- a/adapters/inbound/util.go +++ b/adapters/inbound/util.go @@ -1,4 +1,4 @@ -package adapters +package inbound import ( "net" diff --git a/adapters/outbound/base.go b/adapters/outbound/base.go index 505489b4..0b9c90df 100644 --- a/adapters/outbound/base.go +++ b/adapters/outbound/base.go @@ -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"` -} diff --git a/adapters/outbound/direct.go b/adapters/outbound/direct.go index 22a4171c..061d0361 100644 --- a/adapters/outbound/direct.go +++ b/adapters/outbound/direct.go @@ -1,4 +1,4 @@ -package adapters +package outbound import ( "context" diff --git a/adapters/outbound/fallback.go b/adapters/outbound/fallback.go deleted file mode 100644 index 46247c86..00000000 --- a/adapters/outbound/fallback.go +++ /dev/null @@ -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 -} diff --git a/adapters/outbound/http.go b/adapters/outbound/http.go index 03bd99cb..5223f27d 100644 --- a/adapters/outbound/http.go +++ b/adapters/outbound/http.go @@ -1,4 +1,4 @@ -package adapters +package outbound import ( "bufio" diff --git a/adapters/outbound/parser.go b/adapters/outbound/parser.go new file mode 100644 index 00000000..6aeec866 --- /dev/null +++ b/adapters/outbound/parser.go @@ -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 +} diff --git a/adapters/outbound/reject.go b/adapters/outbound/reject.go index 65ab1192..b0535082 100644 --- a/adapters/outbound/reject.go +++ b/adapters/outbound/reject.go @@ -1,4 +1,4 @@ -package adapters +package outbound import ( "context" diff --git a/adapters/outbound/shadowsocks.go b/adapters/outbound/shadowsocks.go index 091f6f37..80aa6659 100644 --- a/adapters/outbound/shadowsocks.go +++ b/adapters/outbound/shadowsocks.go @@ -1,4 +1,4 @@ -package adapters +package outbound import ( "context" diff --git a/adapters/outbound/snell.go b/adapters/outbound/snell.go index ecc0a685..4626bbef 100644 --- a/adapters/outbound/snell.go +++ b/adapters/outbound/snell.go @@ -1,4 +1,4 @@ -package adapters +package outbound import ( "context" diff --git a/adapters/outbound/socks5.go b/adapters/outbound/socks5.go index 6551df17..7632be30 100644 --- a/adapters/outbound/socks5.go +++ b/adapters/outbound/socks5.go @@ -1,4 +1,4 @@ -package adapters +package outbound import ( "context" diff --git a/adapters/outbound/urltest.go b/adapters/outbound/urltest.go deleted file mode 100644 index 33f80717..00000000 --- a/adapters/outbound/urltest.go +++ /dev/null @@ -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 -} diff --git a/adapters/outbound/util.go b/adapters/outbound/util.go index 3af093c4..b3deb0a7 100644 --- a/adapters/outbound/util.go +++ b/adapters/outbound/util.go @@ -1,4 +1,4 @@ -package adapters +package outbound import ( "bytes" diff --git a/adapters/outbound/vmess.go b/adapters/outbound/vmess.go index d61172e5..c653b106 100644 --- a/adapters/outbound/vmess.go +++ b/adapters/outbound/vmess.go @@ -1,4 +1,4 @@ -package adapters +package outbound import ( "context" diff --git a/adapters/outboundgroup/fallback.go b/adapters/outboundgroup/fallback.go new file mode 100644 index 00000000..7c0525b8 --- /dev/null +++ b/adapters/outboundgroup/fallback.go @@ -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, + } +} diff --git a/adapters/outbound/loadbalance.go b/adapters/outboundgroup/loadbalance.go similarity index 54% rename from adapters/outbound/loadbalance.go rename to adapters/outboundgroup/loadbalance.go index d27beece..74a154ce 100644 --- a/adapters/outbound/loadbalance.go +++ b/adapters/outboundgroup/loadbalance.go @@ -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 } diff --git a/adapters/outboundgroup/parser.go b/adapters/outboundgroup/parser.go new file mode 100644 index 00000000..972305c2 --- /dev/null +++ b/adapters/outboundgroup/parser.go @@ -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 +} diff --git a/adapters/outbound/selector.go b/adapters/outboundgroup/selector.go similarity index 51% rename from adapters/outbound/selector.go rename to adapters/outboundgroup/selector.go index b7ed661c..fc53be71 100644 --- a/adapters/outbound/selector.go +++ b/adapters/outboundgroup/selector.go @@ -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 } diff --git a/adapters/outboundgroup/urltest.go b/adapters/outboundgroup/urltest.go new file mode 100644 index 00000000..2e57e0f2 --- /dev/null +++ b/adapters/outboundgroup/urltest.go @@ -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, + } +} diff --git a/adapters/provider/healthcheck.go b/adapters/provider/healthcheck.go new file mode 100644 index 00000000..00520666 --- /dev/null +++ b/adapters/provider/healthcheck.go @@ -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, + } +} diff --git a/adapters/provider/parser.go b/adapters/provider/parser.go new file mode 100644 index 00000000..aea22f8d --- /dev/null +++ b/adapters/provider/parser.go @@ -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 +} diff --git a/adapters/provider/provider.go b/adapters/provider/provider.go new file mode 100644 index 00000000..1149273d --- /dev/null +++ b/adapters/provider/provider.go @@ -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 +} diff --git a/adapters/provider/vehicle.go b/adapters/provider/vehicle.go new file mode 100644 index 00000000..83143194 --- /dev/null +++ b/adapters/provider/vehicle.go @@ -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} +} diff --git a/common/structure/structure.go b/common/structure/structure.go index 8e595a17..0a047a41 100644 --- a/common/structure/structure.go +++ b/common/structure/structure.go @@ -76,6 +76,8 @@ func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error return d.decodeMap(name, data, val) case reflect.Interface: return d.setInterface(name, data, val) + case reflect.Struct: + return d.decodeStruct(name, data, val) default: return fmt.Errorf("type %s not support", val.Kind().String()) } @@ -232,6 +234,159 @@ func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val refle return nil } +func (d *Decoder) decodeStruct(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.Indirect(reflect.ValueOf(data)) + + // If the type of the value to write to and the data match directly, + // then we just set it directly instead of recursing into the structure. + if dataVal.Type() == val.Type() { + val.Set(dataVal) + return nil + } + + dataValKind := dataVal.Kind() + switch dataValKind { + case reflect.Map: + return d.decodeStructFromMap(name, dataVal, val) + default: + return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind()) + } +} + +func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) error { + dataValType := dataVal.Type() + if kind := dataValType.Key().Kind(); kind != reflect.String && kind != reflect.Interface { + return fmt.Errorf( + "'%s' needs a map with string keys, has '%s' keys", + name, dataValType.Key().Kind()) + } + + dataValKeys := make(map[reflect.Value]struct{}) + dataValKeysUnused := make(map[interface{}]struct{}) + for _, dataValKey := range dataVal.MapKeys() { + dataValKeys[dataValKey] = struct{}{} + dataValKeysUnused[dataValKey.Interface()] = struct{}{} + } + + errors := make([]string, 0) + + // This slice will keep track of all the structs we'll be decoding. + // There can be more than one struct if there are embedded structs + // that are squashed. + structs := make([]reflect.Value, 1, 5) + structs[0] = val + + // Compile the list of all the fields that we're going to be decoding + // from all the structs. + type field struct { + field reflect.StructField + val reflect.Value + } + fields := []field{} + for len(structs) > 0 { + structVal := structs[0] + structs = structs[1:] + + structType := structVal.Type() + + for i := 0; i < structType.NumField(); i++ { + fieldType := structType.Field(i) + fieldKind := fieldType.Type.Kind() + + // If "squash" is specified in the tag, we squash the field down. + squash := false + tagParts := strings.Split(fieldType.Tag.Get(d.option.TagName), ",") + for _, tag := range tagParts[1:] { + if tag == "squash" { + squash = true + break + } + } + + if squash { + if fieldKind != reflect.Struct { + errors = append(errors, + fmt.Errorf("%s: unsupported type for squash: %s", fieldType.Name, fieldKind).Error()) + } else { + structs = append(structs, structVal.FieldByName(fieldType.Name)) + } + continue + } + + // Normal struct field, store it away + fields = append(fields, field{fieldType, structVal.Field(i)}) + } + } + + // for fieldType, field := range fields { + for _, f := range fields { + field, fieldValue := f.field, f.val + fieldName := field.Name + + tagValue := field.Tag.Get(d.option.TagName) + tagValue = strings.SplitN(tagValue, ",", 2)[0] + if tagValue != "" { + fieldName = tagValue + } + + rawMapKey := reflect.ValueOf(fieldName) + rawMapVal := dataVal.MapIndex(rawMapKey) + if !rawMapVal.IsValid() { + // Do a slower search by iterating over each key and + // doing case-insensitive search. + for dataValKey := range dataValKeys { + mK, ok := dataValKey.Interface().(string) + if !ok { + // Not a string key + continue + } + + if strings.EqualFold(mK, fieldName) { + rawMapKey = dataValKey + rawMapVal = dataVal.MapIndex(dataValKey) + break + } + } + + if !rawMapVal.IsValid() { + // There was no matching key in the map for the value in + // the struct. Just ignore. + continue + } + } + + // Delete the key we're using from the unused map so we stop tracking + delete(dataValKeysUnused, rawMapKey.Interface()) + + if !fieldValue.IsValid() { + // This should never happen + panic("field is not valid") + } + + // If we can't set the field, then it is unexported or something, + // and we just continue onwards. + if !fieldValue.CanSet() { + continue + } + + // If the name is empty string, then we're at the root, and we + // don't dot-join the fields. + if name != "" { + fieldName = fmt.Sprintf("%s.%s", name, fieldName) + } + + if err := d.decode(fieldName, rawMapVal.Interface(), fieldValue); err != nil { + errors = append(errors, err.Error()) + } + } + + if len(errors) > 0 { + return fmt.Errorf(strings.Join(errors, ",")) + } + + return nil +} + func (d *Decoder) setInterface(name string, data interface{}, val reflect.Value) (err error) { dataVal := reflect.ValueOf(data) val.Set(dataVal) diff --git a/config/config.go b/config/config.go index e5358056..28d4b377 100644 --- a/config/config.go +++ b/config/config.go @@ -7,8 +7,9 @@ import ( "os" "strings" - adapters "github.com/Dreamacro/clash/adapters/outbound" - "github.com/Dreamacro/clash/common/structure" + "github.com/Dreamacro/clash/adapters/outbound" + "github.com/Dreamacro/clash/adapters/outboundgroup" + "github.com/Dreamacro/clash/adapters/provider" "github.com/Dreamacro/clash/component/auth" trie "github.com/Dreamacro/clash/component/domain-trie" "github.com/Dreamacro/clash/component/fakeip" @@ -68,6 +69,7 @@ type Config struct { Rules []C.Rule Users []auth.AuthUser Proxies map[string]C.Proxy + Providers map[string]provider.ProxyProvider } type rawDNS struct { @@ -99,12 +101,13 @@ type rawConfig struct { ExternalUI string `yaml:"external-ui"` Secret string `yaml:"secret"` - Hosts map[string]string `yaml:"hosts"` - DNS rawDNS `yaml:"dns"` - Experimental Experimental `yaml:"experimental"` - Proxy []map[string]interface{} `yaml:"Proxy"` - ProxyGroup []map[string]interface{} `yaml:"Proxy Group"` - Rule []string `yaml:"Rule"` + ProxyProvider map[string]map[string]interface{} `yaml:"proxy-provider"` + Hosts map[string]string `yaml:"hosts"` + DNS rawDNS `yaml:"dns"` + Experimental Experimental `yaml:"experimental"` + Proxy []map[string]interface{} `yaml:"Proxy"` + ProxyGroup []map[string]interface{} `yaml:"Proxy Group"` + Rule []string `yaml:"Rule"` } // Parse config @@ -146,11 +149,12 @@ func Parse(buf []byte) (*Config, error) { } config.General = general - proxies, err := parseProxies(rawCfg) + proxies, providers, err := parseProxies(rawCfg) if err != nil { return nil, err } config.Proxies = proxies + config.Providers = providers rules, err := parseRules(rawCfg, proxies) if err != nil { @@ -171,7 +175,6 @@ func Parse(buf []byte) (*Config, error) { config.Hosts = hosts config.Users = parseAuthentication(rawCfg.Authentication) - return config, nil } @@ -210,75 +213,38 @@ func parseGeneral(cfg *rawConfig) (*General, error) { return general, nil } -func parseProxies(cfg *rawConfig) (map[string]C.Proxy, error) { - proxies := make(map[string]C.Proxy) +func parseProxies(cfg *rawConfig) (proxies map[string]C.Proxy, providersMap map[string]provider.ProxyProvider, err error) { + proxies = make(map[string]C.Proxy) + providersMap = make(map[string]provider.ProxyProvider) proxyList := []string{} proxiesConfig := cfg.Proxy groupsConfig := cfg.ProxyGroup + providersConfig := cfg.ProxyProvider - decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true}) + defer func() { + // Destroy already created provider when err != nil + if err != nil { + for _, provider := range providersMap { + provider.Destroy() + } + } + }() - proxies["DIRECT"] = adapters.NewProxy(adapters.NewDirect()) - proxies["REJECT"] = adapters.NewProxy(adapters.NewReject()) + proxies["DIRECT"] = outbound.NewProxy(outbound.NewDirect()) + proxies["REJECT"] = outbound.NewProxy(outbound.NewReject()) proxyList = append(proxyList, "DIRECT", "REJECT") // parse proxy for idx, mapping := range proxiesConfig { - proxyType, existType := mapping["type"].(string) - if !existType { - return nil, fmt.Errorf("Proxy %d missing type", idx) - } - - var proxy C.ProxyAdapter - err := fmt.Errorf("cannot parse") - switch proxyType { - case "ss": - ssOption := &adapters.ShadowSocksOption{} - err = decoder.Decode(mapping, ssOption) - if err != nil { - break - } - proxy, err = adapters.NewShadowSocks(*ssOption) - case "socks5": - socksOption := &adapters.Socks5Option{} - err = decoder.Decode(mapping, socksOption) - if err != nil { - break - } - proxy = adapters.NewSocks5(*socksOption) - case "http": - httpOption := &adapters.HttpOption{} - err = decoder.Decode(mapping, httpOption) - if err != nil { - break - } - proxy = adapters.NewHttp(*httpOption) - case "vmess": - vmessOption := &adapters.VmessOption{} - err = decoder.Decode(mapping, vmessOption) - if err != nil { - break - } - proxy, err = adapters.NewVmess(*vmessOption) - case "snell": - snellOption := &adapters.SnellOption{} - err = decoder.Decode(mapping, snellOption) - if err != nil { - break - } - proxy, err = adapters.NewSnell(*snellOption) - default: - return nil, fmt.Errorf("Unsupport proxy type: %s", proxyType) - } - + proxy, err := outbound.ParseProxy(mapping) if err != nil { - return nil, fmt.Errorf("Proxy [%d]: %s", idx, err.Error()) + return nil, nil, fmt.Errorf("Proxy %d: %w", idx, err) } if _, exist := proxies[proxy.Name()]; exist { - return nil, fmt.Errorf("Proxy %s is the duplicate name", proxy.Name()) + return nil, nil, fmt.Errorf("Proxy %s is the duplicate name", proxy.Name()) } - proxies[proxy.Name()] = adapters.NewProxy(proxy) + proxies[proxy.Name()] = proxy proxyList = append(proxyList, proxy.Name()) } @@ -286,95 +252,62 @@ func parseProxies(cfg *rawConfig) (map[string]C.Proxy, error) { for idx, mapping := range groupsConfig { groupName, existName := mapping["name"].(string) if !existName { - return nil, fmt.Errorf("ProxyGroup %d: missing name", idx) + return nil, nil, fmt.Errorf("ProxyGroup %d: missing name", idx) } proxyList = append(proxyList, groupName) } // check if any loop exists and sort the ProxyGroups - if err := proxyGroupsDagSort(groupsConfig, decoder); err != nil { - return nil, err + if err := proxyGroupsDagSort(groupsConfig); err != nil { + return nil, nil, err + } + + // parse and initial providers + for name, mapping := range providersConfig { + if name == provider.ReservedName { + return nil, nil, fmt.Errorf("can not defined a provider called `%s`", provider.ReservedName) + } + + pd, err := provider.ParseProxyProvider(name, mapping) + if err != nil { + return nil, nil, err + } + + providersMap[name] = pd + } + + for _, provider := range providersMap { + log.Infoln("Start initial provider %s", provider.Name()) + if err := provider.Initial(); err != nil { + return nil, nil, err + } } // parse proxy group - for _, mapping := range groupsConfig { - groupType, existType := mapping["type"].(string) - groupName, _ := mapping["name"].(string) - if !existType { - return nil, fmt.Errorf("ProxyGroup %s: missing type", groupName) - } - - if _, exist := proxies[groupName]; exist { - return nil, fmt.Errorf("ProxyGroup %s: the duplicate name", groupName) - } - var group C.ProxyAdapter - ps := []C.Proxy{} - - err := fmt.Errorf("cannot parse") - switch groupType { - case "url-test": - urlTestOption := &adapters.URLTestOption{} - err = decoder.Decode(mapping, urlTestOption) - if err != nil { - break - } - - ps, err = getProxies(proxies, urlTestOption.Proxies) - if err != nil { - return nil, fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error()) - } - group, err = adapters.NewURLTest(*urlTestOption, ps) - case "select": - selectorOption := &adapters.SelectorOption{} - err = decoder.Decode(mapping, selectorOption) - if err != nil { - break - } - - ps, err = getProxies(proxies, selectorOption.Proxies) - if err != nil { - return nil, fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error()) - } - group, err = adapters.NewSelector(selectorOption.Name, ps) - case "fallback": - fallbackOption := &adapters.FallbackOption{} - err = decoder.Decode(mapping, fallbackOption) - if err != nil { - break - } - - ps, err = getProxies(proxies, fallbackOption.Proxies) - if err != nil { - return nil, fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error()) - } - group, err = adapters.NewFallback(*fallbackOption, ps) - case "load-balance": - loadBalanceOption := &adapters.LoadBalanceOption{} - err = decoder.Decode(mapping, loadBalanceOption) - if err != nil { - break - } - - ps, err = getProxies(proxies, loadBalanceOption.Proxies) - if err != nil { - return nil, fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error()) - } - group, err = adapters.NewLoadBalance(*loadBalanceOption, ps) - } + for idx, mapping := range groupsConfig { + group, err := outboundgroup.ParseProxyGroup(mapping, proxies, providersMap) if err != nil { - return nil, fmt.Errorf("Proxy %s: %s", groupName, err.Error()) + return nil, nil, fmt.Errorf("ProxyGroup[%d]: %w", idx, err) } - proxies[groupName] = adapters.NewProxy(group) + + groupName := group.Name() + if _, exist := proxies[groupName]; exist { + return nil, nil, fmt.Errorf("ProxyGroup %s: the duplicate name", groupName) + } + + proxies[groupName] = outbound.NewProxy(group) } ps := []C.Proxy{} for _, v := range proxyList { ps = append(ps, proxies[v]) } + pd, _ := provider.NewCompatibleProvier(provider.ReservedName, ps, nil) + providersMap[provider.ReservedName] = pd - global, _ := adapters.NewSelector("GLOBAL", ps) - proxies["GLOBAL"] = adapters.NewProxy(global) - return proxies, nil + global := outboundgroup.NewSelector("GLOBAL", []provider.ProxyProvider{pd}) + proxies["GLOBAL"] = outbound.NewProxy(global) + return proxies, providersMap, nil } func parseRules(cfg *rawConfig, proxies map[string]C.Proxy) ([]C.Rule, error) { diff --git a/config/utils.go b/config/utils.go index e263d68b..79fae6c9 100644 --- a/config/utils.go +++ b/config/utils.go @@ -4,9 +4,8 @@ import ( "fmt" "strings" - adapters "github.com/Dreamacro/clash/adapters/outbound" + "github.com/Dreamacro/clash/adapters/outboundgroup" "github.com/Dreamacro/clash/common/structure" - C "github.com/Dreamacro/clash/constant" ) func trimArr(arr []string) (r []string) { @@ -16,18 +15,6 @@ func trimArr(arr []string) (r []string) { return } -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 { for _, p := range pointers { if p != nil { @@ -40,8 +27,7 @@ func or(pointers ...*int) *int { // Check if ProxyGroups form DAG(Directed Acyclic Graph), and sort all ProxyGroups by dependency order. // Meanwhile, record the original index in the config file. // If loop is detected, return an error with location of loop. -func proxyGroupsDagSort(groupsConfig []map[string]interface{}, decoder *structure.Decoder) error { - +func proxyGroupsDagSort(groupsConfig []map[string]interface{}) error { type graphNode struct { indegree int // topological order @@ -50,34 +36,36 @@ func proxyGroupsDagSort(groupsConfig []map[string]interface{}, decoder *structur data map[string]interface{} // `outdegree` and `from` are used in loop locating outdegree int + option *outboundgroup.GroupCommonOption from []string } + decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true}) graph := make(map[string]*graphNode) // Step 1.1 build dependency graph for _, mapping := range groupsConfig { - option := &adapters.ProxyGroupOption{} - err := decoder.Decode(mapping, option) - groupName := option.Name - if err != nil { - return fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error()) + option := &outboundgroup.GroupCommonOption{} + if err := decoder.Decode(mapping, option); err != nil { + return fmt.Errorf("ProxyGroup %s: %s", option.Name, err.Error()) } + groupName := option.Name if node, ok := graph[groupName]; ok { if node.data != nil { return fmt.Errorf("ProxyGroup %s: duplicate group name", groupName) } node.data = mapping + node.option = option } else { - graph[groupName] = &graphNode{0, -1, mapping, 0, nil} + graph[groupName] = &graphNode{0, -1, mapping, 0, option, nil} } for _, proxy := range option.Proxies { if node, ex := graph[proxy]; ex { node.indegree++ } else { - graph[proxy] = &graphNode{1, -1, nil, 0, nil} + graph[proxy] = &graphNode{1, -1, nil, 0, nil, nil} } } } @@ -95,14 +83,19 @@ func proxyGroupsDagSort(groupsConfig []map[string]interface{}, decoder *structur for ; len(queue) > 0; queue = queue[1:] { name := queue[0] node := graph[name] - if node.data != nil { + if node.option != nil { index++ groupsConfig[len(groupsConfig)-index] = node.data - for _, proxy := range node.data["proxies"].([]interface{}) { - child := graph[proxy.(string)] + if len(node.option.Proxies) == 0 { + delete(graph, name) + continue + } + + for _, proxy := range node.option.Proxies { + child := graph[proxy] child.indegree-- if child.indegree == 0 { - queue = append(queue, proxy.(string)) + queue = append(queue, proxy) } } } @@ -117,12 +110,17 @@ func proxyGroupsDagSort(groupsConfig []map[string]interface{}, decoder *structur // if loop is detected, locate the loop and throw an error // Step 2.1 rebuild the graph, fill `outdegree` and `from` filed for name, node := range graph { - if node.data == nil { + if node.option == nil { continue } - for _, proxy := range node.data["proxies"].([]interface{}) { + + if len(node.option.Proxies) == 0 { + continue + } + + for _, proxy := range node.option.Proxies { node.outdegree++ - child := graph[proxy.(string)] + child := graph[proxy] if child.from == nil { child.from = make([]string, 0, child.indegree) } diff --git a/constant/adapters.go b/constant/adapters.go index 97d65a50..b51d083b 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -61,7 +61,6 @@ type ProxyAdapter interface { DialContext(ctx context.Context, metadata *Metadata) (Conn, error) DialUDP(metadata *Metadata) (PacketConn, net.Addr, error) SupportUDP() bool - Destroy() MarshalJSON() ([]byte, error) } diff --git a/hub/executor/executor.go b/hub/executor/executor.go index ddfa3e6d..7d44d974 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" + "github.com/Dreamacro/clash/adapters/provider" "github.com/Dreamacro/clash/component/auth" trie "github.com/Dreamacro/clash/component/domain-trie" "github.com/Dreamacro/clash/config" @@ -78,7 +79,7 @@ func ApplyConfig(cfg *config.Config, force bool) { if force { updateGeneral(cfg.General) } - updateProxies(cfg.Proxies) + updateProxies(cfg.Proxies, cfg.Providers) updateRules(cfg.Rules) updateDNS(cfg.DNS) updateHosts(cfg.Hosts) @@ -142,16 +143,16 @@ func updateHosts(tree *trie.Trie) { dns.DefaultHosts = tree } -func updateProxies(proxies map[string]C.Proxy) { +func updateProxies(proxies map[string]C.Proxy, providers map[string]provider.ProxyProvider) { tunnel := T.Instance() - oldProxies := tunnel.Proxies() + oldProviders := tunnel.Providers() - // close proxy group goroutine - for _, proxy := range oldProxies { - proxy.Destroy() + // close providers goroutine + for _, provider := range oldProviders { + provider.Destroy() } - tunnel.UpdateProxies(proxies) + tunnel.UpdateProxies(proxies, providers) } func updateRules(rules []C.Rule) { diff --git a/hub/route/proxies.go b/hub/route/proxies.go index 201fd03f..bdf4eee0 100644 --- a/hub/route/proxies.go +++ b/hub/route/proxies.go @@ -8,7 +8,8 @@ import ( "strconv" "time" - A "github.com/Dreamacro/clash/adapters/outbound" + "github.com/Dreamacro/clash/adapters/outbound" + "github.com/Dreamacro/clash/adapters/outboundgroup" C "github.com/Dreamacro/clash/constant" T "github.com/Dreamacro/clash/tunnel" @@ -81,8 +82,8 @@ func updateProxy(w http.ResponseWriter, r *http.Request) { return } - proxy := r.Context().Value(CtxKeyProxy).(*A.Proxy) - selector, ok := proxy.ProxyAdapter.(*A.Selector) + proxy := r.Context().Value(CtxKeyProxy).(*outbound.Proxy) + selector, ok := proxy.ProxyAdapter.(*outboundgroup.Selector) if !ok { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError("Must be a Selector")) diff --git a/proxy/redir/tcp.go b/proxy/redir/tcp.go index 1c0fee65..fd455603 100644 --- a/proxy/redir/tcp.go +++ b/proxy/redir/tcp.go @@ -59,5 +59,5 @@ func handleRedir(conn net.Conn) { return } conn.(*net.TCPConn).SetKeepAlive(true) - tun.Add(adapters.NewSocket(target, conn, C.REDIR, C.TCP)) + tun.Add(inbound.NewSocket(target, conn, C.REDIR, C.TCP)) } diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go index 29196556..a578a490 100644 --- a/tunnel/tunnel.go +++ b/tunnel/tunnel.go @@ -6,7 +6,8 @@ import ( "sync" "time" - InboundAdapter "github.com/Dreamacro/clash/adapters/inbound" + "github.com/Dreamacro/clash/adapters/inbound" + "github.com/Dreamacro/clash/adapters/provider" "github.com/Dreamacro/clash/component/nat" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/dns" @@ -30,6 +31,7 @@ type Tunnel struct { natTable *nat.Table rules []C.Rule proxies map[string]C.Proxy + providers map[string]provider.ProxyProvider configMux sync.RWMutex // experimental features @@ -66,10 +68,16 @@ func (t *Tunnel) Proxies() map[string]C.Proxy { return t.proxies } +// Providers return all compatible providers +func (t *Tunnel) Providers() map[string]provider.ProxyProvider { + return t.providers +} + // UpdateProxies handle update proxies -func (t *Tunnel) UpdateProxies(proxies map[string]C.Proxy) { +func (t *Tunnel) UpdateProxies(proxies map[string]C.Proxy, providers map[string]provider.ProxyProvider) { t.configMux.Lock() t.proxies = proxies + t.providers = providers t.configMux.Unlock() } @@ -240,9 +248,9 @@ func (t *Tunnel) handleTCPConn(localConn C.ServerAdapter) { } switch adapter := localConn.(type) { - case *InboundAdapter.HTTPAdapter: + case *inbound.HTTPAdapter: t.handleHTTP(adapter, remoteConn) - case *InboundAdapter.SocketAdapter: + case *inbound.SocketAdapter: t.handleSocket(adapter, remoteConn) } }