Матрица протоколов 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:
@@ -0,0 +1,14 @@
|
||||
# Матрица «протокол × клиент»
|
||||
|
||||
Проверяет настоящим подключением, что каждый протокол 3X-UI работает с каждым клиентом:
|
||||
Xray, Mihomo, sing-box и официальным клиентом AmneziaWG. Прогонять на каждой новой версии 3X-UI и Xray.
|
||||
|
||||
1. Сервер: чистый Ubuntu в Docker с systemd, на нём `scripts/3x-ui.sh`, затем `mk-inbounds.sh` —
|
||||
создаёт через API подключения VLESS XHTTP, WS+TLS, Trojan gRPC, VMess, Shadowsocks 2022,
|
||||
Hysteria2, TUIC, WireGuard, AmneziaWG и MTProto.
|
||||
2. Ссылки: `GET /panel/api/inbounds/allLinks` → `links.txt`.
|
||||
3. Конфиги клиентов: `PIN=<sha256 сертификата> node matrix-gen.js ../../tools/lib links.txt cases`.
|
||||
4. Прогон в клиентском контейнере с бинарниками в `/cl`: `bash matrix-run.sh`.
|
||||
5. AmneziaWG: `awg-set.sh classic|mid|full` меняет обфускацию на сервере.
|
||||
|
||||
Результат на 2026-09-27 (3X-UI 3.8.5, Xray 26.6.27, Mihomo 1.19.31, sing-box 1.14.2) — в описании коммита.
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# awg-set.sh classic|full — выставляет обфускацию AmneziaWG на подключении и печатает ссылку.
|
||||
. /etc/x-ui/install-result.env
|
||||
API="http://127.0.0.1:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH/panel/api"; H=(-H "Authorization: Bearer $XUI_API_TOKEN" -H 'Content-Type: application/json')
|
||||
ID=$(curl -fsS "${H[@]}" "$API/inbounds/list" | jq -r '.obj[] | select(.protocol=="amneziawg") | .id')
|
||||
IN=$(curl -fsS "${H[@]}" "$API/inbounds/get/$ID" | jq -c .obj)
|
||||
r() { shuf -i "$1-$2" -n 1; }
|
||||
JMIN=$(r 40 89); S1=$(r 15 150); S2=$(r 15 150); while [[ $((S1+56)) == "$S2" ]]; do S2=$(r 15 150); done
|
||||
OBF=$(jq -nc --argjson jc "$(r 3 6)" --argjson jmin $JMIN --argjson jmax $((JMIN + $(r 50 250))) --argjson s1 $S1 --argjson s2 $S2 \
|
||||
--arg h1 "$(r 5 536870911)" --arg h2 "$(r 536870912 1073741823)" --arg h3 "$(r 1073741824 1610612735)" --arg h4 "$(r 1610612736 2147483647)" \
|
||||
'{jc:$jc, jmin:$jmin, jmax:$jmax, s1:$s1, s2:$s2, h1:$h1, h2:$h2, h3:$h3, h4:$h4}')
|
||||
if [[ $1 == mid ]]; then
|
||||
OBF=$(jq -c --argjson s3 "$(r 12 55)" --argjson s4 "$(r 12 27)" --arg i1 "<r $(r 32 256)>" ". + {s3:\$s3, s4:\$s4, i1:\$i1}" <<<"$OBF")
|
||||
fi
|
||||
if [[ $1 == full ]]; then
|
||||
OBF=$(jq -c --argjson s3 "$(r 12 55)" --argjson s4 "$(r 12 27)" --arg i1 "<r $(r 32 256)>" --arg hp "$(openssl rand -base64 32)" \
|
||||
'. + {s3:$s3, s4:$s4, i1:$i1, headerProtectionKey:$hp, contentPaddingAddition:"12-40", rekeyAfterTime:"110-140", rejectAfterTime:"180-250", rekeyTimeout:"4-7", keepaliveTimeout:"10-16", maxHandshakeAttempts:"20-40"}' <<<"$OBF")
|
||||
fi
|
||||
SET=$(jq -c --argjson o "$OBF" '(.settings | if type=="string" then fromjson else . end) | .server |= (del(.s3, .s4, .i1, .i2, .i3, .i4, .i5, .headerProtectionKey, .contentPaddingAddition, .rekeyAfterTime, .rejectAfterTime, .rekeyTimeout, .keepaliveTimeout, .maxHandshakeAttempts) + $o)' <<<"$IN")
|
||||
BODY=$(jq -c --arg s "$SET" '.settings = $s | .streamSettings = (.streamSettings | if type=="string" then . else tojson end) | .sniffing = (.sniffing | if type=="string" then . else tojson end)' <<<"$IN")
|
||||
curl -fsS "${H[@]}" -X POST -d "$BODY" "$API/inbounds/update/$ID" | jq -r '.success, (.msg|tostring)' | tr '\n' ' '; echo
|
||||
sleep 2
|
||||
curl -fsS "${H[@]}" -H "Host: xs" "$API/inbounds/allLinks" | jq -r '.obj[] | select(startswith("vpn://"))'
|
||||
@@ -0,0 +1,34 @@
|
||||
// Генерирует конфиги клиентов для каждой пары «протокол × клиент».
|
||||
const fs = require('fs'), path = require('path');
|
||||
const [lib, linksFile, out] = process.argv.slice(2);
|
||||
const PIN = process.env.PIN || ''; // отпечаток тестового сертификата сервера
|
||||
const L = require(lib + '/links.js'), X = require(lib + '/xray.js'), M = require(lib + '/mihomo.js'), SB = require(lib + '/singbox.js');
|
||||
const { proxies } = L.parseText(fs.readFileSync(linksFile, 'utf8'));
|
||||
fs.rmSync(out, { recursive: true, force: true });
|
||||
const rows = [];
|
||||
proxies.filter((p) => p.type !== 'mtproto').forEach((p, i) => {
|
||||
// Тестовый сервер с самоподписанным сертификатом — разрешаем клиентам его принять.
|
||||
if (p.tls && p.tls.security === 'tls') { p.tls.insecure = true; p.tls.pin = PIN; }
|
||||
if (p.type === 'hysteria2' || p.type === 'tuic') p.insecure = true;
|
||||
if (p.type === 'hysteria2') p.pinSHA256 = PIN;
|
||||
const id = String(i).padStart(2, '0') + '-' + p.name.replace(/[^a-z0-9-]/gi, '').slice(0, 24);
|
||||
const dir = path.join(out, id);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const res = { id, type: p.type, name: p.name };
|
||||
try {
|
||||
const x = X.buildXray([p], { finalProxy: true });
|
||||
const o = x.files['04_outbounds.json'];
|
||||
o.inbounds = [{ listen: '127.0.0.1', port: 1080, protocol: 'socks', settings: { udp: true } }];
|
||||
o.log = { loglevel: 'warning' };
|
||||
fs.writeFileSync(path.join(dir, 'xray.json'), JSON.stringify(o));
|
||||
} catch (e) { res.xray = 'n/a: ' + e.message; }
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, 'mihomo.yaml'), M.buildMihomo([p], { finalProxy: true, secret: 't', services: [] }).yaml + 'mixed-port: 1081\n');
|
||||
} catch (e) { res.mihomo = 'n/a: ' + e.message; }
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, 'singbox.json'), JSON.stringify(SB.buildSingbox([p], { inbound: { port: 1082 } }).config));
|
||||
} catch (e) { res.singbox = 'n/a: ' + e.message; }
|
||||
rows.push(res);
|
||||
});
|
||||
fs.writeFileSync(path.join(out, 'index.json'), JSON.stringify(rows, null, 1));
|
||||
console.log(rows.map((r) => r.id + (r.xray ? ' [xray ' + r.xray + ']' : '') + (r.singbox ? ' [sb ' + r.singbox + ']' : '')).join('\n'));
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# Прогон матрицы: каждый конфиг — отдельный клиент, через него открываем сайт.
|
||||
cd /lab/cases
|
||||
t() { # client port cmd...
|
||||
local port=$1; shift
|
||||
("$@" >/tmp/c.log 2>&1 &)
|
||||
local code=000
|
||||
for i in 1 2 3 4 5 6; do
|
||||
sleep 1
|
||||
code=$(curl -s -m 8 -x socks5h://127.0.0.1:$port -o /dev/null -w '%{http_code}' https://www.google.com/generate_204)
|
||||
[[ $code == 204 ]] && break
|
||||
done
|
||||
pkill -x xray; pkill -x mihomo; pkill -x sing-box; sleep 0.5
|
||||
if [[ $code == 204 ]]; then echo "✓"; else echo "✗ $(grep -i -m1 -E 'error|fail|fatal|invalid' /tmp/c.log | cut -c1-90)"; fi
|
||||
}
|
||||
printf '%-32s %-40s %-40s %s\n' "протокол" "Xray 26.6.27" "Mihomo" "sing-box"
|
||||
for d in */; do
|
||||
d=${d%/}
|
||||
x='—'; m='—'; s='—'
|
||||
[[ -f $d/xray.json ]] && x=$(t 1080 /cl/xray run -c $d/xray.json)
|
||||
[[ -f $d/mihomo.yaml ]] && m=$(t 1081 /cl/mihomo -d /tmp/mh -f $d/mihomo.yaml)
|
||||
[[ -f $d/singbox.json ]] && s=$(t 1082 /cl/sing-box run -c $d/singbox.json)
|
||||
printf '%-32s %-40s %-40s %s\n' "$d" "$x" "$m" "$s"
|
||||
done
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
# Создаёт по подключению на каждый протокол через API 3x-ui. Запускать на сервере.
|
||||
set -uo pipefail
|
||||
. /etc/x-ui/install-result.env
|
||||
API="http://127.0.0.1:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH/panel/api"
|
||||
H=(-H "Authorization: Bearer $XUI_API_TOKEN" -H 'Content-Type: application/json')
|
||||
mkdir -p /root/cert
|
||||
[[ -f /root/cert/crt ]] || openssl req -x509 -nodes -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -keyout /root/cert/key -out /root/cert/crt -subj /CN=test.pm -addext subjectAltName=DNS:test.pm -days 365 2>/dev/null
|
||||
chmod 644 /root/cert/*
|
||||
uuid() { cat /proc/sys/kernel/random/uuid; }
|
||||
b64k() { openssl rand -base64 "$1"; }
|
||||
cb() { jq -nc --arg e "$1" '{email:$e, limitIp:0, totalGB:0, expiryTime:0, enable:true, tgId:0, subId:$e, comment:"", reset:0}'; }
|
||||
TLS=$(jq -nc '{serverName:"test.pm", certificates:[{certificateFile:"/root/cert/crt", keyFile:"/root/cert/key"}]}')
|
||||
KEYS=$(curl -fsS "${H[@]}" "$API/server/getNewX25519Cert" | jq -c .obj)
|
||||
|
||||
add() { # remark port protocol settings stream
|
||||
local body r
|
||||
body=$(jq -nc --arg rm "$1" --argjson port "$2" --arg p "$3" --arg s "$4" --arg st "$5" \
|
||||
'{remark:$rm, enable:true, listen:"", port:$port, protocol:$p, settings:$s, streamSettings:$st,
|
||||
sniffing:"{\"enabled\":true,\"destOverride\":[\"http\",\"tls\",\"quic\"]}", expiryTime:0, total:0}')
|
||||
r=$(curl -sS "${H[@]}" -X POST -d "$body" "$API/inbounds/add")
|
||||
printf '%-22s %s\n' "$1" "$(jq -r 'if .success then "создан" else "ОШИБКА: " + (.msg|tostring) end' <<<"$r" | head -c 300)"
|
||||
}
|
||||
|
||||
add vless-xhttp-reality 8443 vless \
|
||||
"$(jq -nc --arg id "$(uuid)" --argjson c "$(cb t-xhttp)" '{clients:[$c + {id:$id, flow:""}], decryption:"none"}')" \
|
||||
"$(jq -nc --argjson k "$KEYS" --arg sid "$(openssl rand -hex 8)" '{network:"xhttp", security:"reality", xhttpSettings:{path:"/x", mode:"auto"},
|
||||
realitySettings:{target:"dl.google.com:443", serverNames:["dl.google.com"], privateKey:$k.privateKey, shortIds:[$sid], settings:{publicKey:$k.publicKey, fingerprint:"chrome", spiderX:"/"}}}')"
|
||||
|
||||
add vless-ws-tls 2053 vless \
|
||||
"$(jq -nc --arg id "$(uuid)" --argjson c "$(cb t-ws)" '{clients:[$c + {id:$id, flow:""}], decryption:"none"}')" \
|
||||
"$(jq -nc --argjson t "$TLS" '{network:"ws", security:"tls", wsSettings:{path:"/ws"}, tlsSettings:($t + {alpn:["http/1.1"]})}')"
|
||||
|
||||
add trojan-grpc-tls 2083 trojan \
|
||||
"$(jq -nc --argjson c "$(cb t-trojan)" --arg pw "$(openssl rand -hex 8)" '{clients:[$c + {password:$pw}]}')" \
|
||||
"$(jq -nc --argjson t "$TLS" '{network:"grpc", security:"tls", grpcSettings:{serviceName:"gun"}, tlsSettings:($t + {alpn:["h2"]})}')"
|
||||
|
||||
add vmess-ws-tls 2087 vmess \
|
||||
"$(jq -nc --arg id "$(uuid)" --argjson c "$(cb t-vmess)" '{clients:[$c + {id:$id, security:"auto", alterId:0}]}')" \
|
||||
"$(jq -nc --argjson t "$TLS" '{network:"ws", security:"tls", wsSettings:{path:"/vm"}, tlsSettings:($t + {alpn:["http/1.1"]})}')"
|
||||
|
||||
add shadowsocks-2022 8388 shadowsocks \
|
||||
"$(jq -nc --arg pw "$(b64k 16)" --arg upw "$(b64k 16)" --argjson c "$(cb t-ss)" '{method:"2022-blake3-aes-128-gcm", password:$pw, network:"tcp,udp", clients:[$c + {method:"", password:$upw}]}')" \
|
||||
'{"network":"tcp","security":"none"}'
|
||||
|
||||
add hysteria2 443 hysteria \
|
||||
"$(jq -nc --argjson c "$(cb t-hy2)" --arg a "$(openssl rand -hex 8)" '{version:2, clients:[$c + {auth:$a}]}')" \
|
||||
"$(jq -nc --argjson t "$TLS" '{network:"hysteria", hysteriaSettings:{version:2}, security:"tls", tlsSettings:($t + {alpn:["h3"]})}')"
|
||||
|
||||
add tuic 8444 tuic \
|
||||
"$(jq -nc --arg id "$(uuid)" --arg pw "$(openssl rand -hex 8)" --argjson c "$(cb t-tuic)" '{server:{certificate:"/root/cert/crt", private_key:"/root/cert/key", congestion_control:"bbr", alpn:["h3"], udp_relay_mode:"native", zero_rtt_handshake:false, log_level:"info", sni:""}, clients:[$c + {uuid:$id, id:$id, password:$pw}]}')" \
|
||||
'{}'
|
||||
|
||||
wgkey() { /usr/local/x-ui/bin/xray-linux-* wg | awk -v w="$1" 'tolower($0) ~ w {print $NF}'; }
|
||||
WGK=$(/usr/local/x-ui/bin/xray-linux-* wg); WPRIV=$(awk '/Private/ {print $NF}' <<<"$WGK"); WPUB=$(awk '/Public|Password/ {print $NF}' <<<"$WGK" | head -1)
|
||||
AGK=$(/usr/local/x-ui/bin/xray-linux-* wg); APRIV=$(awk '/Private/ {print $NF}' <<<"$AGK"); APUB=$(awk '/Public|Password/ {print $NF}' <<<"$AGK" | head -1)
|
||||
add wireguard 51820 wireguard \
|
||||
"$(jq -nc --arg k "$(/usr/local/x-ui/bin/xray-linux-* wg | awk '/Private/ {print $NF}')" --arg pr "$WPRIV" --arg pu "$WPUB" --argjson c "$(cb t-wg)" '{mtu:1420, secretKey:$k, peers:[], clients:[$c + {privateKey:$pr, publicKey:$pu, allowedIPs:["10.0.0.2/32"]}], subnetIp:"10.0.0.0", subnetCidr:24}')" \
|
||||
'{"network":"tcp","security":"none"}'
|
||||
|
||||
add amneziawg 51821 amneziawg \
|
||||
"$(jq -nc --arg pr "$APRIV" --arg pu "$APUB" --argjson c "$(cb t-awg)" '{server:{subnetIp:"10.8.1.0", subnetCidr:24, primaryDns:"8.8.8.8", secondaryDns:"8.8.4.4"}, clients:[$c + {privateKey:$pr, publicKey:$pu, allowedIPs:["10.8.1.2/32"]}]}')" \
|
||||
'{}'
|
||||
|
||||
add mtproto 8445 mtproto \
|
||||
"$(jq -nc --argjson c "$(cb t-mtproto)" '{fakeTlsDomain:"www.cloudflare.com", clients:[$c + {secret:""}]}')" \
|
||||
'{}'
|
||||
+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