feat: add simple proxy provider support.

This commit is contained in:
Howard Cheung 2022-09-12 14:02:08 +08:00
parent eaf1ace681
commit cb026e63c1
11 changed files with 338 additions and 3 deletions

View File

@ -19,6 +19,7 @@ type Router interface {
Outbounds() []Outbound Outbounds() []Outbound
Outbound(tag string) (Outbound, bool) Outbound(tag string) (Outbound, bool)
AddOutbound(string, Outbound)
DefaultOutbound(network string) Outbound DefaultOutbound(network string) Outbound
RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error

2
box.go
View File

@ -14,6 +14,7 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing-box/outbound"
_ "github.com/sagernet/sing-box/outbound/provider"
"github.com/sagernet/sing-box/route" "github.com/sagernet/sing-box/route"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@ -120,6 +121,7 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
} }
inbounds = append(inbounds, in) inbounds = append(inbounds, in)
} }
outbounds = append(outbounds, outbound.InitCompatibleProxy(router, logFactory.NewLogger("outbound/provider"), option.Outbound{Type: "direct", Tag: "compatible"}.DirectOptions))
for i, outboundOptions := range options.Outbounds { for i, outboundOptions := range options.Outbounds {
var out adapter.Outbound var out adapter.Outbound
var tag string var tag string

3
go.mod
View File

@ -7,6 +7,7 @@ require (
github.com/cloudflare/circl v1.2.1-0.20220831060716-4cf0150356fc github.com/cloudflare/circl v1.2.1-0.20220831060716-4cf0150356fc
github.com/cretz/bine v0.2.0 github.com/cretz/bine v0.2.0
github.com/database64128/tfo-go v1.1.2 github.com/database64128/tfo-go v1.1.2
github.com/dlclark/regexp2 v1.7.0
github.com/dustin/go-humanize v1.0.0 github.com/dustin/go-humanize v1.0.0
github.com/fsnotify/fsnotify v1.5.4 github.com/fsnotify/fsnotify v1.5.4
github.com/go-chi/chi/v5 v5.0.7 github.com/go-chi/chi/v5 v5.0.7
@ -40,6 +41,7 @@ require (
golang.zx2c4.com/wireguard v0.0.0-20220904105730-b51010ba13f0 golang.zx2c4.com/wireguard v0.0.0-20220904105730-b51010ba13f0
google.golang.org/grpc v1.49.0 google.golang.org/grpc v1.49.0
google.golang.org/protobuf v1.28.1 google.golang.org/protobuf v1.28.1
gopkg.in/yaml.v3 v3.0.1
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c
) )
@ -78,6 +80,5 @@ require (
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f // indirect google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.1.7 // indirect lukechampine.com/blake3 v1.1.7 // indirect
) )

2
go.sum
View File

@ -26,6 +26,8 @@ github.com/database64128/tfo-go v1.1.2/go.mod h1:jgrSUPyOvTGQyn6irCOpk7L2W/q/0VL
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=

View File

@ -22,6 +22,7 @@ type _Outbound struct {
SSHOptions SSHOutboundOptions `json:"-"` SSHOptions SSHOutboundOptions `json:"-"`
ShadowTLSOptions ShadowTLSOutboundOptions `json:"-"` ShadowTLSOptions ShadowTLSOutboundOptions `json:"-"`
SelectorOptions SelectorOutboundOptions `json:"-"` SelectorOptions SelectorOutboundOptions `json:"-"`
ProviderOptions []ProviderOutboundOptions `json:"providers,omitempty"`
} }
type Outbound _Outbound type Outbound _Outbound

7
option/provider.go Normal file
View File

@ -0,0 +1,7 @@
package option
type ProviderOutboundOptions struct {
Url string `json:"url"`
Filter string `json:"filter"`
Interval int `json:"interval,omitempty"`
}

View File

@ -42,7 +42,7 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o
case C.TypeShadowTLS: case C.TypeShadowTLS:
return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions) return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions)
case C.TypeSelector: case C.TypeSelector:
return NewSelector(router, logger, options.Tag, options.SelectorOptions) return NewSelector(ctx, router, logger, options.Tag, options.SelectorOptions, options.ProviderOptions)
default: default:
return nil, E.New("unknown outbound type: ", options.Type) return nil, E.New("unknown outbound type: ", options.Type)
} }

180
outbound/provider.go Normal file
View File

@ -0,0 +1,180 @@
package outbound
import (
"context"
"io"
"net/http"
"sync"
"time"
"github.com/dlclark/regexp2"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
type Provider struct {
url string
filter *regexp2.Regexp
interval time.Duration
logger log.ContextLogger
ctx context.Context
router adapter.Router
}
type providerAdapter struct {
providers []Provider
}
type CachedProvider struct {
Outbounds []adapter.Outbound
LastUpdate time.Time
Lock *sync.Mutex
}
type ProviderResolver interface {
GetOutbounds([]byte, context.Context, adapter.Router, log.ContextLogger) []adapter.Outbound
}
var (
providerResolvers = make(map[string]ProviderResolver)
cachedProviders = make(map[string]*CachedProvider, 0)
compatibleProxy adapter.Outbound
)
func NewProvider(url, filterStr string, interval int, ctx context.Context, router adapter.Router,
logger log.ContextLogger) (Provider, error) {
if interval == 0 {
interval = 3600 * 24
}
filter, err := regexp2.Compile(filterStr, 0)
if err != nil {
return Provider{}, E.New("cannot parse provider regex filter")
}
return Provider{
url: url,
filter: filter,
interval: time.Duration(interval) * time.Second,
ctx: ctx,
router: router,
logger: logger},
nil
}
func (p *Provider) GetOutbounds() ([]string, map[string]adapter.Outbound) {
tags := make([]string, 0)
outbounds := make(map[string]adapter.Outbound, 0)
allOutbounds := p.getAllOutbounds()
for _, outbound := range allOutbounds {
if ok, _ := p.filter.MatchString(outbound.Tag()); ok {
p.router.AddOutbound(outbound.Tag(), outbound)
tags = append(tags, outbound.Tag())
outbounds[outbound.Tag()] = outbound
}
}
return tags, outbounds
}
func (p *Provider) getAllOutbounds() (res []adapter.Outbound) {
defer func() {
if r := recover(); r != nil {
res = make([]adapter.Outbound, 0)
p.logger.Warn("failed to get provider outbounds: ", r)
}
}()
if _, ok := cachedProviders[p.url]; !ok {
cachedProviders[p.url] = &CachedProvider{
Outbounds: make([]adapter.Outbound, 0),
LastUpdate: time.Time{},
Lock: &sync.Mutex{},
}
}
cachedProviders[p.url].Lock.Lock()
defer cachedProviders[p.url].Lock.Unlock()
if (cachedProviders[p.url].LastUpdate.Add(p.interval)).Before(time.Now()) {
outbounds := make([]adapter.Outbound, 0)
resp, err := http.DefaultClient.Get(p.url)
if err == nil {
body := resp.Body
defer body.Close()
content, _ := io.ReadAll(body)
for _, resolver := range providerResolvers {
if len(outbounds) == 0 {
outbounds = resolver.GetOutbounds(content, p.ctx, p.router, p.logger)
}
}
cachedProviders[p.url].SetOutbounds(outbounds)
}
}
cachedProviders[p.url].RefeshUpdateTime()
return cachedProviders[p.url].Outbounds
}
func (p *providerAdapter) NewUpdateFunc(tags *[]string, outbounds *map[string]adapter.Outbound, router adapter.Router, funcs []func()) func() {
res := func() {
for _, f := range funcs {
defer f()
}
for _, provider := range p.providers {
_, newOutbounds := provider.GetOutbounds()
for k, v := range newOutbounds {
if _, ok := (*outbounds)[k]; !ok {
*tags = append(*tags, k)
(*outbounds)[k] = v
}
}
}
if len(*tags) == 0 {
p.AddCompatibleProxy(tags, outbounds, router)
}
if len(*tags) > 2 {
if _, ok := (*outbounds)["compatible"]; ok {
delete(*outbounds, "compatible")
for i, tag := range *tags {
if tag == "compatible" {
*tags = append((*tags)[:i], (*tags)[i+1:]...)
break
}
}
}
}
}
go func() {
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for range ticker.C {
res()
}
}()
return res
}
func (p *providerAdapter) AddCompatibleProxy(tags *[]string, outbounds *map[string]adapter.Outbound, router adapter.Router) {
if len(*tags) == 0 {
*tags = append(*tags, "compatible")
(*outbounds)["compatible"] = compatibleProxy
router.AddOutbound("compatible", compatibleProxy)
}
}
func (p *CachedProvider) SetOutbounds(outbounds []adapter.Outbound) {
p.Outbounds = outbounds
}
func (p *CachedProvider) RefeshUpdateTime() {
p.LastUpdate = time.Now()
}
func InjectClashProviderResolver(name string, resolver ProviderResolver) {
providerResolvers[name] = resolver
}
func InitCompatibleProxy(router adapter.Router, logger log.ContextLogger, opts option.DirectOutboundOptions) adapter.Outbound {
var err error
compatibleProxy, err = NewDirect(router, logger, "compatible", opts)
if err != nil {
logger.Panic("cannot create compatible proxy")
}
return compatibleProxy
}

111
outbound/provider/clash.go Normal file
View File

@ -0,0 +1,111 @@
package provider
import (
"context"
"fmt"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/outbound"
"gopkg.in/yaml.v3"
)
var (
clashProxyParsers = make(map[string]func(context.Context, adapter.Router,
log.ContextLogger, map[string]interface{}) (adapter.Outbound, error))
)
type ClashProviderResolver struct {
}
func (r *ClashProviderResolver) GetOutbounds(rawData []byte, ctx context.Context,
router adapter.Router, logger log.ContextLogger) []adapter.Outbound {
data := make(map[string]interface{}, 0)
yaml.Unmarshal(rawData, &data)
outbounds := make([]adapter.Outbound, 0)
proxies, ok := data["proxies"]
if !ok {
return outbounds
}
proxyList, ok := proxies.([]interface{})
if !ok {
return outbounds
}
for _, proxy := range proxyList {
if proxyItem, ok := proxy.(map[string]interface{}); ok {
newOutbound, err := parseClashProxy(ctx, router, logger, proxyItem)
if err == nil {
outbounds = append(outbounds, newOutbound)
}
}
}
return outbounds
}
func parseClashProxy(ctx context.Context, router adapter.Router,
logger log.ContextLogger, proxyItem map[string]interface{}) (res adapter.Outbound, err error) {
defer func() {
if r := recover(); r != nil {
logger.Warn("cannot parse proxy: ", r)
res = nil
err = fmt.Errorf("cannot parse proxy: %v", r)
}
}()
if proxyType, ok := proxyItem["type"].(string); ok {
if parser, ok := clashProxyParsers[proxyType]; ok {
return parser(ctx, router, logger, proxyItem)
}
}
return nil, fmt.Errorf("unkown proxy type")
}
func parseClashSsProxy(ctx context.Context, router adapter.Router,
logger log.ContextLogger, proxyItem map[string]interface{}) (res adapter.Outbound, err error) {
options := option.ShadowsocksOutboundOptions{
DialerOptions: option.DialerOptions{},
ServerOptions: option.ServerOptions{
Server: proxyItem["server"].(string),
ServerPort: uint16(proxyItem["port"].(int))},
Password: proxyItem["password"].(string),
Method: proxyItem["cipher"].(string),
}
if udpEnabled, ok := proxyItem["udp"].(bool); ok && !udpEnabled {
options.Network = "tcp"
}
return outbound.NewShadowsocks(
ctx, router, logger,
proxyItem["name"].(string),
options,
)
}
func ParseClashTrojanProxy(ctx context.Context, router adapter.Router,
logger log.ContextLogger, proxyItem map[string]interface{}) (res adapter.Outbound, err error) {
options := option.TrojanOutboundOptions{
DialerOptions: option.DialerOptions{},
ServerOptions: option.ServerOptions{
Server: proxyItem["server"].(string),
ServerPort: uint16(proxyItem["port"].(int))},
Password: proxyItem["password"].(string),
TLS: &option.OutboundTLSOptions{},
Multiplex: &option.MultiplexOptions{},
Transport: &option.V2RayTransportOptions{},
}
if udpEnabled, ok := proxyItem["udp"].(bool); ok && !udpEnabled {
options.Network = "tcp"
}
if sni, ok := proxyItem["sni"].(string); ok {
options.TLS.ServerName = sni
}
if skipCertVerity, ok := proxyItem["skip-cert-verify"].(bool); ok && skipCertVerity {
options.TLS.Insecure = true
}
return outbound.NewTrojan(ctx, router, logger, proxyItem["name"].(string), options)
}
func init() {
clashProxyParsers["ss"] = parseClashSsProxy
clashProxyParsers["trojan"] = ParseClashTrojanProxy
outbound.InjectClashProviderResolver("clash", &ClashProviderResolver{})
}

View File

@ -20,13 +20,14 @@ var (
type Selector struct { type Selector struct {
myOutboundAdapter myOutboundAdapter
providerAdapter
tags []string tags []string
defaultTag string defaultTag string
outbounds map[string]adapter.Outbound outbounds map[string]adapter.Outbound
selected adapter.Outbound selected adapter.Outbound
} }
func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) { func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions, providers []option.ProviderOutboundOptions) (*Selector, error) {
outbound := &Selector{ outbound := &Selector{
myOutboundAdapter: myOutboundAdapter{ myOutboundAdapter: myOutboundAdapter{
protocol: C.TypeSelector, protocol: C.TypeSelector,
@ -38,6 +39,25 @@ func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, op
defaultTag: options.Default, defaultTag: options.Default,
outbounds: make(map[string]adapter.Outbound), outbounds: make(map[string]adapter.Outbound),
} }
for _, providerOption := range providers {
provider, err := NewProvider(providerOption.Url, providerOption.Filter, providerOption.Interval, ctx, router, logger)
if err != nil {
return nil, err
}
outbound.providers = append(outbound.providers, provider)
go outbound.NewUpdateFunc(&outbound.tags, &outbound.outbounds, router, []func(){
func() {
if outbound.selected != nil {
if _, ok := outbound.outbounds[outbound.selected.Tag()]; ok {
return
}
}
outbound.InitSelected()
}})()
}
if len(outbound.providers) > 0 {
outbound.AddCompatibleProxy(&outbound.tags, &outbound.outbounds, router)
}
if len(outbound.tags) == 0 { if len(outbound.tags) == 0 {
return nil, E.New("missing tags") return nil, E.New("missing tags")
} }
@ -59,7 +79,10 @@ func (s *Selector) Start() error {
} }
s.outbounds[tag] = detour s.outbounds[tag] = detour
} }
return s.InitSelected()
}
func (s *Selector) InitSelected() error {
if s.tag != "" { if s.tag != "" {
if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() { if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() {
selected := clashServer.CacheFile().LoadSelected(s.tag) selected := clashServer.CacheFile().LoadSelected(s.tag)

View File

@ -507,6 +507,13 @@ func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
return outbound, loaded return outbound, loaded
} }
func (r *Router) AddOutbound(tag string, outbound adapter.Outbound) {
if _, loaded := r.outboundByTag[tag]; !loaded {
r.outbounds = append(r.outbounds, outbound)
}
r.outboundByTag[tag] = outbound
}
func (r *Router) DefaultOutbound(network string) adapter.Outbound { func (r *Router) DefaultOutbound(network string) adapter.Outbound {
if network == N.NetworkTCP { if network == N.NetworkTCP {
return r.defaultOutboundForConnection return r.defaultOutboundForConnection