From d3f67f9e8e834d85f158b8d4fb65f95447b99fe9 Mon Sep 17 00:00:00 2001 From: Xi Xu Date: Tue, 31 Mar 2026 02:46:55 +0800 Subject: [PATCH] fix(mihomo): harden generated META config --- META.js | 120 ++++++++++++++++++++++++++++++++++++++++----------- META.test.js | 104 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+), 26 deletions(-) create mode 100644 META.test.js diff --git a/META.js b/META.js index a18ffd1..c43abd5 100644 --- a/META.js +++ b/META.js @@ -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 diff --git a/META.test.js b/META.test.js new file mode 100644 index 0000000..619f28d --- /dev/null +++ b/META.test.js @@ -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); +});