package option import ( "net/url" "path/filepath" "reflect" "strings" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/schema" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/domain" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badjson" "github.com/sagernet/sing/common/json/badoption" "go4.org/netipx" ) type _RuleSet struct { Type string `json:"type,omitempty" enum:"inline,local,remote"` Tag badoption.Listable[string] `json:"tag"` Format string `json:"format,omitempty" enum:"source,binary"` InlineOptions PlainRuleSet `json:"-"` LocalOptions LocalRuleSet `json:"-"` RemoteOptions RemoteRuleSet `json:"-"` } type RuleSet _RuleSet func (r RuleSet) MarshalJSON() ([]byte, error) { if r.Type != C.RuleSetTypeInline { var defaultFormat string switch r.Type { case C.RuleSetTypeLocal: defaultFormat = ruleSetDefaultFormat(r.LocalOptions.Path) case C.RuleSetTypeRemote: defaultFormat = ruleSetDefaultFormat(r.RemoteOptions.URL) } if r.Format == defaultFormat { r.Format = "" } } var v any switch r.Type { case "", C.RuleSetTypeInline: r.Type = "" v = r.InlineOptions case C.RuleSetTypeLocal: v = r.LocalOptions case C.RuleSetTypeRemote: v = r.RemoteOptions default: return nil, E.New("unknown rule-set type: " + r.Type) } return badjson.MarshallObjects((_RuleSet)(r), v) } func (r *RuleSet) UnmarshalJSON(bytes []byte) error { err := json.Unmarshal(bytes, (*_RuleSet)(r)) if err != nil { return err } if len(r.Tag) == 0 || common.Any(r.Tag, func(tag string) bool { return tag == "" }) { return E.New("missing tag") } var v any switch r.Type { case "", C.RuleSetTypeInline: r.Type = C.RuleSetTypeInline v = &r.InlineOptions case C.RuleSetTypeLocal: v = &r.LocalOptions case C.RuleSetTypeRemote: v = &r.RemoteOptions default: return E.New("unknown rule-set type: " + r.Type) } err = badjson.UnmarshallExcluded(bytes, (*_RuleSet)(r), v) if err != nil { return err } if r.Type != C.RuleSetTypeInline { if r.Format == "" { switch r.Type { case C.RuleSetTypeLocal: r.Format = ruleSetDefaultFormat(r.LocalOptions.Path) case C.RuleSetTypeRemote: r.Format = ruleSetDefaultFormat(r.RemoteOptions.URL) } } switch r.Format { case "": return E.New("missing format") case C.RuleSetFormatSource, C.RuleSetFormatBinary: default: return E.New("unknown rule-set format: " + r.Format) } } 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") } if r.RemoteOptions.InitialPath != "" && !strings.Contains(r.RemoteOptions.InitialPath, C.RuleSetTagPlaceholder) { return E.New("missing ", C.RuleSetTagPlaceholder, " placeholder in initial_path") } } } return nil } func ruleSetDefaultFormat(path string) string { if pathURL, err := url.Parse(path); err == nil { path = pathURL.Path } switch filepath.Ext(path) { case ".json": return C.RuleSetFormatSource case ".srs": return C.RuleSetFormatBinary default: return "" } } func (r RuleSet) DescribeSchema(builder schema.Builder) (*schema.Node, error) { return builder.Define("RuleSet", func() (*schema.Node, error) { headlessRef, err := builder.Describe(reflect.TypeFor[HeadlessRule]()) if err != nil { return nil, err } tagNode := schema.ListableOf(schema.StringNode()) formatNode := schema.StringEnum(C.RuleSetFormatSource, C.RuleSetFormatBinary) inlineVariant := schema.StrictObject() inlineVariant.Properties.Put("type", schema.StringEnum(C.RuleSetTypeInline, "")) inlineVariant.Properties.Put("tag", tagNode) inlineVariant.Properties.Put("rules", &schema.Node{Type: "array", Items: headlessRef}) inlineVariant.Required = []string{"tag"} localVariant := schema.StrictObject() localVariant.Properties.Put("type", schema.StringConst(C.RuleSetTypeLocal)) localVariant.Properties.Put("tag", tagNode) localVariant.Properties.Put("format", formatNode) err = builder.FlattenStruct(localVariant, reflect.TypeFor[LocalRuleSet]()) if err != nil { return nil, err } localVariant.Required = []string{"type", "tag"} remoteVariant := schema.StrictObject() remoteVariant.Properties.Put("type", schema.StringConst(C.RuleSetTypeRemote)) remoteVariant.Properties.Put("tag", tagNode) remoteVariant.Properties.Put("format", formatNode) err = builder.FlattenStruct(remoteVariant, reflect.TypeFor[RemoteRuleSet]()) if err != nil { return nil, err } remoteVariant.Required = []string{"type", "tag"} return schema.OneOf(inlineVariant, localVariant, remoteVariant), nil }) } type LocalRuleSet struct { Path string `json:"path,omitempty"` } 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 DownloadDetour string `json:"download_detour,omitempty" reference:"outbound" schema:"omit"` } type _HeadlessRule struct { Type string `json:"type,omitempty" enum:"default,logical"` DefaultOptions DefaultHeadlessRule `json:"-"` LogicalOptions LogicalHeadlessRule `json:"-"` } type HeadlessRule _HeadlessRule func (r HeadlessRule) MarshalJSON() ([]byte, error) { var v any switch r.Type { case C.RuleTypeDefault: r.Type = "" v = r.DefaultOptions case C.RuleTypeLogical: v = r.LogicalOptions default: return nil, E.New("unknown rule type: " + r.Type) } return badjson.MarshallObjects((_HeadlessRule)(r), v) } func (r *HeadlessRule) UnmarshalJSON(bytes []byte) error { err := json.Unmarshal(bytes, (*_HeadlessRule)(r)) if err != nil { return err } var v any switch r.Type { case "", C.RuleTypeDefault: r.Type = C.RuleTypeDefault v = &r.DefaultOptions case C.RuleTypeLogical: v = &r.LogicalOptions default: return E.New("unknown rule type: " + r.Type) } err = badjson.UnmarshallExcluded(bytes, (*_HeadlessRule)(r), v) if err != nil { return err } return nil } func (r HeadlessRule) IsValid() bool { switch r.Type { case C.RuleTypeDefault, "": return r.DefaultOptions.IsValid() case C.RuleTypeLogical: return r.LogicalOptions.IsValid() default: panic("unknown rule type: " + r.Type) } } func (r HeadlessRule) DescribeSchema(builder schema.Builder) (*schema.Node, error) { return builder.Define("HeadlessRule", func() (*schema.Node, error) { return nestedRuleUnion(builder, reflect.TypeFor[DefaultHeadlessRule](), "HeadlessRule") }) } type DefaultHeadlessRule struct { QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` Network badoption.Listable[string] `json:"network,omitempty" enum:"tcp,udp,icmp"` Domain badoption.Listable[string] `json:"domain,omitempty"` DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` Port badoption.Listable[uint16] `json:"port,omitempty"` PortRange badoption.Listable[string] `json:"port_range,omitempty"` ProcessName badoption.Listable[string] `json:"process_name,omitempty"` ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` PackageName badoption.Listable[string] `json:"package_name,omitempty"` PackageNameRegex badoption.Listable[string] `json:"package_name_regex,omitempty"` NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[*badoption.Prefixable]] `json:"network_interface_address,omitempty"` DefaultInterfaceAddress badoption.Listable[*badoption.Prefixable] `json:"default_interface_address,omitempty"` Invert bool `json:"invert,omitempty"` DomainMatcher *domain.Matcher `json:"-"` SourceIPSet *netipx.IPSet `json:"-"` IPSet *netipx.IPSet `json:"-"` AdGuardDomain badoption.Listable[string] `json:"-"` AdGuardDomainMatcher *domain.AdGuardMatcher `json:"-"` } func (r DefaultHeadlessRule) IsValid() bool { var defaultValue DefaultHeadlessRule defaultValue.Invert = r.Invert return !reflect.DeepEqual(r, defaultValue) } type LogicalHeadlessRule struct { Mode string `json:"mode" enum:"and,or"` Rules []HeadlessRule `json:"rules,omitempty"` Invert bool `json:"invert,omitempty"` } func (r LogicalHeadlessRule) IsValid() bool { return len(r.Rules) > 0 && common.All(r.Rules, HeadlessRule.IsValid) } type _PlainRuleSetCompat struct { Version uint8 `json:"version" enum:"1,2,3,4,5"` Options PlainRuleSet `json:"-"` RawMessage json.RawMessage `json:"-"` } type PlainRuleSetCompat _PlainRuleSetCompat func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { var v any switch r.Version { case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4, C.RuleSetVersion5: v = r.Options default: return nil, E.New("unknown rule-set version: ", r.Version) } return badjson.MarshallObjects((_PlainRuleSetCompat)(r), v) } func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { err := json.Unmarshal(bytes, (*_PlainRuleSetCompat)(r)) if err != nil { return err } var v any switch r.Version { case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4, C.RuleSetVersion5: v = &r.Options case 0: return E.New("missing rule-set version") default: return E.New("unknown rule-set version: ", r.Version) } err = badjson.UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v) if err != nil { return err } r.RawMessage = bytes return nil } func (r PlainRuleSetCompat) Upgrade() (PlainRuleSet, error) { switch r.Version { case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4, C.RuleSetVersion5: default: return PlainRuleSet{}, E.New("unknown rule-set version: " + F.ToString(r.Version)) } return r.Options, nil } type PlainRuleSet struct { Rules []HeadlessRule `json:"rules,omitempty"` }