Files
sing-box-extended-mirror/route/rule/rule_set_remote.go
T

339 lines
9.3 KiB
Go
Raw Normal View History

2024-10-21 23:38:34 +08:00
package rule
2023-12-01 13:24:12 +08:00
import (
"bytes"
"context"
"crypto/sha256"
2023-12-01 13:24:12 +08:00
"io"
"net/http"
2026-07-24 20:33:13 +08:00
"path/filepath"
2024-02-03 17:45:27 +08:00
"strings"
2024-06-07 15:55:21 +08:00
"sync"
2025-08-25 19:49:12 +08:00
"sync/atomic"
2023-12-01 13:24:12 +08:00
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/deprecated"
2023-12-01 13:24:12 +08:00
"github.com/sagernet/sing-box/option"
2024-06-07 15:55:21 +08:00
"github.com/sagernet/sing/common"
2023-12-01 13:24:12 +08:00
E "github.com/sagernet/sing/common/exceptions"
2024-02-03 17:45:27 +08:00
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json"
2023-12-01 13:24:12 +08:00
"github.com/sagernet/sing/common/logger"
2024-06-07 15:55:21 +08:00
"github.com/sagernet/sing/common/x/list"
2023-12-01 13:24:12 +08:00
"github.com/sagernet/sing/service"
2026-07-24 20:33:13 +08:00
"github.com/sagernet/sing/service/filemanager"
2023-12-01 13:24:12 +08:00
"github.com/sagernet/sing/service/pause"
2024-06-07 15:55:21 +08:00
"go4.org/netipx"
2023-12-01 13:24:12 +08:00
)
var _ adapter.RuleSet = (*RemoteRuleSet)(nil)
type RemoteRuleSet struct {
2025-02-01 19:42:21 +08:00
ctx context.Context
cancel context.CancelFunc
logger logger.ContextLogger
outbound adapter.OutboundManager
2026-07-16 18:57:55 +08:00
tag string
url string
urlHash [32]byte
2026-07-24 20:33:13 +08:00
initialPath string
2025-02-01 19:42:21 +08:00
options option.RuleSet
updateInterval time.Duration
httpClient *http.Client
2025-09-12 18:04:44 +08:00
access sync.RWMutex
2025-02-01 19:42:21 +08:00
rules []adapter.HeadlessRule
2025-09-12 18:04:44 +08:00
metadata adapter.RuleSetMetadata
2025-02-01 19:42:21 +08:00
lastUpdated time.Time
lastEtag string
cacheFile adapter.CacheFile
pauseManager pause.Manager
callbacks list.List[adapter.RuleSetUpdateCallback]
refs atomic.Int32
2023-12-01 13:24:12 +08:00
}
2026-07-16 18:57:55 +08:00
func NewRemoteRuleSet(ctx context.Context, logger logger.ContextLogger, tag string, options option.RuleSet) (*RemoteRuleSet, error) {
2023-12-01 13:24:12 +08:00
ctx, cancel := context.WithCancel(ctx)
var updateInterval time.Duration
if options.RemoteOptions.UpdateInterval > 0 {
updateInterval = time.Duration(options.RemoteOptions.UpdateInterval)
} else {
updateInterval = 24 * time.Hour
}
2026-07-24 20:33:13 +08:00
var initialPath string
if options.RemoteOptions.InitialPath != "" {
initialPath = filemanager.BasePath(ctx, strings.ReplaceAll(options.RemoteOptions.InitialPath, C.RuleSetTagPlaceholder, tag))
initialPath, _ = filepath.Abs(initialPath)
}
url := strings.ReplaceAll(options.RemoteOptions.URL, C.RuleSetTagPlaceholder, tag)
2023-12-01 13:24:12 +08:00
return &RemoteRuleSet{
2025-02-01 19:42:21 +08:00
ctx: ctx,
cancel: cancel,
outbound: service.FromContext[adapter.OutboundManager](ctx),
logger: logger,
2026-07-16 18:57:55 +08:00
tag: tag,
url: url,
urlHash: sha256.Sum256([]byte(url)),
2026-07-24 20:33:13 +08:00
initialPath: initialPath,
2025-02-01 19:42:21 +08:00
options: options,
updateInterval: updateInterval,
pauseManager: service.FromContext[pause.Manager](ctx),
}, nil
2023-12-01 13:24:12 +08:00
}
2024-06-07 15:55:21 +08:00
func (s *RemoteRuleSet) Name() string {
2026-07-16 18:57:55 +08:00
return s.tag
2023-12-01 13:24:12 +08:00
}
2024-02-03 17:45:27 +08:00
func (s *RemoteRuleSet) String() string {
return strings.Join(F.MapToString(s.rules), " ")
}
2024-10-25 22:24:19 +08:00
func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext *adapter.HTTPStartContext) error {
s.cacheFile = service.FromContext[adapter.CacheFile](s.ctx)
transport, err := s.resolveTransport()
if err != nil {
return E.Cause(err, "create rule-set http client")
2023-12-01 13:24:12 +08:00
}
startContext.Register(transport)
s.httpClient = &http.Client{Transport: transport}
2024-10-25 22:24:19 +08:00
if s.cacheFile != nil {
savedSet := s.cacheFile.LoadRuleSet(s.tag)
if savedSet != nil {
if len(savedSet.URLHash) > 0 && !bytes.Equal(savedSet.URLHash, s.urlHash[:]) {
s.logger.Info("cached rule-set was downloaded from another URL, will refetch")
} else {
err = s.loadBytes(savedSet.Content)
if err != nil {
s.logger.Warn(E.Cause(err, "restore cached rule-set, will refetch"))
} else {
s.lastUpdated = savedSet.LastUpdated
s.lastEtag = savedSet.LastEtag
}
2023-12-01 13:24:12 +08:00
}
}
}
2026-07-24 20:33:13 +08:00
var loadedFromInitialPath bool
if s.lastUpdated.IsZero() && s.initialPath != "" {
var content []byte
content, err = filemanager.ReadFile(s.ctx, s.initialPath)
if err == nil {
err = s.loadBytes(content)
}
if err != nil {
s.logger.Warn(E.Cause(err, "load initial rule-set from ", s.initialPath))
} else {
loadedFromInitialPath = true
}
}
if s.lastUpdated.IsZero() && !loadedFromInitialPath {
err = s.fetch(ctx, true)
2023-12-01 13:24:12 +08:00
if err != nil {
2026-07-16 18:57:55 +08:00
return E.Cause(err, "initial rule-set: ", s.tag)
2023-12-01 13:24:12 +08:00
}
}
return nil
}
func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata {
2025-09-12 18:04:44 +08:00
s.access.RLock()
defer s.access.RUnlock()
2023-12-01 13:24:12 +08:00
return s.metadata
}
2024-06-07 15:55:21 +08:00
func (s *RemoteRuleSet) ExtractIPSet() []*netipx.IPSet {
2025-09-12 18:04:44 +08:00
s.access.RLock()
defer s.access.RUnlock()
2024-06-07 15:55:21 +08:00
return common.FlatMap(s.rules, extractIPSetFromRule)
}
func (s *RemoteRuleSet) IncRef() {
s.refs.Add(1)
}
func (s *RemoteRuleSet) DecRef() {
if s.refs.Add(-1) < 0 {
panic("rule-set: negative refs")
}
}
func (s *RemoteRuleSet) Cleanup() {
if s.refs.Load() == 0 {
s.rules = nil
}
}
func (s *RemoteRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
2025-09-12 18:04:44 +08:00
s.access.Lock()
defer s.access.Unlock()
2024-06-07 15:55:21 +08:00
return s.callbacks.PushBack(callback)
}
func (s *RemoteRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) {
2025-09-12 18:04:44 +08:00
s.access.Lock()
defer s.access.Unlock()
2024-06-07 15:55:21 +08:00
s.callbacks.Remove(element)
}
2023-12-01 13:24:12 +08:00
func (s *RemoteRuleSet) loadBytes(content []byte) error {
var (
2024-11-11 16:27:28 +08:00
ruleSet option.PlainRuleSetCompat
err error
2023-12-01 13:24:12 +08:00
)
switch s.options.Format {
2023-12-20 20:00:00 +08:00
case C.RuleSetFormatSource:
2024-11-11 16:27:28 +08:00
ruleSet, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content)
if err != nil {
return err
}
2023-12-01 13:24:12 +08:00
case C.RuleSetFormatBinary:
2024-11-11 16:27:28 +08:00
ruleSet, err = srs.Read(bytes.NewReader(content), false)
2023-12-01 13:24:12 +08:00
if err != nil {
return err
}
default:
2024-06-26 00:45:10 +08:00
return E.New("unknown rule-set format: ", s.options.Format)
2023-12-01 13:24:12 +08:00
}
2024-11-11 16:27:28 +08:00
plainRuleSet, err := ruleSet.Upgrade()
if err != nil {
return err
}
2023-12-01 13:24:12 +08:00
rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules))
for i, ruleOptions := range plainRuleSet.Rules {
2024-11-10 12:11:21 +08:00
rules[i], err = NewHeadlessRule(s.ctx, ruleOptions)
2023-12-01 13:24:12 +08:00
if err != nil {
return E.Cause(err, "parse rule_set.rules.[", i, "]")
}
}
metadata := buildRuleSetMetadata(plainRuleSet.Rules)
2026-07-16 18:57:55 +08:00
err = validateRuleSetMetadataUpdate(s.ctx, s.tag, metadata)
if err != nil {
return err
}
2025-09-12 18:04:44 +08:00
s.access.Lock()
s.metadata = metadata
2023-12-01 13:24:12 +08:00
s.rules = rules
2024-06-07 15:55:21 +08:00
callbacks := s.callbacks.Array()
2025-09-12 18:04:44 +08:00
s.access.Unlock()
2024-06-07 15:55:21 +08:00
for _, callback := range callbacks {
callback(s)
}
2023-12-01 13:24:12 +08:00
return nil
}
2025-04-06 16:35:18 +08:00
func (s *RemoteRuleSet) updateOnce() {
err := s.fetch(s.ctx, false)
2025-04-06 16:35:18 +08:00
if err != nil {
2026-07-16 18:57:55 +08:00
s.logger.Error("fetch rule-set ", s.tag, ": ", err)
2025-04-06 16:35:18 +08:00
} else if s.refs.Load() == 0 {
s.rules = nil
}
}
func (s *RemoteRuleSet) fetch(ctx context.Context, isStart bool) error {
2026-07-16 18:57:55 +08:00
s.logger.Debug("updating rule-set ", s.tag, " from URL: ", s.url)
request, err := http.NewRequest("GET", s.url, nil)
2023-12-01 13:24:12 +08:00
if err != nil {
return err
}
if s.lastEtag != "" {
request.Header.Set("If-None-Match", s.lastEtag)
}
if !isStart {
defer s.httpClient.CloseIdleConnections()
}
response, err := s.httpClient.Do(request.WithContext(ctx))
2023-12-01 13:24:12 +08:00
if err != nil {
return err
}
defer response.Body.Close()
2023-12-01 13:24:12 +08:00
switch response.StatusCode {
case http.StatusOK:
case http.StatusNotModified:
s.lastUpdated = time.Now()
2024-10-25 22:24:19 +08:00
if s.cacheFile != nil {
2026-07-16 18:57:55 +08:00
savedRuleSet := s.cacheFile.LoadRuleSet(s.tag)
2023-12-01 13:24:12 +08:00
if savedRuleSet != nil {
savedRuleSet.LastUpdated = s.lastUpdated
savedRuleSet.URLHash = s.urlHash[:]
2026-07-16 18:57:55 +08:00
err = s.cacheFile.SaveRuleSet(s.tag, savedRuleSet)
2023-12-01 13:24:12 +08:00
if err != nil {
s.logger.Error("save rule-set updated time: ", err)
return nil
}
}
}
2026-07-16 18:57:55 +08:00
s.logger.Info("update rule-set ", s.tag, ": not modified")
2023-12-01 13:24:12 +08:00
return nil
default:
return E.New("unexpected status: ", response.Status)
}
content, err := io.ReadAll(response.Body)
if err != nil {
return err
}
err = s.loadBytes(content)
if err != nil {
return err
}
eTagHeader := response.Header.Get("Etag")
if eTagHeader != "" {
s.lastEtag = eTagHeader
}
s.lastUpdated = time.Now()
2024-10-25 22:24:19 +08:00
if s.cacheFile != nil {
2026-07-16 18:57:55 +08:00
err = s.cacheFile.SaveRuleSet(s.tag, &adapter.SavedBinary{
2023-12-01 13:24:12 +08:00
LastUpdated: s.lastUpdated,
Content: content,
LastEtag: s.lastEtag,
URLHash: s.urlHash[:],
2023-12-01 13:24:12 +08:00
})
if err != nil {
s.logger.Error("save rule-set cache: ", err)
}
}
2026-07-16 18:57:55 +08:00
s.logger.Info("updated rule-set ", s.tag)
2023-12-01 13:24:12 +08:00
return nil
}
func (s *RemoteRuleSet) resolveTransport() (adapter.HTTPTransport, error) {
httpClientManager := service.FromContext[adapter.HTTPClientManager](s.ctx)
if s.options.RemoteOptions.HTTPClient != nil && !s.options.RemoteOptions.HTTPClient.IsEmpty() {
if s.options.RemoteOptions.DownloadDetour != "" { //nolint:staticcheck
return nil, E.New("http_client is conflict with deprecated download_detour field")
}
return httpClientManager.ResolveTransport(s.ctx, s.logger, *s.options.RemoteOptions.HTTPClient)
}
if s.options.RemoteOptions.DownloadDetour != "" { //nolint:staticcheck
deprecated.Report(s.ctx, deprecated.OptionLegacyRuleSetDownloadDetour)
return httpClientManager.ResolveTransport(s.ctx, s.logger, option.HTTPClientOptions{
DialerOptions: option.DialerOptions{
Detour: s.options.RemoteOptions.DownloadDetour, //nolint:staticcheck
},
DisableEmptyDirectCheck: true,
})
}
defaultTransport := httpClientManager.DefaultTransport()
if defaultTransport == nil {
return nil, E.New("default http client transport is not initialized")
}
return defaultTransport, nil
}
2023-12-01 13:24:12 +08:00
func (s *RemoteRuleSet) Close() error {
2024-06-07 15:55:21 +08:00
s.rules = nil
2023-12-01 13:24:12 +08:00
s.cancel()
return nil
}
2024-06-07 15:55:21 +08:00
func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool {
2026-07-23 11:09:48 +08:00
return matchAnyHeadlessRule(s.rules, metadata)
}
2026-07-23 11:09:48 +08:00
func (s *RemoteRuleSet) mergeableRule() *DefaultHeadlessRule {
return mergeableRuleIn(s.rules)
2024-06-07 15:55:21 +08:00
}