Browse Source

Add `network_[type/is_expensive/is_constrained]` rule items

世界 11 months ago
parent
commit
05ea0ca00e

+ 4 - 3
cmd/sing-box/internal/convertor/adguard/convertor_test.go

@@ -1,6 +1,7 @@
 package adguard
 
 import (
+	"context"
 	"strings"
 	"testing"
 
@@ -26,7 +27,7 @@ example.arpa
 `))
 	require.NoError(t, err)
 	require.Len(t, rules, 1)
-	rule, err := rule.NewHeadlessRule(nil, rules[0])
+	rule, err := rule.NewHeadlessRule(context.Background(), rules[0])
 	require.NoError(t, err)
 	matchDomain := []string{
 		"example.org",
@@ -85,7 +86,7 @@ func TestHosts(t *testing.T) {
 `))
 	require.NoError(t, err)
 	require.Len(t, rules, 1)
-	rule, err := rule.NewHeadlessRule(nil, rules[0])
+	rule, err := rule.NewHeadlessRule(context.Background(), rules[0])
 	require.NoError(t, err)
 	matchDomain := []string{
 		"google.com",
@@ -115,7 +116,7 @@ www.example.org
 `))
 	require.NoError(t, err)
 	require.Len(t, rules, 1)
-	rule, err := rule.NewHeadlessRule(nil, rules[0])
+	rule, err := rule.NewHeadlessRule(context.Background(), rules[0])
 	require.NoError(t, err)
 	matchDomain := []string{
 		"example.com",

+ 30 - 0
common/srs/binary.go

@@ -38,6 +38,9 @@ const (
 	ruleItemWIFIBSSID
 	ruleItemAdGuardDomain
 	ruleItemProcessPathRegex
+	ruleItemNetworkType
+	ruleItemNetworkIsExpensive
+	ruleItemNetworkIsConstrained
 	ruleItemFinal uint8 = 0xFF
 )
 
@@ -222,6 +225,12 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
 				return
 			}
 			rule.AdGuardDomainMatcher = matcher
+		case ruleItemNetworkType:
+			rule.NetworkType, err = readRuleItemString(reader)
+		case ruleItemNetworkIsExpensive:
+			rule.NetworkIsExpensive = true
+		case ruleItemNetworkIsConstrained:
+			rule.NetworkIsConstrained = true
 		case ruleItemFinal:
 			err = binary.Read(reader, binary.BigEndian, &rule.Invert)
 			return
@@ -336,6 +345,27 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
 			return err
 		}
 	}
+	if len(rule.NetworkType) > 0 {
+		if generateVersion < C.RuleSetVersion3 {
+			return E.New("network_type rule item is only supported in version 3 or later")
+		}
+		err = writeRuleItemString(writer, ruleItemNetworkType, rule.NetworkType)
+		if err != nil {
+			return err
+		}
+	}
+	if rule.NetworkIsExpensive {
+		err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsExpensive)
+		if err != nil {
+			return err
+		}
+	}
+	if rule.NetworkIsConstrained {
+		err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsConstrained)
+		if err != nil {
+			return err
+		}
+	}
 	if len(rule.WIFISSID) > 0 {
 		err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID)
 		if err != nil {

+ 2 - 1
constant/rule.go

@@ -21,7 +21,8 @@ const (
 const (
 	RuleSetVersion1 = 1 + iota
 	RuleSetVersion2
-	RuleSetVersionCurrent = RuleSetVersion2
+	RuleSetVersion3
+	RuleSetVersionCurrent = RuleSetVersion3
 )
 
 const (

+ 42 - 1
docs/configuration/dns/rule.md

@@ -8,7 +8,10 @@ icon: material/new-box
     :material-alert: [server](#server)  
     :material-alert: [disable_cache](#disable_cache)  
     :material-alert: [rewrite_ttl](#rewrite_ttl)  
-    :material-alert: [client_subnet](#client_subnet)
+    :material-alert: [client_subnet](#client_subnet)  
+    :material-plus: [network_type](#network_type)  
+    :material-plus: [network_is_expensive](#network_is_expensive)  
+    :material-plus: [network_is_constrained](#network_is_constrained)
 
 !!! quote "Changes in sing-box 1.10.0"
 
@@ -125,6 +128,11 @@ icon: material/new-box
           1000
         ],
         "clash_mode": "direct",
+        "network_type": [
+          "wifi"
+        ],
+        "network_is_expensive": false,
+        "network_is_constrained": false,
         "wifi_ssid": [
           "My WIFI"
         ],
@@ -310,6 +318,39 @@ Match user id.
 
 Match Clash mode.
 
+#### network_type
+
+!!! question "Since sing-box 1.11.0"
+
+!!! quote ""
+
+    Only supported in graphical clients on Android and Apple platforms.
+
+Match network type.
+
+Available values: `wifi`, `cellular`, `ethernet` and `other`.
+
+#### network_is_expensive
+
+!!! question "Since sing-box 1.11.0"
+
+!!! quote ""
+
+    Only supported in graphical clients on Android and Apple platforms.
+
+Match if network is considered Metered (on Android) or considered expensive,
+such as Cellular or a Personal Hotspot (on Apple platforms).
+
+#### network_is_constrained
+
+!!! question "Since sing-box 1.11.0"
+
+!!! quote ""
+
+    Only supported in graphical clients on Apple platforms.
+
+Match if network is in Low Data Mode.
+
 #### wifi_ssid
 
 !!! quote ""

+ 74 - 17
docs/configuration/dns/rule.zh.md

@@ -2,6 +2,17 @@
 icon: material/new-box
 ---
 
+!!! quote "sing-box 1.11.0 中的更改"
+
+    :material-plus: [action](#action)  
+    :material-alert: [server](#server)  
+    :material-alert: [disable_cache](#disable_cache)  
+    :material-alert: [rewrite_ttl](#rewrite_ttl)  
+    :material-alert: [client_subnet](#client_subnet)  
+    :material-plus: [network_type](#network_type)  
+    :material-plus: [network_is_expensive](#network_is_expensive)  
+    :material-plus: [network_is_constrained](#network_is_constrained)
+
 !!! quote "sing-box 1.10.0 中的更改"
 
     :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)  
@@ -117,6 +128,11 @@ icon: material/new-box
           1000
         ],
         "clash_mode": "direct",
+        "network_type": [
+          "wifi"
+        ],
+        "network_is_expensive": false,
+        "network_is_constrained": false,
         "wifi_ssid": [
           "My WIFI"
         ],
@@ -135,17 +151,15 @@ icon: material/new-box
         "outbound": [
           "direct"
         ],
-        "server": "local",
-        "disable_cache": false,
-        "client_subnet": "127.0.0.1/24"
+        "action": "route",
+        "server": "local"
       },
       {
         "type": "logical",
         "mode": "and",
         "rules": [],
-        "server": "local",
-        "disable_cache": false,
-        "client_subnet": "127.0.0.1/24"
+        "action": "route",
+        "server": "local"
       }
     ]
   }
@@ -304,6 +318,39 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
 
 匹配 Clash 模式。
 
+#### network_type
+
+!!! question "自 sing-box 1.11.0 起"
+
+!!! quote ""
+
+    仅在 Android 与 Apple 平台图形客户端中支持。
+
+匹配网络类型。
+
+Available values: `wifi`, `cellular`, `ethernet` and `other`.
+
+#### network_is_expensive
+
+!!! question "自 sing-box 1.11.0 起"
+
+!!! quote ""
+
+    仅在 Android 与 Apple 平台图形客户端中支持。
+
+匹配如果网络被视为计费 (在 Android) 或被视为昂贵,
+像蜂窝网络或个人热点 (在 Apple 平台)。
+
+#### network_is_constrained
+
+!!! question "自 sing-box 1.11.0 起"
+
+!!! quote ""
+
+    仅在 Apple 平台图形客户端中支持。
+
+匹配如果网络在低数据模式下。
+
 #### wifi_ssid
 
 !!! quote ""
@@ -352,29 +399,35 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
 
 `any` 可作为值用于匹配任意出站。
 
-#### server
+#### action
 
 ==必填==
 
-目标 DNS 服务器的标签。
+参阅 [规则动作](../rule_action/)。
+
+#### server
+
+!!! failure "已在 sing-box 1.11.0 废弃"
+
+    已移动到 [DNS 规则动作](../rule_action#route).
 
 #### disable_cache
 
-在此查询中禁用缓存。
+!!! failure "已在 sing-box 1.11.0 废弃"
+
+    已移动到 [DNS 规则动作](../rule_action#route).
 
 #### rewrite_ttl
 
-重写 DNS 回应中的 TTL。
+!!! failure "已在 sing-box 1.11.0 废弃"
 
-#### client_subnet
+    已移动到 [DNS 规则动作](../rule_action#route).
 
-!!! question "自 sing-box 1.9.0 起"
-
-默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。
+#### client_subnet
 
-如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。
+!!! failure "已在 sing-box 1.11.0 废弃"
 
-将覆盖 `dns.client_subnet` 与 `servers.[].client_subnet`。
+    已移动到 [DNS 规则动作](../rule_action#route).
 
 ### 地址筛选字段
 
@@ -420,8 +473,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
 
 #### mode
 
+==必填==
+
 `and` 或 `or`
 
 #### rules
 
-包括的规则。
+==必填==
+
+包括的规则。

+ 42 - 1
docs/configuration/route/rule.md

@@ -5,7 +5,10 @@ icon: material/new-box
 !!! quote "Changes in sing-box 1.11.0"
 
     :material-plus: [action](#action)  
-    :material-alert: [outbound](#outbound)
+    :material-alert: [outbound](#outbound)  
+    :material-plus: [network_type](#network_type)  
+    :material-plus: [network_is_expensive](#network_is_expensive)  
+    :material-plus: [network_is_constrained](#network_is_constrained)
 
 !!! quote "Changes in sing-box 1.10.0"
 
@@ -120,6 +123,11 @@ icon: material/new-box
           1000
         ],
         "clash_mode": "direct",
+        "network_type": [
+          "wifi"
+        ],
+        "network_is_expensive": false,
+        "network_is_constrained": false,
         "wifi_ssid": [
           "My WIFI"
         ],
@@ -322,6 +330,39 @@ Match user id.
 
 Match Clash mode.
 
+#### network_type
+
+!!! question "Since sing-box 1.11.0"
+
+!!! quote ""
+
+    Only supported in graphical clients on Android and Apple platforms.
+
+Match network type.
+
+Available values: `wifi`, `cellular`, `ethernet` and `other`.
+
+#### network_is_expensive
+
+!!! question "Since sing-box 1.11.0"
+
+!!! quote ""
+
+    Only supported in graphical clients on Android and Apple platforms.
+
+Match if network is considered Metered (on Android) or considered expensive,
+such as Cellular or a Personal Hotspot (on Apple platforms).
+
+#### network_is_constrained
+
+!!! question "Since sing-box 1.11.0"
+
+!!! quote ""
+
+    Only supported in graphical clients on Apple platforms.
+
+Match if network is in Low Data Mode.
+
 #### wifi_ssid
 
 !!! quote ""

+ 45 - 5
docs/configuration/route/rule.zh.md

@@ -5,7 +5,10 @@ icon: material/new-box
 !!! quote "sing-box 1.11.0 中的更改"
 
     :material-plus: [action](#action)  
-    :material-alert: [outbound](#outbound)
+    :material-alert: [outbound](#outbound)  
+    :material-plus: [network_type](#network_type)  
+    :material-plus: [network_is_expensive](#network_is_expensive)  
+    :material-plus: [network_is_constrained](#network_is_constrained)
 
 !!! quote "sing-box 1.10.0 中的更改"
 
@@ -13,7 +16,6 @@ icon: material/new-box
     :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)  
     :material-plus: [process_path_regex](#process_path_regex)
 
-
 !!! quote "sing-box 1.8.0 中的更改"
 
     :material-plus: [rule_set](#rule_set)  
@@ -118,6 +120,11 @@ icon: material/new-box
           1000
         ],
         "clash_mode": "direct",
+        "network_type": [
+          "wifi"
+        ],
+        "network_is_expensive": false,
+        "network_is_constrained": false,
         "wifi_ssid": [
           "My WIFI"
         ],
@@ -153,7 +160,7 @@ icon: material/new-box
 
     当内容只有一项时,可以忽略 JSON 数组 [] 标签。
 
-### Default Fields
+### 默认字段
 
 !!! note ""
 
@@ -320,6 +327,39 @@ icon: material/new-box
 
 匹配 Clash 模式。
 
+#### network_type
+
+!!! question "自 sing-box 1.11.0 起"
+
+!!! quote ""
+
+    仅在 Android 与 Apple 平台图形客户端中支持。
+
+匹配网络类型。
+
+Available values: `wifi`, `cellular`, `ethernet` and `other`.
+
+#### network_is_expensive
+
+!!! question "自 sing-box 1.11.0 起"
+
+!!! quote ""
+
+    仅在 Android 与 Apple 平台图形客户端中支持。
+
+匹配如果网络被视为计费 (在 Android) 或被视为昂贵,
+像蜂窝网络或个人热点 (在 Apple 平台)。
+
+#### network_is_constrained
+
+!!! question "自 sing-box 1.11.0 起"
+
+!!! quote ""
+
+    仅在 Apple 平台图形客户端中支持。
+
+匹配如果网络在低数据模式下。
+
 #### wifi_ssid
 
 !!! quote ""
@@ -366,13 +406,13 @@ icon: material/new-box
 
 ==必填==
 
-参阅 [规则动](../rule_action/)。
+参阅 [规则动](../rule_action/)。
 
 #### outbound
 
 !!! failure "已在 sing-box 1.11.0 废弃"
 
-    已移动到 [规则动](../rule_action#route).
+    已移动到 [规则动](../rule_action#route).
 
 ### 逻辑字段
 

+ 70 - 0
docs/configuration/rule-set/adguard.zh.md

@@ -0,0 +1,70 @@
+---
+icon: material/new-box
+---
+
+# AdGuard DNS Filter
+
+!!! question "自 sing-box 1.10.0 起"
+
+sing-box 支持其他项目的一些规则集格式,这些格式无法完全转换为 sing-box,
+目前只有 AdGuard DNS Filter。
+
+这些格式不直接作为源格式支持,
+而是需要将它们转换为二进制规则集。
+
+## 转换
+
+使用 `sing-box rule-set convert --type adguard [--output <file-name>.srs] <file-name>.txt` 以转换为二进制规则集。
+
+## 性能
+
+AdGuard 将所有规则保存在内存中并按顺序匹配,
+而 sing-box 选择高性能和较小的内存使用量。
+作为权衡,您无法知道匹配了哪个规则项。
+
+## 兼容性
+
+[AdGuardSDNSFilter](https://github.com/AdguardTeam/AdGuardSDNSFilter)
+中的几乎所有规则以及 [adguard-filter-list](https://github.com/ppfeufer/adguard-filter-list)
+中列出的规则集中的规则均受支持。
+
+## 支持的格式
+
+### AdGuard Filter
+
+#### 基本规则语法
+
+| 语法     | 支持               |
+|--------|------------------|
+| `@@`   | :material-check: | 
+| `\|\|` | :material-check: | 
+| `\|`   | :material-check: |
+| `^`    | :material-check: |
+| `*`    | :material-check: |
+
+#### 主机语法
+
+| 语法          | 示例                       | 支持                       |
+|-------------|--------------------------|--------------------------|
+| Scheme      | `https://`               | :material-alert: Ignored |
+| Domain Host | `example.org`            | :material-check:         |
+| IP Host     | `1.1.1.1`, `10.0.0.`     | :material-close:         |
+| Regexp      | `/regexp/`               | :material-check:         |
+| Port        | `example.org:80`         | :material-close:         |
+| Path        | `example.org/path/ad.js` | :material-close:         |
+
+#### 描述符语法
+
+| 描述符                   | 支持                       |
+|-----------------------|--------------------------|
+| `$important`          | :material-check:         |
+| `$dnsrewrite=0.0.0.0` | :material-alert: Ignored |
+| 任何其他描述符               | :material-close:         |
+
+### Hosts
+
+只有 IP 地址为 `0.0.0.0` 的条目将被接受。
+
+### 简易
+
+当所有行都是有效域时,它们被视为简单的逐行域规则, 与 hosts 一样,只匹配完全相同的域。

+ 48 - 0
docs/configuration/rule-set/headless-rule.md

@@ -1,3 +1,13 @@
+---
+icon: material/new-box
+---
+
+!!! quote "Changes in sing-box 1.11.0"
+
+    :material-plus: [network_type](#network_type)  
+    :material-plus: [network_is_expensive](#network_is_expensive)  
+    :material-plus: [network_is_constrained](#network_is_constrained)
+
 ### Structure
 
 !!! question "Since sing-box 1.8.0"
@@ -63,6 +73,11 @@
       "package_name": [
         "com.termux"
       ],
+      "network_type": [
+        "wifi"
+      ],
+      "network_is_expensive": false,
+      "network_is_constrained": false,
       "wifi_ssid": [
         "My WIFI"
       ],
@@ -177,6 +192,39 @@ Match process path using regular expression.
 
 Match android package name.
 
+#### network_type
+
+!!! question "Since sing-box 1.11.0"
+
+!!! quote ""
+
+    Only supported in graphical clients on Android and Apple platforms.
+
+Match network type.
+
+Available values: `wifi`, `cellular`, `ethernet` and `other`.
+
+#### network_is_expensive
+
+!!! question "Since sing-box 1.11.0"
+
+!!! quote ""
+
+    Only supported in graphical clients on Android and Apple platforms.
+
+Match if network is considered Metered (on Android) or considered expensive,
+such as Cellular or a Personal Hotspot (on Apple platforms).
+
+#### network_is_constrained
+
+!!! question "Since sing-box 1.11.0"
+
+!!! quote ""
+
+    Only supported in graphical clients on Apple platforms.
+
+Match if network is in Low Data Mode.
+
 #### wifi_ssid
 
 !!! quote ""

+ 258 - 0
docs/configuration/rule-set/headless-rule.zh.md

@@ -0,0 +1,258 @@
+---
+icon: material/new-box
+---
+
+!!! quote "sing-box 1.11.0 中的更改"
+
+    :material-plus: [network_type](#network_type)  
+    :material-alert: [network_is_expensive](#network_is_expensive)  
+    :material-alert: [network_is_constrained](#network_is_constrained)
+
+### 结构
+
+!!! question "自 sing-box 1.8.0 起"
+
+```json
+{
+  "rules": [
+    {
+      "query_type": [
+        "A",
+        "HTTPS",
+        32768
+      ],
+      "network": [
+        "tcp"
+      ],
+      "domain": [
+        "test.com"
+      ],
+      "domain_suffix": [
+        ".cn"
+      ],
+      "domain_keyword": [
+        "test"
+      ],
+      "domain_regex": [
+        "^stun\\..+"
+      ],
+      "source_ip_cidr": [
+        "10.0.0.0/24",
+        "192.168.0.1"
+      ],
+      "ip_cidr": [
+        "10.0.0.0/24",
+        "192.168.0.1"
+      ],
+      "source_port": [
+        12345
+      ],
+      "source_port_range": [
+        "1000:2000",
+        ":3000",
+        "4000:"
+      ],
+      "port": [
+        80,
+        443
+      ],
+      "port_range": [
+        "1000:2000",
+        ":3000",
+        "4000:"
+      ],
+      "process_name": [
+        "curl"
+      ],
+      "process_path": [
+        "/usr/bin/curl"
+      ],
+      "process_path_regex": [
+        "^/usr/bin/.+"
+      ],
+      "package_name": [
+        "com.termux"
+      ],
+      "network_type": [
+        "wifi"
+      ],
+      "network_is_expensive": false,
+      "network_is_constrained": false,
+      "wifi_ssid": [
+        "My WIFI"
+      ],
+      "wifi_bssid": [
+        "00:00:00:00:00:00"
+      ],
+      "invert": false
+    },
+    {
+      "type": "logical",
+      "mode": "and",
+      "rules": [],
+      "invert": false
+    }
+  ]
+}
+```
+
+!!! note ""
+
+    当内容只有一项时,可以忽略 JSON 数组 [] 标签。
+
+### Default Fields
+
+!!! note ""
+
+    默认规则使用以下匹配逻辑:  
+    (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `ip_cidr`) &&  
+    (`port` || `port_range`) &&  
+    (`source_port` || `source_port_range`) &&  
+    `other fields`
+
+#### query_type
+
+DNS 查询类型。值可以为整数或者类型名称字符串。
+
+#### network
+
+`tcp` 或 `udp`。
+
+#### domain
+
+匹配完整域名。
+
+#### domain_suffix
+
+匹配域名后缀。
+
+#### domain_keyword
+
+匹配域名关键字。
+
+#### domain_regex
+
+匹配域名正则表达式。
+
+#### source_ip_cidr
+
+匹配源 IP CIDR。
+
+#### ip_cidr
+
+匹配 IP CIDR。
+
+#### source_port
+
+匹配源端口。
+
+#### source_port_range
+
+匹配源端口范围。
+
+#### port
+
+匹配端口。
+
+#### port_range
+
+匹配端口范围。
+
+#### process_name
+
+!!! quote ""
+
+    仅支持 Linux、Windows 和 macOS。
+
+匹配进程名称。
+
+#### process_path
+
+!!! quote ""
+
+    仅支持 Linux、Windows 和 macOS.
+
+匹配进程路径。
+
+#### process_path_regex
+
+!!! question "自 sing-box 1.10.0 起"
+
+!!! quote ""
+
+    仅支持 Linux、Windows 和 macOS.
+
+使用正则表达式匹配进程路径。
+
+#### package_name
+
+匹配 Android 应用包名。
+
+#### network_type
+
+!!! question "自 sing-box 1.11.0 起"
+
+!!! quote ""
+
+    仅在 Android 与 Apple 平台图形客户端中支持。
+
+匹配网络类型。
+
+Available values: `wifi`, `cellular`, `ethernet` and `other`.
+
+#### network_is_expensive
+
+!!! question "自 sing-box 1.11.0 起"
+
+!!! quote ""
+
+    仅在 Android 与 Apple 平台图形客户端中支持。
+
+匹配如果网络被视为计费 (在 Android) 或被视为昂贵,
+像蜂窝网络或个人热点 (在 Apple 平台)。
+
+#### network_is_constrained
+
+!!! question "自 sing-box 1.11.0 起"
+
+!!! quote ""
+
+    仅在 Apple 平台图形客户端中支持。
+
+匹配如果网络在低数据模式下。
+
+#### wifi_ssid
+
+!!! quote ""
+
+    仅在 Android 与 Apple 平台图形客户端中支持。
+
+匹配 WiFi SSID。
+
+#### wifi_bssid
+
+!!! quote ""
+
+    仅在 Android 与 Apple 平台图形客户端中支持。
+
+#### invert
+
+反选匹配结果。
+
+### 逻辑字段
+
+#### type
+
+`logical`
+
+#### mode
+
+==必填==
+
+`and` 或 `or`
+
+#### rules
+
+==必填==
+
+包括的规则。

+ 1 - 1
docs/configuration/rule-set/index.md

@@ -74,7 +74,7 @@ Tag of rule-set.
 
 ==Required==
 
-List of [Headless Rule](./headless-rule.md/).
+List of [Headless Rule](../headless-rule/).
 
 ### Local or Remote Fields
 

+ 117 - 0
docs/configuration/rule-set/index.zh.md

@@ -0,0 +1,117 @@
+---
+icon: material/new-box
+---
+
+!!! quote "sing-box 1.10.0 中的更改"
+
+    :material-plus: `type: inline`
+
+# 规则集
+
+!!! question "自 sing-box 1.8.0 起"
+
+### 结构
+
+=== "内联"
+
+    !!! question "自 sing-box 1.10.0 起"
+
+    ```json
+    {
+      "type": "inline", // 可选
+      "tag": "",
+      "rules": []
+    }
+    ```
+
+=== "本地文件"
+
+    ```json
+    {
+      "type": "local",
+      "tag": "",
+      "format": "source", // or binary
+      "path": ""
+    }
+    ```
+
+=== "远程文件"
+
+    !!! info ""
+    
+        远程规则集将被缓存如果 `experimental.cache_file.enabled` 已启用。
+
+    ```json
+    {
+      "type": "remote",
+      "tag": "",
+      "format": "source", // or binary
+      "url": "",
+      "download_detour": "", // 可选
+      "update_interval": "" // 可选
+    }
+    ```
+
+### 字段
+
+#### type
+
+==必填==
+
+规则集类型, `local` 或 `remote`。
+
+#### tag
+
+==必填==
+
+规则集的标签。
+
+### 内联字段
+
+!!! question "自 sing-box 1.10.0 起"
+
+#### rules
+
+==必填==
+
+一组 [无头规则](../headless-rule/).
+
+### 本地或远程字段
+
+#### format
+
+==必填==
+
+规则集格式, `source` 或 `binary`。
+
+### 本地字段
+
+#### path
+
+==必填==
+
+!!! note ""
+
+    自 sing-box 1.10.0 起,文件更改时将自动重新加载。
+
+规则集的文件路径。
+
+### 远程字段
+
+#### url
+
+==必填==
+
+规则集的下载 URL。
+
+#### download_detour
+
+用于下载规则集的出站的标签。
+
+如果为空,将使用默认出站。
+
+#### update_interval
+
+规则集的更新间隔。
+
+默认使用 `1d`。

+ 10 - 11
docs/configuration/rule-set/source-format.md

@@ -4,6 +4,10 @@ icon: material/new-box
 
 # Source Format
 
+!!! quote "Changes in sing-box 1.11.0"
+
+    :material-plus: version `3`
+
 !!! quote "Changes in sing-box 1.10.0"
 
     :material-plus: version `2`
@@ -14,7 +18,7 @@ icon: material/new-box
 
 ```json
 {
-  "version": 2,
+  "version": 3,
   "rules": []
 }
 ```
@@ -29,19 +33,14 @@ Use `sing-box rule-set compile [--output <file-name>.srs] <file-name>.json` to c
 
 ==Required==
 
-Version of rule-set, one of `1` or `2`.
-
-* 1: Initial rule-set version, since sing-box 1.8.0.
-* 2: Optimized memory usages of `domain_suffix` rules.
-
-The new rule-set version `2` does not make any changes to the format, only affecting `binary` rule-sets compiled by command `rule-set compile`
-
-Since 1.10.0, the optimization is always applied to `source` rule-sets even if version is set to `1`.
+Version of rule-set.
 
-It is recommended to upgrade to `2` after sing-box 1.10.0 becomes a stable version.
+* 1: sing-box 1.8.0: Initial rule-set version.
+* 2: sing-box 1.10.0: Optimized memory usages of `domain_suffix` rules in binary rule-sets.
+* 3: sing-box 1.11.0: Added `network_type`, `network_is_expensive` and `network_is_constrainted` rule items.
 
 #### rules
 
 ==Required==
 
-List of [Headless Rule](./headless-rule.md/).
+List of [Headless Rule](../headless-rule/).

+ 46 - 0
docs/configuration/rule-set/source-format.zh.md

@@ -0,0 +1,46 @@
+---
+icon: material/new-box
+---
+
+# 源文件格式
+
+!!! quote "sing-box 1.11.0 中的更改"
+
+    :material-plus: version `3`
+
+!!! quote "sing-box 1.10.0 中的更改"
+
+    :material-plus: version `2`
+
+!!! question "自 sing-box 1.8.0 起"
+
+### 结构
+
+```json
+{
+  "version": 3,
+  "rules": []
+}
+```
+
+### 编译
+
+使用 `sing-box rule-set compile [--output <file-name>.srs] <file-name>.json` 以编译源文件为二进制规则集。
+
+### 字段
+
+#### version
+
+==必填==
+
+规则集版本。
+
+* 1: sing-box 1.8.0: 初始规则集版本。
+* 2: sing-box 1.10.0: 优化了二进制规则集中 `domain_suffix` 规则的内存使用。
+* 3: sing-box 1.11.0: 添加了 `network_type`、 `network_is_expensive` 和 `network_is_constrainted` 规则项。
+
+#### rules
+
+==必填==
+
+一组 [无头规则](../headless-rule/).

+ 3 - 0
option/rule.go

@@ -95,6 +95,9 @@ type RawDefaultRule struct {
 	User                     badoption.Listable[string] `json:"user,omitempty"`
 	UserID                   badoption.Listable[int32]  `json:"user_id,omitempty"`
 	ClashMode                string                     `json:"clash_mode,omitempty"`
+	NetworkType              badoption.Listable[string] `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"`
 	RuleSet                  badoption.Listable[string] `json:"rule_set,omitempty"`

+ 3 - 0
option/rule_dns.go

@@ -97,6 +97,9 @@ type RawDefaultDNSRule struct {
 	UserID                   badoption.Listable[int32]        `json:"user_id,omitempty"`
 	Outbound                 badoption.Listable[string]       `json:"outbound,omitempty"`
 	ClashMode                string                           `json:"clash_mode,omitempty"`
+	NetworkType              badoption.Listable[string]       `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"`
 	RuleSet                  badoption.Listable[string]       `json:"rule_set,omitempty"`

+ 25 - 22
option/rule_set.go

@@ -146,25 +146,28 @@ func (r HeadlessRule) IsValid() bool {
 }
 
 type DefaultHeadlessRule struct {
-	QueryType        badoption.Listable[DNSQueryType] `json:"query_type,omitempty"`
-	Network          badoption.Listable[string]       `json:"network,omitempty"`
-	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"`
-	WIFISSID         badoption.Listable[string]       `json:"wifi_ssid,omitempty"`
-	WIFIBSSID        badoption.Listable[string]       `json:"wifi_bssid,omitempty"`
-	Invert           bool                             `json:"invert,omitempty"`
+	QueryType            badoption.Listable[DNSQueryType] `json:"query_type,omitempty"`
+	Network              badoption.Listable[string]       `json:"network,omitempty"`
+	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"`
+	NetworkType          badoption.Listable[string]       `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"`
+	Invert               bool                             `json:"invert,omitempty"`
 
 	DomainMatcher *domain.Matcher `json:"-"`
 	SourceIPSet   *netipx.IPSet   `json:"-"`
@@ -200,7 +203,7 @@ type PlainRuleSetCompat _PlainRuleSetCompat
 func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) {
 	var v any
 	switch r.Version {
-	case C.RuleSetVersion1, C.RuleSetVersion2:
+	case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3:
 		v = r.Options
 	default:
 		return nil, E.New("unknown rule-set version: ", r.Version)
@@ -215,7 +218,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error {
 	}
 	var v any
 	switch r.Version {
-	case C.RuleSetVersion1, C.RuleSetVersion2:
+	case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3:
 		v = &r.Options
 	case 0:
 		return E.New("missing rule-set version")
@@ -231,7 +234,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error {
 
 func (r PlainRuleSetCompat) Upgrade() (PlainRuleSet, error) {
 	switch r.Version {
-	case C.RuleSetVersion1, C.RuleSetVersion2:
+	case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3:
 	default:
 		return PlainRuleSet{}, E.New("unknown rule-set version: " + F.ToString(r.Version))
 	}

+ 15 - 0
route/rule/rule_default.go

@@ -223,6 +223,21 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio
 		rule.items = append(rule.items, item)
 		rule.allItems = append(rule.allItems, item)
 	}
+	if len(options.NetworkType) > 0 {
+		item := NewNetworkTypeItem(networkManager, options.NetworkType)
+		rule.items = append(rule.items, item)
+		rule.allItems = append(rule.allItems, item)
+	}
+	if options.NetworkIsExpensive {
+		item := NewNetworkIsExpensiveItem(networkManager)
+		rule.items = append(rule.items, item)
+		rule.allItems = append(rule.allItems, item)
+	}
+	if options.NetworkIsConstrained {
+		item := NewNetworkIsConstrainedItem(networkManager)
+		rule.items = append(rule.items, item)
+		rule.allItems = append(rule.allItems, item)
+	}
 	if len(options.WIFISSID) > 0 {
 		item := NewWIFISSIDItem(networkManager, options.WIFISSID)
 		rule.items = append(rule.items, item)

+ 15 - 0
route/rule/rule_dns.go

@@ -220,6 +220,21 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
 		rule.items = append(rule.items, item)
 		rule.allItems = append(rule.allItems, item)
 	}
+	if len(options.NetworkType) > 0 {
+		item := NewNetworkTypeItem(networkManager, options.NetworkType)
+		rule.items = append(rule.items, item)
+		rule.allItems = append(rule.allItems, item)
+	}
+	if options.NetworkIsExpensive {
+		item := NewNetworkIsExpensiveItem(networkManager)
+		rule.items = append(rule.items, item)
+		rule.allItems = append(rule.allItems, item)
+	}
+	if options.NetworkIsConstrained {
+		item := NewNetworkIsConstrainedItem(networkManager)
+		rule.items = append(rule.items, item)
+		rule.allItems = append(rule.allItems, item)
+	}
 	if len(options.WIFISSID) > 0 {
 		item := NewWIFISSIDItem(networkManager, options.WIFISSID)
 		rule.items = append(rule.items, item)

+ 20 - 5
route/rule/rule_headless.go

@@ -140,18 +140,33 @@ func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessR
 		rule.items = append(rule.items, item)
 		rule.allItems = append(rule.allItems, item)
 	}
-	if len(options.WIFISSID) > 0 {
-		if networkManager != nil {
+	if networkManager != nil {
+		if len(options.NetworkType) > 0 {
+			item := NewNetworkTypeItem(networkManager, options.NetworkType)
+			rule.items = append(rule.items, item)
+			rule.allItems = append(rule.allItems, item)
+		}
+		if options.NetworkIsExpensive {
+			item := NewNetworkIsExpensiveItem(networkManager)
+			rule.items = append(rule.items, item)
+			rule.allItems = append(rule.allItems, item)
+		}
+		if options.NetworkIsConstrained {
+			item := NewNetworkIsConstrainedItem(networkManager)
+			rule.items = append(rule.items, item)
+			rule.allItems = append(rule.allItems, item)
+		}
+		if len(options.WIFISSID) > 0 {
 			item := NewWIFISSIDItem(networkManager, options.WIFISSID)
 			rule.items = append(rule.items, item)
 			rule.allItems = append(rule.allItems, item)
+
 		}
-	}
-	if len(options.WIFIBSSID) > 0 {
-		if networkManager != nil {
+		if len(options.WIFIBSSID) > 0 {
 			item := NewWIFIBSSIDItem(networkManager, options.WIFIBSSID)
 			rule.items = append(rule.items, item)
 			rule.allItems = append(rule.allItems, item)
+
 		}
 	}
 	if len(options.AdGuardDomain) > 0 {

+ 29 - 0
route/rule/rule_item_network_is_constrained.go

@@ -0,0 +1,29 @@
+package rule
+
+import (
+	"github.com/sagernet/sing-box/adapter"
+)
+
+var _ RuleItem = (*NetworkIsConstrainedItem)(nil)
+
+type NetworkIsConstrainedItem struct {
+	networkManager adapter.NetworkManager
+}
+
+func NewNetworkIsConstrainedItem(networkManager adapter.NetworkManager) *NetworkIsConstrainedItem {
+	return &NetworkIsConstrainedItem{
+		networkManager: networkManager,
+	}
+}
+
+func (r *NetworkIsConstrainedItem) Match(metadata *adapter.InboundContext) bool {
+	networkInterface := r.networkManager.DefaultNetworkInterface()
+	if networkInterface == nil {
+		return false
+	}
+	return networkInterface.Constrained
+}
+
+func (r *NetworkIsConstrainedItem) String() string {
+	return "network_is_expensive=true"
+}

+ 29 - 0
route/rule/rule_item_network_is_expensive.go

@@ -0,0 +1,29 @@
+package rule
+
+import (
+	"github.com/sagernet/sing-box/adapter"
+)
+
+var _ RuleItem = (*NetworkIsExpensiveItem)(nil)
+
+type NetworkIsExpensiveItem struct {
+	networkManager adapter.NetworkManager
+}
+
+func NewNetworkIsExpensiveItem(networkManager adapter.NetworkManager) *NetworkIsExpensiveItem {
+	return &NetworkIsExpensiveItem{
+		networkManager: networkManager,
+	}
+}
+
+func (r *NetworkIsExpensiveItem) Match(metadata *adapter.InboundContext) bool {
+	networkInterface := r.networkManager.DefaultNetworkInterface()
+	if networkInterface == nil {
+		return false
+	}
+	return networkInterface.Expensive
+}
+
+func (r *NetworkIsExpensiveItem) String() string {
+	return "network_is_expensive=true"
+}

+ 39 - 0
route/rule/rule_item_network_type.go

@@ -0,0 +1,39 @@
+package rule
+
+import (
+	"strings"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing/common"
+	F "github.com/sagernet/sing/common/format"
+)
+
+var _ RuleItem = (*NetworkTypeItem)(nil)
+
+type NetworkTypeItem struct {
+	networkManager adapter.NetworkManager
+	networkType    []string
+}
+
+func NewNetworkTypeItem(networkManager adapter.NetworkManager, networkType []string) *NetworkTypeItem {
+	return &NetworkTypeItem{
+		networkManager: networkManager,
+		networkType:    networkType,
+	}
+}
+
+func (r *NetworkTypeItem) Match(metadata *adapter.InboundContext) bool {
+	networkInterface := r.networkManager.DefaultNetworkInterface()
+	if networkInterface == nil {
+		return false
+	}
+	return common.Contains(r.networkType, networkInterface.Type)
+}
+
+func (r *NetworkTypeItem) String() string {
+	if len(r.networkType) == 1 {
+		return F.ToString("network_type=", r.networkType[0])
+	} else {
+		return F.ToString("network_type=", "["+strings.Join(F.MapToString(r.networkType), " ")+"]")
+	}
+}