From 9ae4f644f158af86f2b409a08e80795b9b085ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 11 May 2024 22:43:29 +0800 Subject: [PATCH] Add `tempalte.extended`, `template..`, `template.block_tag` --- constant/rule_set.go | 6 +++ docs/configuration/template.md | 4 +- option/template.go | 58 ++++++++++++++++++++++++++-- template/manager.go | 38 +++++++++++++++++++ template/render_dns.go | 6 ++- template/render_geo_resources.go | 65 +++++++++++++++++++++++++++----- template/render_outbounds.go | 12 ++++-- template/render_route.go | 8 +++- template/template.go | 4 +- 9 files changed, 178 insertions(+), 23 deletions(-) create mode 100644 constant/rule_set.go diff --git a/constant/rule_set.go b/constant/rule_set.go new file mode 100644 index 0000000..96ea296 --- /dev/null +++ b/constant/rule_set.go @@ -0,0 +1,6 @@ +package constant + +const ( + RuleSetTypeDefault = "default" + RuleSetTypeGitHub = "github" +) diff --git a/docs/configuration/template.md b/docs/configuration/template.md index c4bfdd4..fc9a326 100644 --- a/docs/configuration/template.md +++ b/docs/configuration/template.md @@ -57,7 +57,7 @@ "custom_geoip": {}, "custom_geosite": {}, "custom_rule_set": [], - "post_custom_rule_set": [], + "post_rule_set": [], // Experimental @@ -253,7 +253,7 @@ List of [RuleSet](https://sing-box.sagernet.org/configuration/rule-set/). Default rule sets will not be generated if not empty. -#### post_custom_rule_set +#### post_rule_set List of [RuleSet](https://sing-box.sagernet.org/configuration/rule-set/). diff --git a/option/template.go b/option/template.go index f08265f..5bd19c9 100644 --- a/option/template.go +++ b/option/template.go @@ -2,13 +2,17 @@ package option import ( "github.com/sagernet/serenity/common/semver" + C "github.com/sagernet/serenity/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-dns" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badjson" ) type Template struct { - Name string `json:"name,omitempty"` + Name string `json:"name,omitempty"` + Extend string `json:"extend,omitempty"` // Global @@ -37,6 +41,7 @@ type Template struct { ExtraGroups []ExtraGroup `json:"extra_groups,omitempty"` GenerateGlobalURLTest bool `json:"generate_global_urltest,omitempty"` DirectTag string `json:"direct_tag,omitempty"` + BlockTag string `json:"block_tag,omitempty"` DefaultTag string `json:"default_tag,omitempty"` URLTestTag string `json:"urltest_tag,omitempty"` CustomDirect *option.DirectOutboundOptions `json:"custom_direct,omitempty"` @@ -51,8 +56,8 @@ type Template struct { EnableJSDelivr bool `json:"enable_jsdelivr,omitempty"` CustomGeoIP *option.GeoIPOptions `json:"custom_geoip,omitempty"` CustomGeosite *option.GeositeOptions `json:"custom_geosite,omitempty"` - CustomRuleSet []option.RuleSet `json:"custom_rule_set,omitempty"` - PostCustomRuleSet []option.RuleSet `json:"post_custom_rule_set,omitempty"` + CustomRuleSet []RuleSet `json:"custom_rule_set,omitempty"` + PostRuleSet []RuleSet `json:"post_rule_set,omitempty"` // Experimental DisableCacheFile bool `json:"disable_cache_file,omitempty"` @@ -70,6 +75,53 @@ type Template struct { MemoryLimit option.MemoryBytes `json:"memory_limit,omitempty"` } +type _RuleSet struct { + Type string `json:"type,omitempty"` + DefaultOptions option.RuleSet `json:"-"` + GitHubOptions GitHubRuleSetOptions `json:"-"` +} + +type RuleSet _RuleSet + +func (r *RuleSet) RawOptions() (any, error) { + switch r.Type { + case C.RuleSetTypeDefault, "": + r.Type = "" + return &r.DefaultOptions, nil + case C.RuleSetTypeGitHub: + return &r.GitHubOptions, nil + default: + return nil, E.New("unknown rule set type", r.Type) + } +} + +func (r *RuleSet) MarshalJSON() ([]byte, error) { + rawOptions, err := r.RawOptions() + if err != nil { + return nil, err + } + return option.MarshallObjects((*_RuleSet)(r), rawOptions) +} + +func (r *RuleSet) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_RuleSet)(r)) + if err != nil { + return err + } + rawOptions, err := r.RawOptions() + if err != nil { + return err + } + return option.UnmarshallExcluded(bytes, (*_RuleSet)(r), rawOptions) +} + +type GitHubRuleSetOptions struct { + Owner string `json:"owner,omitempty"` + Repo string `json:"repo,omitempty"` + Branch string `json:"branch,omitempty"` + RuleSet option.Listable[string] `json:"rule_set,omitempty"` +} + func (t Template) DisableIPv6() bool { return t.DomainStrategy == option.DomainStrategy(dns.DomainStrategyUseIPv4) } diff --git a/template/manager.go b/template/manager.go index aacc682..103cc15 100644 --- a/template/manager.go +++ b/template/manager.go @@ -7,6 +7,7 @@ import ( "github.com/sagernet/serenity/option" C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badjson" "github.com/sagernet/sing/common/logger" ) @@ -16,12 +17,49 @@ type Manager struct { templates []*Template } +func extendTemplate(rawTemplates []option.Template, root, current option.Template) (option.Template, error) { + if current.Extend == "" { + return current, nil + } else if root.Name == current.Extend { + return option.Template{}, E.New("initialize template[", current.Name, "]: circular extend detected: ", current.Extend) + } + var next option.Template + for _, it := range rawTemplates { + if it.Name == current.Extend { + next = it + break + } + } + if next.Name == "" { + return option.Template{}, E.New("initialize template[", current.Name, "]: extended template not found: ", current.Extend) + } + if next.Extend != "" { + newNext, err := extendTemplate(rawTemplates, root, next) + if err != nil { + return option.Template{}, E.Cause(err, next.Extend) + } + next = newNext + } + newTemplate, err := badjson.Merge(current, next) + if err != nil { + return option.Template{}, E.Cause(err, "initialize template[", current.Name, "]: merge extended template: ", current.Extend) + } + return newTemplate, nil +} + func NewManager(ctx context.Context, logger logger.Logger, rawTemplates []option.Template) (*Manager, error) { var templates []*Template for templateIndex, template := range rawTemplates { if template.Name == "" { return nil, E.New("initialize template[", templateIndex, "]: missing name") } + if template.Extend != "" { + newTemplate, err := extendTemplate(rawTemplates, template, template) + if err != nil { + return nil, err + } + template = newTemplate + } var groups []*ExtraGroup for groupIndex, group := range template.ExtraGroups { if group.Tag == "" { diff --git a/template/render_dns.go b/template/render_dns.go index 2dabea4..4528309 100644 --- a/template/render_dns.go +++ b/template/render_dns.go @@ -37,6 +37,10 @@ func (t *Template) renderDNS(metadata M.Metadata, options *option.Options) error if dnsLocal == "" { dnsLocal = DefaultDNSLocal } + directTag := t.DirectTag + if directTag == "" { + directTag = DefaultDirectTag + } defaultDNSOptions := option.DNSServerOptions{ Tag: DNSDefaultTag, Address: dnsDefault, @@ -58,7 +62,7 @@ func (t *Template) renderDNS(metadata M.Metadata, options *option.Options) error localDNSOptions = option.DNSServerOptions{ Tag: DNSLocalTag, Address: dnsLocal, - Detour: DefaultDirectTag, + Detour: directTag, } if dnsLocalUrl, err := url.Parse(dnsLocal); err == nil && BM.IsDomainName(dnsLocalUrl.Hostname()) { localDNSOptions.AddressResolver = DNSLocalSetupTag diff --git a/template/render_geo_resources.go b/template/render_geo_resources.go index 1c080af..05914c0 100644 --- a/template/render_geo_resources.go +++ b/template/render_geo_resources.go @@ -3,11 +3,13 @@ package template import ( M "github.com/sagernet/serenity/common/metadata" "github.com/sagernet/serenity/common/semver" + "github.com/sagernet/serenity/constant" + "github.com/sagernet/serenity/option" C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/option" + boxOption "github.com/sagernet/sing-box/option" ) -func (t *Template) renderGeoResources(metadata M.Metadata, options *option.Options) { +func (t *Template) renderGeoResources(metadata M.Metadata, options *boxOption.Options) { if t.DisableRuleSet || (metadata.Version != nil && metadata.Version.LessThan(semver.ParseVersion("1.8.0-alpha.10"))) { var ( geoipDownloadURL string @@ -27,13 +29,13 @@ func (t *Template) renderGeoResources(metadata M.Metadata, options *option.Optio geositeDownloadURL = "https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite-cn.db" } if t.CustomGeoIP == nil { - options.Route.GeoIP = &option.GeoIPOptions{ + options.Route.GeoIP = &boxOption.GeoIPOptions{ DownloadURL: geoipDownloadURL, DownloadDetour: downloadDetour, } } if t.CustomGeosite == nil { - options.Route.Geosite = &option.GeositeOptions{ + options.Route.Geosite = &boxOption.GeositeOptions{ DownloadURL: geositeDownloadURL, DownloadDetour: downloadDetour, } @@ -57,12 +59,12 @@ func (t *Template) renderGeoResources(metadata M.Metadata, options *option.Optio downloadURL = "https://raw.githubusercontent.com/" branchSplit = "/" } - options.Route.RuleSet = []option.RuleSet{ + options.Route.RuleSet = []boxOption.RuleSet{ { Type: C.RuleSetTypeRemote, Tag: "geoip-cn", Format: C.RuleSetFormatBinary, - RemoteOptions: option.RemoteRuleSet{ + RemoteOptions: boxOption.RemoteRuleSet{ URL: downloadURL + "SagerNet/sing-geoip" + branchSplit + "rule-set/geoip-cn.srs", DownloadDetour: downloadDetour, }, @@ -71,7 +73,7 @@ func (t *Template) renderGeoResources(metadata M.Metadata, options *option.Optio Type: C.RuleSetTypeRemote, Tag: "geosite-geolocation-cn", Format: C.RuleSetFormatBinary, - RemoteOptions: option.RemoteRuleSet{ + RemoteOptions: boxOption.RemoteRuleSet{ URL: downloadURL + "SagerNet/sing-geosite" + branchSplit + "rule-set/geosite-geolocation-cn.srs", DownloadDetour: downloadDetour, }, @@ -80,13 +82,58 @@ func (t *Template) renderGeoResources(metadata M.Metadata, options *option.Optio Type: C.RuleSetTypeRemote, Tag: "geosite-geolocation-!cn", Format: C.RuleSetFormatBinary, - RemoteOptions: option.RemoteRuleSet{ + RemoteOptions: boxOption.RemoteRuleSet{ URL: downloadURL + "SagerNet/sing-geosite" + branchSplit + "rule-set/geosite-geolocation-!cn.srs", DownloadDetour: downloadDetour, }, }, } } - options.Route.RuleSet = append(options.Route.RuleSet, t.PostCustomRuleSet...) + options.Route.RuleSet = append(options.Route.RuleSet, t.renderRuleSet(t.PostRuleSet)...) } } + +func (t *Template) renderRuleSet(ruleSets []option.RuleSet) []boxOption.RuleSet { + var result []boxOption.RuleSet + for _, ruleSet := range ruleSets { + switch ruleSet.Type { + case constant.RuleSetTypeDefault, "": + result = append(result, ruleSet.DefaultOptions) + case constant.RuleSetTypeGitHub: + var ( + downloadURL string + downloadDetour string + branchSplit string + ) + if t.EnableJSDelivr { + downloadURL = "https://testingcf.jsdelivr.net/gh/" + if t.DirectTag != "" { + downloadDetour = t.DirectTag + } else { + downloadDetour = DefaultDirectTag + } + branchSplit = "@" + } else { + downloadURL = "https://raw.githubusercontent.com/" + branchSplit = "/" + } + for _, code := range ruleSet.GitHubOptions.RuleSet { + result = append(result, boxOption.RuleSet{ + Type: C.RuleSetTypeRemote, + Tag: code, + Format: C.RuleSetFormatBinary, + RemoteOptions: boxOption.RemoteRuleSet{ + URL: downloadURL + + ruleSet.GitHubOptions.Owner + "/" + + ruleSet.GitHubOptions.Repo + "/" + + branchSplit + + ruleSet.GitHubOptions.Branch + "/" + + code + ".srs", + DownloadDetour: downloadDetour, + }, + }) + } + } + } + return result +} diff --git a/template/render_outbounds.go b/template/render_outbounds.go index 4f8f165..8557e26 100644 --- a/template/render_outbounds.go +++ b/template/render_outbounds.go @@ -20,6 +20,10 @@ func (t *Template) renderOutbounds(metadata M.Metadata, options *option.Options, if directTag == "" { directTag = DefaultDirectTag } + blockTag := t.BlockTag + if blockTag == "" { + blockTag = DefaultBlockTag + } options.Outbounds = []option.Outbound{ { Tag: directTag, @@ -27,7 +31,7 @@ func (t *Template) renderOutbounds(metadata M.Metadata, options *option.Options, DirectOptions: common.PtrValueOrDefault(t.CustomDirect), }, { - Tag: BlockTag, + Tag: blockTag, Type: C.TypeBlock, }, { @@ -85,7 +89,7 @@ func (t *Template) renderOutbounds(metadata M.Metadata, options *option.Options, selectorOutbound := option.Outbound{ Type: C.TypeSelector, Tag: it.Name, - SelectorOptions: common.PtrValueOrDefault(t.CustomSelector), + SelectorOptions: common.PtrValueOrDefault(it.CustomSelector), } selectorOutbound.SelectorOptions.Outbounds = append(selectorOutbound.SelectorOptions.Outbounds, joinOutbounds...) allGroups = append(allGroups, selectorOutbound) @@ -171,9 +175,9 @@ func groupJoin(outbounds []option.Outbound, groupTag string, groupOutbounds ...s groupOutbound := outbounds[groupIndex] switch groupOutbound.Type { case C.TypeSelector: - groupOutbound.SelectorOptions.Outbounds = append(groupOutbound.SelectorOptions.Outbounds, groupOutbounds...) + groupOutbound.SelectorOptions.Outbounds = common.Dup(append(groupOutbound.SelectorOptions.Outbounds, groupOutbounds...)) case C.TypeURLTest: - groupOutbound.URLTestOptions.Outbounds = append(groupOutbound.URLTestOptions.Outbounds, groupOutbounds...) + groupOutbound.URLTestOptions.Outbounds = common.Dup(append(groupOutbound.URLTestOptions.Outbounds, groupOutbounds...)) } outbounds[groupIndex] = groupOutbound return outbounds diff --git a/template/render_route.go b/template/render_route.go index ff4e1a1..ca3aa9f 100644 --- a/template/render_route.go +++ b/template/render_route.go @@ -13,7 +13,7 @@ func (t *Template) renderRoute(metadata M.Metadata, options *option.Options) err options.Route = &option.RouteOptions{ GeoIP: t.CustomGeoIP, Geosite: t.CustomGeosite, - RuleSet: t.CustomRuleSet, + RuleSet: t.renderRuleSet(t.CustomRuleSet), } } if !t.DisableTrafficBypass { @@ -45,6 +45,10 @@ func (t *Template) renderRoute(metadata M.Metadata, options *option.Options) err }, } if !t.DisableTrafficBypass && !t.DisableDefaultRules { + blockTag := t.BlockTag + if blockTag == "" { + blockTag = DefaultBlockTag + } options.Route.Rules = append(options.Route.Rules, option.Rule{ Type: C.RuleTypeLogical, LogicalOptions: option.LogicalRule{ @@ -64,7 +68,7 @@ func (t *Template) renderRoute(metadata M.Metadata, options *option.Options) err }, }, }, - Outbound: BlockTag, + Outbound: blockTag, }, }) } diff --git a/template/template.go b/template/template.go index 9bdc76a..2582d3a 100644 --- a/template/template.go +++ b/template/template.go @@ -19,9 +19,9 @@ const ( DNSFakeIPTag = "remote" DefaultDNS = "tls://8.8.8.8" DefaultDNSLocal = "https://223.5.5.5/dns-query" - DefaultDefaultTag = "Default" + DefaultDefaultTag = "default" DefaultDirectTag = "direct" - BlockTag = "block" + DefaultBlockTag = "block" DNSTag = "dns" DefaultURLTestTag = "URLTest" )