Матрица протоколов 3X-UI: все протоколы × клиенты, исправление allowInsecure для Xray 26
Проверено настоящим подключением (3X-UI 3.8.5, Xray 26.6.27, Mihomo 1.19.31, sing-box 1.14.2, amneziawg-tools 3.1): протокол Xray Mihomo sing-box VLESS REALITY ✓ ✓ ✓ VLESS XHTTP REALITY ✓ ✓ — VLESS WS + TLS ✓ ✓ ✓ Trojan gRPC + TLS ✓ ✓ ✓ VMess WS + TLS ✓ ✓ ✓ Shadowsocks 2022 ✓ ✓ ✓ Hysteria2 ✓ ✓ ✓ TUIC v5 — ✓ ✓ WireGuard ✓ ✓ ✓ AmneziaWG классика — ✓ — (официальный клиент 3.1: ✗) AmneziaWG 3.1 — ✗ — (официальный клиент 3.1: ✓) MTProto mtg doctor: все DC Telegram доступны - Генератор Xray: allowInsecure удалён в Xray 26 и ронял конфиг — теперь отпечаток сертификата (pcs/pinSHA256) или предупреждение на странице - Генераторы: Hysteria2 и WireGuard для Xray; TUIC, WireGuard, AmneziaWG для Mihomo - tools/lib/singbox.js — сборка конфигов sing-box (для будущей единой подписки) - tests/matrix — стенд матрицы для прогона на каждой версии
This commit is contained in:
+83
-2
@@ -78,6 +78,8 @@
|
||||
s.fp = q.get('fp') || (security === 'reality' ? 'chrome' : '');
|
||||
s.alpn = splitList(q.get('alpn'));
|
||||
s.insecure = truthy(q.get('allowInsecure')) || truthy(q.get('insecure'));
|
||||
// Отпечаток сертификата: pcs — имя параметра в ссылках Xray 26, pinSHA256 — в ссылках Hysteria.
|
||||
s.pin = (q.get('pcs') || q.get('pinSHA256') || '').replace(/:/g, '').toLowerCase();
|
||||
}
|
||||
if (security === 'reality') {
|
||||
s.pbk = q.get('pbk') || '';
|
||||
@@ -194,9 +196,88 @@
|
||||
}, hp);
|
||||
}
|
||||
|
||||
const PARSERS = { 'vless': parseVless, 'vmess': parseVmess, 'trojan': parseTrojan, 'ss': parseSs, 'hy2': parseHy2, 'hysteria2': parseHy2 };
|
||||
function parseTuic(link) {
|
||||
const u = new URL(link);
|
||||
const uuid = dec(u.username), password = dec(u.password);
|
||||
if (!uuid || !password) throw new Error('в ссылке TUIC нет UUID или пароля');
|
||||
const q = u.searchParams, hp = hostPort(u);
|
||||
return Object.assign({
|
||||
type: 'tuic', name: dec(u.hash.slice(1)), uuid, password,
|
||||
sni: dec(q.get('sni') || '') || hp.server,
|
||||
alpn: splitList(q.get('alpn')).length ? splitList(q.get('alpn')) : ['h3'],
|
||||
congestion: q.get('congestion_control') || 'bbr',
|
||||
udpRelay: q.get('udp_relay_mode') || 'native',
|
||||
insecure: truthy(q.get('allow_insecure')) || truthy(q.get('insecure')),
|
||||
}, hp);
|
||||
}
|
||||
|
||||
function wgAddr(v) {
|
||||
return splitList(v).map((a) => a.replace(/\/\d+$/, ''));
|
||||
}
|
||||
|
||||
function parseWireguard(link) {
|
||||
const u = new URL(link.replace(/^wg:/, 'wireguard:'));
|
||||
const q = u.searchParams, hp = hostPort(u);
|
||||
const priv = dec(u.username);
|
||||
const pub = dec(q.get('publickey') || q.get('publicKey') || '');
|
||||
if (!priv || !pub) throw new Error('в ссылке WireGuard нет ключей');
|
||||
return Object.assign({
|
||||
type: 'wireguard', name: dec(u.hash.slice(1)), privateKey: priv, publicKey: pub,
|
||||
preSharedKey: dec(q.get('presharedkey') || ''), address: wgAddr(q.get('address') || q.get('ip') || ''),
|
||||
mtu: Number(q.get('mtu') || 1420), reserved: q.get('reserved') ? splitList(q.get('reserved')).map(Number) : null,
|
||||
}, hp);
|
||||
}
|
||||
|
||||
// Конфиг wg-quick / AmneziaWG: [Interface] + [Peer]. Параметры Jc…H4 — обфускация AmneziaWG.
|
||||
function parseWgConf(text, name) {
|
||||
const sec = {};
|
||||
let cur = null;
|
||||
text.split(/\r?\n/).forEach((line) => {
|
||||
const l = line.trim();
|
||||
const m = /^\[(\w+)\]$/.exec(l);
|
||||
if (m) { cur = m[1].toLowerCase(); sec[cur] = sec[cur] || {}; return; }
|
||||
const kv = /^([A-Za-z0-9]+)\s*=\s*(.*)$/.exec(l);
|
||||
if (kv && cur) sec[cur][kv[1].toLowerCase()] = kv[2].trim();
|
||||
const c = /^#\s*(.+)$/.exec(l);
|
||||
if (c && !name) name = c[1];
|
||||
});
|
||||
const i = sec.interface || {}, p = sec.peer || {};
|
||||
if (!i.privatekey || !p.publickey || !p.endpoint) throw new Error('конфиг WireGuard неполный');
|
||||
const ep = /^\[?([^\]]+?)\]?:(\d+)$/.exec(p.endpoint);
|
||||
if (!ep) throw new Error('в конфиге WireGuard неверный Endpoint');
|
||||
const awgKeys = ['jc', 'jmin', 'jmax', 's1', 's2', 's3', 's4', 'h1', 'h2', 'h3', 'h4', 'i1', 'i2', 'i3', 'i4', 'i5'];
|
||||
const awg = {};
|
||||
awgKeys.forEach((k) => { if (i[k] !== undefined && i[k] !== '') awg[k] = /^\d+$/.test(i[k]) ? Number(i[k]) : i[k]; });
|
||||
const isAwg = Object.keys(awg).length > 0;
|
||||
return {
|
||||
type: isAwg ? 'amneziawg' : 'wireguard', name: name || (isAwg ? 'amneziawg' : 'wireguard') + '-' + ep[1],
|
||||
server: ep[1], port: int(ep[2], 'порт'), privateKey: i.privatekey, publicKey: p.publickey,
|
||||
preSharedKey: p.presharedkey || '', address: wgAddr(i.address || ''), mtu: Number(i.mtu || 1420),
|
||||
dns: splitList(i.dns || ''), awg: isAwg ? awg : null, reserved: null,
|
||||
};
|
||||
}
|
||||
|
||||
function parseVpn(link) {
|
||||
let text;
|
||||
try { text = b64decode(link.slice('vpn://'.length).split('#')[0]); } catch (e) { throw new Error('ссылка vpn:// повреждена'); }
|
||||
if (!/\[Interface\]/.test(text)) throw new Error('формат vpn:// не поддерживается — нужен конфиг AmneziaWG');
|
||||
return parseWgConf(text, dec(link.split('#')[1] || ''));
|
||||
}
|
||||
|
||||
function parseTg(link) {
|
||||
const u = new URL(link.replace(/^https:\/\/t\.me\/proxy/, 'tg://proxy'));
|
||||
const q = u.searchParams;
|
||||
if (!q.get('server') || !q.get('secret')) throw new Error('в ссылке MTProto нет сервера или секрета');
|
||||
return { type: 'mtproto', name: 'mtproto-' + q.get('server'), server: q.get('server'), port: int(q.get('port') || '443', 'порт'), secret: q.get('secret') };
|
||||
}
|
||||
|
||||
const PARSERS = {
|
||||
'vless': parseVless, 'vmess': parseVmess, 'trojan': parseTrojan, 'ss': parseSs, 'hy2': parseHy2, 'hysteria2': parseHy2,
|
||||
'tuic': parseTuic, 'wireguard': parseWireguard, 'wg': parseWireguard, 'vpn': parseVpn, 'tg': parseTg,
|
||||
};
|
||||
|
||||
function parseLink(line) {
|
||||
if (/^https:\/\/t\.me\/proxy\?/.test(line)) line = line.replace(/^https:\/\/t\.me\/proxy/, 'tg://proxy');
|
||||
const m = /^([a-z0-9]+):\/\//i.exec(line);
|
||||
if (!m) throw new Error('это не ссылка на подключение');
|
||||
const p = PARSERS[m[1].toLowerCase()];
|
||||
@@ -233,7 +314,7 @@
|
||||
return { proxies: ok, errors };
|
||||
}
|
||||
|
||||
const api = { parseLink, parseText, b64decode };
|
||||
const api = { parseLink, parseText, parseWgConf, b64decode };
|
||||
if (typeof module === 'object' && module.exports) module.exports = api;
|
||||
root.PM = Object.assign(root.PM || {}, api);
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
|
||||
@@ -128,6 +128,21 @@
|
||||
case 'ss':
|
||||
o.cipher = p.method; o.password = p.password;
|
||||
break;
|
||||
case 'tuic':
|
||||
o.uuid = p.uuid; o.password = p.password; o.sni = p.sni; o.alpn = p.alpn;
|
||||
o['congestion-controller'] = p.congestion; o['udp-relay-mode'] = p.udpRelay;
|
||||
o['skip-cert-verify'] = p.insecure || undefined;
|
||||
break;
|
||||
case 'wireguard':
|
||||
case 'amneziawg':
|
||||
o.type = 'wireguard';
|
||||
o.ip = p.address.find((a) => a.includes('.'));
|
||||
o.ipv6 = p.address.find((a) => a.includes(':'));
|
||||
o['private-key'] = p.privateKey; o['public-key'] = p.publicKey;
|
||||
o['pre-shared-key'] = p.preSharedKey || undefined; o.mtu = p.mtu;
|
||||
o.reserved = p.reserved || undefined;
|
||||
if (p.awg) o['amnezia-wg-option'] = p.awg;
|
||||
break;
|
||||
case 'hysteria2':
|
||||
o.password = p.password; o.sni = p.sni; o.alpn = p.alpn;
|
||||
o['skip-cert-verify'] = (p.insecure && !p.pinSHA256) || undefined;
|
||||
@@ -174,6 +189,7 @@
|
||||
};
|
||||
if (services.some((s) => s.id === 'telegram')) cfg.sniffer['skip-dst-address'] = ['rule-set:telegram@ipcidr'];
|
||||
|
||||
proxies = proxies.filter((p) => p.type !== 'mtproto');
|
||||
if (proxies.length) cfg.proxies = proxies.map(proxy);
|
||||
if (opts.subscription) {
|
||||
cfg['proxy-providers'] = {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// Сборка конфига sing-box (Hiddify, Karing, NekoBox, sing-box) из разобранных ссылок.
|
||||
(function (root) {
|
||||
'use strict';
|
||||
|
||||
function clean(o) {
|
||||
Object.keys(o).forEach((k) => (o[k] === undefined || o[k] === '' || (Array.isArray(o[k]) && !o[k].length)) && delete o[k]);
|
||||
return o;
|
||||
}
|
||||
|
||||
function tls(p) {
|
||||
const s = p.tls;
|
||||
if (!s || s.security === 'none') return undefined;
|
||||
const t = clean({ enabled: true, server_name: s.sni, alpn: s.alpn, insecure: s.insecure || undefined });
|
||||
if (s.fp) t.utls = { enabled: true, fingerprint: s.fp };
|
||||
if (s.security === 'reality') t.reality = clean({ enabled: true, public_key: s.pbk, short_id: s.sid });
|
||||
return t;
|
||||
}
|
||||
|
||||
function transport(p) {
|
||||
const t = p.transport;
|
||||
if (!t || t.network === 'tcp') {
|
||||
if (t && t.headerType === 'http') return clean({ type: 'http', path: t.path, host: t.host ? [t.host] : undefined });
|
||||
return undefined;
|
||||
}
|
||||
if (t.network === 'ws') {
|
||||
const o = { type: 'ws', path: t.path };
|
||||
if (t.host) o.headers = { Host: t.host };
|
||||
return o;
|
||||
}
|
||||
if (t.network === 'httpupgrade') return clean({ type: 'httpupgrade', path: t.path, host: t.host });
|
||||
if (t.network === 'grpc') return { type: 'grpc', service_name: t.serviceName };
|
||||
throw new Error('sing-box не поддерживает транспорт ' + t.network);
|
||||
}
|
||||
|
||||
// Возвращает { outbound } или { endpoint } (WireGuard в sing-box — endpoint).
|
||||
function convert(p) {
|
||||
const base = { tag: p.name, server: p.server, server_port: p.port };
|
||||
switch (p.type) {
|
||||
case 'vless':
|
||||
return { outbound: clean(Object.assign(base, { type: 'vless', uuid: p.uuid, flow: p.flow, tls: tls(p), transport: transport(p) })) };
|
||||
case 'vmess':
|
||||
return { outbound: clean(Object.assign(base, { type: 'vmess', uuid: p.uuid, security: p.cipher, alter_id: p.alterId, tls: tls(p), transport: transport(p) })) };
|
||||
case 'trojan':
|
||||
return { outbound: clean(Object.assign(base, { type: 'trojan', password: p.password, tls: tls(p), transport: transport(p) })) };
|
||||
case 'ss':
|
||||
return { outbound: Object.assign(base, { type: 'shadowsocks', method: p.method, password: p.password }) };
|
||||
case 'hysteria2': {
|
||||
// sing-box не умеет закреплять отпечаток сертификата — при pinSHA256 проверку отключаем.
|
||||
const t = clean({ enabled: true, server_name: p.sni, alpn: p.alpn, insecure: (p.insecure || !!p.pinSHA256) || undefined });
|
||||
const o = clean(Object.assign(base, { type: 'hysteria2', password: p.password, tls: t }));
|
||||
if (p.obfs) o.obfs = { type: p.obfs, password: p.obfsPassword };
|
||||
return { outbound: o };
|
||||
}
|
||||
case 'tuic':
|
||||
return { outbound: clean(Object.assign(base, { type: 'tuic', uuid: p.uuid, password: p.password, congestion_control: p.congestion,
|
||||
udp_relay_mode: p.udpRelay, tls: clean({ enabled: true, server_name: p.sni, alpn: p.alpn, insecure: p.insecure || undefined }) })) };
|
||||
case 'wireguard':
|
||||
return { endpoint: clean({ type: 'wireguard', tag: p.name, address: p.address.map((a) => a + (a.includes(':') ? '/128' : '/32')),
|
||||
private_key: p.privateKey, mtu: p.mtu,
|
||||
peers: [clean({ address: p.server, port: p.port, public_key: p.publicKey, pre_shared_key: p.preSharedKey, allowed_ips: ['0.0.0.0/0', '::/0'], reserved: p.reserved || undefined })] }) };
|
||||
case 'amneziawg':
|
||||
throw new Error('sing-box не поддерживает AmneziaWG');
|
||||
default:
|
||||
throw new Error('sing-box не поддерживает ' + p.type);
|
||||
}
|
||||
}
|
||||
|
||||
// opts: { inbound: {type:'mixed', port} } — для тестов и десктопных клиентов.
|
||||
function build(proxies, opts) {
|
||||
opts = opts || {};
|
||||
const outbounds = [], endpoints = [], skipped = [];
|
||||
proxies.filter((p) => p.type !== 'mtproto').forEach((p) => {
|
||||
try {
|
||||
const r = convert(p);
|
||||
if (r.outbound) outbounds.push(r.outbound); else endpoints.push(r.endpoint);
|
||||
} catch (e) { skipped.push({ name: p.name, error: e.message }); }
|
||||
});
|
||||
const tags = outbounds.map((o) => o.tag).concat(endpoints.map((e) => e.tag));
|
||||
if (!tags.length) throw new Error(skipped.length ? skipped[0].error : 'Нет подходящих серверов для sing-box.');
|
||||
const cfg = { log: { level: 'warn' } };
|
||||
if (opts.inbound) cfg.inbounds = [{ type: 'mixed', tag: 'in', listen: '127.0.0.1', listen_port: opts.inbound.port }];
|
||||
cfg.outbounds = [{ type: 'selector', tag: 'Прокси', outbounds: ['Авто'].concat(tags) },
|
||||
{ type: 'urltest', tag: 'Авто', outbounds: tags, url: 'https://www.gstatic.com/generate_204', interval: '5m' }]
|
||||
.concat(outbounds, [{ type: 'direct', tag: 'direct' }]);
|
||||
if (endpoints.length) cfg.endpoints = endpoints;
|
||||
cfg.route = { final: opts.final || 'Прокси', auto_detect_interface: true };
|
||||
return { config: cfg, skipped, count: tags.length };
|
||||
}
|
||||
|
||||
const api = { buildSingbox: build, singboxConvert: convert };
|
||||
if (typeof module === 'object' && module.exports) module.exports = api;
|
||||
root.PM = Object.assign(root.PM || {}, api);
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
+25
-5
@@ -20,7 +20,8 @@
|
||||
st.rawSettings = { header: { type: 'http', request: { path: [t.path], headers: t.host ? { Host: [t.host] } : {} } } };
|
||||
}
|
||||
if (s.security === 'tls') {
|
||||
st.tlsSettings = clean({ serverName: s.sni, fingerprint: s.fp, alpn: s.alpn.length ? s.alpn : undefined, allowInsecure: s.insecure || undefined });
|
||||
// В 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 });
|
||||
@@ -53,10 +54,26 @@
|
||||
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(p.type === 'hysteria2'
|
||||
? 'Hysteria2 в XKeen работает через ядро Mihomo — возьмите генератор Mihomo'
|
||||
: 'протокол ' + p.type + ' не поддерживается Xray');
|
||||
throw new Error({ tuic: 'TUIC не поддерживается Xray — возьмите генератор Mihomo',
|
||||
amneziawg: 'AmneziaWG не поддерживается Xray — возьмите генератор Mihomo',
|
||||
mtproto: 'MTProto — это прокси для Telegram, в Xray он не нужен' }[p.type] || 'протокол ' + p.type + ' не поддерживается Xray');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +154,10 @@
|
||||
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));
|
||||
@@ -169,7 +189,7 @@
|
||||
'04_outbounds.json': Object.assign({ outbounds: outs }, obs || {}),
|
||||
'05_routing.json': rt,
|
||||
};
|
||||
return { files, skipped, count: outs.length - 2 };
|
||||
return { files, skipped, warnings, count: outs.length - 2 };
|
||||
}
|
||||
|
||||
const api = { buildXray: build, XRAY_SERVICES: SERVICES, XRAY_BASES: BASES, xrayServiceAvailable: available };
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<label class="field" for="sub">Или ссылка на подписку</label>
|
||||
<input type="url" id="sub" class="mono" spellcheck="false" placeholder="https://…">
|
||||
<p class="hint">Подписку роутер скачает сам и будет обновлять каждый час. Поддерживаются vless (REALITY, TLS, ws, gRPC, xhttp),
|
||||
vmess, trojan, shadowsocks и Hysteria2.</p>
|
||||
vmess, trojan, shadowsocks, Hysteria2, TUIC, WireGuard и AmneziaWG (ссылки wireguard:// и vpn:// из 3X-UI).</p>
|
||||
</section>
|
||||
|
||||
<section class="step">
|
||||
|
||||
@@ -61,7 +61,7 @@ test('подписка в base64 и ошибки по строкам', () => {
|
||||
const r = parseText(sub);
|
||||
assert.equal(r.proxies.length, 2);
|
||||
assert.equal(r.errors.length, 2);
|
||||
assert.match(r.errors[1].error, /tuic/);
|
||||
assert.match(r.errors[1].error, /TUIC/);
|
||||
});
|
||||
|
||||
test('одинаковые имена становятся уникальными', () => {
|
||||
@@ -83,8 +83,8 @@ test('Xray: теги, балансировщик и маршрутизация',
|
||||
const rules = one.files['05_routing.json'].routing.rules;
|
||||
assert.equal(rules[rules.length - 1].outboundTag, 'direct');
|
||||
const multi = buildXray(parseText([REALITY, WS_TLS, HY2].join('\n')).proxies, {});
|
||||
assert.equal(multi.count, 2);
|
||||
assert.equal(multi.skipped.length, 1);
|
||||
assert.equal(multi.count, 3);
|
||||
assert.equal(multi.skipped.length, 0);
|
||||
assert.ok(multi.files['05_routing.json'].routing.balancers);
|
||||
});
|
||||
|
||||
@@ -95,6 +95,30 @@ test('Xray: сервисы без нужной геобазы не попада
|
||||
assert.ok(s.includes('ext:zkeen.dat:youtube'));
|
||||
});
|
||||
|
||||
test('TUIC, WireGuard, AmneziaWG (vpn://) и MTProto', () => {
|
||||
const t = parseLink('tuic://d017584f-52ea-43c8-a420-9d559f27b96a:secret@t.example.com:8444?alpn=h3&congestion_control=bbr&udp_relay_mode=native#tuic');
|
||||
assert.equal(t.type, 'tuic');
|
||||
assert.equal(t.password, 'secret');
|
||||
const w = parseLink('wireguard://cHJpdg%3D%3D@w.example.com:51820?address=10.0.0.2%2F32&mtu=1420&publickey=cHVi#wg');
|
||||
assert.deepEqual(w.address, ['10.0.0.2']);
|
||||
const conf = '[Interface]\nPrivateKey = cHJpdg==\nAddress = 10.8.1.2/32\nJc = 4\nJmin = 40\nJmax = 70\nS1 = 20\nS2 = 30\nH1 = 11\nH2 = 22\nH3 = 33\nH4 = 44\n[Peer]\nPublicKey = cHVi\nEndpoint = a.example.com:51821\n';
|
||||
const a = parseLink('vpn://' + Buffer.from(conf).toString('base64'));
|
||||
assert.equal(a.type, 'amneziawg');
|
||||
assert.equal(a.awg.jc, 4);
|
||||
assert.equal(parseLink('tg://proxy?server=m.example.com&port=8445&secret=ee00').type, 'mtproto');
|
||||
assert.equal(parseLink('https://t.me/proxy?server=m.example.com&port=8445&secret=ee00').port, 8445);
|
||||
});
|
||||
|
||||
test('Xray 26: вместо allowInsecure — отпечаток или предупреждение', () => {
|
||||
const insecure = WS_TLS.replace('#', '&allowInsecure=1#');
|
||||
const r = buildXray(parseText(insecure).proxies, {});
|
||||
assert.ok(!JSON.stringify(r.files).includes('allowInsecure'));
|
||||
assert.equal(r.warnings.length, 1);
|
||||
const pinned = buildXray(parseText(WS_TLS.replace('#', '&pcs=AB:CD#')).proxies, {});
|
||||
assert.equal(pinned.files['04_outbounds.json'].outbounds[0].streamSettings.tlsSettings.pinnedPeerCertSha256, 'abcd');
|
||||
assert.equal(pinned.warnings.length, 0);
|
||||
});
|
||||
|
||||
test('Mihomo: порты XKeen и прокси', () => {
|
||||
const r = buildMihomo(parseText([REALITY, HY2, SS_2022].join('\n')).proxies, { secret: 'x' });
|
||||
assert.equal(r.config['redir-port'], 5000);
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
<h2><span class="num">1</span>Ссылки на серверы</h2>
|
||||
<textarea id="links" class="mono" spellcheck="false" placeholder="vless://… По одной на строку. Можно вставить подписку в base64 целиком."></textarea>
|
||||
<div class="parsed" id="parsed"></div>
|
||||
<p class="hint">Поддерживаются vless (REALITY, TLS, ws, gRPC, xhttp, httpupgrade), vmess, trojan и shadowsocks.
|
||||
Несколько серверов — Xray сам выберет самый быстрый. Hysteria2 работает через <a href="../mihomo/">Mihomo</a>.</p>
|
||||
<p class="hint">Поддерживаются vless (REALITY, TLS, ws, gRPC, xhttp, httpupgrade), vmess, trojan, shadowsocks, Hysteria2 и WireGuard.
|
||||
Несколько серверов — Xray сам выберет самый быстрый. TUIC и AmneziaWG работают через <a href="../mihomo/">Mihomo</a>.</p>
|
||||
</section>
|
||||
|
||||
<section class="step">
|
||||
@@ -154,6 +154,7 @@
|
||||
const notes = [];
|
||||
if (r.count > 1) notes.push('Серверов: ' + r.count + '. Xray будет каждую минуту проверять их и выбирать самый быстрый.');
|
||||
if (r.skipped.length) notes.push('Пропущено: ' + r.skipped.map((s) => s.name + ' — ' + s.error).join('; '));
|
||||
(r.warnings || []).forEach((w) => notes.push(w));
|
||||
$('#note').innerHTML = notes.map((n) => '<div class="msg warn">' + esc(n) + '</div>').join('');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user