fix(mihomo): harden generated META config

This commit is contained in:
xixu-me committed 2026-03-31 02:46:55 +08:00
1 parent 84eff4ed5f
commit d3f67f9e8e
2 files changed
+198 -26

No files matched your search

+94 -26
View File
@@ -51,101 +51,135 @@ const locations = [
{
name: "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 🇨🇦",
icon: `${LOCATION_ICON_SET_URL}canada`,
filter: "(?i)\u52a0\u62ff\u5927|Canada|CAN|CA|canada|ca|🇨🇦",
keywords: ["加拿大", "Canada"],
codes: ["CAN", "CA"],
emoji: ["🇨🇦"],
},
{
name: "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 🇫🇷",
icon: `${LOCATION_ICON_SET_URL}france`,
filter: "(?i)\u6cd5\u56fd|France|FR|france|fr|🇫🇷",
keywords: ["法国", "France"],
codes: ["FR"],
emoji: ["🇫🇷"],
},
{
name: "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 🇭🇰",
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 🇮🇳",
icon: `${LOCATION_ICON_SET_URL}india`,
filter: "(?i)\u5370\u5ea6|India|IND|IN|india|ind|in|🇮🇳",
keywords: ["印度", "India"],
codes: ["IND", "IN"],
emoji: ["🇮🇳"],
},
{
name: "Iraq 🇮🇶",
icon: `${LOCATION_ICON_SET_URL}iraq`,
filter: "(?i)\u4f0a\u62c9\u514b|Iraq|IQ|iraq|iq|🇮🇶",
keywords: ["伊拉克", "Iraq"],
codes: ["IQ"],
emoji: ["🇮🇶"],
},
{
name: "Japan 🇯🇵",
icon: `${LOCATION_ICON_SET_URL}japan`,
filter: "(?i)\u65e5\u672c|Japan|JP|japan|jp|🇯🇵",
keywords: ["日本", "Japan"],
codes: ["JP"],
emoji: ["🇯🇵"],
},
{
name: "Korea 🇰🇷",
icon: `${LOCATION_ICON_SET_URL}south-korea`,
filter: "(?i)\u97e9\u56fd|Korea|KR|korea|kr|🇰🇷",
keywords: ["韩国", "Korea"],
codes: ["KR"],
emoji: ["🇰🇷"],
},
{
name: "Russia 🇷🇺",
icon: `${LOCATION_ICON_SET_URL}russian-federation`,
filter:
"(?i)\u4fc4\u7f57\u65af|Russia Federation|Russia|RU|russia federation|russia|ru|🇷🇺",
keywords: ["俄罗斯", "Russia Federation", "Russia"],
codes: ["RU"],
emoji: ["🇷🇺"],
},
{
name: "Singapore 🇸🇬",
icon: `${LOCATION_ICON_SET_URL}singapore`,
filter: "(?i)\u65b0\u52a0\u5761|Singapore|SG|singapore|sg|🇸🇬",
keywords: ["新加坡", "Singapore"],
codes: ["SG"],
emoji: ["🇸🇬"],
},
{
name: "Taiwan, China 🇨🇳",
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 🇹🇭",
icon: `${LOCATION_ICON_SET_URL}thailand`,
filter: "(?i)\u6cf0\u56fd|Thailand|TH|thailand|th|🇹🇭",
keywords: ["泰国", "Thailand"],
codes: ["TH"],
emoji: ["🇹🇭"],
},
{
name: "Türkiye 🇹🇷",
icon: `${LOCATION_ICON_SET_URL}turkey`,
filter:
"(?i)\u571f\u8033\u5176|Türkiye|Turkey|TUR|TR|türkiye|turkey|tur|tr|🇹🇷",
keywords: ["土耳其", "Türkiye", "Turkey"],
codes: ["TUR", "TR"],
emoji: ["🇹🇷"],
},
{
name: "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 🇬🇧",
icon: `${LOCATION_ICON_SET_URL}great-britain`,
filter:
"(?i)\u82f1\u56fd|United Kingdom|Great Britain|UK|GB|united kingdom|great britain|uk|gb|🇬🇧",
keywords: ["英国", "United Kingdom", "Great Britain"],
codes: ["UK", "GB"],
emoji: ["🇬🇧"],
},
{
name: "United States 🇺🇸",
icon: `${LOCATION_ICON_SET_URL}usa`,
filter:
"(?i)\u7f8e\u56fd|United States of America|United States|USA|US|united states of america|united states|usa|us|🇺🇸",
keywords: ["美国", "United States of America", "United States"],
codes: ["USA", "US"],
emoji: ["🇺🇸"],
},
{
name: "Global 🌐",
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`;
}
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
const generalConfig = {
@@ -170,7 +222,6 @@ const generalConfig = {
},
"unified-delay": true,
"tcp-concurrent": true,
"global-client-fingerprint": "chrome",
"global-ua": "\u0063\u006c\u0061\u0073\u0068.\u006D\u0065\u0074\u0061",
"etag-support": true,
};
@@ -237,7 +288,6 @@ const tun = {
enable: true,
stack: "mixed",
"auto-route": true,
"auto-redirect": true,
"auto-detect-interface": true,
"strict-route": true,
"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/)"
);
if (proxyCount > 0) {
const proxiesWithoutRemoteEndpoint = new Set([
"direct",
"dns",
"pass",
"reject",
]);
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(
`Invalid proxy number ${
index + 1
+104
View File
@@ -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);
});