⛄ Sync 2026-07-31 00:26:24

This commit is contained in:
github-actions[bot] committed 2026-07-31 00:26:24 +08:00
1 parent 47d0743808
commit b44a4364c4
125 files changed
+3954 -140418

No files matched your search

@@ -0,0 +1,130 @@
[
{
"name": "shadowsocks-basic",
"description": "普通 Shadowsocks 节点生成单个 outbound",
"generator_fixture": true,
"expects": {
"outbounds": ["main-out"],
"diagnostics": []
}
},
{
"name": "shadowsocks-shadowtls",
"description": "Shadowsocks + ShadowTLS 生成 detour 链路,并隐藏中间层 tag",
"generator_fixture": true,
"expects": {
"outbounds": ["main-out", "main-out-shadowtls"],
"tag_map": true,
"diagnostics": []
}
},
{
"name": "selector-manual-nodes",
"description": "Selector 手选代理节点生成 selector outbound",
"generator_fixture": true,
"expects": {
"outbound_type": "selector",
"diagnostics": []
}
},
{
"name": "selector-regex-nodes",
"description": "Selector 通过节点正则展开代理节点,并受节点数量上限保护",
"generator_fixture": true,
"expects": {
"node_filter_limit": 500,
"diagnostics": []
}
},
{
"name": "selector-references-urltest",
"description": "Selector 引用 URLTest 路由节点时补齐实际代理节点依赖",
"generator_fixture": true,
"expects": {
"outbound_type": "selector",
"contains_nested_outbound": true,
"diagnostics": []
}
},
{
"name": "disabled-routing-node",
"description": "禁用的 routing_node 不能作为有效路由节点解析,引用时回退到 block-out 并产生 warning",
"generator_fixture": true,
"expects": {
"route_final": "block-out",
"diagnostics": ["warning"]
}
},
{
"name": "routing-cycle",
"description": "循环引用路由节点应产生 error 诊断且不覆盖旧运行配置",
"generator_fixture": true,
"expects": {
"diagnostics": ["error"],
"write_runtime_config": false
}
},
{
"name": "selector-cycle",
"description": "Selector 节点列表互相引用时应产生 error 诊断且不覆盖旧运行配置",
"generator_fixture": true,
"expects": {
"diagnostics": ["error"],
"write_runtime_config": false
}
},
{
"name": "routing-rule-direct-node",
"description": "路由规则直选具体节点时自动补齐该节点 outbound",
"generator_fixture": true,
"expects": {
"route_rule_outbound": true,
"required_outbound": true
}
},
{
"name": "gfwlist-default-outbound-fallback",
"description": "非 custom 模式下 main_node 为空时仍可回退使用 default_outbound 生成运行配置",
"generator_fixture": true,
"expects": {
"default_outbound_fallback": true,
"route_final": true
}
},
{
"name": "duplicate-label",
"description": "同名 label 通过稳定后缀生成唯一 readable tag",
"generator_fixture": true,
"expects": {
"unique_tags": true,
"tag_map": true
}
},
{
"name": "reserved-tag-label",
"description": "label 与保留 tag 冲突时生成安全后缀",
"generator_fixture": true,
"expects": {
"unique_tags": true,
"reserved_tag_avoided": true
}
},
{
"name": "unsupported-certificate-pin",
"description": "订阅节点包含证书 pin 且 sing-box 不支持时默认跳过节点",
"generator_fixture": false,
"expects": {
"subscription_skip_node": true,
"tls_insecure": false
}
},
{
"name": "remove-subscription-node-references",
"description": "删除订阅节点时清理 main、routing、dns server、dns rule、ruleset 引用",
"generator_fixture": false,
"expects": {
"references_cleaned": true,
"uci_commit": true
}
}
]
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
const casesPath = path.join(__dirname, 'cases.json');
const cases = JSON.parse(fs.readFileSync(casesPath, 'utf8'));
const fixturesDir = path.join(__dirname, 'fixtures');
const requiredCases = [
'shadowsocks-basic',
'shadowsocks-shadowtls',
'selector-manual-nodes',
'selector-regex-nodes',
'selector-references-urltest',
'disabled-routing-node',
'routing-cycle',
'selector-cycle',
'routing-rule-direct-node',
'gfwlist-default-outbound-fallback',
'duplicate-label',
'reserved-tag-label',
'unsupported-certificate-pin',
'remove-subscription-node-references'
];
function assert(condition, message) {
if (!condition) {
console.error(message);
process.exitCode = 1;
}
}
function renameTagValue(value, rename) {
if (typeof value === 'string')
return Object.prototype.hasOwnProperty.call(rename, value) ? rename[value] : value;
if (Array.isArray(value))
return value.map((item) => (
item && typeof item === 'object' && !Array.isArray(item)
? renameTags(item, rename)
: renameTagValue(item, rename)
));
return value;
}
function renameTags(value, rename) {
if (Array.isArray(value))
return value.map((item) => renameTags(item, rename));
if (value && typeof value === 'object') {
for (const key of Object.keys(value)) {
if (['tag', 'outbound', 'outbounds', 'detour', 'default', 'final', 'download_detour'].includes(key))
value[key] = renameTagValue(value[key], rename);
else
value[key] = renameTags(value[key], rename);
}
}
return value;
}
assert(Array.isArray(cases), 'cases.json must contain an array');
const names = new Set();
for (const item of cases) {
assert(item && typeof item === 'object', 'each case must be an object');
assert(typeof item.name === 'string' && item.name.length > 0, 'case must have a name');
assert(!names.has(item.name), `duplicate case name: ${item.name}`);
names.add(item.name);
assert(typeof item.description === 'string' && item.description.length > 0, `${item.name}: missing description`);
assert(item.expects && typeof item.expects === 'object' && !Array.isArray(item.expects), `${item.name}: missing expects object`);
if (item.generator_fixture) {
const fixtureDir = path.join(fixturesDir, item.name);
const caseMetaPath = path.join(fixtureDir, 'case.json');
const configPath = path.join(fixtureDir, 'homeproxy');
const diagnosticsPath = path.join(fixtureDir, 'expected', 'config-diagnostics.json');
assert(fs.existsSync(configPath), `${item.name}: missing generator fixture homeproxy config`);
assert(fs.existsSync(caseMetaPath), `${item.name}: missing generator fixture case.json`);
assert(fs.existsSync(diagnosticsPath), `${item.name}: missing expected config diagnostics`);
if (fs.existsSync(caseMetaPath)) {
const meta = JSON.parse(fs.readFileSync(caseMetaPath, 'utf8'));
assert(typeof meta.success === 'boolean', `${item.name}: case.json must define boolean success`);
if (meta.success)
assert(fs.existsSync(path.join(fixtureDir, 'expected', 'sing-box-c.json')), `${item.name}: missing expected sing-box-c.json`);
}
}
}
for (const name of requiredCases)
assert(names.has(name), `missing required generator golden case: ${name}`);
const renameRegression = renameTags({
outbounds: [
{ type: 'selector', tag: 'Proxy_Selector', outbounds: ['Proxy_Auto', 'direct-out'] },
{ type: 'urltest', tag: 'Proxy_Auto', outbounds: ['node-a'] }
],
route: {
final: 'Proxy_Selector',
rules: [{ outbound: 'Proxy_Auto' }]
}
}, {
Proxy_Selector: '♻️ 节点选择',
Proxy_Auto: 'Proxy_Auto 标签',
'node-a': '节点 A'
});
assert(renameRegression.route.final === '♻️ 节点选择', 'rename_tags regression: route.final not renamed');
assert(renameRegression.outbounds[0].tag === '♻️ 节点选择', 'rename_tags regression: top-level outbound tag not renamed');
assert(renameRegression.outbounds[0].outbounds[0] === 'Proxy_Auto 标签', 'rename_tags regression: selector outbound list not renamed');
assert(renameRegression.outbounds[1].tag === 'Proxy_Auto 标签', 'rename_tags regression: nested outbound object not renamed');
if (!process.exitCode)
console.log(`generator golden cases: ${cases.length} ok`);
@@ -0,0 +1,3 @@
{
"success": true
}
@@ -0,0 +1,9 @@
{
"items": [
{
"type": "warning",
"message": "默认出站 引用了已删除或无效的节点:rn_disabled,已在本次生成中回退为 block-out。",
"suggestion": "请进入 LuCI 界面检查路由、DNS、规则集中的出站引用"
}
]
}
@@ -0,0 +1,98 @@
{
"log": {
"disabled": false,
"level": "warn",
"output": "__TEST_ROOT__/var/run/homeproxy/sing-box-c.log",
"timestamp": true
},
"ntp": {
"enabled": true,
"server": "time.apple.com",
"detour": "direct-out",
"domain_resolver": "default-dns"
},
"dns": {
"servers": [
{
"tag": "default-dns",
"type": "udp",
"server": "223.5.5.5",
"detour": "direct-out"
},
{
"tag": "system-dns",
"type": "local",
"detour": "direct-out"
}
],
"strategy": "ipv4_only",
"final": "default-dns"
},
"inbounds": [
{
"type": "direct",
"tag": "dns-in",
"listen": "::",
"listen_port": 5333
},
{
"type": "mixed",
"tag": "mixed-in",
"listen": "::",
"listen_port": 5330,
"sniff": true,
"sniff_override_destination": true,
"set_system_proxy": false
},
{
"type": "redirect",
"tag": "redirect-in",
"listen": "::",
"listen_port": 5331,
"sniff": true,
"sniff_override_destination": true
},
{
"type": "tproxy",
"tag": "tproxy-in",
"listen": "::",
"listen_port": 5332,
"network": "udp",
"sniff": true,
"sniff_override_destination": true
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct-out",
"routing_mark": 100
},
{
"type": "block",
"tag": "block-out"
}
],
"route": {
"rules": [
{
"inbound": "dns-in",
"action": "hijack-dns"
}
],
"rule_set": [
],
"auto_detect_interface": true,
"default_domain_resolver": {
"action": "resolve",
"server": "default-dns"
},
"final": "block-out"
},
"experimental": {
"cache_file": {
"enabled": true,
"path": "__TEST_ROOT__/var/run/homeproxy/cache.db"
}
}
}
@@ -0,0 +1,39 @@
config homeproxy 'config'
option routing_mode 'custom'
option proxy_mode 'redirect_tproxy'
option ipv6_support '0'
option log_level 'warn'
option clash_api_enabled '0'
config homeproxy 'infra'
option mixed_port '5330'
option redirect_port '5331'
option tproxy_port '5332'
option self_mark '100'
config homeproxy 'control'
config homeproxy 'dns'
option default_strategy 'ipv4_only'
option default_server 'default-dns'
option disable_cache '0'
option disable_cache_expire '0'
config homeproxy 'routing'
option default_outbound 'rn_disabled'
option default_outbound_dns 'default-dns'
option sniff_override '1'
config routing_node 'rn_disabled'
option enabled '0'
option label 'Disabled Selector'
option node 'selector'
list selector_nodes 'n1'
config node 'n1'
option type 'shadowsocks'
option label 'Disabled Child'
option address 'disabled-child.example'
option port '8388'
option shadowsocks_encrypt_method '2022-blake3-aes-128-gcm'
option password 'disabled-child-password'
@@ -0,0 +1,3 @@
{
"success": true
}
@@ -0,0 +1,183 @@
{
"log": {
"disabled": false,
"level": "warn",
"output": "__TEST_ROOT__/var/run/homeproxy/sing-box-c.log",
"timestamp": true
},
"ntp": {
"enabled": true,
"server": "time.apple.com",
"detour": "direct-out",
"domain_resolver": "default-dns"
},
"dns": {
"servers": [
{
"tag": "default-dns",
"type": "udp",
"server": "223.5.5.5",
"detour": "direct-out"
},
{
"tag": "system-dns",
"type": "local",
"detour": "direct-out"
},
{
"tag": "main-dns",
"domain_resolver": {
"server": "default-dns",
"strategy": "ipv4_only"
},
"detour": "main-out",
"type": "tcp",
"server": "223.5.5.5"
},
{
"tag": "china-dns",
"domain_resolver": {
"server": "default-dns",
"strategy": "prefer_ipv6"
},
"detour": "direct-out",
"type": "udp",
"server": "223.5.5.5"
}
],
"rules": [
{
"rule_set": "geosite-cn",
"action": "route",
"server": "china-dns",
"strategy": "prefer_ipv6"
},
{
"type": "logical",
"mode": "and",
"rules": [
{
"rule_set": "geosite-noncn",
"invert": true
},
{
"rule_set": "geoip-cn"
}
],
"action": "route",
"server": "china-dns",
"strategy": "prefer_ipv6"
}
],
"strategy": "ipv4_only",
"final": "main-dns"
},
"inbounds": [
{
"type": "direct",
"tag": "dns-in",
"listen": "::",
"listen_port": 5333
},
{
"type": "mixed",
"tag": "mixed-in",
"listen": "::",
"listen_port": 5330,
"sniff": true,
"sniff_override_destination": true,
"set_system_proxy": false
},
{
"type": "redirect",
"tag": "redirect-in",
"listen": "::",
"listen_port": 5331,
"sniff": true,
"sniff_override_destination": true
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct-out",
"routing_mark": 100
},
{
"type": "block",
"tag": "block-out"
},
{
"type": "urltest",
"tag": "main-out",
"outbounds": [
"Duplicate Label",
"Duplicate Label #n2"
],
"interval": "300s",
"tolerance": 50
},
{
"type": "shadowsocks",
"tag": "Duplicate Label",
"routing_mark": 100,
"server": "duplicate-a.example",
"server_port": 8388,
"password": "duplicate-a-password",
"method": "2022-blake3-aes-128-gcm"
},
{
"type": "shadowsocks",
"tag": "Duplicate Label #n2",
"routing_mark": 100,
"server": "duplicate-b.example",
"server_port": 8389,
"password": "duplicate-b-password",
"method": "2022-blake3-aes-128-gcm"
}
],
"route": {
"rules": [
{
"inbound": "dns-in",
"action": "hijack-dns"
}
],
"rule_set": [
{
"type": "remote",
"tag": "geoip-cn",
"format": "binary",
"url": "https://fastly.jsdelivr.net/gh/1715173329/IPCIDR-CHINA@rule-set/cn.srs",
"download_detour": "main-out"
},
{
"type": "remote",
"tag": "geosite-cn",
"format": "binary",
"url": "https://fastly.jsdelivr.net/gh/1715173329/sing-geosite@rule-set-unstable/geosite-geolocation-cn.srs",
"download_detour": "main-out"
},
{
"type": "remote",
"tag": "geosite-noncn",
"format": "binary",
"url": "https://fastly.jsdelivr.net/gh/1715173329/sing-geosite@rule-set-unstable/geosite-geolocation-!cn.srs",
"download_detour": "main-out"
}
],
"auto_detect_interface": true,
"default_domain_resolver": {
"action": "route",
"server": "china-dns",
"strategy": "prefer_ipv4"
},
"final": "main-out"
},
"experimental": {
"cache_file": {
"enabled": true,
"path": "__TEST_ROOT__/var/run/homeproxy/cache.db"
}
}
}
@@ -0,0 +1,35 @@
config homeproxy 'config'
option routing_mode 'bypass_mainland_china'
option proxy_mode 'redirect'
option ipv6_support '0'
option main_node 'urltest'
option main_udp_node 'same'
option log_level 'warn'
option clash_api_enabled '0'
list main_urltest_nodes 'n1'
list main_urltest_nodes 'n2'
option main_urltest_interval '300'
option main_urltest_tolerance '50'
config homeproxy 'infra'
option mixed_port '5330'
option redirect_port '5331'
option self_mark '100'
config homeproxy 'control'
config node 'n1'
option type 'shadowsocks'
option label 'Duplicate Label'
option address 'duplicate-a.example'
option port '8388'
option shadowsocks_encrypt_method '2022-blake3-aes-128-gcm'
option password 'duplicate-a-password'
config node 'n2'
option type 'shadowsocks'
option label 'Duplicate Label'
option address 'duplicate-b.example'
option port '8389'
option shadowsocks_encrypt_method '2022-blake3-aes-128-gcm'
option password 'duplicate-b-password'
@@ -0,0 +1,3 @@
{
"success": true
}
@@ -0,0 +1,125 @@
{
"log": {
"disabled": false,
"level": "warn",
"output": "__TEST_ROOT__/var/run/homeproxy/sing-box-c.log",
"timestamp": true
},
"ntp": {
"enabled": true,
"server": "time.apple.com",
"detour": "direct-out",
"domain_resolver": "default-dns"
},
"dns": {
"servers": [
{
"tag": "default-dns",
"type": "udp",
"server": "223.5.5.5",
"detour": "direct-out"
},
{
"tag": "system-dns",
"type": "local",
"detour": "direct-out"
},
{
"tag": "cfg-Proxy_DNS-dns",
"type": "https",
"server": "1.1.1.1",
"path": "/dns-query",
"detour": "Proxy Auto"
}
],
"strategy": "prefer_ipv4",
"final": "cfg-Proxy_DNS-dns"
},
"inbounds": [
{
"type": "direct",
"tag": "dns-in",
"listen": "::",
"listen_port": 5333
},
{
"type": "mixed",
"tag": "mixed-in",
"listen": "::",
"listen_port": 5330,
"sniff": true,
"sniff_override_destination": true,
"set_system_proxy": false
},
{
"type": "redirect",
"tag": "redirect-in",
"listen": "::",
"listen_port": 5331,
"sniff": true,
"sniff_override_destination": true
},
{
"type": "tproxy",
"tag": "tproxy-in",
"listen": "::",
"listen_port": 5332,
"network": "udp",
"sniff": true,
"sniff_override_destination": true
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct-out",
"routing_mark": 100
},
{
"type": "block",
"tag": "block-out"
},
{
"type": "selector",
"tag": "Proxy Selector",
"outbounds": [
"Proxy Auto"
]
},
{
"type": "urltest",
"tag": "Proxy Auto",
"outbounds": [
"Fallback Node"
],
"url": "https://www.gstatic.com/generate_204",
"interval": "300s",
"tolerance": 50
},
{
"type": "shadowsocks",
"tag": "Fallback Node",
"routing_mark": 100,
"server": "fallback-node.example",
"server_port": 8388,
"password": "fallback-node-password",
"method": "2022-blake3-aes-128-gcm"
}
],
"route": {
"rules": [
{
"inbound": "dns-in",
"action": "hijack-dns"
}
],
"rule_set": [
],
"auto_detect_interface": true,
"default_domain_resolver": {
"action": "resolve",
"server": "default-dns"
},
"final": "Proxy Selector"
}
}
@@ -0,0 +1,59 @@
config homeproxy 'config'
option routing_mode 'gfwlist'
option proxy_mode 'redirect_tproxy'
option ipv6_support '1'
option log_level 'warn'
option clash_api_enabled '0'
option main_node 'nil'
option main_udp_node 'nil'
option dns_server '8.8.8.8'
config homeproxy 'infra'
option mixed_port '5330'
option redirect_port '5331'
option tproxy_port '5332'
option dns_port '5333'
option self_mark '100'
config homeproxy 'control'
config homeproxy 'routing'
option default_outbound 'Proxy_Selector'
option default_outbound_dns 'default-dns'
option sniff_override '1'
option bypass_cn_traffic '0'
config homeproxy 'dns'
option dns_strategy 'prefer_ipv4'
option default_server 'Proxy_DNS'
config dns_server 'Proxy_DNS'
option label 'Proxy_DNS'
option enabled '1'
option type 'https'
option server '1.1.1.1'
option path '/dns-query'
option outbound 'Proxy_Auto'
config routing_node 'Proxy_Selector'
option enabled '1'
option label 'Proxy Selector'
option node 'selector'
list selector_nodes 'Proxy_Auto'
config routing_node 'Proxy_Auto'
option enabled '1'
option label 'Proxy Auto'
option node 'urltest'
list urltest_nodes 'n1'
option urltest_url 'https://www.gstatic.com/generate_204'
option urltest_interval '300'
option urltest_tolerance '50'
config node 'n1'
option type 'shadowsocks'
option label 'Fallback Node'
option address 'fallback-node.example'
option port '8388'
option shadowsocks_encrypt_method '2022-blake3-aes-128-gcm'
option password 'fallback-node-password'
@@ -0,0 +1,3 @@
{
"success": true
}
@@ -0,0 +1,164 @@
{
"log": {
"disabled": false,
"level": "warn",
"output": "__TEST_ROOT__/var/run/homeproxy/sing-box-c.log",
"timestamp": true
},
"ntp": {
"enabled": true,
"server": "time.apple.com",
"detour": "direct-out",
"domain_resolver": "default-dns"
},
"dns": {
"servers": [
{
"tag": "default-dns",
"type": "udp",
"server": "223.5.5.5",
"detour": "direct-out"
},
{
"tag": "system-dns",
"type": "local",
"detour": "direct-out"
},
{
"tag": "main-dns",
"domain_resolver": {
"server": "default-dns",
"strategy": "ipv4_only"
},
"detour": "main-out",
"type": "tcp",
"server": "223.5.5.5"
},
{
"tag": "china-dns",
"domain_resolver": {
"server": "default-dns",
"strategy": "prefer_ipv6"
},
"detour": "direct-out",
"type": "udp",
"server": "223.5.5.5"
}
],
"rules": [
{
"rule_set": "geosite-cn",
"action": "route",
"server": "china-dns",
"strategy": "prefer_ipv6"
},
{
"type": "logical",
"mode": "and",
"rules": [
{
"rule_set": "geosite-noncn",
"invert": true
},
{
"rule_set": "geoip-cn"
}
],
"action": "route",
"server": "china-dns",
"strategy": "prefer_ipv6"
}
],
"strategy": "ipv4_only",
"final": "main-dns"
},
"inbounds": [
{
"type": "direct",
"tag": "dns-in",
"listen": "::",
"listen_port": 5333
},
{
"type": "mixed",
"tag": "mixed-in",
"listen": "::",
"listen_port": 5330,
"sniff": true,
"sniff_override_destination": true,
"set_system_proxy": false
},
{
"type": "redirect",
"tag": "redirect-in",
"listen": "::",
"listen_port": 5331,
"sniff": true,
"sniff_override_destination": true
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct-out",
"routing_mark": 100
},
{
"type": "block",
"tag": "block-out"
},
{
"type": "shadowsocks",
"tag": "main-out",
"routing_mark": 100,
"server": "reserved.example",
"server_port": 8388,
"password": "reserved-password",
"method": "2022-blake3-aes-128-gcm"
}
],
"route": {
"rules": [
{
"inbound": "dns-in",
"action": "hijack-dns"
}
],
"rule_set": [
{
"type": "remote",
"tag": "geoip-cn",
"format": "binary",
"url": "https://fastly.jsdelivr.net/gh/1715173329/IPCIDR-CHINA@rule-set/cn.srs",
"download_detour": "main-out"
},
{
"type": "remote",
"tag": "geosite-cn",
"format": "binary",
"url": "https://fastly.jsdelivr.net/gh/1715173329/sing-geosite@rule-set-unstable/geosite-geolocation-cn.srs",
"download_detour": "main-out"
},
{
"type": "remote",
"tag": "geosite-noncn",
"format": "binary",
"url": "https://fastly.jsdelivr.net/gh/1715173329/sing-geosite@rule-set-unstable/geosite-geolocation-!cn.srs",
"download_detour": "main-out"
}
],
"auto_detect_interface": true,
"default_domain_resolver": {
"action": "route",
"server": "china-dns",
"strategy": "prefer_ipv4"
},
"final": "main-out"
},
"experimental": {
"cache_file": {
"enabled": true,
"path": "__TEST_ROOT__/var/run/homeproxy/cache.db"
}
}
}
@@ -0,0 +1,23 @@
config homeproxy 'config'
option routing_mode 'bypass_mainland_china'
option proxy_mode 'redirect'
option ipv6_support '0'
option main_node 'n1'
option main_udp_node 'same'
option log_level 'warn'
option clash_api_enabled '0'
config homeproxy 'infra'
option mixed_port '5330'
option redirect_port '5331'
option self_mark '100'
config homeproxy 'control'
config node 'n1'
option type 'shadowsocks'
option label 'direct-out'
option address 'reserved.example'
option port '8388'
option shadowsocks_encrypt_method '2022-blake3-aes-128-gcm'
option password 'reserved-password'
@@ -0,0 +1,3 @@
{
"success": false
}
@@ -0,0 +1,34 @@
{
"items": [
{
"type": "error",
"message": "路由节点配置错误:检测到循环引用\n循环路径: rn_b -> rn_a -> rn_b",
"suggestion": "进入 LuCI 界面 -> 服务 -> HomeProxy -> 路由节点,检查以下节点的\"出站\"配置,移除循环引用"
},
{
"type": "warning",
"message": "路由节点 Cycle A 的节点引用已失效,已在本次生成中临时回退为阻断出站。",
"suggestion": "请进入 LuCI 界面 -> 服务 -> HomeProxy -> 路由节点,重新选择有效节点"
},
{
"type": "error",
"message": "路由节点配置错误:检测到循环引用\n循环路径: rn_a -> rn_b -> rn_a",
"suggestion": "进入 LuCI 界面 -> 服务 -> HomeProxy -> 路由节点,检查以下节点的\"出站\"配置,移除循环引用"
},
{
"type": "warning",
"message": "路由节点 Cycle B 的节点引用已失效,已在本次生成中临时回退为阻断出站。",
"suggestion": "请进入 LuCI 界面 -> 服务 -> HomeProxy -> 路由节点,重新选择有效节点"
},
{
"type": "error",
"message": "路由节点配置错误:检测到循环引用\n循环路径: rn_a -> rn_b -> rn_a",
"suggestion": "进入 LuCI 界面 -> 服务 -> HomeProxy -> 路由节点,检查以下节点的\"出站\"配置,移除循环引用"
},
{
"type": "warning",
"message": "默认出站 引用了已删除或无效的节点:rn_a,已在本次生成中回退为 block-out。",
"suggestion": "请进入 LuCI 界面检查路由、DNS、规则集中的出站引用"
}
]
}
@@ -0,0 +1,35 @@
config homeproxy 'config'
option routing_mode 'custom'
option proxy_mode 'redirect_tproxy'
option ipv6_support '0'
option log_level 'warn'
option clash_api_enabled '0'
config homeproxy 'infra'
option mixed_port '5330'
option redirect_port '5331'
option tproxy_port '5332'
option self_mark '100'
config homeproxy 'control'
config homeproxy 'dns'
option default_strategy 'ipv4_only'
option default_server 'default-dns'
option disable_cache '0'
option disable_cache_expire '0'
config homeproxy 'routing'
option default_outbound 'rn_a'
option default_outbound_dns 'default-dns'
option sniff_override '1'
config routing_node 'rn_a'
option enabled '1'
option label 'Cycle A'
option node 'rn_b'
config routing_node 'rn_b'
option enabled '1'
option label 'Cycle B'
option node 'rn_a'
@@ -0,0 +1,3 @@
{
"success": true
}
@@ -0,0 +1,114 @@
{
"log": {
"disabled": false,
"level": "warn",
"output": "__TEST_ROOT__/var/run/homeproxy/sing-box-c.log",
"timestamp": true
},
"ntp": {
"enabled": true,
"server": "time.apple.com",
"detour": "direct-out",
"domain_resolver": "default-dns"
},
"dns": {
"servers": [
{
"tag": "default-dns",
"type": "udp",
"server": "223.5.5.5",
"detour": "direct-out"
},
{
"tag": "system-dns",
"type": "local",
"detour": "direct-out"
}
],
"strategy": "ipv4_only",
"final": "default-dns"
},
"inbounds": [
{
"type": "direct",
"tag": "dns-in",
"listen": "::",
"listen_port": 5333
},
{
"type": "mixed",
"tag": "mixed-in",
"listen": "::",
"listen_port": 5330,
"sniff": true,
"sniff_override_destination": true,
"set_system_proxy": false
},
{
"type": "redirect",
"tag": "redirect-in",
"listen": "::",
"listen_port": 5331,
"sniff": true,
"sniff_override_destination": true
},
{
"type": "tproxy",
"tag": "tproxy-in",
"listen": "::",
"listen_port": 5332,
"network": "udp",
"sniff": true,
"sniff_override_destination": true
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct-out",
"routing_mark": 100
},
{
"type": "block",
"tag": "block-out"
},
{
"type": "shadowsocks",
"tag": "Rule Node",
"routing_mark": 100,
"server": "rule-node.example",
"server_port": 8388,
"password": "rule-node-password",
"method": "2022-blake3-aes-128-gcm"
}
],
"route": {
"rules": [
{
"inbound": "dns-in",
"action": "hijack-dns"
},
{
"domain_suffix": [
"example.com"
],
"action": "route",
"outbound": "Rule Node"
}
],
"rule_set": [
],
"auto_detect_interface": true,
"default_domain_resolver": {
"action": "resolve",
"server": "default-dns"
},
"final": "direct-out"
},
"experimental": {
"cache_file": {
"enabled": true,
"path": "__TEST_ROOT__/var/run/homeproxy/cache.db"
}
}
}
@@ -0,0 +1,40 @@
config homeproxy 'config'
option routing_mode 'custom'
option proxy_mode 'redirect_tproxy'
option ipv6_support '0'
option log_level 'warn'
option clash_api_enabled '0'
config homeproxy 'infra'
option mixed_port '5330'
option redirect_port '5331'
option tproxy_port '5332'
option self_mark '100'
config homeproxy 'control'
config homeproxy 'dns'
option default_strategy 'ipv4_only'
option default_server 'default-dns'
option disable_cache '0'
option disable_cache_expire '0'
config homeproxy 'routing'
option default_outbound 'direct-out'
option default_outbound_dns 'default-dns'
option sniff_override '1'
config routing_rule 'rule_node'
option enabled '1'
option label 'Route To Concrete Node'
option action 'route'
list domain_suffix 'example.com'
option outbound 'n1'
config node 'n1'
option type 'shadowsocks'
option label 'Rule Node'
option address 'rule-node.example'
option port '8388'
option shadowsocks_encrypt_method '2022-blake3-aes-128-gcm'
option password 'rule-node-password'
@@ -0,0 +1,3 @@
{
"success": false
}
@@ -0,0 +1,24 @@
{
"items": [
{
"type": "error",
"message": "路由节点配置错误:Selector Selector A 与 rn_b 形成循环引用。",
"suggestion": "进入 LuCI 界面 -> 服务 -> HomeProxy -> 路由节点,检查 Selector 节点列表、默认节点和上游出站配置"
},
{
"type": "warning",
"message": "路由节点 Selector A 没有可用节点,已在本次生成中临时回退为阻断出站。",
"suggestion": "请手动选择节点,或调整节点正则/排除正则以命中可用节点"
},
{
"type": "error",
"message": "路由节点配置错误:Selector Selector B 与 rn_a 形成循环引用。",
"suggestion": "进入 LuCI 界面 -> 服务 -> HomeProxy -> 路由节点,检查 Selector 节点列表、默认节点和上游出站配置"
},
{
"type": "warning",
"message": "路由节点 Selector B 没有可用节点,已在本次生成中临时回退为阻断出站。",
"suggestion": "请手动选择节点,或调整节点正则/排除正则以命中可用节点"
}
]
}
@@ -0,0 +1,39 @@
config homeproxy 'config'
option routing_mode 'custom'
option proxy_mode 'redirect_tproxy'
option ipv6_support '0'
option log_level 'warn'
option clash_api_enabled '0'
config homeproxy 'infra'
option mixed_port '5330'
option redirect_port '5331'
option tproxy_port '5332'
option self_mark '100'
config homeproxy 'control'
config homeproxy 'dns'
option default_strategy 'ipv4_only'
option default_server 'default-dns'
option disable_cache '0'
option disable_cache_expire '0'
config homeproxy 'routing'
option default_outbound 'rn_a'
option default_outbound_dns 'default-dns'
option sniff_override '1'
config routing_node 'rn_a'
option enabled '1'
option label 'Selector A'
option node 'selector'
list selector_nodes 'rn_b'
option selector_default 'rn_b'
config routing_node 'rn_b'
option enabled '1'
option label 'Selector B'
option node 'selector'
list selector_nodes 'rn_a'
option selector_default 'rn_a'
@@ -0,0 +1,3 @@
{
"success": true
}
@@ -0,0 +1,125 @@
{
"log": {
"disabled": false,
"level": "warn",
"output": "__TEST_ROOT__/var/run/homeproxy/sing-box-c.log",
"timestamp": true
},
"ntp": {
"enabled": true,
"server": "time.apple.com",
"detour": "direct-out",
"domain_resolver": "default-dns"
},
"dns": {
"servers": [
{
"tag": "default-dns",
"type": "udp",
"server": "223.5.5.5",
"detour": "direct-out"
},
{
"tag": "system-dns",
"type": "local",
"detour": "direct-out"
}
],
"strategy": "ipv4_only",
"final": "default-dns"
},
"inbounds": [
{
"type": "direct",
"tag": "dns-in",
"listen": "::",
"listen_port": 5333
},
{
"type": "mixed",
"tag": "mixed-in",
"listen": "::",
"listen_port": 5330,
"sniff": true,
"sniff_override_destination": true,
"set_system_proxy": false
},
{
"type": "redirect",
"tag": "redirect-in",
"listen": "::",
"listen_port": 5331,
"sniff": true,
"sniff_override_destination": true
},
{
"type": "tproxy",
"tag": "tproxy-in",
"listen": "::",
"listen_port": 5332,
"network": "udp",
"sniff": true,
"sniff_override_destination": true
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct-out",
"routing_mark": 100
},
{
"type": "block",
"tag": "block-out"
},
{
"type": "selector",
"tag": "Manual Selector",
"outbounds": [
"Manual A",
"Manual B"
],
"default": "Manual B"
},
{
"type": "shadowsocks",
"tag": "Manual A",
"routing_mark": 100,
"server": "manual-a.example",
"server_port": 8388,
"password": "manual-a-password",
"method": "2022-blake3-aes-128-gcm"
},
{
"type": "shadowsocks",
"tag": "Manual B",
"routing_mark": 100,
"server": "manual-b.example",
"server_port": 8389,
"password": "manual-b-password",
"method": "2022-blake3-aes-128-gcm"
}
],
"route": {
"rules": [
{
"inbound": "dns-in",
"action": "hijack-dns"
}
],
"rule_set": [
],
"auto_detect_interface": true,
"default_domain_resolver": {
"action": "resolve",
"server": "default-dns"
},
"final": "Manual Selector"
},
"experimental": {
"cache_file": {
"enabled": true,
"path": "__TEST_ROOT__/var/run/homeproxy/cache.db"
}
}
}
@@ -0,0 +1,49 @@
config homeproxy 'config'
option routing_mode 'custom'
option proxy_mode 'redirect_tproxy'
option ipv6_support '0'
option log_level 'warn'
option clash_api_enabled '0'
config homeproxy 'infra'
option mixed_port '5330'
option redirect_port '5331'
option tproxy_port '5332'
option self_mark '100'
config homeproxy 'control'
config homeproxy 'dns'
option default_strategy 'ipv4_only'
option default_server 'default-dns'
option disable_cache '0'
option disable_cache_expire '0'
config homeproxy 'routing'
option default_outbound 'rn_selector'
option default_outbound_dns 'default-dns'
option sniff_override '1'
config routing_node 'rn_selector'
option enabled '1'
option label 'Manual Selector'
option node 'selector'
list selector_nodes 'n1'
list selector_nodes 'n2'
option selector_default 'n2'
config node 'n1'
option type 'shadowsocks'
option label 'Manual A'
option address 'manual-a.example'
option port '8388'
option shadowsocks_encrypt_method '2022-blake3-aes-128-gcm'
option password 'manual-a-password'
config node 'n2'
option type 'shadowsocks'
option label 'Manual B'
option address 'manual-b.example'
option port '8389'
option shadowsocks_encrypt_method '2022-blake3-aes-128-gcm'
option password 'manual-b-password'
@@ -0,0 +1,3 @@
{
"success": true
}
@@ -0,0 +1,145 @@
{
"log": {
"disabled": false,
"level": "warn",
"output": "__TEST_ROOT__/var/run/homeproxy/sing-box-c.log",
"timestamp": true
},
"ntp": {
"enabled": true,
"server": "time.apple.com",
"detour": "direct-out",
"domain_resolver": "default-dns"
},
"dns": {
"servers": [
{
"tag": "default-dns",
"type": "udp",
"server": "223.5.5.5",
"detour": "direct-out"
},
{
"tag": "system-dns",
"type": "local",
"detour": "direct-out"
}
],
"strategy": "ipv4_only",
"final": "default-dns"
},
"inbounds": [
{
"type": "direct",
"tag": "dns-in",
"listen": "::",
"listen_port": 5333
},
{
"type": "mixed",
"tag": "mixed-in",
"listen": "::",
"listen_port": 5330,
"sniff": true,
"sniff_override_destination": true,
"set_system_proxy": false
},
{
"type": "redirect",
"tag": "redirect-in",
"listen": "::",
"listen_port": 5331,
"sniff": true,
"sniff_override_destination": true
},
{
"type": "tproxy",
"tag": "tproxy-in",
"listen": "::",
"listen_port": 5332,
"network": "udp",
"sniff": true,
"sniff_override_destination": true
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct-out",
"routing_mark": 100
},
{
"type": "block",
"tag": "block-out"
},
{
"type": "urltest",
"tag": "URLTest Group",
"outbounds": [
"URLTest A",
"URLTest B"
],
"url": "https://www.gstatic.com/generate_204",
"interval": "300s",
"tolerance": 50
},
{
"type": "selector",
"tag": "Selector With URLTest",
"outbounds": [
"URLTest Group",
"Selector Direct Node"
],
"default": "URLTest Group"
},
{
"type": "shadowsocks",
"tag": "URLTest A",
"routing_mark": 100,
"server": "urltest-a.example",
"server_port": 8388,
"password": "urltest-a-password",
"method": "2022-blake3-aes-128-gcm"
},
{
"type": "shadowsocks",
"tag": "URLTest B",
"routing_mark": 100,
"server": "urltest-b.example",
"server_port": 8389,
"password": "urltest-b-password",
"method": "2022-blake3-aes-128-gcm"
},
{
"type": "shadowsocks",
"tag": "Selector Direct Node",
"routing_mark": 100,
"server": "selector-direct.example",
"server_port": 8390,
"password": "selector-direct-password",
"method": "2022-blake3-aes-128-gcm"
}
],
"route": {
"rules": [
{
"inbound": "dns-in",
"action": "hijack-dns"
}
],
"rule_set": [
],
"auto_detect_interface": true,
"default_domain_resolver": {
"action": "resolve",
"server": "default-dns"
},
"final": "Selector With URLTest"
},
"experimental": {
"cache_file": {
"enabled": true,
"path": "__TEST_ROOT__/var/run/homeproxy/cache.db"
}
}
}
@@ -0,0 +1,67 @@
config homeproxy 'config'
option routing_mode 'custom'
option proxy_mode 'redirect_tproxy'
option ipv6_support '0'
option log_level 'warn'
option clash_api_enabled '0'
config homeproxy 'infra'
option mixed_port '5330'
option redirect_port '5331'
option tproxy_port '5332'
option self_mark '100'
config homeproxy 'control'
config homeproxy 'dns'
option default_strategy 'ipv4_only'
option default_server 'default-dns'
option disable_cache '0'
option disable_cache_expire '0'
config homeproxy 'routing'
option default_outbound 'rn_selector'
option default_outbound_dns 'default-dns'
option sniff_override '1'
config routing_node 'rn_urltest'
option enabled '1'
option label 'URLTest Group'
option node 'urltest'
list urltest_nodes 'n1'
list urltest_nodes 'n2'
option urltest_url 'https://www.gstatic.com/generate_204'
option urltest_interval '300'
option urltest_tolerance '50'
config routing_node 'rn_selector'
option enabled '1'
option label 'Selector With URLTest'
option node 'selector'
list selector_nodes 'rn_urltest'
list selector_nodes 'n3'
option selector_default 'rn_urltest'
config node 'n1'
option type 'shadowsocks'
option label 'URLTest A'
option address 'urltest-a.example'
option port '8388'
option shadowsocks_encrypt_method '2022-blake3-aes-128-gcm'
option password 'urltest-a-password'
config node 'n2'
option type 'shadowsocks'
option label 'URLTest B'
option address 'urltest-b.example'
option port '8389'
option shadowsocks_encrypt_method '2022-blake3-aes-128-gcm'
option password 'urltest-b-password'
config node 'n3'
option type 'shadowsocks'
option label 'Selector Direct Node'
option address 'selector-direct.example'
option port '8390'
option shadowsocks_encrypt_method '2022-blake3-aes-128-gcm'
option password 'selector-direct-password'
@@ -0,0 +1,3 @@
{
"success": true
}
@@ -0,0 +1,114 @@
{
"log": {
"disabled": false,
"level": "warn",
"output": "__TEST_ROOT__/var/run/homeproxy/sing-box-c.log",
"timestamp": true
},
"ntp": {
"enabled": true,
"server": "time.apple.com",
"detour": "direct-out",
"domain_resolver": "default-dns"
},
"dns": {
"servers": [
{
"tag": "default-dns",
"type": "udp",
"server": "223.5.5.5",
"detour": "direct-out"
},
{
"tag": "system-dns",
"type": "local",
"detour": "direct-out"
}
],
"strategy": "ipv4_only",
"final": "default-dns"
},
"inbounds": [
{
"type": "direct",
"tag": "dns-in",
"listen": "::",
"listen_port": 5333
},
{
"type": "mixed",
"tag": "mixed-in",
"listen": "::",
"listen_port": 5330,
"sniff": true,
"sniff_override_destination": true,
"set_system_proxy": false
},
{
"type": "redirect",
"tag": "redirect-in",
"listen": "::",
"listen_port": 5331,
"sniff": true,
"sniff_override_destination": true
},
{
"type": "tproxy",
"tag": "tproxy-in",
"listen": "::",
"listen_port": 5332,
"network": "udp",
"sniff": true,
"sniff_override_destination": true
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct-out",
"routing_mark": 100
},
{
"type": "block",
"tag": "block-out"
},
{
"type": "selector",
"tag": "Regex Selector",
"outbounds": [
"香港 01"
]
},
{
"type": "shadowsocks",
"tag": "香港 01",
"routing_mark": 100,
"server": "hk.example",
"server_port": 8388,
"password": "hk-password",
"method": "2022-blake3-aes-128-gcm"
}
],
"route": {
"rules": [
{
"inbound": "dns-in",
"action": "hijack-dns"
}
],
"rule_set": [
],
"auto_detect_interface": true,
"default_domain_resolver": {
"action": "resolve",
"server": "default-dns"
},
"final": "Regex Selector"
},
"experimental": {
"cache_file": {
"enabled": true,
"path": "__TEST_ROOT__/var/run/homeproxy/cache.db"
}
}
}
@@ -0,0 +1,56 @@
config homeproxy 'config'
option routing_mode 'custom'
option proxy_mode 'redirect_tproxy'
option ipv6_support '0'
option log_level 'warn'
option clash_api_enabled '0'
config homeproxy 'infra'
option mixed_port '5330'
option redirect_port '5331'
option tproxy_port '5332'
option self_mark '100'
config homeproxy 'control'
config homeproxy 'dns'
option default_strategy 'ipv4_only'
option default_server 'default-dns'
option disable_cache '0'
option disable_cache_expire '0'
config homeproxy 'routing'
option default_outbound 'rn_selector'
option default_outbound_dns 'default-dns'
option sniff_override '1'
config routing_node 'rn_selector'
option enabled '1'
option label 'Regex Selector'
option node 'selector'
option node_filter '香港|日本'
option node_filter_exclude '日本'
config node 'n_hk'
option type 'shadowsocks'
option label '香港 01'
option address 'hk.example'
option port '8388'
option shadowsocks_encrypt_method '2022-blake3-aes-128-gcm'
option password 'hk-password'
config node 'n_jp'
option type 'shadowsocks'
option label '日本 01'
option address 'jp.example'
option port '8389'
option shadowsocks_encrypt_method '2022-blake3-aes-128-gcm'
option password 'jp-password'
config node 'n_us'
option type 'shadowsocks'
option label '美国 01'
option address 'us.example'
option port '8390'
option shadowsocks_encrypt_method '2022-blake3-aes-128-gcm'
option password 'us-password'
@@ -0,0 +1,3 @@
{
"success": true
}
@@ -0,0 +1,164 @@
{
"log": {
"disabled": false,
"level": "warn",
"output": "__TEST_ROOT__/var/run/homeproxy/sing-box-c.log",
"timestamp": true
},
"ntp": {
"enabled": true,
"server": "time.apple.com",
"detour": "direct-out",
"domain_resolver": "default-dns"
},
"dns": {
"servers": [
{
"tag": "default-dns",
"type": "udp",
"server": "223.5.5.5",
"detour": "direct-out"
},
{
"tag": "system-dns",
"type": "local",
"detour": "direct-out"
},
{
"tag": "main-dns",
"domain_resolver": {
"server": "default-dns",
"strategy": "ipv4_only"
},
"detour": "main-out",
"type": "tcp",
"server": "223.5.5.5"
},
{
"tag": "china-dns",
"domain_resolver": {
"server": "default-dns",
"strategy": "prefer_ipv6"
},
"detour": "direct-out",
"type": "udp",
"server": "223.5.5.5"
}
],
"rules": [
{
"rule_set": "geosite-cn",
"action": "route",
"server": "china-dns",
"strategy": "prefer_ipv6"
},
{
"type": "logical",
"mode": "and",
"rules": [
{
"rule_set": "geosite-noncn",
"invert": true
},
{
"rule_set": "geoip-cn"
}
],
"action": "route",
"server": "china-dns",
"strategy": "prefer_ipv6"
}
],
"strategy": "ipv4_only",
"final": "main-dns"
},
"inbounds": [
{
"type": "direct",
"tag": "dns-in",
"listen": "::",
"listen_port": 5333
},
{
"type": "mixed",
"tag": "mixed-in",
"listen": "::",
"listen_port": 5330,
"sniff": true,
"sniff_override_destination": true,
"set_system_proxy": false
},
{
"type": "redirect",
"tag": "redirect-in",
"listen": "::",
"listen_port": 5331,
"sniff": true,
"sniff_override_destination": true
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct-out",
"routing_mark": 100
},
{
"type": "block",
"tag": "block-out"
},
{
"type": "shadowsocks",
"tag": "main-out",
"routing_mark": 100,
"server": "ss-basic.example",
"server_port": 8388,
"password": "basic-password",
"method": "2022-blake3-aes-128-gcm"
}
],
"route": {
"rules": [
{
"inbound": "dns-in",
"action": "hijack-dns"
}
],
"rule_set": [
{
"type": "remote",
"tag": "geoip-cn",
"format": "binary",
"url": "https://fastly.jsdelivr.net/gh/1715173329/IPCIDR-CHINA@rule-set/cn.srs",
"download_detour": "main-out"
},
{
"type": "remote",
"tag": "geosite-cn",
"format": "binary",
"url": "https://fastly.jsdelivr.net/gh/1715173329/sing-geosite@rule-set-unstable/geosite-geolocation-cn.srs",
"download_detour": "main-out"
},
{
"type": "remote",
"tag": "geosite-noncn",
"format": "binary",
"url": "https://fastly.jsdelivr.net/gh/1715173329/sing-geosite@rule-set-unstable/geosite-geolocation-!cn.srs",
"download_detour": "main-out"
}
],
"auto_detect_interface": true,
"default_domain_resolver": {
"action": "route",
"server": "china-dns",
"strategy": "prefer_ipv4"
},
"final": "main-out"
},
"experimental": {
"cache_file": {
"enabled": true,
"path": "__TEST_ROOT__/var/run/homeproxy/cache.db"
}
}
}
@@ -0,0 +1,23 @@
config homeproxy 'config'
option routing_mode 'bypass_mainland_china'
option proxy_mode 'redirect'
option ipv6_support '0'
option main_node 'n1'
option main_udp_node 'same'
option log_level 'warn'
option clash_api_enabled '0'
config homeproxy 'infra'
option mixed_port '5330'
option redirect_port '5331'
option self_mark '100'
config homeproxy 'control'
config node 'n1'
option type 'shadowsocks'
option label 'SS Basic'
option address 'ss-basic.example'
option port '8388'
option shadowsocks_encrypt_method '2022-blake3-aes-128-gcm'
option password 'basic-password'
@@ -0,0 +1,3 @@
{
"success": true
}
@@ -0,0 +1,178 @@
{
"log": {
"disabled": false,
"level": "warn",
"output": "__TEST_ROOT__/var/run/homeproxy/sing-box-c.log",
"timestamp": true
},
"ntp": {
"enabled": true,
"server": "time.apple.com",
"detour": "direct-out",
"domain_resolver": "default-dns"
},
"dns": {
"servers": [
{
"tag": "default-dns",
"type": "udp",
"server": "223.5.5.5",
"detour": "direct-out"
},
{
"tag": "system-dns",
"type": "local",
"detour": "direct-out"
},
{
"tag": "main-dns",
"domain_resolver": {
"server": "default-dns",
"strategy": "ipv4_only"
},
"detour": "main-out",
"type": "tcp",
"server": "223.5.5.5"
},
{
"tag": "china-dns",
"domain_resolver": {
"server": "default-dns",
"strategy": "prefer_ipv6"
},
"detour": "direct-out",
"type": "udp",
"server": "223.5.5.5"
}
],
"rules": [
{
"rule_set": "geosite-cn",
"action": "route",
"server": "china-dns",
"strategy": "prefer_ipv6"
},
{
"type": "logical",
"mode": "and",
"rules": [
{
"rule_set": "geosite-noncn",
"invert": true
},
{
"rule_set": "geoip-cn"
}
],
"action": "route",
"server": "china-dns",
"strategy": "prefer_ipv6"
}
],
"strategy": "ipv4_only",
"final": "main-dns"
},
"inbounds": [
{
"type": "direct",
"tag": "dns-in",
"listen": "::",
"listen_port": 5333
},
{
"type": "mixed",
"tag": "mixed-in",
"listen": "::",
"listen_port": 5330,
"sniff": true,
"sniff_override_destination": true,
"set_system_proxy": false
},
{
"type": "redirect",
"tag": "redirect-in",
"listen": "::",
"listen_port": 5331,
"sniff": true,
"sniff_override_destination": true
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct-out",
"routing_mark": 100
},
{
"type": "block",
"tag": "block-out"
},
{
"type": "shadowtls",
"tag": "main-out-shadowtls",
"routing_mark": 100,
"server": "shadowtls.example",
"server_port": 443,
"version": 3,
"password": "shadowtls-password",
"tls": {
"enabled": true,
"server_name": "www.apple.com"
}
},
{
"type": "shadowsocks",
"tag": "main-out",
"routing_mark": 100,
"server": "ss-shadowtls.example",
"server_port": 8388,
"password": "ss-password",
"method": "2022-blake3-aes-128-gcm",
"detour": "main-out-shadowtls"
}
],
"route": {
"rules": [
{
"inbound": "dns-in",
"action": "hijack-dns"
}
],
"rule_set": [
{
"type": "remote",
"tag": "geoip-cn",
"format": "binary",
"url": "https://fastly.jsdelivr.net/gh/1715173329/IPCIDR-CHINA@rule-set/cn.srs",
"download_detour": "main-out"
},
{
"type": "remote",
"tag": "geosite-cn",
"format": "binary",
"url": "https://fastly.jsdelivr.net/gh/1715173329/sing-geosite@rule-set-unstable/geosite-geolocation-cn.srs",
"download_detour": "main-out"
},
{
"type": "remote",
"tag": "geosite-noncn",
"format": "binary",
"url": "https://fastly.jsdelivr.net/gh/1715173329/sing-geosite@rule-set-unstable/geosite-geolocation-!cn.srs",
"download_detour": "main-out"
}
],
"auto_detect_interface": true,
"default_domain_resolver": {
"action": "route",
"server": "china-dns",
"strategy": "prefer_ipv4"
},
"final": "main-out"
},
"experimental": {
"cache_file": {
"enabled": true,
"path": "__TEST_ROOT__/var/run/homeproxy/cache.db"
}
}
}
@@ -0,0 +1,29 @@
config homeproxy 'config'
option routing_mode 'bypass_mainland_china'
option proxy_mode 'redirect'
option ipv6_support '0'
option main_node 'n1'
option main_udp_node 'same'
option log_level 'warn'
option clash_api_enabled '0'
config homeproxy 'infra'
option mixed_port '5330'
option redirect_port '5331'
option self_mark '100'
config homeproxy 'control'
config node 'n1'
option type 'shadowsocks'
option label 'SS ShadowTLS'
option address 'ss-shadowtls.example'
option port '8388'
option shadowsocks_encrypt_method '2022-blake3-aes-128-gcm'
option password 'ss-password'
option shadowtls_enabled '1'
option shadowtls_address 'shadowtls.example'
option shadowtls_port '443'
option shadowtls_version '3'
option shadowtls_password 'shadowtls-password'
option shadowtls_sni 'www.apple.com'
@@ -0,0 +1,182 @@
#!/usr/bin/ucode
'use strict';
import { readfile, writefile, stat } from 'fs';
const base_dir = ARGV?.[0] || 'tests/generator-golden';
const generator = ARGV?.[1] || '/etc/homeproxy/scripts/generate_client.uc';
const module_dir = ARGV?.[2];
function hasFlag(flag) {
const argv = (type(ARGV) === 'array') ? ARGV : [];
for (let item in argv)
if (item === flag)
return true;
return false;
}
const update_expected = hasFlag('--update');
function shellquote(s) {
return `'${replace(s, "'", "'\\''")}'`;
}
function fail(message) {
warn(message + '\n');
exit(1);
}
function path(...parts) {
return join('/', parts);
}
function exists(file) {
return stat(file)?.type === 'file';
}
function readJson(file, fallback) {
if (!exists(file))
return fallback;
return json(readfile(file));
}
function normalize(value, test_root) {
let value_type = type(value);
if (value_type === 'string')
return replace(value, test_root, '__TEST_ROOT__');
if (value_type === 'array') {
let result = [];
for (let item in value)
push(result, normalize(item, test_root));
return result;
}
if (value_type === 'object') {
let result = {};
for (let key in value) {
if (key === 'time')
continue;
result[key] = normalize(value[key], test_root);
}
return result;
}
return value;
}
function canonical(value) {
return sprintf('%.J', value);
}
function compareJson(case_name, label, actual, expected) {
if (canonical(actual) === canonical(expected))
return;
warn(sprintf('%s: %s golden mismatch\n', case_name, label));
warn('expected:\n' + sprintf('%.J\n', expected));
warn('actual:\n' + sprintf('%.J\n', actual));
exit(1);
}
function generatorCommand() {
if (module_dir && module_dir !== '--update')
return sprintf('ucode -S -L %s %s', shellquote(module_dir), shellquote(generator));
return shellquote(generator);
}
function prepareRoot(case_name, fixture_dir) {
const test_root = '/tmp/homeproxy-golden-' + replace(case_name, /[^A-Za-z0-9_-]/g, '_');
system('rm -rf ' + shellquote(test_root));
system('mkdir -p ' +
shellquote(path(test_root, 'etc/config')) + ' ' +
shellquote(path(test_root, 'etc/homeproxy/resources')) + ' ' +
shellquote(path(test_root, 'var/run/homeproxy')));
writefile(path(test_root, 'etc/config/homeproxy'), readfile(path(fixture_dir, 'homeproxy')));
const direct_list = path(fixture_dir, 'resources/direct_list.txt'),
proxy_list = path(fixture_dir, 'resources/proxy_list.txt');
writefile(path(test_root, 'etc/homeproxy/resources/direct_list.txt'), exists(direct_list) ? readfile(direct_list) : '');
writefile(path(test_root, 'etc/homeproxy/resources/proxy_list.txt'), exists(proxy_list) ? readfile(proxy_list) : '');
return test_root;
}
function runCase(item) {
const case_name = item.name,
fixture_dir = path(base_dir, 'fixtures', case_name),
meta = readJson(path(fixture_dir, 'case.json'), null);
if (!meta)
fail(sprintf('%s: missing fixture case.json', case_name));
const test_root = prepareRoot(case_name, fixture_dir),
stdout_path = path(test_root, 'var/run/homeproxy/stdout.log'),
stderr_path = path(test_root, 'var/run/homeproxy/stderr.log'),
command = sprintf('%s --test-root %s >%s 2>%s',
generatorCommand(),
shellquote(test_root),
shellquote(stdout_path),
shellquote(stderr_path)),
status = system(command),
success = (status === 0);
if (!!meta.success !== success) {
warn(sprintf('%s: expected success=%J, got status=%d\n', case_name, meta.success, status));
warn(readfile(stderr_path) || '');
system('rm -rf ' + shellquote(test_root));
exit(1);
}
const actual_config_path = path(test_root, 'var/run/homeproxy/sing-box-c.json'),
expected_config_path = path(fixture_dir, 'expected/sing-box-c.json');
if (meta.success) {
if (!exists(actual_config_path))
fail(sprintf('%s: generator succeeded but sing-box-c.json is missing', case_name));
const actual_config = normalize(json(readfile(actual_config_path)), test_root);
if (update_expected)
writefile(expected_config_path, sprintf('%.J\n', actual_config));
else
compareJson(case_name, 'sing-box-c.json',
actual_config,
readJson(expected_config_path, null));
}
const actual_diagnostics = exists(path(test_root, 'var/run/homeproxy/config-diagnostics.json'))
? json(readfile(path(test_root, 'var/run/homeproxy/config-diagnostics.json')))
: { items: [] };
const normalized_diagnostics = normalize(actual_diagnostics, test_root),
expected_diagnostics_path = path(fixture_dir, 'expected/config-diagnostics.json');
if (update_expected)
writefile(expected_diagnostics_path, sprintf('%.J\n', normalized_diagnostics));
else
compareJson(case_name, 'config-diagnostics.json',
normalized_diagnostics,
readJson(expected_diagnostics_path, { items: [] }));
system('rm -rf ' + shellquote(test_root));
printf('%s ok\n', case_name);
}
const cases = readJson(path(base_dir, 'cases.json'), []);
let count = 0;
for (let item in cases) {
if (!item.generator_fixture)
continue;
runCase(item);
count++;
}
printf('generator golden fixtures: %d ok\n', count);