Add multiple tags support to rule-sets
This commit is contained in:
@@ -18,6 +18,8 @@ const (
|
||||
RuleSetFormatBinary = "binary"
|
||||
)
|
||||
|
||||
const RuleSetTagPlaceholder = "{tag}"
|
||||
|
||||
const (
|
||||
RuleSetVersion1 = 1 + iota
|
||||
RuleSetVersion2
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [http_client](#http_client)
|
||||
:material-delete-clock: [download_detour](#download_detour)
|
||||
:material-delete-clock: [download_detour](#download_detour)
|
||||
:material-alert: [tag](#tag)
|
||||
|
||||
!!! quote "Changes in sing-box 1.10.0"
|
||||
|
||||
@@ -30,7 +31,7 @@
|
||||
```json
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "",
|
||||
"tag": "", // or []
|
||||
"format": "source", // or binary
|
||||
"path": ""
|
||||
}
|
||||
@@ -45,7 +46,7 @@
|
||||
```json
|
||||
{
|
||||
"type": "remote",
|
||||
"tag": "",
|
||||
"tag": "", // or []
|
||||
"format": "source", // or binary
|
||||
"url": "",
|
||||
"http_client": "", // or {}
|
||||
@@ -71,6 +72,15 @@ Type of rule-set, `local` or `remote`.
|
||||
|
||||
Tag of rule-set.
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
`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,
|
||||
and is required when multiple tags are set.
|
||||
|
||||
Multiple tags conflict with `type: inline`.
|
||||
|
||||
### Inline Fields
|
||||
|
||||
!!! question "Since sing-box 1.10.0"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [http_client](#http_client)
|
||||
:material-delete-clock: [download_detour](#download_detour)
|
||||
:material-delete-clock: [download_detour](#download_detour)
|
||||
:material-alert: [tag](#tag)
|
||||
|
||||
!!! quote "sing-box 1.10.0 中的更改"
|
||||
|
||||
@@ -30,7 +31,7 @@
|
||||
```json
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "",
|
||||
"tag": "", // 或 []
|
||||
"format": "source", // or binary
|
||||
"path": ""
|
||||
}
|
||||
@@ -45,7 +46,7 @@
|
||||
```json
|
||||
{
|
||||
"type": "remote",
|
||||
"tag": "",
|
||||
"tag": "", // 或 []
|
||||
"format": "source", // or binary
|
||||
"url": "",
|
||||
"http_client": "", // 或 {}
|
||||
@@ -71,6 +72,14 @@
|
||||
|
||||
规则集的标签。
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
`tag` 也接受一组标签,用于一次定义多个共享其他选项的规则集。
|
||||
|
||||
`path` 或 `url` 中的 `{tag}` 占位符将被替换为每个标签,设置多个标签时必填。
|
||||
|
||||
多个标签与 `type: inline` 冲突。
|
||||
|
||||
### 内联字段
|
||||
|
||||
!!! question "自 sing-box 1.10.0 起"
|
||||
|
||||
+22
-7
@@ -4,6 +4,7 @@ import (
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common"
|
||||
@@ -18,12 +19,12 @@ import (
|
||||
)
|
||||
|
||||
type _RuleSet struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Tag string `json:"tag"`
|
||||
Format string `json:"format,omitempty"`
|
||||
InlineOptions PlainRuleSet `json:"-"`
|
||||
LocalOptions LocalRuleSet `json:"-"`
|
||||
RemoteOptions RemoteRuleSet `json:"-"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Tag badoption.Listable[string] `json:"tag"`
|
||||
Format string `json:"format,omitempty"`
|
||||
InlineOptions PlainRuleSet `json:"-"`
|
||||
LocalOptions LocalRuleSet `json:"-"`
|
||||
RemoteOptions RemoteRuleSet `json:"-"`
|
||||
}
|
||||
|
||||
type RuleSet _RuleSet
|
||||
@@ -61,7 +62,7 @@ func (r *RuleSet) UnmarshalJSON(bytes []byte) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Tag == "" {
|
||||
if len(r.Tag) == 0 || common.Any(r.Tag, func(tag string) bool { return tag == "" }) {
|
||||
return E.New("missing tag")
|
||||
}
|
||||
var v any
|
||||
@@ -99,6 +100,20 @@ func (r *RuleSet) UnmarshalJSON(bytes []byte) error {
|
||||
} else {
|
||||
r.Format = ""
|
||||
}
|
||||
if len(r.Tag) > 1 {
|
||||
switch r.Type {
|
||||
case C.RuleSetTypeInline:
|
||||
return E.New("inline rule-set does not support multiple tags")
|
||||
case C.RuleSetTypeLocal:
|
||||
if !strings.Contains(r.LocalOptions.Path, C.RuleSetTagPlaceholder) {
|
||||
return E.New("missing ", C.RuleSetTagPlaceholder, " placeholder in path")
|
||||
}
|
||||
case C.RuleSetTypeRemote:
|
||||
if !strings.Contains(r.RemoteOptions.URL, C.RuleSetTagPlaceholder) {
|
||||
return E.New("missing ", C.RuleSetTagPlaceholder, " placeholder in url")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+10
-8
@@ -84,15 +84,17 @@ func (r *Router) Initialize(rules []option.Rule, ruleSets []option.RuleSet) erro
|
||||
r.rules = append(r.rules, rule)
|
||||
}
|
||||
for i, options := range ruleSets {
|
||||
if _, exists := r.ruleSetMap[options.Tag]; exists {
|
||||
return E.New("duplicate rule-set tag: ", options.Tag)
|
||||
for _, tag := range options.Tag {
|
||||
if _, exists := r.ruleSetMap[tag]; exists {
|
||||
return E.New("duplicate rule-set tag: ", tag)
|
||||
}
|
||||
ruleSet, err := R.NewRuleSet(r.ctx, r.logger, tag, options)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse rule-set[", i, "]")
|
||||
}
|
||||
r.ruleSets = append(r.ruleSets, ruleSet)
|
||||
r.ruleSetMap[tag] = ruleSet
|
||||
}
|
||||
ruleSet, err := R.NewRuleSet(r.ctx, r.logger, options)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse rule-set[", i, "]")
|
||||
}
|
||||
r.ruleSets = append(r.ruleSets, ruleSet)
|
||||
r.ruleSetMap[options.Tag] = ruleSet
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,12 +15,12 @@ import (
|
||||
"go4.org/netipx"
|
||||
)
|
||||
|
||||
func NewRuleSet(ctx context.Context, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) {
|
||||
func NewRuleSet(ctx context.Context, logger logger.ContextLogger, tag string, options option.RuleSet) (adapter.RuleSet, error) {
|
||||
switch options.Type {
|
||||
case C.RuleSetTypeInline, C.RuleSetTypeLocal, "":
|
||||
return NewLocalRuleSet(ctx, logger, options)
|
||||
return NewLocalRuleSet(ctx, logger, tag, options)
|
||||
case C.RuleSetTypeRemote:
|
||||
return NewRemoteRuleSet(ctx, logger, options)
|
||||
return NewRemoteRuleSet(ctx, logger, tag, options)
|
||||
default:
|
||||
return nil, E.New("unknown rule-set type: ", options.Type)
|
||||
}
|
||||
|
||||
@@ -38,11 +38,11 @@ type LocalRuleSet struct {
|
||||
refs atomic.Int32
|
||||
}
|
||||
|
||||
func NewLocalRuleSet(ctx context.Context, logger logger.Logger, options option.RuleSet) (*LocalRuleSet, error) {
|
||||
func NewLocalRuleSet(ctx context.Context, logger logger.Logger, tag string, options option.RuleSet) (*LocalRuleSet, error) {
|
||||
ruleSet := &LocalRuleSet{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
tag: options.Tag,
|
||||
tag: tag,
|
||||
fileFormat: options.Format,
|
||||
}
|
||||
if options.Type == C.RuleSetTypeInline {
|
||||
@@ -54,7 +54,7 @@ func NewLocalRuleSet(ctx context.Context, logger logger.Logger, options option.R
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
filePath := filemanager.BasePath(ctx, options.LocalOptions.Path)
|
||||
filePath := filemanager.BasePath(ctx, strings.ReplaceAll(options.LocalOptions.Path, C.RuleSetTagPlaceholder, tag))
|
||||
filePath, _ = filepath.Abs(filePath)
|
||||
err := ruleSet.reloadFile(filePath)
|
||||
if err != nil {
|
||||
@@ -65,7 +65,7 @@ func NewLocalRuleSet(ctx context.Context, logger logger.Logger, options option.R
|
||||
Callback: func(path string) {
|
||||
uErr := ruleSet.reloadFile(path)
|
||||
if uErr != nil {
|
||||
logger.Error(E.Cause(uErr, "reload rule-set ", options.Tag))
|
||||
logger.Error(E.Cause(uErr, "reload rule-set ", tag))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -34,6 +34,8 @@ type RemoteRuleSet struct {
|
||||
cancel context.CancelFunc
|
||||
logger logger.ContextLogger
|
||||
outbound adapter.OutboundManager
|
||||
tag string
|
||||
url string
|
||||
options option.RuleSet
|
||||
updateInterval time.Duration
|
||||
httpClient *http.Client
|
||||
@@ -48,7 +50,7 @@ type RemoteRuleSet struct {
|
||||
refs atomic.Int32
|
||||
}
|
||||
|
||||
func NewRemoteRuleSet(ctx context.Context, logger logger.ContextLogger, options option.RuleSet) (*RemoteRuleSet, error) {
|
||||
func NewRemoteRuleSet(ctx context.Context, logger logger.ContextLogger, tag string, options option.RuleSet) (*RemoteRuleSet, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
var updateInterval time.Duration
|
||||
if options.RemoteOptions.UpdateInterval > 0 {
|
||||
@@ -61,6 +63,8 @@ func NewRemoteRuleSet(ctx context.Context, logger logger.ContextLogger, options
|
||||
cancel: cancel,
|
||||
outbound: service.FromContext[adapter.OutboundManager](ctx),
|
||||
logger: logger,
|
||||
tag: tag,
|
||||
url: strings.ReplaceAll(options.RemoteOptions.URL, C.RuleSetTagPlaceholder, tag),
|
||||
options: options,
|
||||
updateInterval: updateInterval,
|
||||
pauseManager: service.FromContext[pause.Manager](ctx),
|
||||
@@ -68,7 +72,7 @@ func NewRemoteRuleSet(ctx context.Context, logger logger.ContextLogger, options
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) Name() string {
|
||||
return s.options.Tag
|
||||
return s.tag
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) String() string {
|
||||
@@ -84,7 +88,7 @@ func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext *adapter.
|
||||
startContext.Register(transport)
|
||||
s.httpClient = &http.Client{Transport: transport}
|
||||
if s.cacheFile != nil {
|
||||
if savedSet := s.cacheFile.LoadRuleSet(s.options.Tag); savedSet != nil {
|
||||
if savedSet := s.cacheFile.LoadRuleSet(s.tag); savedSet != nil {
|
||||
err = s.loadBytes(savedSet.Content)
|
||||
if err != nil {
|
||||
s.logger.Warn(E.Cause(err, "restore cached rule-set, will refetch"))
|
||||
@@ -97,7 +101,7 @@ func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext *adapter.
|
||||
if s.lastUpdated.IsZero() {
|
||||
err = s.fetch(ctx, true)
|
||||
if err != nil {
|
||||
return E.Cause(err, "initial rule-set: ", s.options.Tag)
|
||||
return E.Cause(err, "initial rule-set: ", s.tag)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -174,7 +178,7 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
|
||||
}
|
||||
}
|
||||
metadata := buildRuleSetMetadata(plainRuleSet.Rules)
|
||||
err = validateRuleSetMetadataUpdate(s.ctx, s.options.Tag, metadata)
|
||||
err = validateRuleSetMetadataUpdate(s.ctx, s.tag, metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -192,15 +196,15 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
|
||||
func (s *RemoteRuleSet) updateOnce() {
|
||||
err := s.fetch(s.ctx, false)
|
||||
if err != nil {
|
||||
s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
|
||||
s.logger.Error("fetch rule-set ", s.tag, ": ", err)
|
||||
} else if s.refs.Load() == 0 {
|
||||
s.rules = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) fetch(ctx context.Context, isStart bool) error {
|
||||
s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL)
|
||||
request, err := http.NewRequest("GET", s.options.RemoteOptions.URL, nil)
|
||||
s.logger.Debug("updating rule-set ", s.tag, " from URL: ", s.url)
|
||||
request, err := http.NewRequest("GET", s.url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -220,17 +224,17 @@ func (s *RemoteRuleSet) fetch(ctx context.Context, isStart bool) error {
|
||||
case http.StatusNotModified:
|
||||
s.lastUpdated = time.Now()
|
||||
if s.cacheFile != nil {
|
||||
savedRuleSet := s.cacheFile.LoadRuleSet(s.options.Tag)
|
||||
savedRuleSet := s.cacheFile.LoadRuleSet(s.tag)
|
||||
if savedRuleSet != nil {
|
||||
savedRuleSet.LastUpdated = s.lastUpdated
|
||||
err = s.cacheFile.SaveRuleSet(s.options.Tag, savedRuleSet)
|
||||
err = s.cacheFile.SaveRuleSet(s.tag, savedRuleSet)
|
||||
if err != nil {
|
||||
s.logger.Error("save rule-set updated time: ", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
s.logger.Info("update rule-set ", s.options.Tag, ": not modified")
|
||||
s.logger.Info("update rule-set ", s.tag, ": not modified")
|
||||
return nil
|
||||
default:
|
||||
return E.New("unexpected status: ", response.Status)
|
||||
@@ -249,7 +253,7 @@ func (s *RemoteRuleSet) fetch(ctx context.Context, isStart bool) error {
|
||||
}
|
||||
s.lastUpdated = time.Now()
|
||||
if s.cacheFile != nil {
|
||||
err = s.cacheFile.SaveRuleSet(s.options.Tag, &adapter.SavedBinary{
|
||||
err = s.cacheFile.SaveRuleSet(s.tag, &adapter.SavedBinary{
|
||||
LastUpdated: s.lastUpdated,
|
||||
Content: content,
|
||||
LastEtag: s.lastEtag,
|
||||
@@ -258,7 +262,7 @@ func (s *RemoteRuleSet) fetch(ctx context.Context, isStart bool) error {
|
||||
s.logger.Error("save rule-set cache: ", err)
|
||||
}
|
||||
}
|
||||
s.logger.Info("updated rule-set ", s.options.Tag)
|
||||
s.logger.Info("updated rule-set ", s.tag)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/convertor/adguard"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
slogger "github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
@@ -1104,8 +1103,8 @@ func newLocalRuleSetForTest(tag string, rules ...adapter.HeadlessRule) *LocalRul
|
||||
|
||||
func newRemoteRuleSetForTest(tag string, rules ...adapter.HeadlessRule) *RemoteRuleSet {
|
||||
return &RemoteRuleSet{
|
||||
options: option.RuleSet{Tag: tag},
|
||||
rules: rules,
|
||||
tag: tag,
|
||||
rules: rules,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,8 +87,8 @@ func TestRemoteRuleSetLoadBytesRejectsInvalidUpdateBeforeCommit(t *testing.T) {
|
||||
})
|
||||
ruleSet := &RemoteRuleSet{
|
||||
ctx: ctx,
|
||||
tag: "dynamic-set",
|
||||
options: option.RuleSet{
|
||||
Tag: "dynamic-set",
|
||||
Format: C.RuleSetFormatSource,
|
||||
},
|
||||
callbacks: list.List[adapter.RuleSetUpdateCallback]{},
|
||||
|
||||
Reference in New Issue
Block a user