github.com/sagernet/sing-box@v1.9.0-rc.20/route/rule_set_remote.go (about)

     1  package route
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"io"
     7  	"net"
     8  	"net/http"
     9  	"runtime"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/sagernet/sing-box/adapter"
    14  	"github.com/sagernet/sing-box/common/srs"
    15  	C "github.com/sagernet/sing-box/constant"
    16  	"github.com/sagernet/sing-box/option"
    17  	E "github.com/sagernet/sing/common/exceptions"
    18  	F "github.com/sagernet/sing/common/format"
    19  	"github.com/sagernet/sing/common/json"
    20  	"github.com/sagernet/sing/common/logger"
    21  	M "github.com/sagernet/sing/common/metadata"
    22  	N "github.com/sagernet/sing/common/network"
    23  	"github.com/sagernet/sing/service"
    24  	"github.com/sagernet/sing/service/pause"
    25  )
    26  
    27  var _ adapter.RuleSet = (*RemoteRuleSet)(nil)
    28  
    29  type RemoteRuleSet struct {
    30  	ctx            context.Context
    31  	cancel         context.CancelFunc
    32  	router         adapter.Router
    33  	logger         logger.ContextLogger
    34  	options        option.RuleSet
    35  	metadata       adapter.RuleSetMetadata
    36  	updateInterval time.Duration
    37  	dialer         N.Dialer
    38  	rules          []adapter.HeadlessRule
    39  	lastUpdated    time.Time
    40  	lastEtag       string
    41  	updateTicker   *time.Ticker
    42  	pauseManager   pause.Manager
    43  }
    44  
    45  func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet {
    46  	ctx, cancel := context.WithCancel(ctx)
    47  	var updateInterval time.Duration
    48  	if options.RemoteOptions.UpdateInterval > 0 {
    49  		updateInterval = time.Duration(options.RemoteOptions.UpdateInterval)
    50  	} else {
    51  		updateInterval = 24 * time.Hour
    52  	}
    53  	return &RemoteRuleSet{
    54  		ctx:            ctx,
    55  		cancel:         cancel,
    56  		router:         router,
    57  		logger:         logger,
    58  		options:        options,
    59  		updateInterval: updateInterval,
    60  		pauseManager:   service.FromContext[pause.Manager](ctx),
    61  	}
    62  }
    63  
    64  func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool {
    65  	for _, rule := range s.rules {
    66  		if rule.Match(metadata) {
    67  			return true
    68  		}
    69  	}
    70  	return false
    71  }
    72  
    73  func (s *RemoteRuleSet) String() string {
    74  	return strings.Join(F.MapToString(s.rules), " ")
    75  }
    76  
    77  func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error {
    78  	var dialer N.Dialer
    79  	if s.options.RemoteOptions.DownloadDetour != "" {
    80  		outbound, loaded := s.router.Outbound(s.options.RemoteOptions.DownloadDetour)
    81  		if !loaded {
    82  			return E.New("download_detour not found: ", s.options.RemoteOptions.DownloadDetour)
    83  		}
    84  		dialer = outbound
    85  	} else {
    86  		outbound, err := s.router.DefaultOutbound(N.NetworkTCP)
    87  		if err != nil {
    88  			return err
    89  		}
    90  		dialer = outbound
    91  	}
    92  	s.dialer = dialer
    93  	cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
    94  	if cacheFile != nil {
    95  		if savedSet := cacheFile.LoadRuleSet(s.options.Tag); savedSet != nil {
    96  			err := s.loadBytes(savedSet.Content)
    97  			if err != nil {
    98  				return E.Cause(err, "restore cached rule-set")
    99  			}
   100  			s.lastUpdated = savedSet.LastUpdated
   101  			s.lastEtag = savedSet.LastEtag
   102  		}
   103  	}
   104  	if s.lastUpdated.IsZero() {
   105  		err := s.fetchOnce(ctx, startContext)
   106  		if err != nil {
   107  			return E.Cause(err, "initial rule-set: ", s.options.Tag)
   108  		}
   109  	}
   110  	s.updateTicker = time.NewTicker(s.updateInterval)
   111  	go s.loopUpdate()
   112  	return nil
   113  }
   114  
   115  func (s *RemoteRuleSet) PostStart() error {
   116  	if s.lastUpdated.IsZero() {
   117  		err := s.fetchOnce(s.ctx, nil)
   118  		if err != nil {
   119  			s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
   120  		}
   121  	}
   122  	return nil
   123  }
   124  
   125  func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata {
   126  	return s.metadata
   127  }
   128  
   129  func (s *RemoteRuleSet) loadBytes(content []byte) error {
   130  	var (
   131  		plainRuleSet option.PlainRuleSet
   132  		err          error
   133  	)
   134  	switch s.options.Format {
   135  	case C.RuleSetFormatSource:
   136  		var compat option.PlainRuleSetCompat
   137  		compat, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content)
   138  		if err != nil {
   139  			return err
   140  		}
   141  		plainRuleSet = compat.Upgrade()
   142  	case C.RuleSetFormatBinary:
   143  		plainRuleSet, err = srs.Read(bytes.NewReader(content), false)
   144  		if err != nil {
   145  			return err
   146  		}
   147  	default:
   148  		return E.New("unknown rule set format: ", s.options.Format)
   149  	}
   150  	rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules))
   151  	for i, ruleOptions := range plainRuleSet.Rules {
   152  		rules[i], err = NewHeadlessRule(s.router, ruleOptions)
   153  		if err != nil {
   154  			return E.Cause(err, "parse rule_set.rules.[", i, "]")
   155  		}
   156  	}
   157  	s.metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
   158  	s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
   159  	s.metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule)
   160  	s.rules = rules
   161  	return nil
   162  }
   163  
   164  func (s *RemoteRuleSet) loopUpdate() {
   165  	if time.Since(s.lastUpdated) > s.updateInterval {
   166  		err := s.fetchOnce(s.ctx, nil)
   167  		if err != nil {
   168  			s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
   169  		}
   170  	}
   171  	for {
   172  		runtime.GC()
   173  		select {
   174  		case <-s.ctx.Done():
   175  			return
   176  		case <-s.updateTicker.C:
   177  			s.pauseManager.WaitActive()
   178  			err := s.fetchOnce(s.ctx, nil)
   179  			if err != nil {
   180  				s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
   181  			}
   182  		}
   183  	}
   184  }
   185  
   186  func (s *RemoteRuleSet) fetchOnce(ctx context.Context, startContext adapter.RuleSetStartContext) error {
   187  	s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL)
   188  	var httpClient *http.Client
   189  	if startContext != nil {
   190  		httpClient = startContext.HTTPClient(s.options.RemoteOptions.DownloadDetour, s.dialer)
   191  	} else {
   192  		httpClient = &http.Client{
   193  			Transport: &http.Transport{
   194  				ForceAttemptHTTP2:   true,
   195  				TLSHandshakeTimeout: C.TCPTimeout,
   196  				DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
   197  					return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
   198  				},
   199  			},
   200  		}
   201  	}
   202  	request, err := http.NewRequest("GET", s.options.RemoteOptions.URL, nil)
   203  	if err != nil {
   204  		return err
   205  	}
   206  	if s.lastEtag != "" {
   207  		request.Header.Set("If-None-Match", s.lastEtag)
   208  	}
   209  	response, err := httpClient.Do(request.WithContext(ctx))
   210  	if err != nil {
   211  		return err
   212  	}
   213  	switch response.StatusCode {
   214  	case http.StatusOK:
   215  	case http.StatusNotModified:
   216  		s.lastUpdated = time.Now()
   217  		cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
   218  		if cacheFile != nil {
   219  			savedRuleSet := cacheFile.LoadRuleSet(s.options.Tag)
   220  			if savedRuleSet != nil {
   221  				savedRuleSet.LastUpdated = s.lastUpdated
   222  				err = cacheFile.SaveRuleSet(s.options.Tag, savedRuleSet)
   223  				if err != nil {
   224  					s.logger.Error("save rule-set updated time: ", err)
   225  					return nil
   226  				}
   227  			}
   228  		}
   229  		s.logger.Info("update rule-set ", s.options.Tag, ": not modified")
   230  		return nil
   231  	default:
   232  		return E.New("unexpected status: ", response.Status)
   233  	}
   234  	content, err := io.ReadAll(response.Body)
   235  	if err != nil {
   236  		response.Body.Close()
   237  		return err
   238  	}
   239  	err = s.loadBytes(content)
   240  	if err != nil {
   241  		response.Body.Close()
   242  		return err
   243  	}
   244  	response.Body.Close()
   245  	eTagHeader := response.Header.Get("Etag")
   246  	if eTagHeader != "" {
   247  		s.lastEtag = eTagHeader
   248  	}
   249  	s.lastUpdated = time.Now()
   250  	cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
   251  	if cacheFile != nil {
   252  		err = cacheFile.SaveRuleSet(s.options.Tag, &adapter.SavedRuleSet{
   253  			LastUpdated: s.lastUpdated,
   254  			Content:     content,
   255  			LastEtag:    s.lastEtag,
   256  		})
   257  		if err != nil {
   258  			s.logger.Error("save rule-set cache: ", err)
   259  		}
   260  	}
   261  	s.logger.Info("updated rule-set ", s.options.Tag)
   262  	return nil
   263  }
   264  
   265  func (s *RemoteRuleSet) Close() error {
   266  	s.updateTicker.Stop()
   267  	s.cancel()
   268  	return nil
   269  }