feat(gui): TTL-desync toggle in client settings

Adds a TTL Desync toggle to the obfuscation section of the desktop client
settings, wired the same way as the Junk/TCP-fragmentation toggles: persisted in
client settings, applied into transport.ttl_desync in the built config, included
in the hot-reload fingerprint so flipping it while connected re-applies, and
labelled in both EN and RU. ttl_desync_auto is passed true, so the engine
measures the hop distance and aims the decoys itself — the user just flips it on.
This commit is contained in:
ospab 2026-08-19 19:58:18 +03:00
parent 1e9111ab9f
commit bfd079ff48
3 changed files with 24 additions and 3 deletions

View File

@ -28,6 +28,8 @@ const translations = {
label_kill_switch: 'Kill Switch', label_kill_switch: 'Kill Switch',
kill_switch_hint: 'Block non-VPN traffic when tunnel drops', kill_switch_hint: 'Block non-VPN traffic when tunnel drops',
label_transport: 'Transport Protocol', label_transport: 'Transport Protocol',
label_ttl_desync: 'TTL Desync',
hint_ttl_desync: 'Decoy packets that die before the server (UDP, auto-tuned)',
label_mtu: 'MTU Size', label_mtu: 'MTU Size',
label_transport: 'Transport Protocol', label_transport: 'Transport Protocol',
label_sni: 'Stealth SNI (Fake Host)', label_sni: 'Stealth SNI (Fake Host)',
@ -93,6 +95,8 @@ const translations = {
label_kill_switch: 'Kill Switch', label_kill_switch: 'Kill Switch',
kill_switch_hint: 'Блокировать трафик вне VPN при обрыве связи', kill_switch_hint: 'Блокировать трафик вне VPN при обрыве связи',
label_transport: 'Транспортный протокол', label_transport: 'Транспортный протокол',
label_ttl_desync: 'TTL-десинхронизация',
hint_ttl_desync: 'Пакеты-приманки, умирающие до сервера (UDP, авто-настройка)',
label_mtu: 'Размер MTU', label_mtu: 'Размер MTU',
label_transport: 'Транспортный протокол', label_transport: 'Транспортный протокол',
label_sni: 'Маскировочный SNI', label_sni: 'Маскировочный SNI',

View File

@ -336,6 +336,17 @@
</div> </div>
</div> </div>
<div class="toggle-row" style="border-top:none;">
<div class="toggle-text">
<span class="toggle-name" data-i18n="label_ttl_desync">TTL Desync</span>
<span class="toggle-hint" data-i18n="hint_ttl_desync">Decoy packets that die before the server (UDP, auto-tuned)</span>
</div>
<label class="toggle">
<input type="checkbox" id="cs-ttl-desync" />
<span class="toggle-track"><span class="toggle-thumb"></span></span>
</label>
</div>
</div> <!-- client-settings-card --> </div> <!-- client-settings-card -->
<div class="app-version" id="app-version">OSTP GUI</div> <div class="app-version" id="app-version">OSTP GUI</div>

View File

@ -161,6 +161,7 @@ const btnJunkDone = $('btn-junk-done');
const inTcpFrag = $('cs-tcp-frag'); const inTcpFrag = $('cs-tcp-frag');
const btnFragSettings = $('btn-frag-settings'); const btnFragSettings = $('btn-frag-settings');
const inTtlDesync = $('cs-ttl-desync');
const fragModal = $('frag-modal'); const fragModal = $('frag-modal');
const inFragChunk = $('cs-frag-chunk'); const inFragChunk = $('cs-frag-chunk');
const inFragSleep = $('cs-frag-sleep'); const inFragSleep = $('cs-frag-sleep');
@ -318,7 +319,9 @@ function buildConfig() {
frag_chunk: s.tcpFrag ? (s.fragChunk || 2) : (active.frag_chunk || 2), frag_chunk: s.tcpFrag ? (s.fragChunk || 2) : (active.frag_chunk || 2),
frag_sleep: s.tcpFrag ? (!isNaN(parseInt(s.fragSleep)) ? s.fragSleep : 2) : (active.frag_sleep !== undefined ? active.frag_sleep : 2), frag_sleep: s.tcpFrag ? (!isNaN(parseInt(s.fragSleep)) ? s.fragSleep : 2) : (active.frag_sleep !== undefined ? active.frag_sleep : 2),
junk_pc: s.junkEnabled ? [s.junkPcMin || 2, s.junkPcMax || 5] : (active.junk_pc || [2, 5]), junk_pc: s.junkEnabled ? [s.junkPcMin || 2, s.junkPcMax || 5] : (active.junk_pc || [2, 5]),
junk_ps: s.junkEnabled ? [s.junkPsMin || 100, s.junkPsMax || 1000] : (active.junk_ps || [100, 1000]) junk_ps: s.junkEnabled ? [s.junkPsMin || 100, s.junkPsMax || 1000] : (active.junk_ps || [100, 1000]),
ttl_desync: !!s.ttlDesync,
ttl_desync_auto: true
}, },
tun: { tun: {
enable: !!s.tun, enable: !!s.tun,
@ -657,6 +660,7 @@ function loadSettingsIntoForm() {
inTcpFrag.checked = !!s.tcpFrag; inTcpFrag.checked = !!s.tcpFrag;
inFragChunk.value = s.fragChunk || 2; inFragChunk.value = s.fragChunk || 2;
inFragSleep.value = !isNaN(parseInt(s.fragSleep)) ? s.fragSleep : 2; inFragSleep.value = !isNaN(parseInt(s.fragSleep)) ? s.fragSleep : 2;
if (inTtlDesync) inTtlDesync.checked = !!s.ttlDesync;
updateClientVisibility(); updateClientVisibility();
} }
@ -692,6 +696,7 @@ function collectAndSaveSettings() {
tcpFrag: inTcpFrag.checked, tcpFrag: inTcpFrag.checked,
fragChunk: parseInt(inFragChunk.value) || 2, fragChunk: parseInt(inFragChunk.value) || 2,
fragSleep: !isNaN(parseInt(inFragSleep.value)) ? parseInt(inFragSleep.value) : 2, fragSleep: !isNaN(parseInt(inFragSleep.value)) ? parseInt(inFragSleep.value) : 2,
ttlDesync: inTtlDesync ? inTtlDesync.checked : false,
}; };
// Cheap and local: safe to run on every debounced keystroke. // Cheap and local: safe to run on every debounced keystroke.
saveClientSettings(s); saveClientSettings(s);
@ -715,7 +720,7 @@ function collectAndSaveSettings() {
const tunnelRelevant = JSON.stringify([ const tunnelRelevant = JSON.stringify([
s.tun, s.killSwitch, s.mux, s.muxSessions, s.mtu, s.dns, s.socks, s.tun, s.killSwitch, s.mux, s.muxSessions, s.mtu, s.dns, s.socks,
s.exDomains, s.exIps, s.exProcs, s.junkEnabled, s.junkPcMin, s.junkPcMax, s.exDomains, s.exIps, s.exProcs, s.junkEnabled, s.junkPcMin, s.junkPcMax,
s.junkPsMin, s.junkPsMax, s.tcpFrag, s.fragChunk, s.fragSleep, s.junkPsMin, s.junkPsMax, s.tcpFrag, s.fragChunk, s.fragSleep, s.ttlDesync,
]); ]);
if (tunnelRelevant !== lastAppliedTunnelConfig) { if (tunnelRelevant !== lastAppliedTunnelConfig) {
clearTimeout(hotReloadTimer); clearTimeout(hotReloadTimer);
@ -901,7 +906,8 @@ window.addEventListener('DOMContentLoaded', async () => {
wintunModal.addEventListener('click', e => { if (e.target === wintunModal) wintunModal.classList.add('hidden'); }); wintunModal.addEventListener('click', e => { if (e.target === wintunModal) wintunModal.classList.add('hidden'); });
// Client settings — wire all inputs // Client settings — wire all inputs
[inTun, inKillSwitch, inMux, inAutoconnect, inLaunchStartup, inDebug, inShowRtt, inShowSpeed, inJunkEnabled, inTcpFrag] [inTun, inKillSwitch, inMux, inAutoconnect, inLaunchStartup, inDebug, inShowRtt, inShowSpeed, inJunkEnabled, inTcpFrag, inTtlDesync]
.filter(Boolean)
.forEach(el => el.addEventListener('change', collectAndSaveSettings)); .forEach(el => el.addEventListener('change', collectAndSaveSettings));
[inMuxSessions, inMtu, inDns, inSocks, inExDomains, inExIps, inExProcs, inJunkPcMin, inJunkPcMax, inJunkPsMin, inJunkPsMax, inFragChunk, inFragSleep] [inMuxSessions, inMtu, inDns, inSocks, inExDomains, inExIps, inExProcs, inJunkPcMin, inJunkPcMax, inJunkPsMin, inJunkPsMax, inFragChunk, inFragSleep]
.forEach(el => { .forEach(el => {