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 }