Fix cascade silent dead internet with per-step settings and handshake checks.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+150
-3
@@ -522,6 +522,84 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-hint" style="margin-bottom:var(--space-md);">{{ _('cascade_hint') }}</div>
|
||||
|
||||
<details id="cascadeSettingsPanel" open style="margin-bottom:var(--space-md); border:1px solid var(--border); border-radius:var(--radius-md); padding:var(--space-sm) var(--space-md);">
|
||||
<summary style="cursor:pointer; font-weight:600; margin-bottom:var(--space-sm);">{{ _('cascade_settings_title') }}</summary>
|
||||
<p class="form-hint" style="margin:0 0 var(--space-md);">{{ _('cascade_settings_desc') }}</p>
|
||||
<div class="grid" style="display:grid; grid-template-columns:1fr 1fr; gap:var(--space-sm) var(--space-md);">
|
||||
<label class="form-check" style="display:flex; gap:var(--space-sm); align-items:flex-start;">
|
||||
<input type="checkbox" id="cascadePinExitRoute" checked>
|
||||
<span>
|
||||
<strong>{{ _('cascade_opt_pin_exit') }}</strong>
|
||||
<span class="form-hint" style="display:block;">{{ _('cascade_opt_pin_exit_hint') }}</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="form-check" style="display:flex; gap:var(--space-sm); align-items:flex-start;">
|
||||
<input type="checkbox" id="cascadeRemoveEthMasq" checked>
|
||||
<span>
|
||||
<strong>{{ _('cascade_opt_remove_eth_masq') }}</strong>
|
||||
<span class="form-hint" style="display:block;">{{ _('cascade_opt_remove_eth_masq_hint') }}</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="form-check" style="display:flex; gap:var(--space-sm); align-items:flex-start;">
|
||||
<input type="checkbox" id="cascadeForceForward" checked>
|
||||
<span>
|
||||
<strong>{{ _('cascade_opt_force_forward') }}</strong>
|
||||
<span class="form-hint" style="display:block;">{{ _('cascade_opt_force_forward_hint') }}</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="form-check" style="display:flex; gap:var(--space-sm); align-items:flex-start;">
|
||||
<input type="checkbox" id="cascadeMssClamp" checked>
|
||||
<span>
|
||||
<strong>{{ _('cascade_opt_mss_clamp') }}</strong>
|
||||
<span class="form-hint" style="display:block;">{{ _('cascade_opt_mss_clamp_hint') }}</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="form-check" style="display:flex; gap:var(--space-sm); align-items:flex-start;">
|
||||
<input type="checkbox" id="cascadeWaitHandshake" checked>
|
||||
<span>
|
||||
<strong>{{ _('cascade_opt_wait_handshake') }}</strong>
|
||||
<span class="form-hint" style="display:block;">{{ _('cascade_opt_wait_handshake_hint') }}</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="form-check" style="display:flex; gap:var(--space-sm); align-items:flex-start;">
|
||||
<input type="checkbox" id="cascadeKeepExitDns">
|
||||
<span>
|
||||
<strong>{{ _('cascade_opt_keep_dns') }}</strong>
|
||||
<span class="form-hint" style="display:block;">{{ _('cascade_opt_keep_dns_hint') }}</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid" style="display:grid; grid-template-columns:1fr 1fr 1fr; gap:var(--space-md); margin-top:var(--space-md);">
|
||||
<div class="form-group" style="margin:0;">
|
||||
<label class="form-label" for="cascadeHandshakeTimeout">{{ _('cascade_opt_handshake_timeout') }}</label>
|
||||
<input type="number" class="form-input" id="cascadeHandshakeTimeout" min="5" max="120" value="25">
|
||||
</div>
|
||||
<div class="form-group" style="margin:0;">
|
||||
<label class="form-label" for="cascadeTableId">{{ _('cascade_opt_table_id') }}</label>
|
||||
<input type="number" class="form-input" id="cascadeTableId" min="1" max="252" value="200">
|
||||
</div>
|
||||
<div class="form-group" style="margin:0;">
|
||||
<label class="form-label" for="cascadeRulePriority">{{ _('cascade_opt_rule_priority') }}</label>
|
||||
<input type="number" class="form-input" id="cascadeRulePriority" min="1" max="32765" value="100">
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid" style="display:grid; grid-template-columns:1fr 1fr; gap:var(--space-md); margin-top:var(--space-md);">
|
||||
<div class="form-group" style="margin:0;">
|
||||
<label class="form-label" for="cascadeAllowedIps">{{ _('cascade_opt_allowed_ips') }}</label>
|
||||
<input type="text" class="form-input" id="cascadeAllowedIps" value="0.0.0.0/0, ::/0">
|
||||
<span class="form-hint">{{ _('cascade_opt_allowed_ips_hint') }}</span>
|
||||
</div>
|
||||
<div class="form-group" style="margin:0;">
|
||||
<label class="form-label" for="cascadeVpnSubnet">{{ _('cascade_opt_vpn_subnet') }}</label>
|
||||
<input type="text" class="form-input" id="cascadeVpnSubnet" placeholder="10.8.1.0/24" value="">
|
||||
<span class="form-hint">{{ _('cascade_opt_vpn_subnet_hint') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div id="cascadeDiagBox" class="hidden" style="margin-bottom:var(--space-md); padding:var(--space-sm) var(--space-md); background:var(--bg-muted, rgba(0,0,0,.04)); border-radius:var(--radius-md); font-family:ui-monospace,monospace; font-size:12px; white-space:pre-wrap; max-height:160px; overflow:auto;"></div>
|
||||
|
||||
<div class="flex gap-sm" style="flex-wrap:wrap;">
|
||||
<button type="button" class="btn btn-primary" id="cascadeEnableBtn" onclick="setupCascade(true)">
|
||||
{{ _('cascade_enable') }}
|
||||
@@ -2703,15 +2781,78 @@
|
||||
if (dis) dis.disabled = !!on;
|
||||
}
|
||||
|
||||
function cascadeBool(id, fallback) {
|
||||
const el = document.getElementById(id);
|
||||
return el ? !!el.checked : !!fallback;
|
||||
}
|
||||
|
||||
function cascadeNum(id, fallback) {
|
||||
const el = document.getElementById(id);
|
||||
const n = el ? parseInt(el.value, 10) : NaN;
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
|
||||
function collectCascadeSettings() {
|
||||
return {
|
||||
pin_exit_route: cascadeBool('cascadePinExitRoute', true),
|
||||
remove_eth_masquerade: cascadeBool('cascadeRemoveEthMasq', true),
|
||||
force_forward: cascadeBool('cascadeForceForward', true),
|
||||
mss_clamp: cascadeBool('cascadeMssClamp', true),
|
||||
wait_handshake: cascadeBool('cascadeWaitHandshake', true),
|
||||
keep_exit_dns: cascadeBool('cascadeKeepExitDns', false),
|
||||
handshake_timeout_sec: cascadeNum('cascadeHandshakeTimeout', 25),
|
||||
table_id: cascadeNum('cascadeTableId', 200),
|
||||
rule_priority: cascadeNum('cascadeRulePriority', 100),
|
||||
allowed_ips: (document.getElementById('cascadeAllowedIps') || {}).value || '0.0.0.0/0, ::/0',
|
||||
vpn_subnet_override: (document.getElementById('cascadeVpnSubnet') || {}).value || '',
|
||||
};
|
||||
}
|
||||
|
||||
function applyCascadeSettingsToForm(settings, defaults) {
|
||||
const s = Object.assign({}, defaults || {}, settings || {});
|
||||
const setCheck = (id, val) => { const el = document.getElementById(id); if (el) el.checked = !!val; };
|
||||
const setVal = (id, val) => { const el = document.getElementById(id); if (el && val !== undefined && val !== null) el.value = val; };
|
||||
setCheck('cascadePinExitRoute', s.pin_exit_route !== false);
|
||||
setCheck('cascadeRemoveEthMasq', s.remove_eth_masquerade !== false);
|
||||
setCheck('cascadeForceForward', s.force_forward !== false);
|
||||
setCheck('cascadeMssClamp', s.mss_clamp !== false);
|
||||
setCheck('cascadeWaitHandshake', s.wait_handshake !== false);
|
||||
setCheck('cascadeKeepExitDns', !!s.keep_exit_dns);
|
||||
setVal('cascadeHandshakeTimeout', s.handshake_timeout_sec ?? 25);
|
||||
setVal('cascadeTableId', s.table_id ?? 200);
|
||||
setVal('cascadeRulePriority', s.rule_priority ?? 100);
|
||||
setVal('cascadeAllowedIps', s.allowed_ips || '0.0.0.0/0, ::/0');
|
||||
setVal('cascadeVpnSubnet', s.vpn_subnet_override || '');
|
||||
}
|
||||
|
||||
function updateCascadeDiag(cascade, live) {
|
||||
const box = document.getElementById('cascadeDiagBox');
|
||||
if (!box) return;
|
||||
const parts = [];
|
||||
if (cascade && cascade.enabled) {
|
||||
parts.push('handshake: ' + ((live && live.handshake) || cascade.handshake ? 'ok' : 'NONE'));
|
||||
if (cascade.subnet || (live && live.raw)) { /* keep */ }
|
||||
if (cascade.diagnostics) parts.push(String(cascade.diagnostics).trim());
|
||||
else if (live && live.raw) parts.push(String(live.raw).trim());
|
||||
if (cascade.last_error) parts.push('error: ' + cascade.last_error);
|
||||
}
|
||||
const text = parts.filter(Boolean).join('\n\n');
|
||||
box.textContent = text;
|
||||
box.classList.toggle('hidden', !text);
|
||||
}
|
||||
|
||||
function updateCascadeBadge(cascade, live) {
|
||||
const badge = document.getElementById('cascadeStatusBadge');
|
||||
if (!badge) return;
|
||||
const enabled = !!(cascade && cascade.enabled);
|
||||
const up = !!(live && live.up);
|
||||
badge.className = 'badge ' + (enabled && up ? 'badge-success' : (enabled ? 'badge-warn' : 'badge-warn'));
|
||||
const hs = !!(live && live.handshake) || !!(cascade && cascade.handshake);
|
||||
badge.className = 'badge ' + (enabled && up && hs ? 'badge-success' : (enabled ? 'badge-warn' : 'badge-warn'));
|
||||
if (!enabled) badge.textContent = _('cascade_off');
|
||||
else if (up) badge.textContent = _('cascade_on');
|
||||
else if (up && hs) badge.textContent = _('cascade_on');
|
||||
else if (up && !hs) badge.textContent = _('cascade_no_handshake');
|
||||
else badge.textContent = _('cascade_configured_down');
|
||||
updateCascadeDiag(cascade, live);
|
||||
}
|
||||
|
||||
async function loadCascadeStatus() {
|
||||
@@ -2729,6 +2870,7 @@
|
||||
exitProto.value = c.exit_protocol;
|
||||
}
|
||||
}
|
||||
applyCascadeSettingsToForm(c.settings || {}, res.defaults || {});
|
||||
updateCascadeBadge(c, res.live || {});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -2749,13 +2891,18 @@
|
||||
setCascadeBusy(true, enable ? _('cascade_working') : _('cascade_disabling'));
|
||||
showToast(enable ? _('cascade_working') : _('cascade_disabling'), 'info');
|
||||
try {
|
||||
const settings = collectCascadeSettings();
|
||||
const res = await apiCall(`/api/servers/${SERVER_ID}/cascade/setup`, 'POST', {
|
||||
enabled: !!enable,
|
||||
entry_protocol: entryProtocol,
|
||||
exit_server_id: parseInt(exitServerId || '0', 10),
|
||||
exit_protocol: exitProtocol,
|
||||
...settings,
|
||||
});
|
||||
updateCascadeBadge(res.cascade || {}, {
|
||||
up: !!(res.cascade && res.cascade.up),
|
||||
handshake: !!(res.cascade && res.cascade.handshake),
|
||||
});
|
||||
updateCascadeBadge(res.cascade || {}, { up: !!(res.cascade && res.cascade.up) });
|
||||
showToast(enable ? _('cascade_enabled') : _('cascade_disabled'), 'success');
|
||||
await loadCascadeStatus();
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user