From 95230c6349cb0b5ce230ec970ed383d41f62363a Mon Sep 17 00:00:00 2001 From: DaTekShaman Date: Sat, 11 Apr 2026 19:32:05 +0300 Subject: [PATCH] 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. --- .../scripts/assemble-mihomo-config-dev.js | 233 -------- .../scripts/assemble-mihomo-config.js | 239 -------- .../scripts/convert-awg-to-clash.js | 235 -------- config-sub-converter/scripts/demo/embedded.js | 28 - .../scripts/demo/fancy-chars.js | 59 -- .../scripts/demo/gemeni-tried-orginal.js | 328 ----------- .../scripts/external-proxies-sanitizer-dev.js | 540 ------------------ .../scripts/external-proxies-sanitizer.js | 516 ----------------- config-sub-converter/scripts/test-options.js | 110 ---- .../{ => legacy}/config-warpgate-debian.sh | 0 .../iptables-mihomo-setup-mark2.sh | 0 scripts/{ => legacy}/iptables-mihomo-setup.sh | 0 .../dnssec-test.sh} | 0 .../{ => warpgates}/config-warpgate-alpine.sh | 3 +- .../iptables-mihomo-setup-alpine-mark2.sh | 121 ++++ .../warpgates/iptables-mihomo-setup-alpine.sh | 89 +++ scripts/warpgates/update-core-and-dash.sh | 81 +++ 17 files changed, 293 insertions(+), 2289 deletions(-) delete mode 100644 config-sub-converter/scripts/assemble-mihomo-config-dev.js delete mode 100644 config-sub-converter/scripts/assemble-mihomo-config.js delete mode 100644 config-sub-converter/scripts/convert-awg-to-clash.js delete mode 100644 config-sub-converter/scripts/demo/embedded.js delete mode 100644 config-sub-converter/scripts/demo/fancy-chars.js delete mode 100644 config-sub-converter/scripts/demo/gemeni-tried-orginal.js delete mode 100644 config-sub-converter/scripts/external-proxies-sanitizer-dev.js delete mode 100644 config-sub-converter/scripts/external-proxies-sanitizer.js delete mode 100644 config-sub-converter/scripts/test-options.js rename scripts/{ => legacy}/config-warpgate-debian.sh (100%) rename scripts/{ => legacy}/iptables-mihomo-setup-mark2.sh (100%) rename scripts/{ => legacy}/iptables-mihomo-setup.sh (100%) rename scripts/{dnssec-tesst.sh => testing/dnssec-test.sh} (100%) rename scripts/{ => warpgates}/config-warpgate-alpine.sh (99%) create mode 100644 scripts/warpgates/iptables-mihomo-setup-alpine-mark2.sh create mode 100644 scripts/warpgates/iptables-mihomo-setup-alpine.sh create mode 100644 scripts/warpgates/update-core-and-dash.sh diff --git a/config-sub-converter/scripts/assemble-mihomo-config-dev.js b/config-sub-converter/scripts/assemble-mihomo-config-dev.js deleted file mode 100644 index ae68e73..0000000 --- a/config-sub-converter/scripts/assemble-mihomo-config-dev.js +++ /dev/null @@ -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}`; -} \ No newline at end of file diff --git a/config-sub-converter/scripts/assemble-mihomo-config.js b/config-sub-converter/scripts/assemble-mihomo-config.js deleted file mode 100644 index c4eec6c..0000000 --- a/config-sub-converter/scripts/assemble-mihomo-config.js +++ /dev/null @@ -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}`; -} \ No newline at end of file diff --git a/config-sub-converter/scripts/convert-awg-to-clash.js b/config-sub-converter/scripts/convert-awg-to-clash.js deleted file mode 100644 index fd9d1e3..0000000 --- a/config-sub-converter/scripts/convert-awg-to-clash.js +++ /dev/null @@ -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 }); diff --git a/config-sub-converter/scripts/demo/embedded.js b/config-sub-converter/scripts/demo/embedded.js deleted file mode 100644 index df95a75..0000000 --- a/config-sub-converter/scripts/demo/embedded.js +++ /dev/null @@ -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; - }); -} \ No newline at end of file diff --git a/config-sub-converter/scripts/demo/fancy-chars.js b/config-sub-converter/scripts/demo/fancy-chars.js deleted file mode 100644 index 5116fd6..0000000 --- a/config-sub-converter/scripts/demo/fancy-chars.js +++ /dev/null @@ -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; } \ No newline at end of file diff --git a/config-sub-converter/scripts/demo/gemeni-tried-orginal.js b/config-sub-converter/scripts/demo/gemeni-tried-orginal.js deleted file mode 100644 index ca168c9..0000000 --- a/config-sub-converter/scripts/demo/gemeni-tried-orginal.js +++ /dev/null @@ -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; -} diff --git a/config-sub-converter/scripts/external-proxies-sanitizer-dev.js b/config-sub-converter/scripts/external-proxies-sanitizer-dev.js deleted file mode 100644 index 2be169a..0000000 --- a/config-sub-converter/scripts/external-proxies-sanitizer-dev.js +++ /dev/null @@ -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; -} diff --git a/config-sub-converter/scripts/external-proxies-sanitizer.js b/config-sub-converter/scripts/external-proxies-sanitizer.js deleted file mode 100644 index 521fbca..0000000 --- a/config-sub-converter/scripts/external-proxies-sanitizer.js +++ /dev/null @@ -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; -} diff --git a/config-sub-converter/scripts/test-options.js b/config-sub-converter/scripts/test-options.js deleted file mode 100644 index 366a716..0000000 --- a/config-sub-converter/scripts/test-options.js +++ /dev/null @@ -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); \ No newline at end of file diff --git a/scripts/config-warpgate-debian.sh b/scripts/legacy/config-warpgate-debian.sh similarity index 100% rename from scripts/config-warpgate-debian.sh rename to scripts/legacy/config-warpgate-debian.sh diff --git a/scripts/iptables-mihomo-setup-mark2.sh b/scripts/legacy/iptables-mihomo-setup-mark2.sh similarity index 100% rename from scripts/iptables-mihomo-setup-mark2.sh rename to scripts/legacy/iptables-mihomo-setup-mark2.sh diff --git a/scripts/iptables-mihomo-setup.sh b/scripts/legacy/iptables-mihomo-setup.sh similarity index 100% rename from scripts/iptables-mihomo-setup.sh rename to scripts/legacy/iptables-mihomo-setup.sh diff --git a/scripts/dnssec-tesst.sh b/scripts/testing/dnssec-test.sh similarity index 100% rename from scripts/dnssec-tesst.sh rename to scripts/testing/dnssec-test.sh diff --git a/scripts/config-warpgate-alpine.sh b/scripts/warpgates/config-warpgate-alpine.sh similarity index 99% rename from scripts/config-warpgate-alpine.sh rename to scripts/warpgates/config-warpgate-alpine.sh index 1da98de..fd8eba4 100644 --- a/scripts/config-warpgate-alpine.sh +++ b/scripts/warpgates/config-warpgate-alpine.sh @@ -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 diff --git a/scripts/warpgates/iptables-mihomo-setup-alpine-mark2.sh b/scripts/warpgates/iptables-mihomo-setup-alpine-mark2.sh new file mode 100644 index 0000000..cdd835e --- /dev/null +++ b/scripts/warpgates/iptables-mihomo-setup-alpine-mark2.sh @@ -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." \ No newline at end of file diff --git a/scripts/warpgates/iptables-mihomo-setup-alpine.sh b/scripts/warpgates/iptables-mihomo-setup-alpine.sh new file mode 100644 index 0000000..44200ee --- /dev/null +++ b/scripts/warpgates/iptables-mihomo-setup-alpine.sh @@ -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 \ No newline at end of file diff --git a/scripts/warpgates/update-core-and-dash.sh b/scripts/warpgates/update-core-and-dash.sh new file mode 100644 index 0000000..329d701 --- /dev/null +++ b/scripts/warpgates/update-core-and-dash.sh @@ -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." \ No newline at end of file