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
Outbound(tag string) (Outbound, bool)
AddOutbound(string, Outbound)
DefaultOutbound(network string) Outbound
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/option"
"github.com/sagernet/sing-box/outbound"
_ "github.com/sagernet/sing-box/outbound/provider"
"github.com/sagernet/sing-box/route"
"github.com/sagernet/sing/common"
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)
}
outbounds = append(outbounds, outbound.InitCompatibleProxy(router, logFactory.NewLogger("outbound/provider"), option.Outbound{Type: "direct", Tag: "compatible"}.DirectOptions))
for i, outboundOptions := range options.Outbounds {
var out adapter.Outbound
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/cretz/bine v0.2.0
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/fsnotify/fsnotify v1.5.4
github.com/go-chi/chi/v5 v5.0.7
@ -40,6 +41,7 @@ require (
golang.zx2c4.com/wireguard v0.0.0-20220904105730-b51010ba13f0
google.golang.org/grpc v1.49.0
google.golang.org/protobuf v1.28.1
gopkg.in/yaml.v3 v3.0.1
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c
)
@ -78,6 +80,5 @@ require (
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f // 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
)

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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
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:"-"`
ShadowTLSOptions ShadowTLSOutboundOptions `json:"-"`
SelectorOptions SelectorOutboundOptions `json:"-"`
ProviderOptions []ProviderOutboundOptions `json:"providers,omitempty"`
}
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:
return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions)
case C.TypeSelector:
return NewSelector(router, logger, options.Tag, options.SelectorOptions)
return NewSelector(ctx, router, logger, options.Tag, options.SelectorOptions, options.ProviderOptions)
default:
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 {
myOutboundAdapter
providerAdapter
tags []string
defaultTag string
outbounds map[string]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{
myOutboundAdapter: myOutboundAdapter{
protocol: C.TypeSelector,
@ -38,6 +39,25 @@ func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, op
defaultTag: options.Default,
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 {
return nil, E.New("missing tags")
}
@ -59,7 +79,10 @@ func (s *Selector) Start() error {
}
s.outbounds[tag] = detour
}
return s.InitSelected()
}
func (s *Selector) InitSelected() error {
if s.tag != "" {
if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() {
selected := clashServer.CacheFile().LoadSelected(s.tag)

View File

@ -507,6 +507,13 @@ func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
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 {
if network == N.NetworkTCP {
return r.defaultOutboundForConnection