mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-05-14 00:51:12 +03:00
Add MTProxy, MASQUE, VPN, Link parser. Update AmneziaWG. Remove Tunneling
This commit is contained in:
338
provider/remote/provider.go
Normal file
338
provider/remote/provider.go
Normal file
@@ -0,0 +1,338 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/provider"
|
||||
boxCommon "github.com/sagernet/sing-box/common"
|
||||
"github.com/sagernet/sing-box/common/interrupt"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/parser"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func RegisterProvider(registry *provider.Registry) {
|
||||
provider.Register[option.ProviderRemoteOptions](registry, C.ProviderTypeRemote, NewProviderRemote)
|
||||
}
|
||||
|
||||
var _ adapter.Provider = (*ProviderRemote)(nil)
|
||||
|
||||
type ProviderRemote struct {
|
||||
provider.Adapter
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
logger log.ContextLogger
|
||||
outbound adapter.OutboundManager
|
||||
provider adapter.ProviderManager
|
||||
cacheFile adapter.CacheFile
|
||||
dialer N.Dialer
|
||||
lastEtag string
|
||||
lastOutOpts []option.Outbound
|
||||
lastUpdated time.Time
|
||||
subscriptionInfo adapter.SubscriptionInfo
|
||||
ticker *time.Ticker
|
||||
updating atomic.Bool
|
||||
|
||||
url string
|
||||
userAgent string
|
||||
downloadDetour string
|
||||
updateInterval time.Duration
|
||||
exclude *regexp.Regexp
|
||||
include *regexp.Regexp
|
||||
}
|
||||
|
||||
func NewProviderRemote(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, options option.ProviderRemoteOptions) (adapter.Provider, error) {
|
||||
if options.URL == "" {
|
||||
return nil, E.New("provider URL is required")
|
||||
}
|
||||
updateInterval := time.Duration(options.UpdateInterval)
|
||||
if updateInterval <= 0 {
|
||||
updateInterval = 24 * time.Hour
|
||||
}
|
||||
if updateInterval < time.Minute {
|
||||
updateInterval = time.Minute
|
||||
}
|
||||
var userAgent string
|
||||
if options.UserAgent == "" {
|
||||
userAgent = "sing-box " + C.Version
|
||||
} else {
|
||||
userAgent = options.UserAgent
|
||||
}
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
outbound := service.FromContext[adapter.OutboundManager](ctx)
|
||||
logger := logFactory.NewLogger(F.ToString("provider/remote", "[", tag, "]"))
|
||||
updateChan := make(chan struct{})
|
||||
close(updateChan)
|
||||
return &ProviderRemote{
|
||||
Adapter: provider.NewAdapter(ctx, router, outbound, logFactory, logger, tag, C.ProviderTypeRemote, options.HealthCheck),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
logger: logger,
|
||||
outbound: outbound,
|
||||
provider: service.FromContext[adapter.ProviderManager](ctx),
|
||||
|
||||
url: options.URL,
|
||||
userAgent: userAgent,
|
||||
downloadDetour: options.DownloadDetour,
|
||||
updateInterval: updateInterval,
|
||||
exclude: (*regexp.Regexp)(options.Exclude),
|
||||
include: (*regexp.Regexp)(options.Include),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ProviderRemote) Start() error {
|
||||
s.cacheFile = service.FromContext[adapter.CacheFile](s.ctx)
|
||||
if s.cacheFile != nil {
|
||||
if saveSub := s.cacheFile.LoadSubscription(s.Tag()); saveSub != nil {
|
||||
content, _ := boxCommon.DecodeBase64URLSafe(string(saveSub.Content))
|
||||
firstLine, others := getFirstLine(content)
|
||||
if info, ok := parseInfo(firstLine); ok {
|
||||
s.subscriptionInfo = info
|
||||
content, _ = boxCommon.DecodeBase64URLSafe(others)
|
||||
}
|
||||
if err := s.updateProviderFromContent(content); err != nil {
|
||||
return E.Cause(err, "restore cached outbound provider")
|
||||
}
|
||||
s.UpdateGroups()
|
||||
s.lastUpdated, s.lastEtag = saveSub.LastUpdated, saveSub.LastEtag
|
||||
}
|
||||
}
|
||||
if s.downloadDetour != "" {
|
||||
outbound, loaded := s.outbound.Outbound(s.downloadDetour)
|
||||
if !loaded {
|
||||
return E.New("detour outbound not found: ", s.downloadDetour)
|
||||
}
|
||||
s.dialer = outbound
|
||||
} else {
|
||||
s.dialer = s.outbound.Default()
|
||||
}
|
||||
|
||||
go s.loopUpdate()
|
||||
return s.Adapter.Start()
|
||||
}
|
||||
|
||||
func (s *ProviderRemote) Update() error {
|
||||
if s.ticker != nil {
|
||||
s.ticker.Reset(s.updateInterval)
|
||||
}
|
||||
ctx := interrupt.ContextWithIsProviderConnection(s.ctx)
|
||||
return s.fetch(ctx)
|
||||
}
|
||||
|
||||
func (s *ProviderRemote) UpdatedAt() time.Time {
|
||||
return s.lastUpdated
|
||||
}
|
||||
|
||||
func (s *ProviderRemote) SubscriptionInfo() adapter.SubscriptionInfo {
|
||||
return s.subscriptionInfo
|
||||
}
|
||||
|
||||
func (s *ProviderRemote) Close() error {
|
||||
s.cancel()
|
||||
if s.ticker != nil {
|
||||
s.ticker.Stop()
|
||||
}
|
||||
return common.Close(&s.Adapter)
|
||||
}
|
||||
|
||||
func (s *ProviderRemote) updateOnce() {
|
||||
ctx := interrupt.ContextWithIsProviderConnection(s.ctx)
|
||||
if err := s.fetch(ctx); err != nil {
|
||||
s.logger.Error("update outbound provider: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ProviderRemote) fetch(ctx context.Context) error {
|
||||
if s.updating.Swap(true) {
|
||||
return E.New("provider is updating")
|
||||
}
|
||||
defer s.updating.Store(false)
|
||||
s.logger.Debug("updating outbound provider ", s.Tag(), " from URL: ", s.url)
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
ForceAttemptHTTP2: true,
|
||||
TLSHandshakeTimeout: C.TCPTimeout,
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
},
|
||||
TLSClientConfig: &tls.Config{
|
||||
Time: ntp.TimeFuncFromContext(ctx),
|
||||
RootCAs: adapter.RootPoolFromContext(ctx),
|
||||
},
|
||||
},
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, s.url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.lastEtag != "" {
|
||||
req.Header.Set("If-None-Match", s.lastEtag)
|
||||
}
|
||||
req.Header.Set("User-Agent", s.userAgent)
|
||||
resp, err := client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
infoStr := resp.Header.Get("subscription-userinfo")
|
||||
info, hasInfo := parseInfo(infoStr)
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
case http.StatusNotModified:
|
||||
s.subscriptionInfo = info
|
||||
s.lastUpdated = time.Now()
|
||||
if s.cacheFile != nil {
|
||||
saveSub := s.cacheFile.LoadSubscription(s.Tag())
|
||||
if saveSub != nil {
|
||||
if hasInfo {
|
||||
index := bytes.IndexByte(saveSub.Content, '\n')
|
||||
if index != -1 {
|
||||
saveSub.Content = append([]byte(infoStr+"\n"), saveSub.Content[index+1:]...)
|
||||
}
|
||||
}
|
||||
saveSub.LastUpdated = s.lastUpdated
|
||||
err := s.cacheFile.SaveSubscription(s.Tag(), saveSub)
|
||||
if err != nil {
|
||||
s.logger.Error("save outbound provider cache file: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
s.logger.Info("update outbound provider ", s.Tag(), ": not modified")
|
||||
return nil
|
||||
default:
|
||||
return E.New("unexpected status: ", resp.Status)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
contentRaw, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
eTagHeader := resp.Header.Get("Etag")
|
||||
if eTagHeader != "" {
|
||||
s.lastEtag = eTagHeader
|
||||
}
|
||||
content, _ := boxCommon.DecodeBase64URLSafe(string(contentRaw))
|
||||
if !hasInfo {
|
||||
firstLine, others := getFirstLine(content)
|
||||
if info, hasInfo = parseInfo(firstLine); hasInfo {
|
||||
infoStr = firstLine
|
||||
content, _ = boxCommon.DecodeBase64URLSafe(others)
|
||||
}
|
||||
}
|
||||
if err := s.updateProviderFromContent(content); err != nil {
|
||||
return err
|
||||
}
|
||||
s.UpdateGroups()
|
||||
s.subscriptionInfo = info
|
||||
s.lastUpdated = time.Now()
|
||||
if s.cacheFile != nil {
|
||||
content, _ := json.Marshal(option.Options{
|
||||
Outbounds: s.lastOutOpts,
|
||||
})
|
||||
if hasInfo {
|
||||
content = append([]byte(infoStr+"\n"), content...)
|
||||
}
|
||||
err = s.cacheFile.SaveSubscription(s.Tag(), &adapter.SavedBinary{
|
||||
LastUpdated: s.lastUpdated,
|
||||
Content: content,
|
||||
LastEtag: s.lastEtag,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("save outbound provider cache file: ", err)
|
||||
}
|
||||
}
|
||||
s.logger.Info("updated outbound provider ", s.Tag())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ProviderRemote) loopUpdate() {
|
||||
if time.Since(s.lastUpdated) < s.updateInterval {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-time.After(time.Until(s.lastUpdated.Add(s.updateInterval))):
|
||||
s.updateOnce()
|
||||
}
|
||||
} else {
|
||||
s.updateOnce()
|
||||
}
|
||||
s.ticker = time.NewTicker(s.updateInterval)
|
||||
for {
|
||||
runtime.GC()
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-s.ticker.C:
|
||||
s.updateOnce()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ProviderRemote) updateProviderFromContent(content string) error {
|
||||
outboundOpts, err := parser.ParseSubscription(s.ctx, content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outboundOpts = common.Filter(outboundOpts, func(it option.Outbound) bool {
|
||||
return (s.exclude == nil || !s.exclude.MatchString(it.Tag)) && (s.include == nil || s.include.MatchString(it.Tag))
|
||||
})
|
||||
s.UpdateOutbounds(s.lastOutOpts, outboundOpts)
|
||||
s.lastOutOpts = outboundOpts
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFirstLine(content string) (string, string) {
|
||||
lines := strings.Split(content, "\n")
|
||||
if len(lines) == 1 {
|
||||
return lines[0], ""
|
||||
}
|
||||
others := strings.Join(lines[1:], "\n")
|
||||
return lines[0], others
|
||||
}
|
||||
|
||||
func parseInfo(infoStr string) (adapter.SubscriptionInfo, bool) {
|
||||
info := adapter.SubscriptionInfo{}
|
||||
if infoStr == "" {
|
||||
return info, false
|
||||
}
|
||||
reg := regexp.MustCompile(`(upload|download|total|expire)[\s\t]*=[\s\t]*(-?\d*);?`)
|
||||
matches := reg.FindAllStringSubmatch(infoStr, 4)
|
||||
if len(matches) == 0 {
|
||||
return info, false
|
||||
}
|
||||
for _, match := range matches {
|
||||
key, value := match[1], match[2]
|
||||
switch key {
|
||||
case "upload":
|
||||
info.Upload = boxCommon.StringToType[int64](value)
|
||||
case "download":
|
||||
info.Download = boxCommon.StringToType[int64](value)
|
||||
case "total":
|
||||
info.Total = boxCommon.StringToType[int64](value)
|
||||
case "expire":
|
||||
info.Expire = boxCommon.StringToType[int64](value)
|
||||
default:
|
||||
return info, false
|
||||
}
|
||||
}
|
||||
return info, true
|
||||
}
|
||||
Reference in New Issue
Block a user