From 07dba74657ab8ad6a69417327fbb1c698bb3e3d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 24 Jul 2026 20:33:13 +0800 Subject: [PATCH] Add initial_path option to remote rule-sets --- docs/configuration/rule-set/index.md | 16 ++++++++++++++-- docs/configuration/rule-set/index.zh.md | 15 +++++++++++++-- docs/schema.json | 3 +++ option/rule_set.go | 4 ++++ route/rule/rule_set_remote.go | 24 +++++++++++++++++++++++- 5 files changed, 57 insertions(+), 5 deletions(-) diff --git a/docs/configuration/rule-set/index.md b/docs/configuration/rule-set/index.md index 851cab6f6..1d4ca9427 100644 --- a/docs/configuration/rule-set/index.md +++ b/docs/configuration/rule-set/index.md @@ -2,7 +2,8 @@ :material-plus: [http_client](#http_client) :material-delete-clock: [download_detour](#download_detour) - :material-alert: [tag](#tag) + :material-alert: [tag](#tag) + :material-plus: [initial_path](#initial_path) !!! quote "Changes in sing-box 1.10.0" @@ -49,6 +50,7 @@ "tag": "", // or [] "format": "source", // or binary "url": "", + "initial_path": "", "http_client": "", // or {} "update_interval": "", @@ -76,7 +78,7 @@ Tag of rule-set. `tag` also accepts a list of tags to define multiple rule-sets sharing other options at once. - The `{tag}` placeholder in `path` or `url` is replaced by each tag, + The `{tag}` placeholder in `path`, `url` or `initial_path` is replaced by each tag, and is required when multiple tags are set. Multiple tags conflict with `type: inline`. @@ -121,6 +123,16 @@ File path of rule-set. Download URL of rule-set. +#### initial_path + +!!! question "Since sing-box 1.14.0" + +File path of the initial rule-set content. + +Read once at startup when no cached rule-set is available, so startup is not +blocked by the initial download. The rule-set is still updated in the background +immediately after startup. + #### http_client !!! question "Since sing-box 1.14.0" diff --git a/docs/configuration/rule-set/index.zh.md b/docs/configuration/rule-set/index.zh.md index e1704087e..69cedf1a9 100644 --- a/docs/configuration/rule-set/index.zh.md +++ b/docs/configuration/rule-set/index.zh.md @@ -2,7 +2,8 @@ :material-plus: [http_client](#http_client) :material-delete-clock: [download_detour](#download_detour) - :material-alert: [tag](#tag) + :material-alert: [tag](#tag) + :material-plus: [initial_path](#initial_path) !!! quote "sing-box 1.10.0 中的更改" @@ -49,6 +50,7 @@ "tag": "", // 或 [] "format": "source", // or binary "url": "", + "initial_path": "", "http_client": "", // 或 {} "update_interval": "", @@ -76,7 +78,7 @@ `tag` 也接受一组标签,用于一次定义多个共享其他选项的规则集。 - `path` 或 `url` 中的 `{tag}` 占位符将被替换为每个标签,设置多个标签时必填。 + `path`、`url` 或 `initial_path` 中的 `{tag}` 占位符将被替换为每个标签,设置多个标签时必填。 多个标签与 `type: inline` 冲突。 @@ -120,6 +122,15 @@ 规则集的下载 URL。 +#### initial_path + +!!! question "自 sing-box 1.14.0 起" + +规则集初始内容的文件路径。 + +仅在启动时没有可用的规则集缓存时读取一次,使启动不被初始下载阻塞。 +启动后规则集仍会立即在后台更新。 + #### http_client !!! question "自 sing-box 1.14.0 起" diff --git a/docs/schema.json b/docs/schema.json index ce381e595..e11edb7e0 100644 --- a/docs/schema.json +++ b/docs/schema.json @@ -15327,6 +15327,9 @@ "url": { "type": "string" }, + "initial_path": { + "type": "string" + }, "http_client": { "$ref": "#/$defs/HTTPClientReference" }, diff --git a/option/rule_set.go b/option/rule_set.go index 7f75cdd64..8eb1dfdf3 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -113,6 +113,9 @@ func (r *RuleSet) UnmarshalJSON(bytes []byte) error { if !strings.Contains(r.RemoteOptions.URL, C.RuleSetTagPlaceholder) { return E.New("missing ", C.RuleSetTagPlaceholder, " placeholder in url") } + if r.RemoteOptions.InitialPath != "" && !strings.Contains(r.RemoteOptions.InitialPath, C.RuleSetTagPlaceholder) { + return E.New("missing ", C.RuleSetTagPlaceholder, " placeholder in initial_path") + } } } return nil @@ -177,6 +180,7 @@ type LocalRuleSet struct { type RemoteRuleSet struct { URL string `json:"url"` + InitialPath string `json:"initial_path,omitempty"` HTTPClient *HTTPClientOptions `json:"http_client,omitempty"` UpdateInterval badoption.Duration `json:"update_interval,omitempty"` // Deprecated: use http_client instead diff --git a/route/rule/rule_set_remote.go b/route/rule/rule_set_remote.go index 56a3ea53c..64f6c8eea 100644 --- a/route/rule/rule_set_remote.go +++ b/route/rule/rule_set_remote.go @@ -5,6 +5,7 @@ import ( "context" "io" "net/http" + "path/filepath" "strings" "sync" "sync/atomic" @@ -22,6 +23,7 @@ import ( "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/filemanager" "github.com/sagernet/sing/service/pause" "go4.org/netipx" @@ -36,6 +38,7 @@ type RemoteRuleSet struct { outbound adapter.OutboundManager tag string url string + initialPath string options option.RuleSet updateInterval time.Duration httpClient *http.Client @@ -58,6 +61,11 @@ func NewRemoteRuleSet(ctx context.Context, logger logger.ContextLogger, tag stri } else { updateInterval = 24 * time.Hour } + var initialPath string + if options.RemoteOptions.InitialPath != "" { + initialPath = filemanager.BasePath(ctx, strings.ReplaceAll(options.RemoteOptions.InitialPath, C.RuleSetTagPlaceholder, tag)) + initialPath, _ = filepath.Abs(initialPath) + } return &RemoteRuleSet{ ctx: ctx, cancel: cancel, @@ -65,6 +73,7 @@ func NewRemoteRuleSet(ctx context.Context, logger logger.ContextLogger, tag stri logger: logger, tag: tag, url: strings.ReplaceAll(options.RemoteOptions.URL, C.RuleSetTagPlaceholder, tag), + initialPath: initialPath, options: options, updateInterval: updateInterval, pauseManager: service.FromContext[pause.Manager](ctx), @@ -98,7 +107,20 @@ func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext *adapter. } } } - if s.lastUpdated.IsZero() { + 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) if err != nil { return E.Cause(err, "initial rule-set: ", s.tag)