feat: Add Mihomo and TProxy setup scripts for Alpine and legacy systems
- Introduced `iptables-mihomo-setup-mark2.sh` for advanced TProxy configuration. - Created `iptables-mihomo-setup.sh` for legacy iptables management. - Added `dnssec-test.sh` for DNSSEC interception testing. - Implemented `config-warpgate-alpine.sh` for comprehensive Warpgate setup. - Developed `iptables-mihomo-setup-alpine-mark2.sh` for refined TProxy rules on Alpine. - Added `iptables-mihomo-setup-alpine.sh` for basic TProxy setup on Alpine. - Created `update-core-and-dash.sh` for automated updates of Mihomo core and Zashboard UI.
This commit is contained in:
@@ -1,233 +0,0 @@
|
||||
/**
|
||||
* SUB STORE YAML ASSEMBLER (v4: Fix Duplicate Headers)
|
||||
* * Arguments:
|
||||
* - clear-comments=true
|
||||
* - clear-manifest=true
|
||||
* - clear-replacements=true
|
||||
* * Requires: Header "# @file: filename.yaml" in input files.
|
||||
*/
|
||||
|
||||
// --- OPTIONS PARSING ---
|
||||
function normalizeOptions() {
|
||||
const args = (typeof $arguments !== "undefined" && $arguments) ? $arguments : {};
|
||||
const asBool = (v, def = false) => {
|
||||
if (v === undefined || v === null || v === "") return def;
|
||||
if (typeof v === "boolean") return v;
|
||||
const s = String(v).toLowerCase().trim();
|
||||
if (["1", "true", "yes", "y", "on"].includes(s)) return true;
|
||||
if (["0", "false", "no", "n", "off"].includes(s)) return false;
|
||||
return def;
|
||||
};
|
||||
return {
|
||||
clearComments: asBool(args['clear-comments'], false),
|
||||
clearManifest: asBool(args['clear-manifest'], false),
|
||||
clearReplacements: asBool(args['clear-replacements'], false),
|
||||
};
|
||||
}
|
||||
|
||||
// --- UTILS ---
|
||||
function normalizeFiles(rawFiles) {
|
||||
const map = new Map();
|
||||
if (!rawFiles || !Array.isArray(rawFiles)) return map;
|
||||
rawFiles.forEach((content) => {
|
||||
if (typeof content !== 'string') return;
|
||||
const match = content.match(/^#\s*@file:\s*(.+?)(\s|$)/m);
|
||||
if (match) map.set(match[1].trim(), content);
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
function cleanText(text) {
|
||||
return String(text || "").replace(/\r\n/g, "\n");
|
||||
}
|
||||
|
||||
function extractName(block) {
|
||||
const match = block.match(/^ {4}name:\s*(?:["']?)(.*?)(?:["']?)\s*(?:#.*)?$/m);
|
||||
return match ? match[1].trim() : null;
|
||||
}
|
||||
|
||||
function extractKey(block, indentLevel) {
|
||||
const spaceStr = " ".repeat(indentLevel);
|
||||
const re = new RegExp(`^${spaceStr}([^#\\s][^:]+):`);
|
||||
const match = block.match(re);
|
||||
return match ? match[1].trim().replace(/['"]/g, "") : null;
|
||||
}
|
||||
|
||||
function stripFullLineComments(text) {
|
||||
return text.split('\n')
|
||||
.filter(line => !line.trim().startsWith('#'))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function splitBlocks(text, type) {
|
||||
const lines = cleanText(text).split("\n");
|
||||
const blocks = [];
|
||||
let currentBuf = [];
|
||||
|
||||
const isListStart = (l) => l.match(/^ {2}-\s/);
|
||||
const isMapStart2 = (l) => l.match(/^ {2}[^ \-#][^:]*:/);
|
||||
const isMapStart0 = (l) => l.match(/^[^ \-#][^:]*:/);
|
||||
|
||||
const isStart = (l) => {
|
||||
if (type === 'list') return isListStart(l);
|
||||
if (type === 'map2') return isMapStart2(l);
|
||||
if (type === 'map0') return isMapStart0(l);
|
||||
return false;
|
||||
};
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line.match(/^#\s*@file:/)) continue;
|
||||
|
||||
if (isStart(line)) {
|
||||
if (currentBuf.length > 0) {
|
||||
blocks.push(currentBuf.join("\n"));
|
||||
currentBuf = [];
|
||||
}
|
||||
}
|
||||
if (currentBuf.length === 0 && !line.trim()) continue;
|
||||
currentBuf.push(line);
|
||||
}
|
||||
if (currentBuf.length > 0) blocks.push(currentBuf.join("\n"));
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// --- MERGE LOGIC ---
|
||||
function processSection(sectionName, manifestEntries, fileMap, opts) {
|
||||
let sectionOutput = [];
|
||||
const seenKeys = new Set();
|
||||
|
||||
// Добавляем заголовок секции
|
||||
if (!['root', 'x-substore'].includes(sectionName)) {
|
||||
sectionOutput.push(`${sectionName}:`);
|
||||
}
|
||||
if (sectionName === 'x-substore') {
|
||||
sectionOutput.push(`x-substore:`);
|
||||
}
|
||||
|
||||
for (const entry of manifestEntries) {
|
||||
let content = fileMap.get(entry.file);
|
||||
if (!content) throw new Error(`CRITICAL: File "${entry.file}" not found.`);
|
||||
|
||||
if (!opts.clearComments) {
|
||||
sectionOutput.push(`\n# --- source: ${entry.file} | mode: ${entry.mode || "concat"} ---`);
|
||||
}
|
||||
|
||||
if (opts.clearComments) {
|
||||
content = stripFullLineComments(content);
|
||||
}
|
||||
|
||||
// [FIX] Удаляем дублирующий заголовок x-substore из контента файла,
|
||||
// но оставляем содержимое (оно уже имеет правильный отступ 2 пробела)
|
||||
if (sectionName === 'x-substore') {
|
||||
content = content.replace(/^x-substore:\s*(?:#.*)?$/m, '');
|
||||
}
|
||||
|
||||
const mode = entry.mode || "concat";
|
||||
|
||||
if (mode === 'concat') {
|
||||
const lines = cleanText(content).split('\n').filter(l => !l.match(/^#\s*@file:/));
|
||||
sectionOutput.push(lines.join('\n'));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mode === 'first_wins') {
|
||||
let blockType = 'map2';
|
||||
if (sectionName === 'root') blockType = 'map0';
|
||||
if (['proxies', 'proxy-groups'].includes(sectionName)) blockType = 'list';
|
||||
|
||||
const blocks = splitBlocks(content, blockType);
|
||||
|
||||
if (blocks.length === 0 && !opts.clearComments) {
|
||||
sectionOutput.push(`# WARNING: No valid blocks found in ${entry.file}`);
|
||||
}
|
||||
|
||||
for (const block of blocks) {
|
||||
let id = null;
|
||||
if (blockType === 'list') {
|
||||
id = extractName(block);
|
||||
} else if (blockType === 'map0') {
|
||||
id = extractKey(block, 0);
|
||||
} else {
|
||||
id = extractKey(block, 2);
|
||||
}
|
||||
|
||||
// Apply cleaning options for x-substore content
|
||||
if (sectionName === 'x-substore' && id) {
|
||||
if (opts.clearManifest && id === 'manifest') continue;
|
||||
if (opts.clearReplacements && id === 'replacements') continue;
|
||||
}
|
||||
|
||||
if (id) {
|
||||
if (seenKeys.has(id)) {
|
||||
if (!opts.clearComments) sectionOutput.push(`# [SKIP] Duplicate "${id}" ignored`);
|
||||
continue;
|
||||
}
|
||||
seenKeys.add(id);
|
||||
}
|
||||
sectionOutput.push(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sectionOutput.join("\n");
|
||||
}
|
||||
|
||||
// --- MAIN EXECUTION ---
|
||||
try {
|
||||
const opts = normalizeOptions();
|
||||
const fileMap = normalizeFiles($files);
|
||||
|
||||
let manifestKey = null;
|
||||
for (const k of fileMap.keys()) {
|
||||
if (k.startsWith("00-manifest")) {
|
||||
manifestKey = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!manifestKey) throw new Error("Manifest file (00-manifest-...) not found.");
|
||||
|
||||
const manifestRaw = fileMap.get(manifestKey);
|
||||
const manifestObj = ProxyUtils.yaml.safeLoad(manifestRaw);
|
||||
|
||||
if (!manifestObj?.['x-substore']?.manifest) {
|
||||
throw new Error("Invalid Manifest structure.");
|
||||
}
|
||||
|
||||
const manifestList = manifestObj['x-substore'].manifest;
|
||||
const replacements = manifestObj['x-substore'].replacements || [];
|
||||
|
||||
const sectionOrder = [
|
||||
"x-substore", "root", "hosts", "sniffer", "tun", "dns",
|
||||
"proxies", "proxy-providers", "proxy-groups", "rule-providers", "rules"
|
||||
];
|
||||
|
||||
const plan = {};
|
||||
sectionOrder.forEach(s => plan[s] = []);
|
||||
manifestList.forEach(entry => {
|
||||
if (!plan[entry.section]) plan[entry.section] = [];
|
||||
plan[entry.section].push(entry);
|
||||
});
|
||||
|
||||
const finalChunks = [];
|
||||
for (const sec of sectionOrder) {
|
||||
if (!plan[sec] || plan[sec].length === 0) continue;
|
||||
const secStr = processSection(sec, plan[sec], fileMap, opts);
|
||||
finalChunks.push(secStr);
|
||||
}
|
||||
|
||||
let result = finalChunks.join("\n\n");
|
||||
|
||||
if (Array.isArray(replacements)) {
|
||||
replacements.forEach(rep => {
|
||||
if (rep.from && rep.to) {
|
||||
result = result.split(rep.from).join(rep.to);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$content = result;
|
||||
|
||||
} catch (err) {
|
||||
$content = `# CRITICAL ERROR:\n# ${err.message}\n# Stack: ${err.stack}`;
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
/**
|
||||
* SUB STORE YAML ASSEMBLER (Content-Tag Aware)
|
||||
* * Требование: Каждый файл должен начинаться с комментария:
|
||||
* # @file: filename.yaml
|
||||
*/
|
||||
|
||||
// --- UTILS ---
|
||||
|
||||
// Функция нормализации теперь парсит содержимое, чтобы найти имя файла
|
||||
function normalizeFiles(rawFiles) {
|
||||
const map = new Map();
|
||||
if (!rawFiles || !Array.isArray(rawFiles)) return map;
|
||||
|
||||
rawFiles.forEach((content, index) => {
|
||||
if (typeof content !== 'string') return;
|
||||
|
||||
// Ищем магический тег: # @file: filename.yaml
|
||||
const match = content.match(/^#\s*@file:\s*(.+?)(\s|$)/m);
|
||||
|
||||
if (match) {
|
||||
const filename = match[1].trim();
|
||||
map.set(filename, content);
|
||||
} else {
|
||||
// Если тега нет, файл остается "анонимным" и недоступным через манифест,
|
||||
// но мы можем логировать это.
|
||||
// console.log(`File at index ${index} has no @file tag`);
|
||||
}
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function cleanText(text) {
|
||||
return String(text || "").replace(/\r\n/g, "\n");
|
||||
}
|
||||
|
||||
// Извлечение name из элемента списка
|
||||
function extractName(block) {
|
||||
// Ищем name: value с учетом отступов (4 пробела)
|
||||
const match = block.match(/^ {4}name:\s*(?:["']?)(.*?)(?:["']?)\s*(?:#.*)?$/m);
|
||||
return match ? match[1].trim() : null;
|
||||
}
|
||||
|
||||
// Извлечение ключа из map (0 или 2 пробела)
|
||||
function extractKey(block, indentLevel) {
|
||||
const spaceStr = " ".repeat(indentLevel);
|
||||
const re = new RegExp(`^${spaceStr}([^#\\s][^:]+):`);
|
||||
const match = block.match(re);
|
||||
return match ? match[1].trim().replace(/['"]/g, "") : null;
|
||||
}
|
||||
|
||||
// Разбивка текста на логические блоки
|
||||
function splitBlocks(text, type) {
|
||||
const lines = cleanText(text).split("\n");
|
||||
const blocks = [];
|
||||
let currentBuf = [];
|
||||
|
||||
// Детекторы начала блока
|
||||
const isListStart = (l) => l.match(/^ {2}-\s/); // " - "
|
||||
const isMapStart2 = (l) => l.match(/^ {2}[^ \-#][^:]*:/); // " key:"
|
||||
const isMapStart0 = (l) => l.match(/^[^ \-#][^:]*:/); // "key:" (root)
|
||||
|
||||
const isStart = (l) => {
|
||||
if (type === 'list') return isListStart(l);
|
||||
if (type === 'map2') return isMapStart2(l);
|
||||
if (type === 'map0') return isMapStart0(l);
|
||||
return false;
|
||||
};
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Если строка начинается с # @file:, пропускаем ее, чтобы не мусорить в конфиге
|
||||
if (line.match(/^#\s*@file:/)) continue;
|
||||
|
||||
if (isStart(line)) {
|
||||
if (currentBuf.length > 0) {
|
||||
blocks.push(currentBuf.join("\n"));
|
||||
currentBuf = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Пропуск пустых строк в начале, если буфер пуст
|
||||
if (currentBuf.length === 0 && !line.trim()) continue;
|
||||
|
||||
currentBuf.push(line);
|
||||
}
|
||||
|
||||
if (currentBuf.length > 0) {
|
||||
blocks.push(currentBuf.join("\n"));
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// --- MERGE LOGIC ---
|
||||
|
||||
function processSection(sectionName, manifestEntries, fileMap) {
|
||||
let sectionOutput = [];
|
||||
const seenKeys = new Set();
|
||||
|
||||
// Добавляем заголовок секции (кроме root, x-substore и rules)
|
||||
if (!['root', 'x-substore'].includes(sectionName)) {
|
||||
sectionOutput.push(`${sectionName}:`);
|
||||
}
|
||||
if (sectionName === 'x-substore') {
|
||||
sectionOutput.push(`x-substore:`);
|
||||
}
|
||||
|
||||
for (const entry of manifestEntries) {
|
||||
const content = fileMap.get(entry.file);
|
||||
|
||||
if (!content) {
|
||||
// FAIL-FAST: Если файл из манифеста не найден (нет тега или не загружен)
|
||||
throw new Error(`CRITICAL: File "${entry.file}" not found inside input bundle. Did you add '# @file: ${entry.file}' header?`);
|
||||
}
|
||||
|
||||
const mode = entry.mode || "concat";
|
||||
sectionOutput.push(`\n# --- source: ${entry.file} | mode: ${mode} ---`);
|
||||
|
||||
if (mode === 'concat') {
|
||||
// Просто чистим от тега @file при вставке
|
||||
const lines = cleanText(content).split('\n').filter(l => !l.match(/^#\s*@file:/));
|
||||
sectionOutput.push(lines.join('\n'));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mode === 'first_wins') {
|
||||
let blockType = 'map2';
|
||||
if (sectionName === 'root') blockType = 'map0';
|
||||
if (['proxies', 'proxy-groups'].includes(sectionName)) blockType = 'list';
|
||||
|
||||
const blocks = splitBlocks(content, blockType);
|
||||
|
||||
if (blocks.length === 0) {
|
||||
sectionOutput.push(`# WARNING: No valid blocks found in ${entry.file}`);
|
||||
}
|
||||
|
||||
for (const block of blocks) {
|
||||
let id = null;
|
||||
|
||||
if (blockType === 'list') {
|
||||
id = extractName(block);
|
||||
} else if (blockType === 'map0') {
|
||||
id = extractKey(block, 0);
|
||||
} else {
|
||||
id = extractKey(block, 2);
|
||||
}
|
||||
|
||||
if (id) {
|
||||
if (seenKeys.has(id)) {
|
||||
sectionOutput.push(`# [SKIP] Duplicate "${id}" ignored`);
|
||||
continue;
|
||||
}
|
||||
seenKeys.add(id);
|
||||
}
|
||||
|
||||
sectionOutput.push(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sectionOutput.join("\n");
|
||||
}
|
||||
|
||||
// --- MAIN EXECUTION ---
|
||||
|
||||
try {
|
||||
// 1. Создаем карту файлов на основе тегов # @file:
|
||||
const fileMap = normalizeFiles($files);
|
||||
|
||||
// 2. Ищем Манифест
|
||||
let manifestKey = null;
|
||||
// Ищем файл, чье имя (из тега) начинается с 00-manifest
|
||||
for (const k of fileMap.keys()) {
|
||||
if (k.startsWith("00-manifest")) {
|
||||
manifestKey = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!manifestKey) {
|
||||
const foundFiles = Array.from(fileMap.keys()).join(", ");
|
||||
throw new Error(`Manifest file (00-manifest-...) not found in headers. Found files: [${foundFiles}]`);
|
||||
}
|
||||
|
||||
// 3. Парсим Манифест
|
||||
const manifestRaw = fileMap.get(manifestKey);
|
||||
const manifestObj = ProxyUtils.yaml.safeLoad(manifestRaw);
|
||||
|
||||
if (!manifestObj?.['x-substore']?.manifest) {
|
||||
throw new Error("Invalid Manifest: missing x-substore.manifest structure.");
|
||||
}
|
||||
|
||||
const manifestList = manifestObj['x-substore'].manifest;
|
||||
const replacements = manifestObj['x-substore'].replacements || [];
|
||||
|
||||
// 4. Планирование порядка секций
|
||||
const sectionOrder = [
|
||||
"x-substore", "root", "hosts", "sniffer", "tun", "dns",
|
||||
"proxies", "proxy-providers", "proxy-groups", "rule-providers", "rules"
|
||||
];
|
||||
|
||||
const plan = {};
|
||||
sectionOrder.forEach(s => plan[s] = []);
|
||||
|
||||
manifestList.forEach(entry => {
|
||||
// Если секция в манифесте есть, а в нашем плане нет - добавляем динамически (на всякий случай)
|
||||
if (!plan[entry.section]) plan[entry.section] = [];
|
||||
plan[entry.section].push(entry);
|
||||
});
|
||||
|
||||
// 5. Сборка
|
||||
const finalChunks = [];
|
||||
|
||||
for (const sec of sectionOrder) {
|
||||
if (!plan[sec] || plan[sec].length === 0) continue;
|
||||
|
||||
// Обработка секции
|
||||
const secStr = processSection(sec, plan[sec], fileMap);
|
||||
finalChunks.push(secStr);
|
||||
}
|
||||
|
||||
let result = finalChunks.join("\n\n");
|
||||
|
||||
// 6. Replacements (Literal)
|
||||
if (Array.isArray(replacements)) {
|
||||
replacements.forEach(rep => {
|
||||
if (rep.from && rep.to) {
|
||||
result = result.split(rep.from).join(rep.to);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$content = result;
|
||||
|
||||
} catch (err) {
|
||||
$content = `# CRITICAL ERROR in Assembler:\n# ${err.message}\n\n# Stack:\n${err.stack}`;
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
/**********************
|
||||
* Defaults (AmneziaWG)
|
||||
* Если в исходнике нет параметра, берём отсюда.
|
||||
* Если итоговое значение == 0, параметр пропускаем в amnezia-wg-option.
|
||||
**********************/
|
||||
const AMZ_DEFAULTS = {
|
||||
Jc: 4,
|
||||
Jmin: 10,
|
||||
Jmax: 50,
|
||||
S1: 110,
|
||||
S2: 120,
|
||||
H1: 0,
|
||||
H2: 0,
|
||||
H3: 0,
|
||||
H4: 0,
|
||||
};
|
||||
|
||||
/**********************
|
||||
* Options from Sub Store
|
||||
* Example URL:
|
||||
* .../convert-awg-to-clash.js#dns=false&ipv6=false#noCache
|
||||
*
|
||||
* Требования:
|
||||
* - dns=false => remote-dns-resolve: false (вне зависимости от входа)
|
||||
* - ipv6=false => удалить IPv6 из allowed-ips (и вообще не добавлять ipv6-части)
|
||||
**********************/
|
||||
function normalizeOptions() {
|
||||
const args = (typeof $arguments !== "undefined" && $arguments) ? $arguments : {};
|
||||
|
||||
const asBool = (v, def = true) => {
|
||||
if (v === undefined || v === null || v === "") return def;
|
||||
if (typeof v === "boolean") return v;
|
||||
const s = String(v).toLowerCase().trim();
|
||||
if (["1", "true", "yes", "y", "on"].includes(s)) return true;
|
||||
if (["0", "false", "no", "n", "off"].includes(s)) return false;
|
||||
return def;
|
||||
};
|
||||
|
||||
return {
|
||||
dns: asBool(args.dns, true),
|
||||
ipv6: asBool(args.ipv6, true),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**********************
|
||||
* Parsing WG INI blocks
|
||||
**********************/
|
||||
function cleanLines(text) {
|
||||
return String(text ?? "")
|
||||
.replace(/\r\n/g, "\n")
|
||||
.split("\n");
|
||||
}
|
||||
|
||||
// Парсим один INI-фрагмент с [Interface] и [Peer] (один peer)
|
||||
function parseIniOne(text) {
|
||||
const lines = cleanLines(text)
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l.length > 0 && !l.startsWith("#") && !l.startsWith(";"));
|
||||
|
||||
let section = null;
|
||||
const data = { Interface: {}, Peer: {} };
|
||||
|
||||
for (const line of lines) {
|
||||
const mSec = line.match(/^\[(.+?)\]$/);
|
||||
if (mSec) {
|
||||
section = mSec[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
const mKV = line.match(/^([^=]+?)\s*=\s*(.+)$/);
|
||||
if (!mKV || !section) continue;
|
||||
|
||||
const key = mKV[1].trim();
|
||||
const value = mKV[2].trim();
|
||||
|
||||
if (section === "Interface") data.Interface[key] = value;
|
||||
else if (section === "Peer") data.Peer[key] = value;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Разбиваем весь файл на блоки по заголовкам "##### ..."
|
||||
function splitByHeaders(fullText) {
|
||||
const lines = cleanLines(fullText);
|
||||
|
||||
const blocks = [];
|
||||
let current = { name: "amz-wg", buf: [] };
|
||||
|
||||
const headerRe = /^#{5}\s*(.+)\s*$/;
|
||||
|
||||
for (const line of lines) {
|
||||
const mh = line.match(headerRe);
|
||||
if (mh) {
|
||||
// закрываем предыдущий блок, если там что-то есть
|
||||
if (current.buf.join("\n").trim().length > 0) blocks.push(current);
|
||||
current = { name: mh[1].trim(), buf: [] };
|
||||
continue;
|
||||
}
|
||||
current.buf.push(line);
|
||||
}
|
||||
|
||||
if (current.buf.join("\n").trim().length > 0) blocks.push(current);
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function splitList(val) {
|
||||
return String(val || "")
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function parseEndpoint(endpoint) {
|
||||
// Поддержка:
|
||||
// - host:port
|
||||
// - [ipv6]:port
|
||||
const s = String(endpoint || "").trim();
|
||||
const v6 = s.match(/^\[(.+?)\]:(\d+)$/);
|
||||
if (v6) return { host: v6[1], port: Number(v6[2]) };
|
||||
|
||||
const v4 = s.match(/^(.+?):(\d+)$/);
|
||||
if (v4) return { host: v4[1], port: Number(v4[2]) };
|
||||
|
||||
return { host: "", port: 0 };
|
||||
}
|
||||
|
||||
function toNumberOrNull(v) {
|
||||
const s = String(v ?? "").trim();
|
||||
if (s === "") return null;
|
||||
if (/^-?\d+$/.test(s)) return Number(s);
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildAmzOptions(interfaceObj) {
|
||||
// Правило:
|
||||
// - если в файле есть параметр => используем его
|
||||
// - иначе берём из AMZ_DEFAULTS
|
||||
// - если итог == 0 => пропускаем
|
||||
const out = {};
|
||||
const keys = Object.keys(AMZ_DEFAULTS);
|
||||
|
||||
for (const K of keys) {
|
||||
const fromFile = interfaceObj[K];
|
||||
const fileNum = toNumberOrNull(fromFile);
|
||||
const fallback = AMZ_DEFAULTS[K];
|
||||
|
||||
const finalVal =
|
||||
fileNum !== null ? fileNum : (fallback ?? 0);
|
||||
|
||||
if (Number(finalVal) !== 0) {
|
||||
out[K.toLowerCase()] = Number(finalVal);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function filterAllowedIPs(allowed, enableIPv6) {
|
||||
if (enableIPv6) return allowed;
|
||||
// выкидываем всё, что похоже на IPv6
|
||||
return allowed.filter((cidr) => !cidr.includes(":"));
|
||||
}
|
||||
|
||||
function buildProxy(blockName, wg, options) {
|
||||
const i = wg.Interface || {};
|
||||
const p = wg.Peer || {};
|
||||
|
||||
const address = i.Address || "";
|
||||
const dnsList = splitList(i.DNS);
|
||||
|
||||
const ep = parseEndpoint(p.Endpoint);
|
||||
let allowed = splitList(p.AllowedIPs);
|
||||
allowed = filterAllowedIPs(allowed, options.ipv6);
|
||||
|
||||
const proxy = {
|
||||
name: blockName || "amz-wg",
|
||||
type: "wireguard",
|
||||
ip: address,
|
||||
// ipv6 поле в твоём примере закомментировано, так что не добавляем вообще
|
||||
"private-key": i.PrivateKey || "",
|
||||
peers: [
|
||||
{
|
||||
server: ep.host,
|
||||
port: ep.port,
|
||||
"public-key": p.PublicKey || "",
|
||||
...(p.PresharedKey ? { "pre-shared-key": p.PresharedKey } : {}),
|
||||
"allowed-ips": allowed,
|
||||
},
|
||||
],
|
||||
udp: true,
|
||||
// dns=false => принудительно false
|
||||
"remote-dns-resolve": options.dns ? true : false,
|
||||
...(dnsList.length ? { dns: dnsList } : {}),
|
||||
};
|
||||
|
||||
const amz = buildAmzOptions(i);
|
||||
if (Object.keys(amz).length) {
|
||||
proxy["amnezia-wg-option"] = amz;
|
||||
}
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
/**********************
|
||||
* ENTRYPOINT
|
||||
**********************/
|
||||
const opts = normalizeOptions();
|
||||
|
||||
// Вход: чаще всего $content, но на всякий пожарный берём $files[0]
|
||||
const input = String($content ?? ($files && $files[0]) ?? "");
|
||||
|
||||
// Разбиваем по заголовкам ##### ...
|
||||
const blocks = splitByHeaders(input);
|
||||
|
||||
// Для каждого блока парсим INI и строим proxy
|
||||
const proxies = [];
|
||||
for (const b of blocks) {
|
||||
const iniText = b.buf.join("\n").trim();
|
||||
if (!iniText) continue;
|
||||
|
||||
const wg = parseIniOne(iniText);
|
||||
|
||||
// минимальная валидация: нужны ключи
|
||||
if (!wg.Interface?.PrivateKey || !wg.Peer?.PublicKey || !wg.Peer?.Endpoint) {
|
||||
// пропускаем мусорные блоки, чтобы не ронять весь конвертер
|
||||
continue;
|
||||
}
|
||||
|
||||
proxies.push(buildProxy(b.name, wg, opts));
|
||||
}
|
||||
|
||||
// Финальный YAML
|
||||
$content = ProxyUtils.yaml.safeDump({ proxies });
|
||||
@@ -1,28 +0,0 @@
|
||||
// Example:
|
||||
// Script Operator
|
||||
// 1. backend version(>2.14.88):
|
||||
$server.name = 'prefix-' + $server.name
|
||||
$server.ecn = true
|
||||
$server['test-url'] = 'http://1.0.0.1/generate_204'
|
||||
// 2. operator function
|
||||
function operator(proxies, targetPlatform, context) {
|
||||
return proxies.map( proxy => {
|
||||
// Change proxy information here
|
||||
|
||||
return proxy;
|
||||
});
|
||||
}
|
||||
|
||||
// Script Filter
|
||||
// 1. backend version(>2.14.119):
|
||||
const port = Number($server.port)
|
||||
return [80].includes(port)
|
||||
|
||||
// 2. filter function
|
||||
function filter(proxies, targetPlatform) {
|
||||
return proxies.map( proxy => {
|
||||
// Return true if the current proxy is selected
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/**
|
||||
* 节点名改为花里胡哨字体,仅支持英文字符和数字
|
||||
*
|
||||
* 【字体】
|
||||
* 可参考:https://www.dute.org/weird-fonts
|
||||
* serif-bold, serif-italic, serif-bold-italic, sans-serif-regular, sans-serif-bold-italic, script-regular, script-bold, fraktur-regular, fraktur-bold, monospace-regular, double-struck-bold, circle-regular, square-regular, modifier-letter(小写没有 q, 用 ᵠ 替代. 大写缺的太多, 用小写替代)
|
||||
*
|
||||
* 【示例】
|
||||
* 1️⃣ 设置所有格式为 "serif-bold"
|
||||
* #type=serif-bold
|
||||
*
|
||||
* 2️⃣ 设置字母格式为 "serif-bold",数字格式为 "circle-regular"
|
||||
* #type=serif-bold&num=circle-regular
|
||||
*/
|
||||
|
||||
global.$arguments = { type: "serif-bold" };
|
||||
|
||||
function operator(proxies) {
|
||||
const { type, num } = $arguments;
|
||||
const TABLE = {
|
||||
"serif-bold": ["𝟎","𝟏","𝟐","𝟑","𝟒","𝟓","𝟔","𝟕","𝟖","𝟗","𝐚","𝐛","𝐜","𝐝","𝐞","𝐟","𝐠","𝐡","𝐢","𝐣","𝐤","𝐥","𝐦","𝐧","𝐨","𝐩","𝐪","𝐫","𝐬","𝐭","𝐮","𝐯","𝐰","𝐱","𝐲","𝐳","𝐀","𝐁","𝐂","𝐃","𝐄","𝐅","𝐆","𝐇","𝐈","𝐉","𝐊","𝐋","𝐌","𝐍","𝐎","𝐏","𝐐","𝐑","𝐒","𝐓","𝐔","𝐕","𝐖","𝐗","𝐘","𝐙"] ,
|
||||
"serif-italic": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "𝑎", "𝑏", "𝑐", "𝑑", "𝑒", "𝑓", "𝑔", "ℎ", "𝑖", "𝑗", "𝑘", "𝑙", "𝑚", "𝑛", "𝑜", "𝑝", "𝑞", "𝑟", "𝑠", "𝑡", "𝑢", "𝑣", "𝑤", "𝑥", "𝑦", "𝑧", "𝐴", "𝐵", "𝐶", "𝐷", "𝐸", "𝐹", "𝐺", "𝐻", "𝐼", "𝐽", "𝐾", "𝐿", "𝑀", "𝑁", "𝑂", "𝑃", "𝑄", "𝑅", "𝑆", "𝑇", "𝑈", "𝑉", "𝑊", "𝑋", "𝑌", "𝑍"],
|
||||
"serif-bold-italic": ["0","1","2","3","4","5","6","7","8","9","𝒂","𝒃","𝒄","𝒅","𝒆","𝒇","𝒈","𝒉","𝒊","𝒋","𝒌","𝒍","𝒎","𝒏","𝒐","𝒑","𝒒","𝒓","𝒔","𝒕","𝒖","𝒗","𝒘","𝒙","𝒚","𝒛","𝑨","𝑩","𝑪","𝑫","𝑬","𝑭","𝑮","𝑯","𝑰","𝑱","𝑲","𝑳","𝑴","𝑵","𝑶","𝑷","𝑸","𝑹","𝑺","𝑻","𝑼","𝑽","𝑾","𝑿","𝒀","𝒁"],
|
||||
"sans-serif-regular": ["𝟢", "𝟣", "𝟤", "𝟥", "𝟦", "𝟧", "𝟨", "𝟩", "𝟪", "𝟫", "𝖺", "𝖻", "𝖼", "𝖽", "𝖾", "𝖿", "𝗀", "𝗁", "𝗂", "𝗃", "𝗄", "𝗅", "𝗆", "𝗇", "𝗈", "𝗉", "𝗊", "𝗋", "𝗌", "𝗍", "𝗎", "𝗏", "𝗐", "𝗑", "𝗒", "𝗓", "𝖠", "𝖡", "𝖢", "𝖣", "𝖤", "𝖥", "𝖦", "𝖧", "𝖨", "𝖩", "𝖪", "𝖫", "𝖬", "𝖭", "𝖮", "𝖯", "𝖰", "𝖱", "𝖲", "𝖳", "𝖴", "𝖵", "𝖶", "𝖷", "𝖸", "𝖹"],
|
||||
"sans-serif-bold": ["𝟬","𝟭","𝟮","𝟯","𝟰","𝟱","𝟲","𝟳","𝟴","𝟵","𝗮","𝗯","𝗰","𝗱","𝗲","𝗳","𝗴","𝗵","𝗶","𝗷","𝗸","𝗹","𝗺","𝗻","𝗼","𝗽","𝗾","𝗿","𝘀","𝘁","𝘂","𝘃","𝘄","𝘅","𝘆","𝘇","𝗔","𝗕","𝗖","𝗗","𝗘","𝗙","𝗚","𝗛","𝗜","𝗝","𝗞","𝗟","𝗠","𝗡","𝗢","𝗣","𝗤","𝗥","𝗦","𝗧","𝗨","𝗩","𝗪","𝗫","𝗬","𝗭"],
|
||||
"sans-serif-italic": ["0","1","2","3","4","5","6","7","8","9","𝘢","𝘣","𝘤","𝘥","𝘦","𝘧","𝘨","𝘩","𝘪","𝘫","𝘬","𝘭","𝘮","𝘯","𝘰","𝘱","𝘲","𝘳","𝘴","𝘵","𝘶","𝘷","𝘸","𝘹","𝘺","𝘻","𝘈","𝘉","𝘊","𝘋","𝘌","𝘍","𝘎","𝘏","𝘐","𝘑","𝘒","𝘓","𝘔","𝘕","𝘖","𝘗","𝘘","𝘙","𝘚","𝘛","𝘜","𝘝","𝘞","𝘟","𝘠","𝘡"],
|
||||
"sans-serif-bold-italic": ["0","1","2","3","4","5","6","7","8","9","𝙖","𝙗","𝙘","𝙙","𝙚","𝙛","𝙜","𝙝","𝙞","𝙟","𝙠","𝙡","𝙢","𝙣","𝙤","𝙥","𝙦","𝙧","𝙨","𝙩","𝙪","𝙫","𝙬","𝙭","𝙮","𝙯","𝘼","𝘽","𝘾","𝘿","𝙀","𝙁","𝙂","𝙃","𝙄","𝙅","𝙆","𝙇","𝙈","𝙉","𝙊","𝙋","𝙌","𝙍","𝙎","𝙏","𝙐","𝙑","𝙒","𝙓","𝙔","𝙕"],
|
||||
"script-regular": ["0","1","2","3","4","5","6","7","8","9","𝒶","𝒷","𝒸","𝒹","ℯ","𝒻","ℊ","𝒽","𝒾","𝒿","𝓀","𝓁","𝓂","𝓃","ℴ","𝓅","𝓆","𝓇","𝓈","𝓉","𝓊","𝓋","𝓌","𝓍","𝓎","𝓏","𝒜","ℬ","𝒞","𝒟","ℰ","ℱ","𝒢","ℋ","ℐ","𝒥","𝒦","ℒ","ℳ","𝒩","𝒪","𝒫","𝒬","ℛ","𝒮","𝒯","𝒰","𝒱","𝒲","𝒳","𝒴","𝒵"],
|
||||
"script-bold": ["0","1","2","3","4","5","6","7","8","9","𝓪","𝓫","𝓬","𝓭","𝓮","𝓯","𝓰","𝓱","𝓲","𝓳","𝓴","𝓵","𝓶","𝓷","𝓸","𝓹","𝓺","𝓻","𝓼","𝓽","𝓾","𝓿","𝔀","𝔁","𝔂","𝔃","𝓐","𝓑","𝓒","𝓓","𝓔","𝓕","𝓖","𝓗","𝓘","𝓙","𝓚","𝓛","𝓜","𝓝","𝓞","𝓟","𝓠","𝓡","𝓢","𝓣","𝓤","𝓥","𝓦","𝓧","𝓨","𝓩"],
|
||||
"fraktur-regular": ["0","1","2","3","4","5","6","7","8","9","𝔞","𝔟","𝔠","𝔡","𝔢","𝔣","𝔤","𝔥","𝔦","𝔧","𝔨","𝔩","𝔪","𝔫","𝔬","𝔭","𝔮","𝔯","𝔰","𝔱","𝔲","𝔳","𝔴","𝔵","𝔶","𝔷","𝔄","𝔅","ℭ","𝔇","𝔈","𝔉","𝔊","ℌ","ℑ","𝔍","𝔎","𝔏","𝔐","𝔑","𝔒","𝔓","𝔔","ℜ","𝔖","𝔗","𝔘","𝔙","𝔚","𝔛","𝔜","ℨ"],
|
||||
"fraktur-bold": ["0","1","2","3","4","5","6","7","8","9","𝖆","𝖇","𝖈","𝖉","𝖊","𝖋","𝖌","𝖍","𝖎","𝖏","𝖐","𝖑","𝖒","𝖓","𝖔","𝖕","𝖖","𝖗","𝖘","𝖙","𝖚","𝖛","𝖜","𝖝","𝖞","𝖟","𝕬","𝕭","𝕮","𝕯","𝕰","𝕱","𝕲","𝕳","𝕴","𝕵","𝕶","𝕷","𝕸","𝕹","𝕺","𝕻","𝕼","𝕽","𝕾","𝕿","𝖀","𝖁","𝖂","𝖃","𝖄","𝖅"],
|
||||
"monospace-regular": ["𝟶","𝟷","𝟸","𝟹","𝟺","𝟻","𝟼","𝟽","𝟾","𝟿","𝚊","𝚋","𝚌","𝚍","𝚎","𝚏","𝚐","𝚑","𝚒","𝚓","𝚔","𝚕","𝚖","𝚗","𝚘","𝚙","𝚚","𝚛","𝚜","𝚝","𝚞","𝚟","𝚠","𝚡","𝚢","𝚣","𝙰","𝙱","𝙲","𝙳","𝙴","𝙵","𝙶","𝙷","𝙸","𝙹","𝙺","𝙻","𝙼","𝙽","𝙾","𝙿","𝚀","𝚁","𝚂","𝚃","𝚄","𝚅","𝚆","𝚇","𝚈","𝚉"],
|
||||
"double-struck-bold": ["𝟘","𝟙","𝟚","𝟛","𝟜","𝟝","𝟞","𝟟","𝟠","𝟡","𝕒","𝕓","𝕔","𝕕","𝕖","𝕗","𝕘","𝕙","𝕚","𝕛","𝕜","𝕝","𝕞","𝕟","𝕠","𝕡","𝕢","𝕣","𝕤","𝕥","𝕦","𝕧","𝕨","𝕩","𝕪","𝕫","𝔸","𝔹","ℂ","𝔻","𝔼","𝔽","𝔾","ℍ","𝕀","𝕁","𝕂","𝕃","𝕄","ℕ","𝕆","ℙ","ℚ","ℝ","𝕊","𝕋","𝕌","𝕍","𝕎","𝕏","𝕐","ℤ"],
|
||||
"circle-regular": ["⓪","①","②","③","④","⑤","⑥","⑦","⑧","⑨","ⓐ","ⓑ","ⓒ","ⓓ","ⓔ","ⓕ","ⓖ","ⓗ","ⓘ","ⓙ","ⓚ","ⓛ","ⓜ","ⓝ","ⓞ","ⓟ","ⓠ","ⓡ","ⓢ","ⓣ","ⓤ","ⓥ","ⓦ","ⓧ","ⓨ","ⓩ","Ⓐ","Ⓑ","Ⓒ","Ⓓ","Ⓔ","Ⓕ","Ⓖ","Ⓗ","Ⓘ","Ⓙ","Ⓚ","Ⓛ","Ⓜ","Ⓝ","Ⓞ","Ⓟ","Ⓠ","Ⓡ","Ⓢ","Ⓣ","Ⓤ","Ⓥ","Ⓦ","Ⓧ","Ⓨ","Ⓩ"],
|
||||
"square-regular": ["0","1","2","3","4","5","6","7","8","9","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉"],
|
||||
"modifier-letter": ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹", "ᵃ", "ᵇ", "ᶜ", "ᵈ", "ᵉ", "ᶠ", "ᵍ", "ʰ", "ⁱ", "ʲ", "ᵏ", "ˡ", "ᵐ", "ⁿ", "ᵒ", "ᵖ", "ᵠ", "ʳ", "ˢ", "ᵗ", "ᵘ", "ᵛ", "ʷ", "ˣ", "ʸ", "ᶻ", "ᴬ", "ᴮ", "ᶜ", "ᴰ", "ᴱ", "ᶠ", "ᴳ", "ʰ", "ᴵ", "ᴶ", "ᴷ", "ᴸ", "ᴹ", "ᴺ", "ᴼ", "ᴾ", "ᵠ", "ᴿ", "ˢ", "ᵀ", "ᵁ", "ᵛ", "ᵂ", "ˣ", "ʸ", "ᶻ"],
|
||||
};
|
||||
|
||||
// charCode => index in `TABLE`
|
||||
const INDEX = { "48": 0, "49": 1, "50": 2, "51": 3, "52": 4, "53": 5, "54": 6, "55": 7, "56": 8, "57": 9, "65": 36, "66": 37, "67": 38, "68": 39, "69": 40, "70": 41, "71": 42, "72": 43, "73": 44, "74": 45, "75": 46, "76": 47, "77": 48, "78": 49, "79": 50, "80": 51, "81": 52, "82": 53, "83": 54, "84": 55, "85": 56, "86": 57, "87": 58, "88": 59, "89": 60, "90": 61, "97": 10, "98": 11, "99": 12, "100": 13, "101": 14, "102": 15, "103": 16, "104": 17, "105": 18, "106": 19, "107": 20, "108": 21, "109": 22, "110": 23, "111": 24, "112": 25, "113": 26, "114": 27, "115": 28, "116": 29, "117": 30, "118": 31, "119": 32, "120": 33, "121": 34, "122": 35 };
|
||||
|
||||
return proxies.map(p => {
|
||||
p.name = [...p.name].map(c => {
|
||||
if (/[a-zA-Z0-9]/.test(c)) {
|
||||
const code = c.charCodeAt(0);
|
||||
const index = INDEX[code];
|
||||
if (isNumber(code) && num) {
|
||||
return TABLE[num][index];
|
||||
} else {
|
||||
return TABLE[type][index];
|
||||
}
|
||||
}
|
||||
return c;
|
||||
}).join("");
|
||||
return p;
|
||||
})
|
||||
}
|
||||
|
||||
function isNumber(code) { return code >= 48 && code <= 57; }
|
||||
@@ -1,328 +0,0 @@
|
||||
/**
|
||||
* Sub-Store operator: Normalize + tag + country detect + per-country numbering
|
||||
*
|
||||
* Output format (default):
|
||||
* 🇺🇸 USA-01 🔒 📺 ❻ ▫️ws/vless/443
|
||||
*
|
||||
* Notes:
|
||||
* - Numbering is computed per-country AFTER grouping the full list.
|
||||
* - Tags (icons) do NOT affect numbering order.
|
||||
* - GeoIP is optional and only used when server is an IP and utils.geoip.lookup exists.
|
||||
*/
|
||||
|
||||
///////////////////////
|
||||
// CONFIG (EDIT ME)
|
||||
///////////////////////
|
||||
|
||||
// 1) Remove these patterns (marketing noise, brackets, separators, etc.)
|
||||
const NOISE_PATTERNS = [
|
||||
/\[[^\]]*]/g, // [ ... ]
|
||||
/\([^)]*\)/g, // ( ... )
|
||||
/\{[^}]*}/g, // { ... }
|
||||
/\btraffic\b/gi,
|
||||
/\bfree\b/gi,
|
||||
/\bwebsite\b/gi,
|
||||
/\bexpire\b/gi,
|
||||
/\blow\s*ping\b/gi,
|
||||
/\bai\s*studio\b/gi,
|
||||
/\bno\s*p2p\b/gi,
|
||||
/\b10\s*gbit\b/gi,
|
||||
/\bvless\b/gi, // you said you don't want it in the visible name
|
||||
/\bvmess\b/gi,
|
||||
/\bssr?\b/gi,
|
||||
/\btrojan\b/gi,
|
||||
/\bhysteria2?\b/gi,
|
||||
/\btuic\b/gi,
|
||||
/[|]/g,
|
||||
/[_]+/g,
|
||||
/[-]{2,}/g
|
||||
];
|
||||
|
||||
// 2) Keyword -> icon tags (if found in original name, icon is added; the keyword is removed from base name)
|
||||
const ICON_RULES = [
|
||||
{ regex: /\bYT\b/gi, icon: "📺" },
|
||||
{ regex: /\bIPv6\b/gi, icon: "❻" },
|
||||
{ regex: /\bNetflix\b|\bNF\b/gi, icon: "🎬" },
|
||||
{ regex: /\bDisney\+?\b|\bDSNY\b/gi, icon: "🏰" },
|
||||
{ regex: /\bHBO\b/gi, icon: "📼" },
|
||||
{ regex: /\bPrime\b|\bAmazon\b/gi, icon: "📦" },
|
||||
{ regex: /\bChatGPT\b|\bOpenAI\b/gi, icon: "🤖" },
|
||||
{ regex: /\bSteam\b/gi, icon: "🎮" },
|
||||
];
|
||||
|
||||
// 3) Optional “network” tag rules based on NAME text (not $server.network)
|
||||
// (Useful if providers shove "BGP/IPLC" into the node name)
|
||||
const NAME_NETWORK_TAGS = [
|
||||
{ regex: /\bIPLC\b/gi, tag: "🛰️" },
|
||||
{ regex: /\bBGP\b/gi, tag: "🧭" },
|
||||
{ regex: /\bAnycast\b/gi, tag: "🌍" }
|
||||
];
|
||||
|
||||
// 4) Country detection rules by NAME (regex). First match wins (priority = lower is earlier)
|
||||
const COUNTRY_RULES = [
|
||||
// USA
|
||||
{ regex: /\b(USA|US|UNITED\s*STATES|AMERICA|NEW\s*YORK|NYC|LOS\s*ANGELES|LA|DALLAS|CHI(CAGO)?)\b/i, iso3: "USA", flag: "🇺🇸", priority: 10 },
|
||||
|
||||
// Germany
|
||||
{ regex: /\b(DE|DEU|GER(MANY)?|FRANKFURT|BERLIN|MUNICH|MÜNCHEN)\b/i, iso3: "DEU", flag: "🇩🇪", priority: 20 },
|
||||
|
||||
// Netherlands
|
||||
{ regex: /\b(NL|NLD|NETHERLANDS|HOLLAND|AMSTERDAM)\b/i, iso3: "NLD", flag: "🇳🇱", priority: 30 },
|
||||
|
||||
// UK
|
||||
{ regex: /\b(UK|GB|GBR|UNITED\s*KINGDOM|LONDON|MANCHESTER)\b/i, iso3: "GBR", flag: "🇬🇧", priority: 40 },
|
||||
|
||||
// France
|
||||
{ regex: /\b(FR|FRA|FRANCE|PARIS|MARSEILLE)\b/i, iso3: "FRA", flag: "🇫🇷", priority: 50 },
|
||||
|
||||
// Poland
|
||||
{ regex: /\b(PL|POL|POLAND|WARSAW|WARSZAWA)\b/i, iso3: "POL", flag: "🇵🇱", priority: 60 },
|
||||
|
||||
// Finland
|
||||
{ regex: /\b(FI|FIN|FINLAND|HELSINKI)\b/i, iso3: "FIN", flag: "🇫🇮", priority: 70 },
|
||||
|
||||
// Sweden
|
||||
{ regex: /\b(SE|SWE|SWEDEN|STOCKHOLM)\b/i, iso3: "SWE", flag: "🇸🇪", priority: 80 },
|
||||
|
||||
// Norway
|
||||
{ regex: /\b(NO|NOR|NORWAY|OSLO)\b/i, iso3: "NOR", flag: "🇳🇴", priority: 90 },
|
||||
|
||||
// Switzerland
|
||||
{ regex: /\b(CH|CHE|SWITZERLAND|ZURICH|GENEVA)\b/i, iso3: "CHE", flag: "🇨🇭", priority: 100 },
|
||||
|
||||
// Estonia / Latvia / Lithuania
|
||||
{ regex: /\b(EE|EST|ESTONIA|TALLINN)\b/i, iso3: "EST", flag: "🇪🇪", priority: 110 },
|
||||
{ regex: /\b(LV|LVA|LATVIA|RIGA)\b/i, iso3: "LVA", flag: "🇱🇻", priority: 120 },
|
||||
{ regex: /\b(LT|LTU|LITHUANIA|VILNIUS)\b/i, iso3: "LTU", flag: "🇱🇹", priority: 130 },
|
||||
|
||||
// Turkey
|
||||
{ regex: /\b(TR|TUR|TURKEY|ISTANBUL)\b/i, iso3: "TUR", flag: "🇹🇷", priority: 140 },
|
||||
|
||||
// Singapore / Japan / Korea / Hong Kong
|
||||
{ regex: /\b(SG|SGP|SINGAPORE)\b/i, iso3: "SGP", flag: "🇸🇬", priority: 200 },
|
||||
{ regex: /\b(JP|JPN|JAPAN|TOKYO|OSAKA)\b/i, iso3: "JPN", flag: "🇯🇵", priority: 210 },
|
||||
{ regex: /\b(KR|KOR|KOREA|SEOUL)\b/i, iso3: "KOR", flag: "🇰🇷", priority: 220 },
|
||||
{ regex: /\b(HK|HKG|HONG\s*KONG)\b/i, iso3: "HKG", flag: "🇭🇰", priority: 230 },
|
||||
];
|
||||
|
||||
// 5) GeoIP mapping (ISO2 -> ISO3 + flag) used only if utils.geoip.lookup(ip) returns ISO2
|
||||
const ISO2_TO_ISO3 = {
|
||||
US: { iso3: "USA", flag: "🇺🇸" },
|
||||
DE: { iso3: "DEU", flag: "🇩🇪" },
|
||||
NL: { iso3: "NLD", flag: "🇳🇱" },
|
||||
GB: { iso3: "GBR", flag: "🇬🇧" },
|
||||
FR: { iso3: "FRA", flag: "🇫🇷" },
|
||||
PL: { iso3: "POL", flag: "🇵🇱" },
|
||||
FI: { iso3: "FIN", flag: "🇫🇮" },
|
||||
SE: { iso3: "SWE", flag: "🇸🇪" },
|
||||
NO: { iso3: "NOR", flag: "🇳🇴" },
|
||||
CH: { iso3: "CHE", flag: "🇨🇭" },
|
||||
EE: { iso3: "EST", flag: "🇪🇪" },
|
||||
LV: { iso3: "LVA", flag: "🇱🇻" },
|
||||
LT: { iso3: "LTU", flag: "🇱🇹" },
|
||||
TR: { iso3: "TUR", flag: "🇹🇷" },
|
||||
SG: { iso3: "SGP", flag: "🇸🇬" },
|
||||
JP: { iso3: "JPN", flag: "🇯🇵" },
|
||||
KR: { iso3: "KOR", flag: "🇰🇷" },
|
||||
HK: { iso3: "HKG", flag: "🇭🇰" },
|
||||
};
|
||||
|
||||
// 6) Protocol icons (based on proxy.type)
|
||||
const PROTOCOL_ICONS = {
|
||||
ss: "🔒",
|
||||
ssr: "☂️",
|
||||
vmess: "🪁",
|
||||
vless: "🌌",
|
||||
trojan: "🐎",
|
||||
http: "🌐",
|
||||
socks5: "🧦",
|
||||
snell: "🐌",
|
||||
wireguard: "🐲",
|
||||
hysteria: "🤪",
|
||||
hysteria2: "⚡",
|
||||
tuic: "🚅"
|
||||
};
|
||||
|
||||
///////////////////////
|
||||
// HELPERS
|
||||
///////////////////////
|
||||
|
||||
function isIPv4(str) {
|
||||
if (typeof str !== "string") return false;
|
||||
const m = str.match(/^(\d{1,3})(\.\d{1,3}){3}$/);
|
||||
if (!m) return false;
|
||||
return str.split(".").every(oct => {
|
||||
const n = Number(oct);
|
||||
return n >= 0 && n <= 255 && String(n) === oct.replace(/^0+(\d)/, "$1"); // avoids "001" weirdness
|
||||
});
|
||||
}
|
||||
|
||||
function uniq(arr) {
|
||||
return [...new Set(arr.filter(Boolean))];
|
||||
}
|
||||
|
||||
function sanitizeBaseName(name) {
|
||||
let s = String(name || "");
|
||||
|
||||
// Remove noise patterns
|
||||
for (const re of NOISE_PATTERNS) s = s.replace(re, " ");
|
||||
|
||||
// Collapse spaces
|
||||
s = s.replace(/\s+/g, " ").trim();
|
||||
return s;
|
||||
}
|
||||
|
||||
function extractIconTagsAndStrip(name) {
|
||||
let s = String(name || "");
|
||||
const tags = [];
|
||||
|
||||
for (const r of ICON_RULES) {
|
||||
if (r.regex.test(s)) {
|
||||
tags.push(r.icon);
|
||||
s = s.replace(r.regex, " ");
|
||||
}
|
||||
}
|
||||
|
||||
for (const t of NAME_NETWORK_TAGS) {
|
||||
if (t.regex.test(s)) {
|
||||
tags.push(t.tag);
|
||||
s = s.replace(t.regex, " ");
|
||||
}
|
||||
}
|
||||
|
||||
return { stripped: s.replace(/\s+/g, " ").trim(), tags: uniq(tags) };
|
||||
}
|
||||
|
||||
function detectCountryByName(name) {
|
||||
const n = String(name || "");
|
||||
// Order by priority, then first match wins
|
||||
const sorted = COUNTRY_RULES.slice().sort((a, b) => a.priority - b.priority);
|
||||
for (const c of sorted) {
|
||||
if (c.regex.test(n)) return { iso3: c.iso3, flag: c.flag, priority: c.priority, source: "name" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function detectCountryByGeoIP(server, utils) {
|
||||
if (!isIPv4(server)) return null;
|
||||
if (!utils || !utils.geoip || typeof utils.geoip.lookup !== "function") return null;
|
||||
|
||||
try {
|
||||
const geo = utils.geoip.lookup(server);
|
||||
const iso2 = geo && (geo.country || geo.country_code || geo.iso_code);
|
||||
if (!iso2 || typeof iso2 !== "string") return null;
|
||||
|
||||
const key = iso2.toUpperCase();
|
||||
const mapped = ISO2_TO_ISO3[key];
|
||||
if (mapped) return { iso3: mapped.iso3, flag: mapped.flag, priority: 900, source: "geoip" };
|
||||
|
||||
// Unknown ISO2: keep something sane
|
||||
return { iso3: key, flag: "🏳️", priority: 950, source: "geoip" };
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function pad2(n) {
|
||||
const x = Number(n);
|
||||
return x < 10 ? `0${x}` : String(x);
|
||||
}
|
||||
|
||||
function safeStr(v) {
|
||||
return (v === undefined || v === null) ? "" : String(v);
|
||||
}
|
||||
|
||||
///////////////////////
|
||||
// OPERATOR
|
||||
///////////////////////
|
||||
|
||||
function operator(proxies, targetPlatform, utils) {
|
||||
// Sub-Store sometimes passes utils as global $utils; sometimes as 3rd arg; sometimes not at all.
|
||||
// We'll accept any of them without whining.
|
||||
const U = utils || (typeof $utils !== "undefined" ? $utils : null);
|
||||
|
||||
const buckets = Object.create(null);
|
||||
|
||||
for (const proxy of proxies) {
|
||||
const originalName = safeStr(proxy && proxy.name);
|
||||
|
||||
// 1) Extract tags (icons) from ORIGINAL name, then strip those keywords out
|
||||
const iconStage = extractIconTagsAndStrip(originalName);
|
||||
|
||||
// 2) Sanitize remaining base name (remove marketing trash, brackets, etc.)
|
||||
const cleanBase = sanitizeBaseName(iconStage.stripped);
|
||||
|
||||
// 3) Detect country (name first, then GeoIP)
|
||||
const byName = detectCountryByName(originalName);
|
||||
const byGeo = detectCountryByGeoIP(proxy && proxy.server, U);
|
||||
const country = byName || byGeo || { iso3: "UNK", flag: "🏴☠️", priority: 9999, source: "fallback" };
|
||||
|
||||
// 4) Protocol icon (based on type)
|
||||
const proto = PROTOCOL_ICONS[(proxy && proxy.type) || ""] || "🔌";
|
||||
|
||||
// 5) Network/type/port tag (from proxy fields)
|
||||
const net = safeStr(proxy && proxy.network) || "net?";
|
||||
const typ = safeStr(proxy && proxy.type) || "type?";
|
||||
const port = safeStr(proxy && proxy.port) || "port?";
|
||||
const metaTag = `▫️${net}/${typ}/${port}`;
|
||||
|
||||
// 6) Prepare bucket key
|
||||
const key = country.iso3;
|
||||
|
||||
if (!buckets[key]) {
|
||||
buckets[key] = {
|
||||
country,
|
||||
list: []
|
||||
};
|
||||
}
|
||||
|
||||
// Keep meta used for sorting and final formatting
|
||||
buckets[key].list.push({
|
||||
proxy,
|
||||
_meta: {
|
||||
cleanBase,
|
||||
iconTags: iconStage.tags,
|
||||
proto,
|
||||
metaTag
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 7) Sort buckets by priority
|
||||
const bucketKeys = Object.keys(buckets).sort((a, b) => {
|
||||
return (buckets[a].country.priority || 9999) - (buckets[b].country.priority || 9999);
|
||||
});
|
||||
|
||||
// 8) Sort inside each country bucket and rename with per-country numbering
|
||||
const result = [];
|
||||
|
||||
for (const key of bucketKeys) {
|
||||
const group = buckets[key];
|
||||
|
||||
group.list.sort((A, B) => {
|
||||
// Tags do not affect numbering: sort only by sanitized base + server:port as tie-breaker
|
||||
const an = A._meta.cleanBase.toLowerCase();
|
||||
const bn = B._meta.cleanBase.toLowerCase();
|
||||
if (an !== bn) return an.localeCompare(bn);
|
||||
|
||||
const as = `${safeStr(A.proxy.server)}:${safeStr(A.proxy.port)}`;
|
||||
const bs = `${safeStr(B.proxy.server)}:${safeStr(B.proxy.port)}`;
|
||||
return as.localeCompare(bs);
|
||||
});
|
||||
|
||||
for (let i = 0; i < group.list.length; i++) {
|
||||
const item = group.list[i];
|
||||
const p = item.proxy;
|
||||
const num = pad2(i + 1);
|
||||
|
||||
const tagStr = item._meta.iconTags.length ? ` ${item._meta.iconTags.join(" ")}` : "";
|
||||
// Final name format:
|
||||
// 🇩🇪 DEU-03 🌌 📺 ❻ ▫️ws/vless/443
|
||||
p.name = `${group.country.flag} ${group.country.iso3}-${num} ${item._meta.proto}${tagStr} ${item._meta.metaTag}`.replace(/\s+/g, " ").trim();
|
||||
|
||||
result.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,540 +0,0 @@
|
||||
/**
|
||||
* Sub-Store operator: Normalize + tag + country detect + per-country numbering
|
||||
*
|
||||
* Output format (default):
|
||||
* 🇺🇸 USA-01 🔒 📺 ❻ ▫️ws/vless/443
|
||||
*
|
||||
* Notes:
|
||||
* - Numbering is computed per-country AFTER grouping the full list.
|
||||
* - Tags (icons) do NOT affect numbering order.
|
||||
* - GeoIP is optional and only used when server is an IP and utils.geoip.lookup exists.
|
||||
*/
|
||||
|
||||
// --- OPTIONS PARSING ---
|
||||
function normalizeOptions() {
|
||||
const args = (typeof $arguments !== "undefined" && $arguments) ? $arguments : {};
|
||||
const asBool = (v, def = false) => {
|
||||
if (v === undefined || v === null || v === "") return def;
|
||||
if (typeof v === "boolean") return v;
|
||||
const s = String(v).toLowerCase().trim();
|
||||
if (["1", "true", "yes", "y", "on"].includes(s)) return true;
|
||||
if (["0", "false", "no", "n", "off"].includes(s)) return false;
|
||||
return def;
|
||||
};
|
||||
return {
|
||||
appendOriginalName: asBool(args['append-original'], false),
|
||||
};
|
||||
}
|
||||
|
||||
///////////////////////
|
||||
// CONFIG (EDIT ME)
|
||||
///////////////////////
|
||||
|
||||
const DEBUG_APPEND_ORIGINAL_NAME = false; // set true to enable debug mode (appends original name as comment)
|
||||
|
||||
// 1) Remove these patterns (marketing noise, brackets, separators, etc.)
|
||||
const NOISE_PATTERNS = [
|
||||
/\[[^\]]*]/g, // [ ... ]
|
||||
/\([^)]*\)/g, // ( ... )
|
||||
/\{[^}]*}/g, // { ... }
|
||||
/\btraffic\b/gi,
|
||||
/\bfree\b/gi,
|
||||
/\bwebsite\b/gi,
|
||||
/\bexpire\b/gi,
|
||||
/\blow\s*ping\b/gi,
|
||||
/\bai\s*studio\b/gi,
|
||||
/\bno\s*p2p\b/gi,
|
||||
/\b10\s*gbit\b/gi,
|
||||
/\bvless\b/gi,
|
||||
/\bvmess\b/gi,
|
||||
/\bssr?\b/gi,
|
||||
/\btrojan\b/gi,
|
||||
/\bhysteria2?\b/gi,
|
||||
/\btuic\b/gi,
|
||||
/[|]/g,
|
||||
/[_]+/g,
|
||||
/[-]{2,}/g
|
||||
];
|
||||
|
||||
// 2) Keyword -> icon tags (if found in original name, icon is added; the keyword is removed from base name)
|
||||
// 🇫🇿 🇺🇳 🇩🇻 🇻🇿 🇵🇷 🇦🇿 🇬🇺🇦🇷🇩
|
||||
// 🌀 - double hop
|
||||
const ICON_RULES = [
|
||||
{ regex: /TEST/gi, icon: "🧪" },
|
||||
{ regex: uWordBoundaryGroup("Low Ping"), icon: "⚡️" },
|
||||
{ regex: uWordBoundaryGroup("YT|Russia|Россия"), icon: "📺" },
|
||||
{ regex: uWordBoundaryGroup("IPv6"), icon: "🎱" },
|
||||
{ regex: uWordBoundaryGroup("Gemini|AI Studio"), icon: "🤖" },
|
||||
{ regex: uWordBoundaryGroup("Torrent|P2P|P2P-Torrents"), icon: "🧲" },
|
||||
|
||||
{ regex: uWordBoundaryGroup("local"), icon: "🚪" },
|
||||
{ regex: uWordBoundaryGroup("neighbourhood"), icon: "🫂" },
|
||||
|
||||
{ regex: uWordBoundaryGroup("🌀|Мост⚡|Мост|-Мост⚡"), icon: "🌀" },
|
||||
{ regex: uWordBoundaryGroup("Авто|Balance"), icon: "⚖️" },
|
||||
|
||||
|
||||
{ regex: uWordBoundaryGroup("xfizz|x-fizz"), icon: " 🇫" },
|
||||
{ regex: uWordBoundaryGroup("uncd|unicade"), icon: " 🇺" },
|
||||
{ regex: uWordBoundaryGroup("vzdh|vezdehod"), icon: " 🇻" },
|
||||
{ regex: uWordBoundaryGroup("dvpn|d-vpn"), icon: " 🇩" },
|
||||
{ regex: uWordBoundaryGroup("ovsc|oversecure"), icon: " 🇴" },
|
||||
{ regex: uWordBoundaryGroup("snow|snowy") , icon: " 🇸" },
|
||||
{ regex: uWordBoundaryGroup("proton"), icon: " 🇵" },
|
||||
{ regex: uWordBoundaryGroup("amnezia"), icon: " 🇦" },
|
||||
{ regex: uWordBoundaryGroup("adguard"), icon: " 🇬" },
|
||||
];
|
||||
|
||||
// 3) Optional “network” tag rules based on NAME text (not $server.network)
|
||||
// (Useful if providers shove "BGP/IPLC" into the node name)
|
||||
const NAME_NETWORK_TAGS = [
|
||||
{ regex: uWordBoundaryGroup("IPLC"), tag: "🛰️" },
|
||||
{ regex: uWordBoundaryGroup("BGP"), tag: "🧭" },
|
||||
{ regex: uWordBoundaryGroup("Anycast"), tag: "🌍" }
|
||||
];
|
||||
|
||||
// 4) Country detection rules by NAME (regex). First match wins (priority = lower is earlier)
|
||||
const COUNTRY_RULES = [
|
||||
{ regex: uWordBoundaryGroup("(Аргентина|Argentina|AR|ARG|ARGENTINA|BUENOS\s*AIRES)"), iso3: "ARG", flag: "🇦🇷", priority: 100 }, // Argentina
|
||||
{ regex: uWordBoundaryGroup("(Australia|AU|AUS|AUSTRALIA|SYDNEY)"), iso3: "AUS", flag: "🇦🇺", priority: 110 }, // Australia
|
||||
{ regex: uWordBoundaryGroup("(Austria|AT|AUT|AUSTRIA|VIENNA)"), iso3: "AUT", flag: "🇦🇹", priority: 120 }, // Austria
|
||||
{ regex: uWordBoundaryGroup("(Беларусь|Белоруссия|BELARUS)"), iso3: "BLR", flag: "🇧🇾", priority: 130 }, // Belarus
|
||||
{ regex: uWordBoundaryGroup("(Brazil|BR|BRA|BRAZIL|SAO\s*PAULO)"), iso3: "BRA", flag: "🇧🇷", priority: 140 }, // Brazil
|
||||
{ regex: uWordBoundaryGroup("(Bulgaria|BG|BGR|BULGARIA|SOFIA)"), iso3: "BGR", flag: "🇧🇬", priority: 150 }, // Bulgaria
|
||||
{ regex: uWordBoundaryGroup("(Canada|CA|CAN|CANADA|TORONTO)"), iso3: "CAN", flag: "🇨🇦", priority: 160 }, // Canada
|
||||
{ regex: uWordBoundaryGroup("(КИТАЙ|China)"), iso3: "CHN", flag: "🇨🇳", priority: 170 }, // China
|
||||
{ regex: uWordBoundaryGroup("(Czech\s*Republic|CZ|CZE|CZECH|PRAGUE)"), iso3: "CZE", flag: "🇨🇿", priority: 180 }, // Czech Republic
|
||||
{ regex: uWordBoundaryGroup("(Denmark|DK|DNK|DENMARK|COPENHAGEN)"), iso3: "DNK", flag: "🇩🇰", priority: 190 }, // Denmark
|
||||
{ regex: uWordBoundaryGroup("(Egypt|EG|EGY|EGYPT|CAIRO)"), iso3: "EGY", flag: "🇪🇬", priority: 200 }, // Egypt
|
||||
{ regex: uWordBoundaryGroup("(Эстония|EE|EST|ESTONIA|TALLINN)"), iso3: "EST", flag: "🇪🇪", priority: 210 }, // Estonia
|
||||
{ regex: uWordBoundaryGroup("(Финляндия|FI|FIN|FINLAND|HELSINKI)"), iso3: "FIN", flag: "🇫🇮", priority: 220 }, // Finland
|
||||
{ regex: uWordBoundaryGroup("(Франция|FR|FRA|FRANCE|PARIS|MARSEILLE)"), iso3: "FRA", flag: "🇫🇷", priority: 230 }, // France
|
||||
{ regex: uWordBoundaryGroup("(Georgia|GE|GEO|GEORGIA|TBILISI)"), iso3: "GEO", flag: "🇬🇪", priority: 240 }, // Georgia
|
||||
{ regex: uWordBoundaryGroup("(Германия|DE|DEU|GER(MANY)?|FRANKFURT|BERLIN|MUNICH)"), iso3: "DEU", flag: "🇩🇪", priority: 250 }, // Germany
|
||||
{ regex: uWordBoundaryGroup("(Гонконг|HK|HKG|HONG\s*KONG)"), iso3: "HKG", flag: "🇭🇰", priority: 260 }, // Hong Kong
|
||||
{ regex: uWordBoundaryGroup("(India|IN|IND|INDIA|MUMBAI)"), iso3: "IND", flag: "🇮🇳", priority: 270 }, // India
|
||||
{ regex: uWordBoundaryGroup("(Ireland|IE|IRL|IRELAND|DUBLIN)"), iso3: "IRL", flag: "🇮🇪", priority: 280 }, // Ireland
|
||||
{ regex: uWordBoundaryGroup("(Israel|IL|ISR|ISRAEL|TEL\s*AVIV)"), iso3: "ISR", flag: "🇮🇱", priority: 290 }, // Israel
|
||||
{ regex: uWordBoundaryGroup("(Italy|IT|ITA|ITALY|ROME)"), iso3: "ITA", flag: "🇮🇹", priority: 300 }, // Italy
|
||||
{ regex: uWordBoundaryGroup("(Япония|JP|JPN|JAPAN|TOKYO|OSAKA)"), iso3: "JPN", flag: "🇯🇵", priority: 310 }, // Japan
|
||||
{ regex: uWordBoundaryGroup("(Kazakhstan|KZ|KAZ|KAZAKHSTAN|ALMATY)"), iso3: "KAZ", flag: "🇰🇿", priority: 320 }, // Kazakhstan
|
||||
{ regex: uWordBoundaryGroup("(Латвия|LV|LVA|LATVIA|RIGA)"), iso3: "LVA", flag: "🇱🇻", priority: 330 }, // Latvia
|
||||
{ regex: uWordBoundaryGroup("(Литва|LT|LTU|LITHUANIA|VILNIUS)"), iso3: "LTU", flag: "🇱🇹", priority: 340 }, // Lithuania
|
||||
{ regex: uWordBoundaryGroup("(Malaysia|MY|MYS|MALAYSIA|KUALA\s*LUMPUR)"), iso3: "MYS", flag: "🇲🇾", priority: 350 }, // Malaysia
|
||||
{ regex: uWordBoundaryGroup("(Moldova|MD|MDA|MOLDOVA|CHISINAU)"), iso3: "MDA", flag: "🇲🇩", priority: 360 }, // Moldova
|
||||
{ regex: uWordBoundaryGroup("(Нидерланды|NL|NLD|NETHERLANDS|HOLLAND|AMSTERDAM)"), iso3: "NLD", flag: "🇳🇱", priority: 370 }, // Netherlands
|
||||
{ regex: uWordBoundaryGroup("(Nigeria|NG|NGA|NIGERIA|LAGOS)"), iso3: "NGA", flag: "🇳🇬", priority: 380 }, // Nigeria
|
||||
{ regex: uWordBoundaryGroup("(Норвегия|NO|NOR|NORWAY|OSLO)"), iso3: "NOR", flag: "🇳🇴", priority: 390 }, // Norway
|
||||
{ regex: uWordBoundaryGroup("(Philippines|PH|PHL|PHILIPPINES|MANILA)"), iso3: "PHL", flag: "🇵🇭", priority: 400 }, // Philippines
|
||||
{ regex: uWordBoundaryGroup("(Польша|PL|POL|POLAND|WARSAW|WARSZAWA)"), iso3: "POL", flag: "🇵🇱", priority: 410 }, // Poland
|
||||
{ regex: uWordBoundaryGroup("(Portugal|PT|PRT|PORTUGAL|LISBON)"), iso3: "PRT", flag: "🇵🇹", priority: 420 }, // Portugal
|
||||
{ regex: uWordBoundaryGroup("(Romania|RO|ROU|ROMANIA|BUCHAREST)"), iso3: "ROU", flag: "🇷🇴", priority: 430 }, // Romania
|
||||
{ regex: uWordBoundaryGroup("(Russia|RU|RUS|RUSSIA|MOSCOW)"), iso3: "RUS", flag: "🇷🇺", priority: 440 }, // Russia
|
||||
{ regex: uWordBoundaryGroup("(Сингапур|SG|SGP|SINGAPORE)"), iso3: "SGP", flag: "🇸🇬", priority: 200 }, // Singapore
|
||||
{ regex: uWordBoundaryGroup("(South Korea|Корея|KR|KOR|KOREA|SEOUL)"), iso3: "KOR", flag: "🇰🇷", priority: 450 }, // South Korea
|
||||
{ regex: uWordBoundaryGroup("(Spain|ES|ESP|SPAIN|MADRID)"), iso3: "ESP", flag: "🇪🇸", priority: 460 }, // Spain
|
||||
{ regex: uWordBoundaryGroup("(Швеция|SE|SWE|SWEDEN|STOCKHOLM)"), iso3: "SWE", flag: "🇸🇪", priority: 470 }, // Sweden
|
||||
{ regex: uWordBoundaryGroup("(Швейцария|CH|CHE|SWITZERLAND|Switzerl)"), iso3: "CHE", flag: "🇨🇭", priority: 480 }, // Switzerland
|
||||
{ regex: uWordBoundaryGroup("(Taiwan|TW|TWN|TAIWAN|TAIPEI)"), iso3: "TWN", flag: "🇹🇼", priority: 490 }, // Taiwan
|
||||
{ regex: uWordBoundaryGroup("(Thailand|TH|THA|THAILAND|BANGKOK)"), iso3: "THA", flag: "🇹🇭", priority: 500 }, // Thailand
|
||||
{ regex: uWordBoundaryGroup("(Турция|TR|TUR|TURKEY|ISTANBUL)"), iso3: "TUR", flag: "🇹🇷", priority: 510 }, // Turkey
|
||||
{ regex: uWordBoundaryGroup("(UAE|United\s*Arab\s*Emirates|AE|ARE|DUBAI)"), iso3: "ARE", flag: "🇦🇪", priority: 520 }, // UAE
|
||||
{ regex: uWordBoundaryGroup("(Великобритания|Англия|England|UK|GB|GBR|UNITED\s*KINGDOM)"), iso3: "GBR", flag: "🇬🇧", priority: 530 }, // UK
|
||||
{ regex: uWordBoundaryGroup("(США|USA|US|UNITED\s*STATES|AMERICA|NEW\s*YORK|NYC)"), iso3: "USA", flag: "🇺🇸", priority: 540 }, // USA
|
||||
{ regex: uWordBoundaryGroup("(Vietnam|VN|VNM|VIETNAM|HANOI)"), iso3: "VNM", flag: "🇻🇳", priority: 500 } // Vietnam
|
||||
];
|
||||
|
||||
// 5) GeoIP mapping (ISO2 -> ISO3 + flag) used only if utils.geoip.lookup(ip) returns ISO2
|
||||
const ISO2_TO_ISO3 = {
|
||||
US: { iso3: "USA", flag: "🇺🇸" },
|
||||
DE: { iso3: "DEU", flag: "🇩🇪" },
|
||||
NL: { iso3: "NLD", flag: "🇳🇱" },
|
||||
GB: { iso3: "GBR", flag: "🇬🇧" },
|
||||
FR: { iso3: "FRA", flag: "🇫🇷" },
|
||||
PL: { iso3: "POL", flag: "🇵🇱" },
|
||||
FI: { iso3: "FIN", flag: "🇫🇮" },
|
||||
SE: { iso3: "SWE", flag: "🇸🇪" },
|
||||
NO: { iso3: "NOR", flag: "🇳🇴" },
|
||||
CH: { iso3: "CHE", flag: "🇨🇭" },
|
||||
EE: { iso3: "EST", flag: "🇪🇪" },
|
||||
LV: { iso3: "LVA", flag: "🇱🇻" },
|
||||
LT: { iso3: "LTU", flag: "🇱🇹" },
|
||||
TR: { iso3: "TUR", flag: "🇹🇷" },
|
||||
SG: { iso3: "SGP", flag: "🇸🇬" },
|
||||
JP: { iso3: "JPN", flag: "🇯🇵" },
|
||||
KR: { iso3: "KOR", flag: "🇰🇷" },
|
||||
HK: { iso3: "HKG", flag: "🇭🇰" },
|
||||
};
|
||||
|
||||
// 6) Protocol icons (based on proxy.type)
|
||||
const PROTOCOL_ICONS = {
|
||||
ss: "",
|
||||
ssr: "",
|
||||
vmess: "",
|
||||
vless: "",
|
||||
trojan: "",
|
||||
http: "",
|
||||
socks5: "",
|
||||
snell: "",
|
||||
wireguard: "",
|
||||
hysteria: "",
|
||||
hysteria2: "",
|
||||
tuic: ""
|
||||
};
|
||||
|
||||
const STANDARD_PORTS_BY_TYPE = {
|
||||
wireguard: new Set(["51820"]),
|
||||
vless: new Set(["443"]),
|
||||
trojan: new Set(["443"]),
|
||||
ss: new Set(["443"]),
|
||||
};
|
||||
|
||||
const PROTOCOL_ICON_DEFAULT = ""; // fallback icon if type is unknown
|
||||
|
||||
|
||||
const METATAG_RULES = {
|
||||
// Keys are "network/type" OR "/type" (network-agnostic) OR "network/" (type-agnostic)
|
||||
// Matching priority: exact "network/type" -> "/type" -> "network/" -> default
|
||||
// 🅶🆃 🆃🆂 🆃🆅 🆆🆅 🆇🆅 🆆🅶 🅽🅸
|
||||
pairMap: {
|
||||
"grpc/trojan": "🅶🆃",
|
||||
"tcp/trojan": "🆃🆃",
|
||||
"tcp/ss": "🆃🆂",
|
||||
"tcp/vless": "🆃🆅",
|
||||
"ws/vless": "🆆🆅",
|
||||
"xhttp/vless": "🆇🆅",
|
||||
"grpc/vless": "🅶🆅",
|
||||
|
||||
"/wireguard": "🆆🅶",
|
||||
"/naive": "🅽🅸",
|
||||
},
|
||||
|
||||
defaultPair: "▫️", // fallback if nothing matches
|
||||
includeFallbackText: false, // if true, append "(net/type)" when defaultPair is used
|
||||
};
|
||||
|
||||
// Port formatting: superscript digits with left padding to 4 chars
|
||||
// 𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗
|
||||
const PORT_FORMAT = {
|
||||
padLeftTo: 3,
|
||||
padChar: "0",
|
||||
fancy: {
|
||||
"0": "𝟎", "1": "𝟏", "2": "𝟐", "3": "𝟑", "4": "𝟒", "5": "𝟓", "6": "𝟔", "7": "𝟕", "8": "𝟖", "9": "𝟗",
|
||||
},
|
||||
};
|
||||
|
||||
///////////////////////
|
||||
// HELPERS
|
||||
///////////////////////
|
||||
|
||||
function normalizeToken(s) {
|
||||
return String(s || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function uWordBoundaryGroup(inner) {
|
||||
// Match if surrounded by non-letter/non-digit (Unicode-aware)
|
||||
// We don't use lookbehind for max compatibility.
|
||||
return new RegExp(`(?:^|[^\\p{L}\\p{N}])(?:${inner})(?=$|[^\\p{L}\\p{N}])`, "iu");
|
||||
}
|
||||
|
||||
function portToFancy(port, type) {
|
||||
let p = String(port ?? "").trim();
|
||||
p = p.replace(/[^\d]/g, "");
|
||||
if (!p) return "";
|
||||
|
||||
if (STANDARD_PORTS_BY_TYPE[type]?.has(p)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// left pad to fixed width
|
||||
//if (PORT_FORMAT.padLeftTo && p.length < PORT_FORMAT.padLeftTo) {
|
||||
// p = p.padStart(PORT_FORMAT.padLeftTo, PORT_FORMAT.padChar);
|
||||
//}
|
||||
|
||||
// map digits
|
||||
//let out = "";
|
||||
//for (const ch of p) out += PORT_FORMAT.fancy[ch] ?? ch;
|
||||
out = "✳️"
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildMetaTag(proxy) {
|
||||
const net = safeStr(proxy && proxy.network) || "";
|
||||
const typ = safeStr(proxy && proxy.type) || "";
|
||||
const port = safeStr(proxy && proxy.port);
|
||||
|
||||
const { icon, matched } = metaPairIcon(net, typ);
|
||||
const portSup = portToFancy(port, typ);
|
||||
|
||||
if (icon === METATAG_RULES.defaultPair && METATAG_RULES.includeFallbackText) {
|
||||
return `${icon}(${normalizeToken(net)}/${normalizeToken(typ)})`;
|
||||
}
|
||||
|
||||
return `${icon}`;
|
||||
}
|
||||
|
||||
function metaPairIcon(network, type) {
|
||||
const net = normalizeToken(network);
|
||||
const typ = normalizeToken(type);
|
||||
|
||||
const exact = `${net}/${typ}`;
|
||||
const typeOnly = `/${typ}`;
|
||||
const netOnly = `${net}/`;
|
||||
|
||||
const m = METATAG_RULES.pairMap;
|
||||
|
||||
if (m[exact]) return { icon: m[exact], matched: exact };
|
||||
if (m[typeOnly]) return { icon: m[typeOnly], matched: typeOnly };
|
||||
if (m[netOnly]) return { icon: m[netOnly], matched: netOnly };
|
||||
|
||||
return { icon: METATAG_RULES.defaultPair, matched: null };
|
||||
}
|
||||
|
||||
function isIPv4(str) {
|
||||
if (typeof str !== "string") return false;
|
||||
const m = str.match(/^(\d{1,3})(\.\d{1,3}){3}$/);
|
||||
if (!m) return false;
|
||||
return str.split(".").every(oct => {
|
||||
const n = Number(oct);
|
||||
return n >= 0 && n <= 255 && String(n) === oct.replace(/^0+(\d)/, "$1"); // avoids "001" weirdness
|
||||
});
|
||||
}
|
||||
|
||||
function uniq(arr) {
|
||||
return [...new Set(arr.filter(Boolean))];
|
||||
}
|
||||
|
||||
function sanitizeBaseName(name) {
|
||||
let s = String(name || "");
|
||||
|
||||
// Remove noise patterns
|
||||
for (const re of NOISE_PATTERNS) s = s.replace(re, " ");
|
||||
|
||||
// Collapse spaces
|
||||
s = s.replace(/\s+/g, " ").trim();
|
||||
return s;
|
||||
}
|
||||
|
||||
function extractIconTagsAndStrip(name, proxy) {
|
||||
let s = String(name || "");
|
||||
const tags = [];
|
||||
|
||||
const typ = safeStr(proxy && proxy.type) || "";
|
||||
const port = safeStr(proxy && proxy.port);
|
||||
tags.push(portToFancy(port, typ))
|
||||
|
||||
for (const r of ICON_RULES) {
|
||||
if (r.regex.test(s)) {
|
||||
tags.push(r.icon);
|
||||
s = s.replace(r.regex, " ");
|
||||
}
|
||||
}
|
||||
|
||||
for (const t of NAME_NETWORK_TAGS) {
|
||||
if (t.regex.test(s)) {
|
||||
tags.push(t.tag);
|
||||
s = s.replace(t.regex, " ");
|
||||
}
|
||||
}
|
||||
|
||||
return { stripped: s.replace(/\s+/g, " ").trim(), tags: uniq(tags) };
|
||||
}
|
||||
|
||||
function detectCountryByName(name) {
|
||||
const n = String(name || "");
|
||||
// Order by priority, then first match wins
|
||||
|
||||
// Fast path: flag emoji
|
||||
if (n.includes("🇦🇪")) return { iso3: "ARE", flag: "🇦🇪", priority: 1, source: "flag" };
|
||||
if (n.includes("🇦🇱")) return { iso3: "ALB", flag: "🇦🇱", priority: 2, source: "flag" };
|
||||
if (n.includes("🇦🇲")) return { iso3: "ARM", flag: "🇦🇲", priority: 2, source: "flag" };
|
||||
if (n.includes("🇦🇷")) return { iso3: "ARG", flag: "🇦🇷", priority: 2, source: "flag" };
|
||||
if (n.includes("🇦🇹")) return { iso3: "AUT", flag: "🇦🇹", priority: 3, source: "flag" };
|
||||
if (n.includes("🇦🇺")) return { iso3: "AUS", flag: "🇦🇺", priority: 4, source: "flag" };
|
||||
if (n.includes("🇧🇪")) return { iso3: "BEL", flag: "🇧🇪", priority: 5, source: "flag" };
|
||||
if (n.includes("🇧🇬")) return { iso3: "BGR", flag: "🇧🇬", priority: 5, source: "flag" };
|
||||
if (n.includes("🇧🇾")) return { iso3: "BLR", flag: "🇧🇾", priority: 6, source: "flag" };
|
||||
if (n.includes("🇧🇷")) return { iso3: "BRA", flag: "🇧🇷", priority: 7, source: "flag" };
|
||||
if (n.includes("🇨🇦")) return { iso3: "CAN", flag: "🇨🇦", priority: 8, source: "flag" };
|
||||
if (n.includes("🇨🇭")) return { iso3: "CHE", flag: "🇨🇭", priority: 9, source: "flag" };
|
||||
if (n.includes("🇨🇳")) return { iso3: "CHN", flag: "🇨🇳", priority: 10, source: "flag" };
|
||||
if (n.includes("🇨🇾")) return { iso3: "CYP", flag: "🇨🇾", priority: 11, source: "flag" };
|
||||
if (n.includes("🇨🇿")) return { iso3: "CZE", flag: "🇨🇿", priority: 11, source: "flag" };
|
||||
if (n.includes("🇩🇪")) return { iso3: "DEU", flag: "🇩🇪", priority: 12, source: "flag" };
|
||||
if (n.includes("🇩🇰")) return { iso3: "DNK", flag: "🇩🇰", priority: 13, source: "flag" };
|
||||
if (n.includes("🇪🇪")) return { iso3: "EST", flag: "🇪🇪", priority: 14, source: "flag" };
|
||||
if (n.includes("🇪🇬")) return { iso3: "EGY", flag: "🇪🇬", priority: 15, source: "flag" };
|
||||
if (n.includes("🇪🇸")) return { iso3: "ESP", flag: "🇪🇸", priority: 16, source: "flag" };
|
||||
if (n.includes("🇫🇮")) return { iso3: "FIN", flag: "🇫🇮", priority: 17, source: "flag" };
|
||||
if (n.includes("🇫🇷")) return { iso3: "FRA", flag: "🇫🇷", priority: 18, source: "flag" };
|
||||
if (n.includes("🇬🇧")) return { iso3: "GBR", flag: "🇬🇧", priority: 19, source: "flag" };
|
||||
if (n.includes("🇬🇪")) return { iso3: "GEO", flag: "🇬🇪", priority: 20, source: "flag" };
|
||||
if (n.includes("🇬🇷")) return { iso3: "GRC", flag: "🇬🇷", priority: 2, source: "flag" };
|
||||
if (n.includes("🇭🇰")) return { iso3: "HKG", flag: "🇭🇰", priority: 21, source: "flag" };
|
||||
if (n.includes("🇭🇷")) return { iso3: "HRV", flag: "🇭🇷", priority: 21, source: "flag" };
|
||||
if (n.includes("🇭🇺")) return { iso3: "HUN", flag: "🇭🇺", priority: 1, source: "flag" };
|
||||
if (n.includes("🇮🇪")) return { iso3: "IRL", flag: "🇮🇪", priority: 22, source: "flag" };
|
||||
if (n.includes("🇮🇱")) return { iso3: "ISR", flag: "🇮🇱", priority: 23, source: "flag" };
|
||||
if (n.includes("🇮🇳")) return { iso3: "IND", flag: "🇮🇳", priority: 24, source: "flag" };
|
||||
if (n.includes("🇮🇹")) return { iso3: "ITA", flag: "🇮🇹", priority: 25, source: "flag" };
|
||||
if (n.includes("🇮🇸")) return { iso3: "ISL", flag: "🇮🇸", priority: 1, source: "flag" };
|
||||
if (n.includes("🇯🇵")) return { iso3: "JPN", flag: "🇯🇵", priority: 26, source: "flag" };
|
||||
if (n.includes("🇰🇷")) return { iso3: "KOR", flag: "🇰🇷", priority: 27, source: "flag" };
|
||||
if (n.includes("🇰🇿")) return { iso3: "KAZ", flag: "🇰🇿", priority: 28, source: "flag" };
|
||||
if (n.includes("🇱🇹")) return { iso3: "LTU", flag: "🇱🇹", priority: 29, source: "flag" };
|
||||
if (n.includes("🇱🇻")) return { iso3: "LVA", flag: "🇱🇻", priority: 30, source: "flag" };
|
||||
if (n.includes("🇲🇩")) return { iso3: "MDA", flag: "🇲🇩", priority: 31, source: "flag" };
|
||||
if (n.includes("🇲🇰")) return { iso3: "MKD", flag: "🇲🇰", priority: 2, source: "flag" };
|
||||
if (n.includes("🇲🇾")) return { iso3: "MYS", flag: "🇲🇾", priority: 32, source: "flag" };
|
||||
if (n.includes("🇳🇬")) return { iso3: "NGA", flag: "🇳🇬", priority: 33, source: "flag" };
|
||||
if (n.includes("🇳🇱")) return { iso3: "NLD", flag: "🇳🇱", priority: 34, source: "flag" };
|
||||
if (n.includes("🇳🇴")) return { iso3: "NOR", flag: "🇳🇴", priority: 35, source: "flag" };
|
||||
if (n.includes("🇵🇭")) return { iso3: "PHL", flag: "🇵🇭", priority: 36, source: "flag" };
|
||||
if (n.includes("🇵🇱")) return { iso3: "POL", flag: "🇵🇱", priority: 37, source: "flag" };
|
||||
if (n.includes("🇵🇹")) return { iso3: "PRT", flag: "🇵🇹", priority: 38, source: "flag" };
|
||||
if (n.includes("🇷🇴")) return { iso3: "ROU", flag: "🇷🇴", priority: 39, source: "flag" };
|
||||
if (n.includes("🇷🇺")) return { iso3: "RUS", flag: "🇷🇺", priority: 40, source: "flag" };
|
||||
if (n.includes("🇸🇪")) return { iso3: "SWE", flag: "🇸🇪", priority: 41, source: "flag" };
|
||||
if (n.includes("🇸🇬")) return { iso3: "SGP", flag: "🇸🇬", priority: 42, source: "flag" };
|
||||
if (n.includes("🇹🇭")) return { iso3: "THA", flag: "🇹🇭", priority: 43, source: "flag" };
|
||||
if (n.includes("🇹🇷")) return { iso3: "TUR", flag: "🇹🇷", priority: 44, source: "flag" };
|
||||
if (n.includes("🇹🇼")) return { iso3: "TWN", flag: "🇹🇼", priority: 45, source: "flag" };
|
||||
if (n.includes("🇺🇦")) return { iso3: "UKR", flag: "🇺🇦", priority: 2, source: "flag" };
|
||||
if (n.includes("🇺🇸")) return { iso3: "USA", flag: "🇺🇸", priority: 46, source: "flag" };
|
||||
if (n.includes("🇻🇳")) return { iso3: "VNM", flag: "🇻🇳", priority: 47, source: "flag" };
|
||||
|
||||
const sorted = COUNTRY_RULES.slice().sort((a, b) => a.priority - b.priority);
|
||||
for (const c of sorted) {
|
||||
if (c.regex.test(n)) return { iso3: c.iso3, flag: c.flag, priority: c.priority, source: "name" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function detectCountryByGeoIP(server, utils) {
|
||||
if (!isIPv4(server)) return null;
|
||||
if (!utils || !utils.geoip || typeof utils.geoip.lookup !== "function") return null;
|
||||
|
||||
try {
|
||||
const geo = utils.geoip.lookup(server);
|
||||
const iso2 = geo && (geo.country || geo.country_code || geo.iso_code);
|
||||
if (!iso2 || typeof iso2 !== "string") return null;
|
||||
|
||||
const key = iso2.toUpperCase();
|
||||
const mapped = ISO2_TO_ISO3[key];
|
||||
if (mapped) return { iso3: mapped.iso3, flag: mapped.flag, priority: 900, source: "geoip" };
|
||||
|
||||
// Unknown ISO2: keep something sane
|
||||
return { iso3: key, flag: "🏳️", priority: 950, source: "geoip" };
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function pad2(n) {
|
||||
const x = Number(n);
|
||||
return x < 10 ? `0${x}` : String(x);
|
||||
}
|
||||
|
||||
function safeStr(v) {
|
||||
return (v === undefined || v === null) ? "" : String(v);
|
||||
}
|
||||
|
||||
///////////////////////
|
||||
// OPERATOR
|
||||
///////////////////////
|
||||
|
||||
function operator(proxies, targetPlatform, utils) {
|
||||
// Sub-Store sometimes passes utils as global $utils; sometimes as 3rd arg; sometimes not at all.
|
||||
// We'll accept any of them without whining.
|
||||
const opts = normalizeOptions();
|
||||
|
||||
const U = utils || (typeof $utils !== "undefined" ? $utils : null);
|
||||
|
||||
const buckets = Object.create(null);
|
||||
|
||||
for (const proxy of proxies) {
|
||||
const originalName = safeStr(proxy && proxy.name);
|
||||
|
||||
// 1) Extract tags (icons) from ORIGINAL name, then strip those keywords out
|
||||
const iconStage = extractIconTagsAndStrip(originalName, proxy);
|
||||
|
||||
// 2) Sanitize remaining base name (remove marketing trash, brackets, etc.)
|
||||
const cleanBase = sanitizeBaseName(iconStage.stripped);
|
||||
|
||||
// 3) Detect country (name first, then GeoIP)
|
||||
const byName = detectCountryByName(originalName);
|
||||
const byGeo = detectCountryByGeoIP(proxy && proxy.server, U);
|
||||
const country = byName || byGeo || { iso3: "UNK", flag: "🏴☠️", priority: 9999, source: "fallback" };
|
||||
|
||||
// 4) Protocol icon (based on type)
|
||||
const proto = PROTOCOL_ICONS[(proxy && proxy.type) || ""] || PROTOCOL_ICON_DEFAULT;
|
||||
|
||||
// 5) Network/type/port tag (from proxy fields)
|
||||
const metaTag = buildMetaTag(proxy);
|
||||
|
||||
// 6) Prepare bucket key
|
||||
const key = country.iso3;
|
||||
|
||||
if (!buckets[key]) {
|
||||
buckets[key] = {
|
||||
country,
|
||||
list: []
|
||||
};
|
||||
}
|
||||
|
||||
// Keep meta used for sorting and final formatting
|
||||
buckets[key].list.push({
|
||||
proxy,
|
||||
_meta: {
|
||||
originalName,
|
||||
cleanBase,
|
||||
iconTags: iconStage.tags,
|
||||
proto,
|
||||
metaTag
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 7) Sort buckets by priority
|
||||
const bucketKeys = Object.keys(buckets).sort((a, b) => {
|
||||
return (buckets[a].country.priority || 9999) - (buckets[b].country.priority || 9999);
|
||||
});
|
||||
|
||||
// 8) Sort inside each country bucket and rename with per-country numbering
|
||||
const result = [];
|
||||
|
||||
for (const key of bucketKeys) {
|
||||
const group = buckets[key];
|
||||
|
||||
group.list.sort((A, B) => {
|
||||
// Tags do not affect numbering: sort only by sanitized base + server:port as tie-breaker
|
||||
const an = A._meta.cleanBase.toLowerCase();
|
||||
const bn = B._meta.cleanBase.toLowerCase();
|
||||
if (an !== bn) return an.localeCompare(bn);
|
||||
|
||||
const as = `${safeStr(A.proxy.server)}:${safeStr(A.proxy.port)}`;
|
||||
const bs = `${safeStr(B.proxy.server)}:${safeStr(B.proxy.port)}`;
|
||||
return as.localeCompare(bs);
|
||||
});
|
||||
|
||||
for (let i = 0; i < group.list.length; i++) {
|
||||
const item = group.list[i];
|
||||
const p = item.proxy;
|
||||
const num = pad2(i + 1);
|
||||
|
||||
const debugSuffix = opts.appendOriginalName
|
||||
? ` ⟦${item._meta.originalName}⟧`
|
||||
: "";
|
||||
|
||||
const tagStr = item._meta.iconTags.length ? ` ${item._meta.iconTags.join("")}` : "";
|
||||
|
||||
p.name = `${group.country.flag}${item._meta.metaTag} ${group.country.iso3}-${num} ${item._meta.proto}${tagStr} ${debugSuffix}`
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
|
||||
result.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,516 +0,0 @@
|
||||
/**
|
||||
* Sub-Store operator: Normalize + tag + country detect + per-country numbering
|
||||
*
|
||||
* Output format (default):
|
||||
* 🇺🇸 USA-01 🔒 📺 ❻ ▫️ws/vless/443
|
||||
*
|
||||
* Notes:
|
||||
* - Numbering is computed per-country AFTER grouping the full list.
|
||||
* - Tags (icons) do NOT affect numbering order.
|
||||
* - GeoIP is optional and only used when server is an IP and utils.geoip.lookup exists.
|
||||
*/
|
||||
|
||||
///////////////////////
|
||||
// CONFIG (EDIT ME)
|
||||
///////////////////////
|
||||
|
||||
const DEBUG_APPEND_ORIGINAL_NAME = false; // set true to enable debug mode (appends original name as comment)
|
||||
|
||||
// 1) Remove these patterns (marketing noise, brackets, separators, etc.)
|
||||
const NOISE_PATTERNS = [
|
||||
/\[[^\]]*]/g, // [ ... ]
|
||||
/\([^)]*\)/g, // ( ... )
|
||||
/\{[^}]*}/g, // { ... }
|
||||
/\btraffic\b/gi,
|
||||
/\bfree\b/gi,
|
||||
/\bwebsite\b/gi,
|
||||
/\bexpire\b/gi,
|
||||
/\blow\s*ping\b/gi,
|
||||
/\bai\s*studio\b/gi,
|
||||
/\bno\s*p2p\b/gi,
|
||||
/\b10\s*gbit\b/gi,
|
||||
/\bvless\b/gi,
|
||||
/\bvmess\b/gi,
|
||||
/\bssr?\b/gi,
|
||||
/\btrojan\b/gi,
|
||||
/\bhysteria2?\b/gi,
|
||||
/\btuic\b/gi,
|
||||
/[|]/g,
|
||||
/[_]+/g,
|
||||
/[-]{2,}/g
|
||||
];
|
||||
|
||||
// 2) Keyword -> icon tags (if found in original name, icon is added; the keyword is removed from base name)
|
||||
// 🇫🇿 🇺🇳 🇩🇻 🇻🇿 🇵🇷 🇦🇿 🇬🇺🇦🇷🇩
|
||||
const ICON_RULES = [
|
||||
{ regex: /TEST/gi, icon: "🧪" },
|
||||
{ regex: uWordBoundaryGroup("Low Ping|⚡|Быстрое"), icon: "⚡️" },
|
||||
{ regex: uWordBoundaryGroup("10 Gbit|20 Гбит/c"), icon: "🛤️" },
|
||||
{ regex: uWordBoundaryGroup("YT|YouTube|Russia|Россия|Saint Petersburg|Moscow"), icon: "📺" },
|
||||
{ regex: uWordBoundaryGroup("IPv6"), icon: "🎱" },
|
||||
{ regex: uWordBoundaryGroup("Gemini|AI Studio"), icon: "🤖" },
|
||||
{ regex: uWordBoundaryGroup("Torrent|P2P|P2P-Torrents"), icon: "🧲" },
|
||||
{ regex: uWordBoundaryGroup("Мегафон|MTS|Yota|T2|Все операторы|Обход"), icon: "📃" },
|
||||
{ regex: uWordBoundaryGroup("Мост"), icon: "🌉" },
|
||||
{ regex: uWordBoundaryGroup("Сильные блокировки"), icon: "🚧" },
|
||||
|
||||
{ regex: uWordBoundaryGroup("local"), icon: "🚪" },
|
||||
{ regex: uWordBoundaryGroup("neighbourhood"), icon: "🫂" },
|
||||
|
||||
{ regex: uWordBoundaryGroup("xfizz|x-fizz"), icon: "🇫" },
|
||||
{ regex: uWordBoundaryGroup("unicade|uncd"), icon: "🇺" },
|
||||
{ regex: uWordBoundaryGroup("vzdh|vezdehod"), icon: "🇻" },
|
||||
{ regex: uWordBoundaryGroup("dvpn|d-vpn"), icon: "🇩" },
|
||||
{ regex: uWordBoundaryGroup("proton"), icon: "🇵" },
|
||||
{ regex: uWordBoundaryGroup("amnezia"), icon: "🇦" },
|
||||
{ regex: uWordBoundaryGroup("adguard"), icon: "🇬" },
|
||||
{ regex: uWordBoundaryGroup("snow"), icon: "🇸" },
|
||||
{ regex: uWordBoundaryGroup("ovsc"), icon: "🇴" },
|
||||
];
|
||||
|
||||
// 3) Optional “network” tag rules based on NAME text (not $server.network)
|
||||
// (Useful if providers shove "BGP/IPLC" into the node name)
|
||||
const NAME_NETWORK_TAGS = [
|
||||
{ regex: uWordBoundaryGroup("IPLC"), tag: "🛰️" },
|
||||
{ regex: uWordBoundaryGroup("BGP"), tag: "🧭" },
|
||||
{ regex: uWordBoundaryGroup("Anycast"), tag: "🌍" }
|
||||
];
|
||||
|
||||
// 4) Country detection rules by NAME (regex). First match wins (priority = lower is earlier)
|
||||
const COUNTRY_RULES = [
|
||||
{ regex: uWordBoundaryGroup("(Аргентина|Argentina|AR|ARG|ARGENTINA|BUENOS\s*AIRES)"), iso3: "ARG", flag: "🇦🇷", priority: 100 }, // Argentina
|
||||
{ regex: uWordBoundaryGroup("(Australia|AU|AUS|AUSTRALIA|SYDNEY)"), iso3: "AUS", flag: "🇦🇺", priority: 110 }, // Australia
|
||||
{ regex: uWordBoundaryGroup("(Austria|AT|AUT|AUSTRIA|VIENNA)"), iso3: "AUT", flag: "🇦🇹", priority: 120 }, // Austria
|
||||
{ regex: uWordBoundaryGroup("(Беларусь|Белоруссия|BELARUS)"), iso3: "BLR", flag: "🇧🇾", priority: 130 }, // Belarus
|
||||
{ regex: uWordBoundaryGroup("(Brazil|BR|BRA|BRAZIL|SAO\s*PAULO)"), iso3: "BRA", flag: "🇧🇷", priority: 140 }, // Brazil
|
||||
{ regex: uWordBoundaryGroup("(Bulgaria|BG|BGR|BULGARIA|SOFIA)"), iso3: "BGR", flag: "🇧🇬", priority: 150 }, // Bulgaria
|
||||
{ regex: uWordBoundaryGroup("(Canada|CA|CAN|CANADA|TORONTO)"), iso3: "CAN", flag: "🇨🇦", priority: 160 }, // Canada
|
||||
{ regex: uWordBoundaryGroup("(КИТАЙ|China)"), iso3: "CHN", flag: "🇨🇳", priority: 170 }, // China
|
||||
{ regex: uWordBoundaryGroup("(Czech\s*Republic|CZ|CZE|CZECH|PRAGUE)"), iso3: "CZE", flag: "🇨🇿", priority: 180 }, // Czech Republic
|
||||
{ regex: uWordBoundaryGroup("(Denmark|DK|DNK|DENMARK|COPENHAGEN)"), iso3: "DNK", flag: "🇩🇰", priority: 190 }, // Denmark
|
||||
{ regex: uWordBoundaryGroup("(Egypt|EG|EGY|EGYPT|CAIRO)"), iso3: "EGY", flag: "🇪🇬", priority: 200 }, // Egypt
|
||||
{ regex: uWordBoundaryGroup("(Эстония|EE|EST|ESTONIA|TALLINN)"), iso3: "EST", flag: "🇪🇪", priority: 210 }, // Estonia
|
||||
{ regex: uWordBoundaryGroup("(Финляндия|FI|FIN|FINLAND|HELSINKI)"), iso3: "FIN", flag: "🇫🇮", priority: 220 }, // Finland
|
||||
{ regex: uWordBoundaryGroup("(Франция|FR|FRA|FRANCE|PARIS|MARSEILLE)"), iso3: "FRA", flag: "🇫🇷", priority: 230 }, // France
|
||||
{ regex: uWordBoundaryGroup("(Georgia|GE|GEO|GEORGIA|TBILISI)"), iso3: "GEO", flag: "🇬🇪", priority: 240 }, // Georgia
|
||||
{ regex: uWordBoundaryGroup("(Германия|DE|DEU|GER(MANY)?|FRANKFURT|BERLIN|MUNICH)"), iso3: "DEU", flag: "🇩🇪", priority: 250 }, // Germany
|
||||
{ regex: uWordBoundaryGroup("(Гонконг|HK|HKG|HONG\s*KONG)"), iso3: "HKG", flag: "🇭🇰", priority: 260 }, // Hong Kong
|
||||
{ regex: uWordBoundaryGroup("(India|IN|IND|INDIA|MUMBAI)"), iso3: "IND", flag: "🇮🇳", priority: 270 }, // India
|
||||
{ regex: uWordBoundaryGroup("(Ireland|IE|IRL|IRELAND|DUBLIN)"), iso3: "IRL", flag: "🇮🇪", priority: 280 }, // Ireland
|
||||
{ regex: uWordBoundaryGroup("(Israel|IL|ISR|ISRAEL|TEL\s*AVIV)"), iso3: "ISR", flag: "🇮🇱", priority: 290 }, // Israel
|
||||
{ regex: uWordBoundaryGroup("(Italy|IT|ITA|ITALY|ROME)"), iso3: "ITA", flag: "🇮🇹", priority: 300 }, // Italy
|
||||
{ regex: uWordBoundaryGroup("(Япония|JP|JPN|JAPAN|TOKYO|OSAKA)"), iso3: "JPN", flag: "🇯🇵", priority: 310 }, // Japan
|
||||
{ regex: uWordBoundaryGroup("(Kazakhstan|KZ|KAZ|KAZAKHSTAN|ALMATY)"), iso3: "KAZ", flag: "🇰🇿", priority: 320 }, // Kazakhstan
|
||||
{ regex: uWordBoundaryGroup("(Латвия|LV|LVA|LATVIA|RIGA)"), iso3: "LVA", flag: "🇱🇻", priority: 330 }, // Latvia
|
||||
{ regex: uWordBoundaryGroup("(Литва|LT|LTU|LITHUANIA|VILNIUS)"), iso3: "LTU", flag: "🇱🇹", priority: 340 }, // Lithuania
|
||||
{ regex: uWordBoundaryGroup("(Malaysia|MY|MYS|MALAYSIA|KUALA\s*LUMPUR)"), iso3: "MYS", flag: "🇲🇾", priority: 350 }, // Malaysia
|
||||
{ regex: uWordBoundaryGroup("(Moldova|MD|MDA|MOLDOVA|CHISINAU)"), iso3: "MDA", flag: "🇲🇩", priority: 360 }, // Moldova
|
||||
{ regex: uWordBoundaryGroup("(Нидерланды|NL|NLD|NETHERLANDS|HOLLAND|AMSTERDAM)"), iso3: "NLD", flag: "🇳🇱", priority: 370 }, // Netherlands
|
||||
{ regex: uWordBoundaryGroup("(Nigeria|NG|NGA|NIGERIA|LAGOS)"), iso3: "NGA", flag: "🇳🇬", priority: 380 }, // Nigeria
|
||||
{ regex: uWordBoundaryGroup("(Норвегия|NO|NOR|NORWAY|OSLO)"), iso3: "NOR", flag: "🇳🇴", priority: 390 }, // Norway
|
||||
{ regex: uWordBoundaryGroup("(Philippines|PH|PHL|PHILIPPINES|MANILA)"), iso3: "PHL", flag: "🇵🇭", priority: 400 }, // Philippines
|
||||
{ regex: uWordBoundaryGroup("(Польша|PL|POL|POLAND|WARSAW|WARSZAWA)"), iso3: "POL", flag: "🇵🇱", priority: 410 }, // Poland
|
||||
{ regex: uWordBoundaryGroup("(Portugal|PT|PRT|PORTUGAL|LISBON)"), iso3: "PRT", flag: "🇵🇹", priority: 420 }, // Portugal
|
||||
{ regex: uWordBoundaryGroup("(Romania|RO|ROU|ROMANIA|BUCHAREST)"), iso3: "ROU", flag: "🇷🇴", priority: 430 }, // Romania
|
||||
{ regex: uWordBoundaryGroup("(Russia|RU|RUS|RUSSIA|MOSCOW)"), iso3: "RUS", flag: "🇷🇺", priority: 440 }, // Russia
|
||||
{ regex: uWordBoundaryGroup("(Сингапур|SG|SGP|SINGAPORE)"), iso3: "SGP", flag: "🇸🇬", priority: 200 }, // Singapore
|
||||
{ regex: uWordBoundaryGroup("(South Korea|Корея|KR|KOR|KOREA|SEOUL)"), iso3: "KOR", flag: "🇰🇷", priority: 450 }, // South Korea
|
||||
{ regex: uWordBoundaryGroup("(Spain|ES|ESP|SPAIN|MADRID)"), iso3: "ESP", flag: "🇪🇸", priority: 460 }, // Spain
|
||||
{ regex: uWordBoundaryGroup("(Швеция|SE|SWE|SWEDEN|STOCKHOLM)"), iso3: "SWE", flag: "🇸🇪", priority: 470 }, // Sweden
|
||||
{ regex: uWordBoundaryGroup("(Швейцария|CH|CHE|SWITZERLAND|Switzerl)"), iso3: "CHE", flag: "🇨🇭", priority: 480 }, // Switzerland
|
||||
{ regex: uWordBoundaryGroup("(Taiwan|TW|TWN|TAIWAN|TAIPEI)"), iso3: "TWN", flag: "🇹🇼", priority: 490 }, // Taiwan
|
||||
{ regex: uWordBoundaryGroup("(Thailand|TH|THA|THAILAND|BANGKOK)"), iso3: "THA", flag: "🇹🇭", priority: 500 }, // Thailand
|
||||
{ regex: uWordBoundaryGroup("(Турция|TR|TUR|TURKEY|ISTANBUL)"), iso3: "TUR", flag: "🇹🇷", priority: 510 }, // Turkey
|
||||
{ regex: uWordBoundaryGroup("(UAE|United\s*Arab\s*Emirates|AE|ARE|DUBAI)"), iso3: "ARE", flag: "🇦🇪", priority: 520 }, // UAE
|
||||
{ regex: uWordBoundaryGroup("(Великобритания|Англия|England|UK|GB|GBR|UNITED\s*KINGDOM)"), iso3: "GBR", flag: "🇬🇧", priority: 530 }, // UK
|
||||
{ regex: uWordBoundaryGroup("(США|USA|US|UNITED\s*STATES|AMERICA|NEW\s*YORK|NYC)"), iso3: "USA", flag: "🇺🇸", priority: 540 }, // USA
|
||||
{ regex: uWordBoundaryGroup("(Vietnam|VN|VNM|VIETNAM|HANOI)"), iso3: "VNM", flag: "🇻🇳", priority: 500 } // Vietnam
|
||||
];
|
||||
|
||||
// 5) GeoIP mapping (ISO2 -> ISO3 + flag) used only if utils.geoip.lookup(ip) returns ISO2
|
||||
const ISO2_TO_ISO3 = {
|
||||
US: { iso3: "USA", flag: "🇺🇸" },
|
||||
DE: { iso3: "DEU", flag: "🇩🇪" },
|
||||
NL: { iso3: "NLD", flag: "🇳🇱" },
|
||||
GB: { iso3: "GBR", flag: "🇬🇧" },
|
||||
FR: { iso3: "FRA", flag: "🇫🇷" },
|
||||
PL: { iso3: "POL", flag: "🇵🇱" },
|
||||
FI: { iso3: "FIN", flag: "🇫🇮" },
|
||||
SE: { iso3: "SWE", flag: "🇸🇪" },
|
||||
NO: { iso3: "NOR", flag: "🇳🇴" },
|
||||
CH: { iso3: "CHE", flag: "🇨🇭" },
|
||||
EE: { iso3: "EST", flag: "🇪🇪" },
|
||||
LV: { iso3: "LVA", flag: "🇱🇻" },
|
||||
LT: { iso3: "LTU", flag: "🇱🇹" },
|
||||
TR: { iso3: "TUR", flag: "🇹🇷" },
|
||||
SG: { iso3: "SGP", flag: "🇸🇬" },
|
||||
JP: { iso3: "JPN", flag: "🇯🇵" },
|
||||
KR: { iso3: "KOR", flag: "🇰🇷" },
|
||||
HK: { iso3: "HKG", flag: "🇭🇰" },
|
||||
};
|
||||
|
||||
// 6) Protocol icons (based on proxy.type)
|
||||
const PROTOCOL_ICONS = {
|
||||
ss: "",
|
||||
ssr: "",
|
||||
vmess: "",
|
||||
vless: "",
|
||||
trojan: "",
|
||||
http: "",
|
||||
socks5: "",
|
||||
snell: "",
|
||||
wireguard: "",
|
||||
hysteria: "",
|
||||
hysteria2: "",
|
||||
tuic: ""
|
||||
};
|
||||
|
||||
const STANDARD_PORTS_BY_TYPE = {
|
||||
wireguard: new Set(["51820"]),
|
||||
vless: new Set(["443"]),
|
||||
trojan: new Set(["443"]),
|
||||
ss: new Set(["443"]),
|
||||
};
|
||||
|
||||
const PROTOCOL_ICON_DEFAULT = ""; // fallback icon if type is unknown
|
||||
|
||||
|
||||
const METATAG_RULES = {
|
||||
// Keys are "network/type" OR "/type" (network-agnostic) OR "network/" (type-agnostic)
|
||||
// Matching priority: exact "network/type" -> "/type" -> "network/" -> default
|
||||
// 🅶🆃 🆃🆂 🆃🆅 🆆🆅 🆇🆅 🆆🅶 🅽🅸
|
||||
pairMap: {
|
||||
"grpc/trojan": "🅶🆃",
|
||||
"tcp/trojan": "🆃🆃",
|
||||
"tcp/ss": "🆃🆂",
|
||||
"grpc/vless": "🅶🆅",
|
||||
"tcp/vless": "🆃🆅",
|
||||
"ws/vless": "🆆🆅",
|
||||
"xhttp/vless": "🆇🆅",
|
||||
|
||||
"/wireguard": "🆆🅶",
|
||||
"/naive": "🅽🅸",
|
||||
},
|
||||
|
||||
defaultPair: "▫️", // fallback if nothing matches
|
||||
includeFallbackText: false, // if true, append "(net/type)" when defaultPair is used
|
||||
};
|
||||
|
||||
// Port formatting: superscript digits with left padding to 4 chars
|
||||
// 𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗
|
||||
const PORT_FORMAT = {
|
||||
padLeftTo: 3,
|
||||
padChar: "0",
|
||||
fancy: {
|
||||
"0": "𝟎", "1": "𝟏", "2": "𝟐", "3": "𝟑", "4": "𝟒", "5": "𝟓", "6": "𝟔", "7": "𝟕", "8": "𝟖", "9": "𝟗",
|
||||
},
|
||||
};
|
||||
|
||||
///////////////////////
|
||||
// HELPERS
|
||||
///////////////////////
|
||||
|
||||
function normalizeToken(s) {
|
||||
return String(s || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function uWordBoundaryGroup(inner) {
|
||||
// Match if surrounded by non-letter/non-digit (Unicode-aware)
|
||||
// We don't use lookbehind for max compatibility.
|
||||
return new RegExp(`(?:^|[^\\p{L}\\p{N}])(?:${inner})(?=$|[^\\p{L}\\p{N}])`, "iu");
|
||||
}
|
||||
|
||||
function portToFancy(port, type) {
|
||||
let p = String(port ?? "").trim();
|
||||
p = p.replace(/[^\d]/g, "");
|
||||
if (!p) return "";
|
||||
|
||||
if (STANDARD_PORTS_BY_TYPE[type]?.has(p)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// left pad to fixed width
|
||||
if (PORT_FORMAT.padLeftTo && p.length < PORT_FORMAT.padLeftTo) {
|
||||
p = p.padStart(PORT_FORMAT.padLeftTo, PORT_FORMAT.padChar);
|
||||
}
|
||||
|
||||
// map digits
|
||||
let out = "";
|
||||
for (const ch of p) out += PORT_FORMAT.fancy[ch] ?? ch;
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildMetaTag(proxy) {
|
||||
const net = safeStr(proxy && proxy.network) || "";
|
||||
const typ = safeStr(proxy && proxy.type) || "";
|
||||
const port = safeStr(proxy && proxy.port);
|
||||
|
||||
const { icon, matched } = metaPairIcon(net, typ);
|
||||
const portSup = portToFancy(port, typ);
|
||||
|
||||
if (icon === METATAG_RULES.defaultPair && METATAG_RULES.includeFallbackText) {
|
||||
return `${icon}${portSup}(${normalizeToken(net)}/${normalizeToken(typ)})`;
|
||||
}
|
||||
|
||||
return `${icon}${portSup}`;
|
||||
}
|
||||
|
||||
function metaPairIcon(network, type) {
|
||||
const net = normalizeToken(network);
|
||||
const typ = normalizeToken(type);
|
||||
|
||||
const exact = `${net}/${typ}`;
|
||||
const typeOnly = `/${typ}`;
|
||||
const netOnly = `${net}/`;
|
||||
|
||||
const m = METATAG_RULES.pairMap;
|
||||
|
||||
if (m[exact]) return { icon: m[exact], matched: exact };
|
||||
if (m[typeOnly]) return { icon: m[typeOnly], matched: typeOnly };
|
||||
if (m[netOnly]) return { icon: m[netOnly], matched: netOnly };
|
||||
|
||||
return { icon: METATAG_RULES.defaultPair, matched: null };
|
||||
}
|
||||
|
||||
function isIPv4(str) {
|
||||
if (typeof str !== "string") return false;
|
||||
const m = str.match(/^(\d{1,3})(\.\d{1,3}){3}$/);
|
||||
if (!m) return false;
|
||||
return str.split(".").every(oct => {
|
||||
const n = Number(oct);
|
||||
return n >= 0 && n <= 255 && String(n) === oct.replace(/^0+(\d)/, "$1"); // avoids "001" weirdness
|
||||
});
|
||||
}
|
||||
|
||||
function uniq(arr) {
|
||||
return [...new Set(arr.filter(Boolean))];
|
||||
}
|
||||
|
||||
function sanitizeBaseName(name) {
|
||||
let s = String(name || "");
|
||||
|
||||
// Remove noise patterns
|
||||
for (const re of NOISE_PATTERNS) s = s.replace(re, " ");
|
||||
|
||||
// Collapse spaces
|
||||
s = s.replace(/\s+/g, " ").trim();
|
||||
return s;
|
||||
}
|
||||
|
||||
function extractIconTagsAndStrip(name) {
|
||||
let s = String(name || "");
|
||||
const tags = [];
|
||||
|
||||
for (const r of ICON_RULES) {
|
||||
if (r.regex.test(s)) {
|
||||
tags.push(r.icon);
|
||||
s = s.replace(r.regex, " ");
|
||||
}
|
||||
}
|
||||
|
||||
for (const t of NAME_NETWORK_TAGS) {
|
||||
if (t.regex.test(s)) {
|
||||
tags.push(t.tag);
|
||||
s = s.replace(t.regex, " ");
|
||||
}
|
||||
}
|
||||
|
||||
return { stripped: s.replace(/\s+/g, " ").trim(), tags: uniq(tags) };
|
||||
}
|
||||
|
||||
function detectCountryByName(name) {
|
||||
const n = String(name || "");
|
||||
// Order by priority, then first match wins
|
||||
|
||||
// Fast path: flag emoji
|
||||
if (n.includes("🇦🇪")) return { iso3: "ARE", flag: "🇦🇪", priority: 1, source: "flag" };
|
||||
if (n.includes("🇦🇱")) return { iso3: "ALB", flag: "🇦🇱", priority: 11, source: "flag" };
|
||||
if (n.includes("🇦🇷")) return { iso3: "ARG", flag: "🇦🇷", priority: 2, source: "flag" };
|
||||
if (n.includes("🇦🇹")) return { iso3: "AUT", flag: "🇦🇹", priority: 3, source: "flag" };
|
||||
if (n.includes("🇦🇶")) return { iso3: "ATA", flag: "🇦🇶", priority: 11, source: "flag" };
|
||||
if (n.includes("🇦🇺")) return { iso3: "AUS", flag: "🇦🇺", priority: 4, source: "flag" };
|
||||
if (n.includes("🇧🇪")) return { iso3: "BEL", flag: "🇧🇪", priority: 11, source: "flag" };
|
||||
if (n.includes("🇧🇬")) return { iso3: "BGR", flag: "🇧🇬", priority: 5, source: "flag" };
|
||||
if (n.includes("🇧🇾")) return { iso3: "BLR", flag: "🇧🇾", priority: 6, source: "flag" };
|
||||
if (n.includes("🇧🇷")) return { iso3: "BRA", flag: "🇧🇷", priority: 7, source: "flag" };
|
||||
if (n.includes("🇨🇦")) return { iso3: "CAN", flag: "🇨🇦", priority: 8, source: "flag" };
|
||||
if (n.includes("🇨🇭")) return { iso3: "CHE", flag: "🇨🇭", priority: 9, source: "flag" };
|
||||
if (n.includes("🇨🇳")) return { iso3: "CHN", flag: "🇨🇳", priority: 10, source: "flag" };
|
||||
if (n.includes("🇨🇳")) return { iso3: "CHN", flag: "🇨🇳", priority: 10, source: "flag" };
|
||||
if (n.includes("🇨🇾")) return { iso3: "CYP", flag: "🇨🇾", priority: 11, source: "flag" };
|
||||
if (n.includes("🇩🇪")) return { iso3: "DEU", flag: "🇩🇪", priority: 12, source: "flag" };
|
||||
if (n.includes("🇩🇰")) return { iso3: "DNK", flag: "🇩🇰", priority: 13, source: "flag" };
|
||||
if (n.includes("🇪🇪")) return { iso3: "EST", flag: "🇪🇪", priority: 14, source: "flag" };
|
||||
if (n.includes("🇪🇬")) return { iso3: "EGY", flag: "🇪🇬", priority: 15, source: "flag" };
|
||||
if (n.includes("🇪🇸")) return { iso3: "ESP", flag: "🇪🇸", priority: 16, source: "flag" };
|
||||
if (n.includes("🇫🇮")) return { iso3: "FIN", flag: "🇫🇮", priority: 17, source: "flag" };
|
||||
if (n.includes("🇫🇷")) return { iso3: "FRA", flag: "🇫🇷", priority: 18, source: "flag" };
|
||||
if (n.includes("🇬🇧")) return { iso3: "GBR", flag: "🇬🇧", priority: 19, source: "flag" };
|
||||
if (n.includes("🇬🇪")) return { iso3: "GEO", flag: "🇬🇪", priority: 20, source: "flag" };
|
||||
if (n.includes("🇬🇷")) return { iso3: "GRC", flag: "🇬🇷", priority: 11, source: "flag" };
|
||||
if (n.includes("🇭🇰")) return { iso3: "HKG", flag: "🇭🇰", priority: 21, source: "flag" };
|
||||
if (n.includes("🇭🇷")) return { iso3: "HRV", flag: "🇭🇷", priority: 11, source: "flag" };
|
||||
if (n.includes("🇮🇪")) return { iso3: "IRL", flag: "🇮🇪", priority: 22, source: "flag" };
|
||||
if (n.includes("🇮🇱")) return { iso3: "ISR", flag: "🇮🇱", priority: 23, source: "flag" };
|
||||
if (n.includes("🇮🇳")) return { iso3: "IND", flag: "🇮🇳", priority: 24, source: "flag" };
|
||||
if (n.includes("🇮🇹")) return { iso3: "ITA", flag: "🇮🇹", priority: 25, source: "flag" };
|
||||
if (n.includes("🇮🇸")) return { iso3: "ISL", flag: "🇮🇸", priority: 25, source: "flag" };
|
||||
if (n.includes("🇯🇵")) return { iso3: "JPN", flag: "🇯🇵", priority: 26, source: "flag" };
|
||||
if (n.includes("🇰🇷")) return { iso3: "KOR", flag: "🇰🇷", priority: 27, source: "flag" };
|
||||
if (n.includes("🇰🇿")) return { iso3: "KAZ", flag: "🇰🇿", priority: 28, source: "flag" };
|
||||
if (n.includes("🇱🇹")) return { iso3: "LTU", flag: "🇱🇹", priority: 29, source: "flag" };
|
||||
if (n.includes("🇱🇻")) return { iso3: "LVA", flag: "🇱🇻", priority: 30, source: "flag" };
|
||||
if (n.includes("🇲🇩")) return { iso3: "MDA", flag: "🇲🇩", priority: 31, source: "flag" };
|
||||
if (n.includes("🇲🇾")) return { iso3: "MYS", flag: "🇲🇾", priority: 32, source: "flag" };
|
||||
if (n.includes("🇳🇬")) return { iso3: "NGA", flag: "🇳🇬", priority: 33, source: "flag" };
|
||||
if (n.includes("🇳🇱")) return { iso3: "NLD", flag: "🇳🇱", priority: 34, source: "flag" };
|
||||
if (n.includes("🇳🇴")) return { iso3: "NOR", flag: "🇳🇴", priority: 35, source: "flag" };
|
||||
if (n.includes("🇵🇭")) return { iso3: "PHL", flag: "🇵🇭", priority: 36, source: "flag" };
|
||||
if (n.includes("🇵🇰")) return { iso3: "PAK", flag: "🇵🇰", priority: 11, source: "flag" };
|
||||
if (n.includes("🇵🇱")) return { iso3: "POL", flag: "🇵🇱", priority: 37, source: "flag" };
|
||||
if (n.includes("🇵🇹")) return { iso3: "PRT", flag: "🇵🇹", priority: 38, source: "flag" };
|
||||
if (n.includes("🇷🇴")) return { iso3: "ROU", flag: "🇷🇴", priority: 39, source: "flag" };
|
||||
if (n.includes("🇷🇺")) return { iso3: "RUS", flag: "🇷🇺", priority: 40, source: "flag" };
|
||||
if (n.includes("🇸🇪")) return { iso3: "SWE", flag: "🇸🇪", priority: 41, source: "flag" };
|
||||
if (n.includes("🇸🇬")) return { iso3: "SGP", flag: "🇸🇬", priority: 42, source: "flag" };
|
||||
if (n.includes("🇸🇾")) return { iso3: "SYR", flag: "🇸🇾", priority: 11, source: "flag" };
|
||||
if (n.includes("🇹🇭")) return { iso3: "THA", flag: "🇹🇭", priority: 43, source: "flag" };
|
||||
if (n.includes("🇹🇷")) return { iso3: "TUR", flag: "🇹🇷", priority: 44, source: "flag" };
|
||||
if (n.includes("🇹🇼")) return { iso3: "TWN", flag: "🇹🇼", priority: 45, source: "flag" };
|
||||
if (n.includes("🇺🇦")) return { iso3: "UKR", flag: "🇺🇦", priority: 11, source: "flag" };
|
||||
if (n.includes("🇺🇸")) return { iso3: "USA", flag: "🇺🇸", priority: 46, source: "flag" };
|
||||
if (n.includes("🇻🇳")) return { iso3: "VNM", flag: "🇻🇳", priority: 47, source: "flag" };
|
||||
|
||||
const sorted = COUNTRY_RULES.slice().sort((a, b) => a.priority - b.priority);
|
||||
for (const c of sorted) {
|
||||
if (c.regex.test(n)) return { iso3: c.iso3, flag: c.flag, priority: c.priority, source: "name" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function detectCountryByGeoIP(server, utils) {
|
||||
if (!isIPv4(server)) return null;
|
||||
if (!utils || !utils.geoip || typeof utils.geoip.lookup !== "function") return null;
|
||||
|
||||
try {
|
||||
const geo = utils.geoip.lookup(server);
|
||||
const iso2 = geo && (geo.country || geo.country_code || geo.iso_code);
|
||||
if (!iso2 || typeof iso2 !== "string") return null;
|
||||
|
||||
const key = iso2.toUpperCase();
|
||||
const mapped = ISO2_TO_ISO3[key];
|
||||
if (mapped) return { iso3: mapped.iso3, flag: mapped.flag, priority: 900, source: "geoip" };
|
||||
|
||||
// Unknown ISO2: keep something sane
|
||||
return { iso3: key, flag: "🏳️", priority: 950, source: "geoip" };
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function pad2(n) {
|
||||
const x = Number(n);
|
||||
return x < 10 ? `0${x}` : String(x);
|
||||
}
|
||||
|
||||
function safeStr(v) {
|
||||
return (v === undefined || v === null) ? "" : String(v);
|
||||
}
|
||||
|
||||
///////////////////////
|
||||
// OPERATOR
|
||||
///////////////////////
|
||||
|
||||
function operator(proxies, targetPlatform, utils) {
|
||||
// Sub-Store sometimes passes utils as global $utils; sometimes as 3rd arg; sometimes not at all.
|
||||
// We'll accept any of them without whining.
|
||||
const U = utils || (typeof $utils !== "undefined" ? $utils : null);
|
||||
|
||||
const buckets = Object.create(null);
|
||||
|
||||
for (const proxy of proxies) {
|
||||
const originalName = safeStr(proxy && proxy.name);
|
||||
|
||||
// 1) Extract tags (icons) from ORIGINAL name, then strip those keywords out
|
||||
const iconStage = extractIconTagsAndStrip(originalName);
|
||||
|
||||
// 2) Sanitize remaining base name (remove marketing trash, brackets, etc.)
|
||||
const cleanBase = sanitizeBaseName(iconStage.stripped);
|
||||
|
||||
// 3) Detect country (name first, then GeoIP)
|
||||
const byName = detectCountryByName(originalName);
|
||||
const byGeo = detectCountryByGeoIP(proxy && proxy.server, U);
|
||||
const country = byName || byGeo || { iso3: "UNK", flag: "🏴☠️", priority: 9999, source: "fallback" };
|
||||
|
||||
// 4) Protocol icon (based on type)
|
||||
const proto = PROTOCOL_ICONS[(proxy && proxy.type) || ""] || PROTOCOL_ICON_DEFAULT;
|
||||
|
||||
// 5) Network/type/port tag (from proxy fields)
|
||||
const metaTag = buildMetaTag(proxy);
|
||||
|
||||
// 6) Prepare bucket key
|
||||
const key = country.iso3;
|
||||
|
||||
if (!buckets[key]) {
|
||||
buckets[key] = {
|
||||
country,
|
||||
list: []
|
||||
};
|
||||
}
|
||||
|
||||
// Keep meta used for sorting and final formatting
|
||||
buckets[key].list.push({
|
||||
proxy,
|
||||
_meta: {
|
||||
originalName,
|
||||
cleanBase,
|
||||
iconTags: iconStage.tags,
|
||||
proto,
|
||||
metaTag
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 7) Sort buckets by priority
|
||||
const bucketKeys = Object.keys(buckets).sort((a, b) => {
|
||||
return (buckets[a].country.priority || 9999) - (buckets[b].country.priority || 9999);
|
||||
});
|
||||
|
||||
// 8) Sort inside each country bucket and rename with per-country numbering
|
||||
const result = [];
|
||||
|
||||
for (const key of bucketKeys) {
|
||||
const group = buckets[key];
|
||||
|
||||
group.list.sort((A, B) => {
|
||||
// Tags do not affect numbering: sort only by sanitized base + server:port as tie-breaker
|
||||
const an = A._meta.cleanBase.toLowerCase();
|
||||
const bn = B._meta.cleanBase.toLowerCase();
|
||||
if (an !== bn) return an.localeCompare(bn);
|
||||
|
||||
const as = `${safeStr(A.proxy.server)}:${safeStr(A.proxy.port)}`;
|
||||
const bs = `${safeStr(B.proxy.server)}:${safeStr(B.proxy.port)}`;
|
||||
return as.localeCompare(bs);
|
||||
});
|
||||
|
||||
for (let i = 0; i < group.list.length; i++) {
|
||||
const item = group.list[i];
|
||||
const p = item.proxy;
|
||||
const num = pad2(i + 1);
|
||||
|
||||
const debugSuffix = DEBUG_APPEND_ORIGINAL_NAME
|
||||
? ` ⟦${item._meta.originalName}⟧`
|
||||
: "";
|
||||
|
||||
const tagStr = item._meta.iconTags.length ? ` ${item._meta.iconTags.join(" ")}` : "";
|
||||
|
||||
p.name = `${group.country.flag}${item._meta.metaTag} ${group.country.iso3}-${num} ${item._meta.proto}${tagStr} ${debugSuffix}`
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
|
||||
result.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
function safeStringify(obj) {
|
||||
const seen = new WeakSet();
|
||||
return JSON.stringify(
|
||||
obj,
|
||||
(k, v) => {
|
||||
if (typeof v === "object" && v !== null) {
|
||||
if (seen.has(v)) return "[Circular]";
|
||||
seen.add(v);
|
||||
}
|
||||
if (typeof v === "function") return `[Function: ${v.name || "anonymous"}]`;
|
||||
if (typeof v === "bigint") return v.toString();
|
||||
return v;
|
||||
},
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
function pickEnvSample() {
|
||||
try {
|
||||
const env = (typeof process !== "undefined" && process && process.env) ? process.env : null;
|
||||
if (!env) return null;
|
||||
|
||||
// only show safe-ish keys, no full dump
|
||||
const keys = Object.keys(env).sort();
|
||||
const filtered = keys.filter(k =>
|
||||
k.toLowerCase().includes("sub") ||
|
||||
k.toLowerCase().includes("store") ||
|
||||
k.toLowerCase().includes("script") ||
|
||||
k.toLowerCase().includes("url") ||
|
||||
k.toLowerCase().includes("option") ||
|
||||
k.toLowerCase().includes("param")
|
||||
);
|
||||
|
||||
const sample = {};
|
||||
for (const k of filtered.slice(0, 50)) sample[k] = env[k];
|
||||
return { keysCount: keys.length, filteredKeys: filtered.slice(0, 100), sample };
|
||||
} catch (e) {
|
||||
return { error: String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
function getGlobalDollarKeys() {
|
||||
try {
|
||||
return Object.getOwnPropertyNames(globalThis).filter(k => k.startsWith("$")).sort();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Safe "typeof" probes: never throws even if variable doesn't exist
|
||||
const probes = {
|
||||
$content: typeof $content,
|
||||
$files: typeof $files,
|
||||
$options: typeof $options,
|
||||
|
||||
$params: typeof $params,
|
||||
$args: typeof $args,
|
||||
$arguments: typeof $arguments,
|
||||
$argument: typeof $argument,
|
||||
$argv: typeof $argv,
|
||||
|
||||
$ctx: typeof $ctx,
|
||||
$context: typeof $context,
|
||||
$request: typeof $request,
|
||||
$req: typeof $req,
|
||||
$url: typeof $url,
|
||||
$scriptUrl: typeof $scriptUrl,
|
||||
$script_url: typeof $script_url,
|
||||
|
||||
ProxyUtils: typeof ProxyUtils,
|
||||
produceArtifact: typeof produceArtifact,
|
||||
|
||||
process: typeof process,
|
||||
};
|
||||
|
||||
const values = {};
|
||||
function maybeSet(name, getter) {
|
||||
try {
|
||||
const v = getter();
|
||||
// Avoid huge outputs
|
||||
if (typeof v === "string") values[name] = v.length > 800 ? v.slice(0, 800) + "…(truncated)" : v;
|
||||
else values[name] = v;
|
||||
} catch (e) {
|
||||
values[name] = { error: String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
maybeSet("$options", () => (typeof $options !== "undefined" ? $options : null));
|
||||
maybeSet("$params", () => (typeof $params !== "undefined" ? $params : null));
|
||||
maybeSet("$args", () => (typeof $args !== "undefined" ? $args : null));
|
||||
maybeSet("$arguments", () => (typeof $arguments !== "undefined" ? $arguments : null));
|
||||
maybeSet("$argument", () => (typeof $argument !== "undefined" ? $argument : null));
|
||||
maybeSet("$ctx", () => (typeof $ctx !== "undefined" ? $ctx : null));
|
||||
maybeSet("$request", () => (typeof $request !== "undefined" ? $request : null));
|
||||
maybeSet("$url", () => (typeof $url !== "undefined" ? $url : null));
|
||||
maybeSet("$scriptUrl", () => (typeof $scriptUrl !== "undefined" ? $scriptUrl : null));
|
||||
maybeSet("$script_url", () => (typeof $script_url !== "undefined" ? $script_url : null));
|
||||
|
||||
maybeSet("$contentPreview", () => (typeof $content === "string" ? $content.slice(0, 300) : $content));
|
||||
maybeSet("$contentLength", () => (typeof $content === "string" ? $content.length : null));
|
||||
maybeSet("$files", () => (typeof $files !== "undefined" ? $files : null));
|
||||
|
||||
const report = {
|
||||
probes,
|
||||
values,
|
||||
globalDollarKeys: getGlobalDollarKeys(),
|
||||
envSample: pickEnvSample(),
|
||||
};
|
||||
|
||||
$content = safeStringify(report);
|
||||
@@ -50,7 +50,7 @@ UI_DIR="/etc/mihomo/ui"
|
||||
# ==========================================
|
||||
echo ">>> [1/8] Updating system and installing dependencies..."
|
||||
# Включаем community репозитории (обычно там лежит gcompat и прочее)
|
||||
sed -i 's/^#//g' /etc/apk/repositories
|
||||
sed -i '/v[0-9]\.[0-9]*\/community/s/^#//' /etc/apk/repositories
|
||||
apk update
|
||||
apk add bash curl wget ca-certificates tar iptables ip6tables jq coreutils libcap bind-tools nano openrc openssh sudo shadow
|
||||
|
||||
@@ -67,6 +67,7 @@ net.ipv4.conf.default.rp_filter=0
|
||||
net.ipv4.conf.wt0.rp_filter=0
|
||||
EOF
|
||||
sysctl -p /etc/sysctl.d/99-warpgate.conf
|
||||
rc-update add sysctl boot
|
||||
|
||||
# ==========================================
|
||||
# 3. NETBIRD INSTALLATION
|
||||
121
scripts/warpgates/iptables-mihomo-setup-alpine-mark2.sh
Normal file
121
scripts/warpgates/iptables-mihomo-setup-alpine-mark2.sh
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# ----------------------------
|
||||
# Config
|
||||
# ----------------------------
|
||||
MIHOMO_UID="mihomo"
|
||||
REDIR_PORT="7892" # TCP Redirect
|
||||
TPROXY_PORT="7893" # UDP/TCP TProxy
|
||||
FW_MARK="0x1"
|
||||
ROUTE_TABLE="100"
|
||||
|
||||
EXCLUDE_IFACES=("tun0")
|
||||
|
||||
# ----------------------------
|
||||
# Helpers
|
||||
# ----------------------------
|
||||
ipt() { iptables "$@"; }
|
||||
|
||||
del_loop() {
|
||||
local table=$1
|
||||
local chain=$2
|
||||
shift 2
|
||||
local rule_args="$@"
|
||||
|
||||
while iptables -t "$table" -C "$chain" $rule_args 2>/dev/null; do
|
||||
echo "Deleting from $table/$chain: $rule_args"
|
||||
iptables -t "$table" -D "$chain" $rule_args
|
||||
done
|
||||
}
|
||||
|
||||
ensure_ip_rule() {
|
||||
while ip rule list | grep -q "fwmark ${FW_MARK} lookup ${ROUTE_TABLE}"; do
|
||||
ip rule del fwmark ${FW_MARK} lookup ${ROUTE_TABLE} || true
|
||||
done
|
||||
ip rule add fwmark ${FW_MARK} lookup ${ROUTE_TABLE}
|
||||
ip route replace local 0.0.0.0/0 dev lo table ${ROUTE_TABLE}
|
||||
}
|
||||
|
||||
# ----------------------------
|
||||
# CLEANUP PHASE
|
||||
# ----------------------------
|
||||
echo "--- Cleaning up old rules (Robust Mode) ---"
|
||||
|
||||
del_loop nat OUTPUT -p tcp -m comment --comment "MIHOMO-JUMP" -j MIHOMO_REDIR
|
||||
del_loop nat PREROUTING -i wt0 -p tcp -m comment --comment "MIHOMO-REDIRECT" -j REDIRECT --to-port "${REDIR_PORT}"
|
||||
del_loop mangle PREROUTING -i wt0 -m comment --comment "MIHOMO-JUMP" -j MIHOMO_TPROXY
|
||||
|
||||
del_loop mangle OUTPUT -p tcp -m comment --comment "MIHOMO-MARK" -j MARK --set-mark "${FW_MARK}"
|
||||
del_loop mangle OUTPUT -p udp -m comment --comment "MIHOMO-MARK" -j MARK --set-mark "${FW_MARK}"
|
||||
|
||||
for IFACE in "${EXCLUDE_IFACES[@]}"; do
|
||||
del_loop mangle OUTPUT -o "${IFACE}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||
del_loop mangle PREROUTING -i "${IFACE}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||
del_loop nat OUTPUT -o "${IFACE}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||
done
|
||||
|
||||
del_loop mangle OUTPUT -m owner --uid-owner "${MIHOMO_UID}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||
del_loop nat OUTPUT -m owner --uid-owner "${MIHOMO_UID}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||
|
||||
ipt -t mangle -F MIHOMO_TPROXY 2>/dev/null || true
|
||||
ipt -t mangle -X MIHOMO_TPROXY 2>/dev/null || true
|
||||
|
||||
ipt -t nat -F MIHOMO_REDIR 2>/dev/null || true
|
||||
ipt -t nat -X MIHOMO_REDIR 2>/dev/null || true
|
||||
|
||||
echo "--- Cleanup finished. Applying new rules ---"
|
||||
|
||||
# ----------------------------
|
||||
# NAT (REDIRECT) - TCP
|
||||
# ----------------------------
|
||||
ipt -t nat -N MIHOMO_REDIR
|
||||
|
||||
# Exclusions for gateway's own traffic
|
||||
ipt -t nat -A MIHOMO_REDIR -d 192.168.0.0/16 -j RETURN
|
||||
ipt -t nat -A MIHOMO_REDIR -d 10.0.0.0/8 -j RETURN
|
||||
ipt -t nat -A MIHOMO_REDIR -d 172.16.0.0/12 -j RETURN
|
||||
ipt -t nat -A MIHOMO_REDIR -d 127.0.0.0/8 -j RETURN
|
||||
ipt -t nat -A MIHOMO_REDIR -p tcp -j REDIRECT --to-ports "${REDIR_PORT}"
|
||||
|
||||
# Apply to OUTPUT (Local gateway traffic)
|
||||
for IFACE in "${EXCLUDE_IFACES[@]}"; do
|
||||
ipt -t nat -A OUTPUT -o "${IFACE}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||
done
|
||||
ipt -t nat -A OUTPUT -m owner --uid-owner "${MIHOMO_UID}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||
ipt -t nat -A OUTPUT -p tcp -m comment --comment "MIHOMO-JUMP" -j MIHOMO_REDIR
|
||||
|
||||
# Apply to PREROUTING (wt0 Ingress) - Force Redir for NetBird (skips exclusions by design)
|
||||
ipt -t nat -A PREROUTING -i wt0 -p tcp -m comment --comment "MIHOMO-REDIRECT" -j REDIRECT --to-port "${REDIR_PORT}"
|
||||
|
||||
# ----------------------------
|
||||
# MANGLE (TPROXY) - UDP
|
||||
# ----------------------------
|
||||
ensure_ip_rule
|
||||
ipt -t mangle -N MIHOMO_TPROXY
|
||||
|
||||
# Local exclusions: apply ONLY if traffic is NOT coming from NetBird (wt0)
|
||||
ipt -t mangle -A MIHOMO_TPROXY ! -i wt0 -d 192.168.0.0/16 -j RETURN
|
||||
ipt -t mangle -A MIHOMO_TPROXY ! -i wt0 -d 10.0.0.0/8 -j RETURN
|
||||
ipt -t mangle -A MIHOMO_TPROXY ! -i wt0 -d 172.16.0.0/12 -j RETURN
|
||||
ipt -t mangle -A MIHOMO_TPROXY ! -i wt0 -d 127.0.0.0/8 -j RETURN
|
||||
|
||||
# TProxy Targets (UDP only, TCP is handled by REDIRECT)
|
||||
ipt -t mangle -A MIHOMO_TPROXY -p udp -j TPROXY --on-port "${TPROXY_PORT}" --tproxy-mark "${FW_MARK}/${FW_MARK}"
|
||||
|
||||
# Apply to OUTPUT (Local gateway traffic)
|
||||
for IFACE in "${EXCLUDE_IFACES[@]}"; do
|
||||
ipt -t mangle -A OUTPUT -o "${IFACE}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||
done
|
||||
ipt -t mangle -A OUTPUT -m owner --uid-owner "${MIHOMO_UID}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||
|
||||
# Mark local UDP packets
|
||||
ipt -t mangle -A OUTPUT -p udp -m comment --comment "MIHOMO-MARK" -j MARK --set-mark "${FW_MARK}"
|
||||
|
||||
# Apply to PREROUTING (wt0 Ingress)
|
||||
for IFACE in "${EXCLUDE_IFACES[@]}"; do
|
||||
ipt -t mangle -A PREROUTING -i "${IFACE}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||
done
|
||||
ipt -t mangle -A PREROUTING -i wt0 -m comment --comment "MIHOMO-JUMP" -j MIHOMO_TPROXY
|
||||
|
||||
echo "Done. Suboptimal hypervisor constraints bypassed successfully."
|
||||
89
scripts/warpgates/iptables-mihomo-setup-alpine.sh
Normal file
89
scripts/warpgates/iptables-mihomo-setup-alpine.sh
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
set -u
|
||||
|
||||
# ----------------------------
|
||||
# Config
|
||||
# ----------------------------
|
||||
MIHOMO_UID="mihomo"
|
||||
TPROXY_PORT="7893"
|
||||
FW_MARK="0x1"
|
||||
ROUTE_TABLE="100"
|
||||
|
||||
# Интерфейсы клиентов (откуда прилетают запросы)
|
||||
LAN_IFACES=("wt0" "eth1" "eth2")
|
||||
|
||||
# Порты самого сервера, которые НЕ надо проксировать (Web UI, SSH)
|
||||
LOCAL_PORTS="9090,22"
|
||||
|
||||
# ----------------------------
|
||||
# Helpers
|
||||
# ----------------------------
|
||||
ipt() { iptables "$@"; }
|
||||
|
||||
cleanup_references() {
|
||||
local chain=$1
|
||||
iptables-save | grep "\-j $chain" | sed "s/^-A/-D/" | while read rule; do
|
||||
iptables -t mangle $rule 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
ensure_ip_rule() {
|
||||
# 1. Перехват трафика от клиентов в TProxy (то, что мы уже починили)
|
||||
if ! ip rule list | grep -q "fwmark ${FW_MARK} lookup ${ROUTE_TABLE}"; then
|
||||
ip rule add fwmark ${FW_MARK} lookup ${ROUTE_TABLE} pref 90
|
||||
fi
|
||||
if ! ip route show table ${ROUTE_TABLE} | grep -q "local default"; then
|
||||
ip route add local 0.0.0.0/0 dev lo table ${ROUTE_TABLE}
|
||||
fi
|
||||
|
||||
# 2. НОВОЕ: Выпуск трафика Mihomo в интернет в обход Netbird
|
||||
if ! ip rule list | grep -q "fwmark 1337 lookup main"; then
|
||||
ip rule add fwmark 1337 lookup main pref 80
|
||||
fi
|
||||
}
|
||||
|
||||
# ----------------------------
|
||||
# 1. CLEANUP
|
||||
# ----------------------------
|
||||
echo "--- Cleaning up rules ---"
|
||||
cleanup_references "MIHOMO_TPROXY"
|
||||
ipt -t mangle -F MIHOMO_TPROXY 2>/dev/null || true
|
||||
ipt -t mangle -X MIHOMO_TPROXY 2>/dev/null || true
|
||||
|
||||
# ----------------------------
|
||||
# 2. SETUP
|
||||
# ----------------------------
|
||||
ensure_ip_rule
|
||||
|
||||
# --- CHAIN: PREROUTING (Для клиентов) ---
|
||||
ipt -t mangle -N MIHOMO_TPROXY
|
||||
|
||||
# === 1. Исключения по Портам (CRITICAL FIX) ===
|
||||
# Если стучатся в веб-морду или SSH - пропускаем мимо TProxy
|
||||
ipt -t mangle -A MIHOMO_TPROXY -p tcp -m multiport --dports "${LOCAL_PORTS}" -j RETURN
|
||||
|
||||
# === 2. Исключения по IP (Bypass) ===
|
||||
# RFC1918 Private Networks
|
||||
ipt -t mangle -A MIHOMO_TPROXY -d 0.0.0.0/8 -j RETURN
|
||||
ipt -t mangle -A MIHOMO_TPROXY -d 10.0.0.0/8 -j RETURN
|
||||
ipt -t mangle -A MIHOMO_TPROXY -d 127.0.0.0/8 -j RETURN
|
||||
ipt -t mangle -A MIHOMO_TPROXY -d 169.254.0.0/16 -j RETURN
|
||||
ipt -t mangle -A MIHOMO_TPROXY -d 172.16.0.0/12 -j RETURN
|
||||
ipt -t mangle -A MIHOMO_TPROXY -d 192.168.0.0/16 -j RETURN
|
||||
# Multicast
|
||||
ipt -t mangle -A MIHOMO_TPROXY -d 224.0.0.0/4 -j RETURN
|
||||
ipt -t mangle -A MIHOMO_TPROXY -d 240.0.0.0/4 -j RETURN
|
||||
# !!! NETBIRD / CGNAT (Fix for VPN access) !!!
|
||||
ipt -t mangle -A MIHOMO_TPROXY -d 100.64.0.0/10 -j RETURN
|
||||
|
||||
# === 3. Заворачиваем в TProxy ===
|
||||
ipt -t mangle -A MIHOMO_TPROXY -p tcp -j TPROXY --on-port "${TPROXY_PORT}" --tproxy-mark "${FW_MARK}"
|
||||
ipt -t mangle -A MIHOMO_TPROXY -p udp -j TPROXY --on-port "${TPROXY_PORT}" --tproxy-mark "${FW_MARK}"
|
||||
|
||||
# ----------------------------
|
||||
# 3. APPLY
|
||||
# ----------------------------
|
||||
for IFACE in "${LAN_IFACES[@]}"; do
|
||||
echo "Adding TProxy rules for interface: $IFACE"
|
||||
ipt -t mangle -A PREROUTING -i "$IFACE" -j MIHOMO_TPROXY
|
||||
done
|
||||
81
scripts/warpgates/update-core-and-dash.sh
Normal file
81
scripts/warpgates/update-core-and-dash.sh
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
UI_URL="https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip"
|
||||
BIN_DIR="/usr/local/bin"
|
||||
UI_DIR="/etc/mihomo/ui/zashboard"
|
||||
|
||||
echo "[*] Resolving latest Alpha URL from vernesong/mihomo..."
|
||||
CORE_URL=$(curl -sL "https://api.github.com/repos/vernesong/mihomo/releases/tags/Prerelease-Alpha" | grep -o 'https://[^"]*mihomo-linux-amd64-alpha-smart-[^"]*\.gz' | head -n 1)
|
||||
|
||||
if [ -z "$CORE_URL" ]; then
|
||||
echo "[-] ERROR: Failed to resolve download URL."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[+] Target URL: $CORE_URL"
|
||||
|
||||
# ==========================================
|
||||
# ФАЗА 1: СЕТЕВЫЕ ОПЕРАЦИИ (пока жив DNS)
|
||||
# ==========================================
|
||||
|
||||
echo "[*] Downloading Mihomo Core..."
|
||||
curl -SLf -o /tmp/mihomo.gz "$CORE_URL"
|
||||
|
||||
if [ ! -s /tmp/mihomo.gz ]; then
|
||||
echo "[-] ERROR: Downloaded core file is empty or missing!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[*] Downloading Zashboard UI..."
|
||||
curl -SLf -o /tmp/zashboard.zip "$UI_URL"
|
||||
|
||||
if [ ! -s /tmp/zashboard.zip ]; then
|
||||
echo "[-] ERROR: Downloaded UI file is empty or missing!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ==========================================
|
||||
# ФАЗА 2: ЛОКАЛЬНЫЕ ОПЕРАЦИИ (остановка сервиса)
|
||||
# ==========================================
|
||||
|
||||
echo "[*] Stopping mihomo service..."
|
||||
rc-service mihomo stop
|
||||
|
||||
echo "[*] Unpacking and installing Mihomo Core..."
|
||||
gzip -d -f /tmp/mihomo.gz
|
||||
mv /tmp/mihomo "$BIN_DIR/mihomo"
|
||||
chmod 755 "$BIN_DIR/mihomo"
|
||||
chown root:root "$BIN_DIR/mihomo"
|
||||
setcap 'cap_net_admin,cap_net_bind_service=+ep' "$BIN_DIR/mihomo"
|
||||
|
||||
echo "[*] Unpacking and installing Zashboard UI..."
|
||||
# Создаем изолированную директорию для распаковки
|
||||
mkdir -p /tmp/zash_temp
|
||||
unzip -q -o /tmp/zashboard.zip -d /tmp/zash_temp/
|
||||
|
||||
# Динамически ищем, как GitHub назвал корневую папку внутри архива
|
||||
EXTRACTED_DIR=$(find /tmp/zash_temp -mindepth 1 -maxdepth 1 -type d | head -n 1)
|
||||
|
||||
if [ -z "$EXTRACTED_DIR" ]; then
|
||||
echo "[-] ERROR: Could not find extracted UI directory in the zip archive."
|
||||
rc-service mihomo start
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$UI_DIR"/*
|
||||
# Копируем содержимое найденной папки
|
||||
cp -r "$EXTRACTED_DIR"/* "$UI_DIR"/
|
||||
|
||||
chown -R root:root "$UI_DIR"
|
||||
find "$UI_DIR" -type d -exec chmod 755 {} \;
|
||||
find "$UI_DIR" -type f -exec chmod 644 {} \;
|
||||
|
||||
# Зачищаем следы
|
||||
rm -rf /tmp/zashboard.zip /tmp/zash_temp
|
||||
|
||||
echo "[*] Starting mihomo service..."
|
||||
rc-service mihomo start
|
||||
|
||||
echo "[+] Update completed successfully."
|
||||
Reference in New Issue
Block a user