mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-08 11:44:13 +08:00
feat: add simple proxy provider support.
This commit is contained in:
parent
eaf1ace681
commit
cb026e63c1
@ -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
2
box.go
@ -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
3
go.mod
@ -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
2
go.sum
@ -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=
|
||||
|
@ -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
7
option/provider.go
Normal file
@ -0,0 +1,7 @@
|
||||
package option
|
||||
|
||||
type ProviderOutboundOptions struct {
|
||||
Url string `json:"url"`
|
||||
Filter string `json:"filter"`
|
||||
Interval int `json:"interval,omitempty"`
|
||||
}
|
@ -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
180
outbound/provider.go
Normal 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
111
outbound/provider/clash.go
Normal 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{})
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user