fix(mihomo): harden generated META config
This commit is contained in:
1 parent
84eff4ed5f
commit
d3f67f9e8e
2 files changed
+198
-26
No files matched your search
@@ -51,101 +51,135 @@ const locations = [
|
|||||||
{
|
{
|
||||||
name: "Argentina 🇦🇷",
|
name: "Argentina 🇦🇷",
|
||||||
icon: `${LOCATION_ICON_SET_URL}argentina`,
|
icon: `${LOCATION_ICON_SET_URL}argentina`,
|
||||||
filter: "(?i)\u963f\u6839\u5ef7|Argentina|ARG|AR|argentina|arg|ar|🇦🇷",
|
keywords: ["阿根廷", "Argentina"],
|
||||||
|
codes: ["ARG", "AR"],
|
||||||
|
emoji: ["🇦🇷"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Canada 🇨🇦",
|
name: "Canada 🇨🇦",
|
||||||
icon: `${LOCATION_ICON_SET_URL}canada`,
|
icon: `${LOCATION_ICON_SET_URL}canada`,
|
||||||
filter: "(?i)\u52a0\u62ff\u5927|Canada|CAN|CA|canada|ca|🇨🇦",
|
keywords: ["加拿大", "Canada"],
|
||||||
|
codes: ["CAN", "CA"],
|
||||||
|
emoji: ["🇨🇦"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Finland 🇫🇮",
|
name: "Finland 🇫🇮",
|
||||||
icon: `${LOCATION_ICON_SET_URL}finland`,
|
icon: `${LOCATION_ICON_SET_URL}finland`,
|
||||||
filter: "(?i)\u82ac\u5170|Finland|FIN|FI|finland|fin|fi|🇫🇮",
|
keywords: ["芬兰", "Finland"],
|
||||||
|
codes: ["FIN", "FI"],
|
||||||
|
emoji: ["🇫🇮"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "France 🇫🇷",
|
name: "France 🇫🇷",
|
||||||
icon: `${LOCATION_ICON_SET_URL}france`,
|
icon: `${LOCATION_ICON_SET_URL}france`,
|
||||||
filter: "(?i)\u6cd5\u56fd|France|FR|france|fr|🇫🇷",
|
keywords: ["法国", "France"],
|
||||||
|
codes: ["FR"],
|
||||||
|
emoji: ["🇫🇷"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Germany 🇩🇪",
|
name: "Germany 🇩🇪",
|
||||||
icon: `${LOCATION_ICON_SET_URL}germany`,
|
icon: `${LOCATION_ICON_SET_URL}germany`,
|
||||||
filter: "(?i)\u5fb7\u56fd|Germany|GER|DE|germany|ger|de|🇩🇪",
|
keywords: ["德国", "Germany"],
|
||||||
|
codes: ["GER", "DE"],
|
||||||
|
emoji: ["🇩🇪"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Hong Kong, China 🇭🇰",
|
name: "Hong Kong, China 🇭🇰",
|
||||||
icon: `${LOCATION_ICON_SET_URL}hongkong-circular`,
|
icon: `${LOCATION_ICON_SET_URL}hongkong-circular`,
|
||||||
filter: "(?i)\u9999\u6e2f|Hong Kong|HK|hong kong|hk|🇭🇰",
|
keywords: ["香港", "Hong Kong"],
|
||||||
|
codes: ["HK"],
|
||||||
|
emoji: ["🇭🇰"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "India 🇮🇳",
|
name: "India 🇮🇳",
|
||||||
icon: `${LOCATION_ICON_SET_URL}india`,
|
icon: `${LOCATION_ICON_SET_URL}india`,
|
||||||
filter: "(?i)\u5370\u5ea6|India|IND|IN|india|ind|in|🇮🇳",
|
keywords: ["印度", "India"],
|
||||||
|
codes: ["IND", "IN"],
|
||||||
|
emoji: ["🇮🇳"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Iraq 🇮🇶",
|
name: "Iraq 🇮🇶",
|
||||||
icon: `${LOCATION_ICON_SET_URL}iraq`,
|
icon: `${LOCATION_ICON_SET_URL}iraq`,
|
||||||
filter: "(?i)\u4f0a\u62c9\u514b|Iraq|IQ|iraq|iq|🇮🇶",
|
keywords: ["伊拉克", "Iraq"],
|
||||||
|
codes: ["IQ"],
|
||||||
|
emoji: ["🇮🇶"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Japan 🇯🇵",
|
name: "Japan 🇯🇵",
|
||||||
icon: `${LOCATION_ICON_SET_URL}japan`,
|
icon: `${LOCATION_ICON_SET_URL}japan`,
|
||||||
filter: "(?i)\u65e5\u672c|Japan|JP|japan|jp|🇯🇵",
|
keywords: ["日本", "Japan"],
|
||||||
|
codes: ["JP"],
|
||||||
|
emoji: ["🇯🇵"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Korea 🇰🇷",
|
name: "Korea 🇰🇷",
|
||||||
icon: `${LOCATION_ICON_SET_URL}south-korea`,
|
icon: `${LOCATION_ICON_SET_URL}south-korea`,
|
||||||
filter: "(?i)\u97e9\u56fd|Korea|KR|korea|kr|🇰🇷",
|
keywords: ["韩国", "Korea"],
|
||||||
|
codes: ["KR"],
|
||||||
|
emoji: ["🇰🇷"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Russia 🇷🇺",
|
name: "Russia 🇷🇺",
|
||||||
icon: `${LOCATION_ICON_SET_URL}russian-federation`,
|
icon: `${LOCATION_ICON_SET_URL}russian-federation`,
|
||||||
filter:
|
keywords: ["俄罗斯", "Russia Federation", "Russia"],
|
||||||
"(?i)\u4fc4\u7f57\u65af|Russia Federation|Russia|RU|russia federation|russia|ru|🇷🇺",
|
codes: ["RU"],
|
||||||
|
emoji: ["🇷🇺"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Singapore 🇸🇬",
|
name: "Singapore 🇸🇬",
|
||||||
icon: `${LOCATION_ICON_SET_URL}singapore`,
|
icon: `${LOCATION_ICON_SET_URL}singapore`,
|
||||||
filter: "(?i)\u65b0\u52a0\u5761|Singapore|SG|singapore|sg|🇸🇬",
|
keywords: ["新加坡", "Singapore"],
|
||||||
|
codes: ["SG"],
|
||||||
|
emoji: ["🇸🇬"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Taiwan, China 🇨🇳",
|
name: "Taiwan, China 🇨🇳",
|
||||||
icon: `${LOCATION_ICON_SET_URL}china-circular`,
|
icon: `${LOCATION_ICON_SET_URL}china-circular`,
|
||||||
filter: "(?i)\u53f0\u6e7e|Taiwan|TW|taiwan|tw|\uD83C\uDDF9\uD83C\uDDFC",
|
keywords: ["台湾", "Taiwan"],
|
||||||
|
codes: ["TW"],
|
||||||
|
emoji: ["🇹🇼"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Thailand 🇹🇭",
|
name: "Thailand 🇹🇭",
|
||||||
icon: `${LOCATION_ICON_SET_URL}thailand`,
|
icon: `${LOCATION_ICON_SET_URL}thailand`,
|
||||||
filter: "(?i)\u6cf0\u56fd|Thailand|TH|thailand|th|🇹🇭",
|
keywords: ["泰国", "Thailand"],
|
||||||
|
codes: ["TH"],
|
||||||
|
emoji: ["🇹🇭"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Türkiye 🇹🇷",
|
name: "Türkiye 🇹🇷",
|
||||||
icon: `${LOCATION_ICON_SET_URL}turkey`,
|
icon: `${LOCATION_ICON_SET_URL}turkey`,
|
||||||
filter:
|
keywords: ["土耳其", "Türkiye", "Turkey"],
|
||||||
"(?i)\u571f\u8033\u5176|Türkiye|Turkey|TUR|TR|türkiye|turkey|tur|tr|🇹🇷",
|
codes: ["TUR", "TR"],
|
||||||
|
emoji: ["🇹🇷"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Ukraine 🇺🇦",
|
name: "Ukraine 🇺🇦",
|
||||||
icon: `${LOCATION_ICON_SET_URL}ukraine`,
|
icon: `${LOCATION_ICON_SET_URL}ukraine`,
|
||||||
filter: "(?i)\u4e4c\u514b\u5170|Ukraine|UKR|UA|ukraine|ukr|ua|🇺🇦",
|
keywords: ["乌克兰", "Ukraine"],
|
||||||
|
codes: ["UKR", "UA"],
|
||||||
|
emoji: ["🇺🇦"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "United Kingdom 🇬🇧",
|
name: "United Kingdom 🇬🇧",
|
||||||
icon: `${LOCATION_ICON_SET_URL}great-britain`,
|
icon: `${LOCATION_ICON_SET_URL}great-britain`,
|
||||||
filter:
|
keywords: ["英国", "United Kingdom", "Great Britain"],
|
||||||
"(?i)\u82f1\u56fd|United Kingdom|Great Britain|UK|GB|united kingdom|great britain|uk|gb|🇬🇧",
|
codes: ["UK", "GB"],
|
||||||
|
emoji: ["🇬🇧"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "United States 🇺🇸",
|
name: "United States 🇺🇸",
|
||||||
icon: `${LOCATION_ICON_SET_URL}usa`,
|
icon: `${LOCATION_ICON_SET_URL}usa`,
|
||||||
filter:
|
keywords: ["美国", "United States of America", "United States"],
|
||||||
"(?i)\u7f8e\u56fd|United States of America|United States|USA|US|united states of america|united states|usa|us|🇺🇸",
|
codes: ["USA", "US"],
|
||||||
|
emoji: ["🇺🇸"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Global 🌐",
|
name: "Global 🌐",
|
||||||
icon: "https://img.icons8.com/?size=144&id=3685&format=png&color=7bbbe9",
|
icon: "https://img.icons8.com/?size=144&id=3685&format=png&color=7bbbe9",
|
||||||
filter: "(?i)Global|GL|global|gl|Cloudflare|CF|cloudflare|cf|🌐",
|
keywords: ["Global", "Cloudflare"],
|
||||||
|
codes: ["GL", "CF"],
|
||||||
|
emoji: ["🌐"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -155,6 +189,24 @@ function extractFavicon(domain) {
|
|||||||
return `https://t1.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=https://${domain}&size=128`;
|
return `https://t1.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=https://${domain}&size=128`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeRegex(text) {
|
||||||
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLocationFilter({ keywords = [], codes = [], emoji = [] }) {
|
||||||
|
const patterns = [
|
||||||
|
...keywords.map((keyword) => escapeRegex(keyword)),
|
||||||
|
...codes.map((code) => `\\b${escapeRegex(code)}\\b`),
|
||||||
|
...emoji.map((symbol) => escapeRegex(symbol)),
|
||||||
|
];
|
||||||
|
|
||||||
|
return `(?i)${patterns.join("|")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
locations.forEach((location) => {
|
||||||
|
location.filter = buildLocationFilter(location);
|
||||||
|
});
|
||||||
|
|
||||||
// General Configuration
|
// General Configuration
|
||||||
|
|
||||||
const generalConfig = {
|
const generalConfig = {
|
||||||
@@ -170,7 +222,6 @@ const generalConfig = {
|
|||||||
},
|
},
|
||||||
"unified-delay": true,
|
"unified-delay": true,
|
||||||
"tcp-concurrent": true,
|
"tcp-concurrent": true,
|
||||||
"global-client-fingerprint": "chrome",
|
|
||||||
"global-ua": "\u0063\u006c\u0061\u0073\u0068.\u006D\u0065\u0074\u0061",
|
"global-ua": "\u0063\u006c\u0061\u0073\u0068.\u006D\u0065\u0074\u0061",
|
||||||
"etag-support": true,
|
"etag-support": true,
|
||||||
};
|
};
|
||||||
@@ -237,7 +288,6 @@ const tun = {
|
|||||||
enable: true,
|
enable: true,
|
||||||
stack: "mixed",
|
stack: "mixed",
|
||||||
"auto-route": true,
|
"auto-route": true,
|
||||||
"auto-redirect": true,
|
|
||||||
"auto-detect-interface": true,
|
"auto-detect-interface": true,
|
||||||
"strict-route": true,
|
"strict-route": true,
|
||||||
"dns-hijack": ["any:53", "tcp://any:53"],
|
"dns-hijack": ["any:53", "tcp://any:53"],
|
||||||
@@ -663,8 +713,26 @@ function validateOriginalConfig(config) {
|
|||||||
"The original configuration must contain a non-empty proxies array (see https://wiki.\u006D\u0065\u0074\u0061\u0063\u0075\u0062\u0065\u0078.one/en/config/proxies/) or a proxy-providers object with at least one property (see https://wiki.\u006D\u0065\u0074\u0061\u0063\u0075\u0062\u0065\u0078.one/en/config/proxy-providers/)"
|
"The original configuration must contain a non-empty proxies array (see https://wiki.\u006D\u0065\u0074\u0061\u0063\u0075\u0062\u0065\u0078.one/en/config/proxies/) or a proxy-providers object with at least one property (see https://wiki.\u006D\u0065\u0074\u0061\u0063\u0075\u0062\u0065\u0078.one/en/config/proxy-providers/)"
|
||||||
);
|
);
|
||||||
if (proxyCount > 0) {
|
if (proxyCount > 0) {
|
||||||
|
const proxiesWithoutRemoteEndpoint = new Set([
|
||||||
|
"direct",
|
||||||
|
"dns",
|
||||||
|
"pass",
|
||||||
|
"reject",
|
||||||
|
]);
|
||||||
|
|
||||||
config.proxies.forEach((proxy, index) => {
|
config.proxies.forEach((proxy, index) => {
|
||||||
if (!proxy.name || !proxy.type || !proxy.server || !proxy.port) {
|
if (!proxy.name || !proxy.type) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid proxy number ${
|
||||||
|
index + 1
|
||||||
|
} configuration (see https://wiki.\u006D\u0065\u0074\u0061\u0063\u0075\u0062\u0065\u0078.one/en/config/proxies/)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!proxiesWithoutRemoteEndpoint.has(String(proxy.type).toLowerCase()) &&
|
||||||
|
(!proxy.server || proxy.port == null)
|
||||||
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid proxy number ${
|
`Invalid proxy number ${
|
||||||
index + 1
|
index + 1
|
||||||
|
|||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const vm = require("node:vm");
|
||||||
|
|
||||||
|
function loadMetaModule() {
|
||||||
|
const content = fs.readFileSync("E:/github/meta/META.js", "utf8");
|
||||||
|
const wrapped = `${content}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
locations,
|
||||||
|
generalConfig,
|
||||||
|
tun,
|
||||||
|
main,
|
||||||
|
validateOriginalConfig,
|
||||||
|
};`;
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
module: { exports: {} },
|
||||||
|
console: {
|
||||||
|
log() {},
|
||||||
|
warn() {},
|
||||||
|
error() {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.runInNewContext(wrapped, context, { filename: "META.js" });
|
||||||
|
return context.module.exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mihomoRegex(source) {
|
||||||
|
return new RegExp(source.replace(/^\(\?i\)/, ""), "i");
|
||||||
|
}
|
||||||
|
|
||||||
|
test("validateOriginalConfig accepts mihomo dns outbound proxies", () => {
|
||||||
|
const { validateOriginalConfig } = loadMetaModule();
|
||||||
|
|
||||||
|
assert.doesNotThrow(() =>
|
||||||
|
validateOriginalConfig({
|
||||||
|
proxies: [{ name: "dns-out", type: "dns" }],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("generated config does not force deprecated global fingerprinting", () => {
|
||||||
|
const { main } = loadMetaModule();
|
||||||
|
|
||||||
|
const config = main({
|
||||||
|
proxies: [
|
||||||
|
{
|
||||||
|
name: "sample",
|
||||||
|
type: "socks5",
|
||||||
|
server: "127.0.0.1",
|
||||||
|
port: 1080,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(config["global-client-fingerprint"], undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("generated tun config fully replaces the source tun settings", () => {
|
||||||
|
const { main } = loadMetaModule();
|
||||||
|
|
||||||
|
const config = main({
|
||||||
|
proxies: [
|
||||||
|
{
|
||||||
|
name: "sample",
|
||||||
|
type: "socks5",
|
||||||
|
server: "127.0.0.1",
|
||||||
|
port: 1080,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tun: {
|
||||||
|
enable: false,
|
||||||
|
"auto-route": false,
|
||||||
|
"auto-redirect": true,
|
||||||
|
mtu: 9000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(JSON.parse(JSON.stringify(config.tun)), {
|
||||||
|
enable: true,
|
||||||
|
stack: "mixed",
|
||||||
|
"auto-route": true,
|
||||||
|
"auto-detect-interface": true,
|
||||||
|
"strict-route": true,
|
||||||
|
"dns-hijack": ["any:53", "tcp://any:53"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("location filters avoid matching unrelated country names", () => {
|
||||||
|
const { locations } = loadMetaModule();
|
||||||
|
|
||||||
|
const india = locations.find((location) => location.name === "India 🇮🇳");
|
||||||
|
const canada = locations.find((location) => location.name === "Canada 🇨🇦");
|
||||||
|
|
||||||
|
assert.ok(india, "India location should exist");
|
||||||
|
assert.ok(canada, "Canada location should exist");
|
||||||
|
|
||||||
|
assert.equal(mihomoRegex(india.filter).test("Argentina 01"), false);
|
||||||
|
assert.equal(mihomoRegex(india.filter).test("Finland-IEPL"), false);
|
||||||
|
assert.equal(mihomoRegex(canada.filter).test("Singapore-Anycast"), false);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user