Feature: add experimental provider
This commit is contained in:
parent
4d7096f451
commit
c427bc89ef
33 changed files with 1247 additions and 620 deletions
|
@ -1,4 +1,4 @@
|
||||||
package adapters
|
package inbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package adapters
|
package inbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package adapters
|
package inbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package adapters
|
package inbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package adapters
|
package outbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -38,14 +38,16 @@ func (b *Base) SupportUDP() bool {
|
||||||
return b.udp
|
return b.udp
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Base) Destroy() {}
|
|
||||||
|
|
||||||
func (b *Base) MarshalJSON() ([]byte, error) {
|
func (b *Base) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(map[string]string{
|
return json.Marshal(map[string]string{
|
||||||
"type": b.Type().String(),
|
"type": b.Type().String(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewBase(name string, tp C.AdapterType, udp bool) *Base {
|
||||||
|
return &Base{name, tp, udp}
|
||||||
|
}
|
||||||
|
|
||||||
type conn struct {
|
type conn struct {
|
||||||
net.Conn
|
net.Conn
|
||||||
chain C.Chain
|
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 {
|
func NewProxy(adapter C.ProxyAdapter) *Proxy {
|
||||||
return &Proxy{adapter, queue.New(10), true}
|
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"`
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package adapters
|
package outbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package adapters
|
package outbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
|
64
adapters/outbound/parser.go
Normal file
64
adapters/outbound/parser.go
Normal 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
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package adapters
|
package outbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package adapters
|
package outbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package adapters
|
package outbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package adapters
|
package outbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package adapters
|
package outbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package adapters
|
package outbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
84
adapters/outboundgroup/fallback.go
Normal file
84
adapters/outboundgroup/fallback.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,12 @@
|
||||||
package adapters
|
package outboundgroup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/adapters/outbound"
|
||||||
|
"github.com/Dreamacro/clash/adapters/provider"
|
||||||
"github.com/Dreamacro/clash/common/murmur3"
|
"github.com/Dreamacro/clash/common/murmur3"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
|
||||||
|
@ -15,12 +14,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoadBalance struct {
|
type LoadBalance struct {
|
||||||
*Base
|
*outbound.Base
|
||||||
proxies []C.Proxy
|
maxRetry int
|
||||||
maxRetry int
|
providers []provider.ProxyProvider
|
||||||
rawURL string
|
|
||||||
interval time.Duration
|
|
||||||
done chan struct{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getKey(metadata *C.Metadata) string {
|
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))))
|
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 {
|
for i := 0; i < lb.maxRetry; i, key = i+1, key+1 {
|
||||||
idx := jumpHash(key, buckets)
|
idx := jumpHash(key, buckets)
|
||||||
proxy := lb.proxies[idx]
|
proxy := proxies[idx]
|
||||||
if proxy.Alive() {
|
if proxy.Alive() {
|
||||||
c, err = proxy.DialContext(ctx, metadata)
|
c, err = proxy.DialContext(ctx, metadata)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c, err = lb.proxies[0].DialContext(ctx, metadata)
|
c, err = proxies[0].DialContext(ctx, metadata)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,57 +80,34 @@ func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, addr net.
|
||||||
}()
|
}()
|
||||||
|
|
||||||
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
|
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 {
|
for i := 0; i < lb.maxRetry; i, key = i+1, key+1 {
|
||||||
idx := jumpHash(key, buckets)
|
idx := jumpHash(key, buckets)
|
||||||
proxy := lb.proxies[idx]
|
proxy := proxies[idx]
|
||||||
if proxy.Alive() {
|
if proxy.Alive() {
|
||||||
return proxy.DialUDP(metadata)
|
return proxy.DialUDP(metadata)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return lb.proxies[0].DialUDP(metadata)
|
return proxies[0].DialUDP(metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lb *LoadBalance) SupportUDP() bool {
|
func (lb *LoadBalance) SupportUDP() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lb *LoadBalance) Destroy() {
|
func (lb *LoadBalance) proxies() []C.Proxy {
|
||||||
lb.done <- struct{}{}
|
proxies := []C.Proxy{}
|
||||||
}
|
for _, provider := range lb.providers {
|
||||||
|
proxies = append(proxies, provider.Proxies()...)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
|
func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
|
||||||
var all []string
|
var all []string
|
||||||
for _, proxy := range lb.proxies {
|
for _, proxy := range lb.proxies() {
|
||||||
all = append(all, proxy.Name())
|
all = append(all, proxy.Name())
|
||||||
}
|
}
|
||||||
return json.Marshal(map[string]interface{}{
|
return json.Marshal(map[string]interface{}{
|
||||||
|
@ -142,31 +116,10 @@ func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoadBalanceOption struct {
|
func NewLoadBalance(name string, providers []provider.ProxyProvider) *LoadBalance {
|
||||||
Name string `proxy:"name"`
|
return &LoadBalance{
|
||||||
Proxies []string `proxy:"proxies"`
|
Base: outbound.NewBase(name, C.LoadBalance, false),
|
||||||
URL string `proxy:"url"`
|
maxRetry: 3,
|
||||||
Interval int `proxy:"interval"`
|
providers: providers,
|
||||||
}
|
|
||||||
|
|
||||||
func NewLoadBalance(option LoadBalanceOption, proxies []C.Proxy) (*LoadBalance, error) {
|
|
||||||
if len(proxies) == 0 {
|
|
||||||
return nil, errors.New("Provide at least one proxy")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
139
adapters/outboundgroup/parser.go
Normal file
139
adapters/outboundgroup/parser.go
Normal 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
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package adapters
|
package outboundgroup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -6,19 +6,15 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/adapters/outbound"
|
||||||
|
"github.com/Dreamacro/clash/adapters/provider"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Selector struct {
|
type Selector struct {
|
||||||
*Base
|
*outbound.Base
|
||||||
selected C.Proxy
|
selected C.Proxy
|
||||||
proxies map[string]C.Proxy
|
providers []provider.ProxyProvider
|
||||||
proxyList []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type SelectorOption struct {
|
|
||||||
Name string `proxy:"name"`
|
|
||||||
Proxies []string `proxy:"proxies"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
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) {
|
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{}{
|
return json.Marshal(map[string]interface{}{
|
||||||
"type": s.Type().String(),
|
"type": s.Type().String(),
|
||||||
"now": s.Now(),
|
"now": s.Now(),
|
||||||
"all": s.proxyList,
|
"all": all,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,34 +55,29 @@ func (s *Selector) Now() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Selector) Set(name string) error {
|
func (s *Selector) Set(name string) error {
|
||||||
proxy, exist := s.proxies[name]
|
for _, proxy := range s.proxies() {
|
||||||
if !exist {
|
if proxy.Name() == name {
|
||||||
return errors.New("Proxy does not exist")
|
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) {
|
func (s *Selector) proxies() []C.Proxy {
|
||||||
if len(proxies) == 0 {
|
proxies := []C.Proxy{}
|
||||||
return nil, errors.New("Provide at least one 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
|
|
||||||
}
|
}
|
93
adapters/outboundgroup/urltest.go
Normal file
93
adapters/outboundgroup/urltest.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
53
adapters/provider/healthcheck.go
Normal file
53
adapters/provider/healthcheck.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
60
adapters/provider/parser.go
Normal file
60
adapters/provider/parser.go
Normal 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
|
||||||
|
}
|
293
adapters/provider/provider.go
Normal file
293
adapters/provider/provider.go
Normal 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
|
||||||
|
}
|
109
adapters/provider/vehicle.go
Normal file
109
adapters/provider/vehicle.go
Normal 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}
|
||||||
|
}
|
|
@ -76,6 +76,8 @@ func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error
|
||||||
return d.decodeMap(name, data, val)
|
return d.decodeMap(name, data, val)
|
||||||
case reflect.Interface:
|
case reflect.Interface:
|
||||||
return d.setInterface(name, data, val)
|
return d.setInterface(name, data, val)
|
||||||
|
case reflect.Struct:
|
||||||
|
return d.decodeStruct(name, data, val)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("type %s not support", val.Kind().String())
|
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
|
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) {
|
func (d *Decoder) setInterface(name string, data interface{}, val reflect.Value) (err error) {
|
||||||
dataVal := reflect.ValueOf(data)
|
dataVal := reflect.ValueOf(data)
|
||||||
val.Set(dataVal)
|
val.Set(dataVal)
|
||||||
|
|
207
config/config.go
207
config/config.go
|
@ -7,8 +7,9 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
adapters "github.com/Dreamacro/clash/adapters/outbound"
|
"github.com/Dreamacro/clash/adapters/outbound"
|
||||||
"github.com/Dreamacro/clash/common/structure"
|
"github.com/Dreamacro/clash/adapters/outboundgroup"
|
||||||
|
"github.com/Dreamacro/clash/adapters/provider"
|
||||||
"github.com/Dreamacro/clash/component/auth"
|
"github.com/Dreamacro/clash/component/auth"
|
||||||
trie "github.com/Dreamacro/clash/component/domain-trie"
|
trie "github.com/Dreamacro/clash/component/domain-trie"
|
||||||
"github.com/Dreamacro/clash/component/fakeip"
|
"github.com/Dreamacro/clash/component/fakeip"
|
||||||
|
@ -68,6 +69,7 @@ type Config struct {
|
||||||
Rules []C.Rule
|
Rules []C.Rule
|
||||||
Users []auth.AuthUser
|
Users []auth.AuthUser
|
||||||
Proxies map[string]C.Proxy
|
Proxies map[string]C.Proxy
|
||||||
|
Providers map[string]provider.ProxyProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
type rawDNS struct {
|
type rawDNS struct {
|
||||||
|
@ -99,12 +101,13 @@ type rawConfig struct {
|
||||||
ExternalUI string `yaml:"external-ui"`
|
ExternalUI string `yaml:"external-ui"`
|
||||||
Secret string `yaml:"secret"`
|
Secret string `yaml:"secret"`
|
||||||
|
|
||||||
Hosts map[string]string `yaml:"hosts"`
|
ProxyProvider map[string]map[string]interface{} `yaml:"proxy-provider"`
|
||||||
DNS rawDNS `yaml:"dns"`
|
Hosts map[string]string `yaml:"hosts"`
|
||||||
Experimental Experimental `yaml:"experimental"`
|
DNS rawDNS `yaml:"dns"`
|
||||||
Proxy []map[string]interface{} `yaml:"Proxy"`
|
Experimental Experimental `yaml:"experimental"`
|
||||||
ProxyGroup []map[string]interface{} `yaml:"Proxy Group"`
|
Proxy []map[string]interface{} `yaml:"Proxy"`
|
||||||
Rule []string `yaml:"Rule"`
|
ProxyGroup []map[string]interface{} `yaml:"Proxy Group"`
|
||||||
|
Rule []string `yaml:"Rule"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse config
|
// Parse config
|
||||||
|
@ -146,11 +149,12 @@ func Parse(buf []byte) (*Config, error) {
|
||||||
}
|
}
|
||||||
config.General = general
|
config.General = general
|
||||||
|
|
||||||
proxies, err := parseProxies(rawCfg)
|
proxies, providers, err := parseProxies(rawCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
config.Proxies = proxies
|
config.Proxies = proxies
|
||||||
|
config.Providers = providers
|
||||||
|
|
||||||
rules, err := parseRules(rawCfg, proxies)
|
rules, err := parseRules(rawCfg, proxies)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -171,7 +175,6 @@ func Parse(buf []byte) (*Config, error) {
|
||||||
config.Hosts = hosts
|
config.Hosts = hosts
|
||||||
|
|
||||||
config.Users = parseAuthentication(rawCfg.Authentication)
|
config.Users = parseAuthentication(rawCfg.Authentication)
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,75 +213,38 @@ func parseGeneral(cfg *rawConfig) (*General, error) {
|
||||||
return general, nil
|
return general, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseProxies(cfg *rawConfig) (map[string]C.Proxy, error) {
|
func parseProxies(cfg *rawConfig) (proxies map[string]C.Proxy, providersMap map[string]provider.ProxyProvider, err error) {
|
||||||
proxies := make(map[string]C.Proxy)
|
proxies = make(map[string]C.Proxy)
|
||||||
|
providersMap = make(map[string]provider.ProxyProvider)
|
||||||
proxyList := []string{}
|
proxyList := []string{}
|
||||||
proxiesConfig := cfg.Proxy
|
proxiesConfig := cfg.Proxy
|
||||||
groupsConfig := cfg.ProxyGroup
|
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["DIRECT"] = outbound.NewProxy(outbound.NewDirect())
|
||||||
proxies["REJECT"] = adapters.NewProxy(adapters.NewReject())
|
proxies["REJECT"] = outbound.NewProxy(outbound.NewReject())
|
||||||
proxyList = append(proxyList, "DIRECT", "REJECT")
|
proxyList = append(proxyList, "DIRECT", "REJECT")
|
||||||
|
|
||||||
// parse proxy
|
// parse proxy
|
||||||
for idx, mapping := range proxiesConfig {
|
for idx, mapping := range proxiesConfig {
|
||||||
proxyType, existType := mapping["type"].(string)
|
proxy, err := outbound.ParseProxy(mapping)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
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 {
|
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())
|
proxyList = append(proxyList, proxy.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,95 +252,62 @@ func parseProxies(cfg *rawConfig) (map[string]C.Proxy, error) {
|
||||||
for idx, mapping := range groupsConfig {
|
for idx, mapping := range groupsConfig {
|
||||||
groupName, existName := mapping["name"].(string)
|
groupName, existName := mapping["name"].(string)
|
||||||
if !existName {
|
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)
|
proxyList = append(proxyList, groupName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if any loop exists and sort the ProxyGroups
|
// check if any loop exists and sort the ProxyGroups
|
||||||
if err := proxyGroupsDagSort(groupsConfig, decoder); err != nil {
|
if err := proxyGroupsDagSort(groupsConfig); err != nil {
|
||||||
return nil, err
|
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
|
// parse proxy group
|
||||||
for _, mapping := range groupsConfig {
|
for idx, mapping := range groupsConfig {
|
||||||
groupType, existType := mapping["type"].(string)
|
group, err := outboundgroup.ParseProxyGroup(mapping, proxies, providersMap)
|
||||||
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)
|
|
||||||
}
|
|
||||||
if err != nil {
|
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{}
|
ps := []C.Proxy{}
|
||||||
for _, v := range proxyList {
|
for _, v := range proxyList {
|
||||||
ps = append(ps, proxies[v])
|
ps = append(ps, proxies[v])
|
||||||
}
|
}
|
||||||
|
pd, _ := provider.NewCompatibleProvier(provider.ReservedName, ps, nil)
|
||||||
|
providersMap[provider.ReservedName] = pd
|
||||||
|
|
||||||
global, _ := adapters.NewSelector("GLOBAL", ps)
|
global := outboundgroup.NewSelector("GLOBAL", []provider.ProxyProvider{pd})
|
||||||
proxies["GLOBAL"] = adapters.NewProxy(global)
|
proxies["GLOBAL"] = outbound.NewProxy(global)
|
||||||
return proxies, nil
|
return proxies, providersMap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseRules(cfg *rawConfig, proxies map[string]C.Proxy) ([]C.Rule, error) {
|
func parseRules(cfg *rawConfig, proxies map[string]C.Proxy) ([]C.Rule, error) {
|
||||||
|
|
|
@ -4,9 +4,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
adapters "github.com/Dreamacro/clash/adapters/outbound"
|
"github.com/Dreamacro/clash/adapters/outboundgroup"
|
||||||
"github.com/Dreamacro/clash/common/structure"
|
"github.com/Dreamacro/clash/common/structure"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func trimArr(arr []string) (r []string) {
|
func trimArr(arr []string) (r []string) {
|
||||||
|
@ -16,18 +15,6 @@ func trimArr(arr []string) (r []string) {
|
||||||
return
|
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 {
|
func or(pointers ...*int) *int {
|
||||||
for _, p := range pointers {
|
for _, p := range pointers {
|
||||||
if p != nil {
|
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.
|
// Check if ProxyGroups form DAG(Directed Acyclic Graph), and sort all ProxyGroups by dependency order.
|
||||||
// Meanwhile, record the original index in the config file.
|
// Meanwhile, record the original index in the config file.
|
||||||
// If loop is detected, return an error with location of loop.
|
// 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 {
|
type graphNode struct {
|
||||||
indegree int
|
indegree int
|
||||||
// topological order
|
// topological order
|
||||||
|
@ -50,34 +36,36 @@ func proxyGroupsDagSort(groupsConfig []map[string]interface{}, decoder *structur
|
||||||
data map[string]interface{}
|
data map[string]interface{}
|
||||||
// `outdegree` and `from` are used in loop locating
|
// `outdegree` and `from` are used in loop locating
|
||||||
outdegree int
|
outdegree int
|
||||||
|
option *outboundgroup.GroupCommonOption
|
||||||
from []string
|
from []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true})
|
||||||
graph := make(map[string]*graphNode)
|
graph := make(map[string]*graphNode)
|
||||||
|
|
||||||
// Step 1.1 build dependency graph
|
// Step 1.1 build dependency graph
|
||||||
for _, mapping := range groupsConfig {
|
for _, mapping := range groupsConfig {
|
||||||
option := &adapters.ProxyGroupOption{}
|
option := &outboundgroup.GroupCommonOption{}
|
||||||
err := decoder.Decode(mapping, option)
|
if err := decoder.Decode(mapping, option); err != nil {
|
||||||
groupName := option.Name
|
return fmt.Errorf("ProxyGroup %s: %s", option.Name, err.Error())
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
groupName := option.Name
|
||||||
if node, ok := graph[groupName]; ok {
|
if node, ok := graph[groupName]; ok {
|
||||||
if node.data != nil {
|
if node.data != nil {
|
||||||
return fmt.Errorf("ProxyGroup %s: duplicate group name", groupName)
|
return fmt.Errorf("ProxyGroup %s: duplicate group name", groupName)
|
||||||
}
|
}
|
||||||
node.data = mapping
|
node.data = mapping
|
||||||
|
node.option = option
|
||||||
} else {
|
} else {
|
||||||
graph[groupName] = &graphNode{0, -1, mapping, 0, nil}
|
graph[groupName] = &graphNode{0, -1, mapping, 0, option, nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, proxy := range option.Proxies {
|
for _, proxy := range option.Proxies {
|
||||||
if node, ex := graph[proxy]; ex {
|
if node, ex := graph[proxy]; ex {
|
||||||
node.indegree++
|
node.indegree++
|
||||||
} else {
|
} 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:] {
|
for ; len(queue) > 0; queue = queue[1:] {
|
||||||
name := queue[0]
|
name := queue[0]
|
||||||
node := graph[name]
|
node := graph[name]
|
||||||
if node.data != nil {
|
if node.option != nil {
|
||||||
index++
|
index++
|
||||||
groupsConfig[len(groupsConfig)-index] = node.data
|
groupsConfig[len(groupsConfig)-index] = node.data
|
||||||
for _, proxy := range node.data["proxies"].([]interface{}) {
|
if len(node.option.Proxies) == 0 {
|
||||||
child := graph[proxy.(string)]
|
delete(graph, name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, proxy := range node.option.Proxies {
|
||||||
|
child := graph[proxy]
|
||||||
child.indegree--
|
child.indegree--
|
||||||
if child.indegree == 0 {
|
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
|
// if loop is detected, locate the loop and throw an error
|
||||||
// Step 2.1 rebuild the graph, fill `outdegree` and `from` filed
|
// Step 2.1 rebuild the graph, fill `outdegree` and `from` filed
|
||||||
for name, node := range graph {
|
for name, node := range graph {
|
||||||
if node.data == nil {
|
if node.option == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, proxy := range node.data["proxies"].([]interface{}) {
|
|
||||||
|
if len(node.option.Proxies) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, proxy := range node.option.Proxies {
|
||||||
node.outdegree++
|
node.outdegree++
|
||||||
child := graph[proxy.(string)]
|
child := graph[proxy]
|
||||||
if child.from == nil {
|
if child.from == nil {
|
||||||
child.from = make([]string, 0, child.indegree)
|
child.from = make([]string, 0, child.indegree)
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,6 @@ type ProxyAdapter interface {
|
||||||
DialContext(ctx context.Context, metadata *Metadata) (Conn, error)
|
DialContext(ctx context.Context, metadata *Metadata) (Conn, error)
|
||||||
DialUDP(metadata *Metadata) (PacketConn, net.Addr, error)
|
DialUDP(metadata *Metadata) (PacketConn, net.Addr, error)
|
||||||
SupportUDP() bool
|
SupportUDP() bool
|
||||||
Destroy()
|
|
||||||
MarshalJSON() ([]byte, error)
|
MarshalJSON() ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/adapters/provider"
|
||||||
"github.com/Dreamacro/clash/component/auth"
|
"github.com/Dreamacro/clash/component/auth"
|
||||||
trie "github.com/Dreamacro/clash/component/domain-trie"
|
trie "github.com/Dreamacro/clash/component/domain-trie"
|
||||||
"github.com/Dreamacro/clash/config"
|
"github.com/Dreamacro/clash/config"
|
||||||
|
@ -78,7 +79,7 @@ func ApplyConfig(cfg *config.Config, force bool) {
|
||||||
if force {
|
if force {
|
||||||
updateGeneral(cfg.General)
|
updateGeneral(cfg.General)
|
||||||
}
|
}
|
||||||
updateProxies(cfg.Proxies)
|
updateProxies(cfg.Proxies, cfg.Providers)
|
||||||
updateRules(cfg.Rules)
|
updateRules(cfg.Rules)
|
||||||
updateDNS(cfg.DNS)
|
updateDNS(cfg.DNS)
|
||||||
updateHosts(cfg.Hosts)
|
updateHosts(cfg.Hosts)
|
||||||
|
@ -142,16 +143,16 @@ func updateHosts(tree *trie.Trie) {
|
||||||
dns.DefaultHosts = tree
|
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()
|
tunnel := T.Instance()
|
||||||
oldProxies := tunnel.Proxies()
|
oldProviders := tunnel.Providers()
|
||||||
|
|
||||||
// close proxy group goroutine
|
// close providers goroutine
|
||||||
for _, proxy := range oldProxies {
|
for _, provider := range oldProviders {
|
||||||
proxy.Destroy()
|
provider.Destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
tunnel.UpdateProxies(proxies)
|
tunnel.UpdateProxies(proxies, providers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateRules(rules []C.Rule) {
|
func updateRules(rules []C.Rule) {
|
||||||
|
|
|
@ -8,7 +8,8 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"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"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
T "github.com/Dreamacro/clash/tunnel"
|
T "github.com/Dreamacro/clash/tunnel"
|
||||||
|
|
||||||
|
@ -81,8 +82,8 @@ func updateProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy := r.Context().Value(CtxKeyProxy).(*A.Proxy)
|
proxy := r.Context().Value(CtxKeyProxy).(*outbound.Proxy)
|
||||||
selector, ok := proxy.ProxyAdapter.(*A.Selector)
|
selector, ok := proxy.ProxyAdapter.(*outboundgroup.Selector)
|
||||||
if !ok {
|
if !ok {
|
||||||
render.Status(r, http.StatusBadRequest)
|
render.Status(r, http.StatusBadRequest)
|
||||||
render.JSON(w, r, newError("Must be a Selector"))
|
render.JSON(w, r, newError("Must be a Selector"))
|
||||||
|
|
|
@ -59,5 +59,5 @@ func handleRedir(conn net.Conn) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
conn.(*net.TCPConn).SetKeepAlive(true)
|
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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,8 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"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"
|
"github.com/Dreamacro/clash/component/nat"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/dns"
|
"github.com/Dreamacro/clash/dns"
|
||||||
|
@ -30,6 +31,7 @@ type Tunnel struct {
|
||||||
natTable *nat.Table
|
natTable *nat.Table
|
||||||
rules []C.Rule
|
rules []C.Rule
|
||||||
proxies map[string]C.Proxy
|
proxies map[string]C.Proxy
|
||||||
|
providers map[string]provider.ProxyProvider
|
||||||
configMux sync.RWMutex
|
configMux sync.RWMutex
|
||||||
|
|
||||||
// experimental features
|
// experimental features
|
||||||
|
@ -66,10 +68,16 @@ func (t *Tunnel) Proxies() map[string]C.Proxy {
|
||||||
return t.proxies
|
return t.proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Providers return all compatible providers
|
||||||
|
func (t *Tunnel) Providers() map[string]provider.ProxyProvider {
|
||||||
|
return t.providers
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateProxies handle update proxies
|
// 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.configMux.Lock()
|
||||||
t.proxies = proxies
|
t.proxies = proxies
|
||||||
|
t.providers = providers
|
||||||
t.configMux.Unlock()
|
t.configMux.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,9 +248,9 @@ func (t *Tunnel) handleTCPConn(localConn C.ServerAdapter) {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch adapter := localConn.(type) {
|
switch adapter := localConn.(type) {
|
||||||
case *InboundAdapter.HTTPAdapter:
|
case *inbound.HTTPAdapter:
|
||||||
t.handleHTTP(adapter, remoteConn)
|
t.handleHTTP(adapter, remoteConn)
|
||||||
case *InboundAdapter.SocketAdapter:
|
case *inbound.SocketAdapter:
|
||||||
t.handleSocket(adapter, remoteConn)
|
t.handleSocket(adapter, remoteConn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue