Files
3X-UI_autoVPS/tools/lib/xray.js
T
099117c2a7 1.1.1: kit check и kit fix, заголовки Happ, безопасная загрузка ядра, инструкция XKeen
- kit check и kit fix: проверка служб, сертификата, подписки, сайта маскировки, прав на файлы; безопасная починка
- kit-sub: пропускает заголовки Happ (Routing и др.) до клиентов
- ядро Xray: запасная загрузка через зеркало только с проверкой SHA256, tools/release.sh сверяет суммы с официальными
- установка со своим доменом: при отказе Let's Encrypt предлагается самоподписанный сертификат
- README: инструкция XKeen, команды kit, новый баннер; правки инструкций и changelog

Co-authored-by: xlok1e <124461726+xlok1e@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 16:47:12 +03:00

199 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Сборка конфигов Xray для XKeen: 04_outbounds.json и 05_routing.json.
(function (root) {
'use strict';
// ---------- outbounds ----------
function clean(o) {
Object.keys(o).forEach((k) => (o[k] === undefined || o[k] === '') && delete o[k]);
return o;
}
function stream(p) {
const t = p.transport, s = p.tls;
const st = { network: t.network === 'tcp' ? 'raw' : t.network, security: s.security };
if (t.network === 'ws') st.wsSettings = clean({ path: t.path, host: t.host });
if (t.network === 'httpupgrade') st.httpupgradeSettings = clean({ path: t.path, host: t.host });
if (t.network === 'grpc') st.grpcSettings = { serviceName: t.serviceName, multiMode: !!t.multiMode };
if (t.network === 'xhttp') st.xhttpSettings = clean({ path: t.path, host: t.host, mode: t.mode });
if (t.network === 'tcp' && t.headerType === 'http') {
st.rawSettings = { header: { type: 'http', request: { path: [t.path], headers: t.host ? { Host: [t.host] } : {} } } };
}
if (s.security === 'tls') {
// В Xray 26 allowInsecure удалён: самоподписанный сертификат принимается только по отпечатку.
st.tlsSettings = clean({ serverName: s.sni, fingerprint: s.fp, alpn: s.alpn.length ? s.alpn : undefined, pinnedPeerCertSha256: s.pin || undefined });
}
if (s.security === 'reality') {
st.realitySettings = clean({ serverName: s.sni, fingerprint: s.fp || 'chrome', publicKey: s.pbk, shortId: s.sid, spiderX: s.spx });
}
return st;
}
function outbound(p, tag) {
switch (p.type) {
case 'vless':
return {
tag, protocol: 'vless',
settings: { vnext: [{ address: p.server, port: p.port, users: [clean({ id: p.uuid, encryption: 'none', flow: p.flow })] }] },
streamSettings: stream(p),
};
case 'vmess':
return {
tag, protocol: 'vmess',
settings: { vnext: [{ address: p.server, port: p.port, users: [{ id: p.uuid, alterId: p.alterId, security: p.cipher }] }] },
streamSettings: stream(p),
};
case 'trojan':
return {
tag, protocol: 'trojan',
settings: { servers: [{ address: p.server, port: p.port, password: p.password }] },
streamSettings: stream(p),
};
case 'ss':
return {
tag, protocol: 'shadowsocks',
settings: { servers: [{ address: p.server, port: p.port, method: p.method, password: p.password }] },
};
case 'hysteria2':
return {
tag, protocol: 'hysteria',
settings: { address: p.server, port: p.port, version: 2 },
streamSettings: {
network: 'hysteria', security: 'tls',
hysteriaSettings: { version: 2, auth: p.password, udpIdleTimeout: 60 },
tlsSettings: clean({ serverName: p.sni, alpn: p.alpn.length ? p.alpn : ['h3'], pinnedPeerCertSha256: p.pinSHA256 || undefined }),
},
};
case 'wireguard':
return {
tag, protocol: 'wireguard',
settings: clean({ secretKey: p.privateKey, address: p.address, mtu: p.mtu, reserved: p.reserved || undefined,
peers: [clean({ publicKey: p.publicKey, preSharedKey: p.preSharedKey, endpoint: p.server + ':' + p.port, allowedIPs: ['0.0.0.0/0', '::/0'] })] }),
};
default:
throw new Error({ tuic: 'TUIC не поддерживается Xray – возьмите генератор Mihomo',
amneziawg: 'AmneziaWG не поддерживается Xray – возьмите генератор Mihomo',
mtproto: 'MTProto – это прокси для Telegram, в Xray он не нужен' }[p.type] || 'протокол ' + p.type + ' не поддерживается Xray');
}
}
// ---------- routing ----------
// Геобазы, которые ставит XKeen (имена файлов в /opt/etc/xray/dat).
const BASES = {
zkeen: { label: 'ZKeen', hint: 'ставится XKeen по умолчанию, рекомендуется', site: 'zkeen.dat', ip: 'zkeenip.dat' },
v2fly: { label: 'V2Fly', hint: 'нужна для отдельных сервисов и блокировки рекламы', site: 'geosite_v2fly.dat', ip: 'geoip_v2fly.dat' },
refilter: { label: 'Re:filter', hint: 'большой список сайтов', site: 'geosite_refilter.dat', ip: 'geoip_refilter.dat' },
};
// Сервис → правила по базам. d: домены, i: IP. Берутся все доступные базы.
const SERVICES = [
{ id: 'blocked', label: 'Сайты из списка re:filter', on: true,
zkeen: { d: ['domains', 'other', 'politic'] }, refilter: { d: ['refilter'], i: ['refilter'] } },
{ id: 'youtube', label: 'YouTube', on: true, zkeen: { d: ['youtube'], i: ['youtube'] }, v2fly: { d: ['youtube'] } },
{ id: 'telegram', label: 'Telegram', on: true, zkeen: { i: ['telegram'] }, v2fly: { d: ['telegram'] } },
{ id: 'meta', label: 'Instagram, Facebook, WhatsApp', on: true, zkeen: { i: ['meta'] }, v2fly: { d: ['meta'] } },
{ id: 'discord', label: 'Discord', on: true, zkeen: { i: ['discord'] }, v2fly: { d: ['discord'] } },
{ id: 'twitter', label: 'X (Twitter)', on: true, v2fly: { d: ['twitter'] } },
{ id: 'ai', label: 'ChatGPT, Claude и другие ИИ', on: true, v2fly: { d: ['category-ai-!cn'] } },
{ id: 'cdn', label: 'Зарубежные CDN и хостинги', hint: 'Cloudflare, Hetzner, DigitalOcean… – их часто замедляют', on: true,
zkeen: { i: ['akamai', 'amazon', 'arelion', 'azure', 'bunnycdn', 'cdn77', 'cloudflare', 'cogent', 'colocrossing', 'contabo',
'datacamp', 'digitalocean', 'fastly', 'frantech', 'gcore', 'hetzner', 'leaseweb', 'linode', 'liquidweb', 'mega', 'melbicom',
'oracle', 'ovh', 'scaleway', 'vodafone', 'vultr'] } },
{ id: 'tiktok', label: 'TikTok', on: false, v2fly: { d: ['tiktok'] } },
{ id: 'spotify', label: 'Spotify', on: false, v2fly: { d: ['spotify'] } },
{ id: 'netflix', label: 'Netflix', on: false, v2fly: { d: ['netflix'] } },
{ id: 'github', label: 'GitHub', on: false, v2fly: { d: ['github'] } },
{ id: 'twitch', label: 'Twitch', on: false, v2fly: { d: ['twitch'] } },
];
function available(service, bases) {
return bases.some((b) => service[b]);
}
function domains(text) {
return (text || '').split(/[\s,]+/).map((d) => d.trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/.*$/, '').replace(/^\*\./, ''))
.filter((d) => /^[a-z0-9.-]+\.[a-z]{2,}$/.test(d));
}
function routing(opts, proxyTag) {
const bases = opts.bases || ['zkeen'];
const chosen = SERVICES.filter((s) => (opts.services || SERVICES.filter((x) => x.on).map((x) => x.id)).includes(s.id) && available(s, bases));
const dom = [], ip = [];
chosen.forEach((s) => bases.forEach((b) => {
const r = s[b];
if (!r) return;
(r.d || []).forEach((c) => dom.push('ext:' + BASES[b].site + ':' + c));
(r.i || []).forEach((c) => ip.push('ext:' + BASES[b].ip + ':' + c));
}));
const uniq = (a) => Array.from(new Set(a));
// Политика xkeen_full: всё с этих входов – через прокси (как в стандартном конфиге XKeen).
const rules = [{ inboundTag: ['force-proxy-redirect', 'force-proxy-tproxy'], outboundTag: proxyTag }];
const dd = domains(opts.directDomains), pd = domains(opts.proxyDomains);
if (dd.length) rules.push({ domain: dd.map((d) => 'domain:' + d), outboundTag: 'direct' });
if (opts.blockAds && bases.includes('v2fly')) rules.push({ domain: ['ext:geosite_v2fly.dat:category-ads-all'], outboundTag: 'block' });
if (opts.blockQuic) rules.push({ network: 'udp', port: '443', outboundTag: 'block' });
if (chosen.some((s) => s.id === 'discord') && bases.includes('zkeen')) {
// Голосовые каналы Discord ходят по UDP на эти порты.
rules.push({ network: 'udp', port: '19200-19400,50000-50100', ip: ['ext:zkeenip.dat:discord'], outboundTag: proxyTag });
}
if (pd.length) rules.push({ domain: pd.map((d) => 'domain:' + d), outboundTag: proxyTag });
if (dom.length) rules.push({ domain: uniq(dom), outboundTag: proxyTag });
if (ip.length) rules.push({ ip: uniq(ip), outboundTag: proxyTag });
rules.push({ network: 'tcp,udp', outboundTag: opts.finalProxy ? proxyTag : 'direct' });
return { routing: { domainStrategy: 'IPIfNonMatch', rules } };
}
// ---------- сборка ----------
// opts: tag, services[], bases[], blockAds, blockQuic, finalProxy, proxyDomains, directDomains
function build(proxies, opts) {
opts = opts || {};
const mainTag = opts.tag || 'vless-reality';
const skipped = [];
const outs = [];
const warnings = [];
proxies.forEach((p) => {
const insecure = (p.tls && p.tls.insecure && !p.tls.pin) || (p.type === 'hysteria2' && p.insecure && !p.pinSHA256);
if (insecure) warnings.push(p.name + ': в ссылке отключена проверка сертификата, а Xray 26 так не умеет – нужен отпечаток (pcs) или настоящий сертификат');
try {
const tag = outs.length === 0 ? mainTag : mainTag + '-' + (outs.length + 1);
outs.push(outbound(p, tag));
} catch (e) {
skipped.push({ name: p.name, error: e.message });
}
});
if (!outs.length) throw new Error(skipped.length ? skipped[0].error : 'Добавьте хотя бы одну ссылку.');
// Несколько серверов – балансировщик по пингу выбирает лучший.
const multi = outs.length > 1;
let obs = null;
const proxyTarget = mainTag;
if (multi) {
obs = { observatory: { subjectSelector: [mainTag], probeUrl: 'https://www.gstatic.com/generate_204', probeInterval: '1m', enableConcurrency: true } };
}
outs.push({ tag: 'direct', protocol: 'freedom' });
outs.push({ tag: 'block', protocol: 'blackhole', settings: { response: { type: 'http' } } });
const rt = routing(opts, proxyTarget);
if (multi) {
// Правила на прокси ведут в балансировщик, а не в один сервер.
rt.routing.balancers = [{ tag: 'proxy-balancer', selector: [mainTag], strategy: { type: 'leastPing' }, fallbackTag: mainTag }];
rt.routing.rules.forEach((r) => {
if (r.outboundTag === mainTag) { delete r.outboundTag; r.balancerTag = 'proxy-balancer'; }
});
}
const files = {
'04_outbounds.json': Object.assign({ outbounds: outs }, obs || {}),
'05_routing.json': rt,
};
return { files, skipped, warnings, count: outs.length - 2 };
}
const api = { buildXray: build, XRAY_SERVICES: SERVICES, XRAY_BASES: BASES, xrayServiceAvailable: available };
if (typeof module === 'object' && module.exports) module.exports = api;
root.PM = Object.assign(root.PM || {}, api);
})(typeof window !== 'undefined' ? window : globalThis);