]> Git [git.vados.ru] :: Repos - Mikrotiks/ros.vados.ru.git/commitdiff
Autocommit in repo: ros.vados.ru, generated by owner: root, in 14:15:04 14.08.2026.
authorvados-dev <192440777+vados-dev@users.noreply.github.com>
Fri, 14 Aug 2026 11:15:04 +0000 (14:15 +0300)
committervados-dev <192440777+vados-dev@users.noreply.github.com>
Fri, 14 Aug 2026 11:15:04 +0000 (14:15 +0300)
html/AM-GlobalFunc.rsc
html/amneziawg/configurator/configurator-old1.html [new file with mode: 0644]
html/amneziawg/configurator/configurator.html
html/amneziawg/configurator/configurator3.0.html [new file with mode: 0644]

index 744e1e481dbc79f20df1b67fc00fef72b5285e5d..15898ad0726adfd17b29e1be8d4bb697fea470f6 100644 (file)
@@ -17,9 +17,9 @@
 :global GlobalFuncReady false;
 
 # Git commit id & info, expected configuration version
-:global CommitId "main/8401/39191990";
+:global CommitId "main/8402/c7fbdc63";
 :global CommitTAG "unknown";
-:global CommitInfo "Autocommit in repo: ros.vados.ru, generated by owner: root, in 14:10:03 14.08.2026.";
+:global CommitInfo "Autocommit in repo: ros.vados.ru, generated by owner: root, in 14:15:04 14.08.2026.";
 
 # The variables are received right here.
 :global GlobalConfVersion 140;
diff --git a/html/amneziawg/configurator/configurator-old1.html b/html/amneziawg/configurator/configurator-old1.html
new file mode 100644 (file)
index 0000000..9402c49
--- /dev/null
@@ -0,0 +1,5241 @@
+<!DOCTYPE html>
+<html lang="ru">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>AWG Proxy — Offline MikroTik Command Generator</title>
+    <style>
+        *, *::before, *::after {
+            box-sizing: border-box;
+            margin: 0;
+            padding: 0;
+        }
+
+        :root {
+            --bg: #f5f6f8;
+            --surface: #ffffff;
+            --border: #d1d5db;
+            --border-focus: #6366f1;
+            --text: #111827;
+            --text-muted: #6b7280;
+            --accent: #4f46e5;
+            --accent-hover: #4338ca;
+            --success: #059669;
+            --success-bg: #ecfdf5;
+            --error: #dc2626;
+            --error-bg: #fef2f2;
+            --tag-ok: #d1fae5;
+            --tag-ok-text: #065f46;
+            --tag-miss: #fee2e2;
+            --tag-miss-text: #991b1b;
+            --mono: 'Cascadia Code', 'Fira Code', 'Consolas', 'Courier New', monospace;
+        }
+
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+            background: var(--bg);
+            color: var(--text);
+            line-height: 1.6;
+            min-height: 100vh;
+            padding: 24px 16px 48px;
+        }
+
+        .container {
+            max-width: 900px;
+            margin: 0 auto;
+        }
+
+        header {
+            margin-bottom: 32px;
+        }
+
+        header h1 {
+            font-size: 1.75rem;
+            font-weight: 700;
+            margin-bottom: 6px;
+        }
+
+        header p {
+            color: var(--text-muted);
+            font-size: 0.95rem;
+        }
+
+        .header-row {
+            display: flex;
+            align-items: flex-start;
+            justify-content: space-between;
+            gap: 12px;
+        }
+
+        #lang-toggle {
+            background: var(--surface);
+            border: 1px solid var(--border);
+            border-radius: 6px;
+            padding: 4px 12px;
+            font-size: 0.85rem;
+            font-weight: 600;
+            cursor: pointer;
+            color: var(--text);
+            flex-shrink: 0;
+            transition: background 0.15s;
+        }
+
+        #lang-toggle:hover {
+            background: #e5e7eb;
+        }
+
+        .card {
+            background: var(--surface);
+            border: 1px solid var(--border);
+            border-radius: 12px;
+            padding: 24px;
+            margin-bottom: 20px;
+        }
+
+        .card-title {
+            font-size: 1rem;
+            font-weight: 600;
+            margin-bottom: 14px;
+        }
+
+        label {
+            display: block;
+            font-size: 0.875rem;
+            font-weight: 500;
+            margin-bottom: 6px;
+        }
+
+        textarea {
+            width: 100%;
+            font-family: var(--mono);
+            font-size: 0.82rem;
+            line-height: 1.5;
+            padding: 12px;
+            border: 1px solid var(--border);
+            border-radius: 8px;
+            resize: vertical;
+            background: #fafafa;
+            color: var(--text);
+            transition: border-color 0.15s;
+            min-height: 280px;
+        }
+
+        textarea:focus {
+            outline: none;
+            border-color: var(--border-focus);
+            background: #fff;
+            box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.12);
+        }
+
+        textarea::placeholder {
+            color: #9ca3af;
+        }
+
+        select, input[type="text"] {
+            font-family: var(--mono);
+            font-size: 0.85rem;
+            padding: 8px 12px;
+            border: 1px solid var(--border);
+            border-radius: 8px;
+            background: #fafafa;
+            color: var(--text);
+            transition: border-color 0.15s;
+        }
+
+        select:focus, input[type="text"]:focus {
+            outline: none;
+            border-color: var(--border-focus);
+            background: #fff;
+            box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.12);
+        }
+
+        #storage-custom, #prefix-custom {
+            margin-left: 8px;
+            width: 160px;
+        }
+
+        #proxy-ip {
+            width: 200px;
+        }
+
+        .radio-group {
+            display: flex;
+            gap: 20px;
+            flex-wrap: wrap;
+        }
+
+        .radio-group label {
+            display: inline-flex;
+            align-items: center;
+            gap: 6px;
+            font-weight: 400;
+            cursor: pointer;
+            font-size: 0.875rem;
+        }
+
+        .btn {
+            display: inline-flex;
+            align-items: center;
+            gap: 6px;
+            padding: 10px 20px;
+            border: none;
+            border-radius: 8px;
+            font-size: 0.9rem;
+            font-weight: 600;
+            cursor: pointer;
+            transition: background 0.15s, transform 0.1s;
+            user-select: none;
+        }
+
+        .btn:active {
+            transform: scale(0.97);
+        }
+
+        .btn-primary {
+            background: var(--accent);
+            color: #fff;
+        }
+
+        .btn-primary:hover {
+            background: var(--accent-hover);
+        }
+
+        .btn-secondary {
+            background: #f3f4f6;
+            color: var(--text);
+            border: 1px solid var(--border);
+        }
+
+        .btn-secondary:hover {
+            background: #e5e7eb;
+        }
+
+        .btn-success {
+            background: var(--success);
+            color: #fff;
+        }
+
+        .actions {
+            display: flex;
+            gap: 10px;
+            margin-top: 14px;
+            flex-wrap: wrap;
+            align-items: center;
+        }
+
+        .fields-grid {
+            display: grid;
+            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+            gap: 8px;
+            margin-top: 12px;
+        }
+
+        .field-tag {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            padding: 6px 10px;
+            border-radius: 6px;
+            font-size: 0.8rem;
+            font-family: var(--mono);
+            gap: 8px;
+        }
+
+        .field-tag.ok {
+            background: var(--tag-ok);
+            color: var(--tag-ok-text);
+        }
+
+        .field-tag.miss {
+            background: var(--tag-miss);
+            color: var(--tag-miss-text);
+        }
+
+        .field-tag .fname {
+            font-weight: 600;
+        }
+
+        .field-tag .fval {
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            max-width: 120px;
+            opacity: 0.8;
+        }
+
+        .output-header {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            margin-bottom: 10px;
+            flex-wrap: wrap;
+            gap: 8px;
+        }
+
+        pre#output, pre#uninstall-output, pre#routing-output, pre.s2s-output {
+            font-family: var(--mono);
+            font-size: 0.8rem;
+            line-height: 1.6;
+            background: #0f172a;
+            color: #e2e8f0;
+            padding: 20px;
+            border-radius: 8px;
+            overflow-x: auto;
+            white-space: pre;
+            tab-size: 2;
+        }
+
+        pre#output .comment, pre#uninstall-output .comment, pre#routing-output .comment, pre.s2s-output .comment {
+            color: #64748b;
+        }
+
+        pre#output .cmd, pre#uninstall-output .cmd, pre#routing-output .cmd, pre.s2s-output .cmd {
+            color: #7dd3fc;
+        }
+
+        .alert {
+            border-radius: 8px;
+            padding: 12px 16px;
+            font-size: 0.875rem;
+            margin-top: 12px;
+        }
+
+        .alert-error {
+            background: var(--error-bg);
+            border: 1px solid #fca5a5;
+            color: var(--error);
+        }
+
+        #errors-container {
+            margin-bottom: 4px;
+        }
+
+        #parsed-section, #output-section, #routing-section, #uninstall-section, #s2s-output-section {
+            display: none;
+        }
+
+        .s2s-fields {
+            display: grid;
+            grid-template-columns: 1fr 1fr;
+            gap: 12px;
+            margin-bottom: 12px;
+        }
+
+        .s2s-fields label {
+            font-size: 0.82rem;
+        }
+
+        .s2s-fields input[type="text"], .s2s-fields select {
+            width: 100%;
+        }
+
+        .s2s-side {
+            margin-bottom: 20px;
+        }
+
+        .s2s-side-header {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            margin-bottom: 10px;
+        }
+
+        @media (max-width: 600px) {
+            .s2s-fields {
+                grid-template-columns: 1fr;
+            }
+        }
+
+        footer {
+            text-align: center;
+            color: var(--text-muted);
+            font-size: 0.8rem;
+            margin-top: 40px;
+        }
+
+        .checkbox-grid {
+            display: grid;
+            grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+            gap: 6px;
+            margin-top: 8px;
+        }
+
+        .checkbox-grid label {
+            display: inline-flex;
+            align-items: center;
+            gap: 6px;
+            font-weight: 400;
+            cursor: pointer;
+            font-size: 0.82rem;
+            padding: 4px 6px;
+            border-radius: 4px;
+            transition: background 0.1s;
+        }
+
+        .checkbox-grid label:hover {
+            background: #f3f4f6;
+        }
+
+        .btn-sm {
+            padding: 4px 10px;
+            font-size: 0.75rem;
+            border-radius: 4px;
+        }
+
+        .domain-tag {
+            display: inline-flex;
+            align-items: center;
+            gap: 4px;
+            padding: 2px 8px;
+            border-radius: 4px;
+            background: var(--tag-ok);
+            color: var(--tag-ok-text);
+            font-size: 0.82rem;
+            font-family: var(--mono);
+        }
+
+        .domain-tag button {
+            background: none;
+            border: none;
+            color: inherit;
+            cursor: pointer;
+            font-size: 1rem;
+            line-height: 1;
+            padding: 0 2px;
+            opacity: 0.7;
+        }
+
+        .domain-tag button:hover {
+            opacity: 1;
+        }
+
+        .scenario-panel {
+            margin-top: 10px;
+            padding: 12px;
+            border: 1px solid var(--border);
+            border-radius: 8px;
+            background: #fafafa;
+        }
+
+        .scenario-panel-header {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            margin-bottom: 6px;
+            flex-wrap: wrap;
+            gap: 6px;
+        }
+
+        .alert-warning {
+            background: #fffbeb;
+            border: 1px solid #fcd34d;
+            color: #92400e;
+        }
+
+        .hint {
+            font-size: 0.8rem;
+            color: var(--text-muted);
+            margin-top: 4px;
+        }
+
+        .dns-info-icon {
+            display: inline-block;
+            cursor: help;
+            font-size: 1rem;
+            color: var(--accent);
+            position: relative;
+        }
+
+        .dns-info-icon:hover::after {
+            content: attr(title);
+            position: absolute;
+            bottom: 120%;
+            left: 50%;
+            transform: translateX(-50%);
+            background: #1e293b;
+            color: #e2e8f0;
+            padding: 8px 12px;
+            border-radius: 6px;
+            font-size: 0.75rem;
+            white-space: pre-line;
+            width: 320px;
+            z-index: 10;
+            pointer-events: none;
+            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
+            line-height: 1.5;
+        }
+
+        pre.collapsed { max-height: calc(15 * 1.6em); overflow: hidden; position: relative; }
+        pre.collapsed::after { content:''; position:absolute; bottom:0; left:0; right:0; height:3em; background:linear-gradient(transparent, #0f172a); pointer-events:none; }
+        .btn-expand { display:block; width:100%; margin-top:4px; padding:6px; background:#1e293b; color:#94a3b8; border:1px solid #334155; border-radius:0 0 8px 8px; font-size:0.8rem; cursor:pointer; text-align:center; }
+        .btn-expand:hover { background:#334155; color:#e2e8f0; }
+
+        @media (max-width: 600px) {
+            header h1 {
+                font-size: 1.4rem;
+            }
+
+            .card {
+                padding: 16px;
+            }
+
+            pre#output {
+                font-size: 0.72rem;
+                padding: 14px;
+            }
+
+            .dns-info-icon:hover::after {
+                width: 240px;
+                left: 0;
+                transform: none;
+            }
+        }
+
+        input[type="radio"], input[type="checkbox"] {
+            accent-color: var(--accent);
+        }
+
+        @media (prefers-color-scheme: dark) {
+            :root {
+                --bg: #111111;
+                --surface: #1e1e1e;
+                --border: #333333;
+                --border-focus: #f97316;
+                --text: #e8e8e8;
+                --text-muted: #888888;
+                --accent: #f97316;
+                --accent-hover: #ea580c;
+                --success: #22c55e;
+                --success-bg: #14532d;
+                --error: #f87171;
+                --error-bg: #2d1515;
+                --tag-ok: #14532d;
+                --tag-ok-text: #86efac;
+                --tag-miss: #450a0a;
+                --tag-miss-text: #fca5a5;
+            }
+
+            #lang-toggle:hover { background: #2a2a2a; }
+
+            textarea {
+                background: #242424;
+            }
+            textarea:focus {
+                background: #1a1a1a;
+                box-shadow: 0 0 0 3px rgba(249, 115, 22, 0.15);
+            }
+            textarea::placeholder { color: #555; }
+
+            select, input[type="text"] { background: #242424; }
+            select:focus, input[type="text"]:focus {
+                background: #1a1a1a;
+                box-shadow: 0 0 0 3px rgba(249, 115, 22, 0.15);
+            }
+
+            .btn-secondary { background: #2a2a2a; }
+            .btn-secondary:hover { background: #333; }
+
+            .scenario-panel { background: #242424; }
+            .checkbox-grid label:hover { background: #2a2a2a; }
+
+            .alert-warning { background: #2a1a00; border-color: #92400e; color: #fcd34d; }
+
+            pre.collapsed::after { background: linear-gradient(transparent, #0f172a); }
+            .infographic-wrap { background: #1e1e1e; border-color: #444; }
+            .infographic-wrap { background: #1e1e1e; border-color: #444; }
+            #ig-tip { background: #1a1a1a; border-color: #555; color: #e5e7eb; }
+            #ig-tip::after { border-bottom-color: #1a1a1a; }
+            #ig-tip::before { border-bottom-color: #555; }
+            #ig-tip.above::after { border-bottom-color: transparent; border-top-color: #1a1a1a; }
+            #ig-tip.above::before { border-bottom-color: transparent; border-top-color: #555; }
+            #ig-tip code { background: #2a2a2a; border-color: #444; }
+        }
+
+        .infographic-wrap {
+            margin-top: 12px;
+            padding: 16px;
+            border: 1px solid var(--border);
+            border-radius: 10px;
+            background: var(--surface);
+            overflow-x: auto;
+        }
+        .infographic-wrap svg { display: block; margin: 0 auto; }
+        .infographic-wrap .ig-block { cursor: pointer; }
+        .infographic-wrap .ig-block:hover rect { filter: brightness(1.08); }
+        #ig-tip {
+            position: fixed;
+            max-width: 400px;
+            padding: 12px 16px;
+            border-radius: 8px;
+            border: 1px solid var(--border);
+            background: var(--surface);
+            color: var(--text);
+            font-size: 0.82rem;
+            line-height: 1.7;
+            box-shadow: 0 4px 20px rgba(0,0,0,0.18);
+            pointer-events: none;
+            z-index: 10000;
+            display: none;
+        }
+        #ig-tip.visible { display: block; }
+        #ig-tip::after {
+            content: '';
+            position: absolute;
+            top: -7px; left: var(--arrow-left, 50%);
+            transform: translateX(-50%);
+            border-left: 7px solid transparent;
+            border-right: 7px solid transparent;
+            border-bottom: 7px solid var(--surface);
+        }
+        #ig-tip::before {
+            content: '';
+            position: absolute;
+            top: -8px; left: var(--arrow-left, 50%);
+            transform: translateX(-50%);
+            border-left: 8px solid transparent;
+            border-right: 8px solid transparent;
+            border-bottom: 8px solid var(--border);
+        }
+        #ig-tip.above::after {
+            top: auto; bottom: -7px;
+            border-bottom: none;
+            border-top: 7px solid var(--surface);
+        }
+        #ig-tip.above::before {
+            top: auto; bottom: -8px;
+            border-bottom: none;
+            border-top: 8px solid var(--border);
+        }
+        .ig-title {
+            font-size: 1rem;
+            font-weight: 600;
+            margin-bottom: 10px;
+            text-align: center;
+            color: var(--text);
+        }
+        #ig-tip .ig-tip-title {
+            font-weight: 700;
+            font-size: 0.9rem;
+            margin-bottom: 6px;
+        }
+        #ig-tip code {
+            font-family: var(--mono);
+            font-size: 0.78rem;
+            background: var(--bg);
+            border: 1px solid var(--border);
+            border-radius: 4px;
+            padding: 1px 5px;
+        }
+    </style>
+</head>
+<body>
+<div class="container">
+
+    <header>
+        <div class="header-row">
+            <div>
+                <h1 data-i18n="title">AWG Proxy &mdash; MikroTik Offline Command Generator</h1>
+                <p data-i18n="subtitle">Paste your AmneziaWG .conf file contents to generate ready-to-use MikroTik RouterOS commands</p>
+            </div>
+            <button id="lang-toggle" onclick="setLang(currentLang==='ru'?'en':'ru')">EN</button>
+        </div>
+    </header>
+
+    <div class="card">
+        <div class="card-title" data-i18n="confTitle">AmneziaWG Configuration File</div>
+        <p style="font-size:0.82rem;color:var(--text-muted);margin-bottom:12px;line-height:1.5" data-i18n="privacy">All configuration and data is processed locally in your browser. Nothing is transmitted anywhere. Your private keys are safe.</p>
+        <div style="margin-bottom:14px">
+            <label data-i18n="modeLabel">Deployment mode</label>
+            <div class="radio-group">
+                <label><input type="radio" name="deploy-mode" value="container" checked onchange="onModeChange()"> <span data-i18n="modeContainer">MikroTik + Container (Docker)</span></label>
+                <label><input type="radio" name="deploy-mode" value="standalone" onchange="onModeChange()"> <span data-i18n="modeStandalone">MikroTik + Server (Standalone)</span></label>
+                <label><input type="radio" name="deploy-mode" value="site-to-site" onchange="onModeChange()"> <span data-i18n="modeSiteToSite">Site-to-site (two MikroTik)</span></label>
+                <label><input type="radio" name="deploy-mode" value="server" onchange="onModeChange()"> <span data-i18n="modeServer">Server (1:N hub)</span></label>
+            </div>
+            <div id="mode-hint" class="hint" data-i18n="modeContainerHint">WireGuard client on MikroTik + awg-proxy runs inside a Docker container directly on the router</div>
+        </div>
+        <label for="conf-input" data-i18n="confLabel">.conf file contents</label>
+        <textarea id="conf-input"
+                  placeholder="[Interface]&#10;PrivateKey = ...&#10;Address = 10.1.1.1/32&#10;DNS = 1.1.1.1&#10;Jc = 2&#10;Jmin = 1&#10;Jmax = 500&#10;S1 = 123&#10;S2 = 12&#10;H1 = 12345&#10;...&#10;&#10;[Peer]&#10;PublicKey = ...&#10;Endpoint = 1.2.3.4:12345&#10;AllowedIPs = 0.0.0.0/0"></textarea>
+        <div id="scenario-row" style="margin-top:14px">
+            <label data-i18n="scenarioLabel">Routing scenario</label>
+            <div class="radio-group">
+                <label><input type="radio" name="scenario" value="dns-fwd" checked onchange="onScenarioChange()"> <span data-i18n="scenarioDns">DNS-based routing for services</span></label>
+                <label><input type="radio" name="scenario" value="tunnel" onchange="onScenarioChange()"> <span data-i18n="scenarioTunnel">Tunnel only (manual routes)</span></label>
+                <label><input type="radio" name="scenario" value="non-ru" onchange="onScenarioChange()"> <span data-i18n="scenarioNonRu">All non-RU traffic through tunnel</span></label>
+            </div>
+            <div id="scenario-hint" class="hint" data-i18n="scenarioDnsHint">Routes traffic to selected services through the tunnel using DNS forwarding (DoH). <span class="dns-info-icon" title="" data-i18n-title="dnsBrowserHint">&#9432;</span></div>
+        </div>
+        <div id="fasttrack-warn" class="alert alert-warning" style="margin-top:8px" data-i18n="warnFasttrack">FastTrack rules will be modified (out-interface-list=WAN) to keep FastTrack active for non-VPN traffic. Uninstall script restores them.</div>
+        <div id="non-ru-warn" class="alert alert-error" style="display:none;margin-top:8px" data-i18n="scenarioNonRuWarn"></div>
+        <div id="doh-panel" style="margin-bottom:10px">
+            <label data-i18n="dohForwarderLabel">DNS-forwarder (DoH)</label>
+            <div class="radio-group" id="doh-selector">
+                <label><input type="radio" name="doh-fwd" value="GoogleDNS" checked> Google</label>
+                <label><input type="radio" name="doh-fwd" value="CloudFlare"> CloudFlare</label>
+                <label><input type="radio" name="doh-fwd" value="Comss"> Comss</label>
+                <label><input type="radio" name="doh-fwd" value="Quad9"> Quad9</label>
+                <label><input type="radio" name="doh-fwd" value="Yandex"> Yandex</label>
+                <label><input type="radio" name="doh-fwd" value="__custom"> <span data-i18n="dohCustom">Custom...</span></label>
+            </div>
+            <input type="text" id="doh-custom-url" placeholder="https://your-dns/dns-query or IP" style="display:none;margin-top:6px;width:100%">
+            <div class="hint" data-i18n="dohHint">All services will use this DNS-over-HTTPS (DoH) provider for domain resolution.</div>
+        </div>
+        <div id="services-panel" class="scenario-panel">
+            <div class="scenario-panel-header">
+                <label style="margin-bottom:0" data-i18n="servicesLabel">Services to route through tunnel</label>
+                <div>
+                    <button class="btn btn-secondary btn-sm" onclick="toggleAllServices(true)" data-i18n="btnSelectAll">Select all</button>
+                    <button class="btn btn-secondary btn-sm" onclick="toggleAllServices(false)" data-i18n="btnDeselectAll">Deselect all</button>
+                </div>
+            </div>
+            <div id="services-grid" class="checkbox-grid"></div>
+            <div id="custom-domains-section" style="margin-top:12px;border-top:1px solid var(--border);padding-top:10px">
+                <label style="font-weight:600;font-size:0.9rem" data-i18n="customDomainsLabel">Custom domains</label>
+                <div style="display:flex;gap:6px;margin-top:6px">
+                    <input type="text" id="custom-domain-input" placeholder="example.com" data-i18n-ph="customDomainsPlaceholder" style="flex:1;min-width:0" onkeydown="if(event.key==='Enter'){event.preventDefault();addCustomDomain()}">
+                    <button class="btn btn-secondary btn-sm" onclick="addCustomDomain()" data-i18n="customDomainsAdd">+</button>
+                </div>
+                <div id="custom-domains-list" style="margin-top:6px;display:flex;flex-wrap:wrap;gap:4px"></div>
+            </div>
+        </div>
+        <div id="exclusions-panel" style="display:none" class="scenario-panel">
+            <div class="scenario-panel-header">
+                <label style="margin-bottom:0" data-i18n="exclusionsLabel">Exclude from tunnel (keep direct)</label>
+            </div>
+            <div id="exclusions-grid" class="checkbox-grid"></div>
+        </div>
+        <div id="netwatch-row" style="margin-top:8px">
+            <label><input type="checkbox" id="netwatch-enable" checked>
+            <span data-i18n="netwatchLabel">Netwatch failover</span></label>
+            <div class="hint" data-i18n="netwatchHint">Monitors tunnel availability by pinging 8.8.8.8 through the WG interface (via a pinned probe route). If the tunnel is down, disables its route &mdash; traffic falls back to the next tunnel (by distance) or goes direct. Re-enables the route when the tunnel recovers.</div>
+        </div>
+        <div id="df-row" style="margin-top:8px">
+            <label><input type="checkbox" id="df-enable">
+            <span data-i18n="dfLabel">Clear DF bit on UDP (DPI workaround)</span></label>
+            <div class="hint" data-i18n="dfHint">Adds AWG_NO_DF=1 to the container ENV: the proxy clears the Don&rsquo;t Fragment bit on its UDP packets. Some DPI equipment passes DF=0 UDP better than DF=1. Enable only if the tunnel has connectivity issues &mdash; with DF=0 large packets may be fragmented along the path.</div>
+        </div>
+        <div id="storage-row" style="margin-top:14px">
+            <label for="storage-select" data-i18n="storageLabel">Container storage</label>
+            <select id="storage-select">
+                <option value="__auto" data-i18n="sAuto">Auto (usb1/ &rarr; disk1/)</option>
+                <option value="disk1" data-i18n="sDisk1">disk1/ &mdash; internal storage (default)</option>
+                <option value="usb1" data-i18n="sUsb1">usb1/ &mdash; USB flash drive (common)</option>
+                <option value="usb1-part1" data-i18n="sUsb1p">usb1-part1/ &mdash; USB partition</option>
+                <option value="usb2" data-i18n="sUsb2">usb2/ &mdash; USB flash drive (2nd)</option>
+                <option value="sd1" data-i18n="sSd1">sd1/ &mdash; SD / microSD card</option>
+                <option value="nvme1" data-i18n="sNvme1">nvme1/ &mdash; NVMe SSD</option>
+                <option value="sata1" data-i18n="sSata1">sata1/ &mdash; SATA drive</option>
+                <option value="__custom" data-i18n="sCustom">Custom...</option>
+            </select>
+            <input type="text" id="storage-custom" placeholder="e.g. sata1-part1" style="display:none">
+        </div>
+        <div id="proxy-ip-row" style="margin-top:14px;display:none">
+            <label for="proxy-ip" data-i18n="proxyIpLabel">Proxy device IP address</label>
+            <input type="text" id="proxy-ip" placeholder="e.g. 192.168.1.100" data-i18n-ph="proxyIpPh">
+        </div>
+        <div style="margin-top:14px">
+            <label for="prefix-select" data-i18n="prefixLabel">Tunnel name (prefix)</label>
+            <select id="prefix-select">
+                <option value="awg-proxy-1" data-i18n="pAwg1">awg-proxy-1 &mdash; 1st tunnel (default)</option>
+                <option value="awg-proxy-2" data-i18n="pAwg2">awg-proxy-2 &mdash; 2nd tunnel</option>
+                <option value="awg-proxy-3" data-i18n="pAwg3">awg-proxy-3 &mdash; 3rd tunnel</option>
+                <option value="awg-proxy-4" data-i18n="pAwg4">awg-proxy-4 &mdash; 4th tunnel</option>
+                <option value="awg-proxy-5" data-i18n="pAwg5">awg-proxy-5 &mdash; 5th tunnel</option>
+                <option value="awg-server-1" data-i18n="pHub1" style="display:none">awg-server-1 &mdash; 1st server (default)</option>
+                <option value="awg-server-2" data-i18n="pHub2" style="display:none">awg-server-2 &mdash; 2nd server</option>
+                <option value="__custom" data-i18n="pCustom">Custom...</option>
+            </select>
+            <input type="text" id="prefix-custom" placeholder="e.g. awg-office" style="display:none">
+            <div class="hint" data-i18n="prefixHint">Change the prefix only if you need 2+ tunnels with different routing rules</div>
+        </div>
+        <div id="s2s-panel" style="display:none;margin-top:14px">
+            <div class="s2s-fields">
+                <div>
+                    <label for="s2s-endpoint-b" data-i18n="s2sEndpointB">Server endpoint (Side B IP or domain)</label>
+                    <input type="text" id="s2s-endpoint-b" placeholder="e.g. 198.51.100.1 or vpn.example.com">
+                </div>
+                <div>
+                    <label for="s2s-awg-port" data-i18n="s2sAwgPort">AWG port on server (Side B)</label>
+                    <input type="text" id="s2s-awg-port">
+                </div>
+                <div>
+                    <label for="s2s-tunnel-subnet" data-i18n="s2sTunnelSubnet">Tunnel subnet</label>
+                    <input type="text" id="s2s-tunnel-subnet" value="10.0.0.0/30">
+                </div>
+                <div>
+                    <label for="s2s-dns" data-i18n="s2sDns">DNS server (required for domain endpoint)</label>
+                    <input type="text" id="s2s-dns" placeholder="e.g. 192.168.88.1">
+                </div>
+            </div>
+            <div class="s2s-fields">
+                <div>
+                    <label for="s2s-lan-a" data-i18n="s2sLanA">Side A LAN subnet (optional)</label>
+                    <input type="text" id="s2s-lan-a" placeholder="e.g. 192.168.1.0/24">
+                </div>
+                <div>
+                    <label for="s2s-lan-b" data-i18n="s2sLanB">Side B LAN subnet (optional)</label>
+                    <input type="text" id="s2s-lan-b" placeholder="e.g. 192.168.2.0/24">
+                </div>
+            </div>
+            <div class="s2s-fields">
+                <div>
+                    <label for="s2s-storage-a" data-i18n="s2sStorageA">Side A storage</label>
+                    <select id="s2s-storage-a">
+                        <option value="__auto" data-i18n="sAuto">Auto (usb1/ &rarr; disk1/)</option>
+                        <option value="disk1" data-i18n="sDisk1">disk1/</option>
+                        <option value="usb1" data-i18n="sUsb1">usb1/</option>
+                        <option value="usb1-part1">usb1-part1/</option>
+                    </select>
+                </div>
+                <div>
+                    <label for="s2s-storage-b" data-i18n="s2sStorageB">Side B storage</label>
+                    <select id="s2s-storage-b">
+                        <option value="__auto" data-i18n="sAuto">Auto (usb1/ &rarr; disk1/)</option>
+                        <option value="disk1" data-i18n="sDisk1">disk1/</option>
+                        <option value="usb1" data-i18n="sUsb1">usb1/</option>
+                        <option value="usb1-part1">usb1-part1/</option>
+                    </select>
+                </div>
+            </div>
+            <div id="s2s-awg-params" class="fields-grid" style="margin-top:10px"></div>
+            <button class="btn btn-secondary" onclick="regenerateS2SParams()" data-i18n="s2sRegenerate">Regenerate AWG params</button>
+        </div>
+        <div id="srv-panel" style="display:none;margin-top:14px">
+            <div id="srv-add-client-banner" class="alert alert-info" style="display:none;font-weight:bold;margin-bottom:12px">
+                <span data-i18n="srvAddClientBanner">Adding a new client to server</span> <span id="srv-add-client-info"></span>
+            </div>
+            <div style="margin-bottom:12px">
+                <label data-i18n="srvNetLabel">Network merging</label>
+                <div class="radio-group">
+                    <label><input type="radio" name="srv-net" value="none" onchange="onSrvNetChange()"> <span data-i18n="srvNetNone">None</span></label>
+                    <label><input type="radio" name="srv-net" value="l3" checked onchange="onSrvNetChange()"> <span data-i18n="srvNetL3">L3 routing (IP)</span></label>
+                    <label><input type="radio" name="srv-net" value="awg" onchange="onSrvNetChange()"> <span data-i18n="srvNetAwg">Direct AmneziaWG client</span></label>
+                </div>
+                <div id="srv-net-hint" class="hint" data-i18n="srvNetL3Hint">Devices will be able to reach each other by IP across subnets. What becomes possible: access NAS/file shares, IP cameras, printers by address, remote desktop (RDP/VNC), manage routers via WinBox/SSH, host game servers, access home automation (Home Assistant). Does NOT work: broadcast/multicast services. Note: each client only sees the server LAN by default. To connect two clients to each other, additional routes must be added manually.</div>
+            </div>
+            <div class="s2s-fields">
+                <div>
+                    <label for="srv-endpoint" data-i18n="srvEndpoint">Server public IP or domain</label>
+                    <input type="text" id="srv-endpoint" placeholder="e.g. 198.51.100.1 or vpn.example.com">
+                </div>
+                <div>
+                    <label for="srv-awg-port" data-i18n="srvAwgPort">Server AWG port (public)</label>
+                    <input type="text" id="srv-awg-port">
+                </div>
+                <div>
+                    <label for="srv-tunnel-subnet" data-i18n="srvTunnelSubnet">Tunnel subnet (/24)</label>
+                    <input type="text" id="srv-tunnel-subnet" value="10.10.10.0/24">
+                </div>
+                <div>
+                    <label for="srv-dns" data-i18n="srvDns">DNS server (required if endpoint is a domain, not IP)</label>
+                    <input type="text" id="srv-dns" placeholder="e.g. 192.168.88.1">
+                </div>
+            </div>
+            <div id="srv-lan-fields" class="s2s-fields" style="margin-top:10px">
+                <div>
+                    <label for="srv-lan-server" id="srv-lan-server-label" data-i18n="srvLanServerReq">Server LAN subnet (required)</label>
+                    <input type="text" id="srv-lan-server" placeholder="e.g. 192.168.1.0/24">
+                </div>
+                <div>
+                    <label for="srv-lan-client" id="srv-lan-client-label" data-i18n="srvLanClientReq">Client LAN subnet (required)</label>
+                    <input type="text" id="srv-lan-client" placeholder="e.g. 192.168.2.0/24">
+                </div>
+            </div>
+            <div class="s2s-fields" style="margin-top:10px">
+                <div>
+                    <label for="srv-storage-server" data-i18n="srvStorageServer">Server storage</label>
+                    <select id="srv-storage-server">
+                        <option value="__auto" data-i18n="sAuto">Auto (usb1/ &rarr; disk1/)</option>
+                        <option value="disk1" data-i18n="sDisk1">disk1/</option>
+                        <option value="usb1" data-i18n="sUsb1">usb1/</option>
+                        <option value="usb1-part1">usb1-part1/</option>
+                    </select>
+                </div>
+                <div id="srv-storage-client-wrap">
+                    <label for="srv-storage-client" data-i18n="srvStorageClient">Client storage</label>
+                    <select id="srv-storage-client">
+                        <option value="__auto" data-i18n="sAuto">Auto (usb1/ &rarr; disk1/)</option>
+                        <option value="disk1" data-i18n="sDisk1">disk1/</option>
+                        <option value="usb1" data-i18n="sUsb1">usb1/</option>
+                        <option value="usb1-part1">usb1-part1/</option>
+                    </select>
+                </div>
+            </div>
+            <div id="srv-awg-params" class="fields-grid" style="margin-top:10px"></div>
+            <div style="margin-top:10px">
+                <label for="srv-client-num" data-i18n="srvClientNum">Client number</label>
+                <input type="number" id="srv-client-num" value="1" min="1" max="253" style="width:80px">
+            </div>
+            <button class="btn btn-secondary" onclick="regenerateServerParams()" data-i18n="srvRegenerate">Regenerate AWG params</button>
+        </div>
+        <div class="actions">
+            <button class="btn btn-primary" id="btn-generate" onclick="generate()" data-i18n="btnGenerate">Generate commands</button>
+        </div>
+        <div id="infographic-container"></div>
+    </div>
+
+    <div id="errors-container"></div>
+
+    <div id="parsed-section" class="card">
+        <div class="card-title" data-i18n="parsedTitle">Parsed parameters</div>
+        <div id="fields-display"></div>
+    </div>
+
+    <div id="output-section" class="card">
+        <div class="output-header">
+            <div class="card-title" style="margin-bottom:0" data-i18n="outputTitle">MikroTik RouterOS Commands</div>
+            <button class="btn btn-secondary" id="copy-btn" onclick="copyOutput('output','copy-btn')" data-i18n="btnCopy">Copy all</button>
+        </div>
+        <pre id="output"></pre>
+        <button class="btn-expand" id="expand-output" onclick="toggleCollapse('output','expand-output')" data-i18n="btnExpand" style="display:none">Show all</button>
+    </div>
+
+    <div id="routing-section" class="card" style="display:none">
+        <div class="output-header">
+            <div class="card-title" style="margin-bottom:0" data-i18n="routingTitle">Step 2: Routing Setup</div>
+            <button class="btn btn-secondary" id="copy-routing-btn" onclick="copyOutput('routing-output','copy-routing-btn')" data-i18n="btnCopyRouting">Copy routing</button>
+        </div>
+        <div class="alert alert-warning" data-i18n="routingNote">Paste this AFTER Step 1 completes (wait for "Installation complete!").</div>
+        <pre id="routing-output"></pre>
+        <button class="btn-expand" id="expand-routing" onclick="toggleCollapse('routing-output','expand-routing')" data-i18n="btnExpand" style="display:none">Show all</button>
+    </div>
+
+    <div id="uninstall-section" class="card">
+        <div class="output-header">
+            <div class="card-title" style="margin-bottom:0" data-i18n="uninstallTitle">Uninstall Commands</div>
+            <button class="btn btn-secondary" id="copy-uninstall-btn"
+                    onclick="copyOutput('uninstall-output','copy-uninstall-btn')" data-i18n="btnCopyUn">Copy uninstall</button>
+        </div>
+        <pre id="uninstall-output"></pre>
+        <button class="btn-expand" id="expand-uninstall" onclick="toggleCollapse('uninstall-output','expand-uninstall')" data-i18n="btnExpand" style="display:none">Show all</button>
+    </div>
+
+    <div id="s2s-output-section">
+        <div class="card s2s-side">
+            <div class="s2s-side-header">
+                <div class="card-title" style="margin-bottom:0" data-i18n="s2sOutputA">Side A (MikroTik 1) — client</div>
+                <button class="btn btn-secondary" id="s2s-copy-a" onclick="copyOutput('s2s-out-a','s2s-copy-a')" data-i18n="btnCopy">Copy all</button>
+            </div>
+            <pre id="s2s-out-a" class="s2s-output"></pre>
+            <div style="margin-top:12px">
+                <div class="card-title" style="font-size:0.9rem" data-i18n="s2sUninstallA">Uninstall Side A (client)</div>
+                <pre id="s2s-uninst-a" class="s2s-output"></pre>
+            </div>
+        </div>
+        <div class="card s2s-side">
+            <div class="s2s-side-header">
+                <div class="card-title" style="margin-bottom:0" data-i18n="s2sOutputB">Side B (MikroTik 2) — server</div>
+                <button class="btn btn-secondary" id="s2s-copy-b" onclick="copyOutput('s2s-out-b','s2s-copy-b')" data-i18n="btnCopy">Copy all</button>
+            </div>
+            <pre id="s2s-out-b" class="s2s-output"></pre>
+            <div style="margin-top:12px">
+                <div class="card-title" style="font-size:0.9rem" data-i18n="s2sUninstallB">Uninstall Side B (server)</div>
+                <pre id="s2s-uninst-b" class="s2s-output"></pre>
+            </div>
+        </div>
+    </div>
+
+    <div id="srv-output-section" style="display:none">
+        <div id="srv-link-banner" class="alert alert-warning" style="font-size:1.1em;font-weight:bold;margin-bottom:16px">
+            <div data-i18n="srvSaveLink">&#9888;&#65039; SAVE THIS LINK FOR FUTURE CONFIGURATION</div>
+            <div style="display:flex;gap:8px;margin-top:8px">
+                <input type="text" id="srv-share-url" readonly onclick="this.select()" style="flex:1;font-size:0.85em;font-family:monospace">
+                <button class="btn btn-secondary" id="srv-copy-url" onclick="copySrvURL()" data-i18n="btnCopy">Copy</button>
+            </div>
+            <div class="hint" style="margin-top:6px;font-weight:normal;font-size:0.85em" data-i18n="srvEnvHint">This link will also be saved in container env AWG_CONFIG_URL. To retrieve it later: /container/envs/print where key=AWG_CONFIG_URL</div>
+        </div>
+        <div class="card s2s-side">
+            <div class="s2s-side-header">
+                <div class="card-title" style="margin-bottom:0" data-i18n="srvOutputServer">Server (head MikroTik)</div>
+                <button class="btn btn-secondary" id="srv-copy-server" onclick="copyOutput('srv-out-server','srv-copy-server')" data-i18n="btnCopy">Copy all</button>
+            </div>
+            <pre id="srv-out-server" class="s2s-output"></pre>
+        </div>
+        <div class="card s2s-side">
+            <div class="s2s-side-header">
+                <div class="card-title" style="margin-bottom:0" id="srv-client-title" data-i18n="srvOutputClient">Client 1</div>
+                <button class="btn btn-secondary" id="srv-copy-client" onclick="copyOutput('srv-out-client','srv-copy-client')" data-i18n="btnCopy">Copy all</button>
+            </div>
+            <pre id="srv-out-client" class="s2s-output"></pre>
+            <button class="btn btn-primary" id="srv-download-conf" onclick="downloadAwgConf()" data-i18n="btnDownloadConf" style="display:none;margin-top:10px">Download .conf</button>
+        </div>
+        <div id="srv-server-update" class="card s2s-side" style="display:none">
+            <div class="s2s-side-header">
+                <div class="card-title" style="margin-bottom:0" data-i18n="srvServerUpdate">Run on SERVER to add this client</div>
+                <button class="btn btn-secondary" id="srv-copy-update" onclick="copyOutput('srv-out-update','srv-copy-update')" data-i18n="btnCopy">Copy all</button>
+            </div>
+            <pre id="srv-out-update" class="s2s-output"></pre>
+        </div>
+    </div>
+
+    <footer data-i18n="footer">Works entirely offline &mdash; no data leaves your browser.</footer>
+</div>
+
+<script>
+    'use strict';
+
+    // ============================================================
+    // i18n
+    // ============================================================
+    var currentLang = 'ru';
+
+    var REPO = (function() {
+        var h = location.hostname, p = location.pathname.split('/')[1] || '';
+        var m = h.match(/^([^.]+)\.(github|gitlab)\.io$/);
+        if (m) {
+            var base = 'https://' + (m[2] === 'github' ? 'github.com' : 'gitlab.com') + '/' + m[1] + '/' + p;
+            var dl = m[2] === 'gitlab'
+                ? base + '/-/releases/permalink/latest/downloads/'
+                : base + '/releases/latest/download/';
+            return { base: base, dl: dl };
+        }
+        return {
+            base: 'https://github.com/timbrs/amneziawg-mikrotik-c',
+            dl: 'https://github.com/timbrs/amneziawg-mikrotik-c/releases/latest/download/'
+        };
+    })();
+
+    var RU_RANGES_BASE = 'https://github.com/timbrs/ru-ranges/releases/latest/download/';
+
+    var I18N = {
+        en: {
+            title: 'AWG Proxy \u2014 MikroTik Offline Command Generator',
+            subtitle: 'Paste your AmneziaWG .conf file contents to generate ready-to-use MikroTik RouterOS commands',
+            privacy: 'All configuration and data is processed locally in your browser. Nothing is transmitted anywhere. Your private keys are safe.',
+            confTitle: 'AmneziaWG Configuration File',
+            confLabel: '.conf file contents',
+            modeLabel: 'Deployment mode',
+            modeContainer: 'MikroTik + Container (Docker)',
+            modeStandalone: 'MikroTik + Server (Standalone)',
+            storageLabel: 'Container storage',
+            proxyIpLabel: 'Proxy device IP address',
+            proxyIpPh: 'e.g. 192.168.1.100',
+            prefixLabel: 'Tunnel name (prefix)',
+            btnGenerate: 'Generate commands',
+            btnClear: 'Clear',
+            parsedTitle: 'Parsed parameters',
+            outputTitle: 'Step 1: Install AWG Proxy',
+            btnCopy: 'Copy all',
+            btnDownloadConf: 'Download .conf',
+            routingTitle: 'Step 2: Routing Setup',
+            btnCopyRouting: 'Copy routing',
+            routingNote: 'Paste this AFTER Step 1 completes (wait for "Installation complete!").',
+            uninstallTitle: 'Uninstall Commands',
+            btnCopyUn: 'Copy uninstall',
+            footer: 'Works entirely offline \u2014 no data leaves your browser.',
+            errEmpty: 'Configuration field is empty. Paste the contents of your .conf file.',
+            errPrefix: 'Invalid tunnel prefix: only letters, digits, hyphens and underscores allowed (1-30 chars, cannot start/end with hyphen).',
+            errProxyIp: 'Invalid proxy device IP address.',
+            errTitle: 'Errors:',
+            ifaceSec: '[Interface] section',
+            peerSec: '[Peer] section',
+            copied: 'Copied!',
+            sDisk1: 'disk1/ \u2014 internal storage (default)',
+            sUsb1: 'usb1/ \u2014 USB flash drive (common)',
+            sUsb1p: 'usb1-part1/ \u2014 USB partition',
+            sUsb2: 'usb2/ \u2014 USB flash drive (2nd)',
+            sSd1: 'sd1/ \u2014 SD / microSD card',
+            sNvme1: 'nvme1/ \u2014 NVMe SSD',
+            sSata1: 'sata1/ \u2014 SATA drive',
+            sCustom: 'Custom...',
+            pAwg1: 'awg-proxy-1 \u2014 1st tunnel (default)',
+            pAwg2: 'awg-proxy-2 \u2014 2nd tunnel',
+            pAwg3: 'awg-proxy-3 \u2014 3rd tunnel',
+            pAwg4: 'awg-proxy-4 \u2014 4th tunnel',
+            pAwg5: 'awg-proxy-5 \u2014 5th tunnel',
+            pCustom: 'Custom...',
+            netwatchLabel: 'Netwatch failover',
+            netwatchHint: 'Monitors tunnel availability by pinging 8.8.8.8 through the WG interface (via a pinned probe route). If the tunnel is down, disables its route \u2014 traffic falls back to the next tunnel (by distance) or goes direct. Re-enables when the tunnel recovers.',
+            dfLabel: 'Clear DF bit on UDP (DPI workaround)',
+            dfHint: 'Adds AWG_NO_DF=1 to the container ENV: the proxy clears the Don\u2019t Fragment bit on its UDP packets. Some DPI equipment passes DF=0 UDP better than DF=1. Enable only if the tunnel has connectivity issues \u2014 with DF=0 large packets may be fragmented along the path.',
+            scenarioLabel: 'Routing scenario',
+            scenarioTunnel: 'Tunnel only (manual routes)',
+            scenarioDns: 'DNS-based routing for services',
+            scenarioNonRu: 'All non-RU traffic through tunnel',
+            servicesLabel: 'Services to route through tunnel',
+            exclusionsLabel: 'Exclude from tunnel (keep direct)',
+            btnSelectAll: 'Select all',
+            btnDeselectAll: 'Deselect all',
+            sAuto: 'Auto (usb1/ \u2192 disk1/)',
+            warnFasttrack: 'FastTrack rules will be modified (out-interface-list=WAN) to keep FastTrack active for non-VPN traffic while policy routing works correctly. Uninstall script restores the original settings.',
+            noServiceSelected: 'Select at least one service or add a custom domain.',
+            customDomainsLabel: 'Custom domains',
+            customDomainsPlaceholder: 'example.com',
+            customDomainsAdd: '+',
+            modeContainerHint: 'WireGuard client on MikroTik + awg-proxy runs inside a Docker container directly on the router',
+            modeStandaloneHint: 'WireGuard client on MikroTik + awg-proxy runs on a separate device (Raspberry Pi, VPS, or another server). Use this mode for MikroTik routers that do not support containers.',
+            scenarioTunnelHint: 'You configure routes manually via /ip/route or other methods. For advanced users.',
+            scenarioDnsHint: 'Routes traffic to selected services through the tunnel using DNS forwarding (DoH). MikroTik resolves domains via DNS-over-HTTPS and collects IPs into address lists. Firewall mangle rules then route this traffic through the tunnel. Works best on a clean (reset) configuration. Important: disable Secure DNS (DoH) in your browser so that DNS queries go through MikroTik.',
+            scenarioNonRuHint: 'All traffic goes through the tunnel except Russian IP ranges (loaded into RAM on boot and refreshed daily). The .rsc file is stored on USB or selected storage. Works best on a clean (reset) configuration.',
+            prefixHint: 'Change the prefix only if you need 2+ tunnels with different routing rules',
+            modeSiteToSite: 'Site-to-site (two MikroTik)',
+            modeSiteToSiteHint: 'Connect exactly two MikroTik routers via AmneziaWG tunnel (point-to-point). WG keys and AWG params are generated automatically. For 3 or more routers, use "Server (1:N hub)" mode instead.',
+            s2sEndpointB: 'Server endpoint (Side B IP or domain)',
+            s2sTunnelSubnet: 'Tunnel subnet',
+            s2sAwgPort: 'AWG port on server (Side B)',
+            s2sLanA: 'Side A LAN subnet (optional)',
+            s2sLanB: 'Side B LAN subnet (optional)',
+            s2sStorageA: 'Side A storage',
+            s2sStorageB: 'Side B storage',
+            s2sDns: 'DNS server (required for domain endpoint)',
+            s2sRegenerate: 'Regenerate AWG params',
+            s2sOutputA: 'Side A (MikroTik 1) \u2014 client',
+            s2sOutputB: 'Side B (MikroTik 2) \u2014 server',
+            s2sUninstallA: 'Uninstall Side A (client)',
+            s2sUninstallB: 'Uninstall Side B (server)',
+            errEndpointB: 'Server endpoint is required (Side B IP address or domain).',
+            errDnsRequired: 'DNS server is required when endpoint is a hostname (not an IP address).',
+            errLanSubnet: 'Invalid LAN subnet format. Use CIDR notation, e.g. 192.168.1.0/24.',
+            errTunnelSubnet: 'Invalid tunnel subnet. Use /30 CIDR (e.g. 10.0.0.0/30).',
+            errAwgPort: 'AWG port must be 1024\u201365535.',
+            modeServer: 'Server (1:N hub)',
+            modeServerHint: 'Star topology: one MikroTik as central hub, multiple client MikroTiks connect to it via AmneziaWG. Use cases: connecting offices, homes, or remote sites into a single network. WG keys and AWG params are generated automatically. New server installs now set AWG_CLIENT_PUBS to the real direct client keys, while AWG_CLIENT_PUB stays as a legacy fallback for proxy-only clients. You will receive a shareable link to easily generate configs for new clients in the future.',
+            srvEndpoint: 'Server public IP or domain',
+            srvAwgPort: 'Server AWG port (public)',
+            srvTunnelSubnet: 'Tunnel subnet (/24)',
+            srvDns: 'DNS server (required if endpoint is a domain, not IP)',
+            srvStorageServer: 'Server storage',
+            srvStorageClient: 'Client storage',
+            srvLanServerOpt: 'Server LAN subnet (optional)',
+            srvLanClientOpt: 'Client LAN subnet (optional)',
+            srvLanServerReq: 'Server LAN subnet (required)',
+            srvLanClientReq: 'Client LAN subnet (required)',
+            srvNetLabel: 'Network merging',
+            srvNetNone: 'None',
+            srvNetL3: 'L3 routing (IP)',
+            srvNetL2: 'L2+L3 Bridging (EoIP)',
+            srvNetAwg: 'Direct AmneziaWG client',
+            srvNetAwgHint: 'Generates a ready-to-import .conf for a phone / laptop / AmneziaWG app instead of a second MikroTik. All device traffic is routed through the hub (full tunnel, AllowedIPs = 0.0.0.0/0). The head MikroTik just gets a peer with one tunnel IP — no client-side router needed. LAN merging fields are not used here.',
+            srvAwgConfTitle: '.conf for AmneziaWG — Client',
+            srvNetNoneHint: 'VPN tunnel only. No routing between LANs is configured \u2014 further network setup is up to the admin. Once the tunnel is established, you can connect to the remote MikroTik via WinBox/SSH using its tunnel IP (e.g. 10.10.10.2).',
+            srvNetL3Hint: 'Devices will be able to reach each other by IP across subnets. What becomes possible: access NAS/file shares, IP cameras, printers by address, remote desktop (RDP/VNC), manage routers via WinBox/SSH, host game servers, access home automation (Home Assistant). Does NOT work: broadcast/multicast services \u2014 auto-discovery (mDNS/Bonjour, DLNA, AirPlay, Chromecast), Windows Network Neighborhood, printer auto-discovery, Wake-on-LAN. Note: each client only sees the server LAN by default. To connect two clients to each other, additional routes must be added manually \u2014 see README section "Routing between clients".',
+            srvNetL2Hint: 'Single network \u2014 all devices as if on one switch. Everything works: mDNS (Bonjour), DLNA, AirPlay, Chromecast, Windows Network, printer discovery. Downside: broadcast traffic goes through tunnel, more load.',
+            srvClientNum: 'Client number',
+            srvRegenerate: 'Regenerate AWG params',
+            srvOutputServer: 'Server (head MikroTik)',
+            srvOutputClient: 'Client',
+            srvServerUpdate: 'Run on SERVER to add this client',
+            srvSaveLink: '\u26a0\ufe0f SAVE THIS LINK FOR FUTURE CONFIGURATION',
+            srvEnvHint: 'This link will also be saved in container env AWG_CONFIG_URL. To retrieve it later: /container/envs/print where key=AWG_CONFIG_URL',
+            srvAddClientBanner: 'Adding a new client to server',
+            pHub1: 'awg-server-1 \u2014 1st server (default)',
+            pHub2: 'awg-server-2 \u2014 2nd server',
+            errSrvEndpoint: 'Server public IP or domain is required.',
+            errSrvTunnelSubnet: 'Invalid tunnel subnet. Use /24 CIDR (e.g. 10.10.10.0/24).',
+            errSrvLanRequired: 'Both server and client LAN subnets are required for network merging.',
+            scenarioNonRuWarn: 'WARNING: All foreign traffic will go through VPN. Make sure your server can handle the load. Most VPS have limited bandwidth. A typical household with video services easily consumes 10+ TB/month. ALL torrent traffic will also go through the tunnel (that is A LOT of data). Game traffic may also be routed through it. Choose this option only if you are 100% sure. RU address list is loaded into RAM (not stored on flash). The .rsc file (~1MB) is stored on USB if available, otherwise on disk1 (requires >= 1.5MB free).',
+            btnExpand: 'Show all',
+            btnCollapse: 'Collapse',
+            dohForwarderLabel: 'DNS forwarder (DoH)',
+            dohCustom: 'Custom...',
+            dohHint: 'All services will use this DNS-over-HTTPS (DoH) provider for domain resolution.',
+            dnsBrowserHint: 'Disable Secure DNS (DoH) in your browser.\nChrome: Settings \u2192 Privacy \u2192 Security \u2192 Use secure DNS \u2192 Off\nFirefox: Settings \u2192 Privacy \u2192 DNS over HTTPS \u2192 Off\nEdge: Settings \u2192 Privacy \u2192 Security \u2192 Use secure DNS \u2192 Off\nOtherwise MikroTik will not see your DNS queries and cannot route traffic.',
+            igTitle: 'How it works',
+            igWireguard: 'WireGuard Tunnel',
+            igContainer: 'Container (awg-proxy)',
+            igContainerOnRouter: 'Container on router',
+            igStandalone: 'External Device (awg-proxy)',
+            igDns: 'DNS (DoH)',
+            igRouting: 'Routing',
+            igFirewall: 'Firewall / NAT',
+            igMangle: 'Mangle',
+            igInternet: 'Internet',
+            igInternetWan: 'Internet (WAN)',
+            igLan: 'LAN Clients',
+            igAwgServer: 'AWG Server',
+            igRuList: 'RU Address List',
+            igScheduler: 'Scheduler',
+            igManualConfig: 'Manual Configuration',
+            igRouterA: 'MikroTik A (client)',
+            igRouterB: 'MikroTik B (server)',
+            igServerHub: 'MikroTik Server (hub)',
+            igClientN: 'MikroTik Client #N',
+            igContainerClient: 'awg-proxy (connect)',
+            igContainerServer: 'awg-proxy (listen)',
+            igClient2: 'MikroTik Client #2',
+            igTipWireguard: 'WireGuard Tunnel\n\n/interface/wireguard/add \u2014 creates a WireGuard interface with a private key and listen port\n/interface/wireguard/peers/add \u2014 adds a peer (AWG server) with its public key and endpoint pointing to the awg-proxy container\n/ip/address/add \u2014 assigns a tunnel IP address to the WireGuard interface\n\nThis is the encrypted tunnel through which traffic flows to the AWG server.',
+            igTipContainer: 'Container (awg-proxy)\n\n/interface/veth/add \u2014 creates a virtual ethernet pair connecting the router to the container\n/ip/address/add \u2014 assigns an IP address to the veth interface\n/container/envs/add \u2014 sets AWG parameters (Jc, Jmin, Jmax, S1, S2, H1\u2013H4, keys)\n/container/add \u2014 downloads and creates the awg-proxy container\n/container/start \u2014 starts the container\n\nawg-proxy converts standard WireGuard packets to AmneziaWG format (adds junk, changes headers, pads handshakes).',
+            igTipContainerOnRouter: 'Container on router\n\nThe awg-proxy container runs directly on the MikroTik router.\n\n/interface/veth/add \u2014 creates a virtual ethernet pair connecting the router to the container\n/container/envs/add \u2014 sets AWG parameters (Jc, Jmin, Jmax, S1, S2, H1\u2013H4, keys)\n/container/add + /container/start \u2014 downloads and starts the container\n\nawg-proxy converts standard WireGuard packets to AmneziaWG format.',
+            igTipStandalone: 'External Device (awg-proxy)\n\nawg-proxy runs on a separate device (Raspberry Pi, VPS, etc). The MikroTik WireGuard peer endpoint points to this device\u2019s IP address.\n\nThe device converts WireGuard traffic to AmneziaWG format before forwarding to the AWG server.',
+            igTipDns: 'DNS (DoH Forwarding)\n\n/ip/dns/static/add type=FWD \u2014 creates forwarding entries for selected service domains\n\nWhen a client resolves a domain (e.g. youtube.com), MikroTik forwards the query to a DoH provider and saves the resolved IPs in an address list.\nThis address list is provided to Mangle rules which mark matching traffic for routing through the VPN tunnel.',
+            igTipRouting: 'Policy Routing\n\n/routing/table/add \u2014 creates a separate routing table for VPN traffic\n/ip/route/add \u2014 adds a default route through the WireGuard tunnel in that table\n/routing/rule/add \u2014 binds the routing mark to the routing table\n\nTraffic marked by Mangle goes through this table and gets routed through the VPN tunnel. All other traffic uses the default routing table (direct).',
+            igTipFirewall: 'Firewall / NAT\n\n/ip/firewall/nat/add chain=srcnat masquerade src-address=172.18.0.0/30 \u2014 NATs container traffic to the internet\n\nThe out-interface masquerade rule for the WireGuard tunnel is added in the routing section when a routing scenario is configured.',
+            igTipMangle: 'Mangle (Traffic Marking)\n\n/ip/firewall/mangle/add mark-connection \u2014 marks connections going to IPs from DNS address lists\n/ip/firewall/mangle/add mark-routing \u2014 applies a routing mark to marked connections\n/ip/firewall/mangle/add change-mss clamp-to-pmtu \u2014 fixes MSS for tunneled traffic\n\nThis is the core of selective routing: only traffic to selected services gets marked and routed through the VPN.',
+            igTipMangleNonRu: 'Mangle (Traffic Marking)\n\n/ip/firewall/mangle/add mark-connection dst-address-list=!RU \u2014 marks NEW connections to non-Russian IPs (checked once per connection)\n/ip/firewall/mangle/add mark-routing connection-mark=... \u2014 applies routing mark to all packets of marked connections (fast, no address-list lookup)\n/ip/firewall/mangle/add accept dst-address-list=vpn-server \u2014 excludes VPN server\n/ip/firewall/mangle/add change-mss clamp-to-pmtu \u2014 fixes MSS\n\nAll non-Russian traffic goes through the VPN tunnel.',
+            igTipManualConfig: 'Manual Configuration\n\nNo automatic routing is configured. You choose how traffic enters the tunnel:\n\nRoute specific subnets:\n/ip/route/add dst-address=X.X.X.X/X gateway=wg-*\n\nRoute all traffic:\n/ip/route/add dst-address=0.0.0.0/0 gateway=wg-* distance=1\n\nPolicy routing with mangle marks\n\nUse address-lists for selective routing\n\nRemember to add NAT masquerade for tunneled traffic:\n/ip/firewall/nat/add chain=srcnat action=masquerade out-interface=wg-*',
+            igTipRuList: 'RU Address List\n\nDownloaded from GitHub, stored on USB/disk. Entries are loaded into RAM with 30-day timeout (not persisted to flash). Reimported on each boot + daily.\nPrivate networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) are added as static entries to prevent local traffic from entering the tunnel.\nIf scheduler is not allowed by device-mode, the list is imported as permanent entries (ru_ranges.rsc) without auto-update.',
+            igTipScheduler: 'Scheduler\n\n/system/scheduler/add \u2014 runs RU list update on boot (start-time=startup) and daily (interval=1d).\n/system/script/add \u2014 downloads fresh .rsc, clears old entries, imports new ones into RAM.\n\nKeeps the RU address list current so routing decisions stay accurate.\nIf scheduler is not allowed by device-mode, schedulers are skipped and the list is imported as permanent entries (ru_ranges.rsc) without auto-update.',
+            igTipFirewallS2S: 'Firewall\n\n/ip/firewall/filter/add accept \u2014 allows AWG port on WAN interface\n/ip/firewall/nat/add dstnat \u2014 forwards incoming AWG traffic to the container\n/ip/firewall/nat/add masquerade \u2014 NATs container and tunnel traffic\n\nSide B (server) needs additional rules to accept incoming connections.',
+            igTipFirewallSrv: 'Firewall\n\n/ip/firewall/filter/add accept \u2014 allows AWG port on WAN\n/ip/firewall/nat/add dstnat \u2014 redirects AWG traffic to container\n/ip/firewall/nat/add masquerade \u2014 NATs container + tunnel traffic\n/ip/route/add \u2014 routes to client LANs through WG peers\n\nServer accepts connections from all clients and routes between their networks.',
+            igTipRoutingS2S: 'Routing\n\n/ip/route/add \u2014 adds routes to the remote LAN subnet through the WireGuard tunnel\n\nAllows devices on Side A to reach devices on Side B and vice versa.',
+            igTipRoutingSrv: 'Routing (L3)\n\n/ip/route/add \u2014 adds routes to remote LAN subnets through WireGuard peers\n/ip/firewall/filter/add \u2014 allows forwarding between tunnel and LAN\n\nServer routes traffic between all connected client networks. Each client gets a route to the server LAN. For client-to-client routing, additional routes must be added manually.',
+            igTipRoutingTunnel: 'Manual Routing\n\nNo automatic routing is configured.\nAdd routes manually via /ip/route/add dst-address=X.X.X.X/X gateway=wg-awg-proxy\n\nFor advanced users who want full control over what goes through the tunnel.',
+            igTipLan: 'LAN Clients\n\nDevices on the local network. Their traffic passes through the MikroTik router, which decides whether to route it through the VPN tunnel or directly to the internet based on Mangle/routing rules.',
+            igTipInternetWan: 'Internet (WAN)\n\nDirect connection to the internet via the MikroTik WAN port.\nTraffic not marked for VPN goes here directly.',
+            igTipInternet: 'Internet\n\nDirect internet connection via WAN interface. Non-VPN traffic goes here. VPN traffic goes through the WireGuard tunnel to the AWG server.',
+            igTipAwgServer: 'AmneziaWG Server\n\nThe remote VPN server running AmneziaWG. Receives transformed packets from awg-proxy and routes them to the internet.\n\nTraffic flows: Client \u2192 MikroTik WG \u2192 awg-proxy \u2192 AWG Server \u2192 Internet'
+        },
+        ru: {
+            title: 'AWG Proxy \u2014 \u041e\u0444\u043b\u0430\u0439\u043d-\u0433\u0435\u043d\u0435\u0440\u0430\u0442\u043e\u0440 \u043a\u043e\u043c\u0430\u043d\u0434 MikroTik',
+            subtitle: '\u0412\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435 .conf \u0444\u0430\u0439\u043b\u0430 AmneziaWG \u0434\u043b\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u043a\u043e\u043c\u0430\u043d\u0434 MikroTik RouterOS',
+            privacy: '\u0412\u0441\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e \u0432 \u0432\u0430\u0448\u0435\u043c \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435. \u041d\u0438\u0447\u0435\u0433\u043e \u043d\u0438\u043a\u0443\u0434\u0430 \u043d\u0435 \u043f\u0435\u0440\u0435\u0434\u0430\u0451\u0442\u0441\u044f. \u0412\u0430\u0448\u0438 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0435 \u043a\u043b\u044e\u0447\u0438 \u0432 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438.',
+            confTitle: '\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b AmneziaWG',
+            confLabel: '\u0421\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435 .conf \u0444\u0430\u0439\u043b\u0430',
+            modeLabel: '\u0420\u0435\u0436\u0438\u043c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438',
+            modeContainer: 'MikroTik + \u041a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 (Docker)',
+            modeStandalone: 'MikroTik + \u0421\u0435\u0440\u0432\u0435\u0440 (Standalone)',
+            storageLabel: '\u0425\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430',
+            proxyIpLabel: 'IP-\u0430\u0434\u0440\u0435\u0441 \u043f\u0440\u043e\u043a\u0441\u0438-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430',
+            proxyIpPh: '\u043d\u0430\u043f\u0440. 192.168.1.100',
+            prefixLabel: '\u0418\u043c\u044f \u0442\u0443\u043d\u043d\u0435\u043b\u044f (\u043f\u0440\u0435\u0444\u0438\u043a\u0441)',
+            btnGenerate: '\u0421\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c',
+            btnClear: '\u041e\u0447\u0438\u0441\u0442\u0438\u0442\u044c',
+            parsedTitle: '\u0420\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b',
+            outputTitle: '\u0428\u0430\u0433 1: \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 AWG Proxy',
+            btnCopy: '\u0421\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c',
+            btnDownloadConf: '\u0421\u043a\u0430\u0447\u0430\u0442\u044c .conf',
+            routingTitle: '\u0428\u0430\u0433 2: \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438',
+            btnCopyRouting: '\u0421\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c',
+            routingNote: '\u0412\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u042d\u0422\u041e \u041f\u041e\u0421\u041b\u0415 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u0428\u0430\u0433\u0430 1 (\u0434\u043e\u0436\u0434\u0438\u0442\u0435\u0441\u044c "Installation complete!").',
+            uninstallTitle: '\u041a\u043e\u043c\u0430\u043d\u0434\u044b \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f',
+            btnCopyUn: '\u0421\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c',
+            footer: '\u0420\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u043e\u0444\u043b\u0430\u0439\u043d \u2014 \u0434\u0430\u043d\u043d\u044b\u0435 \u043d\u0435 \u043f\u043e\u043a\u0438\u0434\u0430\u044e\u0442 \u0432\u0430\u0448 \u0431\u0440\u0430\u0443\u0437\u0435\u0440.',
+            errEmpty: '\u041f\u043e\u043b\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043f\u0443\u0441\u0442\u043e\u0435. \u0412\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435 .conf \u0444\u0430\u0439\u043b\u0430.',
+            errPrefix: '\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u043f\u0440\u0435\u0444\u0438\u043a\u0441: \u0442\u043e\u043b\u044c\u043a\u043e \u0431\u0443\u043a\u0432\u044b, \u0446\u0438\u0444\u0440\u044b, \u0434\u0435\u0444\u0438\u0441\u044b \u0438 \u043f\u043e\u0434\u0447\u0451\u0440\u043a\u0438\u0432\u0430\u043d\u0438\u044f (1-30 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432, \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u043d\u0430\u0447\u0438\u043d\u0430\u0442\u044c\u0441\u044f/\u0437\u0430\u043a\u0430\u043d\u0447\u0438\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u0435\u0444\u0438\u0441\u043e\u043c).',
+            errProxyIp: '\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 \u043f\u0440\u043e\u043a\u0441\u0438-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.',
+            errTitle: '\u041e\u0448\u0438\u0431\u043a\u0438:',
+            ifaceSec: '\u0421\u0435\u043a\u0446\u0438\u044f [Interface]',
+            peerSec: '\u0421\u0435\u043a\u0446\u0438\u044f [Peer]',
+            copied: '\u0421\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u043e!',
+            sDisk1: 'disk1/ \u2014 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0435\u0435 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447.)',
+            sUsb1: 'usb1/ \u2014 USB-\u043d\u0430\u043a\u043e\u043f\u0438\u0442\u0435\u043b\u044c',
+            sUsb1p: 'usb1-part1/ \u2014 USB \u0440\u0430\u0437\u0434\u0435\u043b',
+            sUsb2: 'usb2/ \u2014 USB-\u043d\u0430\u043a\u043e\u043f\u0438\u0442\u0435\u043b\u044c (2-\u0439)',
+            sSd1: 'sd1/ \u2014 SD / microSD \u043a\u0430\u0440\u0442\u0430',
+            sNvme1: 'nvme1/ \u2014 NVMe SSD',
+            sSata1: 'sata1/ \u2014 SATA \u0434\u0438\u0441\u043a',
+            sCustom: '\u0414\u0440\u0443\u0433\u043e\u0435...',
+            pAwg1: 'awg-proxy-1 \u2014 1-\u0439 \u0442\u0443\u043d\u043d\u0435\u043b\u044c (\u043f\u043e \u0443\u043c\u043e\u043b\u0447.)',
+            pAwg2: 'awg-proxy-2 \u2014 2-\u0439 \u0442\u0443\u043d\u043d\u0435\u043b\u044c',
+            pAwg3: 'awg-proxy-3 \u2014 3-\u0439 \u0442\u0443\u043d\u043d\u0435\u043b\u044c',
+            pAwg4: 'awg-proxy-4 \u2014 4-\u0439 \u0442\u0443\u043d\u043d\u0435\u043b\u044c',
+            pAwg5: 'awg-proxy-5 \u2014 5-\u0439 \u0442\u0443\u043d\u043d\u0435\u043b\u044c',
+            pCustom: '\u0414\u0440\u0443\u0433\u043e\u0435...',
+            netwatchLabel: 'Netwatch failover',
+            netwatchHint: '\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0441\u0442\u044c \u0442\u0443\u043d\u043d\u0435\u043b\u044f \u043f\u0438\u043d\u0433\u043e\u043c 8.8.8.8 \u0447\u0435\u0440\u0435\u0437 WG-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 (\u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043a\u0440\u0435\u043f\u043b\u0451\u043d\u043d\u044b\u0439 probe-\u043c\u0430\u0440\u0448\u0440\u0443\u0442). \u0415\u0441\u043b\u0438 \u0442\u0443\u043d\u043d\u0435\u043b\u044c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u2014 \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u0435\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442, \u0442\u0440\u0430\u0444\u0438\u043a \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u043d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 \u0442\u0443\u043d\u043d\u0435\u043b\u044c (\u043f\u043e distance) \u0438\u043b\u0438 \u0438\u0434\u0451\u0442 \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e. \u041f\u0440\u0438 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0431\u0440\u0430\u0442\u043d\u043e.',
+            dfLabel: '\u0423\u0431\u0440\u0430\u0442\u044c DF-\u0431\u0438\u0442 \u0441 UDP (\u043e\u0431\u0445\u043e\u0434 \u0422\u0421\u041f\u0423)',
+            dfHint: '\u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 AWG_NO_DF=1 \u0432 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430: \u043f\u0440\u043e\u043a\u0441\u0438 \u0441\u043d\u0438\u043c\u0430\u0435\u0442 Don\u2019t Fragment \u0431\u0438\u0442 \u0441\u043e \u0441\u0432\u043e\u0438\u0445 UDP-\u043f\u0430\u043a\u0435\u0442\u043e\u0432. \u0427\u0430\u0441\u0442\u044c DPI-\u043e\u0431\u043e\u0440\u0443\u0434\u043e\u0432\u0430\u043d\u0438\u044f (\u0422\u0421\u041f\u0423) \u043b\u0443\u0447\u0448\u0435 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u0435\u0442 UDP \u0441 DF=0. \u0412\u043a\u043b\u044e\u0447\u0430\u0439\u0442\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0440\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430\u0445 \u0441\u043e \u0441\u0432\u044f\u0437\u044c\u044e \u2014 \u0441 DF=0 \u043a\u0440\u0443\u043f\u043d\u044b\u0435 \u043f\u0430\u043a\u0435\u0442\u044b \u043c\u043e\u0433\u0443\u0442 \u0444\u0440\u0430\u0433\u043c\u0435\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043f\u043e \u043f\u0443\u0442\u0438.',
+            scenarioLabel: '\u0421\u0446\u0435\u043d\u0430\u0440\u0438\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438',
+            scenarioTunnel: '\u0422\u043e\u043b\u044c\u043a\u043e \u0442\u0443\u043d\u043d\u0435\u043b\u044c (\u0440\u0443\u0447\u043d\u044b\u0435 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u044b)',
+            scenarioDns: 'DNS-\u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432',
+            scenarioNonRu: '\u0412\u0435\u0441\u044c \u043d\u0435-\u0420\u0424 \u0442\u0440\u0430\u0444\u0438\u043a \u0447\u0435\u0440\u0435\u0437 \u0442\u0443\u043d\u043d\u0435\u043b\u044c',
+            servicesLabel: '\u0421\u0435\u0440\u0432\u0438\u0441\u044b \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0447\u0435\u0440\u0435\u0437 \u0442\u0443\u043d\u043d\u0435\u043b\u044c',
+            exclusionsLabel: '\u0418\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0438\u0437 \u0442\u0443\u043d\u043d\u0435\u043b\u044f (\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e)',
+            btnSelectAll: '\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0432\u0441\u0435',
+            btnDeselectAll: '\u0421\u043d\u044f\u0442\u044c \u0432\u0441\u0435',
+            sAuto: 'Auto (usb1/ \u2192 disk1/)',
+            warnFasttrack: '\u041f\u0440\u0430\u0432\u0438\u043b\u0430 FastTrack \u0431\u0443\u0434\u0443\u0442 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u044b (out-interface-list=WAN), \u0447\u0442\u043e\u0431\u044b FastTrack \u043e\u0441\u0442\u0430\u0432\u0430\u043b\u0441\u044f \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u043c \u0434\u043b\u044f \u043d\u0435-VPN \u0442\u0440\u0430\u0444\u0438\u043a\u0430, \u0430 policy-\u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0430 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e. \u0421\u043a\u0440\u0438\u043f\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442 \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.',
+            noServiceSelected: '\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0440\u0432\u0438\u0441 \u0438\u043b\u0438 \u0434\u043e\u0431\u0430\u0432\u044c\u0442\u0435 \u0441\u0432\u043e\u0439 \u0434\u043e\u043c\u0435\u043d.',
+            customDomainsLabel: '\u0421\u0432\u043e\u0438 \u0434\u043e\u043c\u0435\u043d\u044b',
+            customDomainsPlaceholder: 'example.com',
+            customDomainsAdd: '+',
+            modeContainerHint: 'WireGuard-\u043a\u043b\u0438\u0435\u043d\u0442 \u043d\u0430 MikroTik + awg-proxy \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u0432 Docker-\u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0435 \u043f\u0440\u044f\u043c\u043e \u043d\u0430 \u0440\u043e\u0443\u0442\u0435\u0440\u0435',
+            modeStandaloneHint: 'WireGuard-\u043a\u043b\u0438\u0435\u043d\u0442 \u043d\u0430 MikroTik + awg-proxy \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u043d\u0430 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 (Raspberry Pi, VPS \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0435\u0440\u0432\u0435\u0440). \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u044d\u0442\u043e\u0442 \u0440\u0435\u0436\u0438\u043c \u0434\u043b\u044f MikroTik, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u044b.',
+            scenarioTunnelHint: '\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u044b \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0435 \u0441\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e \u0447\u0435\u0440\u0435\u0437 /ip/route \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u0438\u043c\u0438 \u0441\u043f\u043e\u0441\u043e\u0431\u0430\u043c\u0438. \u0414\u043b\u044f \u043e\u043f\u044b\u0442\u043d\u044b\u0445 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439.',
+            scenarioDnsHint: '\u041d\u0430\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0442\u0440\u0430\u0444\u0438\u043a \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0445 \u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432 \u0447\u0435\u0440\u0435\u0437 \u0442\u0443\u043d\u043d\u0435\u043b\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e DNS-\u0444\u043e\u0440\u0432\u0430\u0440\u0434\u0438\u043d\u0433\u0430 (DoH). MikroTik \u0440\u0435\u0437\u043e\u043b\u0432\u0438\u0442 \u0434\u043e\u043c\u0435\u043d\u044b \u0447\u0435\u0440\u0435\u0437 DNS-over-HTTPS \u0438 \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442 IP \u0432 \u0430\u0434\u0440\u0435\u0441\u043d\u044b\u0435 \u0441\u043f\u0438\u0441\u043a\u0438. Mangle-\u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0444\u0430\u0439\u0440\u0432\u043e\u043b\u0430 \u0437\u0430\u0442\u0435\u043c \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0438\u0440\u0443\u044e\u0442 \u044d\u0442\u043e\u0442 \u0442\u0440\u0430\u0444\u0438\u043a \u0447\u0435\u0440\u0435\u0437 \u0442\u0443\u043d\u043d\u0435\u043b\u044c. \u041b\u0443\u0447\u0448\u0435 \u0432\u0441\u0435\u0433\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u0447\u0438\u0441\u0442\u043e\u0439 (reset) \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438. \u0412\u0430\u0436\u043d\u043e: \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 DNS (DoH) \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435, \u0447\u0442\u043e\u0431\u044b DNS-\u0437\u0430\u043f\u0440\u043e\u0441\u044b \u0448\u043b\u0438 \u0447\u0435\u0440\u0435\u0437 MikroTik.',
+            scenarioNonRuHint: '\u0412\u0435\u0441\u044c \u0442\u0440\u0430\u0444\u0438\u043a \u0438\u0434\u0451\u0442 \u0447\u0435\u0440\u0435\u0437 \u0442\u0443\u043d\u043d\u0435\u043b\u044c, \u043a\u0440\u043e\u043c\u0435 \u0440\u043e\u0441\u0441\u0438\u0439\u0441\u043a\u0438\u0445 IP-\u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u043e\u0432 (\u0437\u0430\u0433\u0440\u0443\u0436\u0430\u044e\u0442\u0441\u044f \u0432 RAM \u043f\u0440\u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0435 \u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0435\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u043e). .rsc-\u0444\u0430\u0439\u043b \u0445\u0440\u0430\u043d\u0438\u0442\u0441\u044f \u043d\u0430 USB \u0438\u043b\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u043c \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435. \u041b\u0443\u0447\u0448\u0435 \u0432\u0441\u0435\u0433\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u0447\u0438\u0441\u0442\u043e\u0439 (reset) \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438.',
+            prefixHint: '\u041c\u0435\u043d\u044f\u0439\u0442\u0435 \u043f\u0440\u0435\u0444\u0438\u043a\u0441, \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 \u043d\u0443\u0436\u043d\u043e 2+ \u0442\u0443\u043d\u043d\u0435\u043b\u044f \u0441 \u0440\u0430\u0437\u043d\u044b\u043c\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u0430\u043c\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438',
+            modeSiteToSite: 'Site-to-site (\u0434\u0432\u0430 MikroTik)',
+            modeSiteToSiteHint: '\u0421\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0440\u043e\u0432\u043d\u043e \u0434\u0432\u0443\u0445 MikroTik \u0447\u0435\u0440\u0435\u0437 AmneziaWG-\u0442\u0443\u043d\u043d\u0435\u043b\u044c (\u0442\u043e\u0447\u043a\u0430-\u0442\u043e\u0447\u043a\u0430). WG-\u043a\u043b\u044e\u0447\u0438 \u0438 AWG-\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u044e\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438. \u0414\u043b\u044f 3 \u0438 \u0431\u043e\u043b\u0435\u0435 \u0440\u043e\u0443\u0442\u0435\u0440\u043e\u0432 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0440\u0435\u0436\u0438\u043c \u00ab\u0421\u0435\u0440\u0432\u0435\u0440 (1:N \u0445\u0430\u0431)\u00bb.',
+            s2sEndpointB: 'Endpoint \u0441\u0435\u0440\u0432\u0435\u0440\u0430 (\u0441\u0442\u043e\u0440\u043e\u043d\u0430 B, IP \u0438\u043b\u0438 \u0434\u043e\u043c\u0435\u043d)',
+            s2sTunnelSubnet: '\u041f\u043e\u0434\u0441\u0435\u0442\u044c \u0442\u0443\u043d\u043d\u0435\u043b\u044f',
+            s2sAwgPort: 'AWG-\u043f\u043e\u0440\u0442 \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435 (\u0441\u0442\u043e\u0440\u043e\u043d\u0430 B)',
+            s2sLanA: 'LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u044c \u0441\u0442\u043e\u0440\u043e\u043d\u044b A (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)',
+            s2sLanB: 'LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u044c \u0441\u0442\u043e\u0440\u043e\u043d\u044b B (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)',
+            s2sStorageA: '\u0425\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u0441\u0442\u043e\u0440\u043e\u043d\u044b A',
+            s2sStorageB: '\u0425\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u0441\u0442\u043e\u0440\u043e\u043d\u044b B',
+            s2sDns: 'DNS-\u0441\u0435\u0440\u0432\u0435\u0440 (\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u0435\u043d \u0434\u043b\u044f \u0434\u043e\u043c\u0435\u043d\u0430)',
+            s2sRegenerate: '\u041f\u0435\u0440\u0435\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c AWG-\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b',
+            s2sOutputA: '\u0421\u0442\u043e\u0440\u043e\u043d\u0430 A (MikroTik 1) \u2014 \u043a\u043b\u0438\u0435\u043d\u0442',
+            s2sOutputB: '\u0421\u0442\u043e\u0440\u043e\u043d\u0430 B (MikroTik 2) \u2014 \u0441\u0435\u0440\u0432\u0435\u0440',
+            s2sUninstallA: '\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u0441\u0442\u043e\u0440\u043e\u043d\u044b A (\u043a\u043b\u0438\u0435\u043d\u0442)',
+            s2sUninstallB: '\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u0441\u0442\u043e\u0440\u043e\u043d\u044b B (\u0441\u0435\u0440\u0432\u0435\u0440)',
+            errEndpointB: '\u0423\u043a\u0430\u0436\u0438\u0442\u0435 endpoint \u0441\u0442\u043e\u0440\u043e\u043d\u044b B (IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u0434\u043e\u043c\u0435\u043d).',
+            errDnsRequired: '\u0414\u043b\u044f hostname \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u0443\u043a\u0430\u0436\u0438\u0442\u0435 DNS-\u0441\u0435\u0440\u0432\u0435\u0440 (endpoint \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0434\u043e\u043c\u0435\u043d\u043e\u043c, \u0430 \u043d\u0435 IP-\u0430\u0434\u0440\u0435\u0441\u043e\u043c).',
+            modeServer: '\u0421\u0435\u0440\u0432\u0435\u0440 (1:N \u0445\u0430\u0431)',
+            modeServerHint: '\u0422\u043e\u043f\u043e\u043b\u043e\u0433\u0438\u044f \u0437\u0432\u0435\u0437\u0434\u0430: \u043e\u0434\u0438\u043d MikroTik \u043a\u0430\u043a \u0446\u0435\u043d\u0442\u0440\u0430\u043b\u044c\u043d\u044b\u0439 \u0443\u0437\u0435\u043b, \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438\u0445 MikroTik \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u044e\u0442\u0441\u044f \u043a \u043d\u0435\u043c\u0443 \u0447\u0435\u0440\u0435\u0437 AmneziaWG. \u041f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u0435: \u043e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u043e\u0444\u0438\u0441\u043e\u0432, \u043a\u0432\u0430\u0440\u0442\u0438\u0440, \u0443\u0434\u0430\u043b\u0451\u043d\u043d\u044b\u0445 \u0442\u043e\u0447\u0435\u043a \u0432 \u0435\u0434\u0438\u043d\u0443\u044e \u0441\u0435\u0442\u044c. WG-\u043a\u043b\u044e\u0447\u0438 \u0438 AWG-\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u044e\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438. \u041d\u043e\u0432\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u0442\u0435\u043f\u0435\u0440\u044c \u0437\u0430\u0434\u0430\u044e\u0442 AWG_CLIENT_PUBS \u0441 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u043c\u0438 \u043a\u043b\u044e\u0447\u0430\u043c\u0438 \u043f\u0440\u044f\u043c\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 AmneziaWG, \u0430 AWG_CLIENT_PUB \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u043b\u0435\u0433\u0430\u0441\u0438-fallback \u0434\u043b\u044f proxy-only \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432. \u0412\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u0435 \u0441\u0441\u044b\u043b\u043a\u0443 \u0434\u043b\u044f \u0431\u044b\u0441\u0442\u0440\u043e\u0433\u043e \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043d\u043e\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0432 \u0431\u0443\u0434\u0443\u0449\u0435\u043c.',
+            srvEndpoint: '\u041f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0439 IP \u0438\u043b\u0438 \u0434\u043e\u043c\u0435\u043d \u0441\u0435\u0440\u0432\u0435\u0440\u0430',
+            srvAwgPort: 'AWG-\u043f\u043e\u0440\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 (\u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0439)',
+            srvTunnelSubnet: '\u041f\u043e\u0434\u0441\u0435\u0442\u044c \u0442\u0443\u043d\u043d\u0435\u043b\u044f (/24)',
+            srvDns: 'DNS-\u0441\u0435\u0440\u0432\u0435\u0440 (\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u0435\u043d, \u0435\u0441\u043b\u0438 \u0432 endpoint \u0443\u043a\u0430\u0437\u0430\u043d \u0434\u043e\u043c\u0435\u043d, \u0430 \u043d\u0435 IP)',
+            srvStorageServer: '\u0425\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u0430',
+            srvStorageClient: '\u0425\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430',
+            srvLanServerOpt: 'LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440\u0430 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)',
+            srvLanClientOpt: 'LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u0430 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)',
+            srvLanServerReq: 'LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440\u0430 (\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)',
+            srvLanClientReq: 'LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u0430 (\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)',
+            srvNetLabel: '\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441\u0435\u0442\u0435\u0439',
+            srvNetNone: '\u041d\u0435\u0442',
+            srvNetL3: 'L3 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f (IP)',
+            srvNetL2: 'L2+L3 Bridging (EoIP)',
+            srvNetAwg: '\u041f\u0440\u044f\u043c\u043e\u0439 \u043a\u043b\u0438\u0435\u043d\u0442 AmneziaWG',
+            srvNetAwgHint: '\u0412\u043c\u0435\u0441\u0442\u043e \u0432\u0442\u043e\u0440\u043e\u0433\u043e MikroTik \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442 \u0433\u043e\u0442\u043e\u0432\u044b\u0439 .conf \u0434\u043b\u044f \u0438\u043c\u043f\u043e\u0440\u0442\u0430 \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 AmneziaWG (\u0442\u0435\u043b\u0435\u0444\u043e\u043d / \u043d\u043e\u0443\u0442\u0431\u0443\u043a). \u0412\u0435\u0441\u044c \u0442\u0440\u0430\u0444\u0438\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438\u0434\u0451\u0442 \u0447\u0435\u0440\u0435\u0437 \u0445\u0430\u0431 (full-tunnel, AllowedIPs = 0.0.0.0/0). \u041d\u0430 \u0433\u043e\u043b\u043e\u0432\u043d\u043e\u043c MikroTik \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e peer \u0441 \u043e\u0434\u043d\u0438\u043c IP \u0432 \u0442\u0443\u043d\u043d\u0435\u043b\u0435 \u2014 \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438\u0439 \u0440\u043e\u0443\u0442\u0435\u0440 \u043d\u0435 \u043d\u0443\u0436\u0435\u043d. \u041f\u043e\u043b\u044f \u043e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f LAN \u0437\u0434\u0435\u0441\u044c \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f.',
+            srvAwgConfTitle: '.conf \u0434\u043b\u044f AmneziaWG \u2014 \u041a\u043b\u0438\u0435\u043d\u0442',
+            srvNetNoneHint: '\u0422\u043e\u043b\u044c\u043a\u043e VPN-\u0442\u0443\u043d\u043d\u0435\u043b\u044c. \u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u043c\u0435\u0436\u0434\u0443 \u0441\u0435\u0442\u044f\u043c\u0438 \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u2014 \u0434\u0430\u043b\u044c\u043d\u0435\u0439\u0448\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441\u0435\u0442\u0438 \u043d\u0430 \u0430\u0434\u043c\u0438\u043d\u0435. \u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0434\u0430\u043b\u0451\u043d\u043d\u043e\u043c\u0443 \u0440\u043e\u0443\u0442\u0435\u0440\u0443 \u043c\u043e\u0436\u043d\u043e \u0447\u0435\u0440\u0435\u0437 \u041f\u043e\u0441\u043b\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c\u0441\u044f \u043a \u0443\u0434\u0430\u043b\u0451\u043d\u043d\u043e\u043c\u0443 MikroTik \u0447\u0435\u0440\u0435\u0437 WinBox/SSH \u043f\u043e \u0442\u0443\u043d\u043d\u0435\u043b\u044c\u043d\u043e\u043c\u0443 IP (\u043d\u0430\u043f\u0440. 10.10.10.2).',
+            srvNetL3Hint: '\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441\u043c\u043e\u0433\u0443\u0442 \u043e\u0431\u0440\u0430\u0449\u0430\u0442\u044c\u0441\u044f \u0434\u0440\u0443\u0433 \u043a \u0434\u0440\u0443\u0433\u0443 \u043f\u043e IP \u043c\u0435\u0436\u0434\u0443 \u043f\u043e\u0434\u0441\u0435\u0442\u044f\u043c\u0438. \u0421\u0442\u0430\u043d\u0435\u0442 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e: \u0434\u043e\u0441\u0442\u0443\u043f \u043a NAS/\u0444\u0430\u0439\u043b\u043e\u0432\u044b\u043c \u0448\u0430\u0440\u0430\u043c, IP-\u043a\u0430\u043c\u0435\u0440\u0430\u043c, \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430\u043c \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443, \u0443\u0434\u0430\u043b\u0451\u043d\u043d\u044b\u0439 \u0440\u0430\u0431\u043e\u0447\u0438\u0439 \u0441\u0442\u043e\u043b (RDP/VNC), \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0440\u043e\u0443\u0442\u0435\u0440\u0430\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 WinBox/SSH, \u0438\u0433\u0440\u043e\u0432\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b, \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0443\u043c\u043d\u043e\u043c\u0443 \u0434\u043e\u043c\u0443 (Home Assistant). \u041d\u0415 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442: broadcast/multicast-\u0441\u0435\u0440\u0432\u0438\u0441\u044b \u2014 \u0430\u0432\u0442\u043e\u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 (mDNS/Bonjour, DLNA, AirPlay, Chromecast), \u0441\u0435\u0442\u0435\u0432\u043e\u0435 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u0435 Windows, \u0430\u0432\u0442\u043e\u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u043e\u0432, Wake-on-LAN. \u0412\u0430\u0436\u043d\u043e: \u043a\u0430\u0436\u0434\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0432\u0438\u0434\u0438\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0441\u0435\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440\u0430. \u0427\u0442\u043e\u0431\u044b \u043a\u043b\u0438\u0435\u043d\u0442\u044b \u0432\u0438\u0434\u0435\u043b\u0438 \u0434\u0440\u0443\u0433 \u0434\u0440\u0443\u0433\u0430, \u043d\u0443\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u044b \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u2014 \u0441\u043c. README, \u0440\u0430\u0437\u0434\u0435\u043b "\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u043c\u0435\u0436\u0434\u0443 \u043a\u043b\u0438\u0435\u043d\u0442\u0430\u043c\u0438".',
+            srvNetL2Hint: '\u0415\u0434\u0438\u043d\u0430\u044f \u0441\u0435\u0442\u044c \u2014 \u0432\u0441\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043a\u0430\u043a \u0432 \u043e\u0434\u043d\u043e\u043c \u0441\u0432\u0438\u0442\u0447\u0435. \u0420\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0412\u0421\u0401: mDNS (Bonjour), DLNA, AirPlay, Chromecast, \u0441\u0435\u0442\u0435\u0432\u043e\u0435 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u0435 Windows, \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u043e\u0432. \u041c\u0438\u043d\u0443\u0441: broadcast-\u0442\u0440\u0430\u0444\u0438\u043a \u0438\u0434\u0451\u0442 \u0447\u0435\u0440\u0435\u0437 \u0442\u0443\u043d\u043d\u0435\u043b\u044c, \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0430.',
+            srvClientNum: '\u041d\u043e\u043c\u0435\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430',
+            srvRegenerate: '\u041f\u0435\u0440\u0435\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c AWG-\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b',
+            srvOutputServer: '\u0421\u0435\u0440\u0432\u0435\u0440 (\u0433\u043e\u043b\u043e\u0432\u043d\u043e\u0439 MikroTik)',
+            srvOutputClient: '\u041a\u043b\u0438\u0435\u043d\u0442',
+            srvServerUpdate: '\u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u043d\u0430 \u0421\u0415\u0420\u0412\u0415\u0420\u0415 \u0434\u043b\u044f \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u043a\u043b\u0438\u0435\u043d\u0442\u0430',
+            srvSaveLink: '\u26a0\ufe0f \u0421\u041e\u0425\u0420\u0410\u041d\u0418\u0422\u0415 \u042d\u0422\u0423 \u0421\u0421\u042b\u041b\u041a\u0423 \u0414\u041b\u042f \u0411\u0423\u0414\u0423\u0429\u0415\u0419 \u0414\u041e\u041d\u0410\u0421\u0422\u0420\u041e\u0419\u041a\u0418',
+            srvEnvHint: '\u042d\u0442\u0430 \u0441\u0441\u044b\u043b\u043a\u0430 \u0442\u0430\u043a\u0436\u0435 \u0431\u0443\u0434\u0435\u0442 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0430 \u0432 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 AWG_CONFIG_URL. \u0427\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c: /container/envs/print where key=AWG_CONFIG_URL',
+            srvAddClientBanner: '\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u043e\u0432\u043e\u0433\u043e \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443',
+            pHub1: 'awg-server-1 \u2014 1-\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447.)',
+            pHub2: 'awg-server-2 \u2014 2-\u0439 \u0441\u0435\u0440\u0432\u0435\u0440',
+            errSrvEndpoint: '\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0439 IP \u0438\u043b\u0438 \u0434\u043e\u043c\u0435\u043d \u0441\u0435\u0440\u0432\u0435\u0440\u0430.',
+            errSrvTunnelSubnet: '\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u043f\u043e\u0434\u0441\u0435\u0442\u044c. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 /24 CIDR (\u043d\u0430\u043f\u0440. 10.10.10.0/24).',
+            errSrvLanRequired: '\u0414\u043b\u044f \u043e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0441\u0435\u0442\u0435\u0439 \u043d\u0443\u0436\u043d\u044b LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u0438 \u043e\u0431\u0435\u0438\u0445 \u0441\u0442\u043e\u0440\u043e\u043d.',
+            errLanSubnet: '\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442 LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u0438. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 CIDR, \u043d\u0430\u043f\u0440. 192.168.1.0/24.',
+            errTunnelSubnet: '\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u043f\u043e\u0434\u0441\u0435\u0442\u044c. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 /30 CIDR (\u043d\u0430\u043f\u0440. 10.0.0.0/30).',
+            errAwgPort: 'AWG-\u043f\u043e\u0440\u0442 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c 1024\u201365535.',
+            scenarioNonRuWarn: '\u0412\u041d\u0418\u041c\u0410\u041d\u0418\u0415: \u0412\u0435\u0441\u044c \u0437\u0430\u0440\u0443\u0431\u0435\u0436\u043d\u044b\u0439 \u0442\u0440\u0430\u0444\u0438\u043a \u043f\u043e\u0439\u0434\u0451\u0442 \u0447\u0435\u0440\u0435\u0437 VPN. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0432\u0430\u0448 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u044b\u0434\u0435\u0440\u0436\u0438\u0442 \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0443. \u0411\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u043e VPS \u0438\u043c\u0435\u044e\u0442 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u043d\u044b\u0439 \u0442\u0440\u0430\u0444\u0438\u043a. \u0422\u0438\u043f\u0438\u0447\u043d\u043e\u0435 \u0434\u043e\u043c\u043e\u0445\u043e\u0437\u044f\u0439\u0441\u0442\u0432\u043e \u0441 \u0432\u0438\u0434\u0435\u043e\u0441\u0435\u0440\u0432\u0438\u0441\u0430\u043c\u0438 \u043b\u0435\u0433\u043a\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u043b\u044f\u0435\u0442 10+ \u0422\u0411/\u043c\u0435\u0441. \u0412\u0415\u0421\u042c \u0442\u043e\u0440\u0440\u0435\u043d\u0442-\u0442\u0440\u0430\u0444\u0438\u043a \u0442\u043e\u0436\u0435 \u043f\u043e\u0439\u0434\u0451\u0442 \u0447\u0435\u0440\u0435\u0437 \u0442\u0443\u043d\u043d\u0435\u043b\u044c (\u0430 \u044d\u0442\u043e \u041e\u0427\u0415\u041d\u042c \u043c\u043d\u043e\u0433\u043e \u0434\u0430\u043d\u043d\u044b\u0445). \u0418\u0433\u0440\u043e\u0432\u043e\u0439 \u0442\u0440\u0430\u0444\u0438\u043a \u0442\u043e\u0436\u0435 \u043c\u043e\u0436\u0435\u0442 \u043b\u0435\u0433\u043a\u043e \u0437\u0430\u0432\u0435\u0440\u043d\u0443\u0442\u044c\u0441\u044f \u0442\u0443\u0434\u0430. \u0412\u044b\u0431\u0438\u0440\u0430\u0439\u0442\u0435 \u044d\u0442\u043e\u0442 \u0432\u0430\u0440\u0438\u0430\u043d\u0442, \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 \u043d\u0430 100% \u0443\u0432\u0435\u0440\u0435\u043d\u044b. \u0421\u043f\u0438\u0441\u043e\u043a RU-\u0430\u0434\u0440\u0435\u0441\u043e\u0432 \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u0442\u0441\u044f \u0432 RAM (\u043d\u0435 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430 flash). .rsc-\u0444\u0430\u0439\u043b (~1\u041c\u0411) \u0445\u0440\u0430\u043d\u0438\u0442\u0441\u044f \u043d\u0430 USB (\u0435\u0441\u043b\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d), \u0438\u043d\u0430\u0447\u0435 \u043d\u0430 disk1 (\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f >= 1.5\u041c\u0411 \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u043e).',
+            btnExpand: '\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0432\u0441\u0451',
+            btnCollapse: '\u0421\u0432\u0435\u0440\u043d\u0443\u0442\u044c',
+            dohForwarderLabel: 'DNS-\u0444\u043e\u0440\u0432\u0430\u0440\u0434\u0435\u0440 (DoH)',
+            dohCustom: '\u0421\u0432\u043e\u0439...',
+            dohHint: '\u0412\u0441\u0435 \u0441\u0435\u0440\u0432\u0438\u0441\u044b \u0431\u0443\u0434\u0443\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0433\u043e DNS-over-HTTPS (DoH) \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0434\u043b\u044f \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u044f \u0434\u043e\u043c\u0435\u043d\u043e\u0432.',
+            dnsBrowserHint: '\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 DNS (DoH) \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435.\nChrome: \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u2192 \u041a\u043e\u043d\u0444\u0438\u0434\u0435\u043d\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u2192 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c \u2192 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 DNS \u2192 \u0412\u044b\u043a\u043b\nFirefox: \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u2192 \u041f\u0440\u0438\u0432\u0430\u0442\u043d\u043e\u0441\u0442\u044c \u2192 DNS \u0447\u0435\u0440\u0435\u0437 HTTPS \u2192 \u0412\u044b\u043a\u043b\nEdge: \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u2192 \u041a\u043e\u043d\u0444\u0438\u0434\u0435\u043d\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u2192 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c \u2192 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 DNS \u2192 \u0412\u044b\u043a\u043b\n\u0418\u043d\u0430\u0447\u0435 MikroTik \u043d\u0435 \u0443\u0432\u0438\u0434\u0438\u0442 \u0432\u0430\u0448\u0438 DNS-\u0437\u0430\u043f\u0440\u043e\u0441\u044b \u0438 \u043d\u0435 \u0441\u043c\u043e\u0436\u0435\u0442 \u043d\u0430\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u0442\u0440\u0430\u0444\u0438\u043a.',
+            igTitle: '\u041a\u0430\u043a \u044d\u0442\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442',
+            igWireguard: '\u0422\u0443\u043d\u043d\u0435\u043b\u044c WireGuard',
+            igContainer: '\u041a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 (awg-proxy)',
+            igContainerOnRouter: '\u041a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 \u043d\u0430 \u0440\u043e\u0443\u0442\u0435\u0440\u0435',
+            igStandalone: '\u0412\u043d\u0435\u0448\u043d\u0435\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e (awg-proxy)',
+            igDns: 'DNS (DoH)',
+            igRouting: '\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f',
+            igFirewall: 'Firewall / NAT',
+            igMangle: 'Mangle',
+            igInternet: '\u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442',
+            igInternetWan: '\u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 (WAN)',
+            igLan: 'LAN-\u043a\u043b\u0438\u0435\u043d\u0442\u044b',
+            igAwgServer: 'AWG-\u0441\u0435\u0440\u0432\u0435\u0440',
+            igRuList: '\u0421\u043f\u0438\u0441\u043e\u043a RU IP',
+            igScheduler: '\u041f\u043b\u0430\u043d\u0438\u0440\u043e\u0432\u0449\u0438\u043a',
+            igManualConfig: '\u0420\u0443\u0447\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430',
+            igRouterA: 'MikroTik A (\u043a\u043b\u0438\u0435\u043d\u0442)',
+            igRouterB: 'MikroTik B (\u0441\u0435\u0440\u0432\u0435\u0440)',
+            igServerHub: 'MikroTik-\u0441\u0435\u0440\u0432\u0435\u0440 (\u0445\u0430\u0431)',
+            igClientN: 'MikroTik-\u043a\u043b\u0438\u0435\u043d\u0442 #N',
+            igContainerClient: 'awg-proxy (\u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f)',
+            igContainerServer: 'awg-proxy (\u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442)',
+            igClient2: 'MikroTik-\u043a\u043b\u0438\u0435\u043d\u0442 #2',
+            igTipWireguard: '\u0422\u0443\u043d\u043d\u0435\u043b\u044c WireGuard\n\n/interface/wireguard/add \u2014 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 WireGuard-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0441 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u043c \u043a\u043b\u044e\u0447\u043e\u043c \u0438 \u043f\u043e\u0440\u0442\u043e\u043c\n/interface/wireguard/peers/add \u2014 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043f\u0438\u0440 (AWG-\u0441\u0435\u0440\u0432\u0435\u0440) \u0441 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u043c \u043a\u043b\u044e\u0447\u043e\u043c \u0438 endpoint\u2019\u043e\u043c \u043d\u0430 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\n/ip/address/add \u2014 \u043d\u0430\u0437\u043d\u0430\u0447\u0430\u0435\u0442 \u0442\u0443\u043d\u043d\u0435\u043b\u044c\u043d\u044b\u0439 IP\n\n\u042d\u0442\u043e \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0442\u0443\u043d\u043d\u0435\u043b\u044c, \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0442\u0440\u0430\u0444\u0438\u043a \u0438\u0434\u0451\u0442 \u043a AWG-\u0441\u0435\u0440\u0432\u0435\u0440\u0443.',
+            igTipContainer: '\u041a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 (awg-proxy)\n\n/interface/veth/add \u2014 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u0443\u044e Ethernet-\u043f\u0430\u0440\u0443 \u0434\u043b\u044f \u0441\u0432\u044f\u0437\u0438 \u0441 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u043e\u043c\n/container/envs/add \u2014 \u0437\u0430\u0434\u0430\u0451\u0442 AWG-\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b (Jc, Jmin, Jmax, S1, S2, H1\u2013H4, \u043a\u043b\u044e\u0447\u0438)\n/container/add + /container/start \u2014 \u0441\u043a\u0430\u0447\u0438\u0432\u0430\u0435\u0442 \u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\n\nawg-proxy \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u0443\u0435\u0442 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0435 WG-\u043f\u0430\u043a\u0435\u0442\u044b \u0432 \u0444\u043e\u0440\u043c\u0430\u0442 AmneziaWG (\u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 junk, \u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438, \u0434\u043e\u043f\u043e\u043b\u043d\u044f\u0435\u0442 handshake).',
+            igTipContainerOnRouter: '\u041a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 \u043d\u0430 \u0440\u043e\u0443\u0442\u0435\u0440\u0435\n\n\u041a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 awg-proxy \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u044f\u043c\u043e \u043d\u0430 \u0440\u043e\u0443\u0442\u0435\u0440\u0435 MikroTik.\n\n/interface/veth/add \u2014 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u0443\u044e Ethernet-\u043f\u0430\u0440\u0443\n/container/envs/add \u2014 \u0437\u0430\u0434\u0430\u0451\u0442 AWG-\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b (Jc, Jmin, Jmax, S1, S2, H1\u2013H4, \u043a\u043b\u044e\u0447\u0438)\n/container/add + /container/start \u2014 \u0441\u043a\u0430\u0447\u0438\u0432\u0430\u0435\u0442 \u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\n\nawg-proxy \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u0443\u0435\u0442 WG-\u043f\u0430\u043a\u0435\u0442\u044b \u0432 \u0444\u043e\u0440\u043c\u0430\u0442 AmneziaWG.',
+            igTipStandalone: '\u0412\u043d\u0435\u0448\u043d\u0435\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e (awg-proxy)\n\nawg-proxy \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 (Raspberry Pi, VPS \u0438 \u0442.\u0434.). WireGuard-\u043f\u0438\u0440 \u043d\u0430 MikroTik \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 endpoint \u043d\u0430 IP \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.\n\n\u041f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u0443\u0435\u0442 WG-\u0442\u0440\u0430\u0444\u0438\u043a \u0432 \u0444\u043e\u0440\u043c\u0430\u0442 AmneziaWG \u043f\u0435\u0440\u0435\u0434 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u043e\u0439 \u043d\u0430 AWG-\u0441\u0435\u0440\u0432\u0435\u0440.',
+            igTipDns: 'DNS (DoH-\u0444\u043e\u0440\u0432\u0430\u0440\u0434\u0438\u043d\u0433)\n\n/ip/dns/static/add type=FWD \u2014 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u043f\u0435\u0440\u0435\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u0434\u043e\u043c\u0435\u043d\u043e\u0432 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0445 \u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432\n\n\u041a\u043e\u0433\u0434\u0430 \u043a\u043b\u0438\u0435\u043d\u0442 \u0440\u0435\u0437\u043e\u043b\u0432\u0438\u0442 \u0434\u043e\u043c\u0435\u043d (\u043d\u0430\u043f\u0440. youtube.com), MikroTik \u043f\u0435\u0440\u0435\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441 \u0447\u0435\u0440\u0435\u0437 DoH \u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 IP \u0432 address-list.\n\u042d\u0442\u0438 address-list\u2019\u044b \u043f\u0435\u0440\u0435\u0434\u0430\u044e\u0442\u0441\u044f \u0432 Mangle, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u0430\u0440\u043a\u0438\u0440\u0443\u0435\u0442 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0442\u0440\u0430\u0444\u0438\u043a \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0447\u0435\u0440\u0435\u0437 VPN.',
+            igTipRouting: '\u041f\u043e\u043b\u0438\u0442\u0438\u043a\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438\n\n/routing/table/add \u2014 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0443\u044e \u0442\u0430\u0431\u043b\u0438\u0446\u0443 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u043e\u0432 \u0434\u043b\u044f VPN\n/ip/route/add \u2014 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0447\u0435\u0440\u0435\u0437 WG-\u0442\u0443\u043d\u043d\u0435\u043b\u044c\n/routing/rule/add \u2014 \u043f\u0440\u0438\u0432\u044f\u0437\u044b\u0432\u0430\u0435\u0442 \u043c\u0435\u0442\u043a\u0443 \u043a \u0442\u0430\u0431\u043b\u0438\u0446\u0435\n\n\u041c\u0430\u0440\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 Mangle \u0442\u0440\u0430\u0444\u0438\u043a \u0438\u0434\u0451\u0442 \u0447\u0435\u0440\u0435\u0437 \u044d\u0442\u0443 \u0442\u0430\u0431\u043b\u0438\u0446\u0443 \u0432 VPN. \u041e\u0441\u0442\u0430\u043b\u044c\u043d\u043e\u0439 \u2014 \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e.',
+            igTipFirewall: 'Firewall / NAT\n\n/ip/firewall/nat/add chain=srcnat masquerade src-address=172.18.0.0/30 \u2014 NAT \u0442\u0440\u0430\u0444\u0438\u043a\u0430 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430\n\n\u041f\u0440\u0430\u0432\u0438\u043b\u043e masquerade out-interface \u0434\u043b\u044f WG-\u0442\u0443\u043d\u043d\u0435\u043b\u044f \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u0440\u0438 \u0432\u044b\u0431\u043e\u0440\u0435 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u044f.',
+            igTipMangle: 'Mangle (\u043c\u0430\u0440\u043a\u0438\u0440\u043e\u0432\u043a\u0430 \u0442\u0440\u0430\u0444\u0438\u043a\u0430)\n\n/ip/firewall/mangle/add mark-connection \u2014 \u043c\u0430\u0440\u043a\u0438\u0440\u0443\u0435\u0442 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043a IP \u0438\u0437 DNS address-list\n/ip/firewall/mangle/add mark-routing \u2014 \u043f\u0440\u0438\u0441\u0432\u0430\u0438\u0432\u0430\u0435\u0442 \u043c\u0435\u0442\u043a\u0443 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438\n/ip/firewall/mangle/add change-mss clamp-to-pmtu \u2014 \u0438\u0441\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 MSS \u0434\u043b\u044f \u0442\u0443\u043d\u043d\u0435\u043b\u044f\n\n\u042f\u0434\u0440\u043e \u0438\u0437\u0431\u0438\u0440\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438: \u0442\u043e\u043b\u044c\u043a\u043e \u0442\u0440\u0430\u0444\u0438\u043a \u043a \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u043c \u0441\u0435\u0440\u0432\u0438\u0441\u0430\u043c \u0438\u0434\u0451\u0442 \u0447\u0435\u0440\u0435\u0437 VPN.',
+            igTipMangleNonRu: 'Mangle (\u043c\u0430\u0440\u043a\u0438\u0440\u043e\u0432\u043a\u0430 \u0442\u0440\u0430\u0444\u0438\u043a\u0430)\n\n/ip/firewall/mangle/add mark-connection dst-address-list=!RU \u2014 \u043c\u0430\u0440\u043a\u0438\u0440\u0443\u0435\u0442 \u041d\u041e\u0412\u042b\u0415 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043a \u043d\u0435-\u0420\u0424 IP (\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u043d\u0430 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435)\n/ip/firewall/mangle/add mark-routing connection-mark=... \u2014 \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u0442 routing-\u043c\u0435\u0442\u043a\u0443 \u043a\u043e \u0432\u0441\u0435\u043c \u043f\u0430\u043a\u0435\u0442\u0430\u043c \u043c\u0430\u0440\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0439 (\u0431\u044b\u0441\u0442\u0440\u043e, \u0431\u0435\u0437 address-list)\n/ip/firewall/mangle/add accept dst-address-list=vpn-server \u2014 \u0438\u0441\u043a\u043b\u044e\u0447\u0430\u0435\u0442 VPN-\u0441\u0435\u0440\u0432\u0435\u0440\n/ip/firewall/mangle/add change-mss clamp-to-pmtu \u2014 \u0438\u0441\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 MSS\n\n\u0412\u0435\u0441\u044c \u043d\u0435-\u0420\u0424 \u0442\u0440\u0430\u0444\u0438\u043a \u0438\u0434\u0451\u0442 \u0447\u0435\u0440\u0435\u0437 VPN.',
+            igTipRuList: '\u0421\u043f\u0438\u0441\u043e\u043a RU IP\n\n\u0421\u043a\u0430\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0441 GitHub, \u0445\u0440\u0430\u043d\u0438\u0442\u0441\u044f \u043d\u0430 USB/\u0434\u0438\u0441\u043a\u0435. \u0417\u0430\u043f\u0438\u0441\u0438 \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u044e\u0442\u0441\u044f \u0432 RAM \u0441 timeout 30 \u0434\u043d\u0435\u0439 (\u043d\u0435 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u044e\u0442\u0441\u044f \u043d\u0430 flash). \u041f\u0435\u0440\u0435\u0438\u043c\u043f\u043e\u0440\u0442 \u043f\u0440\u0438 \u043a\u0430\u0436\u0434\u043e\u0439 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0435 + \u0435\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u043e.\n\u041f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0435 \u0441\u0435\u0442\u0438 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u043a\u0430\u043a \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0435, \u0447\u0442\u043e\u0431\u044b \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0442\u0440\u0430\u0444\u0438\u043a \u043d\u0435 \u0448\u0451\u043b \u0432 \u0442\u0443\u043d\u043d\u0435\u043b\u044c.\n\u0415\u0441\u043b\u0438 scheduler \u0437\u0430\u043f\u0440\u0435\u0449\u0451\u043d device-mode, \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 (ru_ranges.rsc) \u0431\u0435\u0437 \u0430\u0432\u0442\u043e\u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f.',
+            igTipScheduler: '\u041f\u043b\u0430\u043d\u0438\u0440\u043e\u0432\u0449\u0438\u043a\n\n/system/scheduler/add \u2014 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u043f\u0438\u0441\u043a\u0430 RU \u043f\u0440\u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0435 (start-time=startup) \u0438 \u0435\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u043e (interval=1d).\n/system/script/add \u2014 \u0441\u043a\u0430\u0447\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u0435\u0436\u0438\u0439 .rsc, \u043e\u0447\u0438\u0449\u0430\u0435\u0442 \u0441\u0442\u0430\u0440\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438, \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u0442 \u043d\u043e\u0432\u044b\u0435 \u0432 RAM.\n\n\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u043f\u0438\u0441\u043e\u043a RU IP \u0432 \u0430\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438.\n\u0415\u0441\u043b\u0438 scheduler \u0437\u0430\u043f\u0440\u0435\u0449\u0451\u043d device-mode, \u043f\u043b\u0430\u043d\u0438\u0440\u043e\u0432\u0449\u0438\u043a\u0438 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u044e\u0442\u0441\u044f, \u0430 \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 (ru_ranges.rsc) \u0431\u0435\u0437 \u0430\u0432\u0442\u043e\u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f.',
+            igTipFirewallS2S: 'Firewall\n\n/ip/firewall/filter/add accept \u2014 \u0440\u0430\u0437\u0440\u0435\u0448\u0430\u0435\u0442 AWG-\u043f\u043e\u0440\u0442 \u043d\u0430 WAN\n/ip/firewall/nat/add dstnat \u2014 \u043f\u0435\u0440\u0435\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 AWG-\u0442\u0440\u0430\u0444\u0438\u043a \u0432 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\n/ip/firewall/nat/add masquerade \u2014 NAT \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 + \u0442\u0443\u043d\u043d\u0435\u043b\u044f\n\n\u0421\u0442\u043e\u0440\u043e\u043d\u0430 B (\u0441\u0435\u0440\u0432\u0435\u0440) \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0434\u043e\u043f. \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0439.',
+            igTipFirewallSrv: 'Firewall\n\n/ip/firewall/filter/add accept \u2014 \u0440\u0430\u0437\u0440\u0435\u0448\u0430\u0435\u0442 AWG-\u043f\u043e\u0440\u0442 \u043d\u0430 WAN\n/ip/firewall/nat/add dstnat \u2014 \u043f\u0435\u0440\u0435\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 AWG-\u0442\u0440\u0430\u0444\u0438\u043a \u0432 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\n/ip/firewall/nat/add masquerade \u2014 NAT \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 + \u0442\u0443\u043d\u043d\u0435\u043b\u044f\n/ip/route/add \u2014 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u044b \u043a LAN \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0447\u0435\u0440\u0435\u0437 WG-\u043f\u0438\u0440\u044b\n\n\u0421\u0435\u0440\u0432\u0435\u0440 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043e\u0442 \u0432\u0441\u0435\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0438\u0440\u0443\u0435\u0442 \u043c\u0435\u0436\u0434\u0443 \u0438\u0445 \u0441\u0435\u0442\u044f\u043c\u0438.',
+            igTipRoutingS2S: '\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f\n\n/ip/route/add \u2014 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u044b \u043a \u0443\u0434\u0430\u043b\u0451\u043d\u043d\u043e\u0439 LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u0438 \u0447\u0435\u0440\u0435\u0437 WG-\u0442\u0443\u043d\u043d\u0435\u043b\u044c\n\n\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u044b A \u043c\u043e\u0433\u0443\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0430\u0442\u044c\u0441\u044f \u0434\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0441\u0442\u043e\u0440\u043e\u043d\u044b B \u0438 \u043d\u0430\u043e\u0431\u043e\u0440\u043e\u0442.',
+            igTipRoutingTunnel: '\u0420\u0443\u0447\u043d\u0430\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f\n\n\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.\n\u0414\u043e\u0431\u0430\u0432\u044c\u0442\u0435 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u044b \u0432\u0440\u0443\u0447\u043d\u0443\u044e: /ip/route/add dst-address=X.X.X.X/X gateway=wg-awg-proxy\n\n\u041d\u0435 \u0437\u0430\u0431\u0443\u0434\u044c\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c NAT \u0434\u043b\u044f \u0442\u0443\u043d\u043d\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0442\u0440\u0430\u0444\u0438\u043a\u0430:\n/ip/firewall/nat/add chain=srcnat action=masquerade out-interface=wg-*\n\n\u0414\u043b\u044f \u043e\u043f\u044b\u0442\u043d\u044b\u0445 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0445\u043e\u0442\u044f\u0442 \u043f\u043e\u043b\u043d\u044b\u0439 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044c.',
+            igTipManualConfig: '\u0420\u0443\u0447\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\n\n\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430. \u0412\u044b \u0432\u044b\u0431\u0438\u0440\u0430\u0435\u0442\u0435, \u043a\u0430\u043a \u0442\u0440\u0430\u0444\u0438\u043a \u043f\u043e\u043f\u0430\u0434\u0430\u0435\u0442 \u0432 \u0442\u0443\u043d\u043d\u0435\u043b\u044c:\n\n\u041c\u0430\u0440\u0448\u0440\u0443\u0442 \u0434\u043b\u044f \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0445 \u043f\u043e\u0434\u0441\u0435\u0442\u0435\u0439:\n/ip/route/add dst-address=X.X.X.X/X gateway=wg-*\n\n\u0412\u0435\u0441\u044c \u0442\u0440\u0430\u0444\u0438\u043a \u0447\u0435\u0440\u0435\u0437 \u0442\u0443\u043d\u043d\u0435\u043b\u044c:\n/ip/route/add dst-address=0.0.0.0/0 gateway=wg-* distance=1\n\nPolicy-\u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u0447\u0435\u0440\u0435\u0437 mangle-\u043c\u0435\u0442\u043a\u0438\n\n\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 address-list \u0434\u043b\u044f \u0438\u0437\u0431\u0438\u0440\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438',
+            igTipInternetWan: '\u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 (WAN)\n\n\u041f\u0440\u044f\u043c\u043e\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0447\u0435\u0440\u0435\u0437 WAN-\u043f\u043e\u0440\u0442 MikroTik.\n\u0422\u0440\u0430\u0444\u0438\u043a, \u043d\u0435 \u043e\u0442\u043c\u0435\u0447\u0435\u043d\u043d\u044b\u0439 \u0434\u043b\u044f VPN, \u0438\u0434\u0451\u0442 \u0441\u044e\u0434\u0430 \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e.',
+            igTipRoutingSrv: '\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f (L3)\n\n/ip/route/add \u2014 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u044b \u043a \u0443\u0434\u0430\u043b\u0451\u043d\u043d\u044b\u043c LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u044f\u043c \u0447\u0435\u0440\u0435\u0437 WireGuard-\u043f\u0438\u0440\u044b\n/ip/firewall/filter/add \u2014 \u0440\u0430\u0437\u0440\u0435\u0448\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u0441\u044b\u043b\u043a\u0443 \u043c\u0435\u0436\u0434\u0443 \u0442\u0443\u043d\u043d\u0435\u043b\u0435\u043c \u0438 LAN\n\n\u0421\u0435\u0440\u0432\u0435\u0440 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0438\u0440\u0443\u0435\u0442 \u0442\u0440\u0430\u0444\u0438\u043a \u043c\u0435\u0436\u0434\u0443 \u0432\u0441\u0435\u043c\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0451\u043d\u043d\u044b\u043c\u0438 \u0441\u0435\u0442\u044f\u043c\u0438. \u041a\u0430\u0436\u0434\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 \u043a LAN \u0441\u0435\u0440\u0432\u0435\u0440\u0430. \u0414\u043b\u044f \u0441\u0432\u044f\u0437\u0438 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043c\u0435\u0436\u0434\u0443 \u0441\u043e\u0431\u043e\u0439 \u043d\u0443\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u044b \u0432\u0440\u0443\u0447\u043d\u0443\u044e.',
+            igTipLan: 'LAN-\u043a\u043b\u0438\u0435\u043d\u0442\u044b\n\n\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438. \u0418\u0445 \u0442\u0440\u0430\u0444\u0438\u043a \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442 \u0447\u0435\u0440\u0435\u0437 MikroTik, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0440\u0435\u0448\u0430\u0435\u0442: \u043d\u0430\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u0435\u0433\u043e \u0432 VPN-\u0442\u0443\u043d\u043d\u0435\u043b\u044c \u0438\u043b\u0438 \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e \u0432 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442.',
+            igTipInternet: '\u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\n\n\u041f\u0440\u044f\u043c\u043e\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0447\u0435\u0440\u0435\u0437 WAN. \u041d\u0435-VPN \u0442\u0440\u0430\u0444\u0438\u043a \u0438\u0434\u0451\u0442 \u0441\u044e\u0434\u0430. VPN-\u0442\u0440\u0430\u0444\u0438\u043a \u2014 \u0447\u0435\u0440\u0435\u0437 WG-\u0442\u0443\u043d\u043d\u0435\u043b\u044c \u043a AWG-\u0441\u0435\u0440\u0432\u0435\u0440\u0443.',
+            igTipAwgServer: 'AWG-\u0441\u0435\u0440\u0432\u0435\u0440\n\n\u0423\u0434\u0430\u043b\u0451\u043d\u043d\u044b\u0439 VPN-\u0441\u0435\u0440\u0432\u0435\u0440 \u0441 AmneziaWG. \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043f\u0430\u043a\u0435\u0442\u044b \u043e\u0442 awg-proxy \u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0438\u0440\u0443\u0435\u0442 \u0438\u0445 \u0432 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442.\n\n\u041f\u0443\u0442\u044c: \u041a\u043b\u0438\u0435\u043d\u0442 \u2192 MikroTik WG \u2192 awg-proxy \u2192 AWG-\u0441\u0435\u0440\u0432\u0435\u0440 \u2192 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442'
+        }
+    };
+
+    function t(key) { return (I18N[currentLang] && I18N[currentLang][key]) || I18N.en[key] || key; }
+
+    function setLang(lang) {
+        // Save all radio states before i18n update
+        var allRadios = document.querySelectorAll('input[type=radio]');
+        var radioState = {};
+        for (var i = 0; i < allRadios.length; i++) {
+            if (allRadios[i].checked) radioState[allRadios[i].name] = allRadios[i].value;
+        }
+        // Save select states
+        var allSelects = document.querySelectorAll('select');
+        var selectState = {};
+        for (var i = 0; i < allSelects.length; i++) {
+            if (allSelects[i].id) selectState[allSelects[i].id] = allSelects[i].value;
+        }
+        currentLang = lang;
+        applyI18n();
+        // Restore radio states
+        for (var name in radioState) {
+            var r = document.querySelector('input[type=radio][name="' + name + '"][value="' + radioState[name] + '"]');
+            if (r) r.checked = true;
+        }
+        // Restore select states
+        for (var id in selectState) {
+            var s = document.getElementById(id);
+            if (s) s.value = selectState[id];
+        }
+        onModeChange();
+        onScenarioChange();
+        if (getDeployMode() === 'server') onSrvNetChange();
+    }
+
+    function applyI18n() {
+        var els = document.querySelectorAll('[data-i18n]');
+        for (var i = 0; i < els.length; i++) {
+            els[i].textContent = t(els[i].getAttribute('data-i18n'));
+        }
+        var titles = document.querySelectorAll('[data-i18n-title]');
+        for (var i = 0; i < titles.length; i++) {
+            titles[i].title = t(titles[i].getAttribute('data-i18n-title'));
+        }
+        var phs = document.querySelectorAll('[data-i18n-ph]');
+        for (var i = 0; i < phs.length; i++) {
+            phs[i].placeholder = t(phs[i].getAttribute('data-i18n-ph'));
+        }
+        document.title = t('title');
+        document.documentElement.lang = currentLang;
+        document.getElementById('lang-toggle').textContent = currentLang === 'ru' ? 'EN' : 'RU';
+    }
+
+    // ============================================================
+    // .conf parser
+    // ============================================================
+    function parseConf(text) {
+        var result = {interface: {}, peer: {}};
+        var section = null;
+        var lines = text.split(/\r?\n/);
+        for (var i = 0; i < lines.length; i++) {
+            var line = lines[i].trim();
+            if (!line || line[0] === '#' || line[0] === ';') continue;
+            if (line === '[Interface]') {
+                section = 'interface';
+                continue;
+            }
+            if (line === '[Peer]') {
+                section = 'peer';
+                continue;
+            }
+            if (!section) continue;
+            var eq = line.indexOf('=');
+            if (eq < 0) continue;
+            result[section][line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
+        }
+        return result;
+    }
+
+    // ============================================================
+    // Field definitions
+    // ============================================================
+    var IFACE_REQUIRED = ['PrivateKey', 'Address', 'Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'H1', 'H2', 'H3', 'H4'];
+    var IFACE_OPTIONAL = ['DNS', 'S3', 'S4', 'I1', 'I2', 'I3', 'I4', 'I5'];
+    var PEER_REQUIRED = ['PublicKey', 'Endpoint', 'AllowedIPs'];
+    var PEER_OPTIONAL = ['PresharedKey', 'PersistentKeepalive'];
+
+    // ============================================================
+    // Render parsed fields as tags
+    // ============================================================
+    function renderFields(parsed) {
+        var proto = detectProtocol(parsed.interface);
+        var badgeColors = {
+            'v1':   'background:#e5e7eb;color:#374151',
+            'v1.5': 'background:#fef3c7;color:#92400e',
+            'v2':   'background:#dbeafe;color:#1e40af'
+        };
+        var protoBadge = '<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.75rem;font-weight:600;' + badgeColors[proto] + ';margin-left:8px">AWG ' + proto + '</span>';
+        var html = '<div style="margin-bottom:8px;font-size:0.8rem;color:var(--text-muted)">' + esc(t('ifaceSec')) + protoBadge + '</div>';
+        html += '<div class="fields-grid">';
+        IFACE_REQUIRED.concat(IFACE_OPTIONAL).forEach(function (k) {
+            var v = parsed.interface[k];
+            var ok = !!v;
+            var opt = IFACE_OPTIONAL.indexOf(k) >= 0;
+            var display = v ? (v.length > 18 ? v.slice(0, 15) + '...' : v) : 'missing';
+            html += '<div class="field-tag ' + (ok || opt ? 'ok' : 'miss') + '">' +
+                '<span class="fname">' + esc(k) + '</span>' +
+                '<span class="fval">' + esc(display) + '</span></div>';
+        });
+        html += '</div>';
+
+        html += '<div style="margin:14px 0 8px;font-size:0.8rem;color:var(--text-muted)">' + esc(t('peerSec')) + '</div>';
+        html += '<div class="fields-grid">';
+        PEER_REQUIRED.concat(PEER_OPTIONAL).forEach(function (k) {
+            var v = parsed.peer[k];
+            var ok = !!v;
+            var req = PEER_REQUIRED.indexOf(k) >= 0;
+            var display = v ? (v.length > 18 ? v.slice(0, 15) + '...' : v) : 'missing';
+            html += '<div class="field-tag ' + (ok || !req ? 'ok' : 'miss') + '">' +
+                '<span class="fname">' + esc(k) + '</span>' +
+                '<span class="fval">' + esc(display) + '</span></div>';
+        });
+        html += '</div>';
+        return html;
+    }
+
+    function renderAWGParamsInline(awgParams) {
+        var keys = [
+            ['Jc', awgParams.jc], ['Jmin', awgParams.jmin], ['Jmax', awgParams.jmax],
+            ['S1', awgParams.s1], ['S2', awgParams.s2],
+            ['H1', awgParams.h1], ['H2', awgParams.h2], ['H3', awgParams.h3], ['H4', awgParams.h4]
+        ];
+        var html = '';
+        for (var i = 0; i < keys.length; i++) {
+            if (keys[i][1] === undefined) continue;
+            html += '<div class="field-tag ok"><span class="fname">' + esc(keys[i][0]) + '</span><span class="fval">' + esc(String(keys[i][1])) + '</span></div>';
+        }
+        return html;
+    }
+
+    function showS2SParams() {
+        var p = ensureS2SParams();
+        document.getElementById('s2s-awg-params').innerHTML = renderAWGParamsInline(p);
+    }
+
+    function showSrvParams() {
+        var p = ensureServerParams();
+        document.getElementById('srv-awg-params').innerHTML = renderAWGParamsInline(p);
+    }
+
+    // ============================================================
+    // Validation helpers
+    // ============================================================
+    function isValidBase64Key(s) {
+        if (!s || s.length !== 44) return false;
+        try { return atob(s).length === 32; } catch(e) { return false; }
+    }
+
+    function isNonNegativeInt(s) {
+        return /^(0|[1-9]\d*)$/.test(s);
+    }
+
+    function isUint32(s) {
+        if (!isNonNegativeInt(s)) return false;
+        var n = Number(s);
+        return n >= 0 && n <= 4294967295;
+    }
+
+    function isHValue(s) {
+        if (isUint32(s)) return true;
+        var parts = s.split('-');
+        return parts.length === 2 && isUint32(parts[0]) && isUint32(parts[1])
+               && Number(parts[0]) <= Number(parts[1]);
+    }
+
+    function isValidIPCIDR(s) {
+        var m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$/.exec(s);
+        if (!m) return false;
+        for (var i = 1; i <= 4; i++) { if (Number(m[i]) > 255) return false; }
+        return Number(m[5]) <= 32;
+    }
+
+    function isValidEndpoint(s) {
+        var idx = s.lastIndexOf(':');
+        if (idx < 1) return false;
+        var port = Number(s.slice(idx + 1));
+        return port > 0 && port <= 65535;
+    }
+
+    function isValidIPv6CIDR(s) {
+        var m = /^([0-9a-fA-F:]+)\/(1[0-2][0-9]|[0-9]{1,2})$/.exec(s);
+        if (!m) return false;
+        if (Number(m[2]) > 128) return false;
+        var addr = m[1];
+        // reject leading/trailing single colons (but allow :: anywhere)
+        if (/^:[^:]/.test(addr) || /[^:]:$/.test(addr)) return false;
+        // must contain at least one colon
+        if (addr.indexOf(':') < 0) return false;
+        // at most one :: group
+        if ((addr.match(/::/g) || []).length > 1) return false;
+        return true;
+    }
+
+    // Split a comma-separated AllowedIPs list into trimmed, non-empty tokens.
+    // Tolerates trailing/double commas, stray whitespace and newlines that are
+    // common in large split-tunnel lists (thousands of subnets concatenated by a
+    // script), which previously failed validation as bogus empty entries.
+    function splitAllowedIPs(s) {
+        var out = [];
+        var parts = (s || '').split(',');
+        for (var i = 0; i < parts.length; i++) {
+            var p = parts[i].trim();
+            if (p) out.push(p);
+        }
+        return out;
+    }
+
+    // Returns the first token that is neither a valid IPv4 nor IPv6 CIDR, or null
+    // if every token is valid. Empty list -> '' (nothing valid to use).
+    function firstInvalidAllowedIP(s) {
+        var parts = splitAllowedIPs(s);
+        if (parts.length === 0) return '';
+        for (var i = 0; i < parts.length; i++) {
+            if (!isValidIPCIDR(parts[i]) && !isValidIPv6CIDR(parts[i])) return parts[i];
+        }
+        return null;
+    }
+
+    function isValidAllowedIPs(s) {
+        return firstInvalidAllowedIP(s) === null;
+    }
+
+    // Normalize for RouterOS: drop empty entries, collapse whitespace around commas.
+    function normalizeAllowedIPs(s) {
+        return splitAllowedIPs(s).join(',');
+    }
+
+    function isValidIP(s) {
+        var m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(s);
+        if (!m) return false;
+        for (var i = 1; i <= 4; i++) { if (Number(m[i]) > 255) return false; }
+        return true;
+    }
+
+    function isValidDNS(s) {
+        var parts = s.split(/\s*,\s*/);
+        for (var i = 0; i < parts.length; i++) {
+            if (!isValidIP(parts[i].trim())) return false;
+        }
+        return parts.length > 0;
+    }
+
+    function isPositiveInt(s) {
+        return /^[1-9]\d*$/.test(s);
+    }
+
+    function randomizeKeepalive(val) {
+        if (parseInt(val, 10) === 25) return String(Math.floor(Math.random() * 271) + 30);
+        return val;
+    }
+
+    function isValidCPSTemplate(s) {
+        if (!s || !s.trim()) return false;
+        var i = 0;
+        var count = 0;
+        while (i < s.length) {
+            if (s[i] === ' ' || s[i] === '\t' || s[i] === '\n' || s[i] === '\r') { i++; continue; }
+            if (s[i] !== '<') return false;
+            var end = s.indexOf('>', i + 1);
+            if (end < 0) return false;
+            var tag = s.slice(i + 1, end).trim();
+            if (!tag) return false;
+            var kind = tag[0];
+            if (kind === 'b') {
+                var hex = tag.slice(1).trim();
+                if (hex.length < 3 || hex.slice(0, 2).toLowerCase() !== '0x') return false;
+                if (!/^[0-9a-fA-F]*$/.test(hex.slice(2)) || hex.length < 4 || (hex.length - 2) % 2 !== 0) return false;
+            } else if (kind === 'r') {
+                if (tag.length > 1 && (tag[1] === 'c' || tag[1] === 'd')) {
+                    var n = tag.slice(2).trim();
+                    if (!/^[1-9]\d*$/.test(n)) return false;
+                } else {
+                    var n = tag.slice(1).trim();
+                    if (!/^[1-9]\d*$/.test(n)) return false;
+                }
+            } else if (kind === 't' || kind === 'c') {
+                if (tag.trim().length !== 1) return false;
+            } else {
+                return false;
+            }
+            count++;
+            i = end + 1;
+        }
+        return count > 0;
+    }
+
+    function detectProtocol(iface) {
+        var hasHRange = (iface.H1 && iface.H1.indexOf('-') >= 0) ||
+            (iface.H2 && iface.H2.indexOf('-') >= 0) ||
+            (iface.H3 && iface.H3.indexOf('-') >= 0) ||
+            (iface.H4 && iface.H4.indexOf('-') >= 0);
+        if (iface.S3 || iface.S4 || hasHRange) return 'v2';
+        if (iface.I1 || iface.I2 || iface.I3 || iface.I4 || iface.I5) return 'v1.5';
+        return 'v1';
+    }
+
+    // ============================================================
+    // Validate
+    // ============================================================
+    function validate(parsed) {
+        var errors = [];
+        var iface = parsed.interface;
+        var peer = parsed.peer;
+
+        // Check presence first
+        IFACE_REQUIRED.forEach(function (k) {
+            if (!iface[k]) errors.push('[Interface] ' + k + ' -- missing');
+        });
+        PEER_REQUIRED.forEach(function (k) {
+            if (!peer[k]) errors.push('[Peer] ' + k + ' -- missing');
+        });
+        if (errors.length > 0) return errors;
+
+        // Normalize Address: filter out IPv6, default mask /32
+        if (iface.Address) {
+            var addrParts = iface.Address.split(/\s*,\s*/);
+            var ipv4Addr = '';
+            for (var ai = 0; ai < addrParts.length; ai++) {
+                var ap = addrParts[ai].trim();
+                if (ap && ap.indexOf(':') < 0) { ipv4Addr = ap; break; }
+            }
+            if (ipv4Addr && ipv4Addr.indexOf('/') < 0) ipv4Addr += '/32';
+            iface.Address = ipv4Addr;
+        }
+
+        // Normalize DNS: filter out IPv6 addresses
+        if (iface.DNS) {
+            var dnsParts = iface.DNS.split(/\s*,\s*/);
+            var ipv4Dns = [];
+            for (var di = 0; di < dnsParts.length; di++) {
+                var dp = dnsParts[di].trim();
+                if (dp && dp.indexOf(':') < 0) ipv4Dns.push(dp);
+            }
+            iface.DNS = ipv4Dns.join(', ');
+        }
+
+        // Format validation
+        if (!isValidBase64Key(iface.PrivateKey))
+            errors.push('[Interface] PrivateKey -- invalid base64 key (expected 44 chars, 32 bytes)');
+        if (!isValidIPCIDR(iface.Address))
+            errors.push('[Interface] Address -- invalid format (expected X.X.X.X/N)');
+        if (iface.DNS && !isValidDNS(iface.DNS))
+            errors.push('[Interface] DNS -- invalid format (expected comma-separated IPs)');
+
+        if (!isNonNegativeInt(iface.Jc))
+            errors.push('[Interface] Jc -- must be a non-negative integer');
+        if (!isNonNegativeInt(iface.Jmin))
+            errors.push('[Interface] Jmin -- must be a non-negative integer');
+        if (!isNonNegativeInt(iface.Jmax))
+            errors.push('[Interface] Jmax -- must be a non-negative integer');
+        if (isNonNegativeInt(iface.Jmin) && isNonNegativeInt(iface.Jmax) && Number(iface.Jmin) > Number(iface.Jmax))
+            errors.push('[Interface] Jmin must be <= Jmax');
+
+        if (!isNonNegativeInt(iface.S1))
+            errors.push('[Interface] S1 -- must be a non-negative integer');
+        if (!isNonNegativeInt(iface.S2))
+            errors.push('[Interface] S2 -- must be a non-negative integer');
+        if (iface.S3 && !isNonNegativeInt(iface.S3))
+            errors.push('[Interface] S3 -- must be a non-negative integer');
+        if (iface.S4 && !isNonNegativeInt(iface.S4))
+            errors.push('[Interface] S4 -- must be a non-negative integer');
+
+        if (!isHValue(iface.H1))
+            errors.push('[Interface] H1 -- must be uint32 or range MIN-MAX');
+        if (!isHValue(iface.H2))
+            errors.push('[Interface] H2 -- must be uint32 or range MIN-MAX');
+        if (!isHValue(iface.H3))
+            errors.push('[Interface] H3 -- must be uint32 or range MIN-MAX');
+        if (!isHValue(iface.H4))
+            errors.push('[Interface] H4 -- must be uint32 or range MIN-MAX');
+
+        if (!isValidBase64Key(peer.PublicKey))
+            errors.push('[Peer] PublicKey -- invalid base64 key (expected 44 chars, 32 bytes)');
+        if (!isValidEndpoint(peer.Endpoint))
+            errors.push('[Peer] Endpoint -- invalid format (expected host:port)');
+        var badAllowedIP = firstInvalidAllowedIP(peer.AllowedIPs);
+        if (badAllowedIP !== null)
+            errors.push(badAllowedIP === ''
+                ? '[Peer] AllowedIPs -- no valid CIDR entries found'
+                : '[Peer] AllowedIPs -- invalid entry "' + (badAllowedIP.length > 40 ? badAllowedIP.slice(0, 40) + '…' : badAllowedIP) + '" (expected X.X.X.X/N or IPv6/N)');
+        if (peer.PresharedKey && !isValidBase64Key(peer.PresharedKey))
+            errors.push('[Peer] PresharedKey -- invalid base64 key (expected 44 chars, 32 bytes)');
+        if (peer.PersistentKeepalive && !isPositiveInt(peer.PersistentKeepalive))
+            errors.push('[Peer] PersistentKeepalive -- must be a positive integer');
+
+        ['I1','I2','I3','I4','I5'].forEach(function(k) {
+            if (iface[k] && !isValidCPSTemplate(iface[k]))
+                errors.push('[Interface] ' + k + ' -- invalid CPS template (valid tags: <b 0xHEX>, <r N>, <rc N>, <rd N>, <t>, <c>)');
+        });
+
+        return errors;
+    }
+
+    // ============================================================
+    // DNS helpers
+    // ============================================================
+    function parseDNSServers(s) {
+        if (!s) return [];
+        return s.split(/\s*,\s*/).filter(function(x) { return x; });
+    }
+
+    // ============================================================
+    // Services & exclusions data
+    // ============================================================
+    var SERVICES = {
+        claude:    {list:'Claude',    d:['claude.ai','anthropic.com'], r:[], on:true},
+        discord:   {list:'Discord',   d:['discord.com','discord.gg','discordapp.com','discordapp.net','dis.gd','discord.media'], r:['discord'], on:true},
+        gemini:    {list:'Gemini',    d:['gemini.google.com','aistudio.google.com','generativelanguage.googleapis.com','deepmind.google','deepmind.com'], r:[], on:false},
+        google:    {list:'GoogleWeb', d:['google.com','googleapis.com','gstatic.com','google-analytics.com','googleusercontent.com','googlesyndication.com','doubleclick.net','google.ru'], r:[], on:false},
+        instagram: {list:'Instagram', d:['ig.me','instagram.com','cdninstagram.com','instagram.net'], r:['instagram'], on:true},
+        intel:     {list:'Intel',     d:['intel.com','intel.co.uk','intel.co.jp','intel.de','intel.fr'], r:['intel'], on:false},
+        lg:        {list:'LG',        d:['lg.com','lgappstv.com','lge.com','lgsmartad.com'], r:[], on:false},
+        meta:      {list:'META',      d:['facebook.com','fbcdn.net','fb.com','fb.me','fbsbx.com','whatsapp.com','whatsapp.net','oculus.com','oculuscdn.com','instagram.com','cdninstagram.com'], r:['facebook','instagram','oculus','whatsapp'], on:false},
+        netflix:   {list:'NetFlix',   d:['netflix.com','netflix.net','nflximg.com','nflximg.net','nflxvideo.net','nflxso.net','nflxext.com','fast.com'], r:[], on:true},
+        openai:    {list:'OpenAI',    d:['openai.com','chatgpt.com','sora.com','oaiusercontent.com','oaistatic.com'], r:[], on:true},
+        rezka:     {list:'Rezka',     d:['stream.voidboost.cc','zerocdn.com'], r:['rezka'], on:false},
+        samsung:   {list:'Samsung',   d:['samsung.com','samsungcloud.com','samsungapps.com','samsungdm.com','samsungelectronics.com'], r:[], on:false},
+        spotify:   {list:'Spotify',   d:['spotify.com','spoti.fi','scdn.co','spotifycdn.com','spotifycdn.net','byspotify.com','pscdn.co','spotify-everywhere.com','spotify.design','spotifycharts.com','spotifycodes.com','spotifyforbrands.com','spotifyjobs.com'], r:[], on:true},
+        steam:     {list:'Steam',     d:['steampowered.com','steamcommunity.com','steamstatic.com','steamcdn-a.akamaihd.net','steamgames.com','steamusercontent.com','valve.software','valvesoftware.com'], r:[], on:false},
+        telegram:  {list:'Telegram',  d:['telegram.org','t.me','telesco.pe','tdesktop.com','telegram.me','telegram.dog','telegra.ph'], r:['telegram'], on:true},
+        tiktok:    {list:'TikTok',    d:['tiktok.com','tiktokcdn.com','tiktokv.com','musical.ly','tiktokcdn-us.com','tiktokd.org','byteoversea.com','ibyteimg.com','ibytedtos.com','byteimg.com'], r:[], on:true},
+        twitter:   {list:'Twitter',   d:['twitter.com','x.com','t.co','twimg.com','twitpic.com','vine.co'], r:[], on:true},
+        youtube:   {list:'YouTube',   d:['youtube.com','youtu.be','googlevideo.com','ytimg.com','yt3.ggpht.com','yt.be','youtube-nocookie.com','youtubekids.com','youtube-ui.l.google.com'], r:['youtube','googlevideo'], on:true}
+    };
+
+    var EXCLUSIONS = {
+        steam:   {list:'Steam',      label:'Steam',       d:['steampowered.com','steamcommunity.com','steamstatic.com','steamcdn-a.akamaihd.net','steamgames.com','steamusercontent.com','valve.software','valvesoftware.com'], r:[], on:true},
+        epic:    {list:'EpicGames',   label:'Epic Games',  d:['epicgames.com','unrealengine.com','fortnite.com','epicgames.dev'], r:[], on:false},
+        blizzard:{list:'Blizzard',    label:'Battle.net',  d:['blizzard.com','battle.net','blizzard.net'], r:[], on:false},
+        apple:   {list:'Apple',       label:'Apple',       d:['apple.com','icloud.com','mzstatic.com','apple-dns.net','cdn-apple.com'], r:[], on:false},
+        microsoft:{list:'Microsoft',  label:'Microsoft',   d:['microsoft.com','windowsupdate.com','live.com','office.com','office.net','msftconnecttest.com','azure.com'], r:[], on:false},
+        wot:      {list:'WoT',        label:'World of Tanks', d:['worldoftanks.eu','worldoftanks.com','worldoftanks.asia','wargaming.net','wotblitz.com','tanki.su'], r:[], ip:[
+            '92.223.1.0/24','92.223.4.0/24','92.223.5.0/24','92.223.6.0/24',
+            '92.223.8.0/24','92.223.9.0/24','92.223.10.0/24','92.223.12.0/24',
+            '92.223.14.0/24','92.223.16.0/24','92.223.18.0/24','92.223.19.0/24',
+            '92.223.20.0/24','92.223.36.0/24','92.223.37.0/24','92.223.38.0/24',
+            '92.223.54.0/24','92.223.56.0/24',
+            '185.12.240.0/24','185.12.241.0/24','185.12.242.0/24',
+            '185.12.213.0/24','185.12.214.0/24','185.12.215.0/24',
+            '88.204.200.192/26','83.229.15.0/24','45.82.31.0/24','121.78.67.0/24'
+        ], on:false}
+    };
+
+    var DOH_FORWARDERS = [
+        {doh:'https://dns.google/dns-query', name:'GoogleDNS'},
+        {doh:'https://cloudflare-dns.com/dns-query', name:'CloudFlare'},
+        {doh:'https://dns.comss.one/mikrotik', name:'Comss'},
+        {doh:'https://dns.quad9.net/dns-query', name:'Quad9'},
+        {doh:'https://common.dot.dns.yandex.net/dns-query', name:'Yandex'}
+    ];
+
+    var DNS_STATICS = [
+        {addr:'8.8.8.8', comment:'DNS GoogleDNS', name:'dns.google'},
+        {addr:'8.8.4.4', comment:'DNS GoogleDNS', name:'dns.google'},
+        {addr:'1.1.1.1', comment:'DNS CloudFlare', name:'cloudflare-dns.com'},
+        {addr:'1.0.0.1', comment:'DNS CloudFlare', name:'cloudflare-dns.com'},
+        {addr:'195.133.25.16', comment:'DNS Comss', name:'dns.comss.one'},
+        {addr:'9.9.9.9', comment:'DNS Quad9', name:'dns.quad9.net'},
+        {addr:'149.112.112.112', comment:'DNS Quad9', name:'dns.quad9.net'},
+        {addr:'77.88.8.8', comment:'DNS Yandex', name:'common.dot.dns.yandex.net'},
+        {addr:'77.88.8.1', comment:'DNS Yandex', name:'common.dot.dns.yandex.net'}
+    ];
+
+    // Map DNS server IPs to DoH forwarder names for auto-detection
+    var DNS_IP_TO_DOH = {
+        '8.8.8.8':'GoogleDNS', '8.8.4.4':'GoogleDNS',
+        '1.1.1.1':'CloudFlare', '1.0.0.1':'CloudFlare',
+        '195.133.25.16':'Comss',
+        '9.9.9.9':'Quad9', '149.112.112.112':'Quad9',
+        '77.88.8.8':'Yandex', '77.88.8.1':'Yandex'
+    };
+
+    function detectDohFromDns(dnsString) {
+        var servers = parseDNSServers(dnsString);
+        for (var i = 0; i < servers.length; i++) {
+            var name = DNS_IP_TO_DOH[servers[i]];
+            if (name) return name;
+        }
+        return null;
+    }
+
+    function autoSelectDoh(dnsString) {
+        var detected = detectDohFromDns(dnsString);
+        if (!detected) return;
+        var radios = document.getElementsByName('doh-fwd');
+        for (var i = 0; i < radios.length; i++) {
+            if (radios[i].value === detected) {
+                radios[i].checked = true;
+                document.getElementById('doh-custom-url').style.display = 'none';
+                break;
+            }
+        }
+    }
+
+    // ============================================================
+    // Scenario helpers
+    // ============================================================
+    function getScenario() {
+        var radios = document.getElementsByName('scenario');
+        for (var i = 0; i < radios.length; i++) {
+            if (radios[i].checked) return radios[i].value;
+        }
+        return 'dns-fwd';
+    }
+
+    function onScenarioChange() {
+        var mode = getDeployMode();
+        if (mode === 'server' || mode === 'site-to-site') return;
+        var s = getScenario();
+        document.getElementById('doh-panel').style.display = (s === 'dns-fwd' || s === 'non-ru') ? 'block' : 'none';
+        document.getElementById('services-panel').style.display = s === 'dns-fwd' ? 'block' : 'none';
+        document.getElementById('exclusions-panel').style.display = s === 'non-ru' ? 'block' : 'none';
+        document.getElementById('fasttrack-warn').style.display = (s === 'dns-fwd' || s === 'non-ru') ? 'block' : 'none';
+        document.getElementById('non-ru-warn').style.display = s === 'non-ru' ? 'block' : 'none';
+        document.getElementById('netwatch-row').style.display = (s === 'dns-fwd' || s === 'non-ru') ? 'block' : 'none';
+        var hintKey = s === 'dns-fwd' ? 'scenarioDnsHint' : s === 'non-ru' ? 'scenarioNonRuHint' : 'scenarioTunnelHint';
+        var hintEl = document.getElementById('scenario-hint');
+        hintEl.setAttribute('data-i18n', hintKey);
+        if (s === 'dns-fwd') {
+            hintEl.innerHTML = t(hintKey) + ' <span class="dns-info-icon" title="' + esc(t('dnsBrowserHint')).replace(/\n/g, '&#10;') + '" data-i18n-title="dnsBrowserHint">&#9432;</span>';
+        } else {
+            hintEl.textContent = t(hintKey);
+        }
+        _igRefresh();
+    }
+
+    function initServiceCheckboxes() {
+        var grid = document.getElementById('services-grid');
+        var html = '';
+        var keys = Object.keys(SERVICES);
+        for (var i = 0; i < keys.length; i++) {
+            var k = keys[i];
+            var s = SERVICES[k];
+            html += '<label><input type="checkbox" name="svc" value="' + k + '"' + (s.on ? ' checked' : '') + '> ' + esc(s.list) + '</label>';
+        }
+        grid.innerHTML = html;
+    }
+
+    function initExclusionCheckboxes() {
+        var grid = document.getElementById('exclusions-grid');
+        var html = '';
+        var keys = Object.keys(EXCLUSIONS);
+        for (var i = 0; i < keys.length; i++) {
+            var k = keys[i];
+            var ex = EXCLUSIONS[k];
+            html += '<label><input type="checkbox" name="excl" value="' + k + '"' + (ex.on ? ' checked' : '') + '> ' + esc(ex.label) + '</label>';
+        }
+        grid.innerHTML = html;
+    }
+
+    function toggleAllServices(on) {
+        var cbs = document.querySelectorAll('input[name="svc"]');
+        for (var i = 0; i < cbs.length; i++) cbs[i].checked = on;
+    }
+
+    function getSelectedServices() {
+        var cbs = document.querySelectorAll('input[name="svc"]:checked');
+        var keys = [];
+        for (var i = 0; i < cbs.length; i++) keys.push(cbs[i].value);
+        return keys;
+    }
+
+    function getSelectedExclusions() {
+        var cbs = document.querySelectorAll('input[name="excl"]:checked');
+        var keys = [];
+        for (var i = 0; i < cbs.length; i++) keys.push(cbs[i].value);
+        return keys;
+    }
+
+    // ============================================================
+    // Custom domains
+    // ============================================================
+    var customDomains = [];
+
+    function addCustomDomain() {
+        var input = document.getElementById('custom-domain-input');
+        var domain = input.value.trim().toLowerCase();
+        if (!domain || !/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*\.[a-z]{2,}$/.test(domain)) {
+            input.style.borderColor = 'var(--error)';
+            setTimeout(function() { input.style.borderColor = ''; }, 1500);
+            return;
+        }
+        if (customDomains.indexOf(domain) >= 0) {
+            input.value = '';
+            return;
+        }
+        customDomains.push(domain);
+        input.value = '';
+        renderCustomDomains();
+    }
+
+    function removeCustomDomain(domain) {
+        var idx = customDomains.indexOf(domain);
+        if (idx >= 0) customDomains.splice(idx, 1);
+        renderCustomDomains();
+    }
+
+    function renderCustomDomains() {
+        var list = document.getElementById('custom-domains-list');
+        var html = '';
+        for (var i = 0; i < customDomains.length; i++) {
+            html += '<span class="domain-tag">' + esc(customDomains[i]) +
+                '<button onclick="removeCustomDomain(\'' + esc(customDomains[i]) + '\')">&times;</button></span>';
+        }
+        list.innerHTML = html;
+    }
+
+    function getCustomDomains() {
+        return customDomains.slice();
+    }
+
+    // ============================================================
+    // Routing scenario builders
+    // ============================================================
+    function buildFwdEntries(prefix, serviceKeys, svcMap, forwarderName) {
+        var lines = [];
+        for (var i = 0; i < serviceKeys.length; i++) {
+            var svc = svcMap[serviceKeys[i]];
+            if (!svc) continue;
+            var domains = [];
+            for (var j = 0; j < svc.d.length; j++) domains.push('"' + svc.d[j] + '"');
+            lines.push(':foreach d in={' + domains.join(';') + '} do={');
+            lines.push('  :do { /ip/dns/static/add address-list=' + svc.list + ' forward-to=' + forwarderName + ' match-subdomain=yes type=FWD name=$d comment=' + prefix + '-dns-fwd } on-error={}');
+            lines.push('}');
+            if (svc.r) {
+                for (var j = 0; j < svc.r.length; j++) {
+                    lines.push(':do { /ip/dns/static/add address-list=' + svc.list + ' forward-to=' + forwarderName + ' type=FWD regexp=' + svc.r[j] + ' comment=' + prefix + '-dns-fwd } on-error={}');
+                }
+            }
+        }
+        return lines;
+    }
+
+    function buildIpEntries(prefix, serviceKeys, svcMap) {
+        var lines = [];
+        for (var i = 0; i < serviceKeys.length; i++) {
+            var svc = svcMap[serviceKeys[i]];
+            if (!svc || !svc.ip || svc.ip.length === 0) continue;
+            var nets = [];
+            for (var j = 0; j < svc.ip.length; j++) nets.push('"' + svc.ip[j] + '"');
+            lines.push(':foreach net in={' + nets.join(';') + '} do={');
+            lines.push('  :do { /ip/firewall/address-list/add list=' + svc.list + ' address=$net comment=' + prefix + '-excl-ip } on-error={}');
+            lines.push('}');
+        }
+        return lines;
+    }
+
+    function getPrefixDistance(prefix) {
+        var map = {
+            'awg-proxy-1': 1, 'awg-proxy': 1, 'awg-proxy-2': 2, 'awg-proxy-3': 3,
+            'awg-proxy-4': 4, 'awg-proxy-5': 5
+        };
+        return map[prefix] || 1;
+    }
+
+    function buildNetwatch(prefix, commentTag) {
+        // Пиннинг зонда через route-rule: пингуем 8.8.8.8 с src-address = адрес WG-интерфейса,
+        // а /routing rule по этому src уводит пробу в отдельную probe-таблицу с /32 через ЭТОТ
+        // туннель. Таблица/маршрут/правило netwatch'ем НИКОГДА не трогаются, поэтому после
+        // disable дефолт-роута зонд по-прежнему тестирует ИМЕННО этот туннель → failback честный
+        // (нет ни вечного DOWN, ни ложного UP через чужой живой путь). Один probe-target 8.8.8.8
+        // на все туннели (каждый в своей таблице), без «магических» разных IP. Комментарий
+        // '-probe' ≠ commentTag → down/up-script (find comment=commentTag) probe-объекты не задевают.
+        // Все объекты тегированы prefix → uninstall (comment~prefix) убирает их за собой.
+        var probeTable = prefix + '-probe';
+        var lines = [];
+        lines.push('# Netwatch: auto-failover for ' + prefix + ' (probe pinned via routing-rule -> ' + probeTable + ')');
+        lines.push('{');
+        // Guard: если на WG-интерфейсе нет IPv4 (битое/ручное состояние) — не роняем блок,
+        // просто логируем и пропускаем failover для этого туннеля. В штатной установке адрес
+        // ровно один. netwatch одной строкой (без \\-переносов) — безопасно внутри :if-блока.
+        lines.push(':local wgAddr ""');
+        lines.push(':do { :set wgAddr [/ip/address/get [find where interface=wg-' + prefix + '] address] } on-error={}');
+        lines.push(':if ([:len $wgAddr] > 0) do={');
+        lines.push('  :set wgAddr [:pick $wgAddr 0 [:find $wgAddr "/"]]');
+        lines.push('  :do { /routing/table/add name=' + probeTable + ' fib comment=' + commentTag + '-probe } on-error={}');
+        lines.push('  :do { /ip/route/add dst-address=8.8.8.8/32 gateway=wg-' + prefix + ' routing-table=' + probeTable + ' scope=10 comment=' + commentTag + '-probe } on-error={}');
+        lines.push('  :do { /routing/rule/add src-address=$wgAddr action=lookup table=' + probeTable + ' comment=' + commentTag + '-probe } on-error={}');
+        lines.push('  /tool/netwatch/add host=8.8.8.8 type=icmp src-address=$wgAddr interval=15s timeout=3s comment=' + commentTag + ' down-script="/ip/route/disable [find where comment=' + commentTag + ' and dst-address=0.0.0.0/0]; :log warning \\"' + prefix + ' tunnel DOWN - route disabled\\"" up-script="/ip/route/enable [find where comment=' + commentTag + ' and dst-address=0.0.0.0/0]; :log info \\"' + prefix + ' tunnel UP - route enabled\\""');
+        lines.push('} else={ :log warning ("failover: no IPv4 on wg-' + prefix + ', netwatch skipped") }');
+        lines.push('}');
+        return lines;
+    }
+
+    function buildPolicyRouting(prefix, tableName, commentTag) {
+        var distance = getPrefixDistance(prefix);
+        return [
+            ':do { /routing/table/add name=' + tableName + ' fib comment=' + commentTag + ' } on-error={}',
+            ':do { /ip/route/add dst-address=0.0.0.0/0 gateway=wg-' + prefix + ' distance=' + distance + ' routing-table=' + tableName + ' comment=' + commentTag + ' } on-error={}',
+            ':do { /routing/rule/add routing-mark=' + tableName + ' action=lookup table=' + tableName + ' comment=' + commentTag + ' } on-error={}'
+        ];
+    }
+
+    function buildFasttrackRestore(prefix) {
+        return [
+            '  :foreach rule in=[/ip/firewall/filter/find where comment~"disabled-by-' + prefix + '"] do={',
+            '    :local c [/ip/firewall/filter/get $rule comment]',
+            '    :local origComment [:pick $c ([:find $c "|"] + 1) [:len $c]]',
+            '    /ip/firewall/filter/set $rule disabled=no comment=$origComment',
+            '  }',
+            '  :foreach rule in=[/ip/firewall/filter/find where comment~"ft-modified-by-' + prefix + '"] do={',
+            '    :local c [/ip/firewall/filter/get $rule comment]',
+            '    :local origComment [:pick $c ([:find $c "|"] + 1) [:len $c]]',
+            '    /ip/firewall/filter/set $rule out-interface-list="" comment=$origComment',
+            '  }'
+        ];
+    }
+
+    function getSelectedDohForwarder() {
+        var radios = document.getElementsByName('doh-fwd');
+        for (var i = 0; i < radios.length; i++) {
+            if (radios[i].checked) return radios[i].value;
+        }
+        return 'GoogleDNS';
+    }
+
+    function getDohForwarderInfo(forwarderName) {
+        if (forwarderName === '__custom') {
+            var url = document.getElementById('doh-custom-url').value.trim();
+            if (url.indexOf('https://') === 0) {
+                return {doh: url, name: 'CustomDoH', isDoH: true};
+            }
+            return {addr: url, name: 'CustomDNS', isDoH: false};
+        }
+        for (var i = 0; i < DOH_FORWARDERS.length; i++) {
+            if (DOH_FORWARDERS[i].name === forwarderName) return {doh: DOH_FORWARDERS[i].doh, name: DOH_FORWARDERS[i].name, isDoH: true};
+        }
+        return {doh: DOH_FORWARDERS[0].doh, name: DOH_FORWARDERS[0].name, isDoH: true};
+    }
+
+    function getDnsStaticsForForwarder(forwarderName) {
+        return DNS_STATICS.filter(function(s) { return s.comment === 'DNS ' + forwarderName; });
+    }
+
+    function buildDnsRoutingScenario(prefix, selectedServices, forwarderName) {
+        var fwd = getDohForwarderInfo(forwarderName);
+        var lines = [];
+        lines.push('');
+        lines.push('# ============================================================');
+        lines.push('# DNS-based routing for selected services');
+        lines.push('# ============================================================');
+        lines.push('{');
+        if (fwd.isDoH) {
+            lines.push('# DoH forwarder');
+            lines.push(':do { /ip/dns/forwarders/add doh-servers=' + fwd.doh + ' name=' + fwd.name + ' comment=' + prefix + '-dns-fwd } on-error={}');
+            var statics = getDnsStaticsForForwarder(fwd.name);
+            if (statics.length > 0) {
+                lines.push('# DNS static entries for DoH resolver IPs');
+                for (var i = 0; i < statics.length; i++) {
+                    lines.push(':do { /ip/dns/static/add address=' + statics[i].addr + ' comment="' + prefix + '-dns-fwd" name=' + statics[i].name + ' type=A } on-error={}');
+                }
+            }
+        } else {
+            lines.push('# Custom DNS static entry');
+            lines.push(':do { /ip/dns/static/add address=' + fwd.addr + ' comment="' + prefix + '-dns-fwd" name=' + fwd.name + ' type=A } on-error={}');
+        }
+        var fwdName = fwd.name;
+        lines.push('# FWD entries');
+        lines = lines.concat(buildFwdEntries(prefix, selectedServices, SERVICES, fwdName));
+        var cDomains = getCustomDomains();
+        if (cDomains.length > 0) {
+            lines.push('# Custom domain FWD entries');
+            var domStrs = [];
+            for (var j = 0; j < cDomains.length; j++) domStrs.push('"' + cDomains[j] + '"');
+            lines.push(':foreach d in={' + domStrs.join(';') + '} do={');
+            lines.push('  :do { /ip/dns/static/add address-list=Custom forward-to=' + fwdName + ' match-subdomain=yes type=FWD name=$d comment=' + prefix + '-dns-fwd } on-error={}');
+            lines.push('}');
+        }
+        lines.push('# Policy routing');
+        lines = lines.concat(buildPolicyRouting(prefix, prefix + '-fwd-table', prefix + '-fwd'));
+        lines.push('# NAT for tunneled traffic');
+        lines.push('/ip/firewall/nat/add chain=srcnat action=masquerade out-interface=wg-' + prefix + ' comment=' + prefix + '-fwd');
+        lines.push('# Mangle rules');
+        var listNames = [];
+        for (var i = 0; i < selectedServices.length; i++) {
+            var svc = SERVICES[selectedServices[i]];
+            if (svc) listNames.push('"' + svc.list + '"');
+        }
+        if (cDomains.length > 0) listNames.push('"Custom"');
+        lines.push(':foreach l in={' + listNames.join(';') + '} do={');
+        lines.push('  /ip/firewall/mangle/add chain=prerouting action=mark-connection new-connection-mark=' + prefix + '-fwd-conn dst-address-list=$l in-interface-list=LAN passthrough=yes comment=' + prefix + '-fwd');
+        lines.push('}');
+        lines.push('/ip/firewall/mangle/add chain=prerouting action=mark-routing new-routing-mark=' + prefix + '-fwd-table connection-mark=' + prefix + '-fwd-conn in-interface-list=LAN passthrough=no comment=' + prefix + '-fwd');
+        lines.push('# MSS clamp for WireGuard (MTU 1420)');
+        lines.push('/ip/firewall/mangle/add chain=forward action=change-mss new-mss=clamp-to-pmtu passthrough=yes tcp-flags=syn protocol=tcp out-interface=wg-' + prefix + ' comment=' + prefix + '-mss');
+        lines.push('/ip/firewall/mangle/add chain=forward action=change-mss new-mss=clamp-to-pmtu passthrough=yes tcp-flags=syn protocol=tcp in-interface=wg-' + prefix + ' comment=' + prefix + '-mss');
+        lines.push('# Configure FastTrack: only for non-VPN traffic (out-interface-list=WAN)');
+        lines.push(':foreach rule in=[/ip/firewall/filter/find where action=fasttrack-connection] do={');
+        lines.push('  :local curOut [/ip/firewall/filter/get $rule out-interface-list]');
+        lines.push('  :if ($curOut = "") do={');
+        lines.push('    :local origComment [/ip/firewall/filter/get $rule comment]');
+        lines.push('    /ip/firewall/filter/set $rule out-interface-list=WAN disabled=no comment=("ft-modified-by-' + prefix + '|" . $origComment)');
+        lines.push('  }');
+        lines.push('}');
+        if (document.getElementById('netwatch-enable').checked) {
+            lines = lines.concat(buildNetwatch(prefix, prefix + '-fwd'));
+        }
+        lines.push('}');
+        return lines;
+    }
+
+    function buildNonRuScenario(prefix, exclusionKeys, forwarderName, serverHost, storageDisk, deployMode) {
+        var fwd = getDohForwarderInfo(forwarderName);
+        var isAuto = storageDisk === '__auto';
+        var isStandalone = deployMode === 'standalone';
+        var lines = [];
+        lines.push('');
+        lines.push('# ============================================================');
+        lines.push('# Route all non-RU traffic through tunnel');
+        lines.push('# ============================================================');
+
+        // DNS forwarder + statics (needed for DoH bootstrap in non-RU scenario)
+        lines.push('{');
+        if (fwd.isDoH) {
+            lines.push('# DoH forwarder');
+            lines.push(':do { /ip/dns/forwarders/add doh-servers=' + fwd.doh + ' name=' + fwd.name + ' comment=' + prefix + '-dns-fwd } on-error={}');
+            var statics = getDnsStaticsForForwarder(fwd.name);
+            if (statics.length > 0) {
+                lines.push('# DNS static entries for DoH resolver IPs');
+                for (var i = 0; i < statics.length; i++) {
+                    lines.push(':do { /ip/dns/static/add address=' + statics[i].addr + ' comment="' + prefix + '-dns-fwd" name=' + statics[i].name + ' type=A } on-error={}');
+                }
+            }
+        } else {
+            lines.push('# Custom DNS static entry');
+            lines.push(':do { /ip/dns/static/add address=' + fwd.addr + ' comment="' + prefix + '-dns-fwd" name=' + fwd.name + ' type=A } on-error={}');
+        }
+        if (exclusionKeys.length > 0) {
+            lines.push('# FWD entries for exclusions');
+            lines = lines.concat(buildFwdEntries(prefix, exclusionKeys, EXCLUSIONS, fwd.name));
+            lines = lines.concat(buildIpEntries(prefix, exclusionKeys, EXCLUSIONS));
+        }
+
+        lines.push('# Add private/special ranges to RU list (excluded from VPN routing via dst-address-list=!RU)');
+        lines.push(':foreach net in={"10.0.0.0/8";"172.16.0.0/12";"192.168.0.0/16";"100.64.0.0/10";"169.254.0.0/16";"224.0.0.0/4"} do={');
+        lines.push('  :if ([:len [/ip/firewall/address-list/find where list=RU address=$net comment=' + prefix + '-private]] = 0) do={');
+        lines.push('    /ip/firewall/address-list/add list=RU address=$net comment=' + prefix + '-private');
+        lines.push('  }');
+        lines.push('}');
+        lines.push('# Exclude NTP and DNS servers from tunnel (prevent time-sync deadlock after reboot)');
+        lines.push(':do {');
+        lines.push('  :foreach srv in=[/system/ntp/client/get servers] do={');
+        lines.push('    :do {');
+        lines.push('      :local ip [:resolve $srv]');
+        lines.push('      :if ([:len [/ip/firewall/address-list/find where list=RU address=$ip comment=' + prefix + '-ntp]] = 0) do={');
+        lines.push('        /ip/firewall/address-list/add list=RU address=$ip comment=' + prefix + '-ntp');
+        lines.push('      }');
+        lines.push('    } on-error={}');
+        lines.push('  }');
+        lines.push('} on-error={}');
+        lines.push(':do {');
+        lines.push('  :foreach srv in=[/ip/dns/get servers] do={');
+        lines.push('    :if ($srv ~ "^[0-9]") do={');
+        lines.push('      :if ([:len [/ip/firewall/address-list/find where list=RU address=$srv comment=' + prefix + '-dns]] = 0) do={');
+        lines.push('        /ip/firewall/address-list/add list=RU address=$srv comment=' + prefix + '-dns');
+        lines.push('      }');
+        lines.push('    }');
+        lines.push('  }');
+        lines.push('} on-error={}');
+
+        if (serverHost) {
+            lines.push('# Accept AWG server (direct, prevent routing loop)');
+            lines.push(':do { /ip/firewall/address-list/add address=' + serverHost + ' list=' + prefix + '-vpn-server comment=' + prefix + '-non-ru } on-error={}');
+            lines.push('/ip/firewall/mangle/add chain=prerouting action=accept dst-address-list=' + prefix + '-vpn-server in-interface-list=LAN comment=' + prefix + '-non-ru');
+        }
+        lines.push('# Policy routing');
+        lines = lines.concat(buildPolicyRouting(prefix, prefix + '-non-ru-table', prefix + '-non-ru'));
+        lines.push('# NAT for tunneled traffic');
+        lines.push('/ip/firewall/nat/add chain=srcnat action=masquerade out-interface=wg-' + prefix + ' comment=' + prefix + '-non-ru');
+        if (exclusionKeys.length > 0) {
+            lines.push('# Exclusions: accept (bypass VPN)');
+            for (var ei = 0; ei < exclusionKeys.length; ei++) {
+                var exSvc = EXCLUSIONS[exclusionKeys[ei]];
+                if (exSvc) {
+                    lines.push('/ip/firewall/mangle/add chain=prerouting action=accept dst-address-list=' + exSvc.list + ' in-interface-list=LAN comment=' + prefix + '-non-ru');
+                }
+            }
+        }
+        lines.push('# Mark non-RU connections for tunnel');
+        lines.push('/ip/firewall/mangle/add chain=prerouting action=mark-connection new-connection-mark=' + prefix + '-non-ru connection-state=new dst-address-list=!RU in-interface-list=LAN passthrough=yes comment=' + prefix + '-non-ru');
+        lines.push('/ip/firewall/mangle/add chain=prerouting action=mark-routing new-routing-mark=' + prefix + '-non-ru-table connection-mark=' + prefix + '-non-ru in-interface-list=LAN passthrough=no comment=' + prefix + '-non-ru');
+        lines.push('# MSS clamp for WireGuard (MTU 1420)');
+        lines.push('/ip/firewall/mangle/add chain=forward action=change-mss new-mss=clamp-to-pmtu passthrough=yes tcp-flags=syn protocol=tcp out-interface=wg-' + prefix + ' comment=' + prefix + '-mss');
+        lines.push('/ip/firewall/mangle/add chain=forward action=change-mss new-mss=clamp-to-pmtu passthrough=yes tcp-flags=syn protocol=tcp in-interface=wg-' + prefix + ' comment=' + prefix + '-mss');
+        lines.push('# Disable FastTrack (incompatible with policy routing)');
+        lines.push(':foreach rule in=[/ip/firewall/filter/find where action=fasttrack-connection] do={');
+        lines.push('  :local origComment [/ip/firewall/filter/get $rule comment]');
+        lines.push('  :if ([:find $origComment "disabled-by-"] < 0) do={');
+        lines.push('    /ip/firewall/filter/set $rule disabled=yes comment=("disabled-by-' + prefix + '|" . $origComment)');
+        lines.push('  }');
+        lines.push('}');
+        lines.push('}');
+
+        var scriptSrc = [];
+        scriptSrc.push(':local disk \\"disk1\\"');
+        if (isStandalone) {
+            scriptSrc.push(':do {');
+            scriptSrc.push('  :local usbFree [/disk/get [find where mount-point=\\"usb1\\"] free]');
+            scriptSrc.push('  :if (\\$usbFree > 0) do={ :set disk \\"usb1\\" }');
+            scriptSrc.push('} on-error={}');
+        } else if (isAuto) {
+            scriptSrc.push(':do { :set disk [/container/envs/get [find where list=\\"' + prefix + '-env\\" key=\\"AWG_STORAGE\\"] value] } on-error={}');
+        } else {
+            scriptSrc.push(':set disk \\"' + storageDisk + '\\"');
+        }
+        scriptSrc.push(':if (\\$disk != \\"disk1\\") do={');
+        scriptSrc.push('  :local waited 0');
+        scriptSrc.push('  :while ([:len [/disk/find where mount-point=\\$disk]] = 0 && \\$waited < 60) do={');
+        scriptSrc.push('    :delay 5s');
+        scriptSrc.push('    :set waited (\\$waited + 5)');
+        scriptSrc.push('  }');
+        scriptSrc.push('  :if ([:len [/disk/find where mount-point=\\$disk]] = 0) do={');
+        scriptSrc.push('    :log warning (\\"RU list: disk \\" . \\$disk . \\" not mounted, falling back to disk1\\")');
+        scriptSrc.push('    :set disk \\"disk1\\"');
+        scriptSrc.push('  }');
+        scriptSrc.push('}');
+        scriptSrc.push(':if (\\$disk = \\"disk1\\") do={');
+        scriptSrc.push('  :local freeSpace [/system/resource/get free-hdd-space]');
+        scriptSrc.push('  :if (\\$freeSpace < 1572864) do={');
+        scriptSrc.push('    :log error \\"RU list: insufficient disk space on disk1, need USB\\"');
+        scriptSrc.push('    :error \\"Insufficient disk space on disk1. Connect USB drive and retry.\\"');
+        scriptSrc.push('  }');
+        scriptSrc.push('}');
+        scriptSrc.push(':local schedOk true');
+        scriptSrc.push(':do { :set schedOk [/system/device-mode/get scheduler] } on-error={}');
+        scriptSrc.push(':if ([:len [/system/scheduler/find where name=\\"' + prefix + '-ru-daily\\"]] = 0) do={ :set schedOk false }');
+        scriptSrc.push(':local rscName \\"ru_ranges_timeout.rsc\\"');
+        scriptSrc.push(':if (!\\$schedOk) do={ :set rscName \\"ru_ranges.rsc\\"; :log warning \\"RU list: scheduler unavailable, using permanent list\\" }');
+        scriptSrc.push(':local rscFile (\\$disk . \\"/\\" . \\$rscName)');
+        scriptSrc.push(':local hasCache ([:len [/file/find where name=\\$rscFile]] > 0)');
+        scriptSrc.push(':if (\\$hasCache) do={');
+        scriptSrc.push('  :do { /ip/firewall/address-list/remove [find where list=RU and comment=RIPE-RU] } on-error={}');
+        scriptSrc.push('  :delay 1s');
+        scriptSrc.push('  /import file=\\$rscFile');
+        scriptSrc.push('  :log info \\"RU list: imported from cache\\"');
+        scriptSrc.push('}');
+        scriptSrc.push(':local waitNet 0');
+        scriptSrc.push(':while (\\$waitNet < 60) do={');
+        scriptSrc.push('  :do { :resolve \\"github.com\\"; :set waitNet 99 } on-error={ :delay 5s; :set waitNet (\\$waitNet + 5) }');
+        scriptSrc.push('}');
+        scriptSrc.push(':if (\\$waitNet >= 99) do={');
+        scriptSrc.push('  :do {');
+        scriptSrc.push('    /tool/fetch url=(\\"' + RU_RANGES_BASE + '\\" . \\$rscName) dst-path=\\$rscFile mode=https http-header-field=\\"User-Agent: MikroTik\\" http-max-redirect-count=10');
+        scriptSrc.push('    :delay 5s');
+        scriptSrc.push('    :log info \\"RU list: downloaded\\"');
+        scriptSrc.push('    :do { /ip/firewall/address-list/remove [find where list=RU and comment=RIPE-RU] } on-error={}');
+        scriptSrc.push('    :delay 1s');
+        scriptSrc.push('    /import file=\\$rscFile');
+        scriptSrc.push('    :log info \\"RU list: fresh version imported\\"');
+        scriptSrc.push('    :local otherName \\"ru_ranges.rsc\\"');
+        scriptSrc.push('    :if (!\\$schedOk) do={ :set otherName \\"ru_ranges_timeout.rsc\\" }');
+        scriptSrc.push('    :do { /file/remove [find where name=(\\$disk . \\"/\\" . \\$otherName)] } on-error={}');
+        scriptSrc.push('  } on-error={');
+        scriptSrc.push('    :if (!\\$hasCache) do={');
+        scriptSrc.push('      :log error \\"RU list: download failed and no cached file available\\"');
+        scriptSrc.push('    } else={');
+        scriptSrc.push('      :log warning \\"RU list: download failed, keeping cached version\\"');
+        scriptSrc.push('    }');
+        scriptSrc.push('  }');
+        scriptSrc.push('} else={');
+        scriptSrc.push('  :log warning \\"RU list: no network after 60s, skipping download\\"');
+        scriptSrc.push('}');
+
+        lines.push('# RU address list update script');
+        lines.push('/system/script/add name=' + prefix + '-ru-update comment=' + prefix + ' source="' + scriptSrc.join('\\n') + '"');
+        lines.push('# Schedulers: on boot (interval=0 required for startup trigger) + daily at 04:00');
+        lines.push('# Skipped if scheduler is not allowed by device-mode (update script falls back to permanent list)');
+        lines.push('{');
+        lines.push('  :local schedOk true');
+        lines.push('  :do { :set schedOk [/system/device-mode/get scheduler] } on-error={}');
+        lines.push('  :if ($schedOk) do={');
+        lines.push('    /system/scheduler/add name=' + prefix + '-ru-startup on-event="/system/script/run ' + prefix + '-ru-update" start-time=startup interval=0 comment=' + prefix);
+        lines.push('    /system/scheduler/add name=' + prefix + '-ru-daily on-event="/system/script/run ' + prefix + '-ru-update" start-time=04:00:00 interval=1d comment=' + prefix);
+        lines.push('  } else={');
+        lines.push('    :put "WARNING: scheduler not allowed by device-mode. RU list will be imported as permanent entries (no auto-update)."');
+        lines.push('    :put "To enable auto-updates: /system/device-mode/update scheduler=yes"');
+        lines.push('  }');
+        lines.push('}');
+        lines.push('# First run (immediate)');
+        lines.push('/system/script/run ' + prefix + '-ru-update');
+
+        if (document.getElementById('netwatch-enable').checked) {
+            lines = lines.concat(buildNetwatch(prefix, prefix + '-non-ru'));
+        }
+
+        return lines;
+    }
+
+    // ============================================================
+    // Auto-storage builder
+    // ============================================================
+    function buildAutoStorage(prefix) {
+        return [
+            '# Auto-detect storage',
+            ':global awgDisk "disk1"',
+            '{',
+            '  :local usbFree 0',
+            '  :do { :set usbFree [/disk/get [find where mount-point="usb1"] free] } on-error={}',
+            '  :if ($usbFree > 0) do={',
+            '    :set awgDisk "usb1"',
+            '    :put ("Using USB: usb1 (" . ($usbFree / 1048576) . " MB free)")',
+            '  } else={',
+            '    :if ([:len [/disk/find where slot="usb1"]] > 0) do={',
+            '      :put "USB found but not mounted. Format as ext4?"',
+            "      :put \"Press 'y' to confirm:\"",
+            '      :local key [/terminal/inkey timeout=30]',
+            '      :if ($key = 121 || $key = 89) do={',
+            '        /disk/format-drive usb1 file-system=ext4 label=usb1',
+            '        :delay 3s',
+            '        :do { :set usbFree [/disk/get [find where mount-point="usb1"] free] } on-error={}',
+            '        :if ($usbFree > 0) do={',
+            '          :set awgDisk "usb1"',
+            '        } else={ :put "Format failed, using disk1" }',
+            '      } else={ :put "Using disk1" }',
+            '    } else={ :put "No USB, using disk1" }',
+            '  }',
+            '}'
+        ];
+    }
+
+    // ============================================================
+    // Generate uninstall script as /system/script/add command (container mode)
+    // ============================================================
+    function buildUninstallScriptSource(hasDNS, storageDisk, prefix, net, scenario) {
+        var isAuto = storageDisk === '__auto';
+        var src = [];
+        src.push('/system/script/add name=' + prefix + '-uninstall comment=' + prefix + ' source={');
+        src.push('  :put "Uninstallimg AmneziaWG..."');
+        src.push('  :log info "Uninstallimg AmneziaWG..."');
+        if (hasDNS) {
+            src.push('  :local prevDNS ""');
+            src.push('  :do { :set prevDNS [/container/envs/get [find where list="' + prefix + '-env" key="AWG_PREV_DNS"] value] } on-error={}');
+        }
+        if (isAuto) {
+            src.push('  :local storageDisk "disk1"');
+            src.push('  :do { :set storageDisk [/container/envs/get [find where list="' + prefix + '-env" key="AWG_STORAGE"] value] } on-error={}');
+            src.push('  :local prevTmpdir ""');
+            src.push('  :if ($storageDisk != "disk1") do={');
+            src.push('    :do { :set prevTmpdir [/container/envs/get [find where list="' + prefix + '-env" key="AWG_PREV_TMPDIR"] value] } on-error={}');
+            src.push('  }');
+        } else if (storageDisk !== 'disk1') {
+            src.push('  :local prevTmpdir ""');
+            src.push('  :do { :set prevTmpdir [/container/envs/get [find where list="' + prefix + '-env" key="AWG_PREV_TMPDIR"] value] } on-error={}');
+        }
+        src.push('  /ip/route/remove [find where comment=' + prefix + '-tunnel]');
+        if (hasDNS) {
+            src.push('  /ip/route/remove [find where comment=' + prefix + '-dns]');
+        }
+        if (scenario !== 'tunnel') {
+            // Health-check (netwatch) гасим ПЕРВЫМ, чтобы он не дёргал маршруты во время чистки.
+            // Probe-таблицу/маршрут/правило (comment=<tag>-probe) подхватит общий prefix-sweep
+            // ниже (ip/route + routing/rule + routing/table).
+            src.push('  :do { /tool/netwatch/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/route/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/dns/static/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/dns/forwarders/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/firewall/mangle/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /routing/rule/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /routing/table/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/firewall/address-list/remove [find where list=RU and comment=RIPE-RU] } on-error={}');
+            src.push('  :do { /ip/firewall/address-list/remove [find where comment~"' + prefix + '-private"] } on-error={}');
+            src.push('  :do { /ip/firewall/address-list/remove [find where list="' + prefix + '-vpn-server"] } on-error={}');
+            src.push('  :do { /ip/firewall/address-list/remove [find where comment~"' + prefix + '-excl-ip"] } on-error={}');
+            src.push('  :do { /system/scheduler/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /system/script/remove [find where comment~"' + prefix + '" and name!="' + prefix + '-uninstall"] } on-error={}');
+            src = src.concat(buildFasttrackRestore(prefix));
+        }
+        src.push('  /container/stop [find where interface=veth-' + prefix + ']');
+        src.push('  :delay 7s');
+        src.push('  /container/remove [find where interface=veth-' + prefix + ']');
+        src.push('  /container/envs/remove [find where list="' + prefix + '-env"]');
+        if (hasDNS) {
+            src.push('  :if ($prevDNS != "") do={');
+            src.push('    :do { /ip/dns/set servers="$prevDNS" } on-error={}');
+            src.push('    :put ("DNS restored to: $prevDNS")');
+            src.push('  }');
+        }
+        if (isAuto) {
+            src.push('  :if ($storageDisk != "disk1") do={');
+            src.push('    /container/config set tmpdir=$prevTmpdir');
+            src.push('    :put ("Container tmpdir restored to: $prevTmpdir")');
+            src.push('  }');
+        } else if (storageDisk !== 'disk1') {
+            src.push('  /container/config set tmpdir=$prevTmpdir');
+            src.push('  :put ("Container tmpdir restored to: $prevTmpdir")');
+        }
+        src.push('  /ip/firewall/nat/remove [find where out-interface="wg-' + prefix + '"]');
+        src.push('  /ip/firewall/nat/remove [find where src-address="' + net.natSrc + '"]');
+        src.push('  /ip/address/remove [find where address="' + net.hostAddr + '"]');
+        src.push('  /interface/veth/remove [find where name="veth-' + prefix + '"]');
+        src.push('  /interface/wireguard/peers/remove [find where interface="wg-' + prefix + '"]');
+        src.push('  /ip/address/remove [find where interface="wg-' + prefix + '"]');
+        src.push('  /interface/wireguard/remove [find where name="wg-' + prefix + '"]');
+        src.push('  :do { /file/remove [find where name~"' + prefix + '.+tar"] } on-error={}');
+        if (isAuto) {
+            src.push('  :do { /file/remove [find where name=($storageDisk . "/' + prefix + '")] } on-error={}');
+            src.push('  :do { /file/remove [find where name=($storageDisk . "/ru_ranges_timeout.rsc")] } on-error={}');
+            src.push('  :do { /file/remove [find where name=($storageDisk . "/ru_ranges.rsc")] } on-error={}');
+            src.push('  :if ($storageDisk != "disk1") do={');
+            src.push('    :do { /file/remove [find where name~($storageDisk . "/pull")] } on-error={}');
+            src.push('  }');
+        } else {
+            src.push('  :do { /file/remove [find where name="' + storageDisk + '/' + prefix + '"] } on-error={}');
+            src.push('  :do { /file/remove [find where name="' + storageDisk + '/ru_ranges_timeout.rsc"] } on-error={}');
+            src.push('  :do { /file/remove [find where name="' + storageDisk + '/ru_ranges.rsc"] } on-error={}');
+            if (storageDisk !== 'disk1') {
+                src.push('  :do { /file/remove [find where name~"' + storageDisk + '/pull"] } on-error={}');
+            }
+        }
+        src.push('  :put "Uninstall AmneziaWG Proxy complete!"');
+        src.push('  :log info "Uninstall AmneziaWG Proxy complete!"');
+        src.push('  /system/script/remove [find where name=' + prefix + '-uninstall]');
+        src.push('}');
+        return src.join('\n');
+    }
+
+    // ============================================================
+    // Generate standalone uninstall script (no container/veth)
+    // ============================================================
+    function buildStandaloneUninstallScriptSource(hasDNS, prefix, scenario) {
+        var src = [];
+        src.push('/system/script/add name=' + prefix + '-uninstall comment=' + prefix + ' source={');
+        src.push('  :put "Uninstalling AmneziaWG Standalone..."');
+        src.push('  :log info "Uninstalling AmneziaWG Standalone..."');
+        if (hasDNS) {
+            src.push('  :local prevDNS ""');
+            src.push('  :do { :set prevDNS [/system/script/get [find where name=' + prefix + '-prev-dns] comment] } on-error={}');
+            src.push('  :if ([:len $prevDNS] > 0) do={');
+            src.push('    :do { /ip/dns/set servers="$prevDNS" } on-error={}');
+            src.push('    :put ("DNS restored to: $prevDNS")');
+            src.push('  } else={ :put "No previous DNS to restore." }');
+            src.push('  :do { /system/script/remove [find where name=' + prefix + '-prev-dns] } on-error={}');
+        }
+        src.push('  /ip/route/remove [find where comment=' + prefix + '-tunnel]');
+        if (hasDNS) {
+            src.push('  /ip/route/remove [find where comment=' + prefix + '-dns]');
+        }
+        if (scenario !== 'tunnel') {
+            // Health-check (netwatch) гасим ПЕРВЫМ, чтобы он не дёргал маршруты во время чистки.
+            // Probe-таблицу/маршрут/правило (comment=<tag>-probe) подхватит общий prefix-sweep
+            // ниже (ip/route + routing/rule + routing/table).
+            src.push('  :do { /tool/netwatch/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/route/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/dns/static/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/dns/forwarders/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/firewall/mangle/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /routing/rule/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /routing/table/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/firewall/address-list/remove [find where list=RU and comment=RIPE-RU] } on-error={}');
+            src.push('  :do { /ip/firewall/address-list/remove [find where comment~"' + prefix + '-private"] } on-error={}');
+            src.push('  :do { /ip/firewall/address-list/remove [find where list="' + prefix + '-vpn-server"] } on-error={}');
+            src.push('  :do { /ip/firewall/address-list/remove [find where comment~"' + prefix + '-excl-ip"] } on-error={}');
+            src.push('  :do { /system/scheduler/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /system/script/remove [find where comment~"' + prefix + '" and name!="' + prefix + '-uninstall"] } on-error={}');
+            src = src.concat(buildFasttrackRestore(prefix));
+        }
+        src.push('  /ip/firewall/nat/remove [find where out-interface="wg-' + prefix + '"]');
+        src.push('  /interface/wireguard/peers/remove [find where interface="wg-' + prefix + '"]');
+        src.push('  /ip/address/remove [find where interface="wg-' + prefix + '"]');
+        src.push('  /interface/wireguard/remove [find where name="wg-' + prefix + '"]');
+        src.push('  :do { /file/remove [find where name="disk1/ru_ranges_timeout.rsc"] } on-error={}');
+        src.push('  :do { /file/remove [find where name="usb1/ru_ranges_timeout.rsc"] } on-error={}');
+        src.push('  :do { /file/remove [find where name="disk1/ru_ranges.rsc"] } on-error={}');
+        src.push('  :do { /file/remove [find where name="usb1/ru_ranges.rsc"] } on-error={}');
+        src.push('  :put "Uninstall AmneziaWG Proxy Standalone complete!"');
+        src.push('  :log info "Uninstall AmneziaWG Proxy Standalone complete!"');
+        src.push('  /system/script/remove [find where name=' + prefix + '-uninstall]');
+        src.push('}');
+        return src.join('\n');
+    }
+
+    // ============================================================
+    // Generate MikroTik commands (container mode)
+    // ============================================================
+    function buildCommands(parsed, storageDisk, prefix, net, scenario) {
+        var i = parsed.interface;
+        var p = parsed.peer;
+        var dnsServers = parseDNSServers(i.DNS);
+        var hasDNS = dnsServers.length > 0;
+
+        var vethIP = net.vethAddr.replace(/\/.*/, '');
+        var peerAdd = '/interface/wireguard/peers/add interface=wg-' + prefix +
+            ' public-key="' + p.PublicKey + '"';
+        if (p.PresharedKey) peerAdd += ' preshared-key="' + p.PresharedKey + '"';
+        peerAdd += ' endpoint-address=' + vethIP + ' endpoint-port=51820' +
+            ' allowed-address=' + normalizeAllowedIPs(p.AllowedIPs);
+        if (p.PersistentKeepalive) peerAdd += ' persistent-keepalive=' + randomizeKeepalive(p.PersistentKeepalive);
+
+        var detectedProto = detectProtocol(i);
+        var lines = [
+            '# ============================================================',
+            '# AWG Proxy -- MikroTik RouterOS (protocol: ' + detectedProto + ')',
+            '# ' + new Date().toISOString().slice(0, 19).replace('T', ' '),
+            '# ============================================================',
+            '',
+            '# 0. Check prerequisites',
+            ':if ([:len [/system/package/find where name="container" disabled=no]] = 0) do={',
+            '  :put "Container package is not installed or disabled. Install it and reboot."',
+            '  :error "Usage: /system/device-mode/update container=yes"',
+            '}',
+            '',
+        ];
+
+        if (storageDisk === '__auto') {
+            lines = lines.concat(buildAutoStorage(prefix));
+            lines.push('');
+        } else if (storageDisk !== 'disk1') {
+            lines = lines.concat([
+                '# Check storage device',
+                '{',
+                '  :local targetDisk "' + storageDisk + '"',
+                '  :local diskFree 0',
+                '  :do { :set diskFree [/disk/get [find where mount-point=$targetDisk] free] } on-error={}',
+                '  :if ($diskFree = 0) do={',
+                '    :local baseDisk $targetDisk',
+                '    :local dashPos [:find $targetDisk "-part"]',
+                '    :if ([:typeof $dashPos] != "nil") do={ :set baseDisk [:pick $targetDisk 0 $dashPos] }',
+                '    :if ([:len [/disk/find where slot=$baseDisk]] > 0) do={',
+                '      :put ("Device $baseDisk found but $targetDisk is not available.")',
+                '      :put "Format $baseDisk as ext4? ALL DATA WILL BE ERASED!"',
+                "      :put \"Press 'y' to confirm, any other key to cancel:\"",
+                '      :local key [/terminal/inkey timeout=30]',
+                '      :if ($key = 121 || $key = 89) do={',
+                '        :put "Formatting $baseDisk as ext4..."',
+                '        /disk/format-drive $baseDisk file-system=ext4 label=$baseDisk',
+                '        :delay 3s',
+                '        :do { :set diskFree [/disk/get [find where mount-point=$targetDisk] free] } on-error={}',
+                '        :if ($diskFree = 0) do={',
+                '          :put ("ERROR: $targetDisk not found after formatting.")',
+                '          :put "Check mount point: /disk/print"',
+                '          :error ("Mount point $targetDisk not found after formatting")',
+                '        }',
+                '        :put ("Formatted OK. Free: " . ($diskFree / 1048576) . " MB")',
+                '      } else={',
+                '        :error "Formatting cancelled"',
+                '      }',
+                '    } else={',
+                '      :put ("ERROR: Storage device $targetDisk not found.")',
+                '      :put "Check available disks: /disk/print"',
+                '      :error ("Storage not found: " . $targetDisk)',
+                '    }',
+                '  } else={',
+                '    :put ("Storage OK: $targetDisk (" . ($diskFree / 1048576) . " MB free)")',
+                '  }',
+                '}',
+                '',
+            ]);
+        }
+
+        lines.push('# 1. Uninstall script');
+
+        lines.push(buildUninstallScriptSource(hasDNS, storageDisk, prefix, net, scenario));
+
+        lines = lines.concat([
+            '',
+            '# 2. Network infrastructure',
+            '/interface/veth/add name=veth-' + prefix + ' address=' + net.vethAddr + ' gateway=' + net.vethGw,
+            '/ip/address/add address=' + net.hostAddr + ' interface=veth-' + prefix,
+            '/ip/firewall/nat/add chain=srcnat action=masquerade src-address=' + net.natSrc,
+            '',
+            '# 3. WireGuard interface (MikroTik derives the public key automatically)',
+            '/interface/wireguard/add name=wg-' + prefix + ' private-key="' + i.PrivateKey + '" listen-port=' + net.wgPort + ' disabled=yes',
+            peerAdd,
+            '/ip/address/add address=' + i.Address + ' interface=wg-' + prefix,
+            '',
+            '# 4. Container environment variables',
+            '/container/envs/add list=' + prefix + '-env key=AWG_LISTEN value=":51820"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_REMOTE value="' + p.Endpoint + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_JC value="' + i.Jc + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_JMIN value="' + i.Jmin + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_JMAX value="' + i.Jmax + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_S1 value="' + i.S1 + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_S2 value="' + i.S2 + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_H1 value="' + i.H1 + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_H2 value="' + i.H2 + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_H3 value="' + i.H3 + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_H4 value="' + i.H4 + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_SERVER_PUB value="' + p.PublicKey + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_CLIENT_PUB value=[/interface/wireguard/get [find name=wg-' + prefix + '] public-key]',
+        ]);
+
+        if (i.S3) lines.push('/container/envs/add list=' + prefix + '-env key=AWG_S3 value="' + i.S3 + '"');
+        if (i.S4) lines.push('/container/envs/add list=' + prefix + '-env key=AWG_S4 value="' + i.S4 + '"');
+        ['I1','I2','I3','I4','I5'].forEach(function(k) {
+            if (i[k]) lines.push('/container/envs/add list=' + prefix + '-env key=AWG_' + k + ' value="' + i[k] + '"');
+        });
+        if (dfEnabled()) lines.push('/container/envs/add list=' + prefix + '-env key=AWG_NO_DF value="1"');
+
+        if (hasDNS) {
+            var dnsStr = dnsServers.join(',');
+            lines.push('');
+            lines.push('# 5. DNS');
+            lines.push('{ :local rawDNS [/ip/dns/get servers]; :local prevDNS ""; :foreach s in=[:toarray $rawDNS] do={ :if ([:len $prevDNS] > 0) do={ :set prevDNS "$prevDNS,$s" } else={ :set prevDNS $s } }; /container/envs/add list=' + prefix + '-env key=AWG_PREV_DNS value="$prevDNS" }');
+            lines.push('# NOTE: replaces existing DNS configuration');
+            lines.push('/ip/dns/set servers=' + dnsStr);
+            for (var d = 0; d < dnsServers.length; d++) {
+                lines.push('/ip/route/add dst-address=' + dnsServers[d] + '/32 gateway=wg-' + prefix + ' distance=1 comment=' + prefix + '-dns');
+            }
+        }
+
+        lines = lines.concat([
+            '',
+            '# ' + (hasDNS ? '6' : '5') + '. Download, create and start container',
+        ]);
+        var isAuto = storageDisk === '__auto';
+        var isExternal = storageDisk !== 'disk1' && !isAuto;
+        if (isAuto) {
+            lines.push('/container/envs/add list=' + prefix + '-env key=AWG_STORAGE value=$awgDisk');
+            lines.push(':if ($awgDisk != "disk1") do={');
+            lines.push('  :local tmpPath ($awgDisk . "/pull")');
+            lines.push('  /container/envs/add list=' + prefix + '-env key=AWG_PREV_TMPDIR value=[/container/config/get tmpdir]');
+            lines.push('  /container/config set tmpdir=$tmpPath memory-high=200M');
+            lines.push('}');
+        } else if (isExternal) {
+            lines.push(':local prevTmpdir [/container/config/get tmpdir]');
+            lines.push('/container/envs/add list=' + prefix + '-env key=AWG_PREV_TMPDIR value=$prevTmpdir');
+            lines.push('/container/config set tmpdir=' + storageDisk + '/pull memory-high=200M');
+        }
+        lines = lines.concat([
+            '{',
+            '  :local arch [/system/resource/get architecture-name]',
+            '  :local ver [/system/resource/get version]',
+            '  :local dotPos [:find $ver "."]',
+            '  :local rest [:pick $ver ($dotPos + 1) [:len $ver]]',
+            '  :local endPos [:find $rest "."]',
+            '  :if ([:typeof $endPos] = "nil") do={ :set endPos [:find $rest " "] }',
+            '  :local minor [:tonum [:pick $rest 0 $endPos]]',
+            '  :local suffix ""',
+            '  :if ($minor <= 20) do={ :set suffix "-7.20-Docker" }',
+            '  :local file ""',
+            '  :if ($arch = "arm64") do={ :set file ("awg-proxy-arm64" . $suffix . ".tar.gz") }',
+            '  :if ($arch = "arm") do={',
+            '    :set file ("awg-proxy-arm" . $suffix . ".tar.gz")',
+            '    :local rbModel ""',
+            '    :do { :set rbModel [/system/routerboard/get model] } on-error={}',
+            '    :local bName [/system/resource/get board-name]',
+            '    :if ($rbModel~"E50UG" || $rbModel~"E60iUGS" || $bName~"^hEX") do={',
+            '      :set file ("awg-proxy-armv5" . $suffix . ".tar.gz")',
+            '    }',
+            '  }',
+            '  :if ($arch ~ "x86") do={ :set file ("awg-proxy-amd64" . $suffix . ".tar.gz") }',
+            '  :if ($file = "") do={ :error "Unsupported architecture: $arch" }',
+            '  :local url "' + REPO.dl + '$file"',
+        ]);
+        if (isAuto) {
+            lines.push('  :local filePath $file');
+            lines.push('  :if ($awgDisk != "disk1") do={');
+            lines.push('    :set filePath ($awgDisk . "/pull/" . $file)');
+            lines.push('    :do { /file/make-directory ($awgDisk . "/pull") } on-error={}');
+            lines.push('  }');
+        } else if (isExternal) {
+            lines.push('  :local filePath ("' + storageDisk + '/pull/" . $file)');
+            lines.push('  :do { /file/make-directory ' + storageDisk + '/pull } on-error={}');
+        } else {
+            lines.push('  :local filePath $file');
+        }
+        lines = lines.concat([
+            '  :if ([:len [/file/find where name=$filePath]] = 0) do={',
+            '    :local freeStorage 0',
+        ]);
+        if (isAuto) {
+            lines.push('    :if ($awgDisk = "disk1") do={');
+            lines.push('      :set freeStorage [/system/resource/get free-hdd-space]');
+            lines.push('    } else={');
+            lines.push('      :do { :set freeStorage [/disk/get [find where mount-point=$awgDisk] free] } on-error={}');
+            lines.push('    }');
+        } else if (storageDisk === 'disk1') {
+            lines.push('    :set freeStorage [/system/resource/get free-hdd-space]');
+        } else {
+            lines.push('    :do { :set freeStorage [/disk/get [find where mount-point="' + storageDisk + '"] free] } on-error={}');
+        }
+        lines = lines.concat([
+            '    :if ($freeStorage < 262144) do={',
+            '      :put ("WARNING: Low disk space (" . ($freeStorage / 1024) . "KB free). Need at least 256KB.")',
+            '      :put "See: ' + REPO.base + '#insufficient-disk-space"',
+            '      :error "Insufficient disk space"',
+            '    }',
+            '    :put ("Fetching: $url -> " . $filePath)',
+            '    /tool/fetch url=$url dst-path=$filePath http-max-redirect-count=10',
+            '  } else={',
+            '    :put ("File already exists: " . $filePath)',
+            '  }',
+        ]);
+        if (isAuto) {
+            lines.push('  :local rootDir ($awgDisk . "/' + prefix + '")');
+            lines.push('  /container/add file=$filePath interface=veth-' + prefix + ' envlist=' + prefix + '-env hostname=' + prefix + ' root-dir=$rootDir logging=no start-on-boot=yes comment=' + prefix);
+        } else {
+            lines.push('  /container/add file=$filePath interface=veth-' + prefix + ' envlist=' + prefix + '-env hostname=' + prefix + ' root-dir=' + storageDisk + '/' + prefix + ' logging=no start-on-boot=yes comment=' + prefix);
+        }
+        lines = lines.concat([
+            '  :if ($minor > 20) do={ [:parse "/container/set [find where interface=veth-' + prefix + '] shm-size=4M"] }',
+            '  /file/remove $filePath',
+            '  :local freeMem [/system/resource/get free-memory]',
+            '  :if ($freeMem < 16777216) do={',
+            '    :put ("WARNING: Low memory (" . ($freeMem / 1048576) . "MB free). 16MB+ recommended.")',
+            '  }',
+            '  /container/start [find where interface=veth-' + prefix + ']',
+            '  :do { /file/remove [find where name="console-dump.txt"] } on-error={}',
+            '  :put "Waiting for container to start..."',
+            '  :delay 5s',
+            '  /interface/wireguard/enable wg-' + prefix,
+            '  :put "WireGuard interface enabled"',
+            '  :put "Installation complete!"',
+        ]);
+
+        if (hasDNS) {
+            var firstDNS = dnsServers[0];
+            lines = lines.concat([
+                '  :put "Ping test: ' + firstDNS + ' via AmneziaWG tunnel. repeat 10 times"',
+                '  /ping ' + firstDNS + ' count=10 interval=1',
+            ]);
+        }
+
+        lines = lines.concat([
+            '  :put ""',
+            '  :put "======= NEXT STEPS ======="',
+        ]);
+
+        if (detectedProto !== 'v2') {
+            lines = lines.concat([
+                '  :put ""',
+                '  :put "  [WARNING]: Your config uses AWG ' + detectedProto + '. For better obfuscation,"',
+                '  :put "  update AmneziaVPN (v2) to the latest version and regenerate the config."',
+                '  :put "  Latest version supports AWG v2 with H-ranges and S3/S4 padding."',
+                '  :put "  https://github.com/amnezia-vpn/amnezia-client/releases"',
+            ]);
+        }
+
+        lines = lines.concat([
+            '  :put ""',
+            '  :put "Route specific traffic through the AmneziaWG tunnel:"',
+            '  :put ""',
+            '  :put "  Route a single host:"',
+            '  :put "    /ip/route/add dst-address=8.8.8.8/32 gateway=wg-' + prefix + ' comment=' + prefix + '-tunnel"',
+            '  :put ""',
+            '  :put "  Route a subnet:"',
+            '  :put "    /ip/route/add dst-address=100.0.0.0/8 gateway=wg-' + prefix + ' comment=' + prefix + '-tunnel"',
+            '  :put ""',
+            '  :put "  Remove a route:"',
+            '  :put "    /ip/route/remove [find where comment=' + prefix + '-tunnel dst-address=8.8.8.8/32]"',
+            '  :put ""',
+            '  :put "  Show all tunnel routes:"',
+            '  :put "    /ip/route/print where comment=' + prefix + '-tunnel"',
+            '  :put ""',
+            '  :put "  Uninstall:"',
+            '  :put "    /system/script/run ' + prefix + '-uninstall"',
+            '  :put "========================="',
+            '}',
+            '',
+            '# ' + (hasDNS ? '7' : '6') + '. Verification',
+            '# /container/print',
+            '# /interface/wireguard/print',
+            '# /interface/wireguard/peers/print',
+        ]);
+
+        lines.push('');
+
+        return lines.join('\n');
+    }
+
+    // ============================================================
+    // Generate MikroTik commands (standalone mode)
+    // ============================================================
+    function buildStandaloneCommands(parsed, proxyIP, prefix, net, scenario) {
+        var i = parsed.interface;
+        var p = parsed.peer;
+        var dnsServers = parseDNSServers(i.DNS);
+        var hasDNS = dnsServers.length > 0;
+        var detectedProto = detectProtocol(i);
+
+        var peerAdd = '/interface/wireguard/peers/add interface=wg-' + prefix +
+            ' public-key="' + p.PublicKey + '"';
+        if (p.PresharedKey) peerAdd += ' preshared-key="' + p.PresharedKey + '"';
+        peerAdd += ' endpoint-address=' + proxyIP + ' endpoint-port=51820' +
+            ' allowed-address=' + normalizeAllowedIPs(p.AllowedIPs);
+        if (p.PersistentKeepalive) peerAdd += ' persistent-keepalive=' + randomizeKeepalive(p.PersistentKeepalive);
+
+        var lines = [
+            '# ============================================================',
+            '# AWG Proxy -- MikroTik RouterOS Standalone (protocol: ' + detectedProto + ')',
+            '# Proxy device: ' + proxyIP,
+            '# ' + new Date().toISOString().slice(0, 19).replace('T', ' '),
+            '# ============================================================',
+            '',
+            '# 1. Uninstall script',
+        ];
+
+        lines.push(buildStandaloneUninstallScriptSource(hasDNS, prefix, scenario));
+
+        lines = lines.concat([
+            '',
+            '# 2. WireGuard interface',
+            '/interface/wireguard/add name=wg-' + prefix + ' private-key="' + i.PrivateKey + '" listen-port=' + net.wgPort,
+            peerAdd,
+            '/ip/address/add address=' + i.Address + ' interface=wg-' + prefix,
+            '',
+        ]);
+
+        var stepN = 3;
+        if (hasDNS) {
+            var dnsStr = dnsServers.join(',');
+            lines.push('# ' + stepN + '. DNS');
+            lines.push('{ :local rawDNS [/ip/dns/get servers]; :local prevDNS ""; :foreach s in=[:toarray $rawDNS] do={ :if ([:len $prevDNS] > 0) do={ :set prevDNS "$prevDNS,$s" } else={ :set prevDNS $s } }; /system/script/add name=' + prefix + '-prev-dns comment="$prevDNS" source="" }');
+            lines.push('/ip/dns/set servers=' + dnsStr);
+            for (var d = 0; d < dnsServers.length; d++) {
+                lines.push('/ip/route/add dst-address=' + dnsServers[d] + '/32 gateway=wg-' + prefix + ' distance=1 comment=' + prefix + '-dns');
+            }
+            lines.push('');
+            stepN++;
+        }
+
+        lines.push('# ' + stepN + '. Enable WireGuard');
+        lines.push('/interface/wireguard/enable wg-' + prefix);
+        lines.push('');
+        stepN++;
+
+        lines.push('# ' + stepN + '. Proxy device setup instructions');
+        lines.push('{');
+        lines.push('  :put "======= AWG PROXY SETUP ======="');
+        lines.push('  :put ""');
+        lines.push('  :put "Run awg-proxy on your external device (' + proxyIP + ')."');
+        lines.push('  :put ""');
+        lines.push('  :put "1. Download the binary for your platform:"');
+        lines.push('  :put "   Linux arm64: ' + REPO.dl + 'awg-proxy-linux-arm64"');
+        lines.push('  :put "   Linux arm:   ' + REPO.dl + 'awg-proxy-linux-arm"');
+        lines.push('  :put "   Linux armv5: ' + REPO.dl + 'awg-proxy-linux-armv5"');
+        lines.push('  :put "   Linux amd64: ' + REPO.dl + 'awg-proxy-linux-amd64"');
+        lines.push('  :put ""');
+        lines.push('  :put "2. Set environment variables and run:"');
+        lines.push('  :put "export AWG_LISTEN=\\":51820\\""');
+        lines.push('  :put "export AWG_REMOTE=\\"' + p.Endpoint + '\\""');
+        lines.push('  :put "export AWG_JC=\\"' + i.Jc + '\\""');
+        lines.push('  :put "export AWG_JMIN=\\"' + i.Jmin + '\\""');
+        lines.push('  :put "export AWG_JMAX=\\"' + i.Jmax + '\\""');
+        lines.push('  :put "export AWG_S1=\\"' + i.S1 + '\\""');
+        lines.push('  :put "export AWG_S2=\\"' + i.S2 + '\\""');
+        lines.push('  :put "export AWG_H1=\\"' + i.H1 + '\\""');
+        lines.push('  :put "export AWG_H2=\\"' + i.H2 + '\\""');
+        lines.push('  :put "export AWG_H3=\\"' + i.H3 + '\\""');
+        lines.push('  :put "export AWG_H4=\\"' + i.H4 + '\\""');
+        lines.push('  :put "export AWG_SERVER_PUB=\\"' + p.PublicKey + '\\""');
+
+        lines.push('  :put ("export AWG_CLIENT_PUB=\\"" . [/interface/wireguard/get [find name=wg-' + prefix + '] public-key] . "\\"")');
+
+        if (i.S3) lines.push('  :put "export AWG_S3=\\"' + i.S3 + '\\""');
+        if (i.S4) lines.push('  :put "export AWG_S4=\\"' + i.S4 + '\\""');
+        ['I1','I2','I3','I4','I5'].forEach(function(k) {
+            if (i[k]) lines.push('  :put "export AWG_' + k + '=\\"' + i[k] + '\\""');
+        });
+        if (dfEnabled()) lines.push('  :put "export AWG_NO_DF=\\"1\\""');
+
+        lines.push('  :put ""');
+        lines.push('  :put "chmod +x awg-proxy-linux-* && ./awg-proxy-linux-<arch>"');
+        lines.push('  :put ""');
+        lines.push('  :put "3. (Optional) Create a systemd service for auto-start."');
+
+        if (detectedProto !== 'v2') {
+            lines.push('  :put ""');
+            lines.push('  :put "[WARNING]: Your config uses AWG ' + detectedProto + '. For better obfuscation,"');
+            lines.push('  :put "update AmneziaVPN (v2) to the latest version and regenerate the config."');
+            lines.push('  :put "https://github.com/amnezia-vpn/amnezia-client/releases"');
+        }
+
+        lines = lines.concat([
+            '  :put ""',
+            '  :put "Route specific traffic through the AmneziaWG tunnel:"',
+            '  :put ""',
+            '  :put "  Route a single host:"',
+            '  :put "    /ip/route/add dst-address=8.8.8.8/32 gateway=wg-' + prefix + ' comment=' + prefix + '-tunnel"',
+            '  :put ""',
+            '  :put "  Route a subnet:"',
+            '  :put "    /ip/route/add dst-address=100.0.0.0/8 gateway=wg-' + prefix + ' comment=' + prefix + '-tunnel"',
+            '  :put ""',
+            '  :put "  Remove a route:"',
+            '  :put "    /ip/route/remove [find where comment=' + prefix + '-tunnel dst-address=8.8.8.8/32]"',
+            '  :put ""',
+            '  :put "  Show all tunnel routes:"',
+            '  :put "    /ip/route/print where comment=' + prefix + '-tunnel"',
+            '  :put ""',
+            '  :put "  Uninstall:"',
+            '  :put "    /system/script/run ' + prefix + '-uninstall"',
+            '  :put "================================"',
+            '}',
+            '',
+        ]);
+
+        return lines.join('\n');
+    }
+
+    // ============================================================
+    // Syntax highlight
+    // ============================================================
+    function highlight(text) {
+        return esc(text)
+            .replace(/^(# .*)$/mg, '<span class="comment">$1</span>')
+            .replace(/^(\/.+?\/\w+\/\w+)/mg, '<span class="cmd">$1</span>');
+    }
+
+    function esc(s) {
+        return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+    }
+
+    // ============================================================
+    // Mode selector
+    // ============================================================
+    function getDeployMode() {
+        var radios = document.getElementsByName('deploy-mode');
+        for (var i = 0; i < radios.length; i++) {
+            if (radios[i].checked) return radios[i].value;
+        }
+        return 'container';
+    }
+
+    function dfEnabled() {
+        var el = document.getElementById('df-enable');
+        return !!(el && el.checked);
+    }
+
+    function onModeChange() {
+        var mode = getDeployMode();
+        var isS2S = mode === 'site-to-site';
+        var isSrv = mode === 'server';
+        var hideConf = isS2S || isSrv;
+        document.getElementById('storage-row').style.display = mode === 'container' ? 'block' : 'none';
+        document.getElementById('proxy-ip-row').style.display = mode === 'standalone' ? 'block' : 'none';
+        document.getElementById('s2s-panel').style.display = isS2S ? 'block' : 'none';
+        document.getElementById('srv-panel').style.display = isSrv ? 'block' : 'none';
+        document.getElementById('conf-input').style.display = hideConf ? 'none' : 'block';
+        document.querySelector('label[for="conf-input"]').style.display = hideConf ? 'none' : 'block';
+        document.getElementById('scenario-row').style.display = hideConf ? 'none' : 'block';
+        if (hideConf) {
+            document.getElementById('doh-panel').style.display = 'none';
+            document.getElementById('services-panel').style.display = 'none';
+            document.getElementById('exclusions-panel').style.display = 'none';
+            document.getElementById('fasttrack-warn').style.display = 'none';
+            document.getElementById('non-ru-warn').style.display = 'none';
+            document.getElementById('netwatch-row').style.display = 'none';
+        } else {
+            onScenarioChange();
+        }
+        document.getElementById('prefix-select').parentElement.style.display = 'block';
+        var hintKey = isSrv ? 'modeServerHint' : (isS2S ? 'modeSiteToSiteHint' : (mode === 'container' ? 'modeContainerHint' : 'modeStandaloneHint'));
+        document.getElementById('mode-hint').setAttribute('data-i18n', hintKey);
+        document.getElementById('mode-hint').textContent = t(hintKey);
+        // Show/hide hub prefix options
+        var prefixSel = document.getElementById('prefix-select');
+        var hubOpts = prefixSel.querySelectorAll('[value^="awg-server-"]');
+        var proxyOpts = prefixSel.querySelectorAll('[value^="awg-proxy"]');
+        for (var i = 0; i < hubOpts.length; i++) hubOpts[i].style.display = isSrv ? '' : 'none';
+        for (var i = 0; i < proxyOpts.length; i++) proxyOpts[i].style.display = isSrv ? 'none' : '';
+        if (isSrv && prefixSel.value.indexOf('awg-proxy') === 0) prefixSel.value = 'awg-server-1';
+        if (!isSrv && prefixSel.value.indexOf('awg-server') === 0) prefixSel.value = 'awg-proxy-1';
+        if (isS2S) ensureS2SParams();
+        if (isSrv) ensureServerParams();
+        _igRefresh();
+    }
+
+    // ============================================================
+    // Storage selector
+    // ============================================================
+    document.getElementById('storage-select').addEventListener('change', function() {
+        var custom = document.getElementById('storage-custom');
+        if (this.value === '__custom') {
+            custom.style.display = 'inline-block';
+            custom.focus();
+        } else {
+            custom.style.display = 'none';
+            custom.value = '';
+        }
+    });
+
+    function getStorageDisk() {
+        var sel = document.getElementById('storage-select').value;
+        if (sel === '__custom') {
+            var v = document.getElementById('storage-custom').value.trim();
+            return v || 'disk1';
+        }
+        return sel;
+    }
+
+    document.getElementById('prefix-select').addEventListener('change', function() {
+        var custom = document.getElementById('prefix-custom');
+        if (this.value === '__custom') {
+            custom.style.display = 'inline-block';
+            custom.focus();
+        } else {
+            custom.style.display = 'none';
+            custom.value = '';
+        }
+    });
+
+    function getPrefixName() {
+        var sel = document.getElementById('prefix-select').value;
+        if (sel === '__custom') {
+            var v = document.getElementById('prefix-custom').value.trim();
+            return v || 'awg-proxy-1';
+        }
+        return sel;
+    }
+
+    // Deterministically derives a unique /30 veth subnet and WireGuard listen port
+    // from the tunnel name (prefix), so multiple tunnels on the same MikroTik
+    // don't collide. Uses a simple djb2-style hash spread across different bit
+    // ranges: bits 0-3 → 2nd octet (172.16-31.x.x), bits 4-11 → 3rd octet,
+    // bits 12-17 → 4th octet base (0-252, aligned to /30), bits 18+ → WG port.
+    function getTunnelNetwork(prefix) {
+        var h = 0;
+        for (var i = 0; i < prefix.length; i++)
+            h = ((h * 31) + prefix.charCodeAt(i)) & 0xFFFFFFFF;
+        h = h >>> 0;
+        var oct2 = 16 + (h & 0xF);
+        var oct3 = (h >>> 4) & 0xFF;
+        var oct4base = ((h >>> 12) & 0x3F) * 4;
+        var wgPort = 12000 + ((h >>> 18) % 20000);
+        return {
+            vethAddr: '172.' + oct2 + '.' + oct3 + '.' + (oct4base + 2) + '/30',
+            hostAddr: '172.' + oct2 + '.' + oct3 + '.' + (oct4base + 1) + '/30',
+            vethGw:   '172.' + oct2 + '.' + oct3 + '.' + (oct4base + 1),
+            natSrc:   '172.' + oct2 + '.' + oct3 + '.' + oct4base + '/30',
+            wgPort:   wgPort
+        };
+    }
+
+    // ============================================================
+    // Curve25519 key generation (Montgomery ladder, BigInt)
+    // ============================================================
+    function curve25519ScalarMult(scalar, point) {
+        var P = (1n << 255n) - 19n;
+        function mod(a) { a = a % P; return a < 0n ? a + P : a; }
+        function modPow(b, e, m) {
+            var r = 1n; b = ((b % m) + m) % m;
+            while (e > 0n) { if (e & 1n) r = r * b % m; e >>= 1n; b = b * b % m; }
+            return r;
+        }
+        function inv(a) { return modPow(a, P - 2n, P); }
+
+        var k = new Uint8Array(scalar);
+        k[0] &= 248; k[31] &= 127; k[31] |= 64;
+        var kn = 0n;
+        for (var i = 31; i >= 0; i--) kn = (kn << 8n) | BigInt(k[i]);
+
+        var u = 0n;
+        for (var i = 31; i >= 0; i--) u = (u << 8n) | BigInt(point[i]);
+
+        var x_1 = u, x_2 = 1n, z_2 = 0n, x_3 = u, z_3 = 1n, swap = 0n;
+        var A24 = 121665n;
+
+        for (var pos = 254; pos >= 0; pos--) {
+            var bit = (kn >> BigInt(pos)) & 1n;
+            swap ^= bit;
+            // cswap
+            var d1 = swap * (x_2 - x_3), d2 = swap * (z_2 - z_3);
+            x_2 = mod(x_2 - d1); x_3 = mod(x_3 + d1);
+            z_2 = mod(z_2 - d2); z_3 = mod(z_3 + d2);
+            swap = bit;
+
+            var A = mod(x_2 + z_2), AA = mod(A * A);
+            var B = mod(x_2 - z_2), BB = mod(B * B);
+            var E = mod(AA - BB);
+            var C = mod(x_3 + z_3), D = mod(x_3 - z_3);
+            var DA = mod(D * A), CB = mod(C * B);
+            x_3 = mod(mod(DA + CB) * mod(DA + CB));
+            z_3 = mod(x_1 * mod(mod(DA - CB) * mod(DA - CB)));
+            x_2 = mod(AA * BB);
+            z_2 = mod(E * mod(AA + mod(A24 * E)));
+        }
+        // final cswap
+        var d1 = swap * (x_2 - x_3), d2 = swap * (z_2 - z_3);
+        x_2 = mod(x_2 - d1); z_2 = mod(z_2 - d2);
+
+        var result = mod(x_2 * inv(z_2));
+        var out = new Uint8Array(32);
+        for (var i = 0; i < 32; i++) { out[i] = Number(result & 0xFFn); result >>= 8n; }
+        return out;
+    }
+
+    function generateWgKeyPair() {
+        var priv = new Uint8Array(32);
+        crypto.getRandomValues(priv);
+        var basepoint = new Uint8Array(32);
+        basepoint[0] = 9;
+        var pub = curve25519ScalarMult(priv, basepoint);
+        return {
+            privateKey: uint8ToBase64(priv),
+            publicKey: uint8ToBase64(pub)
+        };
+    }
+
+    function uint8ToBase64(arr) {
+        var s = '';
+        for (var i = 0; i < arr.length; i++) s += String.fromCharCode(arr[i]);
+        return btoa(s);
+    }
+
+    // ============================================================
+    // Random AWG parameter generation
+    // ============================================================
+    function randomInt(min, max) {
+        var range = max - min + 1;
+        var arr = new Uint32Array(1);
+        crypto.getRandomValues(arr);
+        return min + (arr[0] % range);
+    }
+
+    function randomUint32() {
+        var arr = new Uint32Array(1);
+        crypto.getRandomValues(arr);
+        return arr[0];
+    }
+
+    function generateAWGParams() {
+        return {
+            jc: randomInt(3, 8),
+            jmin: randomInt(50, 250),
+            jmax: randomInt(500, 1000),
+            s1: randomInt(15, 150),
+            s2: randomInt(15, 150),
+            h1: randomUint32(),
+            h2: randomUint32(),
+            h3: randomUint32(),
+            h4: randomUint32(),
+            port: randomInt(10000, 60000),
+            wgListenPort: randomInt(10000, 60000)
+        };
+    }
+
+    // Site-to-site uses a v2 primary profile (H ranges + S3) with a v1 fallback
+    // (fixed H) for DPI resilience: if the primary obfuscation stops getting
+    // through, both sides fall back to the secondary profile automatically.
+    function randomHRange() {
+        var base = randomUint32();
+        var width = randomInt(8, 64);
+        if (base > 4294967295 - width) base -= width;
+        return { min: base, max: base + width };
+    }
+
+    function hRangesOverlap(a, b) {
+        return a.min <= b.max && b.min <= a.max;
+    }
+
+    function generateV2Obf(avoidS1) {
+        var s1;
+        do { s1 = randomInt(15, 150); } while (Math.abs(s1 - avoidS1) < 5);
+        var rs;
+        for (var attempt = 0; attempt < 100; attempt++) {
+            rs = [randomHRange(), randomHRange(), randomHRange(), randomHRange()];
+            var ok = true;
+            for (var i = 0; i < 4 && ok; i++)
+                for (var j = i + 1; j < 4; j++)
+                    if (hRangesOverlap(rs[i], rs[j])) { ok = false; break; }
+            if (ok) break;
+        }
+        return {
+            s1: s1, s2: randomInt(15, 150), s3: randomInt(20, 100),
+            h1: rs[0].min + '-' + rs[0].max,
+            h2: rs[1].min + '-' + rs[1].max,
+            h3: rs[2].min + '-' + rs[2].max,
+            h4: rs[3].min + '-' + rs[3].max
+        };
+    }
+
+    // CPS (I1-I5) templates: AmneziaWG signature-junk sent by the initiator
+    // (fixed magic header + random tail). The responder just drops them, so
+    // only the client side needs them — this lifts server-mode clients to v1.5.
+    function generateCPSTemplate() {
+        var nbytes = randomInt(2, 6);
+        var hex = '';
+        for (var i = 0; i < nbytes; i++) {
+            var b = randomInt(0, 255).toString(16);
+            hex += (b.length < 2 ? '0' : '') + b;
+        }
+        return '<b 0x' + hex + '><r ' + randomInt(4, 16) + '>';
+    }
+
+    function generateCPSTemplates(n) {
+        var a = [];
+        for (var i = 0; i < n; i++) a.push(generateCPSTemplate());
+        return a;
+    }
+
+    function cpsEnvLines(prefix, cps) {
+        var e = [];
+        if (cps) for (var k = 0; k < cps.length; k++)
+            e.push('/container/envs/add list=' + prefix + '-env key=AWG_I' + (k + 1) + ' value="' + cps[k] + '"');
+        return e;
+    }
+
+    function generateS2SParams() {
+        var base = generateAWGParams();
+        // Fallback (v1): fixed H + the base S1/S2
+        base.fallback = { s1: base.s1, s2: base.s2,
+                          h1: base.h1, h2: base.h2, h3: base.h3, h4: base.h4 };
+        // Primary (v2): distinct S1/S2, S3 padding, H ranges
+        var pri = generateV2Obf(base.fallback.s1);
+        base.s1 = pri.s1; base.s2 = pri.s2; base.s3 = pri.s3;
+        base.h1 = pri.h1; base.h2 = pri.h2; base.h3 = pri.h3; base.h4 = pri.h4;
+        return base;
+    }
+
+    function s2sObfEnvLines(prefix, ap) {
+        var e = [];
+        e.push('/container/envs/add list=' + prefix + '-env key=AWG_JC value="' + ap.jc + '"');
+        e.push('/container/envs/add list=' + prefix + '-env key=AWG_JMIN value="' + ap.jmin + '"');
+        e.push('/container/envs/add list=' + prefix + '-env key=AWG_JMAX value="' + ap.jmax + '"');
+        e.push('/container/envs/add list=' + prefix + '-env key=AWG_S1 value="' + ap.s1 + '"');
+        e.push('/container/envs/add list=' + prefix + '-env key=AWG_S2 value="' + ap.s2 + '"');
+        e.push('/container/envs/add list=' + prefix + '-env key=AWG_S3 value="' + ap.s3 + '"');
+        e.push('/container/envs/add list=' + prefix + '-env key=AWG_H1 value="' + ap.h1 + '"');
+        e.push('/container/envs/add list=' + prefix + '-env key=AWG_H2 value="' + ap.h2 + '"');
+        e.push('/container/envs/add list=' + prefix + '-env key=AWG_H3 value="' + ap.h3 + '"');
+        e.push('/container/envs/add list=' + prefix + '-env key=AWG_H4 value="' + ap.h4 + '"');
+        // v1 fallback profile (backward-compat / DPI resilience, s2s only)
+        e.push('/container/envs/add list=' + prefix + '-env key=AWG_FB_S1 value="' + ap.fallback.s1 + '"');
+        e.push('/container/envs/add list=' + prefix + '-env key=AWG_FB_S2 value="' + ap.fallback.s2 + '"');
+        e.push('/container/envs/add list=' + prefix + '-env key=AWG_FB_H1 value="' + ap.fallback.h1 + '"');
+        e.push('/container/envs/add list=' + prefix + '-env key=AWG_FB_H2 value="' + ap.fallback.h2 + '"');
+        e.push('/container/envs/add list=' + prefix + '-env key=AWG_FB_H3 value="' + ap.fallback.h3 + '"');
+        e.push('/container/envs/add list=' + prefix + '-env key=AWG_FB_H4 value="' + ap.fallback.h4 + '"');
+        return e;
+    }
+
+    var s2sParams = null;
+
+    function ensureS2SParams() {
+        if (!s2sParams) {
+            s2sParams = generateS2SParams();
+            document.getElementById('s2s-awg-port').value = s2sParams.port;
+            showS2SParams();
+        }
+        return s2sParams;
+    }
+
+    function regenerateS2SParams() {
+        s2sParams = generateS2SParams();
+        document.getElementById('s2s-awg-port').value = s2sParams.port;
+        showS2SParams();
+    }
+
+    // ============================================================
+    // Server mode (1:N hub)
+    // ============================================================
+    var serverParams = null;
+    var serverFromURL = null;
+    var lastAwgConf = null;
+    var lastAwgConfName = 'awg-client.conf';
+
+    function ensureServerParams() {
+        if (!serverParams) {
+            serverParams = generateAWGParams();
+            document.getElementById('srv-awg-port').value = serverParams.port;
+            showSrvParams();
+        }
+        return serverParams;
+    }
+
+    function regenerateServerParams() {
+        serverParams = generateAWGParams();
+        document.getElementById('srv-awg-port').value = serverParams.port;
+        showSrvParams();
+    }
+
+    function onSrvNetChange() {
+        var val = getSrvNetMode();
+        var isAwg = val === 'awg';
+        var hintKey = isAwg ? 'srvNetAwgHint' : (val === 'l3' ? 'srvNetL3Hint' : 'srvNetNoneHint');
+        var el = document.getElementById('srv-net-hint');
+        el.setAttribute('data-i18n', hintKey);
+        el.textContent = t(hintKey);
+        // LAN merging + client-side MikroTik storage are only relevant for site-to-site (none/l3).
+        // A direct AmneziaWG client (phone/laptop) has no LAN to merge and no client router.
+        document.getElementById('srv-lan-fields').style.display = isAwg ? 'none' : '';
+        var csWrap = document.getElementById('srv-storage-client-wrap');
+        if (csWrap) csWrap.style.display = isAwg ? 'none' : '';
+        if (!isAwg) {
+            // Update LAN labels: required for L3, optional for None
+            var req = val !== 'none';
+            var srvKey = req ? 'srvLanServerReq' : 'srvLanServerOpt';
+            var cliKey = req ? 'srvLanClientReq' : 'srvLanClientOpt';
+            var srvLabel = document.getElementById('srv-lan-server-label');
+            var cliLabel = document.getElementById('srv-lan-client-label');
+            srvLabel.setAttribute('data-i18n', srvKey);
+            srvLabel.textContent = t(srvKey);
+            cliLabel.setAttribute('data-i18n', cliKey);
+            cliLabel.textContent = t(cliKey);
+        }
+    }
+
+    function getSrvNetMode() {
+        var radios = document.getElementsByName('srv-net');
+        for (var i = 0; i < radios.length; i++) if (radios[i].checked) return radios[i].value;
+        return 'none';
+    }
+
+    function parseSlash24(cidr) {
+        var m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/24$/.exec(cidr);
+        if (!m) return null;
+        var octets = [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
+        for (var i = 0; i < 4; i++) if (octets[i] > 255) return null;
+        if (octets[3] !== 0) return null;
+        return {
+            network: cidr,
+            prefix: octets[0] + '.' + octets[1] + '.' + octets[2],
+            ip: function(n) { return octets[0] + '.' + octets[1] + '.' + octets[2] + '.' + n; }
+        };
+    }
+
+    // ============================================================
+    // Shareable URL encoding (v3)
+    // ============================================================
+    //
+    // RULES FOR FUTURE CHANGES (backward compatibility contract):
+    //
+    // 1. NEVER change the fixed header layout (34 bytes). It is frozen forever.
+    //    If a new numeric param is needed, add it as a field, not to the header.
+    //
+    // 2. NEVER reorder or remove existing fields. Only APPEND new fields at the end.
+    //    Field indices are permanent: 0=prefix, 1=endpoint, 2=wgPubKey, 3=netMode, 4=serverLAN, ...
+    //
+    // 3. Parser MUST handle missing trailing fields gracefully (field() returns '').
+    //    A URL with 5 fields must work in a parser that knows about 10 fields.
+    //
+    // 4. Parser MUST skip unknown trailing fields it doesn't understand.
+    //    A URL with 10 fields must work in a parser that only knows 5.
+    //
+    // 5. NEVER bump the version byte unless the fixed header changes (which should never happen).
+    //    Adding new fields does NOT require a version bump — fieldCount handles it.
+    //
+    // 6. Fields are ordered by importance (critical first, optional last).
+    //    If a 255-char URL gets truncated, least important fields are lost first.
+    //    Current order: wgPubKey > endpoint > prefix > netMode > serverLAN > (future optional)
+    //
+    // Format:
+    //   Fixed header (34 bytes, frozen):
+    //     [ver:1][jc:1][jmin:2][jmax:2][s1:2][s2:2][h1-h4:16][awgPort:2][wgPort:2][nextClient:1][tunOct:3]
+    //   Fields section:
+    //     [fieldCount:1] followed by fieldCount length-prefixed fields [len:1][data:N]
+    //   Field indices (append-only, never reorder):
+    //     0 = wgPubKey (32 bytes, critical — without it nothing works)
+    //     1 = endpoint (critical — server address)
+    //     2 = prefix (important — tunnel name, has default fallback)
+    //     3 = netMode (1 byte: 0=none, 1=l3, 2=l2; optional, defaults to none)
+    //     4 = serverLAN (optional, only needed for L3/L2)
+    //     5+ = reserved for future use
+    function strToBytes(s) { var a = []; for (var i = 0; i < s.length; i++) a.push(s.charCodeAt(i) & 0xFF); return a; }
+
+    function buildShareableURL(awgParams, serverWgPubKey, endpoint, awgPort, wgPort, tunnelSubnet, prefix, nextClient, netMode, serverLAN) {
+        var tun = tunnelSubnet.replace(/\/24$/, '').split('.');
+        var keyRaw = atob(serverWgPubKey);
+        var netModeB = netMode === 'l3' ? 1 : (netMode === 'l2' ? 2 : 0);
+
+        // Build fields array — ordered by importance (critical first, optional last).
+        // Indices: 0=wgPubKey, 1=endpoint, 2=prefix, 3=netMode, 4=serverLAN
+        var fields = [
+            strToBytes(keyRaw.slice(0, 32)),
+            strToBytes(endpoint),
+            strToBytes(prefix),
+            [netModeB],
+            strToBytes(serverLAN || '')
+        ];
+
+        // Calculate total size: 34 (header) + 1 (fieldCount) + sum(1 + field.length)
+        var totalLen = 35;
+        for (var i = 0; i < fields.length; i++) totalLen += 1 + fields[i].length;
+        var buf = new Uint8Array(totalLen);
+        var o = 0;
+
+        // Fixed header (34 bytes)
+        buf[o++] = 3; // version 3
+        buf[o++] = awgParams.jc & 0xFF;
+        buf[o++] = (awgParams.jmin >> 8) & 0xFF; buf[o++] = awgParams.jmin & 0xFF;
+        buf[o++] = (awgParams.jmax >> 8) & 0xFF; buf[o++] = awgParams.jmax & 0xFF;
+        buf[o++] = (awgParams.s1 >> 8) & 0xFF; buf[o++] = awgParams.s1 & 0xFF;
+        buf[o++] = (awgParams.s2 >> 8) & 0xFF; buf[o++] = awgParams.s2 & 0xFF;
+        buf[o++] = (awgParams.h1 >>> 24) & 0xFF; buf[o++] = (awgParams.h1 >>> 16) & 0xFF; buf[o++] = (awgParams.h1 >>> 8) & 0xFF; buf[o++] = awgParams.h1 & 0xFF;
+        buf[o++] = (awgParams.h2 >>> 24) & 0xFF; buf[o++] = (awgParams.h2 >>> 16) & 0xFF; buf[o++] = (awgParams.h2 >>> 8) & 0xFF; buf[o++] = awgParams.h2 & 0xFF;
+        buf[o++] = (awgParams.h3 >>> 24) & 0xFF; buf[o++] = (awgParams.h3 >>> 16) & 0xFF; buf[o++] = (awgParams.h3 >>> 8) & 0xFF; buf[o++] = awgParams.h3 & 0xFF;
+        buf[o++] = (awgParams.h4 >>> 24) & 0xFF; buf[o++] = (awgParams.h4 >>> 16) & 0xFF; buf[o++] = (awgParams.h4 >>> 8) & 0xFF; buf[o++] = awgParams.h4 & 0xFF;
+        buf[o++] = (awgPort >> 8) & 0xFF; buf[o++] = awgPort & 0xFF;
+        buf[o++] = (wgPort >> 8) & 0xFF; buf[o++] = wgPort & 0xFF;
+        buf[o++] = nextClient & 0xFF;
+        buf[o++] = Number(tun[0]) & 0xFF; buf[o++] = Number(tun[1]) & 0xFF; buf[o++] = Number(tun[2]) & 0xFF;
+
+        // Fields section
+        buf[o++] = fields.length & 0xFF;
+        for (var f = 0; f < fields.length; f++) {
+            buf[o++] = fields[f].length & 0xFF;
+            for (var i = 0; i < fields[f].length; i++) buf[o++] = fields[f][i];
+        }
+
+        var b64 = btoa(String.fromCharCode.apply(null, buf)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
+        return location.origin + location.pathname + '?c=' + b64;
+    }
+
+    function parseURLParams() {
+        var sp = new URLSearchParams(location.search);
+        var c = sp.get('c');
+        if (!c) return null;
+        var b64 = c.replace(/-/g, '+').replace(/_/g, '/');
+        while (b64.length % 4) b64 += '=';
+        try { var raw = atob(b64); } catch(e) { return null; }
+        var buf = new Uint8Array(raw.length);
+        for (var i = 0; i < raw.length; i++) buf[i] = raw.charCodeAt(i);
+        if (buf.length < 35) return null;
+
+        var o = 0;
+        var ver = buf[o++];
+
+        // Fixed header (same for all versions: 33 bytes after ver)
+        var jc = buf[o++];
+        var jmin = (buf[o++] << 8) | buf[o++];
+        var jmax = (buf[o++] << 8) | buf[o++];
+        var s1 = (buf[o++] << 8) | buf[o++];
+        var s2 = (buf[o++] << 8) | buf[o++];
+        var h1 = ((buf[o++] << 24) | (buf[o++] << 16) | (buf[o++] << 8) | buf[o++]) >>> 0;
+        var h2 = ((buf[o++] << 24) | (buf[o++] << 16) | (buf[o++] << 8) | buf[o++]) >>> 0;
+        var h3 = ((buf[o++] << 24) | (buf[o++] << 16) | (buf[o++] << 8) | buf[o++]) >>> 0;
+        var h4 = ((buf[o++] << 24) | (buf[o++] << 16) | (buf[o++] << 8) | buf[o++]) >>> 0;
+        var awgPort = (buf[o++] << 8) | buf[o++];
+        var wgPort = (buf[o++] << 8) | buf[o++];
+        var nextClient = buf[o++];
+        var tunOct = [buf[o++], buf[o++], buf[o++]];
+        var tunnelSubnet = tunOct[0] + '.' + tunOct[1] + '.' + tunOct[2] + '.0/24';
+
+        // Read fields based on version
+        var fields = [];
+        if (ver >= 3) {
+            // v3+: fieldCount + length-prefixed fields
+            var fieldCount = buf[o++];
+            for (var f = 0; f < fieldCount && o < buf.length; f++) {
+                var flen = buf[o++];
+                var fdata = '';
+                for (var i = 0; i < flen && o < buf.length; i++) fdata += String.fromCharCode(buf[o++]);
+                fields.push(fdata);
+            }
+        } else if (ver === 2) {
+            // v2 compat: netMode(1) + prefixLen(1) + srvLanLen(1) + endpointLen(1) + data + key(32)
+            var netModeN = buf[o++];
+            var prefixLen = buf[o++], srvLanLen = buf[o++], endpointLen = buf[o++];
+            var prefix2 = '', srvLan2 = '', endpoint2 = '', key2 = '';
+            for (var i = 0; i < prefixLen; i++) prefix2 += String.fromCharCode(buf[o++]);
+            for (var i = 0; i < srvLanLen; i++) srvLan2 += String.fromCharCode(buf[o++]);
+            for (var i = 0; i < endpointLen; i++) endpoint2 += String.fromCharCode(buf[o++]);
+            for (var i = 0; i < 32 && o < buf.length; i++) key2 += String.fromCharCode(buf[o++]);
+            // Map to v3 field order: [0]=wgPubKey, [1]=endpoint, [2]=prefix, [3]=netMode, [4]=serverLAN
+            fields = [key2, endpoint2, prefix2, String.fromCharCode(netModeN), srvLan2];
+        } else {
+            return null; // unsupported version
+        }
+
+        // Extract known fields — missing/truncated fields gracefully default
+        // Indices: 0=wgPubKey, 1=endpoint, 2=prefix, 3=netMode, 4=serverLAN, 5+=future
+        function field(idx) { return idx < fields.length ? fields[idx] : ''; }
+        var keyStr = field(0);
+        var serverWgPubKey = keyStr.length === 32 ? btoa(keyStr) : '';
+        var endpoint = field(1);
+        var prefix = field(2) || 'awg-server-1';
+        var netModeRaw = field(3);
+        var netModeVal = netModeRaw.length > 0 ? netModeRaw.charCodeAt(0) : 0;
+        var netMode = netModeVal === 1 ? 'l3' : (netModeVal === 2 ? 'l2' : 'none');
+        var serverLAN = field(4);
+        // Fields 5+ reserved for future use — silently ignored by this parser
+
+        return {
+            mode: 'server',
+            jc: jc, jmin: jmin, jmax: jmax, s1: s1, s2: s2,
+            h1: h1, h2: h2, h3: h3, h4: h4,
+            serverWgPubKey: serverWgPubKey,
+            endpoint: endpoint,
+            awgPort: awgPort,
+            wgPort: wgPort,
+            tunnelSubnet: tunnelSubnet,
+            prefix: prefix,
+            nextClient: nextClient || 2,
+            netMode: netMode,
+            serverLAN: serverLAN
+        };
+    }
+
+    function copySrvURL() {
+        var el = document.getElementById('srv-share-url');
+        el.select();
+        document.execCommand('copy');
+        var btn = document.getElementById('srv-copy-url');
+        btn.textContent = '\u2713';
+        setTimeout(function() { btn.textContent = t('btnCopy'); }, 1500);
+    }
+
+    function buildServerUninstallSource(prefix, storageDisk, net) {
+        var isAuto = storageDisk === '__auto';
+        var src = [];
+        src.push('/system/script/add name=' + prefix + '-uninstall comment=' + prefix + ' source={');
+        src.push('  :put "Uninstalling AWG Proxy Server..."');
+        src.push('  :log info "Uninstalling AWG Proxy Server..."');
+        if (isAuto) {
+            src.push('  :local storageDisk "disk1"');
+            src.push('  :do { :set storageDisk [/container/envs/get [find where list="' + prefix + '-env" key="AWG_STORAGE"] value] } on-error={}');
+            src.push('  :local prevTmpdir ""');
+            src.push('  :if ($storageDisk != "disk1") do={');
+            src.push('    :do { :set prevTmpdir [/container/envs/get [find where list="' + prefix + '-env" key="AWG_PREV_TMPDIR"] value] } on-error={}');
+            src.push('  }');
+        } else if (storageDisk !== 'disk1') {
+            src.push('  :local prevTmpdir ""');
+            src.push('  :do { :set prevTmpdir [/container/envs/get [find where list="' + prefix + '-env" key="AWG_PREV_TMPDIR"] value] } on-error={}');
+        }
+        src.push('  /container/stop [find where interface=veth-' + prefix + ']');
+        src.push('  :delay 7s');
+        src.push('  /container/remove [find where interface=veth-' + prefix + ']');
+        src.push('  /container/envs/remove [find where list="' + prefix + '-env"]');
+        if (isAuto) {
+            src.push('  :if ($storageDisk != "disk1") do={');
+            src.push('    /container/config set tmpdir=$prevTmpdir');
+            src.push('  }');
+        } else if (storageDisk !== 'disk1') {
+            src.push('  /container/config set tmpdir=$prevTmpdir');
+        }
+        src.push('  :do { /ip/firewall/filter/remove [find where comment=' + prefix + '-awg-in] } on-error={}');
+        src.push('  :do { /ip/firewall/filter/remove [find where comment=' + prefix + '-wg-in] } on-error={}');
+        src.push('  :do { /ip/firewall/filter/remove [find where comment=' + prefix + '-wg-mgmt] } on-error={}');
+        src.push('  :do { /ip/firewall/nat/remove [find where comment=' + prefix + '-dstnat] } on-error={}');
+        src.push('  :do { /ip/firewall/nat/remove [find where comment=' + prefix + '-ft-nat] } on-error={}');
+        src.push('  :do { /ip/firewall/filter/remove [find where comment=' + prefix + '-ft-fwd] } on-error={}');
+        src.push('  /ip/firewall/nat/remove [find where src-address="' + net.natSrc + '"]');
+        src.push('  :do { /ip/firewall/nat/remove [find where out-interface="wg-' + prefix + '"] } on-error={}');
+        src.push('  /ip/address/remove [find where address="' + net.hostAddr + '"]');
+        src.push('  /interface/veth/remove [find where name="veth-' + prefix + '"]');
+        src.push('  /interface/wireguard/peers/remove [find where interface="wg-' + prefix + '"]');
+        src.push('  /ip/address/remove [find where interface="wg-' + prefix + '"]');
+        src.push('  :do { /ip/route/remove [find where comment~"' + prefix + '"] } on-error={}');
+        src.push('  :do { /interface/eoip/remove [find where comment~"' + prefix + '"] } on-error={}');
+        src.push('  :do { /interface/bridge/port/remove [find where comment~"' + prefix + '"] } on-error={}');
+        src.push('  /interface/wireguard/remove [find where name="wg-' + prefix + '"]');
+        src.push('  :do { /file/remove [find where name~"' + prefix + '.+tar"] } on-error={}');
+        if (isAuto) {
+            src.push('  :do { /file/remove ($storageDisk . "/' + prefix + '") } on-error={}');
+        } else {
+            src.push('  :do { /file/remove "' + (storageDisk === 'disk1' ? '' : storageDisk + '/') + prefix + '" } on-error={}');
+        }
+        src.push('  /system/script/remove [find where name="' + prefix + '-uninstall"]');
+        src.push('  :put "Done."');
+        src.push('}');
+        return src.join('\n');
+    }
+
+    // Build a valid AmneziaWG .conf for a direct full-tunnel client (protocol v1: Jc/Jmin/Jmax/S1/S2/H1-H4).
+    // o: { clientPriv, serverPub, awgParams, endpoint, awgPort, tunClientIP, dnsServer }
+    function buildAwgConf(o) {
+        var p = o.awgParams;
+        var lines = [];
+        lines.push('[Interface]');
+        lines.push('PrivateKey = ' + o.clientPriv);
+        lines.push('Address = ' + o.tunClientIP + '/32');
+        if (o.dnsServer) lines.push('DNS = ' + o.dnsServer);
+        lines.push('Jc = ' + p.jc);
+        lines.push('Jmin = ' + p.jmin);
+        lines.push('Jmax = ' + p.jmax);
+        lines.push('S1 = ' + p.s1);
+        lines.push('S2 = ' + p.s2);
+        lines.push('H1 = ' + p.h1);
+        lines.push('H2 = ' + p.h2);
+        lines.push('H3 = ' + p.h3);
+        lines.push('H4 = ' + p.h4);
+        if (p.cps) {
+            for (var k = 0; k < p.cps.length; k++)
+                lines.push('I' + (k + 1) + ' = ' + p.cps[k]);
+        }
+        lines.push('');
+        lines.push('[Peer]');
+        lines.push('PublicKey = ' + o.serverPub);
+        lines.push('Endpoint = ' + o.endpoint + ':' + o.awgPort);
+        lines.push('AllowedIPs = 0.0.0.0/0');
+        lines.push('PersistentKeepalive = 25');
+        return lines.join('\n');
+    }
+
+    function buildServerCommands(endpoint, awgParams, tunnelCidr, storageServer, storageClient, prefix, serverLAN, clientLAN, netMode, clientNum, dnsServer) {
+        var serverKey = generateWgKeyPair();
+        var clientKey = generateWgKeyPair();
+        // CPS templates for the client side (v1.5). Server ignores inbound CPS.
+        awgParams.cps = generateCPSTemplates(randomInt(2, 3));
+        var net = getTunnelNetwork(prefix);
+        var tun = parseSlash24(tunnelCidr);
+        var vethIP = net.vethAddr.replace(/\/.*/, '');
+        var awgPort = awgParams.port;
+        var wgPort = net.wgPort;
+        var tunServerIP = tun.ip(1);
+        var tunClientIP = tun.ip(clientNum + 1);
+
+        var serverAllowed = tunClientIP + '/32';
+        if (clientLAN) serverAllowed += ',' + clientLAN;
+
+        var clientAllowed = tun.network;
+        if (serverLAN) clientAllowed += ',' + serverLAN;
+
+        // Generate shareable URL before building commands (needed for env)
+        var shareURL = buildShareableURL(awgParams, serverKey.publicKey, endpoint, awgPort, wgPort, tunnelCidr, prefix, clientNum + 1, netMode, serverLAN);
+
+        // === SERVER CONFIG ===
+        var ls = [];
+        ls.push('# ============================================================');
+        ls.push('# AWG Proxy Server (1:N hub) \u2014 Head MikroTik');
+        ls.push('# ' + new Date().toISOString().slice(0, 19).replace('T', ' '));
+        ls.push('# ============================================================');
+        ls.push('');
+        ls.push('# 0. Check prerequisites');
+        ls.push(':if ([:len [/system/package/find where name="container" disabled=no]] = 0) do={');
+        ls.push('  :put "Container package is not installed or disabled. Install it and reboot."');
+        ls.push('  :error "Usage: /system/device-mode/update container=yes"');
+        ls.push('}');
+        ls.push('');
+
+        if (storageServer === '__auto') {
+            ls = ls.concat(buildAutoStorage(prefix));
+            ls.push('');
+        }
+
+        ls.push('# 1. Uninstall script');
+        ls.push(buildServerUninstallSource(prefix, storageServer, net));
+        ls.push('');
+
+        ls.push('# 2. Network infrastructure');
+        ls.push('/interface/veth/add name=veth-' + prefix + ' address=' + net.vethAddr + ' gateway=' + net.vethGw);
+        ls.push('/ip/address/add address=' + net.hostAddr + ' interface=veth-' + prefix);
+        ls.push('/ip/firewall/nat/add chain=srcnat action=masquerade src-address=' + net.natSrc);
+        ls.push('');
+
+        ls.push('# 3. WireGuard server');
+        ls.push('/interface/wireguard/add name=wg-' + prefix + ' private-key="' + serverKey.privateKey + '" listen-port=' + wgPort + ' disabled=yes');
+        ls.push('/interface/wireguard/peers/add interface=wg-' + prefix + ' public-key="' + clientKey.publicKey + '" allowed-address=' + serverAllowed + ' comment=' + prefix + '-client-' + clientNum);
+        ls.push('/ip/address/add address=' + tunServerIP + '/24 interface=wg-' + prefix);
+        ls.push('/ip/firewall/nat/add chain=srcnat action=masquerade out-interface=wg-' + prefix);
+        ls.push('');
+
+        ls.push('# 4. Firewall: allow AWG port and forward to container');
+        ls.push('/ip/firewall/filter/add chain=forward action=accept protocol=udp dst-port=' + awgPort + ' in-interface-list=WAN place-before=0 comment=' + prefix + '-awg-in');
+        ls.push('/ip/firewall/filter/add chain=input action=accept protocol=udp src-address=' + vethIP + ' dst-port=' + wgPort + ' place-before=0 comment=' + prefix + '-wg-in');
+        ls.push('/ip/firewall/filter/add chain=input action=accept in-interface=wg-' + prefix + ' place-before=0 comment=' + prefix + '-wg-mgmt');
+        ls.push('/ip/firewall/nat/add chain=dstnat action=dst-nat protocol=udp dst-port=' + awgPort + ' in-interface-list=WAN to-addresses=' + vethIP + ' to-ports=' + awgPort + ' comment=' + prefix + '-dstnat');
+        ls.push('# Full-tunnel egress for direct AmneziaWG clients (no-op for site-to-site peers that don\'t route 0.0.0.0/0)');
+        ls.push('/ip/firewall/nat/add chain=srcnat action=masquerade src-address=' + tunnelCidr + ' out-interface-list=WAN comment=' + prefix + '-ft-nat');
+        ls.push('/ip/firewall/filter/add chain=forward action=accept in-interface=wg-' + prefix + ' place-before=0 comment=' + prefix + '-ft-fwd');
+        ls.push('');
+
+        ls.push('# 5. Container environment variables');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_MODE value="server"');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_LISTEN value=":' + awgPort + '"');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_REMOTE value="' + net.vethGw + ':' + wgPort + '"');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_JC value="' + awgParams.jc + '"');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_JMIN value="' + awgParams.jmin + '"');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_JMAX value="' + awgParams.jmax + '"');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_S1 value="' + awgParams.s1 + '"');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_S2 value="' + awgParams.s2 + '"');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_H1 value="' + awgParams.h1 + '"');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_H2 value="' + awgParams.h2 + '"');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_H3 value="' + awgParams.h3 + '"');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_H4 value="' + awgParams.h4 + '"');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_SERVER_PUB value="' + serverKey.publicKey + '"');
+        ls.push('# AWG_CLIENT_PUB stays as legacy fallback for proxy-only clients.');
+        ls.push('# AWG_CLIENT_PUBS lists real direct AmneziaWG clients for multi-peer server mode.');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_CLIENT_PUB value="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_CLIENT_PUBS value="' + clientKey.publicKey + '"');
+        if (dnsServer) ls.push('/container/envs/add list=' + prefix + '-env key=AWG_DNS value="' + dnsServer + '"');
+        if (dfEnabled()) ls.push('/container/envs/add list=' + prefix + '-env key=AWG_NO_DF value="1"');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_CONFIG_URL value="' + shareURL + '"');
+        ls.push('');
+
+        var serverCompletion = [
+            '/interface/wireguard/enable wg-' + prefix,
+        ];
+        if (netMode === 'l3' && clientLAN) {
+            serverCompletion.push('# L3 routing to client LAN');
+            serverCompletion.push('/ip/route/add dst-address=' + clientLAN + ' gateway=wg-' + prefix + ' comment=' + prefix + '-client-' + clientNum);
+        }
+        serverCompletion.push(':put "Server installation complete!"');
+        ls.push('# 6. Download, create and start container');
+        ls = ls.concat(buildContainerDownload(prefix, storageServer, serverCompletion));
+        ls.push('');
+
+        // === CLIENT CONFIG ===
+        var lc = [];
+        lc.push('# ============================================================');
+        lc.push('# AWG Proxy Client ' + clientNum + ' \u2014 MikroTik');
+        lc.push('# ' + new Date().toISOString().slice(0, 19).replace('T', ' '));
+        lc.push('# ============================================================');
+        lc.push('');
+        lc.push('# 0. Check prerequisites');
+        lc.push(':if ([:len [/system/package/find where name="container" disabled=no]] = 0) do={');
+        lc.push('  :put "Container package is not installed or disabled. Install it and reboot."');
+        lc.push('  :error "Usage: /system/device-mode/update container=yes"');
+        lc.push('}');
+        lc.push('');
+
+        if (storageClient === '__auto') {
+            lc = lc.concat(buildAutoStorage(prefix));
+            lc.push('');
+        }
+
+        lc.push('# 1. Uninstall script');
+        lc.push(buildServerUninstallSource(prefix, storageClient, getTunnelNetwork(prefix)));
+        lc.push('');
+
+        lc.push('# 2. Network infrastructure');
+        var cnet = getTunnelNetwork(prefix);
+        var cvethIP = cnet.vethAddr.replace(/\/.*/, '');
+        lc.push('/interface/veth/add name=veth-' + prefix + ' address=' + cnet.vethAddr + ' gateway=' + cnet.vethGw);
+        lc.push('/ip/address/add address=' + cnet.hostAddr + ' interface=veth-' + prefix);
+        lc.push('/ip/firewall/nat/add chain=srcnat action=masquerade src-address=' + cnet.natSrc);
+        lc.push('');
+
+        lc.push('# 3. WireGuard client');
+        lc.push('/interface/wireguard/add name=wg-' + prefix + ' private-key="' + clientKey.privateKey + '" listen-port=' + cnet.wgPort + ' disabled=yes');
+        lc.push('/interface/wireguard/peers/add interface=wg-' + prefix + ' public-key="' + serverKey.publicKey + '" endpoint-address=' + cvethIP + ' endpoint-port=51820 allowed-address=' + clientAllowed + ' persistent-keepalive=' + randomInt(25, 55));
+        lc.push('/ip/address/add address=' + tunClientIP + '/24 interface=wg-' + prefix);
+        lc.push('/ip/firewall/filter/add chain=input action=accept in-interface=wg-' + prefix + ' place-before=0 comment=' + prefix + '-wg-mgmt');
+        lc.push('/ip/firewall/nat/add chain=srcnat action=masquerade out-interface=wg-' + prefix);
+        lc.push('');
+
+        lc.push('# 4. Container environment variables');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_LISTEN value=":51820"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_REMOTE value="' + endpoint + ':' + awgPort + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_JC value="' + awgParams.jc + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_JMIN value="' + awgParams.jmin + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_JMAX value="' + awgParams.jmax + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_S1 value="' + awgParams.s1 + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_S2 value="' + awgParams.s2 + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_H1 value="' + awgParams.h1 + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_H2 value="' + awgParams.h2 + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_H3 value="' + awgParams.h3 + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_H4 value="' + awgParams.h4 + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_SERVER_PUB value="' + serverKey.publicKey + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_CLIENT_PUB value="' + clientKey.publicKey + '"');
+        lc = lc.concat(cpsEnvLines(prefix, awgParams.cps));
+        if (dnsServer) lc.push('/container/envs/add list=' + prefix + '-env key=AWG_DNS value="' + dnsServer + '"');
+        if (dfEnabled()) lc.push('/container/envs/add list=' + prefix + '-env key=AWG_NO_DF value="1"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_CONFIG_URL value="' + shareURL + '"');
+        lc.push('');
+
+        var clientCompletion = [
+            '/interface/wireguard/enable wg-' + prefix,
+        ];
+        if (netMode === 'l3' && serverLAN) {
+            clientCompletion.push('# L3 routing to server LAN');
+            clientCompletion.push('/ip/route/add dst-address=' + serverLAN + ' gateway=wg-' + prefix + ' comment=' + prefix + '-srv-route');
+        }
+        clientCompletion.push(':put "Client ' + clientNum + ' installation complete!"');
+        clientCompletion.push(':put "Ping test: ' + tunServerIP + ' (server WG address)"');
+        clientCompletion.push('/ping ' + tunServerIP + ' count=5');
+        lc.push('# 5. Download, create and start container');
+        lc = lc.concat(buildContainerDownload(prefix, storageClient, clientCompletion));
+        lc.push('');
+
+        return {
+            server: ls.join('\n'),
+            client: lc.join('\n'),
+            shareURL: shareURL,
+            serverKey: serverKey,
+            clientKey: clientKey,
+            awgConf: buildAwgConf({
+                clientPriv: clientKey.privateKey,
+                serverPub: serverKey.publicKey,
+                awgParams: awgParams,
+                endpoint: endpoint,
+                awgPort: awgPort,
+                tunClientIP: tunClientIP,
+                dnsServer: dnsServer
+            })
+        };
+    }
+
+    function buildAddClientCommands(urlParams, storageClient, clientLAN, netMode, dnsServer) {
+        var prefix = urlParams.prefix;
+        var clientNum = urlParams.nextClient;
+        var tunnelCidr = urlParams.tunnelSubnet;
+        var tun = parseSlash24(tunnelCidr);
+        var tunServerIP = tun.ip(1);
+        var tunClientIP = tun.ip(clientNum + 1);
+        var endpoint = urlParams.endpoint;
+        var awgPort = urlParams.awgPort;
+        var wgPort = urlParams.wgPort;
+        var serverWgPubKey = urlParams.serverWgPubKey;
+        var awgParams = {jc:urlParams.jc,jmin:urlParams.jmin,jmax:urlParams.jmax,s1:urlParams.s1,s2:urlParams.s2,h1:urlParams.h1,h2:urlParams.h2,h3:urlParams.h3,h4:urlParams.h4};
+        // Fresh CPS templates for this client (v1.5); not carried in the URL.
+        awgParams.cps = generateCPSTemplates(randomInt(2, 3));
+
+        var clientKey = generateWgKeyPair();
+
+        var serverAllowed = tunClientIP + '/32';
+        if (clientLAN) serverAllowed += ',' + clientLAN;
+
+        var clientAllowed = tunnelCidr;
+        if (urlParams.serverLAN) clientAllowed += ',' + urlParams.serverLAN;
+
+        // Commands to run on SERVER
+        var lu = [];
+        lu.push('# ============================================================');
+        lu.push('# Add client ' + clientNum + ' to server \u2014 run on HEAD MikroTik');
+        lu.push('# ============================================================');
+        lu.push('/interface/wireguard/peers/add interface=wg-' + prefix + ' public-key="' + clientKey.publicKey + '" allowed-address=' + serverAllowed + ' comment=' + prefix + '-client-' + clientNum);
+        lu.push('# Keep AWG_CLIENT_PUB as legacy fallback; append this real client key to AWG_CLIENT_PUBS for direct AWG clients.');
+        lu.push('/container/envs/set [find where list="' + prefix + '-env" key="AWG_CLIENT_PUBS"] value=([/container/envs/get [find where list="' + prefix + '-env" key="AWG_CLIENT_PUBS"] value] . "," . "' + clientKey.publicKey + '")');
+        if (netMode === 'l3' && clientLAN) {
+            lu.push('/ip/route/add dst-address=' + clientLAN + ' gateway=wg-' + prefix + ' comment=' + prefix + '-client-' + clientNum);
+        }
+
+        // Full client config
+        var lc = [];
+        lc.push('# ============================================================');
+        lc.push('# AWG Proxy Client ' + clientNum + ' \u2014 MikroTik');
+        lc.push('# ' + new Date().toISOString().slice(0, 19).replace('T', ' '));
+        lc.push('# ============================================================');
+        lc.push('');
+        lc.push('# 0. Check prerequisites');
+        lc.push(':if ([:len [/system/package/find where name="container" disabled=no]] = 0) do={');
+        lc.push('  :put "Container package is not installed or disabled. Install it and reboot."');
+        lc.push('  :error "Usage: /system/device-mode/update container=yes"');
+        lc.push('}');
+        lc.push('');
+
+        if (storageClient === '__auto') {
+            lc = lc.concat(buildAutoStorage(prefix));
+            lc.push('');
+        }
+
+        lc.push('# 1. Uninstall script');
+        lc.push(buildServerUninstallSource(prefix, storageClient, getTunnelNetwork(prefix)));
+        lc.push('');
+
+        var cnet = getTunnelNetwork(prefix);
+        var cvethIP = cnet.vethAddr.replace(/\/.*/, '');
+        lc.push('# 2. Network infrastructure');
+        lc.push('/interface/veth/add name=veth-' + prefix + ' address=' + cnet.vethAddr + ' gateway=' + cnet.vethGw);
+        lc.push('/ip/address/add address=' + cnet.hostAddr + ' interface=veth-' + prefix);
+        lc.push('/ip/firewall/nat/add chain=srcnat action=masquerade src-address=' + cnet.natSrc);
+        lc.push('');
+
+        lc.push('# 3. WireGuard client');
+        lc.push('/interface/wireguard/add name=wg-' + prefix + ' private-key="' + clientKey.privateKey + '" listen-port=' + cnet.wgPort + ' disabled=yes');
+        lc.push('/interface/wireguard/peers/add interface=wg-' + prefix + ' public-key="' + serverWgPubKey + '" endpoint-address=' + cvethIP + ' endpoint-port=51820 allowed-address=' + clientAllowed + ' persistent-keepalive=' + randomInt(25, 55));
+        lc.push('/ip/address/add address=' + tunClientIP + '/24 interface=wg-' + prefix);
+        lc.push('/ip/firewall/filter/add chain=input action=accept in-interface=wg-' + prefix + ' place-before=0 comment=' + prefix + '-wg-mgmt');
+        lc.push('/ip/firewall/nat/add chain=srcnat action=masquerade out-interface=wg-' + prefix);
+        lc.push('');
+
+        lc.push('# 4. Container environment variables');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_LISTEN value=":51820"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_REMOTE value="' + endpoint + ':' + awgPort + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_JC value="' + awgParams.jc + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_JMIN value="' + awgParams.jmin + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_JMAX value="' + awgParams.jmax + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_S1 value="' + awgParams.s1 + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_S2 value="' + awgParams.s2 + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_H1 value="' + awgParams.h1 + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_H2 value="' + awgParams.h2 + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_H3 value="' + awgParams.h3 + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_H4 value="' + awgParams.h4 + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_SERVER_PUB value="' + serverWgPubKey + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_CLIENT_PUB value="' + clientKey.publicKey + '"');
+        lc = lc.concat(cpsEnvLines(prefix, awgParams.cps));
+        if (dnsServer) lc.push('/container/envs/add list=' + prefix + '-env key=AWG_DNS value="' + dnsServer + '"');
+        if (dfEnabled()) lc.push('/container/envs/add list=' + prefix + '-env key=AWG_NO_DF value="1"');
+        var nextURL = buildShareableURL(awgParams, serverWgPubKey, endpoint, awgPort, wgPort, tunnelCidr, prefix, clientNum + 1, netMode, urlParams.serverLAN);
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_CONFIG_URL value="' + nextURL + '"');
+        lc.push('');
+
+        var addClientCompletion = [
+            '/interface/wireguard/enable wg-' + prefix,
+        ];
+        if (netMode === 'l3' && urlParams.serverLAN) {
+            addClientCompletion.push('# L3 routing to server LAN');
+            addClientCompletion.push('/ip/route/add dst-address=' + urlParams.serverLAN + ' gateway=wg-' + prefix + ' comment=' + prefix + '-srv-route');
+        }
+        addClientCompletion.push(':put "Client ' + clientNum + ' installation complete!"');
+        addClientCompletion.push(':put "Ping test: ' + tunServerIP + ' (server WG address)"');
+        addClientCompletion.push('/ping ' + tunServerIP + ' count=5');
+        lc.push('# 5. Download, create and start container');
+        lc = lc.concat(buildContainerDownload(prefix, storageClient, addClientCompletion));
+        lc.push('');
+
+        return {
+            serverUpdate: lu.join('\n'),
+            client: lc.join('\n'),
+            shareURL: nextURL,
+            awgConf: buildAwgConf({
+                clientPriv: clientKey.privateKey,
+                serverPub: serverWgPubKey,
+                awgParams: awgParams,
+                endpoint: endpoint,
+                awgPort: awgPort,
+                tunClientIP: tunClientIP,
+                dnsServer: dnsServer
+            })
+        };
+    }
+
+    // Right-hand output column. For 'awg' mode it shows the importable .conf (plain text,
+    // copyable) + a visible "Download .conf" button; otherwise the client-side MikroTik script.
+    function renderClientPanel(result, netMode, clientNum) {
+        var pre = document.getElementById('srv-out-client');
+        var title = document.getElementById('srv-client-title');
+        var dlBtn = document.getElementById('srv-download-conf');
+        lastAwgConf = result.awgConf;
+        lastAwgConfName = 'awg-client-' + clientNum + '.conf';
+        if (netMode === 'awg') {
+            pre.dataset.plain = result.awgConf;
+            pre.textContent = result.awgConf;
+            title.textContent = t('srvAwgConfTitle') + ' ' + clientNum;
+            if (dlBtn) dlBtn.style.display = '';
+        } else {
+            pre.dataset.plain = result.client;
+            pre.innerHTML = highlight(result.client);
+            title.textContent = t('srvOutputClient') + ' ' + clientNum;
+            if (dlBtn) dlBtn.style.display = 'none';
+        }
+    }
+
+    function generateServer() {
+        var errs = [];
+        var mode = getDeployMode();
+        if (mode !== 'server') return;
+
+        var isAddClient = !!serverFromURL;
+        var endpoint = document.getElementById('srv-endpoint').value.trim();
+        var awgPortStr = document.getElementById('srv-awg-port').value.trim();
+        var tunnelSubnet = document.getElementById('srv-tunnel-subnet').value.trim();
+        var storageServer = document.getElementById('srv-storage-server').value;
+        var storageClient = document.getElementById('srv-storage-client').value;
+        var serverLAN = document.getElementById('srv-lan-server').value.trim();
+        var clientLAN = document.getElementById('srv-lan-client').value.trim();
+        var netMode = getSrvNetMode();
+        var clientNum = Number(document.getElementById('srv-client-num').value) || 1;
+        var dnsServer = document.getElementById('srv-dns').value.trim();
+        var prefix = getPrefixName();
+
+        // Direct AmneziaWG client: full-tunnel, no LAN merging — ignore LAN inputs entirely.
+        if (netMode === 'awg') { serverLAN = ''; clientLAN = ''; }
+
+        if (!endpoint) { errs.push(t('errSrvEndpoint')); }
+        var tun = parseSlash24(tunnelSubnet);
+        if (!tun) { errs.push(t('errSrvTunnelSubnet')); }
+        var awgPort = Number(awgPortStr);
+        if (awgPort < 1024 || awgPort > 65535) { errs.push(t('errAwgPort')); }
+        if (netMode === 'l3' && (!serverLAN || !clientLAN)) { errs.push(t('errSrvLanRequired')); }
+        if (serverLAN && !/^\d{1,3}(\.\d{1,3}){3}\/\d{1,2}$/.test(serverLAN)) { errs.push(t('errLanSubnet')); }
+        if (clientLAN && !/^\d{1,3}(\.\d{1,3}){3}\/\d{1,2}$/.test(clientLAN)) { errs.push(t('errLanSubnet')); }
+        // A direct AmneziaWG client resolves the endpoint hostname itself (via the device's
+        // system DNS, before the tunnel comes up) — no proxy-side DNS is needed. Only the
+        // client-side MikroTik container (none/l3) needs AWG_DNS to resolve a hostname endpoint.
+        var isHostname = endpoint && !/^\d{1,3}(\.\d{1,3}){3}$/.test(endpoint);
+        if (netMode !== 'awg' && isHostname && !dnsServer) { errs.push(t('errDnsRequired')); }
+
+        if (errs.length > 0) {
+            document.getElementById('errors-container').innerHTML = '<div class="alert alert-error">' + errs.map(esc).join('<br>') + '</div>';
+            return;
+        }
+        document.getElementById('errors-container').innerHTML = '';
+
+        if (isAddClient) {
+            serverFromURL.nextClient = clientNum;
+            var result = buildAddClientCommands(serverFromURL, storageClient, clientLAN, netMode, dnsServer);
+            document.getElementById('srv-out-server').style.display = 'none';
+            document.getElementById('srv-out-server').parentElement.style.display = 'none';
+            document.getElementById('srv-server-update').style.display = 'block';
+            document.getElementById('srv-out-update').dataset.plain = result.serverUpdate;
+            document.getElementById('srv-out-update').innerHTML = highlight(result.serverUpdate);
+            renderClientPanel(result, netMode, clientNum);
+            document.getElementById('srv-share-url').value = result.shareURL;
+        } else {
+            ensureServerParams();
+            serverParams.port = awgPort;
+            showSrvParams();
+            var result = buildServerCommands(endpoint, serverParams, tunnelSubnet, storageServer, storageClient, prefix, serverLAN, clientLAN, netMode, clientNum, dnsServer);
+            document.getElementById('srv-out-server').style.display = '';
+            document.getElementById('srv-out-server').parentElement.style.display = '';
+            document.getElementById('srv-server-update').style.display = 'none';
+            document.getElementById('srv-out-server').dataset.plain = result.server;
+            document.getElementById('srv-out-server').innerHTML = highlight(result.server);
+            renderClientPanel(result, netMode, clientNum);
+            document.getElementById('srv-share-url').value = result.shareURL;
+        }
+
+        document.getElementById('srv-output-section').style.display = 'block';
+        document.getElementById('output-section').style.display = 'none';
+        document.getElementById('s2s-output-section').style.display = 'none';
+        document.getElementById('srv-link-banner').style.display = 'block';
+    }
+
+    // ============================================================
+    // S2S subnet parser
+    // ============================================================
+    function parseSlash30(cidr) {
+        var m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/30$/.exec(cidr);
+        if (!m) return null;
+        var octets = [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
+        for (var i = 0; i < 4; i++) if (octets[i] > 255) return null;
+        var base = (octets[0] << 24 | octets[1] << 16 | octets[2] << 8 | octets[3]) >>> 0;
+        if (base % 4 !== 0) return null;
+        function toIP(n) { return ((n>>>24)&255)+'.'+((n>>>16)&255)+'.'+((n>>>8)&255)+'.'+(n&255); }
+        return {
+            network: cidr,
+            ip1: toIP(base + 1),
+            ip2: toIP(base + 2),
+            broadcast: toIP(base + 3),
+            addr1: toIP(base + 1) + '/30',
+            addr2: toIP(base + 2) + '/30'
+        };
+    }
+
+    function isValidEndpointHost(s) {
+        if (!s || !s.trim()) return false;
+        s = s.trim();
+        if (isValidIP(s)) return true;
+        return /^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$/.test(s) && s.indexOf('..') < 0;
+    }
+
+    // ============================================================
+    // Build site-to-site commands
+    // ============================================================
+    function buildSiteToSiteCommands(endpointB, subnet, awgParams, storageA, storageB, prefix, dnsServer, lanA, lanB) {
+        var keyA = generateWgKeyPair();
+        var keyB = generateWgKeyPair();
+        var net = getTunnelNetwork(prefix);
+        var sn = parseSlash30(subnet);
+        var wgPort = awgParams.wgListenPort;
+
+        var result = {};
+
+        // === Side A (normal) ===
+        var la = [];
+        la.push('# ============================================================');
+        la.push('# AWG Proxy Site-to-Site -- Side A (client)');
+        la.push('# ' + new Date().toISOString().slice(0, 19).replace('T', ' '));
+        la.push('# ============================================================');
+        la.push('');
+        la.push('# 0. Check prerequisites');
+        la.push(':if ([:len [/system/package/find where name="container" disabled=no]] = 0) do={');
+        la.push('  :put "Container package is not installed or disabled. Install it and reboot."');
+        la.push('  :error "Usage: /system/device-mode/update container=yes"');
+        la.push('}');
+        la.push('');
+
+        if (storageA === '__auto') {
+            la = la.concat(buildAutoStorage(prefix));
+            la.push('');
+        }
+
+        // Uninstall script
+        la.push('# 1. Uninstall script');
+        la.push(buildS2SUninstallSource(prefix, storageA, net, false));
+        la.push('');
+
+        // Network
+        la.push('# 2. Network infrastructure');
+        la.push('/interface/veth/add name=veth-' + prefix + ' address=' + net.vethAddr + ' gateway=' + net.vethGw);
+        la.push('/ip/address/add address=' + net.hostAddr + ' interface=veth-' + prefix);
+        la.push('/ip/firewall/nat/add chain=srcnat action=masquerade src-address=' + net.natSrc);
+        la.push('');
+
+        // WireGuard
+        la.push('# 3. WireGuard interface');
+        la.push('/interface/wireguard/add name=wg-' + prefix + ' private-key="' + keyA.privateKey + '" listen-port=' + wgPort + ' disabled=yes');
+        la.push('/interface/wireguard/peers/add interface=wg-' + prefix + ' public-key="' + keyB.publicKey + '" endpoint-address=' + net.vethAddr.replace(/\/.*/, '') + ' endpoint-port=51820 allowed-address=' + subnet + (lanB ? ',' + lanB : '') + ' persistent-keepalive=' + randomInt(25, 55));
+        la.push('/ip/address/add address=' + sn.addr1 + ' interface=wg-' + prefix);
+        la.push('/ip/firewall/filter/add chain=input action=accept in-interface=wg-' + prefix + ' place-before=0 comment=' + prefix + '-wg-mgmt');
+        if (lanB) la.push('/ip/route/add dst-address=' + lanB + ' gateway=wg-' + prefix + ' comment=' + prefix + '-s2s-lan');
+        la.push('');
+
+        // Container env
+        la.push('# 4. Container environment variables');
+        la.push('/container/envs/add list=' + prefix + '-env key=AWG_LISTEN value=":51820"');
+        la.push('/container/envs/add list=' + prefix + '-env key=AWG_REMOTE value="' + endpointB + ':' + awgParams.port + '"');
+        la = la.concat(s2sObfEnvLines(prefix, awgParams));
+        la.push('/container/envs/add list=' + prefix + '-env key=AWG_SERVER_PUB value="' + keyB.publicKey + '"');
+        la.push('/container/envs/add list=' + prefix + '-env key=AWG_CLIENT_PUB value="' + keyA.publicKey + '"');
+        if (dnsServer) la.push('/container/envs/add list=' + prefix + '-env key=AWG_DNS value="' + dnsServer + '"');
+        if (dfEnabled()) la.push('/container/envs/add list=' + prefix + '-env key=AWG_NO_DF value="1"');
+        la.push('');
+
+        // Download + start
+        la.push('# 5. Download, create and start container');
+        la = la.concat(buildContainerDownload(prefix, storageA, [
+            '/interface/wireguard/enable wg-' + prefix,
+            ':put "Side A installation complete!"',
+            ':put "Ping test: ' + sn.ip2 + ' (Side B WG address)"',
+            '/ping ' + sn.ip2 + ' count=5',
+        ]));
+        la.push('');
+
+        // === Side B (reverse) ===
+        var lb = [];
+        lb.push('# ============================================================');
+        lb.push('# AWG Proxy Site-to-Site -- Side B (server)');
+        lb.push('# ' + new Date().toISOString().slice(0, 19).replace('T', ' '));
+        lb.push('# ============================================================');
+        lb.push('');
+        lb.push('# 0. Check prerequisites');
+        lb.push(':if ([:len [/system/package/find where name="container" disabled=no]] = 0) do={');
+        lb.push('  :put "Container package is not installed or disabled. Install it and reboot."');
+        lb.push('  :error "Usage: /system/device-mode/update container=yes"');
+        lb.push('}');
+        lb.push('');
+
+        if (storageB === '__auto') {
+            lb = lb.concat(buildAutoStorage(prefix));
+            lb.push('');
+        }
+
+        // Uninstall
+        lb.push('# 1. Uninstall script');
+        lb.push(buildS2SUninstallSource(prefix, storageB, net, true));
+        lb.push('');
+
+        // Network
+        lb.push('# 2. Network infrastructure');
+        lb.push('/interface/veth/add name=veth-' + prefix + ' address=' + net.vethAddr + ' gateway=' + net.vethGw);
+        lb.push('/ip/address/add address=' + net.hostAddr + ' interface=veth-' + prefix);
+        lb.push('/ip/firewall/nat/add chain=srcnat action=masquerade src-address=' + net.natSrc);
+        lb.push('');
+
+        // dst-nat + forward accept for AWG port
+        lb.push('# 3. Firewall: allow and forward AWG port');
+        lb.push('/ip/firewall/filter/add chain=forward action=accept protocol=udp dst-port=' + awgParams.port + ' in-interface-list=WAN place-before=0 comment=' + prefix + '-awg-in');
+        lb.push('/ip/firewall/filter/add chain=input action=accept protocol=udp src-address=' + net.vethAddr.replace(/\/.*/, '') + ' dst-port=' + wgPort + ' place-before=0 comment=' + prefix + '-wg-in');
+        lb.push('/ip/firewall/nat/add chain=dstnat action=dst-nat protocol=udp dst-port=' + awgParams.port + ' in-interface-list=WAN to-addresses=' + net.vethAddr.replace(/\/.*/, '') + ' to-ports=' + awgParams.port + ' comment=' + prefix + '-dstnat');
+        lb.push('');
+
+        // WireGuard
+        lb.push('# 4. WireGuard interface');
+        lb.push('/interface/wireguard/add name=wg-' + prefix + ' private-key="' + keyB.privateKey + '" listen-port=' + wgPort + ' disabled=yes');
+        lb.push('/interface/wireguard/peers/add interface=wg-' + prefix + ' public-key="' + keyA.publicKey + '" allowed-address=' + subnet + (lanA ? ',' + lanA : ''));
+        lb.push('/ip/address/add address=' + sn.addr2 + ' interface=wg-' + prefix);
+        if (lanA) lb.push('/ip/route/add dst-address=' + lanA + ' gateway=wg-' + prefix + ' comment=' + prefix + '-s2s-lan');
+        lb.push('');
+
+        // Container env
+        lb.push('# 5. Container environment variables');
+        lb.push('/container/envs/add list=' + prefix + '-env key=AWG_LISTEN value=":' + awgParams.port + '"');
+        lb.push('/container/envs/add list=' + prefix + '-env key=AWG_REMOTE value="' + net.vethGw + ':' + wgPort + '"');
+        lb.push('/container/envs/add list=' + prefix + '-env key=AWG_MODE value="reverse"');
+        lb = lb.concat(s2sObfEnvLines(prefix, awgParams));
+        lb.push('/container/envs/add list=' + prefix + '-env key=AWG_SERVER_PUB value="' + keyB.publicKey + '"');
+        lb.push('/container/envs/add list=' + prefix + '-env key=AWG_CLIENT_PUB value="' + keyA.publicKey + '"');
+        if (dfEnabled()) lb.push('/container/envs/add list=' + prefix + '-env key=AWG_NO_DF value="1"');
+        lb.push('');
+
+        // Download + start
+        lb.push('# 6. Download, create and start container');
+        lb = lb.concat(buildContainerDownload(prefix, storageB, [
+            '/interface/wireguard/enable wg-' + prefix,
+            ':put "Side B installation complete!"',
+            ':put "Ping test: ' + sn.ip1 + ' (Side A WG address)"',
+            '/ping ' + sn.ip1 + ' count=5',
+        ]));
+        lb.push('');
+
+        result.sideA = la.join('\n');
+        result.sideB = lb.join('\n');
+        result.uninstallA = '# To uninstall Side A:\n/system/script/run ' + prefix + '-uninstall';
+        result.uninstallB = '# To uninstall Side B:\n/system/script/run ' + prefix + '-uninstall';
+        result.keyA = keyA;
+        result.keyB = keyB;
+        return result;
+    }
+
+    function buildS2SUninstallSource(prefix, storageDisk, net, hasNat) {
+        var isAuto = storageDisk === '__auto';
+        var src = [];
+        src.push('/system/script/add name=' + prefix + '-uninstall comment=' + prefix + ' source={');
+        src.push('  :put "Uninstalling AWG Proxy Site-to-Site..."');
+        src.push('  :log info "Uninstalling AWG Proxy Site-to-Site..."');
+        if (isAuto) {
+            src.push('  :local storageDisk "disk1"');
+            src.push('  :do { :set storageDisk [/container/envs/get [find where list="' + prefix + '-env" key="AWG_STORAGE"] value] } on-error={}');
+            src.push('  :local prevTmpdir ""');
+            src.push('  :if ($storageDisk != "disk1") do={');
+            src.push('    :do { :set prevTmpdir [/container/envs/get [find where list="' + prefix + '-env" key="AWG_PREV_TMPDIR"] value] } on-error={}');
+            src.push('  }');
+        } else if (storageDisk !== 'disk1') {
+            src.push('  :local prevTmpdir ""');
+            src.push('  :do { :set prevTmpdir [/container/envs/get [find where list="' + prefix + '-env" key="AWG_PREV_TMPDIR"] value] } on-error={}');
+        }
+        src.push('  /container/stop [find where interface=veth-' + prefix + ']');
+        src.push('  :delay 7s');
+        src.push('  /container/remove [find where interface=veth-' + prefix + ']');
+        src.push('  /container/envs/remove [find where list="' + prefix + '-env"]');
+        if (isAuto) {
+            src.push('  :if ($storageDisk != "disk1") do={');
+            src.push('    /container/config set tmpdir=$prevTmpdir');
+            src.push('  }');
+        } else if (storageDisk !== 'disk1') {
+            src.push('  /container/config set tmpdir=$prevTmpdir');
+        }
+        if (hasNat) {
+            src.push('  :do { /ip/firewall/filter/remove [find where comment=' + prefix + '-awg-in] } on-error={}');
+            src.push('  :do { /ip/firewall/filter/remove [find where comment=' + prefix + '-wg-in] } on-error={}');
+            src.push('  /ip/firewall/nat/remove [find where comment=' + prefix + '-dstnat]');
+        }
+        src.push('  :do { /ip/firewall/filter/remove [find where comment=' + prefix + '-wg-mgmt] } on-error={}');
+        src.push('  /ip/firewall/nat/remove [find where src-address="' + net.natSrc + '"]');
+        src.push('  /ip/address/remove [find where address="' + net.hostAddr + '"]');
+        src.push('  /interface/veth/remove [find where name="veth-' + prefix + '"]');
+        src.push('  /interface/wireguard/peers/remove [find where interface="wg-' + prefix + '"]');
+        src.push('  /ip/address/remove [find where interface="wg-' + prefix + '"]');
+        src.push('  /interface/wireguard/remove [find where name="wg-' + prefix + '"]');
+        src.push('  :do { /file/remove [find where name~"' + prefix + '.+tar"] } on-error={}');
+        if (isAuto) {
+            src.push('  :do { /file/remove [find where name=($storageDisk . "/' + prefix + '")] } on-error={}');
+            src.push('  :if ($storageDisk != "disk1") do={');
+            src.push('    :do { /file/remove [find where name~($storageDisk . "/pull")] } on-error={}');
+            src.push('  }');
+        } else {
+            src.push('  :do { /file/remove [find where name="' + storageDisk + '/' + prefix + '"] } on-error={}');
+            if (storageDisk !== 'disk1') {
+                src.push('  :do { /file/remove [find where name~"' + storageDisk + '/pull"] } on-error={}');
+            }
+        }
+        src.push('  :put "Uninstall complete!"');
+        src.push('  :log info "Uninstall complete!"');
+        src.push('  /system/script/remove [find where name=' + prefix + '-uninstall]');
+        src.push('}');
+        return src.join('\n');
+    }
+
+    function buildContainerDownload(prefix, storageDisk, completionLines) {
+        var isAuto = storageDisk === '__auto';
+        var isExternal = storageDisk !== 'disk1' && !isAuto;
+        var lines = [];
+
+        if (isAuto) {
+            lines.push('/container/envs/add list=' + prefix + '-env key=AWG_STORAGE value=$awgDisk');
+            lines.push(':if ($awgDisk != "disk1") do={');
+            lines.push('  :local tmpPath ($awgDisk . "/pull")');
+            lines.push('  /container/envs/add list=' + prefix + '-env key=AWG_PREV_TMPDIR value=[/container/config/get tmpdir]');
+            lines.push('  /container/config set tmpdir=$tmpPath memory-high=200M');
+            lines.push('}');
+        } else if (isExternal) {
+            lines.push(':local prevTmpdir [/container/config/get tmpdir]');
+            lines.push('/container/envs/add list=' + prefix + '-env key=AWG_PREV_TMPDIR value=$prevTmpdir');
+            lines.push('/container/config set tmpdir=' + storageDisk + '/pull memory-high=200M');
+        }
+
+        lines.push('{');
+        lines.push('  :local arch [/system/resource/get architecture-name]');
+        lines.push('  :local ver [/system/resource/get version]');
+        lines.push('  :local dotPos [:find $ver "."]');
+        lines.push('  :local rest [:pick $ver ($dotPos + 1) [:len $ver]]');
+        lines.push('  :local endPos [:find $rest "."]');
+        lines.push('  :if ([:typeof $endPos] = "nil") do={ :set endPos [:find $rest " "] }');
+        lines.push('  :local minor [:tonum [:pick $rest 0 $endPos]]');
+        lines.push('  :local suffix ""');
+        lines.push('  :if ($minor <= 20) do={ :set suffix "-7.20-Docker" }');
+        lines.push('  :local file ""');
+        lines.push('  :if ($arch = "arm64") do={ :set file ("awg-proxy-arm64" . $suffix . ".tar.gz") }');
+        lines.push('  :if ($arch = "arm") do={');
+        lines.push('    :set file ("awg-proxy-arm" . $suffix . ".tar.gz")');
+        lines.push('    :local rbModel ""');
+        lines.push('    :do { :set rbModel [/system/routerboard/get model] } on-error={}');
+        lines.push('    :local bName [/system/resource/get board-name]');
+        lines.push('    :if ($rbModel~"E50UG" || $rbModel~"E60iUGS" || $bName~"^hEX") do={');
+        lines.push('      :set file ("awg-proxy-armv5" . $suffix . ".tar.gz")');
+        lines.push('    }');
+        lines.push('  }');
+        lines.push('  :if ($arch ~ "x86") do={ :set file ("awg-proxy-amd64" . $suffix . ".tar.gz") }');
+        lines.push('  :if ($file = "") do={ :error "Unsupported architecture: $arch" }');
+        lines.push('  :local url "' + REPO.dl + '$file"');
+
+        if (isAuto) {
+            lines.push('  :local filePath $file');
+            lines.push('  :if ($awgDisk != "disk1") do={');
+            lines.push('    :set filePath ($awgDisk . "/pull/" . $file)');
+            lines.push('    :do { /file/make-directory ($awgDisk . "/pull") } on-error={}');
+            lines.push('  }');
+        } else if (isExternal) {
+            lines.push('  :local filePath ("' + storageDisk + '/pull/" . $file)');
+            lines.push('  :do { /file/make-directory ' + storageDisk + '/pull } on-error={}');
+        } else {
+            lines.push('  :local filePath $file');
+        }
+
+        lines.push('  :if ([:len [/file/find where name=$filePath]] = 0) do={');
+        lines.push('    :local freeStorage 0');
+        if (isAuto) {
+            lines.push('    :if ($awgDisk = "disk1") do={');
+            lines.push('      :set freeStorage [/system/resource/get free-hdd-space]');
+            lines.push('    } else={');
+            lines.push('      :do { :set freeStorage [/disk/get [find where mount-point=$awgDisk] free] } on-error={}');
+            lines.push('    }');
+        } else if (storageDisk === 'disk1') {
+            lines.push('    :set freeStorage [/system/resource/get free-hdd-space]');
+        } else {
+            lines.push('    :do { :set freeStorage [/disk/get [find where mount-point="' + storageDisk + '"] free] } on-error={}');
+        }
+        lines.push('    :if ($freeStorage < 262144) do={');
+        lines.push('      :put ("WARNING: Low disk space (" . ($freeStorage / 1024) . "KB free). Need at least 256KB.")');
+        lines.push('      :error "Insufficient disk space"');
+        lines.push('    }');
+        lines.push('    :put ("Fetching: $url -> " . $filePath)');
+        lines.push('    /tool/fetch url=$url dst-path=$filePath http-max-redirect-count=10');
+        lines.push('  } else={');
+        lines.push('    :put ("File already exists: " . $filePath)');
+        lines.push('  }');
+
+        if (isAuto) {
+            lines.push('  :local rootDir ($awgDisk . "/' + prefix + '")');
+            lines.push('  /container/add file=$filePath interface=veth-' + prefix + ' envlist=' + prefix + '-env hostname=' + prefix + ' root-dir=$rootDir logging=no start-on-boot=yes comment=' + prefix);
+        } else {
+            lines.push('  /container/add file=$filePath interface=veth-' + prefix + ' envlist=' + prefix + '-env hostname=' + prefix + ' root-dir=' + storageDisk + '/' + prefix + ' logging=no start-on-boot=yes comment=' + prefix);
+        }
+
+        lines.push('  :if ($minor > 20) do={ [:parse "/container/set [find where interface=veth-' + prefix + '] shm-size=4M"] }');
+        lines.push('  /file/remove $filePath');
+        lines.push('  /container/start [find where interface=veth-' + prefix + ']');
+        lines.push('  :do { /file/remove [find where name="console-dump.txt"] } on-error={}');
+        lines.push('  :put "Waiting for container to start..."');
+        lines.push('  :delay 5s');
+        if (completionLines) {
+            for (var i = 0; i < completionLines.length; i++) {
+                lines.push('  ' + completionLines[i]);
+            }
+        }
+        lines.push('}');
+
+        return lines;
+    }
+
+    // ============================================================
+    // Main
+    // ============================================================
+    function generate() {
+        var mode = getDeployMode();
+        document.getElementById('errors-container').innerHTML = '';
+        document.getElementById('parsed-section').style.display = 'none';
+        document.getElementById('output-section').style.display = 'none';
+        document.getElementById('routing-section').style.display = 'none';
+        document.getElementById('uninstall-section').style.display = 'none';
+        document.getElementById('s2s-output-section').style.display = 'none';
+        document.getElementById('srv-output-section').style.display = 'none';
+
+        if (mode === 'site-to-site') {
+            generateS2S();
+            return;
+        }
+        if (mode === 'server') {
+            generateServer();
+            return;
+        }
+
+        var text = document.getElementById('conf-input').value;
+
+        if (!text.trim()) {
+            showError(t('errEmpty'));
+            return;
+        }
+
+        var parsed = parseConf(text);
+        var errors = validate(parsed);
+
+        // Auto-select DoH forwarder based on DNS servers in config
+        if (parsed.interface.DNS) autoSelectDoh(parsed.interface.DNS);
+
+        var has = Object.keys(parsed.interface).length > 0 || Object.keys(parsed.peer).length > 0;
+        if (has) {
+            document.getElementById('fields-display').innerHTML = renderFields(parsed);
+            document.getElementById('parsed-section').style.display = 'block';
+        }
+
+        if (errors.length > 0) {
+            var html = '<div class="alert alert-error"><strong>' + esc(t('errTitle')) + '</strong><ul style="margin-top:6px;padding-left:18px">';
+            errors.forEach(function (e) {
+                html += '<li>' + esc(e) + '</li>';
+            });
+            html += '</ul></div>';
+            document.getElementById('errors-container').innerHTML = html;
+            return;
+        }
+
+        var prefix = getPrefixName();
+        if (!(/^[a-zA-Z0-9]$/.test(prefix) || /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,28}[a-zA-Z0-9]$/.test(prefix))) {
+            showError(t('errPrefix'));
+            return;
+        }
+
+        var scenario = getScenario();
+        var selectedServices = [];
+        var exclusions = [];
+        if (scenario === 'dns-fwd') {
+            selectedServices = getSelectedServices();
+            if (selectedServices.length === 0 && getCustomDomains().length === 0) {
+                showError(t('noServiceSelected'));
+                return;
+            }
+        } else if (scenario === 'non-ru') {
+            exclusions = getSelectedExclusions();
+        }
+
+        var mode = getDeployMode();
+        var net = getTunnelNetwork(prefix);
+        var commands;
+
+        if (mode === 'standalone') {
+            var proxyIP = document.getElementById('proxy-ip').value.trim();
+            if (!isValidIP(proxyIP)) {
+                showError(t('errProxyIp'));
+                return;
+            }
+            commands = buildStandaloneCommands(parsed, proxyIP, prefix, net, scenario);
+        } else {
+            commands = buildCommands(parsed, getStorageDisk(), prefix, net, scenario);
+        }
+
+        document.getElementById('output').dataset.plain = commands;
+        document.getElementById('output').innerHTML = highlight(commands);
+        document.getElementById('output-section').style.display = 'block';
+
+        var dohFwd = getSelectedDohForwarder();
+        var storageDisk = getStorageDisk();
+
+        // Routing section (separate for dns-fwd/non-ru to avoid terminal buffer overflow)
+        var routingSection = document.getElementById('routing-section');
+        if (scenario === 'dns-fwd' || scenario === 'non-ru') {
+            var routingCommands = '';
+            if (scenario === 'dns-fwd') {
+                routingCommands = buildDnsRoutingScenario(prefix, selectedServices, dohFwd).join('\n') + '\n';
+            } else {
+                var serverHost = parsed.peer && parsed.peer.Endpoint ? parsed.peer.Endpoint.replace(/:[\d]+$/, '') : '';
+                routingCommands = buildNonRuScenario(prefix, exclusions, dohFwd, serverHost, storageDisk, mode).join('\n') + '\n';
+            }
+            document.getElementById('routing-output').dataset.plain = routingCommands;
+            document.getElementById('routing-output').innerHTML = highlight(routingCommands);
+            routingSection.style.display = 'block';
+            applyCollapse('routing-output', 'expand-routing');
+        } else {
+            routingSection.style.display = 'none';
+        }
+
+        var uninstall = [
+            '# To uninstall ' + prefix + ', run:',
+            '/system/script/run ' + prefix + '-uninstall',
+        ].join('\n');
+        document.getElementById('uninstall-output').dataset.plain = uninstall;
+        document.getElementById('uninstall-output').innerHTML = highlight(uninstall);
+        document.getElementById('uninstall-section').style.display = 'block';
+
+        applyCollapse('output', 'expand-output');
+        applyCollapse('uninstall-output', 'expand-uninstall');
+
+        setTimeout(function () {
+            document.getElementById('output-section').scrollIntoView({behavior: 'smooth', block: 'start'});
+        }, 80);
+    }
+
+    function generateS2S() {
+        var endpointB = document.getElementById('s2s-endpoint-b').value.trim();
+        var subnet = document.getElementById('s2s-tunnel-subnet').value.trim();
+        var portStr = document.getElementById('s2s-awg-port').value.trim();
+
+        if (!isValidEndpointHost(endpointB)) { showError(t('errEndpointB')); return; }
+        if (!parseSlash30(subnet)) { showError(t('errTunnelSubnet')); return; }
+
+        var port = parseInt(portStr, 10);
+        if (isNaN(port) || port < 1024 || port > 65535) { showError(t('errAwgPort')); return; }
+
+        var awgParams = ensureS2SParams();
+        awgParams.port = port;
+
+        var storageA = document.getElementById('s2s-storage-a').value;
+        var storageB = document.getElementById('s2s-storage-b').value;
+        var dnsServer = document.getElementById('s2s-dns').value.trim();
+        var lanA = document.getElementById('s2s-lan-a').value.trim();
+        var lanB = document.getElementById('s2s-lan-b').value.trim();
+
+        if (!isValidIP(endpointB) && !dnsServer) { showError(t('errDnsRequired')); return; }
+        if (lanA && !isValidIPCIDR(lanA)) { showError(t('errLanSubnet')); return; }
+        if (lanB && !isValidIPCIDR(lanB)) { showError(t('errLanSubnet')); return; }
+        var prefix = getPrefixName();
+
+        var result = buildSiteToSiteCommands(endpointB, subnet, awgParams, storageA, storageB, prefix, dnsServer, lanA, lanB);
+
+        document.getElementById('s2s-out-a').dataset.plain = result.sideA;
+        document.getElementById('s2s-out-a').innerHTML = highlight(result.sideA);
+        document.getElementById('s2s-out-b').dataset.plain = result.sideB;
+        document.getElementById('s2s-out-b').innerHTML = highlight(result.sideB);
+        document.getElementById('s2s-uninst-a').dataset.plain = result.uninstallA;
+        document.getElementById('s2s-uninst-a').innerHTML = highlight(result.uninstallA);
+        document.getElementById('s2s-uninst-b').dataset.plain = result.uninstallB;
+        document.getElementById('s2s-uninst-b').innerHTML = highlight(result.uninstallB);
+        document.getElementById('s2s-output-section').style.display = 'block';
+
+        setTimeout(function() {
+            document.getElementById('s2s-output-section').scrollIntoView({behavior:'smooth',block:'start'});
+        }, 80);
+    }
+
+    function showError(msg) {
+        document.getElementById('errors-container').innerHTML =
+            '<div class="alert alert-error">' + esc(msg) + '</div>';
+    }
+
+    function clearAll() {
+        document.getElementById('conf-input').value = '';
+        document.getElementById('errors-container').innerHTML = '';
+        document.getElementById('parsed-section').style.display = 'none';
+        document.getElementById('output-section').style.display = 'none';
+        document.getElementById('routing-section').style.display = 'none';
+        document.getElementById('uninstall-section').style.display = 'none';
+        document.getElementById('s2s-output-section').style.display = 'none';
+        document.getElementById('storage-select').value = '__auto';
+        document.getElementById('storage-custom').style.display = 'none';
+        document.getElementById('storage-custom').value = '';
+        document.getElementById('prefix-select').value = 'awg-proxy-1';
+        document.getElementById('netwatch-enable').checked = true;
+        document.getElementById('df-enable').checked = false;
+        document.getElementById('prefix-custom').style.display = 'none';
+        document.getElementById('prefix-custom').value = '';
+        document.getElementById('proxy-ip').value = '';
+        document.getElementById('s2s-endpoint-b').value = '';
+        document.getElementById('s2s-tunnel-subnet').value = '10.0.0.0/30';
+        document.getElementById('s2s-storage-a').value = '__auto';
+        document.getElementById('s2s-storage-b').value = '__auto';
+        document.getElementById('s2s-dns').value = '';
+        document.getElementById('s2s-lan-a').value = '';
+        document.getElementById('s2s-lan-b').value = '';
+        s2sParams = null;
+        var radios = document.getElementsByName('deploy-mode');
+        radios[0].checked = true;
+        onModeChange();
+        var scenarioRadios = document.getElementsByName('scenario');
+        scenarioRadios[0].checked = true;
+        onScenarioChange();
+        initServiceCheckboxes();
+        initExclusionCheckboxes();
+        // Reset DoH forwarder
+        var dohRadios = document.getElementsByName('doh-fwd');
+        dohRadios[0].checked = true;
+        document.getElementById('doh-custom-url').style.display = 'none';
+        document.getElementById('doh-custom-url').value = '';
+        document.getElementById('conf-input').focus();
+    }
+
+    function downloadFile(name, content) {
+        var b = new Blob([content], { type: 'application/octet-stream' });
+        var u = URL.createObjectURL(b), a = document.createElement('a');
+        a.href = u; a.download = name;
+        document.body.appendChild(a); a.click(); document.body.removeChild(a);
+        setTimeout(function () { URL.revokeObjectURL(u); }, 1500);
+    }
+
+    function downloadAwgConf() {
+        if (lastAwgConf) downloadFile(lastAwgConfName, lastAwgConf);
+    }
+
+    function copyOutput(preId, btnId) {
+        var plain = document.getElementById(preId).dataset.plain || '';
+        if (!plain) return;
+        var btn = document.getElementById(btnId);
+        if (navigator.clipboard && navigator.clipboard.writeText) {
+            navigator.clipboard.writeText(plain).then(function () {
+                flash(btn);
+            }).catch(function () {
+                fallback(plain, btn);
+            });
+        } else {
+            fallback(plain, btn);
+        }
+    }
+
+    function fallback(text, btn) {
+        var ta = document.createElement('textarea');
+        ta.value = text;
+        ta.style.position = 'fixed';
+        ta.style.opacity = '0';
+        document.body.appendChild(ta);
+        ta.focus();
+        ta.select();
+        try {
+            document.execCommand('copy');
+            flash(btn);
+        } catch (e) {
+        }
+        document.body.removeChild(ta);
+    }
+
+    function flash(btn) {
+        var orig = btn.textContent;
+        btn.textContent = t('copied');
+        btn.classList.remove('btn-secondary');
+        btn.classList.add('btn-success');
+        setTimeout(function () {
+            btn.textContent = orig;
+            btn.classList.remove('btn-success');
+            btn.classList.add('btn-secondary');
+        }, 2000);
+    }
+
+    // ============================================================
+    // Collapsible script blocks
+    // ============================================================
+    function toggleCollapse(preId, btnId) {
+        var pre = document.getElementById(preId);
+        var btn = document.getElementById(btnId);
+        if (pre.classList.contains('collapsed')) {
+            pre.classList.remove('collapsed');
+            btn.textContent = t('btnCollapse');
+        } else {
+            pre.classList.add('collapsed');
+            btn.textContent = t('btnExpand');
+        }
+    }
+
+    function applyCollapse(preId, btnId) {
+        var pre = document.getElementById(preId);
+        var btn = document.getElementById(btnId);
+        var lineCount = (pre.textContent || '').split('\n').length;
+        if (lineCount > 18) {
+            pre.classList.add('collapsed');
+            btn.style.display = 'block';
+            btn.textContent = t('btnExpand');
+        } else {
+            pre.classList.remove('collapsed');
+            btn.style.display = 'none';
+        }
+    }
+
+    // ============================================================
+    // DoH selector handler
+    // ============================================================
+    document.getElementById('doh-selector').addEventListener('change', function(e) {
+        var custom = document.getElementById('doh-custom-url');
+        if (e.target.value === '__custom') {
+            custom.style.display = 'block';
+            custom.focus();
+        } else {
+            custom.style.display = 'none';
+        }
+    });
+
+    document.getElementById('conf-input').addEventListener('keydown', function (e) {
+        if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') generate();
+    });
+
+    // Restore saved settings
+    // Initialize checkboxes
+    initServiceCheckboxes();
+    initExclusionCheckboxes();
+
+    // Apply i18n on load
+    applyI18n();
+    // Refresh scenario hint (restores info icon for dns-fwd)
+    onScenarioChange();
+
+    // Check URL params for server mode (add client)
+    (function() {
+        var urlData = parseURLParams();
+        if (!urlData) return;
+        serverFromURL = urlData;
+        // Switch to server mode
+        var radios = document.getElementsByName('deploy-mode');
+        for (var i = 0; i < radios.length; i++) {
+            if (radios[i].value === 'server') { radios[i].checked = true; break; }
+        }
+        onModeChange();
+        // Fill fields from URL
+        document.getElementById('srv-endpoint').value = urlData.endpoint;
+        document.getElementById('srv-awg-port').value = urlData.awgPort;
+        document.getElementById('srv-tunnel-subnet').value = urlData.tunnelSubnet;
+        document.getElementById('srv-client-num').value = urlData.nextClient;
+        // Set server LAN from URL
+        if (urlData.serverLAN) document.getElementById('srv-lan-server').value = urlData.serverLAN;
+        // Set net mode from URL
+        if (urlData.netMode) {
+            var netRadios = document.getElementsByName('srv-net');
+            for (var i = 0; i < netRadios.length; i++) {
+                if (netRadios[i].value === urlData.netMode) { netRadios[i].checked = true; break; }
+            }
+            onSrvNetChange();
+        }
+        // Set prefix
+        var prefixSel = document.getElementById('prefix-select');
+        var found = false;
+        for (var i = 0; i < prefixSel.options.length; i++) {
+            if (prefixSel.options[i].value === urlData.prefix) { prefixSel.value = urlData.prefix; found = true; break; }
+        }
+        if (!found) { prefixSel.value = '__custom'; document.getElementById('prefix-custom').value = urlData.prefix; document.getElementById('prefix-custom').style.display = 'block'; }
+        // Show add-client banner
+        document.getElementById('srv-add-client-banner').style.display = 'block';
+        document.getElementById('srv-add-client-info').textContent = urlData.endpoint + ':' + urlData.awgPort + ' \u2014 client #' + urlData.nextClient;
+        // Hide server storage (not needed for add-client)
+        document.getElementById('srv-storage-server').parentElement.style.display = 'none';
+    })();
+
+    // ============================================================
+    // Infographic — "How will this work?" SVG diagrams
+    // ============================================================
+
+    function _igEsc(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
+
+    function _igFormatTip(text) {
+        var lines = text.split('\n');
+        var html = '<div class="ig-tip-title">' + _igEsc(lines[0]) + '</div>';
+        for (var i = 1; i < lines.length; i++) {
+            var line = lines[i];
+            if (line.trim() === '') { html += '<br>'; continue; }
+            var escaped = _igEsc(line);
+            escaped = escaped.replace(/(\/[a-z][a-z0-9\/]*(?:\/[a-z][a-z0-9-]*)*)/g, '<code>$1</code>');
+            escaped = escaped.replace(/\b(dst-address-list=!?[A-Za-z0-9_-]+|src-address=[0-9.\/]+|out-interface=[a-z0-9*-]+|mark-(?:connection|routing)|change-mss|clamp-to-pmtu|masquerade|dstnat|accept|type=FWD)\b/g, '<code>$1</code>');
+            html += '<div>' + escaped + '</div>';
+        }
+        return html;
+    }
+
+    var IG_COLORS = {
+        wireguard:  { fill: '#dbeafe', stroke: '#3b82f6', dark: '#1e3a5f' },
+        container:  { fill: '#dcfce7', stroke: '#22c55e', dark: '#1a3a2a' },
+        dns:        { fill: '#ffedd5', stroke: '#f97316', dark: '#3a2a1a' },
+        routing:    { fill: '#ede9fe', stroke: '#8b5cf6', dark: '#2a1f4a' },
+        firewall:   { fill: '#fee2e2', stroke: '#ef4444', dark: '#3a1a1a' },
+        mangle:     { fill: '#fef9c3', stroke: '#eab308', dark: '#3a3a1a' },
+        external:   { fill: '#f3f4f6', stroke: '#9ca3af', dark: '#2a2a2a' },
+        rulist:     { fill: '#e0e7ff', stroke: '#6366f1', dark: '#1e1e4a' },
+        scheduler:  { fill: '#fce7f3', stroke: '#ec4899', dark: '#3a1a2a' },
+        manual:     { fill: '#fef3c7', stroke: '#f59e0b', dark: '#3a2f1a' }
+    };
+
+    var _igDark = function() { return window.matchMedia && window.matchMedia('(prefers-color-scheme:dark)').matches; };
+
+    function _igBlock(x, y, w, h, colorKey, labelKey, tipKey) {
+        var c = IG_COLORS[colorKey] || IG_COLORS.external;
+        var dark = _igDark();
+        return '<g class="ig-block" data-tip="' + _igEsc(t(tipKey)) + '">' +
+            '<rect x="'+x+'" y="'+y+'" width="'+w+'" height="'+h+'" rx="8" ' +
+            'fill="'+(dark?c.dark:c.fill)+'" stroke="'+c.stroke+'" stroke-width="1.5"/>' +
+            '<text x="'+(x+w/2)+'" y="'+(y+h/2)+'" text-anchor="middle" dominant-baseline="central" ' +
+            'font-size="12" font-weight="600" fill="'+(dark?'#e5e7eb':'#1f2937')+'">' +
+            _igEsc(t(labelKey)) + '</text></g>';
+    }
+
+    function _igArrow(x1, y1, x2, y2, dashed, bidir) {
+        var d;
+        if (x1 === x2 || y1 === y2) { d = 'M'+x1+','+y1+' L'+x2+','+y2; }
+        else { var m = y1+(y2-y1)/2; d = 'M'+x1+','+y1+' C'+x1+','+m+' '+x2+','+m+' '+x2+','+y2; }
+        var color = _igDark() ? '#888' : '#6b7280';
+        return '<path d="'+d+'" fill="none" stroke="'+color+'" stroke-width="1.5" ' +
+            (dashed ? 'stroke-dasharray="5,3" ' : '') +
+            'marker-end="url(#ig-arrow)"' + (bidir ? ' marker-start="url(#ig-arrow)"' : '') + '/>';
+    }
+
+    function _igDefs() {
+        var color = _igDark() ? '#888' : '#6b7280';
+        return '<defs><marker id="ig-arrow" viewBox="0 0 10 7" refX="9" refY="3.5" ' +
+            'markerWidth="8" markerHeight="6" orient="auto-start-reverse">' +
+            '<polygon points="0 0, 10 3.5, 0 7" fill="'+color+'"/></marker></defs>';
+    }
+
+    function _igSvg(w, h, content) {
+        return '<svg viewBox="0 0 '+w+' '+h+'" width="100%" style="max-width:'+w+'px" xmlns="http://www.w3.org/2000/svg" ' +
+            'font-family="-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif">' +
+            _igDefs() + content + '</svg>';
+    }
+
+    // Consistent grid layout for Container/Standalone diagrams:
+    //   Col L (x=10, w=150)   Col R (x=210, w=150)
+    //   Standard 5 rows: [10, 80, 150, 220, 290]
+    //   Non-RU 6 rows: [10, 80, 150, 220, 290, 360]
+    var CL = 10, CR = 210, BW = 150, BH = 44;
+    var RY = [10, 80, 150, 220, 290];
+    function _clc() { return CL + BW/2; }     // left col center = 85
+    function _crc() { return CR + BW/2; }     // right col center = 285
+
+    // --- Container + dns-fwd ---
+    function _igContainerDns() {
+        var s = '';
+        // Row 0: LAN + DNS
+        s += _igBlock(CL, RY[0], BW, BH, 'external', 'igLan', 'igTipLan');
+        s += _igBlock(CR, RY[0], BW, BH, 'dns', 'igDns', 'igTipDns');
+        s += _igArrow(_clc(), RY[0]+BH, _clc(), RY[1]);                    // LAN -> Mangle
+
+        // Row 1: Mangle + Routing, DNS feeds down to Mangle level then left into Mangle
+        s += _igBlock(CL, RY[1], BW, BH, 'mangle', 'igMangle', 'igTipMangle');
+        s += _igBlock(CR, RY[1], BW, BH, 'routing', 'igRouting', 'igTipRouting');
+        s += _igArrow(CL+BW, RY[1]+BH/2, CR, RY[1]+BH/2);                 // Mangle -> Routing
+        s += _igArrow(_crc(), RY[0]+BH, CL+BW, RY[1], true);              // DNS -> Mangle (dashed curve)
+
+        // Row 2: Firewall + WireGuard
+        s += _igBlock(CL, RY[2], BW, BH, 'firewall', 'igFirewall', 'igTipFirewall');
+        s += _igBlock(CR, RY[2], BW, BH, 'wireguard', 'igWireguard', 'igTipWireguard');
+        s += _igArrow(_clc(), RY[1]+BH, _clc(), RY[2]);                    // Mangle -> Firewall
+        s += _igArrow(_crc(), RY[1]+BH, _crc(), RY[2]);                    // Routing -> WireGuard
+
+        // Row 3: Internet (WAN) + Container on router
+        s += _igBlock(CL, RY[3], BW, BH, 'external', 'igInternetWan', 'igTipInternetWan');
+        s += _igBlock(CR, RY[3], BW, BH, 'container', 'igContainerOnRouter', 'igTipContainerOnRouter');
+        s += _igArrow(_clc(), RY[2]+BH, _clc(), RY[3]);                    // Firewall -> Internet
+        s += _igArrow(_crc(), RY[2]+BH, _crc(), RY[3]);                    // WG -> Container
+
+        // Row 4: AWG Server
+        s += _igBlock(CR, RY[4], BW, BH, 'external', 'igAwgServer', 'igTipAwgServer');
+        s += _igArrow(_crc(), RY[3]+BH, _crc(), RY[4]);                    // Container -> AWG
+
+        return _igSvg(CR+BW+10, RY[4]+BH+10, s);
+    }
+
+    // --- Container + tunnel ---
+    function _igContainerTunnel() {
+        var s = '';
+        // Row 0: LAN
+        s += _igBlock(CL, RY[0], BW, BH, 'external', 'igLan', 'igTipLan');
+        s += _igArrow(_clc(), RY[0]+BH, _clc(), RY[1]);                    // LAN -> Manual Config
+
+        // Row 1: Manual Config + Routing (manual)
+        s += _igBlock(CL, RY[1], BW, BH, 'manual', 'igManualConfig', 'igTipManualConfig');
+        s += _igBlock(CR, RY[1], BW, BH, 'routing', 'igRouting', 'igTipRoutingTunnel');
+        s += _igArrow(_clc(), RY[1]+BH, _clc(), RY[2]);                    // Manual -> Firewall
+        s += _igArrow(_crc(), RY[1]+BH, _crc(), RY[2]);                    // Routing -> WG
+
+        // Row 2: Firewall + WireGuard
+        s += _igBlock(CL, RY[2], BW, BH, 'firewall', 'igFirewall', 'igTipFirewall');
+        s += _igBlock(CR, RY[2], BW, BH, 'wireguard', 'igWireguard', 'igTipWireguard');
+
+        // Row 3: Internet (WAN) + Container on router
+        s += _igBlock(CL, RY[3], BW, BH, 'external', 'igInternetWan', 'igTipInternetWan');
+        s += _igBlock(CR, RY[3], BW, BH, 'container', 'igContainerOnRouter', 'igTipContainerOnRouter');
+        s += _igArrow(_clc(), RY[2]+BH, _clc(), RY[3]);                    // Firewall -> Internet
+        s += _igArrow(_crc(), RY[2]+BH, _crc(), RY[3]);                    // WG -> Container
+
+        // Row 4: AWG Server
+        s += _igBlock(CR, RY[4], BW, BH, 'external', 'igAwgServer', 'igTipAwgServer');
+        s += _igArrow(_crc(), RY[3]+BH, _crc(), RY[4]);                    // Container -> AWG
+
+        return _igSvg(CR+BW+10, RY[4]+BH+10, s);
+    }
+
+    // --- Container + non-ru ---
+    function _igContainerNonRu() {
+        var s = '';
+        // Layout: RU List + Scheduler left of main columns, connected to Mangle
+        // Extra column: x=10, main columns shifted right
+        var EX = 10, EW = 130;                  // extra col (RU List, Scheduler)
+        var ML = EX + EW + 20;                  // main left col x = 160
+        var MR = ML + BW + 50;                  // main right col x = 360
+        var mlc = ML + BW/2, mrc = MR + BW/2;  // centers
+
+        // Row 0: LAN
+        s += _igBlock(ML, RY[0], BW, BH, 'external', 'igLan', 'igTipLan');
+        s += _igArrow(mlc, RY[0]+BH, mlc, RY[1]);                          // LAN -> Mangle
+
+        // Row 0-1: Scheduler (row 0) -> RU List (row 1), left column
+        s += _igBlock(EX, RY[0], EW, BH, 'scheduler', 'igScheduler', 'igTipScheduler');
+        s += _igBlock(EX, RY[1], EW, BH, 'rulist', 'igRuList', 'igTipRuList');
+        s += _igArrow(EX+EW/2, RY[0]+BH, EX+EW/2, RY[1]);                 // Scheduler -> RU List (down)
+        s += _igArrow(EX+EW, RY[1]+BH/2, ML, RY[1]+BH/2, true);           // RU List -> Mangle (dashed, right)
+
+        // Row 1: Mangle + Routing
+        s += _igBlock(ML, RY[1], BW, BH, 'mangle', 'igMangle', 'igTipMangleNonRu');
+        s += _igBlock(MR, RY[1], BW, BH, 'routing', 'igRouting', 'igTipRouting');
+        s += _igArrow(ML+BW, RY[1]+BH/2, MR, RY[1]+BH/2);                 // Mangle -> Routing
+
+        // Row 2: Firewall + WireGuard
+        s += _igBlock(ML, RY[2], BW, BH, 'firewall', 'igFirewall', 'igTipFirewall');
+        s += _igBlock(MR, RY[2], BW, BH, 'wireguard', 'igWireguard', 'igTipWireguard');
+        s += _igArrow(mlc, RY[1]+BH, mlc, RY[2]);
+        s += _igArrow(mrc, RY[1]+BH, mrc, RY[2]);
+
+        // Row 3: Internet (WAN) + Container
+        s += _igBlock(ML, RY[3], BW, BH, 'external', 'igInternetWan', 'igTipInternetWan');
+        s += _igBlock(MR, RY[3], BW, BH, 'container', 'igContainerOnRouter', 'igTipContainerOnRouter');
+        s += _igArrow(mlc, RY[2]+BH, mlc, RY[3]);
+        s += _igArrow(mrc, RY[2]+BH, mrc, RY[3]);
+
+        // Row 4: AWG Server
+        s += _igBlock(MR, RY[4], BW, BH, 'external', 'igAwgServer', 'igTipAwgServer');
+        s += _igArrow(mrc, RY[3]+BH, mrc, RY[4]);
+
+        return _igSvg(MR+BW+10, RY[4]+BH+10, s);
+    }
+
+    // --- Standalone + dns-fwd ---
+    function _igStandaloneDns() {
+        var s = '';
+        // Row 0: LAN + DNS
+        s += _igBlock(CL, RY[0], BW, BH, 'external', 'igLan', 'igTipLan');
+        s += _igBlock(CR, RY[0], BW, BH, 'dns', 'igDns', 'igTipDns');
+        s += _igArrow(_clc(), RY[0]+BH, _clc(), RY[1]);
+
+        // Row 1: Mangle + Routing, DNS feeds down to Mangle
+        s += _igBlock(CL, RY[1], BW, BH, 'mangle', 'igMangle', 'igTipMangle');
+        s += _igBlock(CR, RY[1], BW, BH, 'routing', 'igRouting', 'igTipRouting');
+        s += _igArrow(CL+BW, RY[1]+BH/2, CR, RY[1]+BH/2);
+        s += _igArrow(_crc(), RY[0]+BH, CL+BW, RY[1], true);              // DNS -> Mangle (dashed curve)
+
+        // Row 2: Firewall + WireGuard
+        s += _igBlock(CL, RY[2], BW, BH, 'firewall', 'igFirewall', 'igTipFirewall');
+        s += _igBlock(CR, RY[2], BW, BH, 'wireguard', 'igWireguard', 'igTipWireguard');
+        s += _igArrow(_clc(), RY[1]+BH, _clc(), RY[2]);
+        s += _igArrow(_crc(), RY[1]+BH, _crc(), RY[2]);
+
+        // Row 3: Internet (WAN) + Standalone
+        s += _igBlock(CL, RY[3], BW, BH, 'external', 'igInternetWan', 'igTipInternetWan');
+        s += _igBlock(CR, RY[3], BW, BH, 'container', 'igStandalone', 'igTipStandalone');
+        s += _igArrow(_clc(), RY[2]+BH, _clc(), RY[3]);
+        s += _igArrow(_crc(), RY[2]+BH, _crc(), RY[3]);
+
+        // Row 4: AWG Server
+        s += _igBlock(CR, RY[4], BW, BH, 'external', 'igAwgServer', 'igTipAwgServer');
+        s += _igArrow(_crc(), RY[3]+BH, _crc(), RY[4]);
+
+        return _igSvg(CR+BW+10, RY[4]+BH+10, s);
+    }
+
+    // --- Standalone + tunnel ---
+    function _igStandaloneTunnel() {
+        var s = '';
+        // Row 0: LAN
+        s += _igBlock(CL, RY[0], BW, BH, 'external', 'igLan', 'igTipLan');
+        s += _igArrow(_clc(), RY[0]+BH, _clc(), RY[1]);
+
+        // Row 1: Manual Config + Routing (manual)
+        s += _igBlock(CL, RY[1], BW, BH, 'manual', 'igManualConfig', 'igTipManualConfig');
+        s += _igBlock(CR, RY[1], BW, BH, 'routing', 'igRouting', 'igTipRoutingTunnel');
+        s += _igArrow(_clc(), RY[1]+BH, _clc(), RY[2]);
+        s += _igArrow(_crc(), RY[1]+BH, _crc(), RY[2]);
+
+        // Row 2: Firewall + WireGuard
+        s += _igBlock(CL, RY[2], BW, BH, 'firewall', 'igFirewall', 'igTipFirewall');
+        s += _igBlock(CR, RY[2], BW, BH, 'wireguard', 'igWireguard', 'igTipWireguard');
+
+        // Row 3: Internet (WAN) + Standalone
+        s += _igBlock(CL, RY[3], BW, BH, 'external', 'igInternetWan', 'igTipInternetWan');
+        s += _igBlock(CR, RY[3], BW, BH, 'container', 'igStandalone', 'igTipStandalone');
+        s += _igArrow(_clc(), RY[2]+BH, _clc(), RY[3]);
+        s += _igArrow(_crc(), RY[2]+BH, _crc(), RY[3]);
+
+        // Row 4: AWG Server
+        s += _igBlock(CR, RY[4], BW, BH, 'external', 'igAwgServer', 'igTipAwgServer');
+        s += _igArrow(_crc(), RY[3]+BH, _crc(), RY[4]);
+
+        return _igSvg(CR+BW+10, RY[4]+BH+10, s);
+    }
+
+    // --- Standalone + non-ru ---
+    function _igStandaloneNonRu() {
+        var s = '';
+        var EX = 10, EW = 130;
+        var ML = EX + EW + 20, MR = ML + BW + 50;
+        var mlc = ML + BW/2, mrc = MR + BW/2;
+
+        // Row 0: LAN
+        s += _igBlock(ML, RY[0], BW, BH, 'external', 'igLan', 'igTipLan');
+        s += _igArrow(mlc, RY[0]+BH, mlc, RY[1]);
+
+        // RU List + Scheduler left of Mangle
+        s += _igBlock(EX, RY[0], EW, BH, 'scheduler', 'igScheduler', 'igTipScheduler');
+        s += _igBlock(EX, RY[1], EW, BH, 'rulist', 'igRuList', 'igTipRuList');
+        s += _igArrow(EX+EW/2, RY[0]+BH, EX+EW/2, RY[1]);
+        s += _igArrow(EX+EW, RY[1]+BH/2, ML, RY[1]+BH/2, true);
+
+        // Row 1: Mangle + Routing
+        s += _igBlock(ML, RY[1], BW, BH, 'mangle', 'igMangle', 'igTipMangleNonRu');
+        s += _igBlock(MR, RY[1], BW, BH, 'routing', 'igRouting', 'igTipRouting');
+        s += _igArrow(ML+BW, RY[1]+BH/2, MR, RY[1]+BH/2);
+
+        // Row 2: Firewall + WireGuard
+        s += _igBlock(ML, RY[2], BW, BH, 'firewall', 'igFirewall', 'igTipFirewall');
+        s += _igBlock(MR, RY[2], BW, BH, 'wireguard', 'igWireguard', 'igTipWireguard');
+        s += _igArrow(mlc, RY[1]+BH, mlc, RY[2]);
+        s += _igArrow(mrc, RY[1]+BH, mrc, RY[2]);
+
+        // Row 3: Internet (WAN) + Standalone
+        s += _igBlock(ML, RY[3], BW, BH, 'external', 'igInternetWan', 'igTipInternetWan');
+        s += _igBlock(MR, RY[3], BW, BH, 'container', 'igStandalone', 'igTipStandalone');
+        s += _igArrow(mlc, RY[2]+BH, mlc, RY[3]);
+        s += _igArrow(mrc, RY[2]+BH, mrc, RY[3]);
+
+        // Row 4: AWG Server
+        s += _igBlock(MR, RY[4], BW, BH, 'external', 'igAwgServer', 'igTipAwgServer');
+        s += _igArrow(mrc, RY[3]+BH, mrc, RY[4]);
+
+        return _igSvg(MR+BW+10, RY[4]+BH+10, s);
+    }
+
+    // --- Site-to-Site ---
+    function _igSiteToSite() {
+        var s = '';
+        var tc = _igDark() ? '#e5e7eb' : '#1f2937';
+        var SY = [24, 84, 144, 204, 264, 334];  // 6 rows: last = Internet
+        var bw = 150, bh = 40, gap = 20;
+        var ax = 10, ac = ax + bw/2;             // Side A
+        var bx = ax + bw + gap, bc = bx + bw/2;  // Side B
+        var totalW = bx + bw + 10;
+
+        // Side A (client)
+        s += '<text x="'+ac+'" y="14" text-anchor="middle" font-size="13" font-weight="700" fill="'+tc+'">' + _igEsc(t('igRouterA')) + '</text>';
+        s += _igBlock(ax, SY[0], bw, bh, 'external', 'igLan', 'igTipLan');
+        s += _igArrow(ac, SY[0]+bh, ac, SY[1], false, true);
+        s += _igBlock(ax, SY[1], bw, bh, 'routing', 'igRouting', 'igTipRoutingS2S');
+        s += _igArrow(ac, SY[1]+bh, ac, SY[2], false, true);
+        s += _igBlock(ax, SY[2], bw, bh, 'firewall', 'igFirewall', 'igTipFirewall');
+        s += _igArrow(ac, SY[2]+bh, ac, SY[3], false, true);
+        s += _igBlock(ax, SY[3], bw, bh, 'wireguard', 'igWireguard', 'igTipWireguard');
+        s += _igArrow(ac, SY[3]+bh, ac, SY[4], false, true);
+        s += _igBlock(ax, SY[4], bw, bh, 'container', 'igContainerClient', 'igTipContainer');
+
+        // Side B (server)
+        s += '<text x="'+bc+'" y="14" text-anchor="middle" font-size="13" font-weight="700" fill="'+tc+'">' + _igEsc(t('igRouterB')) + '</text>';
+        s += _igBlock(bx, SY[0], bw, bh, 'external', 'igLan', 'igTipLan');
+        s += _igArrow(bc, SY[0]+bh, bc, SY[1], false, true);
+        s += _igBlock(bx, SY[1], bw, bh, 'routing', 'igRouting', 'igTipRoutingS2S');
+        s += _igArrow(bc, SY[1]+bh, bc, SY[2], false, true);
+        s += _igBlock(bx, SY[2], bw, bh, 'firewall', 'igFirewall', 'igTipFirewallS2S');
+        s += _igArrow(bc, SY[2]+bh, bc, SY[3], false, true);
+        s += _igBlock(bx, SY[3], bw, bh, 'wireguard', 'igWireguard', 'igTipWireguard');
+        s += _igArrow(bc, SY[3]+bh, bc, SY[4], false, true);
+        s += _igBlock(bx, SY[4], bw, bh, 'container', 'igContainerServer', 'igTipContainer');
+
+        // Internet below both containers — 2 arrows down
+        var iw = 180;
+        var ix = (totalW - iw) / 2;
+        s += _igBlock(ix, SY[5], iw, bh, 'external', 'igInternet', 'igTipInternet');
+        s += _igArrow(ac, SY[4]+bh, ix+iw/2-40, SY[5]);                    // Client -> Internet
+        s += _igArrow(bc, SY[4]+bh, ix+iw/2+40, SY[5]);                    // Server -> Internet
+
+        return _igSvg(totalW, SY[5]+bh+10, s);
+    }
+
+    // --- Server (1:N Hub) ---
+    function _igServerHub() {
+        var s = '';
+        var tc = _igDark() ? '#e5e7eb' : '#1f2937';
+        var SY = [24, 84, 144, 204, 264, 334];  // 6 rows: last = Internet
+        var bw = 140, bh = 40, gap = 20;
+
+        // 3 columns: Server | Client#1 | Client#2
+        var sx = 10;                         // server x
+        var sc = sx + bw/2;                  // server center
+        var c1x = sx + bw + gap;             // client 1 x
+        var c1c = c1x + bw/2;               // client 1 center
+        var c2x = c1x + bw + gap;            // client 2 x
+        var c2c = c2x + bw/2;               // client 2 center
+        var totalW = c2x + bw + 10;
+
+        // Server (hub)
+        s += '<text x="'+sc+'" y="14" text-anchor="middle" font-size="13" font-weight="700" fill="'+tc+'">' + _igEsc(t('igServerHub')) + '</text>';
+        s += _igBlock(sx, SY[0], bw, bh, 'external', 'igLan', 'igTipLan');
+        s += _igArrow(sc, SY[0]+bh, sc, SY[1], false, true);
+        s += _igBlock(sx, SY[1], bw, bh, 'routing', 'igRouting', 'igTipRoutingSrv');
+        s += _igArrow(sc, SY[1]+bh, sc, SY[2], false, true);
+        s += _igBlock(sx, SY[2], bw, bh, 'firewall', 'igFirewall', 'igTipFirewallSrv');
+        s += _igArrow(sc, SY[2]+bh, sc, SY[3], false, true);
+        s += _igBlock(sx, SY[3], bw, bh, 'wireguard', 'igWireguard', 'igTipWireguard');
+        s += _igArrow(sc, SY[3]+bh, sc, SY[4], false, true);
+        s += _igBlock(sx, SY[4], bw, bh, 'container', 'igContainerServer', 'igTipContainer');
+
+        // Client #1
+        s += '<text x="'+c1c+'" y="14" text-anchor="middle" font-size="13" font-weight="700" fill="'+tc+'">' + _igEsc(t('igClientN')) + '</text>';
+        s += _igBlock(c1x, SY[0], bw, bh, 'external', 'igLan', 'igTipLan');
+        s += _igArrow(c1c, SY[0]+bh, c1c, SY[1], false, true);
+        s += _igBlock(c1x, SY[1], bw, bh, 'routing', 'igRouting', 'igTipRoutingS2S');
+        s += _igArrow(c1c, SY[1]+bh, c1c, SY[2], false, true);
+        s += _igBlock(c1x, SY[2], bw, bh, 'firewall', 'igFirewall', 'igTipFirewall');
+        s += _igArrow(c1c, SY[2]+bh, c1c, SY[3], false, true);
+        s += _igBlock(c1x, SY[3], bw, bh, 'wireguard', 'igWireguard', 'igTipWireguard');
+        s += _igArrow(c1c, SY[3]+bh, c1c, SY[4], false, true);
+        s += _igBlock(c1x, SY[4], bw, bh, 'container', 'igContainerClient', 'igTipContainer');
+
+        // Client #2
+        s += '<text x="'+c2c+'" y="14" text-anchor="middle" font-size="12" font-weight="700" fill="'+tc+'">' + _igEsc(t('igClient2')) + '</text>';
+        s += _igBlock(c2x, SY[0], bw, bh, 'external', 'igLan', 'igTipLan');
+        s += _igArrow(c2c, SY[0]+bh, c2c, SY[1], false, true);
+        s += _igBlock(c2x, SY[1], bw, bh, 'routing', 'igRouting', 'igTipRoutingS2S');
+        s += _igArrow(c2c, SY[1]+bh, c2c, SY[2], false, true);
+        s += _igBlock(c2x, SY[2], bw, bh, 'firewall', 'igFirewall', 'igTipFirewall');
+        s += _igArrow(c2c, SY[2]+bh, c2c, SY[3], false, true);
+        s += _igBlock(c2x, SY[3], bw, bh, 'wireguard', 'igWireguard', 'igTipWireguard');
+        s += _igArrow(c2c, SY[3]+bh, c2c, SY[4], false, true);
+        s += _igBlock(c2x, SY[4], bw, bh, 'container', 'igContainerClient', 'igTipContainer');
+
+        // Internet below all containers — 3 arrows down to it
+        var iw = 200;
+        var ix = (totalW - iw) / 2;          // centered
+        s += _igBlock(ix, SY[5], iw, bh, 'external', 'igInternet', 'igTipInternet');
+        s += _igArrow(sc, SY[4]+bh, ix+iw/2-60, SY[5]);                    // Server -> Internet
+        s += _igArrow(c1c, SY[4]+bh, ix+iw/2, SY[5]);                     // Client 1 -> Internet
+        s += _igArrow(c2c, SY[4]+bh, ix+iw/2+60, SY[5]);                  // Client 2 -> Internet
+
+        return _igSvg(totalW, SY[5]+bh+10, s);
+    }
+
+    // Tooltip: click to pin, click outside to dismiss
+    var _igPinned = null; // currently pinned block element
+    function _igShowTip(tip, block) {
+        var text = block.getAttribute('data-tip');
+        if (!text) return;
+        tip.innerHTML = _igFormatTip(text);
+        tip.classList.add('visible');
+        var r = block.getBoundingClientRect();
+        var left = r.left + r.width/2 - tip.offsetWidth/2;
+        left = Math.max(8, Math.min(left, window.innerWidth - tip.offsetWidth - 8));
+        tip.style.left = left + 'px';
+        var tipH = tip.offsetHeight;
+        var topAbove = r.top - tipH - 10;
+        if (topAbove >= 8) {
+            tip.style.top = topAbove + 'px';
+            tip.classList.add('above');
+            tip.classList.remove('below');
+        } else {
+            tip.style.top = (r.bottom + 10) + 'px';
+            tip.classList.remove('above');
+            tip.classList.add('below');
+        }
+        var arrowLeft = r.left + r.width/2 - left;
+        arrowLeft = Math.max(16, Math.min(arrowLeft, tip.offsetWidth - 16));
+        tip.style.setProperty('--arrow-left', arrowLeft + 'px');
+    }
+    function _igHideTip() {
+        var tip = document.getElementById('ig-tip');
+        if (tip) tip.classList.remove('visible');
+        _igPinned = null;
+    }
+    function _igBindTooltips(container) {
+        var tip = document.getElementById('ig-tip');
+        if (!tip) {
+            tip = document.createElement('div');
+            tip.id = 'ig-tip';
+            document.body.appendChild(tip);
+        }
+        var blocks = container.querySelectorAll('.ig-block');
+        for (var i = 0; i < blocks.length; i++) {
+            // Hover: show only if nothing is pinned
+            blocks[i].addEventListener('mouseenter', function() {
+                if (_igPinned) return;
+                _igShowTip(tip, this);
+            });
+            blocks[i].addEventListener('mouseleave', function() {
+                if (_igPinned) return;
+                tip.classList.remove('visible');
+            });
+            // Click: pin tooltip
+            blocks[i].addEventListener('click', function(e) {
+                e.stopPropagation();
+                if (_igPinned === this) { _igHideTip(); return; }
+                _igPinned = this;
+                _igShowTip(tip, this);
+            });
+        }
+        // Click outside dismisses pinned tooltip
+        document.addEventListener('click', function(e) {
+            if (!_igPinned) return;
+            if (tip.contains(e.target)) return;
+            _igHideTip();
+        });
+    }
+
+    function _igRefresh() {
+        var wrap = document.getElementById('infographic-container');
+        if (!wrap || !RY || !RY.length) return;
+        var svg = buildInfographic();
+        wrap.innerHTML = '<div class="infographic-wrap"><div class="ig-title" data-i18n="igTitle">' + t('igTitle') + '</div>' + svg + '</div>';
+        _igBindTooltips(wrap);
+    }
+
+    function buildInfographic() {
+        var mode = getDeployMode();
+        var scenario = getScenario();
+        if (mode === 'site-to-site') return _igSiteToSite();
+        if (mode === 'server') return _igServerHub();
+        if (mode === 'standalone') {
+            if (scenario === 'dns-fwd') return _igStandaloneDns();
+            if (scenario === 'non-ru') return _igStandaloneNonRu();
+            return _igStandaloneTunnel();
+        }
+        if (scenario === 'dns-fwd') return _igContainerDns();
+        if (scenario === 'non-ru') return _igContainerNonRu();
+        return _igContainerTunnel();
+    }
+
+    // Initial infographic render (after all constants are defined)
+    _igRefresh();
+</script>
+</body>
+</html>
index 9402c4903c01370e833b0f2f5603fc15fddff98c..72cad297e69a4e537293257d301de368a594f39c 100644 (file)
 <body>
 <div class="container">
 
+    <div class="alert alert-warning" style="margin-bottom:20px">
+        <strong data-i18n="deprecatedTitle">This page is outdated.</strong>
+        <span data-i18n="deprecatedBody">It knows nothing about AmneziaWG 3.0 and is kept only so links shared earlier keep working. For a new install &mdash; or to bring an existing one up to date &mdash; reinstall using the new page:</span>
+        <a id="deprecated-link" href="conf3.0.html" style="font-weight:600">conf3.0.html</a>
+    </div>
+    <div id="link-version-warn" class="alert alert-error" style="display:none;margin-bottom:20px">
+        <span data-i18n="linkFromNewer">This link was created by the new configurator and cannot be opened here. Open it in</span>
+        <a id="link-version-href" href="conf3.0.html" style="font-weight:600">conf3.0.html</a>
+    </div>
+
     <header>
         <div class="header-row">
             <div>
 
     var I18N = {
         en: {
+            deprecatedTitle: 'This page is outdated.',
+            deprecatedBody: 'It knows nothing about AmneziaWG 3.0 and is kept only so links shared earlier keep working. For a new install \u2014 or to bring an existing one up to date \u2014 reinstall using the new page:',
+            linkFromNewer: 'This link was created by the new configurator and cannot be opened here. Open it in',
             title: 'AWG Proxy \u2014 MikroTik Offline Command Generator',
             subtitle: 'Paste your AmneziaWG .conf file contents to generate ready-to-use MikroTik RouterOS commands',
             privacy: 'All configuration and data is processed locally in your browser. Nothing is transmitted anywhere. Your private keys are safe.',
             igTipAwgServer: 'AmneziaWG Server\n\nThe remote VPN server running AmneziaWG. Receives transformed packets from awg-proxy and routes them to the internet.\n\nTraffic flows: Client \u2192 MikroTik WG \u2192 awg-proxy \u2192 AWG Server \u2192 Internet'
         },
         ru: {
+            deprecatedTitle: 'Эта страница устарела.',
+            deprecatedBody: 'Она ничего не знает про AmneziaWG 3.0 и живёт только ради ранее розданных ссылок. Для новой установки — и чтобы обновить существующую — переустановите по новой странице:',
+            linkFromNewer: 'Эта ссылка создана новым конфигуратором и здесь не открывается. Откройте её в',
             title: 'AWG Proxy \u2014 \u041e\u0444\u043b\u0430\u0439\u043d-\u0433\u0435\u043d\u0435\u0440\u0430\u0442\u043e\u0440 \u043a\u043e\u043c\u0430\u043d\u0434 MikroTik',
             subtitle: '\u0412\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435 .conf \u0444\u0430\u0439\u043b\u0430 AmneziaWG \u0434\u043b\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u043a\u043e\u043c\u0430\u043d\u0434 MikroTik RouterOS',
             privacy: '\u0412\u0441\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e \u0432 \u0432\u0430\u0448\u0435\u043c \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435. \u041d\u0438\u0447\u0435\u0433\u043e \u043d\u0438\u043a\u0443\u0434\u0430 \u043d\u0435 \u043f\u0435\u0440\u0435\u0434\u0430\u0451\u0442\u0441\u044f. \u0412\u0430\u0448\u0438 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0435 \u043a\u043b\u044e\u0447\u0438 \u0432 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438.',
             '/ip/firewall/nat/add chain=srcnat action=masquerade src-address=' + net.natSrc,
             '',
             '# 3. WireGuard interface (MikroTik derives the public key automatically)',
-            '/interface/wireguard/add name=wg-' + prefix + ' private-key="' + i.PrivateKey + '" listen-port=' + net.wgPort + ' disabled=yes',
+            '/interface/wireguard/add name=wg-' + prefix + ' private-key="' + i.PrivateKey + '" listen-port=' + randomWgPort() + ' disabled=yes',
             peerAdd,
             '/ip/address/add address=' + i.Address + ' interface=wg-' + prefix,
             '',
             '  :if ($arch = "arm") do={',
             '    :set file ("awg-proxy-arm" . $suffix . ".tar.gz")',
             '    :local rbModel ""',
-            '    :do { :set rbModel [/system/routerboard/get model] } on-error={}',
+            // /system/routerboard does not exist on CHR: RouterOS fails while parsing
+            // the script, before on-error can catch anything, and the whole block dies.
+            // :parse defers the lookup to runtime, so :do handles the missing menu.
+            '    :do { :set rbModel [[:parse ":return [/system/routerboard/get model]"]] } on-error={}',
             '    :local bName [/system/resource/get board-name]',
-            '    :if ($rbModel~"E50UG" || $rbModel~"E60iUGS" || $bName~"^hEX") do={',
+            '    :if ($rbModel~"E50UG" || $rbModel~"E60iUGS" || $bName~"E50UG" || $bName~"E60iUGS" || $bName~"^hEX") do={',
             '      :set file ("awg-proxy-armv5" . $suffix . ".tar.gz")',
             '    }',
             '  }',
         lines = lines.concat([
             '',
             '# 2. WireGuard interface',
-            '/interface/wireguard/add name=wg-' + prefix + ' private-key="' + i.PrivateKey + '" listen-port=' + net.wgPort,
+            '/interface/wireguard/add name=wg-' + prefix + ' private-key="' + i.PrivateKey + '" listen-port=' + randomWgPort(),
             peerAdd,
             '/ip/address/add address=' + i.Address + ' interface=wg-' + prefix,
             '',
         return sel;
     }
 
-    // Deterministically derives a unique /30 veth subnet and WireGuard listen port
-    // from the tunnel name (prefix), so multiple tunnels on the same MikroTik
-    // don't collide. Uses a simple djb2-style hash spread across different bit
-    // ranges: bits 0-3 → 2nd octet (172.16-31.x.x), bits 4-11 → 3rd octet,
-    // bits 12-17 → 4th octet base (0-252, aligned to /30), bits 18+ → WG port.
+    // Deterministically derives a unique /30 veth subnet from the tunnel name
+    // (prefix), so multiple tunnels on the same MikroTik don't collide. It has to
+    // stay deterministic: the uninstall script recomputes the same addresses from
+    // the prefix to clean them up. Uses a simple djb2-style hash spread across
+    // different bit ranges: bits 0-3 → 2nd octet (172.16-31.x.x), bits 4-11 →
+    // 3rd octet, bits 12-17 → 4th octet base (0-252, aligned to /30).
     function getTunnelNetwork(prefix) {
         var h = 0;
         for (var i = 0; i < prefix.length; i++)
         var oct2 = 16 + (h & 0xF);
         var oct3 = (h >>> 4) & 0xFF;
         var oct4base = ((h >>> 12) & 0x3F) * 4;
-        var wgPort = 12000 + ((h >>> 18) % 20000);
         return {
             vethAddr: '172.' + oct2 + '.' + oct3 + '.' + (oct4base + 2) + '/30',
             hostAddr: '172.' + oct2 + '.' + oct3 + '.' + (oct4base + 1) + '/30',
             vethGw:   '172.' + oct2 + '.' + oct3 + '.' + (oct4base + 1),
-            natSrc:   '172.' + oct2 + '.' + oct3 + '.' + oct4base + '/30',
-            wgPort:   wgPort
+            natSrc:   '172.' + oct2 + '.' + oct3 + '.' + oct4base + '/30'
         };
     }
 
+    // The WG listen-port used to come out of the same hash (bits 18+), but those
+    // bits don't change when only the trailing digit of the prefix does: every
+    // awg-proxy-1..5 got 25343 and every awg-server-1..2 got 25081, so a second
+    // tunnel on the same router could not bind its port. The port is local to the
+    // router and nothing recomputes it from the prefix, so draw it at random.
+    function randomWgPort() {
+        return randomInt(10000, 60000);
+    }
+
+    // Tunnel subnets used to ship as fixed defaults (10.10.10.0/24 for the hub,
+    // 10.0.0.0/30 for site-to-site), so a second tunnel on the same router landed
+    // on addresses the first one already held. Second octet starts at 20 to stay
+    // clear of the usual 10.0.x/10.1.x/10.10.x home LANs. The share link carries
+    // the subnet, so add-client runs still follow the server that issued it.
+    function randomTunnelSubnet(bits) {
+        var base = '10.' + randomInt(20, 250) + '.' + randomInt(0, 255) + '.';
+        return bits === 30 ? base + (randomInt(0, 62) * 4) + '/30' : base + '0/24';
+    }
+
     // ============================================================
     // Curve25519 key generation (Montgomery ladder, BigInt)
     // ============================================================
         if (!s2sParams) {
             s2sParams = generateS2SParams();
             document.getElementById('s2s-awg-port').value = s2sParams.port;
+            // Only on first init, never in regenerate*: a subnet typed by hand must
+            // survive a protocol-level change.
+            document.getElementById('s2s-tunnel-subnet').value = randomTunnelSubnet(30);
             showS2SParams();
         }
         return s2sParams;
         if (!serverParams) {
             serverParams = generateAWGParams();
             document.getElementById('srv-awg-port').value = serverParams.port;
+            // Add-client runs overwrite this right after onModeChange() with the
+            // subnet from the share link (see the parseURLParams block on load).
+            document.getElementById('srv-tunnel-subnet').value = randomTunnelSubnet(24);
             showSrvParams();
         }
         return serverParams;
             // Map to v3 field order: [0]=wgPubKey, [1]=endpoint, [2]=prefix, [3]=netMode, [4]=serverLAN
             fields = [key2, endpoint2, prefix2, String.fromCharCode(netModeN), srvLan2];
         } else {
+            // Version 4+ comes from conf3.0.html, which owns its own format.
+            // Point the user there instead of silently ignoring the link.
+            var warn = document.getElementById('link-version-warn');
+            if (warn) {
+                var href = document.getElementById('link-version-href');
+                if (href) href.href = 'conf3.0.html' + location.search;
+                warn.style.display = 'block';
+            }
             return null; // unsupported version
         }
 
         var tun = parseSlash24(tunnelCidr);
         var vethIP = net.vethAddr.replace(/\/.*/, '');
         var awgPort = awgParams.port;
-        var wgPort = net.wgPort;
+        // Random per-generation port (see randomWgPort). It lives in awgParams and
+        // goes into the share link, so clients added from that link keep pointing
+        // at the same server port.
+        var wgPort = awgParams.wgListenPort || randomWgPort();
         var tunServerIP = tun.ip(1);
         var tunClientIP = tun.ip(clientNum + 1);
 
         ls.push('/container/envs/add list=' + prefix + '-env key=AWG_H2 value="' + awgParams.h2 + '"');
         ls.push('/container/envs/add list=' + prefix + '-env key=AWG_H3 value="' + awgParams.h3 + '"');
         ls.push('/container/envs/add list=' + prefix + '-env key=AWG_H4 value="' + awgParams.h4 + '"');
+        // The hub also initiates handshakes (it sends junk + CPS on the reverse
+        // path), so I1-I5 belong on the server side too, not just in the client
+        // .conf -- otherwise the server side comes up without CPS at all (#38).
+        ls = ls.concat(cpsEnvLines(prefix, awgParams.cps));
         ls.push('/container/envs/add list=' + prefix + '-env key=AWG_SERVER_PUB value="' + serverKey.publicKey + '"');
         ls.push('# AWG_CLIENT_PUB stays as legacy fallback for proxy-only clients.');
         ls.push('# AWG_CLIENT_PUBS lists real direct AmneziaWG clients for multi-peer server mode.');
         lc.push('# 2. Network infrastructure');
         var cnet = getTunnelNetwork(prefix);
         var cvethIP = cnet.vethAddr.replace(/\/.*/, '');
+        // Client-side WG port is purely local (the peer talks to the container over
+        // veth), so nothing has to agree on it.
+        var clientWgPort = randomWgPort();
         lc.push('/interface/veth/add name=veth-' + prefix + ' address=' + cnet.vethAddr + ' gateway=' + cnet.vethGw);
         lc.push('/ip/address/add address=' + cnet.hostAddr + ' interface=veth-' + prefix);
         lc.push('/ip/firewall/nat/add chain=srcnat action=masquerade src-address=' + cnet.natSrc);
         lc.push('');
 
         lc.push('# 3. WireGuard client');
-        lc.push('/interface/wireguard/add name=wg-' + prefix + ' private-key="' + clientKey.privateKey + '" listen-port=' + cnet.wgPort + ' disabled=yes');
+        lc.push('/interface/wireguard/add name=wg-' + prefix + ' private-key="' + clientKey.privateKey + '" listen-port=' + clientWgPort + ' disabled=yes');
         lc.push('/interface/wireguard/peers/add interface=wg-' + prefix + ' public-key="' + serverKey.publicKey + '" endpoint-address=' + cvethIP + ' endpoint-port=51820 allowed-address=' + clientAllowed + ' persistent-keepalive=' + randomInt(25, 55));
         lc.push('/ip/address/add address=' + tunClientIP + '/24 interface=wg-' + prefix);
         lc.push('/ip/firewall/filter/add chain=input action=accept in-interface=wg-' + prefix + ' place-before=0 comment=' + prefix + '-wg-mgmt');
 
         var cnet = getTunnelNetwork(prefix);
         var cvethIP = cnet.vethAddr.replace(/\/.*/, '');
+        // Local-only port, same reasoning as in buildServerCommands.
+        var clientWgPort = randomWgPort();
         lc.push('# 2. Network infrastructure');
         lc.push('/interface/veth/add name=veth-' + prefix + ' address=' + cnet.vethAddr + ' gateway=' + cnet.vethGw);
         lc.push('/ip/address/add address=' + cnet.hostAddr + ' interface=veth-' + prefix);
         lc.push('');
 
         lc.push('# 3. WireGuard client');
-        lc.push('/interface/wireguard/add name=wg-' + prefix + ' private-key="' + clientKey.privateKey + '" listen-port=' + cnet.wgPort + ' disabled=yes');
+        lc.push('/interface/wireguard/add name=wg-' + prefix + ' private-key="' + clientKey.privateKey + '" listen-port=' + clientWgPort + ' disabled=yes');
         lc.push('/interface/wireguard/peers/add interface=wg-' + prefix + ' public-key="' + serverWgPubKey + '" endpoint-address=' + cvethIP + ' endpoint-port=51820 allowed-address=' + clientAllowed + ' persistent-keepalive=' + randomInt(25, 55));
         lc.push('/ip/address/add address=' + tunClientIP + '/24 interface=wg-' + prefix);
         lc.push('/ip/firewall/filter/add chain=input action=accept in-interface=wg-' + prefix + ' place-before=0 comment=' + prefix + '-wg-mgmt');
         lines.push('  :if ($arch = "arm") do={');
         lines.push('    :set file ("awg-proxy-arm" . $suffix . ".tar.gz")');
         lines.push('    :local rbModel ""');
-        lines.push('    :do { :set rbModel [/system/routerboard/get model] } on-error={}');
+        // See the note above: :parse keeps the CHR/x86 parser from choking on
+        // /system/routerboard, which only exists on RouterBOARD hardware.
+        lines.push('    :do { :set rbModel [[:parse ":return [/system/routerboard/get model]"]] } on-error={}');
         lines.push('    :local bName [/system/resource/get board-name]');
-        lines.push('    :if ($rbModel~"E50UG" || $rbModel~"E60iUGS" || $bName~"^hEX") do={');
+        lines.push('    :if ($rbModel~"E50UG" || $rbModel~"E60iUGS" || $bName~"E50UG" || $bName~"E60iUGS" || $bName~"^hEX") do={');
         lines.push('      :set file ("awg-proxy-armv5" . $suffix . ".tar.gz")');
         lines.push('    }');
         lines.push('  }');
         document.getElementById('prefix-custom').value = '';
         document.getElementById('proxy-ip').value = '';
         document.getElementById('s2s-endpoint-b').value = '';
-        document.getElementById('s2s-tunnel-subnet').value = '10.0.0.0/30';
+        document.getElementById('s2s-tunnel-subnet').value = randomTunnelSubnet(30);
         document.getElementById('s2s-storage-a').value = '__auto';
         document.getElementById('s2s-storage-b').value = '__auto';
         document.getElementById('s2s-dns').value = '';
diff --git a/html/amneziawg/configurator/configurator3.0.html b/html/amneziawg/configurator/configurator3.0.html
new file mode 100644 (file)
index 0000000..c4848ea
--- /dev/null
@@ -0,0 +1,5836 @@
+<!DOCTYPE html>
+<html lang="ru">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>AWG Proxy 3.0 — Offline MikroTik Command Generator</title>
+    <style>
+        *, *::before, *::after {
+            box-sizing: border-box;
+            margin: 0;
+            padding: 0;
+        }
+
+        :root {
+            --bg: #f5f6f8;
+            --surface: #ffffff;
+            --border: #d1d5db;
+            --border-focus: #6366f1;
+            --text: #111827;
+            --text-muted: #6b7280;
+            --accent: #4f46e5;
+            --accent-hover: #4338ca;
+            --success: #059669;
+            --success-bg: #ecfdf5;
+            --error: #dc2626;
+            --error-bg: #fef2f2;
+            --tag-ok: #d1fae5;
+            --tag-ok-text: #065f46;
+            --tag-miss: #fee2e2;
+            --tag-miss-text: #991b1b;
+            --mono: 'Cascadia Code', 'Fira Code', 'Consolas', 'Courier New', monospace;
+        }
+
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+            background: var(--bg);
+            color: var(--text);
+            line-height: 1.6;
+            min-height: 100vh;
+            padding: 24px 16px 48px;
+        }
+
+        .container {
+            max-width: 900px;
+            margin: 0 auto;
+        }
+
+        header {
+            margin-bottom: 32px;
+        }
+
+        header h1 {
+            font-size: 1.75rem;
+            font-weight: 700;
+            margin-bottom: 6px;
+        }
+
+        header p {
+            color: var(--text-muted);
+            font-size: 0.95rem;
+        }
+
+        .header-row {
+            display: flex;
+            align-items: flex-start;
+            justify-content: space-between;
+            gap: 12px;
+        }
+
+        #lang-toggle {
+            background: var(--surface);
+            border: 1px solid var(--border);
+            border-radius: 6px;
+            padding: 4px 12px;
+            font-size: 0.85rem;
+            font-weight: 600;
+            cursor: pointer;
+            color: var(--text);
+            flex-shrink: 0;
+            transition: background 0.15s;
+        }
+
+        #lang-toggle:hover {
+            background: #e5e7eb;
+        }
+
+        .card {
+            background: var(--surface);
+            border: 1px solid var(--border);
+            border-radius: 12px;
+            padding: 24px;
+            margin-bottom: 20px;
+        }
+
+        .card-title {
+            font-size: 1rem;
+            font-weight: 600;
+            margin-bottom: 14px;
+        }
+
+        label {
+            display: block;
+            font-size: 0.875rem;
+            font-weight: 500;
+            margin-bottom: 6px;
+        }
+
+        textarea {
+            width: 100%;
+            font-family: var(--mono);
+            font-size: 0.82rem;
+            line-height: 1.5;
+            padding: 12px;
+            border: 1px solid var(--border);
+            border-radius: 8px;
+            resize: vertical;
+            background: #fafafa;
+            color: var(--text);
+            transition: border-color 0.15s;
+            min-height: 280px;
+        }
+
+        textarea:focus {
+            outline: none;
+            border-color: var(--border-focus);
+            background: #fff;
+            box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.12);
+        }
+
+        textarea::placeholder {
+            color: #9ca3af;
+        }
+
+        select, input[type="text"] {
+            font-family: var(--mono);
+            font-size: 0.85rem;
+            padding: 8px 12px;
+            border: 1px solid var(--border);
+            border-radius: 8px;
+            background: #fafafa;
+            color: var(--text);
+            transition: border-color 0.15s;
+        }
+
+        select:focus, input[type="text"]:focus {
+            outline: none;
+            border-color: var(--border-focus);
+            background: #fff;
+            box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.12);
+        }
+
+        #storage-custom, #prefix-custom {
+            margin-left: 8px;
+            width: 160px;
+        }
+
+        #proxy-ip {
+            width: 200px;
+        }
+
+        .radio-group {
+            display: flex;
+            gap: 20px;
+            flex-wrap: wrap;
+        }
+
+        .radio-group label {
+            display: inline-flex;
+            align-items: center;
+            gap: 6px;
+            font-weight: 400;
+            cursor: pointer;
+            font-size: 0.875rem;
+        }
+
+        .btn {
+            display: inline-flex;
+            align-items: center;
+            gap: 6px;
+            padding: 10px 20px;
+            border: none;
+            border-radius: 8px;
+            font-size: 0.9rem;
+            font-weight: 600;
+            cursor: pointer;
+            transition: background 0.15s, transform 0.1s;
+            user-select: none;
+        }
+
+        .btn:active {
+            transform: scale(0.97);
+        }
+
+        .btn-primary {
+            background: var(--accent);
+            color: #fff;
+        }
+
+        .btn-primary:hover {
+            background: var(--accent-hover);
+        }
+
+        .btn-secondary {
+            background: #f3f4f6;
+            color: var(--text);
+            border: 1px solid var(--border);
+        }
+
+        .btn-secondary:hover {
+            background: #e5e7eb;
+        }
+
+        .btn-success {
+            background: var(--success);
+            color: #fff;
+        }
+
+        .actions {
+            display: flex;
+            gap: 10px;
+            margin-top: 14px;
+            flex-wrap: wrap;
+            align-items: center;
+        }
+
+        .fields-grid {
+            display: grid;
+            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+            gap: 8px;
+            margin-top: 12px;
+        }
+
+        .field-tag {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            padding: 6px 10px;
+            border-radius: 6px;
+            font-size: 0.8rem;
+            font-family: var(--mono);
+            gap: 8px;
+        }
+
+        .field-tag.ok {
+            background: var(--tag-ok);
+            color: var(--tag-ok-text);
+        }
+
+        .field-tag.miss {
+            background: var(--tag-miss);
+            color: var(--tag-miss-text);
+        }
+
+        .field-tag .fname {
+            font-weight: 600;
+        }
+
+        .field-tag .fval {
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            max-width: 120px;
+            opacity: 0.8;
+        }
+
+        .output-header {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            margin-bottom: 10px;
+            flex-wrap: wrap;
+            gap: 8px;
+        }
+
+        pre#output, pre#uninstall-output, pre#routing-output, pre.s2s-output {
+            font-family: var(--mono);
+            font-size: 0.8rem;
+            line-height: 1.6;
+            background: #0f172a;
+            color: #e2e8f0;
+            padding: 20px;
+            border-radius: 8px;
+            overflow-x: auto;
+            white-space: pre;
+            tab-size: 2;
+        }
+
+        pre#output .comment, pre#uninstall-output .comment, pre#routing-output .comment, pre.s2s-output .comment {
+            color: #64748b;
+        }
+
+        pre#output .cmd, pre#uninstall-output .cmd, pre#routing-output .cmd, pre.s2s-output .cmd {
+            color: #7dd3fc;
+        }
+
+        .alert {
+            border-radius: 8px;
+            padding: 12px 16px;
+            font-size: 0.875rem;
+            margin-top: 12px;
+        }
+
+        .alert-error {
+            background: var(--error-bg);
+            border: 1px solid #fca5a5;
+            color: var(--error);
+        }
+
+        #errors-container {
+            margin-bottom: 4px;
+        }
+
+        #parsed-section, #output-section, #routing-section, #uninstall-section, #s2s-output-section {
+            display: none;
+        }
+
+        .s2s-fields {
+            display: grid;
+            grid-template-columns: 1fr 1fr;
+            gap: 12px;
+            margin-bottom: 12px;
+        }
+
+        .s2s-fields label {
+            font-size: 0.82rem;
+        }
+
+        .s2s-fields input[type="text"], .s2s-fields select {
+            width: 100%;
+        }
+
+        .s2s-side {
+            margin-bottom: 20px;
+        }
+
+        .s2s-side-header {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            margin-bottom: 10px;
+        }
+
+        @media (max-width: 600px) {
+            .s2s-fields {
+                grid-template-columns: 1fr;
+            }
+        }
+
+        footer {
+            text-align: center;
+            color: var(--text-muted);
+            font-size: 0.8rem;
+            margin-top: 40px;
+        }
+
+        .checkbox-grid {
+            display: grid;
+            grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+            gap: 6px;
+            margin-top: 8px;
+        }
+
+        .checkbox-grid label {
+            display: inline-flex;
+            align-items: center;
+            gap: 6px;
+            font-weight: 400;
+            cursor: pointer;
+            font-size: 0.82rem;
+            padding: 4px 6px;
+            border-radius: 4px;
+            transition: background 0.1s;
+        }
+
+        .checkbox-grid label:hover {
+            background: #f3f4f6;
+        }
+
+        .btn-sm {
+            padding: 4px 10px;
+            font-size: 0.75rem;
+            border-radius: 4px;
+        }
+
+        .domain-tag {
+            display: inline-flex;
+            align-items: center;
+            gap: 4px;
+            padding: 2px 8px;
+            border-radius: 4px;
+            background: var(--tag-ok);
+            color: var(--tag-ok-text);
+            font-size: 0.82rem;
+            font-family: var(--mono);
+        }
+
+        .domain-tag button {
+            background: none;
+            border: none;
+            color: inherit;
+            cursor: pointer;
+            font-size: 1rem;
+            line-height: 1;
+            padding: 0 2px;
+            opacity: 0.7;
+        }
+
+        .domain-tag button:hover {
+            opacity: 1;
+        }
+
+        .scenario-panel {
+            margin-top: 10px;
+            padding: 12px;
+            border: 1px solid var(--border);
+            border-radius: 8px;
+            background: #fafafa;
+        }
+
+        .scenario-panel-header {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            margin-bottom: 6px;
+            flex-wrap: wrap;
+            gap: 6px;
+        }
+
+        .alert-warning {
+            background: #fffbeb;
+            border: 1px solid #fcd34d;
+            color: #92400e;
+        }
+
+        .hint {
+            font-size: 0.8rem;
+            color: var(--text-muted);
+            margin-top: 4px;
+        }
+
+        .dns-info-icon {
+            display: inline-block;
+            cursor: help;
+            font-size: 1rem;
+            color: var(--accent);
+            position: relative;
+        }
+
+        .dns-info-icon:hover::after {
+            content: attr(title);
+            position: absolute;
+            bottom: 120%;
+            left: 50%;
+            transform: translateX(-50%);
+            background: #1e293b;
+            color: #e2e8f0;
+            padding: 8px 12px;
+            border-radius: 6px;
+            font-size: 0.75rem;
+            white-space: pre-line;
+            width: 320px;
+            z-index: 10;
+            pointer-events: none;
+            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
+            line-height: 1.5;
+        }
+
+        pre.collapsed { max-height: calc(15 * 1.6em); overflow: hidden; position: relative; }
+        pre.collapsed::after { content:''; position:absolute; bottom:0; left:0; right:0; height:3em; background:linear-gradient(transparent, #0f172a); pointer-events:none; }
+        .btn-expand { display:block; width:100%; margin-top:4px; padding:6px; background:#1e293b; color:#94a3b8; border:1px solid #334155; border-radius:0 0 8px 8px; font-size:0.8rem; cursor:pointer; text-align:center; }
+        .btn-expand:hover { background:#334155; color:#e2e8f0; }
+
+        @media (max-width: 600px) {
+            header h1 {
+                font-size: 1.4rem;
+            }
+
+            .card {
+                padding: 16px;
+            }
+
+            pre#output {
+                font-size: 0.72rem;
+                padding: 14px;
+            }
+
+            .dns-info-icon:hover::after {
+                width: 240px;
+                left: 0;
+                transform: none;
+            }
+        }
+
+        input[type="radio"], input[type="checkbox"] {
+            accent-color: var(--accent);
+        }
+
+        @media (prefers-color-scheme: dark) {
+            :root {
+                --bg: #111111;
+                --surface: #1e1e1e;
+                --border: #333333;
+                --border-focus: #f97316;
+                --text: #e8e8e8;
+                --text-muted: #888888;
+                --accent: #f97316;
+                --accent-hover: #ea580c;
+                --success: #22c55e;
+                --success-bg: #14532d;
+                --error: #f87171;
+                --error-bg: #2d1515;
+                --tag-ok: #14532d;
+                --tag-ok-text: #86efac;
+                --tag-miss: #450a0a;
+                --tag-miss-text: #fca5a5;
+            }
+
+            #lang-toggle:hover { background: #2a2a2a; }
+
+            textarea {
+                background: #242424;
+            }
+            textarea:focus {
+                background: #1a1a1a;
+                box-shadow: 0 0 0 3px rgba(249, 115, 22, 0.15);
+            }
+            textarea::placeholder { color: #555; }
+
+            select, input[type="text"] { background: #242424; }
+            select:focus, input[type="text"]:focus {
+                background: #1a1a1a;
+                box-shadow: 0 0 0 3px rgba(249, 115, 22, 0.15);
+            }
+
+            .btn-secondary { background: #2a2a2a; }
+            .btn-secondary:hover { background: #333; }
+
+            .scenario-panel { background: #242424; }
+            .checkbox-grid label:hover { background: #2a2a2a; }
+
+            .alert-warning { background: #2a1a00; border-color: #92400e; color: #fcd34d; }
+
+            pre.collapsed::after { background: linear-gradient(transparent, #0f172a); }
+            .infographic-wrap { background: #1e1e1e; border-color: #444; }
+            .infographic-wrap { background: #1e1e1e; border-color: #444; }
+            #ig-tip { background: #1a1a1a; border-color: #555; color: #e5e7eb; }
+            #ig-tip::after { border-bottom-color: #1a1a1a; }
+            #ig-tip::before { border-bottom-color: #555; }
+            #ig-tip.above::after { border-bottom-color: transparent; border-top-color: #1a1a1a; }
+            #ig-tip.above::before { border-bottom-color: transparent; border-top-color: #555; }
+            #ig-tip code { background: #2a2a2a; border-color: #444; }
+        }
+
+        .infographic-wrap {
+            margin-top: 12px;
+            padding: 16px;
+            border: 1px solid var(--border);
+            border-radius: 10px;
+            background: var(--surface);
+            overflow-x: auto;
+        }
+        .infographic-wrap svg { display: block; margin: 0 auto; }
+        .infographic-wrap .ig-block { cursor: pointer; }
+        .infographic-wrap .ig-block:hover rect { filter: brightness(1.08); }
+        #ig-tip {
+            position: fixed;
+            max-width: 400px;
+            padding: 12px 16px;
+            border-radius: 8px;
+            border: 1px solid var(--border);
+            background: var(--surface);
+            color: var(--text);
+            font-size: 0.82rem;
+            line-height: 1.7;
+            box-shadow: 0 4px 20px rgba(0,0,0,0.18);
+            pointer-events: none;
+            z-index: 10000;
+            display: none;
+        }
+        #ig-tip.visible { display: block; }
+        #ig-tip::after {
+            content: '';
+            position: absolute;
+            top: -7px; left: var(--arrow-left, 50%);
+            transform: translateX(-50%);
+            border-left: 7px solid transparent;
+            border-right: 7px solid transparent;
+            border-bottom: 7px solid var(--surface);
+        }
+        #ig-tip::before {
+            content: '';
+            position: absolute;
+            top: -8px; left: var(--arrow-left, 50%);
+            transform: translateX(-50%);
+            border-left: 8px solid transparent;
+            border-right: 8px solid transparent;
+            border-bottom: 8px solid var(--border);
+        }
+        #ig-tip.above::after {
+            top: auto; bottom: -7px;
+            border-bottom: none;
+            border-top: 7px solid var(--surface);
+        }
+        #ig-tip.above::before {
+            top: auto; bottom: -8px;
+            border-bottom: none;
+            border-top: 8px solid var(--border);
+        }
+        .ig-title {
+            font-size: 1rem;
+            font-weight: 600;
+            margin-bottom: 10px;
+            text-align: center;
+            color: var(--text);
+        }
+        #ig-tip .ig-tip-title {
+            font-weight: 700;
+            font-size: 0.9rem;
+            margin-bottom: 6px;
+        }
+        #ig-tip code {
+            font-family: var(--mono);
+            font-size: 0.78rem;
+            background: var(--bg);
+            border: 1px solid var(--border);
+            border-radius: 4px;
+            padding: 1px 5px;
+        }
+    </style>
+</head>
+<body>
+<div class="container">
+
+    <div id="link-version-warn" class="alert alert-error" style="display:none;margin-bottom:20px">
+        <span data-i18n="linkFromOlder">This link was created by the previous configurator. Open it in</span>
+        <a id="link-version-href" href="configurator.html" style="font-weight:600">configurator.html</a>
+    </div>
+
+    <header>
+        <div class="header-row">
+            <div>
+                <h1 data-i18n="title">AWG Proxy 3.0 &mdash; MikroTik Offline Command Generator</h1>
+                <p data-i18n="subtitle">Paste an AmneziaWG .conf (1.0 through 3.0) to generate ready-to-use MikroTik RouterOS commands</p>
+            </div>
+            <button id="lang-toggle" onclick="setLang(currentLang==='ru'?'en':'ru')">EN</button>
+        </div>
+    </header>
+
+    <div class="card">
+        <div class="card-title" data-i18n="confTitle">AmneziaWG Configuration File</div>
+        <p style="font-size:0.82rem;color:var(--text-muted);margin-bottom:12px;line-height:1.5" data-i18n="privacy">All configuration and data is processed locally in your browser. Nothing is transmitted anywhere. Your private keys are safe.</p>
+        <div style="margin-bottom:14px">
+            <label data-i18n="modeLabel">Deployment mode</label>
+            <div class="radio-group">
+                <label><input type="radio" name="deploy-mode" value="container" checked onchange="onModeChange()"> <span data-i18n="modeContainer">MikroTik + Container (Docker)</span></label>
+                <label><input type="radio" name="deploy-mode" value="standalone" onchange="onModeChange()"> <span data-i18n="modeStandalone">MikroTik + Server (Standalone)</span></label>
+                <label><input type="radio" name="deploy-mode" value="site-to-site" onchange="onModeChange()"> <span data-i18n="modeSiteToSite">Site-to-site (two MikroTik)</span></label>
+                <label><input type="radio" name="deploy-mode" value="server" onchange="onModeChange()"> <span data-i18n="modeServer">Server (1:N hub)</span></label>
+            </div>
+            <div id="mode-hint" class="hint" data-i18n="modeContainerHint">WireGuard client on MikroTik + awg-proxy runs inside a Docker container directly on the router</div>
+        </div>
+        <label for="conf-input" data-i18n="confLabel">.conf file contents</label>
+        <textarea id="conf-input"
+                  placeholder="[Interface]&#10;PrivateKey = ...&#10;Address = 10.1.1.1/32&#10;DNS = 1.1.1.1&#10;Jc = 2&#10;Jmin = 1&#10;Jmax = 500&#10;S1 = 123&#10;S2 = 12&#10;H1 = 12345&#10;...&#10;&#10;[Peer]&#10;PublicKey = ...&#10;Endpoint = 1.2.3.4:12345&#10;AllowedIPs = 0.0.0.0/0"></textarea>
+        <div id="scenario-row" style="margin-top:14px">
+            <label data-i18n="scenarioLabel">Routing scenario</label>
+            <div class="radio-group">
+                <label><input type="radio" name="scenario" value="dns-fwd" checked onchange="onScenarioChange()"> <span data-i18n="scenarioDns">DNS-based routing for services</span></label>
+                <label><input type="radio" name="scenario" value="tunnel" onchange="onScenarioChange()"> <span data-i18n="scenarioTunnel">Tunnel only (manual routes)</span></label>
+                <label><input type="radio" name="scenario" value="non-ru" onchange="onScenarioChange()"> <span data-i18n="scenarioNonRu">All non-RU traffic through tunnel</span></label>
+            </div>
+            <div id="scenario-hint" class="hint" data-i18n="scenarioDnsHint">Routes traffic to selected services through the tunnel using DNS forwarding (DoH). <span class="dns-info-icon" title="" data-i18n-title="dnsBrowserHint">&#9432;</span></div>
+        </div>
+        <div id="fasttrack-warn" class="alert alert-warning" style="margin-top:8px" data-i18n="warnFasttrack">FastTrack rules will be modified (out-interface-list=WAN) to keep FastTrack active for non-VPN traffic. Uninstall script restores them.</div>
+        <div id="non-ru-warn" class="alert alert-error" style="display:none;margin-top:8px" data-i18n="scenarioNonRuWarn"></div>
+        <div id="doh-panel" style="margin-bottom:10px">
+            <label data-i18n="dohForwarderLabel">DNS-forwarder (DoH)</label>
+            <div class="radio-group" id="doh-selector">
+                <label><input type="radio" name="doh-fwd" value="GoogleDNS" checked> Google</label>
+                <label><input type="radio" name="doh-fwd" value="CloudFlare"> CloudFlare</label>
+                <label><input type="radio" name="doh-fwd" value="Comss"> Comss</label>
+                <label><input type="radio" name="doh-fwd" value="Quad9"> Quad9</label>
+                <label><input type="radio" name="doh-fwd" value="Yandex"> Yandex</label>
+                <label><input type="radio" name="doh-fwd" value="__custom"> <span data-i18n="dohCustom">Custom...</span></label>
+            </div>
+            <input type="text" id="doh-custom-url" placeholder="https://your-dns/dns-query or IP" style="display:none;margin-top:6px;width:100%">
+            <div class="hint" data-i18n="dohHint">All services will use this DNS-over-HTTPS (DoH) provider for domain resolution.</div>
+        </div>
+        <div id="services-panel" class="scenario-panel">
+            <div class="scenario-panel-header">
+                <label style="margin-bottom:0" data-i18n="servicesLabel">Services to route through tunnel</label>
+                <div>
+                    <button class="btn btn-secondary btn-sm" onclick="toggleAllServices(true)" data-i18n="btnSelectAll">Select all</button>
+                    <button class="btn btn-secondary btn-sm" onclick="toggleAllServices(false)" data-i18n="btnDeselectAll">Deselect all</button>
+                </div>
+            </div>
+            <div id="services-grid" class="checkbox-grid"></div>
+            <div id="custom-domains-section" style="margin-top:12px;border-top:1px solid var(--border);padding-top:10px">
+                <label style="font-weight:600;font-size:0.9rem" data-i18n="customDomainsLabel">Custom domains</label>
+                <div style="display:flex;gap:6px;margin-top:6px">
+                    <input type="text" id="custom-domain-input" placeholder="example.com" data-i18n-ph="customDomainsPlaceholder" style="flex:1;min-width:0" onkeydown="if(event.key==='Enter'){event.preventDefault();addCustomDomain()}">
+                    <button class="btn btn-secondary btn-sm" onclick="addCustomDomain()" data-i18n="customDomainsAdd">+</button>
+                </div>
+                <div id="custom-domains-list" style="margin-top:6px;display:flex;flex-wrap:wrap;gap:4px"></div>
+            </div>
+        </div>
+        <div id="exclusions-panel" style="display:none" class="scenario-panel">
+            <div class="scenario-panel-header">
+                <label style="margin-bottom:0" data-i18n="exclusionsLabel">Exclude from tunnel (keep direct)</label>
+            </div>
+            <div id="exclusions-grid" class="checkbox-grid"></div>
+        </div>
+        <div id="netwatch-row" style="margin-top:8px">
+            <label><input type="checkbox" id="netwatch-enable" checked>
+            <span data-i18n="netwatchLabel">Netwatch failover</span></label>
+            <div class="hint" data-i18n="netwatchHint">Monitors tunnel availability by pinging 8.8.8.8 through the WG interface (via a pinned probe route). If the tunnel is down, disables its route &mdash; traffic falls back to the next tunnel (by distance) or goes direct. Re-enables the route when the tunnel recovers.</div>
+        </div>
+        <div id="df-row" style="margin-top:8px">
+            <label><input type="checkbox" id="df-enable">
+            <span data-i18n="dfLabel">Clear DF bit on UDP (DPI workaround)</span></label>
+            <div class="hint" data-i18n="dfHint">Adds AWG_NO_DF=1 to the container ENV: the proxy clears the Don&rsquo;t Fragment bit on its UDP packets. Some DPI equipment passes DF=0 UDP better than DF=1. Enable only if the tunnel has connectivity issues &mdash; with DF=0 large packets may be fragmented along the path.</div>
+        </div>
+        <div id="ipv6-row" style="margin-top:8px">
+            <label><input type="checkbox" id="ipv6-enable">
+            <span data-i18n="ipv6Label">Server is reachable over IPv6</span></label>
+            <div class="hint" data-i18n="ipv6Hint">The IPv6 header is 40 bytes instead of 20, so a full-size packet no longer fits a 1500-byte path and the WireGuard MTU has to come down. Ticked automatically when the endpoint is an IPv6 literal. For a DNS name the box stays off (IPv4 MTU) &mdash; tick it yourself if you know the name has an AAAA record; the generated script also asks the router with <code>:resolve type=ipv6</code> and lowers the MTU if it answers.</div>
+        </div>
+        <div id="storage-row" style="margin-top:14px">
+            <label for="storage-select" data-i18n="storageLabel">Container storage</label>
+            <select id="storage-select">
+                <option value="__auto" data-i18n="sAuto">Auto (usb1/ &rarr; disk1/)</option>
+                <option value="disk1" data-i18n="sDisk1">disk1/ &mdash; internal storage (default)</option>
+                <option value="usb1" data-i18n="sUsb1">usb1/ &mdash; USB flash drive (common)</option>
+                <option value="usb1-part1" data-i18n="sUsb1p">usb1-part1/ &mdash; USB partition</option>
+                <option value="usb2" data-i18n="sUsb2">usb2/ &mdash; USB flash drive (2nd)</option>
+                <option value="sd1" data-i18n="sSd1">sd1/ &mdash; SD / microSD card</option>
+                <option value="nvme1" data-i18n="sNvme1">nvme1/ &mdash; NVMe SSD</option>
+                <option value="sata1" data-i18n="sSata1">sata1/ &mdash; SATA drive</option>
+                <option value="__custom" data-i18n="sCustom">Custom...</option>
+            </select>
+            <input type="text" id="storage-custom" placeholder="e.g. sata1-part1" style="display:none">
+        </div>
+        <div id="proxy-ip-row" style="margin-top:14px;display:none">
+            <label for="proxy-ip" data-i18n="proxyIpLabel">Proxy device IP address</label>
+            <input type="text" id="proxy-ip" placeholder="e.g. 192.168.1.100" data-i18n-ph="proxyIpPh">
+        </div>
+        <div style="margin-top:14px">
+            <label for="prefix-select" data-i18n="prefixLabel">Tunnel name (prefix)</label>
+            <select id="prefix-select">
+                <option value="awg-proxy-1" data-i18n="pAwg1">awg-proxy-1 &mdash; 1st tunnel (default)</option>
+                <option value="awg-proxy-2" data-i18n="pAwg2">awg-proxy-2 &mdash; 2nd tunnel</option>
+                <option value="awg-proxy-3" data-i18n="pAwg3">awg-proxy-3 &mdash; 3rd tunnel</option>
+                <option value="awg-proxy-4" data-i18n="pAwg4">awg-proxy-4 &mdash; 4th tunnel</option>
+                <option value="awg-proxy-5" data-i18n="pAwg5">awg-proxy-5 &mdash; 5th tunnel</option>
+                <option value="awg-server-1" data-i18n="pHub1" style="display:none">awg-server-1 &mdash; 1st server (default)</option>
+                <option value="awg-server-2" data-i18n="pHub2" style="display:none">awg-server-2 &mdash; 2nd server</option>
+                <option value="__custom" data-i18n="pCustom">Custom...</option>
+            </select>
+            <input type="text" id="prefix-custom" placeholder="e.g. awg-office" style="display:none">
+            <div class="hint" data-i18n="prefixHint">Change the prefix only if you need 2+ tunnels with different routing rules</div>
+        </div>
+        <div id="s2s-panel" style="display:none;margin-top:14px">
+            <div class="s2s-fields">
+                <div>
+                    <label for="s2s-endpoint-b" data-i18n="s2sEndpointB">Server endpoint (Side B IP or domain)</label>
+                    <input type="text" id="s2s-endpoint-b" placeholder="e.g. 198.51.100.1 or vpn.example.com">
+                </div>
+                <div>
+                    <label for="s2s-awg-port" data-i18n="s2sAwgPort">AWG port on server (Side B)</label>
+                    <input type="text" id="s2s-awg-port">
+                </div>
+                <div>
+                    <label for="s2s-tunnel-subnet" data-i18n="s2sTunnelSubnet">Tunnel subnet</label>
+                    <input type="text" id="s2s-tunnel-subnet" value="10.0.0.0/30">
+                </div>
+                <div>
+                    <label for="s2s-dns" data-i18n="s2sDns">DNS server (required for domain endpoint)</label>
+                    <input type="text" id="s2s-dns" placeholder="e.g. 192.168.88.1">
+                </div>
+            </div>
+            <div class="s2s-fields">
+                <div>
+                    <label for="s2s-lan-a" data-i18n="s2sLanA">Side A LAN subnet (optional)</label>
+                    <input type="text" id="s2s-lan-a" placeholder="e.g. 192.168.1.0/24">
+                </div>
+                <div>
+                    <label for="s2s-lan-b" data-i18n="s2sLanB">Side B LAN subnet (optional)</label>
+                    <input type="text" id="s2s-lan-b" placeholder="e.g. 192.168.2.0/24">
+                </div>
+            </div>
+            <div class="s2s-fields">
+                <div>
+                    <label for="s2s-storage-a" data-i18n="s2sStorageA">Side A storage</label>
+                    <select id="s2s-storage-a">
+                        <option value="__auto" data-i18n="sAuto">Auto (usb1/ &rarr; disk1/)</option>
+                        <option value="disk1" data-i18n="sDisk1">disk1/</option>
+                        <option value="usb1" data-i18n="sUsb1">usb1/</option>
+                        <option value="usb1-part1">usb1-part1/</option>
+                    </select>
+                </div>
+                <div>
+                    <label for="s2s-storage-b" data-i18n="s2sStorageB">Side B storage</label>
+                    <select id="s2s-storage-b">
+                        <option value="__auto" data-i18n="sAuto">Auto (usb1/ &rarr; disk1/)</option>
+                        <option value="disk1" data-i18n="sDisk1">disk1/</option>
+                        <option value="usb1" data-i18n="sUsb1">usb1/</option>
+                        <option value="usb1-part1">usb1-part1/</option>
+                    </select>
+                </div>
+            </div>
+            <div id="s2s-proto-row" style="margin-top:14px;padding-top:12px;border-top:1px solid var(--border)">
+                <label for="s2s-proto-level" data-i18n="protoLevelLabel">AmneziaWG protocol level</label>
+                <select id="s2s-proto-level" onchange="onProtoLevelChange()" style="max-width:280px">
+                    <option value="v3" selected>AWG 3.0 (header protection)</option>
+                    <option value="v2">AWG 2.0</option>
+                    <option value="v1.5">AWG 1.5 (CPS)</option>
+                    <option value="v1">AWG 1.0</option>
+                </select>
+                <div class="hint" data-i18n="protoLevelHint">Both sides are generated by this page, so the newest level is the safe default. Every side needs an awg-proxy image that understands it.</div>
+                <label style="display:block;margin-top:10px"><input type="checkbox" id="s2s-proto-compat" checked onchange="onProtoLevelChange()"> <span data-i18n="protoCompatLabel">Stay compatible with older installations</span></label>
+                <div class="hint" data-i18n="protoCompatHint">Adds a fallback chain down to AWG 1.0. A peer still running an older container connects on the stage it understands; costs nothing while everyone is on the same level.</div>
+                <div class="alert alert-warning" style="margin-top:10px">
+                    <strong data-i18n="v3UpdateTitle">Updating an existing installation</strong><br>
+                    <span data-i18n="v3UpdateBody">A container built before 3.0 does not understand header protection: adding the new variables is not enough. Pull the new image and recreate the container.</span>
+                </div>
+            </div>
+            <div id="s2s-awg-params" class="fields-grid" style="margin-top:10px"></div>
+            <button class="btn btn-secondary" onclick="regenerateS2SParams()" data-i18n="s2sRegenerate">Regenerate AWG params</button>
+        </div>
+        <div id="srv-panel" style="display:none;margin-top:14px">
+            <div id="srv-add-client-banner" class="alert alert-info" style="display:none;font-weight:bold;margin-bottom:12px">
+                <span data-i18n="srvAddClientBanner">Adding a new client to server</span> <span id="srv-add-client-info"></span>
+            </div>
+            <div style="margin-bottom:12px">
+                <label data-i18n="srvNetLabel">Network merging</label>
+                <div class="radio-group">
+                    <label><input type="radio" name="srv-net" value="none" onchange="onSrvNetChange()"> <span data-i18n="srvNetNone">None</span></label>
+                    <label><input type="radio" name="srv-net" value="l3" checked onchange="onSrvNetChange()"> <span data-i18n="srvNetL3">L3 routing (IP)</span></label>
+                    <label><input type="radio" name="srv-net" value="awg" onchange="onSrvNetChange()"> <span data-i18n="srvNetAwg">Direct AmneziaWG client</span></label>
+                </div>
+                <div id="srv-net-hint" class="hint" data-i18n="srvNetL3Hint">Devices will be able to reach each other by IP across subnets. What becomes possible: access NAS/file shares, IP cameras, printers by address, remote desktop (RDP/VNC), manage routers via WinBox/SSH, host game servers, access home automation (Home Assistant). Does NOT work: broadcast/multicast services. Note: each client only sees the server LAN by default. To connect two clients to each other, additional routes must be added manually.</div>
+            </div>
+            <div class="s2s-fields">
+                <div>
+                    <label for="srv-endpoint" data-i18n="srvEndpoint">Server public IP or domain</label>
+                    <input type="text" id="srv-endpoint" placeholder="e.g. 198.51.100.1 or vpn.example.com">
+                </div>
+                <div>
+                    <label for="srv-awg-port" data-i18n="srvAwgPort">Server AWG port (public)</label>
+                    <input type="text" id="srv-awg-port">
+                </div>
+                <div>
+                    <label for="srv-tunnel-subnet" data-i18n="srvTunnelSubnet">Tunnel subnet (/24)</label>
+                    <input type="text" id="srv-tunnel-subnet" value="10.10.10.0/24">
+                </div>
+                <div>
+                    <label for="srv-dns" data-i18n="srvDns">DNS server (required if endpoint is a domain, not IP)</label>
+                    <input type="text" id="srv-dns" placeholder="e.g. 192.168.88.1">
+                </div>
+            </div>
+            <div id="srv-lan-fields" class="s2s-fields" style="margin-top:10px">
+                <div>
+                    <label for="srv-lan-server" id="srv-lan-server-label" data-i18n="srvLanServerReq">Server LAN subnet (required)</label>
+                    <input type="text" id="srv-lan-server" placeholder="e.g. 192.168.1.0/24">
+                </div>
+                <div>
+                    <label for="srv-lan-client" id="srv-lan-client-label" data-i18n="srvLanClientReq">Client LAN subnet (required)</label>
+                    <input type="text" id="srv-lan-client" placeholder="e.g. 192.168.2.0/24">
+                </div>
+            </div>
+            <div class="s2s-fields" style="margin-top:10px">
+                <div>
+                    <label for="srv-storage-server" data-i18n="srvStorageServer">Server storage</label>
+                    <select id="srv-storage-server">
+                        <option value="__auto" data-i18n="sAuto">Auto (usb1/ &rarr; disk1/)</option>
+                        <option value="disk1" data-i18n="sDisk1">disk1/</option>
+                        <option value="usb1" data-i18n="sUsb1">usb1/</option>
+                        <option value="usb1-part1">usb1-part1/</option>
+                    </select>
+                </div>
+                <div id="srv-storage-client-wrap">
+                    <label for="srv-storage-client" data-i18n="srvStorageClient">Client storage</label>
+                    <select id="srv-storage-client">
+                        <option value="__auto" data-i18n="sAuto">Auto (usb1/ &rarr; disk1/)</option>
+                        <option value="disk1" data-i18n="sDisk1">disk1/</option>
+                        <option value="usb1" data-i18n="sUsb1">usb1/</option>
+                        <option value="usb1-part1">usb1-part1/</option>
+                    </select>
+                </div>
+            </div>
+            <div id="srv-proto-row" style="margin-top:14px;padding-top:12px;border-top:1px solid var(--border)">
+                <label for="srv-proto-level" data-i18n="protoLevelLabel">AmneziaWG protocol level</label>
+                <select id="srv-proto-level" onchange="onProtoLevelChange()" style="max-width:280px">
+                    <option value="v3" selected>AWG 3.0 (header protection)</option>
+                    <option value="v2">AWG 2.0</option>
+                    <option value="v1.5">AWG 1.5 (CPS)</option>
+                    <option value="v1">AWG 1.0</option>
+                </select>
+                <div class="hint" data-i18n="protoLevelHint">Both sides are generated by this page, so the newest level is the safe default. Every side needs an awg-proxy image that understands it.</div>
+                <label style="display:block;margin-top:10px"><input type="checkbox" id="srv-proto-compat" checked onchange="onProtoLevelChange()"> <span data-i18n="protoCompatLabel">Stay compatible with older installations</span></label>
+                <div class="hint" data-i18n="protoCompatHint">Adds a fallback chain down to AWG 1.0. A peer still running an older container connects on the stage it understands; costs nothing while everyone is on the same level.</div>
+                <div class="alert alert-warning" style="margin-top:10px">
+                    <strong data-i18n="v3UpdateTitle">Updating an existing installation</strong><br>
+                    <span data-i18n="v3UpdateBody">A container built before 3.0 does not understand header protection: adding the new variables is not enough. Pull the new image and recreate the container.</span>
+                </div>
+            </div>
+            <div id="srv-awg-params" class="fields-grid" style="margin-top:10px"></div>
+            <div style="margin-top:10px">
+                <label for="srv-client-num" data-i18n="srvClientNum">Client number</label>
+                <input type="number" id="srv-client-num" value="1" min="1" max="253" style="width:80px">
+            </div>
+            <button class="btn btn-secondary" onclick="regenerateServerParams()" data-i18n="srvRegenerate">Regenerate AWG params</button>
+        </div>
+        <div class="actions">
+            <button class="btn btn-primary" id="btn-generate" onclick="generate()" data-i18n="btnGenerate">Generate commands</button>
+        </div>
+        <div id="infographic-container"></div>
+    </div>
+
+    <div id="errors-container"></div>
+
+    <div id="parsed-section" class="card">
+        <div class="card-title" data-i18n="parsedTitle">Parsed parameters</div>
+        <div id="fields-display"></div>
+    </div>
+
+    <div id="output-section" class="card">
+        <div class="output-header">
+            <div class="card-title" style="margin-bottom:0" data-i18n="outputTitle">MikroTik RouterOS Commands</div>
+            <button class="btn btn-secondary" id="copy-btn" onclick="copyOutput('output','copy-btn')" data-i18n="btnCopy">Copy all</button>
+        </div>
+        <pre id="output"></pre>
+        <button class="btn-expand" id="expand-output" onclick="toggleCollapse('output','expand-output')" data-i18n="btnExpand" style="display:none">Show all</button>
+    </div>
+
+    <div id="routing-section" class="card" style="display:none">
+        <div class="output-header">
+            <div class="card-title" style="margin-bottom:0" data-i18n="routingTitle">Step 2: Routing Setup</div>
+            <button class="btn btn-secondary" id="copy-routing-btn" onclick="copyOutput('routing-output','copy-routing-btn')" data-i18n="btnCopyRouting">Copy routing</button>
+        </div>
+        <div class="alert alert-warning" data-i18n="routingNote">Paste this AFTER Step 1 completes (wait for "Installation complete!").</div>
+        <pre id="routing-output"></pre>
+        <button class="btn-expand" id="expand-routing" onclick="toggleCollapse('routing-output','expand-routing')" data-i18n="btnExpand" style="display:none">Show all</button>
+    </div>
+
+    <div id="uninstall-section" class="card">
+        <div class="output-header">
+            <div class="card-title" style="margin-bottom:0" data-i18n="uninstallTitle">Uninstall Commands</div>
+            <button class="btn btn-secondary" id="copy-uninstall-btn"
+                    onclick="copyOutput('uninstall-output','copy-uninstall-btn')" data-i18n="btnCopyUn">Copy uninstall</button>
+        </div>
+        <pre id="uninstall-output"></pre>
+        <button class="btn-expand" id="expand-uninstall" onclick="toggleCollapse('uninstall-output','expand-uninstall')" data-i18n="btnExpand" style="display:none">Show all</button>
+    </div>
+
+    <div id="s2s-output-section">
+        <div class="card s2s-side">
+            <div class="s2s-side-header">
+                <div class="card-title" style="margin-bottom:0" data-i18n="s2sOutputA">Side A (MikroTik 1) — client</div>
+                <button class="btn btn-secondary" id="s2s-copy-a" onclick="copyOutput('s2s-out-a','s2s-copy-a')" data-i18n="btnCopy">Copy all</button>
+            </div>
+            <pre id="s2s-out-a" class="s2s-output"></pre>
+            <div style="margin-top:12px">
+                <div class="card-title" style="font-size:0.9rem" data-i18n="s2sUninstallA">Uninstall Side A (client)</div>
+                <pre id="s2s-uninst-a" class="s2s-output"></pre>
+            </div>
+        </div>
+        <div class="card s2s-side">
+            <div class="s2s-side-header">
+                <div class="card-title" style="margin-bottom:0" data-i18n="s2sOutputB">Side B (MikroTik 2) — server</div>
+                <button class="btn btn-secondary" id="s2s-copy-b" onclick="copyOutput('s2s-out-b','s2s-copy-b')" data-i18n="btnCopy">Copy all</button>
+            </div>
+            <pre id="s2s-out-b" class="s2s-output"></pre>
+            <div style="margin-top:12px">
+                <div class="card-title" style="font-size:0.9rem" data-i18n="s2sUninstallB">Uninstall Side B (server)</div>
+                <pre id="s2s-uninst-b" class="s2s-output"></pre>
+            </div>
+        </div>
+    </div>
+
+    <div id="srv-output-section" style="display:none">
+        <div id="srv-link-banner" class="alert alert-warning" style="font-size:1.1em;font-weight:bold;margin-bottom:16px">
+            <div data-i18n="srvSaveLink">&#9888;&#65039; SAVE THIS LINK FOR FUTURE CONFIGURATION</div>
+            <div style="display:flex;gap:8px;margin-top:8px">
+                <input type="text" id="srv-share-url" readonly onclick="this.select()" style="flex:1;font-size:0.85em;font-family:monospace">
+                <button class="btn btn-secondary" id="srv-copy-url" onclick="copySrvURL()" data-i18n="btnCopy">Copy</button>
+            </div>
+            <div class="hint" style="margin-top:6px;font-weight:normal;font-size:0.85em" data-i18n="srvEnvHint">This link will also be saved in container env AWG_CONFIG_URL. To retrieve it later: /container/envs/print where key=AWG_CONFIG_URL</div>
+        </div>
+        <div class="card s2s-side">
+            <div class="s2s-side-header">
+                <div class="card-title" style="margin-bottom:0" data-i18n="srvOutputServer">Server (head MikroTik)</div>
+                <button class="btn btn-secondary" id="srv-copy-server" onclick="copyOutput('srv-out-server','srv-copy-server')" data-i18n="btnCopy">Copy all</button>
+            </div>
+            <pre id="srv-out-server" class="s2s-output"></pre>
+        </div>
+        <div class="card s2s-side">
+            <div class="s2s-side-header">
+                <div class="card-title" style="margin-bottom:0" id="srv-client-title" data-i18n="srvOutputClient">Client 1</div>
+                <button class="btn btn-secondary" id="srv-copy-client" onclick="copyOutput('srv-out-client','srv-copy-client')" data-i18n="btnCopy">Copy all</button>
+            </div>
+            <pre id="srv-out-client" class="s2s-output"></pre>
+            <button class="btn btn-primary" id="srv-download-conf" onclick="downloadAwgConf()" data-i18n="btnDownloadConf" style="display:none;margin-top:10px">Download .conf</button>
+        </div>
+        <div id="srv-server-update" class="card s2s-side" style="display:none">
+            <div class="s2s-side-header">
+                <div class="card-title" style="margin-bottom:0" data-i18n="srvServerUpdate">Run on SERVER to add this client</div>
+                <button class="btn btn-secondary" id="srv-copy-update" onclick="copyOutput('srv-out-update','srv-copy-update')" data-i18n="btnCopy">Copy all</button>
+            </div>
+            <pre id="srv-out-update" class="s2s-output"></pre>
+        </div>
+    </div>
+
+    <footer data-i18n="footer">Works entirely offline &mdash; no data leaves your browser.</footer>
+</div>
+
+<script>
+    'use strict';
+
+    // ============================================================
+    // i18n
+    // ============================================================
+    var currentLang = 'ru';
+
+    var REPO = (function() {
+        var h = location.hostname, p = location.pathname.split('/')[1] || '';
+        var m = h.match(/^([^.]+)\.(github|gitlab)\.io$/);
+        if (m) {
+            var base = 'https://' + (m[2] === 'github' ? 'github.com' : 'gitlab.com') + '/' + m[1] + '/' + p;
+            var dl = m[2] === 'gitlab'
+                ? base + '/-/releases/permalink/latest/downloads/'
+                : base + '/releases/latest/download/';
+            return { base: base, dl: dl };
+        }
+        return {
+            base: 'https://github.com/timbrs/amneziawg-mikrotik-c',
+            dl: 'https://github.com/timbrs/amneziawg-mikrotik-c/releases/latest/download/'
+        };
+    })();
+
+    var RU_RANGES_BASE = 'https://github.com/timbrs/ru-ranges/releases/latest/download/';
+
+    var I18N = {
+        en: {
+            protoLevelLabel: 'AmneziaWG protocol level',
+            protoLevelHint: 'Both sides are generated by this page, so the newest level is the safe default. Every side needs an awg-proxy image that understands it — update the container image before switching an existing installation.',
+            protoCompatLabel: 'Stay compatible with older installations',
+            protoCompatHint: 'Adds a fallback chain down to AWG 1.0. A peer still running an older container connects on the stage it understands, and it costs nothing while everyone is on the same level.',
+            v3NotesTitle: 'AmneziaWG 3.0 — what the proxy reproduces',
+            v3NotesOk: 'Header protection is reproduced exactly: ChaCha20 over the packet header with the first 12 padding bytes as nonce. That is the only wire-format change in 3.0, so a real 3.0 server accepts this tunnel.',
+            v3NotesSkip: 'ContentPaddingAddition and the timing parameters (RekeyAfterTime, RekeyTimeout, KeepaliveTimeout, MaxHandshakeAttempts) are carried into the container but not applied. The proxy holds no session keys, so it cannot touch encrypted payload, and it forwards packets as they arrive instead of pacing them. All of these are enforced by the sender only — the server never validates them, so the tunnel works either way. What is lost is disguise, not connectivity: packet sizes stay multiples of 16 and the WireGuard rhythm (5 s retries, 10 s keepalive, 120 s rekey) stays visible.',
+            v3RejectWarn: 'RejectAfterTime has a lower bound below 150 s. MikroTik starts rekeying at 120 s and keeps the old keypair until 180 s, so the peer may start rejecting a key MikroTik still considers live. Expect the tunnel to drop periodically — this one cannot be worked around in the proxy.',
+            v3MtuNote: 'This S4 pushes the outer datagram past 1500 bytes, so the WireGuard MTU is lowered to keep it from fragmenting.',
+            v3UpdateTitle: 'Updating an existing installation',
+            v3UpdateBody: 'A container built before 3.0 does not understand header protection: adding the new variables is not enough. Pull the new image and recreate the container — the script below already does both.',
+            linkFromOlder: 'This link was created by the previous configurator and cannot be opened here. Open it in',
+            title: 'AWG Proxy 3.0 \u2014 MikroTik Offline Command Generator',
+            subtitle: 'Paste an AmneziaWG .conf (1.0 through 3.0) to generate ready-to-use MikroTik RouterOS commands',
+            privacy: 'All configuration and data is processed locally in your browser. Nothing is transmitted anywhere. Your private keys are safe.',
+            confTitle: 'AmneziaWG Configuration File',
+            confLabel: '.conf file contents',
+            modeLabel: 'Deployment mode',
+            modeContainer: 'MikroTik + Container (Docker)',
+            modeStandalone: 'MikroTik + Server (Standalone)',
+            storageLabel: 'Container storage',
+            proxyIpLabel: 'Proxy device IP address',
+            proxyIpPh: 'e.g. 192.168.1.100',
+            prefixLabel: 'Tunnel name (prefix)',
+            btnGenerate: 'Generate commands',
+            btnClear: 'Clear',
+            parsedTitle: 'Parsed parameters',
+            outputTitle: 'Step 1: Install AWG Proxy',
+            btnCopy: 'Copy all',
+            btnDownloadConf: 'Download .conf',
+            routingTitle: 'Step 2: Routing Setup',
+            btnCopyRouting: 'Copy routing',
+            routingNote: 'Paste this AFTER Step 1 completes (wait for "Installation complete!").',
+            uninstallTitle: 'Uninstall Commands',
+            btnCopyUn: 'Copy uninstall',
+            footer: 'Works entirely offline \u2014 no data leaves your browser.',
+            errEmpty: 'Configuration field is empty. Paste the contents of your .conf file.',
+            errPrefix: 'Invalid tunnel prefix: only letters, digits, hyphens and underscores allowed (1-30 chars, cannot start/end with hyphen).',
+            errProxyIp: 'Invalid proxy device IP address.',
+            errTitle: 'Errors:',
+            ifaceSec: '[Interface] section',
+            peerSec: '[Peer] section',
+            copied: 'Copied!',
+            sDisk1: 'disk1/ \u2014 internal storage (default)',
+            sUsb1: 'usb1/ \u2014 USB flash drive (common)',
+            sUsb1p: 'usb1-part1/ \u2014 USB partition',
+            sUsb2: 'usb2/ \u2014 USB flash drive (2nd)',
+            sSd1: 'sd1/ \u2014 SD / microSD card',
+            sNvme1: 'nvme1/ \u2014 NVMe SSD',
+            sSata1: 'sata1/ \u2014 SATA drive',
+            sCustom: 'Custom...',
+            pAwg1: 'awg-proxy-1 \u2014 1st tunnel (default)',
+            pAwg2: 'awg-proxy-2 \u2014 2nd tunnel',
+            pAwg3: 'awg-proxy-3 \u2014 3rd tunnel',
+            pAwg4: 'awg-proxy-4 \u2014 4th tunnel',
+            pAwg5: 'awg-proxy-5 \u2014 5th tunnel',
+            pCustom: 'Custom...',
+            netwatchLabel: 'Netwatch failover',
+            netwatchHint: 'Monitors tunnel availability by pinging 8.8.8.8 through the WG interface (via a pinned probe route). If the tunnel is down, disables its route \u2014 traffic falls back to the next tunnel (by distance) or goes direct. Re-enables when the tunnel recovers.',
+            dfLabel: 'Clear DF bit on UDP (DPI workaround)',
+            dfHint: 'Adds AWG_NO_DF=1 to the container ENV: the proxy clears the Don\u2019t Fragment bit on its UDP packets. Some DPI equipment passes DF=0 UDP better than DF=1. Enable only if the tunnel has connectivity issues \u2014 with DF=0 large packets may be fragmented along the path.',
+            ipv6Label: 'Server is reachable over IPv6',
+            ipv6Hint: 'The IPv6 header is 40 bytes instead of 20, so a full-size packet no longer fits a 1500-byte path and the WireGuard MTU has to come down. Ticked automatically when the endpoint is an IPv6 literal. For a DNS name the box stays off (IPv4 MTU) \u2014 tick it yourself if you know the name has an AAAA record; the generated script also asks the router with :resolve type=ipv6 and lowers the MTU if it answers.',
+            ipv6MtuNote: 'The endpoint is IPv6, so the WireGuard MTU is lowered by the extra 20 bytes of IPv6 header.',
+            scenarioLabel: 'Routing scenario',
+            scenarioTunnel: 'Tunnel only (manual routes)',
+            scenarioDns: 'DNS-based routing for services',
+            scenarioNonRu: 'All non-RU traffic through tunnel',
+            servicesLabel: 'Services to route through tunnel',
+            exclusionsLabel: 'Exclude from tunnel (keep direct)',
+            btnSelectAll: 'Select all',
+            btnDeselectAll: 'Deselect all',
+            sAuto: 'Auto (usb1/ \u2192 disk1/)',
+            warnFasttrack: 'FastTrack rules will be modified (out-interface-list=WAN) to keep FastTrack active for non-VPN traffic while policy routing works correctly. Uninstall script restores the original settings.',
+            noServiceSelected: 'Select at least one service or add a custom domain.',
+            customDomainsLabel: 'Custom domains',
+            customDomainsPlaceholder: 'example.com',
+            customDomainsAdd: '+',
+            modeContainerHint: 'WireGuard client on MikroTik + awg-proxy runs inside a Docker container directly on the router',
+            modeStandaloneHint: 'WireGuard client on MikroTik + awg-proxy runs on a separate device (Raspberry Pi, VPS, or another server). Use this mode for MikroTik routers that do not support containers.',
+            scenarioTunnelHint: 'You configure routes manually via /ip/route or other methods. For advanced users.',
+            scenarioDnsHint: 'Routes traffic to selected services through the tunnel using DNS forwarding (DoH). MikroTik resolves domains via DNS-over-HTTPS and collects IPs into address lists. Firewall mangle rules then route this traffic through the tunnel. Works best on a clean (reset) configuration. Important: disable Secure DNS (DoH) in your browser so that DNS queries go through MikroTik.',
+            scenarioNonRuHint: 'All traffic goes through the tunnel except Russian IP ranges (loaded into RAM on boot and refreshed daily). The .rsc file is stored on USB or selected storage. Works best on a clean (reset) configuration.',
+            prefixHint: 'Change the prefix only if you need 2+ tunnels with different routing rules',
+            modeSiteToSite: 'Site-to-site (two MikroTik)',
+            modeSiteToSiteHint: 'Connect exactly two MikroTik routers via AmneziaWG tunnel (point-to-point). WG keys and AWG params are generated automatically. For 3 or more routers, use "Server (1:N hub)" mode instead.',
+            s2sEndpointB: 'Server endpoint (Side B IP or domain)',
+            s2sTunnelSubnet: 'Tunnel subnet',
+            s2sAwgPort: 'AWG port on server (Side B)',
+            s2sLanA: 'Side A LAN subnet (optional)',
+            s2sLanB: 'Side B LAN subnet (optional)',
+            s2sStorageA: 'Side A storage',
+            s2sStorageB: 'Side B storage',
+            s2sDns: 'DNS server (required for domain endpoint)',
+            s2sRegenerate: 'Regenerate AWG params',
+            s2sOutputA: 'Side A (MikroTik 1) \u2014 client',
+            s2sOutputB: 'Side B (MikroTik 2) \u2014 server',
+            s2sUninstallA: 'Uninstall Side A (client)',
+            s2sUninstallB: 'Uninstall Side B (server)',
+            errEndpointB: 'Server endpoint is required (Side B IP address or domain).',
+            errDnsRequired: 'DNS server is required when endpoint is a hostname (not an IP address).',
+            errLanSubnet: 'Invalid LAN subnet format. Use CIDR notation, e.g. 192.168.1.0/24.',
+            errTunnelSubnet: 'Invalid tunnel subnet. Use /30 CIDR (e.g. 10.0.0.0/30).',
+            errAwgPort: 'AWG port must be 1024\u201365535.',
+            modeServer: 'Server (1:N hub)',
+            modeServerHint: 'Star topology: one MikroTik as central hub, multiple client MikroTiks connect to it via AmneziaWG. Use cases: connecting offices, homes, or remote sites into a single network. WG keys and AWG params are generated automatically. New server installs now set AWG_CLIENT_PUBS to the real direct client keys, while AWG_CLIENT_PUB stays as a legacy fallback for proxy-only clients. You will receive a shareable link to easily generate configs for new clients in the future.',
+            srvEndpoint: 'Server public IP or domain',
+            srvAwgPort: 'Server AWG port (public)',
+            srvTunnelSubnet: 'Tunnel subnet (/24)',
+            srvDns: 'DNS server (required if endpoint is a domain, not IP)',
+            srvStorageServer: 'Server storage',
+            srvStorageClient: 'Client storage',
+            srvLanServerOpt: 'Server LAN subnet (optional)',
+            srvLanClientOpt: 'Client LAN subnet (optional)',
+            srvLanServerReq: 'Server LAN subnet (required)',
+            srvLanClientReq: 'Client LAN subnet (required)',
+            srvNetLabel: 'Network merging',
+            srvNetNone: 'None',
+            srvNetL3: 'L3 routing (IP)',
+            srvNetL2: 'L2+L3 Bridging (EoIP)',
+            srvNetAwg: 'Direct AmneziaWG client',
+            srvNetAwgHint: 'Generates a ready-to-import .conf for a phone / laptop / AmneziaWG app instead of a second MikroTik. All device traffic is routed through the hub (full tunnel, AllowedIPs = 0.0.0.0/0). The head MikroTik just gets a peer with one tunnel IP — no client-side router needed. LAN merging fields are not used here.',
+            srvAwgConfTitle: '.conf for AmneziaWG — Client',
+            srvNetNoneHint: 'VPN tunnel only. No routing between LANs is configured \u2014 further network setup is up to the admin. Once the tunnel is established, you can connect to the remote MikroTik via WinBox/SSH using its tunnel IP (e.g. 10.10.10.2).',
+            srvNetL3Hint: 'Devices will be able to reach each other by IP across subnets. What becomes possible: access NAS/file shares, IP cameras, printers by address, remote desktop (RDP/VNC), manage routers via WinBox/SSH, host game servers, access home automation (Home Assistant). Does NOT work: broadcast/multicast services \u2014 auto-discovery (mDNS/Bonjour, DLNA, AirPlay, Chromecast), Windows Network Neighborhood, printer auto-discovery, Wake-on-LAN. Note: each client only sees the server LAN by default. To connect two clients to each other, additional routes must be added manually \u2014 see README section "Routing between clients".',
+            srvNetL2Hint: 'Single network \u2014 all devices as if on one switch. Everything works: mDNS (Bonjour), DLNA, AirPlay, Chromecast, Windows Network, printer discovery. Downside: broadcast traffic goes through tunnel, more load.',
+            srvClientNum: 'Client number',
+            srvRegenerate: 'Regenerate AWG params',
+            srvOutputServer: 'Server (head MikroTik)',
+            srvOutputClient: 'Client',
+            srvServerUpdate: 'Run on SERVER to add this client',
+            srvSaveLink: '\u26a0\ufe0f SAVE THIS LINK FOR FUTURE CONFIGURATION',
+            srvEnvHint: 'This link will also be saved in container env AWG_CONFIG_URL. To retrieve it later: /container/envs/print where key=AWG_CONFIG_URL',
+            srvAddClientBanner: 'Adding a new client to server',
+            pHub1: 'awg-server-1 \u2014 1st server (default)',
+            pHub2: 'awg-server-2 \u2014 2nd server',
+            errSrvEndpoint: 'Server public IP or domain is required.',
+            errSrvTunnelSubnet: 'Invalid tunnel subnet. Use /24 CIDR (e.g. 10.10.10.0/24).',
+            errSrvLanRequired: 'Both server and client LAN subnets are required for network merging.',
+            scenarioNonRuWarn: 'WARNING: All foreign traffic will go through VPN. Make sure your server can handle the load. Most VPS have limited bandwidth. A typical household with video services easily consumes 10+ TB/month. ALL torrent traffic will also go through the tunnel (that is A LOT of data). Game traffic may also be routed through it. Choose this option only if you are 100% sure. RU address list is loaded into RAM (not stored on flash). The .rsc file (~1MB) is stored on USB if available, otherwise on disk1 (requires >= 1.5MB free).',
+            btnExpand: 'Show all',
+            btnCollapse: 'Collapse',
+            dohForwarderLabel: 'DNS forwarder (DoH)',
+            dohCustom: 'Custom...',
+            dohHint: 'All services will use this DNS-over-HTTPS (DoH) provider for domain resolution.',
+            dnsBrowserHint: 'Disable Secure DNS (DoH) in your browser.\nChrome: Settings \u2192 Privacy \u2192 Security \u2192 Use secure DNS \u2192 Off\nFirefox: Settings \u2192 Privacy \u2192 DNS over HTTPS \u2192 Off\nEdge: Settings \u2192 Privacy \u2192 Security \u2192 Use secure DNS \u2192 Off\nOtherwise MikroTik will not see your DNS queries and cannot route traffic.',
+            igTitle: 'How it works',
+            igWireguard: 'WireGuard Tunnel',
+            igContainer: 'Container (awg-proxy)',
+            igContainerOnRouter: 'Container on router',
+            igStandalone: 'External Device (awg-proxy)',
+            igDns: 'DNS (DoH)',
+            igRouting: 'Routing',
+            igFirewall: 'Firewall / NAT',
+            igMangle: 'Mangle',
+            igInternet: 'Internet',
+            igInternetWan: 'Internet (WAN)',
+            igLan: 'LAN Clients',
+            igAwgServer: 'AWG Server',
+            igRuList: 'RU Address List',
+            igScheduler: 'Scheduler',
+            igManualConfig: 'Manual Configuration',
+            igRouterA: 'MikroTik A (client)',
+            igRouterB: 'MikroTik B (server)',
+            igServerHub: 'MikroTik Server (hub)',
+            igClientN: 'MikroTik Client #N',
+            igContainerClient: 'awg-proxy (connect)',
+            igContainerServer: 'awg-proxy (listen)',
+            igClient2: 'MikroTik Client #2',
+            igTipWireguard: 'WireGuard Tunnel\n\n/interface/wireguard/add \u2014 creates a WireGuard interface with a private key and listen port\n/interface/wireguard/peers/add \u2014 adds a peer (AWG server) with its public key and endpoint pointing to the awg-proxy container\n/ip/address/add \u2014 assigns a tunnel IP address to the WireGuard interface\n\nThis is the encrypted tunnel through which traffic flows to the AWG server.',
+            igTipContainer: 'Container (awg-proxy)\n\n/interface/veth/add \u2014 creates a virtual ethernet pair connecting the router to the container\n/ip/address/add \u2014 assigns an IP address to the veth interface\n/container/envs/add \u2014 sets AWG parameters (Jc, Jmin, Jmax, S1, S2, H1\u2013H4, keys)\n/container/add \u2014 downloads and creates the awg-proxy container\n/container/start \u2014 starts the container\n\nawg-proxy converts standard WireGuard packets to AmneziaWG format (adds junk, changes headers, pads handshakes).',
+            igTipContainerOnRouter: 'Container on router\n\nThe awg-proxy container runs directly on the MikroTik router.\n\n/interface/veth/add \u2014 creates a virtual ethernet pair connecting the router to the container\n/container/envs/add \u2014 sets AWG parameters (Jc, Jmin, Jmax, S1, S2, H1\u2013H4, keys)\n/container/add + /container/start \u2014 downloads and starts the container\n\nawg-proxy converts standard WireGuard packets to AmneziaWG format.',
+            igTipStandalone: 'External Device (awg-proxy)\n\nawg-proxy runs on a separate device (Raspberry Pi, VPS, etc). The MikroTik WireGuard peer endpoint points to this device\u2019s IP address.\n\nThe device converts WireGuard traffic to AmneziaWG format before forwarding to the AWG server.',
+            igTipDns: 'DNS (DoH Forwarding)\n\n/ip/dns/static/add type=FWD \u2014 creates forwarding entries for selected service domains\n\nWhen a client resolves a domain (e.g. youtube.com), MikroTik forwards the query to a DoH provider and saves the resolved IPs in an address list.\nThis address list is provided to Mangle rules which mark matching traffic for routing through the VPN tunnel.',
+            igTipRouting: 'Policy Routing\n\n/routing/table/add \u2014 creates a separate routing table for VPN traffic\n/ip/route/add \u2014 adds a default route through the WireGuard tunnel in that table\n/routing/rule/add \u2014 binds the routing mark to the routing table\n\nTraffic marked by Mangle goes through this table and gets routed through the VPN tunnel. All other traffic uses the default routing table (direct).',
+            igTipFirewall: 'Firewall / NAT\n\n/ip/firewall/nat/add chain=srcnat masquerade src-address=172.18.0.0/30 \u2014 NATs container traffic to the internet\n\nThe out-interface masquerade rule for the WireGuard tunnel is added in the routing section when a routing scenario is configured.',
+            igTipMangle: 'Mangle (Traffic Marking)\n\n/ip/firewall/mangle/add mark-connection \u2014 marks connections going to IPs from DNS address lists\n/ip/firewall/mangle/add mark-routing \u2014 applies a routing mark to marked connections\n/ip/firewall/mangle/add change-mss clamp-to-pmtu \u2014 fixes MSS for tunneled traffic\n\nThis is the core of selective routing: only traffic to selected services gets marked and routed through the VPN.',
+            igTipMangleNonRu: 'Mangle (Traffic Marking)\n\n/ip/firewall/mangle/add mark-connection dst-address-list=!RU \u2014 marks NEW connections to non-Russian IPs (checked once per connection)\n/ip/firewall/mangle/add mark-routing connection-mark=... \u2014 applies routing mark to all packets of marked connections (fast, no address-list lookup)\n/ip/firewall/mangle/add accept dst-address-list=vpn-server \u2014 excludes VPN server\n/ip/firewall/mangle/add change-mss clamp-to-pmtu \u2014 fixes MSS\n\nAll non-Russian traffic goes through the VPN tunnel.',
+            igTipManualConfig: 'Manual Configuration\n\nNo automatic routing is configured. You choose how traffic enters the tunnel:\n\nRoute specific subnets:\n/ip/route/add dst-address=X.X.X.X/X gateway=wg-*\n\nRoute all traffic:\n/ip/route/add dst-address=0.0.0.0/0 gateway=wg-* distance=1\n\nPolicy routing with mangle marks\n\nUse address-lists for selective routing\n\nRemember to add NAT masquerade for tunneled traffic:\n/ip/firewall/nat/add chain=srcnat action=masquerade out-interface=wg-*',
+            igTipRuList: 'RU Address List\n\nDownloaded from GitHub, stored on USB/disk. Entries are loaded into RAM with 30-day timeout (not persisted to flash). Reimported on each boot + daily.\nPrivate networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) are added as static entries to prevent local traffic from entering the tunnel.\nIf scheduler is not allowed by device-mode, the list is imported as permanent entries (ru_ranges.rsc) without auto-update.',
+            igTipScheduler: 'Scheduler\n\n/system/scheduler/add \u2014 runs RU list update on boot (start-time=startup) and daily (interval=1d).\n/system/script/add \u2014 downloads fresh .rsc, clears old entries, imports new ones into RAM.\n\nKeeps the RU address list current so routing decisions stay accurate.\nIf scheduler is not allowed by device-mode, schedulers are skipped and the list is imported as permanent entries (ru_ranges.rsc) without auto-update.',
+            igTipFirewallS2S: 'Firewall\n\n/ip/firewall/filter/add accept \u2014 allows AWG port on WAN interface\n/ip/firewall/nat/add dstnat \u2014 forwards incoming AWG traffic to the container\n/ip/firewall/nat/add masquerade \u2014 NATs container and tunnel traffic\n\nSide B (server) needs additional rules to accept incoming connections.',
+            igTipFirewallSrv: 'Firewall\n\n/ip/firewall/filter/add accept \u2014 allows AWG port on WAN\n/ip/firewall/nat/add dstnat \u2014 redirects AWG traffic to container\n/ip/firewall/nat/add masquerade \u2014 NATs container + tunnel traffic\n/ip/route/add \u2014 routes to client LANs through WG peers\n\nServer accepts connections from all clients and routes between their networks.',
+            igTipRoutingS2S: 'Routing\n\n/ip/route/add \u2014 adds routes to the remote LAN subnet through the WireGuard tunnel\n\nAllows devices on Side A to reach devices on Side B and vice versa.',
+            igTipRoutingSrv: 'Routing (L3)\n\n/ip/route/add \u2014 adds routes to remote LAN subnets through WireGuard peers\n/ip/firewall/filter/add \u2014 allows forwarding between tunnel and LAN\n\nServer routes traffic between all connected client networks. Each client gets a route to the server LAN. For client-to-client routing, additional routes must be added manually.',
+            igTipRoutingTunnel: 'Manual Routing\n\nNo automatic routing is configured.\nAdd routes manually via /ip/route/add dst-address=X.X.X.X/X gateway=wg-awg-proxy\n\nFor advanced users who want full control over what goes through the tunnel.',
+            igTipLan: 'LAN Clients\n\nDevices on the local network. Their traffic passes through the MikroTik router, which decides whether to route it through the VPN tunnel or directly to the internet based on Mangle/routing rules.',
+            igTipInternetWan: 'Internet (WAN)\n\nDirect connection to the internet via the MikroTik WAN port.\nTraffic not marked for VPN goes here directly.',
+            igTipInternet: 'Internet\n\nDirect internet connection via WAN interface. Non-VPN traffic goes here. VPN traffic goes through the WireGuard tunnel to the AWG server.',
+            igTipAwgServer: 'AmneziaWG Server\n\nThe remote VPN server running AmneziaWG. Receives transformed packets from awg-proxy and routes them to the internet.\n\nTraffic flows: Client \u2192 MikroTik WG \u2192 awg-proxy \u2192 AWG Server \u2192 Internet'
+        },
+        ru: {
+            protoLevelLabel: 'Уровень протокола AmneziaWG',
+            protoLevelHint: 'Обе стороны генерирует эта страница, поэтому самый новый уровень — безопасный выбор по умолчанию. Каждой стороне нужен образ awg-proxy, который его понимает: перед переключением существующей установки обновите образ контейнера.',
+            protoCompatLabel: 'Совместимость со старыми установками',
+            protoCompatHint: 'Добавляет цепочку отката вплоть до AWG 1.0. Пир со старым контейнером подключится по той ступени, которую понимает, а пока обе стороны на одном уровне, цепочка ничего не стоит.',
+            v3NotesTitle: 'AmneziaWG 3.0 — что прокси воспроизводит',
+            v3NotesOk: 'Шифрование заголовка воспроизводится точно: ChaCha20 поверх заголовка пакета, nonce — первые 12 байт паддинга. Это единственное изменение формата на проводе в 3.0, поэтому настоящий сервер 3.0 принимает такой туннель.',
+            v3NotesSkip: 'ContentPaddingAddition и параметры таймингов (RekeyAfterTime, RekeyTimeout, KeepaliveTimeout, MaxHandshakeAttempts) переносятся в контейнер, но не применяются. У прокси нет сессионных ключей, поэтому он не может изменить зашифрованный payload, и он пересылает пакеты сразу, а не выдерживает паузы. Всё это проверяет только отправитель — сервер их не валидирует, так что туннель работает в любом случае. Теряется маскировка, а не связь: размеры пакетов остаются кратными 16, а ритм WireGuard (ретраи 5 с, keepalive 10 с, ре-кей 120 с) остаётся заметным.',
+            v3RejectWarn: 'Нижняя граница RejectAfterTime меньше 150 с. MikroTik начинает ре-кей на 120-й секунде и держит старый keypair до 180-й, поэтому пир может начать отвергать ключ, который MikroTik ещё считает живым. Ждите периодических обрывов — это единственное, что нельзя обойти на стороне прокси.',
+            v3MtuNote: 'При таком S4 внешняя датаграмма выходит за 1500 байт, поэтому MTU WireGuard понижается, чтобы не было фрагментации.',
+            v3UpdateTitle: 'Обновление существующей установки',
+            v3UpdateBody: 'Контейнер, собранный до 3.0, не понимает шифрование заголовка: добавить новые переменные недостаточно. Нужно скачать новый образ и пересоздать контейнер — скрипт ниже делает и то, и другое.',
+            linkFromOlder: 'Эта ссылка создана предыдущим конфигуратором и здесь не открывается. Откройте её в',
+            title: 'AWG Proxy 3.0 \u2014 \u041e\u0444\u043b\u0430\u0439\u043d-\u0433\u0435\u043d\u0435\u0440\u0430\u0442\u043e\u0440 \u043a\u043e\u043c\u0430\u043d\u0434 MikroTik',
+            subtitle: 'Вставьте .conf AmneziaWG (от 1.0 до 3.0) для генерации команд MikroTik RouterOS',
+            privacy: '\u0412\u0441\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e \u0432 \u0432\u0430\u0448\u0435\u043c \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435. \u041d\u0438\u0447\u0435\u0433\u043e \u043d\u0438\u043a\u0443\u0434\u0430 \u043d\u0435 \u043f\u0435\u0440\u0435\u0434\u0430\u0451\u0442\u0441\u044f. \u0412\u0430\u0448\u0438 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0435 \u043a\u043b\u044e\u0447\u0438 \u0432 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438.',
+            confTitle: '\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b AmneziaWG',
+            confLabel: '\u0421\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435 .conf \u0444\u0430\u0439\u043b\u0430',
+            modeLabel: '\u0420\u0435\u0436\u0438\u043c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438',
+            modeContainer: 'MikroTik + \u041a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 (Docker)',
+            modeStandalone: 'MikroTik + \u0421\u0435\u0440\u0432\u0435\u0440 (Standalone)',
+            storageLabel: '\u0425\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430',
+            proxyIpLabel: 'IP-\u0430\u0434\u0440\u0435\u0441 \u043f\u0440\u043e\u043a\u0441\u0438-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430',
+            proxyIpPh: '\u043d\u0430\u043f\u0440. 192.168.1.100',
+            prefixLabel: '\u0418\u043c\u044f \u0442\u0443\u043d\u043d\u0435\u043b\u044f (\u043f\u0440\u0435\u0444\u0438\u043a\u0441)',
+            btnGenerate: '\u0421\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c',
+            btnClear: '\u041e\u0447\u0438\u0441\u0442\u0438\u0442\u044c',
+            parsedTitle: '\u0420\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b',
+            outputTitle: '\u0428\u0430\u0433 1: \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 AWG Proxy',
+            btnCopy: '\u0421\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c',
+            btnDownloadConf: '\u0421\u043a\u0430\u0447\u0430\u0442\u044c .conf',
+            routingTitle: '\u0428\u0430\u0433 2: \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438',
+            btnCopyRouting: '\u0421\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c',
+            routingNote: '\u0412\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u042d\u0422\u041e \u041f\u041e\u0421\u041b\u0415 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u0428\u0430\u0433\u0430 1 (\u0434\u043e\u0436\u0434\u0438\u0442\u0435\u0441\u044c "Installation complete!").',
+            uninstallTitle: '\u041a\u043e\u043c\u0430\u043d\u0434\u044b \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f',
+            btnCopyUn: '\u0421\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c',
+            footer: '\u0420\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u043e\u0444\u043b\u0430\u0439\u043d \u2014 \u0434\u0430\u043d\u043d\u044b\u0435 \u043d\u0435 \u043f\u043e\u043a\u0438\u0434\u0430\u044e\u0442 \u0432\u0430\u0448 \u0431\u0440\u0430\u0443\u0437\u0435\u0440.',
+            errEmpty: '\u041f\u043e\u043b\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043f\u0443\u0441\u0442\u043e\u0435. \u0412\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435 .conf \u0444\u0430\u0439\u043b\u0430.',
+            errPrefix: '\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u043f\u0440\u0435\u0444\u0438\u043a\u0441: \u0442\u043e\u043b\u044c\u043a\u043e \u0431\u0443\u043a\u0432\u044b, \u0446\u0438\u0444\u0440\u044b, \u0434\u0435\u0444\u0438\u0441\u044b \u0438 \u043f\u043e\u0434\u0447\u0451\u0440\u043a\u0438\u0432\u0430\u043d\u0438\u044f (1-30 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432, \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u043d\u0430\u0447\u0438\u043d\u0430\u0442\u044c\u0441\u044f/\u0437\u0430\u043a\u0430\u043d\u0447\u0438\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u0435\u0444\u0438\u0441\u043e\u043c).',
+            errProxyIp: '\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 \u043f\u0440\u043e\u043a\u0441\u0438-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.',
+            errTitle: '\u041e\u0448\u0438\u0431\u043a\u0438:',
+            ifaceSec: '\u0421\u0435\u043a\u0446\u0438\u044f [Interface]',
+            peerSec: '\u0421\u0435\u043a\u0446\u0438\u044f [Peer]',
+            copied: '\u0421\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u043e!',
+            sDisk1: 'disk1/ \u2014 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0435\u0435 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447.)',
+            sUsb1: 'usb1/ \u2014 USB-\u043d\u0430\u043a\u043e\u043f\u0438\u0442\u0435\u043b\u044c',
+            sUsb1p: 'usb1-part1/ \u2014 USB \u0440\u0430\u0437\u0434\u0435\u043b',
+            sUsb2: 'usb2/ \u2014 USB-\u043d\u0430\u043a\u043e\u043f\u0438\u0442\u0435\u043b\u044c (2-\u0439)',
+            sSd1: 'sd1/ \u2014 SD / microSD \u043a\u0430\u0440\u0442\u0430',
+            sNvme1: 'nvme1/ \u2014 NVMe SSD',
+            sSata1: 'sata1/ \u2014 SATA \u0434\u0438\u0441\u043a',
+            sCustom: '\u0414\u0440\u0443\u0433\u043e\u0435...',
+            pAwg1: 'awg-proxy-1 \u2014 1-\u0439 \u0442\u0443\u043d\u043d\u0435\u043b\u044c (\u043f\u043e \u0443\u043c\u043e\u043b\u0447.)',
+            pAwg2: 'awg-proxy-2 \u2014 2-\u0439 \u0442\u0443\u043d\u043d\u0435\u043b\u044c',
+            pAwg3: 'awg-proxy-3 \u2014 3-\u0439 \u0442\u0443\u043d\u043d\u0435\u043b\u044c',
+            pAwg4: 'awg-proxy-4 \u2014 4-\u0439 \u0442\u0443\u043d\u043d\u0435\u043b\u044c',
+            pAwg5: 'awg-proxy-5 \u2014 5-\u0439 \u0442\u0443\u043d\u043d\u0435\u043b\u044c',
+            pCustom: '\u0414\u0440\u0443\u0433\u043e\u0435...',
+            netwatchLabel: 'Netwatch failover',
+            netwatchHint: '\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0441\u0442\u044c \u0442\u0443\u043d\u043d\u0435\u043b\u044f \u043f\u0438\u043d\u0433\u043e\u043c 8.8.8.8 \u0447\u0435\u0440\u0435\u0437 WG-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 (\u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043a\u0440\u0435\u043f\u043b\u0451\u043d\u043d\u044b\u0439 probe-\u043c\u0430\u0440\u0448\u0440\u0443\u0442). \u0415\u0441\u043b\u0438 \u0442\u0443\u043d\u043d\u0435\u043b\u044c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u2014 \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u0435\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442, \u0442\u0440\u0430\u0444\u0438\u043a \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u043d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 \u0442\u0443\u043d\u043d\u0435\u043b\u044c (\u043f\u043e distance) \u0438\u043b\u0438 \u0438\u0434\u0451\u0442 \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e. \u041f\u0440\u0438 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0431\u0440\u0430\u0442\u043d\u043e.',
+            dfLabel: '\u0423\u0431\u0440\u0430\u0442\u044c DF-\u0431\u0438\u0442 \u0441 UDP (\u043e\u0431\u0445\u043e\u0434 \u0422\u0421\u041f\u0423)',
+            dfHint: '\u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 AWG_NO_DF=1 \u0432 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430: \u043f\u0440\u043e\u043a\u0441\u0438 \u0441\u043d\u0438\u043c\u0430\u0435\u0442 Don\u2019t Fragment \u0431\u0438\u0442 \u0441\u043e \u0441\u0432\u043e\u0438\u0445 UDP-\u043f\u0430\u043a\u0435\u0442\u043e\u0432. \u0427\u0430\u0441\u0442\u044c DPI-\u043e\u0431\u043e\u0440\u0443\u0434\u043e\u0432\u0430\u043d\u0438\u044f (\u0422\u0421\u041f\u0423) \u043b\u0443\u0447\u0448\u0435 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u0435\u0442 UDP \u0441 DF=0. \u0412\u043a\u043b\u044e\u0447\u0430\u0439\u0442\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0440\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430\u0445 \u0441\u043e \u0441\u0432\u044f\u0437\u044c\u044e \u2014 \u0441 DF=0 \u043a\u0440\u0443\u043f\u043d\u044b\u0435 \u043f\u0430\u043a\u0435\u0442\u044b \u043c\u043e\u0433\u0443\u0442 \u0444\u0440\u0430\u0433\u043c\u0435\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043f\u043e \u043f\u0443\u0442\u0438.',
+            ipv6Label: '\u0421\u0435\u0440\u0432\u0435\u0440 \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u043f\u043e IPv6',
+            ipv6Hint: '\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a IPv6 \u0437\u0430\u043d\u0438\u043c\u0430\u0435\u0442 40 \u0431\u0430\u0439\u0442 \u0432\u043c\u0435\u0441\u0442\u043e 20, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043f\u043e\u043b\u043d\u043e\u0440\u0430\u0437\u043c\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u043a\u0435\u0442 \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u0451\u0442 \u043f\u043e\u043c\u0435\u0449\u0430\u0442\u044c\u0441\u044f \u0432 1500 \u0431\u0430\u0439\u0442 \u0438 MTU WireGuard \u043f\u0440\u0438\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u043f\u043e\u043d\u0438\u0437\u0438\u0442\u044c. \u0421\u0442\u0430\u0432\u0438\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438, \u0435\u0441\u043b\u0438 \u0432 \u043f\u043e\u043b\u0435 endpoint \u0443\u043a\u0430\u0437\u0430\u043d IPv6-\u043b\u0438\u0442\u0435\u0440\u0430\u043b. \u0414\u043b\u044f DNS-\u0438\u043c\u0435\u043d\u0438 \u0433\u0430\u043b\u043e\u0447\u043a\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0441\u043d\u044f\u0442\u0430 (MTU \u043a\u0430\u043a \u0434\u043b\u044f IPv4) \u2014 \u043f\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u0435\u0451 \u0432\u0440\u0443\u0447\u043d\u0443\u044e, \u0435\u0441\u043b\u0438 \u0437\u043d\u0430\u0435\u0442\u0435, \u0447\u0442\u043e \u0443 \u0438\u043c\u0435\u043d\u0438 \u0435\u0441\u0442\u044c AAAA-\u0437\u0430\u043f\u0438\u0441\u044c; \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0441\u043a\u0440\u0438\u043f\u0442 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0441\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u0442 \u0440\u043e\u0443\u0442\u0435\u0440 \u0447\u0435\u0440\u0435\u0437 :resolve type=ipv6 \u0438 \u043f\u043e\u043d\u0438\u0436\u0430\u0435\u0442 MTU, \u0435\u0441\u043b\u0438 \u0442\u043e\u0442 \u043e\u0442\u0432\u0435\u0442\u0438\u043b.',
+            ipv6MtuNote: 'Endpoint \u2014 IPv6, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 MTU WireGuard \u043f\u043e\u043d\u0438\u0436\u0435\u043d \u043d\u0430 \u043b\u0438\u0448\u043d\u0438\u0435 20 \u0431\u0430\u0439\u0442 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0430 IPv6.',
+            scenarioLabel: '\u0421\u0446\u0435\u043d\u0430\u0440\u0438\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438',
+            scenarioTunnel: '\u0422\u043e\u043b\u044c\u043a\u043e \u0442\u0443\u043d\u043d\u0435\u043b\u044c (\u0440\u0443\u0447\u043d\u044b\u0435 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u044b)',
+            scenarioDns: 'DNS-\u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432',
+            scenarioNonRu: '\u0412\u0435\u0441\u044c \u043d\u0435-\u0420\u0424 \u0442\u0440\u0430\u0444\u0438\u043a \u0447\u0435\u0440\u0435\u0437 \u0442\u0443\u043d\u043d\u0435\u043b\u044c',
+            servicesLabel: '\u0421\u0435\u0440\u0432\u0438\u0441\u044b \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0447\u0435\u0440\u0435\u0437 \u0442\u0443\u043d\u043d\u0435\u043b\u044c',
+            exclusionsLabel: '\u0418\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0438\u0437 \u0442\u0443\u043d\u043d\u0435\u043b\u044f (\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e)',
+            btnSelectAll: '\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0432\u0441\u0435',
+            btnDeselectAll: '\u0421\u043d\u044f\u0442\u044c \u0432\u0441\u0435',
+            sAuto: 'Auto (usb1/ \u2192 disk1/)',
+            warnFasttrack: '\u041f\u0440\u0430\u0432\u0438\u043b\u0430 FastTrack \u0431\u0443\u0434\u0443\u0442 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u044b (out-interface-list=WAN), \u0447\u0442\u043e\u0431\u044b FastTrack \u043e\u0441\u0442\u0430\u0432\u0430\u043b\u0441\u044f \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u043c \u0434\u043b\u044f \u043d\u0435-VPN \u0442\u0440\u0430\u0444\u0438\u043a\u0430, \u0430 policy-\u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0430 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e. \u0421\u043a\u0440\u0438\u043f\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442 \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.',
+            noServiceSelected: '\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0440\u0432\u0438\u0441 \u0438\u043b\u0438 \u0434\u043e\u0431\u0430\u0432\u044c\u0442\u0435 \u0441\u0432\u043e\u0439 \u0434\u043e\u043c\u0435\u043d.',
+            customDomainsLabel: '\u0421\u0432\u043e\u0438 \u0434\u043e\u043c\u0435\u043d\u044b',
+            customDomainsPlaceholder: 'example.com',
+            customDomainsAdd: '+',
+            modeContainerHint: 'WireGuard-\u043a\u043b\u0438\u0435\u043d\u0442 \u043d\u0430 MikroTik + awg-proxy \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u0432 Docker-\u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0435 \u043f\u0440\u044f\u043c\u043e \u043d\u0430 \u0440\u043e\u0443\u0442\u0435\u0440\u0435',
+            modeStandaloneHint: 'WireGuard-\u043a\u043b\u0438\u0435\u043d\u0442 \u043d\u0430 MikroTik + awg-proxy \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u043d\u0430 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 (Raspberry Pi, VPS \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0435\u0440\u0432\u0435\u0440). \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u044d\u0442\u043e\u0442 \u0440\u0435\u0436\u0438\u043c \u0434\u043b\u044f MikroTik, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u044b.',
+            scenarioTunnelHint: '\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u044b \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0435 \u0441\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e \u0447\u0435\u0440\u0435\u0437 /ip/route \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u0438\u043c\u0438 \u0441\u043f\u043e\u0441\u043e\u0431\u0430\u043c\u0438. \u0414\u043b\u044f \u043e\u043f\u044b\u0442\u043d\u044b\u0445 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439.',
+            scenarioDnsHint: '\u041d\u0430\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0442\u0440\u0430\u0444\u0438\u043a \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0445 \u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432 \u0447\u0435\u0440\u0435\u0437 \u0442\u0443\u043d\u043d\u0435\u043b\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e DNS-\u0444\u043e\u0440\u0432\u0430\u0440\u0434\u0438\u043d\u0433\u0430 (DoH). MikroTik \u0440\u0435\u0437\u043e\u043b\u0432\u0438\u0442 \u0434\u043e\u043c\u0435\u043d\u044b \u0447\u0435\u0440\u0435\u0437 DNS-over-HTTPS \u0438 \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442 IP \u0432 \u0430\u0434\u0440\u0435\u0441\u043d\u044b\u0435 \u0441\u043f\u0438\u0441\u043a\u0438. Mangle-\u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0444\u0430\u0439\u0440\u0432\u043e\u043b\u0430 \u0437\u0430\u0442\u0435\u043c \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0438\u0440\u0443\u044e\u0442 \u044d\u0442\u043e\u0442 \u0442\u0440\u0430\u0444\u0438\u043a \u0447\u0435\u0440\u0435\u0437 \u0442\u0443\u043d\u043d\u0435\u043b\u044c. \u041b\u0443\u0447\u0448\u0435 \u0432\u0441\u0435\u0433\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u0447\u0438\u0441\u0442\u043e\u0439 (reset) \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438. \u0412\u0430\u0436\u043d\u043e: \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 DNS (DoH) \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435, \u0447\u0442\u043e\u0431\u044b DNS-\u0437\u0430\u043f\u0440\u043e\u0441\u044b \u0448\u043b\u0438 \u0447\u0435\u0440\u0435\u0437 MikroTik.',
+            scenarioNonRuHint: '\u0412\u0435\u0441\u044c \u0442\u0440\u0430\u0444\u0438\u043a \u0438\u0434\u0451\u0442 \u0447\u0435\u0440\u0435\u0437 \u0442\u0443\u043d\u043d\u0435\u043b\u044c, \u043a\u0440\u043e\u043c\u0435 \u0440\u043e\u0441\u0441\u0438\u0439\u0441\u043a\u0438\u0445 IP-\u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u043e\u0432 (\u0437\u0430\u0433\u0440\u0443\u0436\u0430\u044e\u0442\u0441\u044f \u0432 RAM \u043f\u0440\u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0435 \u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0435\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u043e). .rsc-\u0444\u0430\u0439\u043b \u0445\u0440\u0430\u043d\u0438\u0442\u0441\u044f \u043d\u0430 USB \u0438\u043b\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u043c \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435. \u041b\u0443\u0447\u0448\u0435 \u0432\u0441\u0435\u0433\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u0447\u0438\u0441\u0442\u043e\u0439 (reset) \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438.',
+            prefixHint: '\u041c\u0435\u043d\u044f\u0439\u0442\u0435 \u043f\u0440\u0435\u0444\u0438\u043a\u0441, \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 \u043d\u0443\u0436\u043d\u043e 2+ \u0442\u0443\u043d\u043d\u0435\u043b\u044f \u0441 \u0440\u0430\u0437\u043d\u044b\u043c\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u0430\u043c\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438',
+            modeSiteToSite: 'Site-to-site (\u0434\u0432\u0430 MikroTik)',
+            modeSiteToSiteHint: '\u0421\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0440\u043e\u0432\u043d\u043e \u0434\u0432\u0443\u0445 MikroTik \u0447\u0435\u0440\u0435\u0437 AmneziaWG-\u0442\u0443\u043d\u043d\u0435\u043b\u044c (\u0442\u043e\u0447\u043a\u0430-\u0442\u043e\u0447\u043a\u0430). WG-\u043a\u043b\u044e\u0447\u0438 \u0438 AWG-\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u044e\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438. \u0414\u043b\u044f 3 \u0438 \u0431\u043e\u043b\u0435\u0435 \u0440\u043e\u0443\u0442\u0435\u0440\u043e\u0432 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0440\u0435\u0436\u0438\u043c \u00ab\u0421\u0435\u0440\u0432\u0435\u0440 (1:N \u0445\u0430\u0431)\u00bb.',
+            s2sEndpointB: 'Endpoint \u0441\u0435\u0440\u0432\u0435\u0440\u0430 (\u0441\u0442\u043e\u0440\u043e\u043d\u0430 B, IP \u0438\u043b\u0438 \u0434\u043e\u043c\u0435\u043d)',
+            s2sTunnelSubnet: '\u041f\u043e\u0434\u0441\u0435\u0442\u044c \u0442\u0443\u043d\u043d\u0435\u043b\u044f',
+            s2sAwgPort: 'AWG-\u043f\u043e\u0440\u0442 \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435 (\u0441\u0442\u043e\u0440\u043e\u043d\u0430 B)',
+            s2sLanA: 'LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u044c \u0441\u0442\u043e\u0440\u043e\u043d\u044b A (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)',
+            s2sLanB: 'LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u044c \u0441\u0442\u043e\u0440\u043e\u043d\u044b B (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)',
+            s2sStorageA: '\u0425\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u0441\u0442\u043e\u0440\u043e\u043d\u044b A',
+            s2sStorageB: '\u0425\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u0441\u0442\u043e\u0440\u043e\u043d\u044b B',
+            s2sDns: 'DNS-\u0441\u0435\u0440\u0432\u0435\u0440 (\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u0435\u043d \u0434\u043b\u044f \u0434\u043e\u043c\u0435\u043d\u0430)',
+            s2sRegenerate: '\u041f\u0435\u0440\u0435\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c AWG-\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b',
+            s2sOutputA: '\u0421\u0442\u043e\u0440\u043e\u043d\u0430 A (MikroTik 1) \u2014 \u043a\u043b\u0438\u0435\u043d\u0442',
+            s2sOutputB: '\u0421\u0442\u043e\u0440\u043e\u043d\u0430 B (MikroTik 2) \u2014 \u0441\u0435\u0440\u0432\u0435\u0440',
+            s2sUninstallA: '\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u0441\u0442\u043e\u0440\u043e\u043d\u044b A (\u043a\u043b\u0438\u0435\u043d\u0442)',
+            s2sUninstallB: '\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u0441\u0442\u043e\u0440\u043e\u043d\u044b B (\u0441\u0435\u0440\u0432\u0435\u0440)',
+            errEndpointB: '\u0423\u043a\u0430\u0436\u0438\u0442\u0435 endpoint \u0441\u0442\u043e\u0440\u043e\u043d\u044b B (IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u0434\u043e\u043c\u0435\u043d).',
+            errDnsRequired: '\u0414\u043b\u044f hostname \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u0443\u043a\u0430\u0436\u0438\u0442\u0435 DNS-\u0441\u0435\u0440\u0432\u0435\u0440 (endpoint \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0434\u043e\u043c\u0435\u043d\u043e\u043c, \u0430 \u043d\u0435 IP-\u0430\u0434\u0440\u0435\u0441\u043e\u043c).',
+            modeServer: '\u0421\u0435\u0440\u0432\u0435\u0440 (1:N \u0445\u0430\u0431)',
+            modeServerHint: '\u0422\u043e\u043f\u043e\u043b\u043e\u0433\u0438\u044f \u0437\u0432\u0435\u0437\u0434\u0430: \u043e\u0434\u0438\u043d MikroTik \u043a\u0430\u043a \u0446\u0435\u043d\u0442\u0440\u0430\u043b\u044c\u043d\u044b\u0439 \u0443\u0437\u0435\u043b, \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438\u0445 MikroTik \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u044e\u0442\u0441\u044f \u043a \u043d\u0435\u043c\u0443 \u0447\u0435\u0440\u0435\u0437 AmneziaWG. \u041f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u0435: \u043e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u043e\u0444\u0438\u0441\u043e\u0432, \u043a\u0432\u0430\u0440\u0442\u0438\u0440, \u0443\u0434\u0430\u043b\u0451\u043d\u043d\u044b\u0445 \u0442\u043e\u0447\u0435\u043a \u0432 \u0435\u0434\u0438\u043d\u0443\u044e \u0441\u0435\u0442\u044c. WG-\u043a\u043b\u044e\u0447\u0438 \u0438 AWG-\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u044e\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438. \u041d\u043e\u0432\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u0442\u0435\u043f\u0435\u0440\u044c \u0437\u0430\u0434\u0430\u044e\u0442 AWG_CLIENT_PUBS \u0441 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u043c\u0438 \u043a\u043b\u044e\u0447\u0430\u043c\u0438 \u043f\u0440\u044f\u043c\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 AmneziaWG, \u0430 AWG_CLIENT_PUB \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u043b\u0435\u0433\u0430\u0441\u0438-fallback \u0434\u043b\u044f proxy-only \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432. \u0412\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u0435 \u0441\u0441\u044b\u043b\u043a\u0443 \u0434\u043b\u044f \u0431\u044b\u0441\u0442\u0440\u043e\u0433\u043e \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043d\u043e\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0432 \u0431\u0443\u0434\u0443\u0449\u0435\u043c.',
+            srvEndpoint: '\u041f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0439 IP \u0438\u043b\u0438 \u0434\u043e\u043c\u0435\u043d \u0441\u0435\u0440\u0432\u0435\u0440\u0430',
+            srvAwgPort: 'AWG-\u043f\u043e\u0440\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 (\u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0439)',
+            srvTunnelSubnet: '\u041f\u043e\u0434\u0441\u0435\u0442\u044c \u0442\u0443\u043d\u043d\u0435\u043b\u044f (/24)',
+            srvDns: 'DNS-\u0441\u0435\u0440\u0432\u0435\u0440 (\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u0435\u043d, \u0435\u0441\u043b\u0438 \u0432 endpoint \u0443\u043a\u0430\u0437\u0430\u043d \u0434\u043e\u043c\u0435\u043d, \u0430 \u043d\u0435 IP)',
+            srvStorageServer: '\u0425\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u0430',
+            srvStorageClient: '\u0425\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430',
+            srvLanServerOpt: 'LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440\u0430 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)',
+            srvLanClientOpt: 'LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u0430 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)',
+            srvLanServerReq: 'LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440\u0430 (\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)',
+            srvLanClientReq: 'LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u0430 (\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)',
+            srvNetLabel: '\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441\u0435\u0442\u0435\u0439',
+            srvNetNone: '\u041d\u0435\u0442',
+            srvNetL3: 'L3 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f (IP)',
+            srvNetL2: 'L2+L3 Bridging (EoIP)',
+            srvNetAwg: '\u041f\u0440\u044f\u043c\u043e\u0439 \u043a\u043b\u0438\u0435\u043d\u0442 AmneziaWG',
+            srvNetAwgHint: '\u0412\u043c\u0435\u0441\u0442\u043e \u0432\u0442\u043e\u0440\u043e\u0433\u043e MikroTik \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442 \u0433\u043e\u0442\u043e\u0432\u044b\u0439 .conf \u0434\u043b\u044f \u0438\u043c\u043f\u043e\u0440\u0442\u0430 \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 AmneziaWG (\u0442\u0435\u043b\u0435\u0444\u043e\u043d / \u043d\u043e\u0443\u0442\u0431\u0443\u043a). \u0412\u0435\u0441\u044c \u0442\u0440\u0430\u0444\u0438\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438\u0434\u0451\u0442 \u0447\u0435\u0440\u0435\u0437 \u0445\u0430\u0431 (full-tunnel, AllowedIPs = 0.0.0.0/0). \u041d\u0430 \u0433\u043e\u043b\u043e\u0432\u043d\u043e\u043c MikroTik \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e peer \u0441 \u043e\u0434\u043d\u0438\u043c IP \u0432 \u0442\u0443\u043d\u043d\u0435\u043b\u0435 \u2014 \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438\u0439 \u0440\u043e\u0443\u0442\u0435\u0440 \u043d\u0435 \u043d\u0443\u0436\u0435\u043d. \u041f\u043e\u043b\u044f \u043e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f LAN \u0437\u0434\u0435\u0441\u044c \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f.',
+            srvAwgConfTitle: '.conf \u0434\u043b\u044f AmneziaWG \u2014 \u041a\u043b\u0438\u0435\u043d\u0442',
+            srvNetNoneHint: '\u0422\u043e\u043b\u044c\u043a\u043e VPN-\u0442\u0443\u043d\u043d\u0435\u043b\u044c. \u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u043c\u0435\u0436\u0434\u0443 \u0441\u0435\u0442\u044f\u043c\u0438 \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u2014 \u0434\u0430\u043b\u044c\u043d\u0435\u0439\u0448\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441\u0435\u0442\u0438 \u043d\u0430 \u0430\u0434\u043c\u0438\u043d\u0435. \u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0434\u0430\u043b\u0451\u043d\u043d\u043e\u043c\u0443 \u0440\u043e\u0443\u0442\u0435\u0440\u0443 \u043c\u043e\u0436\u043d\u043e \u0447\u0435\u0440\u0435\u0437 \u041f\u043e\u0441\u043b\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c\u0441\u044f \u043a \u0443\u0434\u0430\u043b\u0451\u043d\u043d\u043e\u043c\u0443 MikroTik \u0447\u0435\u0440\u0435\u0437 WinBox/SSH \u043f\u043e \u0442\u0443\u043d\u043d\u0435\u043b\u044c\u043d\u043e\u043c\u0443 IP (\u043d\u0430\u043f\u0440. 10.10.10.2).',
+            srvNetL3Hint: '\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441\u043c\u043e\u0433\u0443\u0442 \u043e\u0431\u0440\u0430\u0449\u0430\u0442\u044c\u0441\u044f \u0434\u0440\u0443\u0433 \u043a \u0434\u0440\u0443\u0433\u0443 \u043f\u043e IP \u043c\u0435\u0436\u0434\u0443 \u043f\u043e\u0434\u0441\u0435\u0442\u044f\u043c\u0438. \u0421\u0442\u0430\u043d\u0435\u0442 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e: \u0434\u043e\u0441\u0442\u0443\u043f \u043a NAS/\u0444\u0430\u0439\u043b\u043e\u0432\u044b\u043c \u0448\u0430\u0440\u0430\u043c, IP-\u043a\u0430\u043c\u0435\u0440\u0430\u043c, \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430\u043c \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443, \u0443\u0434\u0430\u043b\u0451\u043d\u043d\u044b\u0439 \u0440\u0430\u0431\u043e\u0447\u0438\u0439 \u0441\u0442\u043e\u043b (RDP/VNC), \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0440\u043e\u0443\u0442\u0435\u0440\u0430\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 WinBox/SSH, \u0438\u0433\u0440\u043e\u0432\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b, \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0443\u043c\u043d\u043e\u043c\u0443 \u0434\u043e\u043c\u0443 (Home Assistant). \u041d\u0415 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442: broadcast/multicast-\u0441\u0435\u0440\u0432\u0438\u0441\u044b \u2014 \u0430\u0432\u0442\u043e\u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 (mDNS/Bonjour, DLNA, AirPlay, Chromecast), \u0441\u0435\u0442\u0435\u0432\u043e\u0435 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u0435 Windows, \u0430\u0432\u0442\u043e\u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u043e\u0432, Wake-on-LAN. \u0412\u0430\u0436\u043d\u043e: \u043a\u0430\u0436\u0434\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0432\u0438\u0434\u0438\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0441\u0435\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440\u0430. \u0427\u0442\u043e\u0431\u044b \u043a\u043b\u0438\u0435\u043d\u0442\u044b \u0432\u0438\u0434\u0435\u043b\u0438 \u0434\u0440\u0443\u0433 \u0434\u0440\u0443\u0433\u0430, \u043d\u0443\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u044b \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u2014 \u0441\u043c. README, \u0440\u0430\u0437\u0434\u0435\u043b "\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u043c\u0435\u0436\u0434\u0443 \u043a\u043b\u0438\u0435\u043d\u0442\u0430\u043c\u0438".',
+            srvNetL2Hint: '\u0415\u0434\u0438\u043d\u0430\u044f \u0441\u0435\u0442\u044c \u2014 \u0432\u0441\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043a\u0430\u043a \u0432 \u043e\u0434\u043d\u043e\u043c \u0441\u0432\u0438\u0442\u0447\u0435. \u0420\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0412\u0421\u0401: mDNS (Bonjour), DLNA, AirPlay, Chromecast, \u0441\u0435\u0442\u0435\u0432\u043e\u0435 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u0435 Windows, \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u043e\u0432. \u041c\u0438\u043d\u0443\u0441: broadcast-\u0442\u0440\u0430\u0444\u0438\u043a \u0438\u0434\u0451\u0442 \u0447\u0435\u0440\u0435\u0437 \u0442\u0443\u043d\u043d\u0435\u043b\u044c, \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0430.',
+            srvClientNum: '\u041d\u043e\u043c\u0435\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430',
+            srvRegenerate: '\u041f\u0435\u0440\u0435\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c AWG-\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b',
+            srvOutputServer: '\u0421\u0435\u0440\u0432\u0435\u0440 (\u0433\u043e\u043b\u043e\u0432\u043d\u043e\u0439 MikroTik)',
+            srvOutputClient: '\u041a\u043b\u0438\u0435\u043d\u0442',
+            srvServerUpdate: '\u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u043d\u0430 \u0421\u0415\u0420\u0412\u0415\u0420\u0415 \u0434\u043b\u044f \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u043a\u043b\u0438\u0435\u043d\u0442\u0430',
+            srvSaveLink: '\u26a0\ufe0f \u0421\u041e\u0425\u0420\u0410\u041d\u0418\u0422\u0415 \u042d\u0422\u0423 \u0421\u0421\u042b\u041b\u041a\u0423 \u0414\u041b\u042f \u0411\u0423\u0414\u0423\u0429\u0415\u0419 \u0414\u041e\u041d\u0410\u0421\u0422\u0420\u041e\u0419\u041a\u0418',
+            srvEnvHint: '\u042d\u0442\u0430 \u0441\u0441\u044b\u043b\u043a\u0430 \u0442\u0430\u043a\u0436\u0435 \u0431\u0443\u0434\u0435\u0442 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0430 \u0432 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 AWG_CONFIG_URL. \u0427\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c: /container/envs/print where key=AWG_CONFIG_URL',
+            srvAddClientBanner: '\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u043e\u0432\u043e\u0433\u043e \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443',
+            pHub1: 'awg-server-1 \u2014 1-\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447.)',
+            pHub2: 'awg-server-2 \u2014 2-\u0439 \u0441\u0435\u0440\u0432\u0435\u0440',
+            errSrvEndpoint: '\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0439 IP \u0438\u043b\u0438 \u0434\u043e\u043c\u0435\u043d \u0441\u0435\u0440\u0432\u0435\u0440\u0430.',
+            errSrvTunnelSubnet: '\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u043f\u043e\u0434\u0441\u0435\u0442\u044c. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 /24 CIDR (\u043d\u0430\u043f\u0440. 10.10.10.0/24).',
+            errSrvLanRequired: '\u0414\u043b\u044f \u043e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0441\u0435\u0442\u0435\u0439 \u043d\u0443\u0436\u043d\u044b LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u0438 \u043e\u0431\u0435\u0438\u0445 \u0441\u0442\u043e\u0440\u043e\u043d.',
+            errLanSubnet: '\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442 LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u0438. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 CIDR, \u043d\u0430\u043f\u0440. 192.168.1.0/24.',
+            errTunnelSubnet: '\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u043f\u043e\u0434\u0441\u0435\u0442\u044c. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 /30 CIDR (\u043d\u0430\u043f\u0440. 10.0.0.0/30).',
+            errAwgPort: 'AWG-\u043f\u043e\u0440\u0442 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c 1024\u201365535.',
+            scenarioNonRuWarn: '\u0412\u041d\u0418\u041c\u0410\u041d\u0418\u0415: \u0412\u0435\u0441\u044c \u0437\u0430\u0440\u0443\u0431\u0435\u0436\u043d\u044b\u0439 \u0442\u0440\u0430\u0444\u0438\u043a \u043f\u043e\u0439\u0434\u0451\u0442 \u0447\u0435\u0440\u0435\u0437 VPN. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0432\u0430\u0448 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u044b\u0434\u0435\u0440\u0436\u0438\u0442 \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0443. \u0411\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u043e VPS \u0438\u043c\u0435\u044e\u0442 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u043d\u044b\u0439 \u0442\u0440\u0430\u0444\u0438\u043a. \u0422\u0438\u043f\u0438\u0447\u043d\u043e\u0435 \u0434\u043e\u043c\u043e\u0445\u043e\u0437\u044f\u0439\u0441\u0442\u0432\u043e \u0441 \u0432\u0438\u0434\u0435\u043e\u0441\u0435\u0440\u0432\u0438\u0441\u0430\u043c\u0438 \u043b\u0435\u0433\u043a\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u043b\u044f\u0435\u0442 10+ \u0422\u0411/\u043c\u0435\u0441. \u0412\u0415\u0421\u042c \u0442\u043e\u0440\u0440\u0435\u043d\u0442-\u0442\u0440\u0430\u0444\u0438\u043a \u0442\u043e\u0436\u0435 \u043f\u043e\u0439\u0434\u0451\u0442 \u0447\u0435\u0440\u0435\u0437 \u0442\u0443\u043d\u043d\u0435\u043b\u044c (\u0430 \u044d\u0442\u043e \u041e\u0427\u0415\u041d\u042c \u043c\u043d\u043e\u0433\u043e \u0434\u0430\u043d\u043d\u044b\u0445). \u0418\u0433\u0440\u043e\u0432\u043e\u0439 \u0442\u0440\u0430\u0444\u0438\u043a \u0442\u043e\u0436\u0435 \u043c\u043e\u0436\u0435\u0442 \u043b\u0435\u0433\u043a\u043e \u0437\u0430\u0432\u0435\u0440\u043d\u0443\u0442\u044c\u0441\u044f \u0442\u0443\u0434\u0430. \u0412\u044b\u0431\u0438\u0440\u0430\u0439\u0442\u0435 \u044d\u0442\u043e\u0442 \u0432\u0430\u0440\u0438\u0430\u043d\u0442, \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 \u043d\u0430 100% \u0443\u0432\u0435\u0440\u0435\u043d\u044b. \u0421\u043f\u0438\u0441\u043e\u043a RU-\u0430\u0434\u0440\u0435\u0441\u043e\u0432 \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u0442\u0441\u044f \u0432 RAM (\u043d\u0435 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430 flash). .rsc-\u0444\u0430\u0439\u043b (~1\u041c\u0411) \u0445\u0440\u0430\u043d\u0438\u0442\u0441\u044f \u043d\u0430 USB (\u0435\u0441\u043b\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d), \u0438\u043d\u0430\u0447\u0435 \u043d\u0430 disk1 (\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f >= 1.5\u041c\u0411 \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u043e).',
+            btnExpand: '\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0432\u0441\u0451',
+            btnCollapse: '\u0421\u0432\u0435\u0440\u043d\u0443\u0442\u044c',
+            dohForwarderLabel: 'DNS-\u0444\u043e\u0440\u0432\u0430\u0440\u0434\u0435\u0440 (DoH)',
+            dohCustom: '\u0421\u0432\u043e\u0439...',
+            dohHint: '\u0412\u0441\u0435 \u0441\u0435\u0440\u0432\u0438\u0441\u044b \u0431\u0443\u0434\u0443\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0433\u043e DNS-over-HTTPS (DoH) \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0434\u043b\u044f \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u044f \u0434\u043e\u043c\u0435\u043d\u043e\u0432.',
+            dnsBrowserHint: '\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 DNS (DoH) \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435.\nChrome: \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u2192 \u041a\u043e\u043d\u0444\u0438\u0434\u0435\u043d\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u2192 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c \u2192 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 DNS \u2192 \u0412\u044b\u043a\u043b\nFirefox: \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u2192 \u041f\u0440\u0438\u0432\u0430\u0442\u043d\u043e\u0441\u0442\u044c \u2192 DNS \u0447\u0435\u0440\u0435\u0437 HTTPS \u2192 \u0412\u044b\u043a\u043b\nEdge: \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u2192 \u041a\u043e\u043d\u0444\u0438\u0434\u0435\u043d\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u2192 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c \u2192 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 DNS \u2192 \u0412\u044b\u043a\u043b\n\u0418\u043d\u0430\u0447\u0435 MikroTik \u043d\u0435 \u0443\u0432\u0438\u0434\u0438\u0442 \u0432\u0430\u0448\u0438 DNS-\u0437\u0430\u043f\u0440\u043e\u0441\u044b \u0438 \u043d\u0435 \u0441\u043c\u043e\u0436\u0435\u0442 \u043d\u0430\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u0442\u0440\u0430\u0444\u0438\u043a.',
+            igTitle: '\u041a\u0430\u043a \u044d\u0442\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442',
+            igWireguard: '\u0422\u0443\u043d\u043d\u0435\u043b\u044c WireGuard',
+            igContainer: '\u041a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 (awg-proxy)',
+            igContainerOnRouter: '\u041a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 \u043d\u0430 \u0440\u043e\u0443\u0442\u0435\u0440\u0435',
+            igStandalone: '\u0412\u043d\u0435\u0448\u043d\u0435\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e (awg-proxy)',
+            igDns: 'DNS (DoH)',
+            igRouting: '\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f',
+            igFirewall: 'Firewall / NAT',
+            igMangle: 'Mangle',
+            igInternet: '\u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442',
+            igInternetWan: '\u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 (WAN)',
+            igLan: 'LAN-\u043a\u043b\u0438\u0435\u043d\u0442\u044b',
+            igAwgServer: 'AWG-\u0441\u0435\u0440\u0432\u0435\u0440',
+            igRuList: '\u0421\u043f\u0438\u0441\u043e\u043a RU IP',
+            igScheduler: '\u041f\u043b\u0430\u043d\u0438\u0440\u043e\u0432\u0449\u0438\u043a',
+            igManualConfig: '\u0420\u0443\u0447\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430',
+            igRouterA: 'MikroTik A (\u043a\u043b\u0438\u0435\u043d\u0442)',
+            igRouterB: 'MikroTik B (\u0441\u0435\u0440\u0432\u0435\u0440)',
+            igServerHub: 'MikroTik-\u0441\u0435\u0440\u0432\u0435\u0440 (\u0445\u0430\u0431)',
+            igClientN: 'MikroTik-\u043a\u043b\u0438\u0435\u043d\u0442 #N',
+            igContainerClient: 'awg-proxy (\u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f)',
+            igContainerServer: 'awg-proxy (\u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442)',
+            igClient2: 'MikroTik-\u043a\u043b\u0438\u0435\u043d\u0442 #2',
+            igTipWireguard: '\u0422\u0443\u043d\u043d\u0435\u043b\u044c WireGuard\n\n/interface/wireguard/add \u2014 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 WireGuard-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0441 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u043c \u043a\u043b\u044e\u0447\u043e\u043c \u0438 \u043f\u043e\u0440\u0442\u043e\u043c\n/interface/wireguard/peers/add \u2014 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043f\u0438\u0440 (AWG-\u0441\u0435\u0440\u0432\u0435\u0440) \u0441 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u043c \u043a\u043b\u044e\u0447\u043e\u043c \u0438 endpoint\u2019\u043e\u043c \u043d\u0430 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\n/ip/address/add \u2014 \u043d\u0430\u0437\u043d\u0430\u0447\u0430\u0435\u0442 \u0442\u0443\u043d\u043d\u0435\u043b\u044c\u043d\u044b\u0439 IP\n\n\u042d\u0442\u043e \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0442\u0443\u043d\u043d\u0435\u043b\u044c, \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0442\u0440\u0430\u0444\u0438\u043a \u0438\u0434\u0451\u0442 \u043a AWG-\u0441\u0435\u0440\u0432\u0435\u0440\u0443.',
+            igTipContainer: '\u041a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 (awg-proxy)\n\n/interface/veth/add \u2014 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u0443\u044e Ethernet-\u043f\u0430\u0440\u0443 \u0434\u043b\u044f \u0441\u0432\u044f\u0437\u0438 \u0441 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u043e\u043c\n/container/envs/add \u2014 \u0437\u0430\u0434\u0430\u0451\u0442 AWG-\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b (Jc, Jmin, Jmax, S1, S2, H1\u2013H4, \u043a\u043b\u044e\u0447\u0438)\n/container/add + /container/start \u2014 \u0441\u043a\u0430\u0447\u0438\u0432\u0430\u0435\u0442 \u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\n\nawg-proxy \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u0443\u0435\u0442 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0435 WG-\u043f\u0430\u043a\u0435\u0442\u044b \u0432 \u0444\u043e\u0440\u043c\u0430\u0442 AmneziaWG (\u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 junk, \u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438, \u0434\u043e\u043f\u043e\u043b\u043d\u044f\u0435\u0442 handshake).',
+            igTipContainerOnRouter: '\u041a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 \u043d\u0430 \u0440\u043e\u0443\u0442\u0435\u0440\u0435\n\n\u041a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 awg-proxy \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u044f\u043c\u043e \u043d\u0430 \u0440\u043e\u0443\u0442\u0435\u0440\u0435 MikroTik.\n\n/interface/veth/add \u2014 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u0443\u044e Ethernet-\u043f\u0430\u0440\u0443\n/container/envs/add \u2014 \u0437\u0430\u0434\u0430\u0451\u0442 AWG-\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b (Jc, Jmin, Jmax, S1, S2, H1\u2013H4, \u043a\u043b\u044e\u0447\u0438)\n/container/add + /container/start \u2014 \u0441\u043a\u0430\u0447\u0438\u0432\u0430\u0435\u0442 \u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\n\nawg-proxy \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u0443\u0435\u0442 WG-\u043f\u0430\u043a\u0435\u0442\u044b \u0432 \u0444\u043e\u0440\u043c\u0430\u0442 AmneziaWG.',
+            igTipStandalone: '\u0412\u043d\u0435\u0448\u043d\u0435\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e (awg-proxy)\n\nawg-proxy \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 (Raspberry Pi, VPS \u0438 \u0442.\u0434.). WireGuard-\u043f\u0438\u0440 \u043d\u0430 MikroTik \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 endpoint \u043d\u0430 IP \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.\n\n\u041f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u0443\u0435\u0442 WG-\u0442\u0440\u0430\u0444\u0438\u043a \u0432 \u0444\u043e\u0440\u043c\u0430\u0442 AmneziaWG \u043f\u0435\u0440\u0435\u0434 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u043e\u0439 \u043d\u0430 AWG-\u0441\u0435\u0440\u0432\u0435\u0440.',
+            igTipDns: 'DNS (DoH-\u0444\u043e\u0440\u0432\u0430\u0440\u0434\u0438\u043d\u0433)\n\n/ip/dns/static/add type=FWD \u2014 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u043f\u0435\u0440\u0435\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u0434\u043e\u043c\u0435\u043d\u043e\u0432 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0445 \u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432\n\n\u041a\u043e\u0433\u0434\u0430 \u043a\u043b\u0438\u0435\u043d\u0442 \u0440\u0435\u0437\u043e\u043b\u0432\u0438\u0442 \u0434\u043e\u043c\u0435\u043d (\u043d\u0430\u043f\u0440. youtube.com), MikroTik \u043f\u0435\u0440\u0435\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441 \u0447\u0435\u0440\u0435\u0437 DoH \u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 IP \u0432 address-list.\n\u042d\u0442\u0438 address-list\u2019\u044b \u043f\u0435\u0440\u0435\u0434\u0430\u044e\u0442\u0441\u044f \u0432 Mangle, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u0430\u0440\u043a\u0438\u0440\u0443\u0435\u0442 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0442\u0440\u0430\u0444\u0438\u043a \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0447\u0435\u0440\u0435\u0437 VPN.',
+            igTipRouting: '\u041f\u043e\u043b\u0438\u0442\u0438\u043a\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438\n\n/routing/table/add \u2014 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0443\u044e \u0442\u0430\u0431\u043b\u0438\u0446\u0443 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u043e\u0432 \u0434\u043b\u044f VPN\n/ip/route/add \u2014 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0447\u0435\u0440\u0435\u0437 WG-\u0442\u0443\u043d\u043d\u0435\u043b\u044c\n/routing/rule/add \u2014 \u043f\u0440\u0438\u0432\u044f\u0437\u044b\u0432\u0430\u0435\u0442 \u043c\u0435\u0442\u043a\u0443 \u043a \u0442\u0430\u0431\u043b\u0438\u0446\u0435\n\n\u041c\u0430\u0440\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 Mangle \u0442\u0440\u0430\u0444\u0438\u043a \u0438\u0434\u0451\u0442 \u0447\u0435\u0440\u0435\u0437 \u044d\u0442\u0443 \u0442\u0430\u0431\u043b\u0438\u0446\u0443 \u0432 VPN. \u041e\u0441\u0442\u0430\u043b\u044c\u043d\u043e\u0439 \u2014 \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e.',
+            igTipFirewall: 'Firewall / NAT\n\n/ip/firewall/nat/add chain=srcnat masquerade src-address=172.18.0.0/30 \u2014 NAT \u0442\u0440\u0430\u0444\u0438\u043a\u0430 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430\n\n\u041f\u0440\u0430\u0432\u0438\u043b\u043e masquerade out-interface \u0434\u043b\u044f WG-\u0442\u0443\u043d\u043d\u0435\u043b\u044f \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u0440\u0438 \u0432\u044b\u0431\u043e\u0440\u0435 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u044f.',
+            igTipMangle: 'Mangle (\u043c\u0430\u0440\u043a\u0438\u0440\u043e\u0432\u043a\u0430 \u0442\u0440\u0430\u0444\u0438\u043a\u0430)\n\n/ip/firewall/mangle/add mark-connection \u2014 \u043c\u0430\u0440\u043a\u0438\u0440\u0443\u0435\u0442 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043a IP \u0438\u0437 DNS address-list\n/ip/firewall/mangle/add mark-routing \u2014 \u043f\u0440\u0438\u0441\u0432\u0430\u0438\u0432\u0430\u0435\u0442 \u043c\u0435\u0442\u043a\u0443 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438\n/ip/firewall/mangle/add change-mss clamp-to-pmtu \u2014 \u0438\u0441\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 MSS \u0434\u043b\u044f \u0442\u0443\u043d\u043d\u0435\u043b\u044f\n\n\u042f\u0434\u0440\u043e \u0438\u0437\u0431\u0438\u0440\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438: \u0442\u043e\u043b\u044c\u043a\u043e \u0442\u0440\u0430\u0444\u0438\u043a \u043a \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u043c \u0441\u0435\u0440\u0432\u0438\u0441\u0430\u043c \u0438\u0434\u0451\u0442 \u0447\u0435\u0440\u0435\u0437 VPN.',
+            igTipMangleNonRu: 'Mangle (\u043c\u0430\u0440\u043a\u0438\u0440\u043e\u0432\u043a\u0430 \u0442\u0440\u0430\u0444\u0438\u043a\u0430)\n\n/ip/firewall/mangle/add mark-connection dst-address-list=!RU \u2014 \u043c\u0430\u0440\u043a\u0438\u0440\u0443\u0435\u0442 \u041d\u041e\u0412\u042b\u0415 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043a \u043d\u0435-\u0420\u0424 IP (\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u043d\u0430 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435)\n/ip/firewall/mangle/add mark-routing connection-mark=... \u2014 \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u0442 routing-\u043c\u0435\u0442\u043a\u0443 \u043a\u043e \u0432\u0441\u0435\u043c \u043f\u0430\u043a\u0435\u0442\u0430\u043c \u043c\u0430\u0440\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0439 (\u0431\u044b\u0441\u0442\u0440\u043e, \u0431\u0435\u0437 address-list)\n/ip/firewall/mangle/add accept dst-address-list=vpn-server \u2014 \u0438\u0441\u043a\u043b\u044e\u0447\u0430\u0435\u0442 VPN-\u0441\u0435\u0440\u0432\u0435\u0440\n/ip/firewall/mangle/add change-mss clamp-to-pmtu \u2014 \u0438\u0441\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 MSS\n\n\u0412\u0435\u0441\u044c \u043d\u0435-\u0420\u0424 \u0442\u0440\u0430\u0444\u0438\u043a \u0438\u0434\u0451\u0442 \u0447\u0435\u0440\u0435\u0437 VPN.',
+            igTipRuList: '\u0421\u043f\u0438\u0441\u043e\u043a RU IP\n\n\u0421\u043a\u0430\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0441 GitHub, \u0445\u0440\u0430\u043d\u0438\u0442\u0441\u044f \u043d\u0430 USB/\u0434\u0438\u0441\u043a\u0435. \u0417\u0430\u043f\u0438\u0441\u0438 \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u044e\u0442\u0441\u044f \u0432 RAM \u0441 timeout 30 \u0434\u043d\u0435\u0439 (\u043d\u0435 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u044e\u0442\u0441\u044f \u043d\u0430 flash). \u041f\u0435\u0440\u0435\u0438\u043c\u043f\u043e\u0440\u0442 \u043f\u0440\u0438 \u043a\u0430\u0436\u0434\u043e\u0439 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0435 + \u0435\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u043e.\n\u041f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0435 \u0441\u0435\u0442\u0438 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u043a\u0430\u043a \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0435, \u0447\u0442\u043e\u0431\u044b \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0442\u0440\u0430\u0444\u0438\u043a \u043d\u0435 \u0448\u0451\u043b \u0432 \u0442\u0443\u043d\u043d\u0435\u043b\u044c.\n\u0415\u0441\u043b\u0438 scheduler \u0437\u0430\u043f\u0440\u0435\u0449\u0451\u043d device-mode, \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 (ru_ranges.rsc) \u0431\u0435\u0437 \u0430\u0432\u0442\u043e\u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f.',
+            igTipScheduler: '\u041f\u043b\u0430\u043d\u0438\u0440\u043e\u0432\u0449\u0438\u043a\n\n/system/scheduler/add \u2014 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u043f\u0438\u0441\u043a\u0430 RU \u043f\u0440\u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0435 (start-time=startup) \u0438 \u0435\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u043e (interval=1d).\n/system/script/add \u2014 \u0441\u043a\u0430\u0447\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u0435\u0436\u0438\u0439 .rsc, \u043e\u0447\u0438\u0449\u0430\u0435\u0442 \u0441\u0442\u0430\u0440\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438, \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u0442 \u043d\u043e\u0432\u044b\u0435 \u0432 RAM.\n\n\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u043f\u0438\u0441\u043e\u043a RU IP \u0432 \u0430\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438.\n\u0415\u0441\u043b\u0438 scheduler \u0437\u0430\u043f\u0440\u0435\u0449\u0451\u043d device-mode, \u043f\u043b\u0430\u043d\u0438\u0440\u043e\u0432\u0449\u0438\u043a\u0438 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u044e\u0442\u0441\u044f, \u0430 \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 (ru_ranges.rsc) \u0431\u0435\u0437 \u0430\u0432\u0442\u043e\u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f.',
+            igTipFirewallS2S: 'Firewall\n\n/ip/firewall/filter/add accept \u2014 \u0440\u0430\u0437\u0440\u0435\u0448\u0430\u0435\u0442 AWG-\u043f\u043e\u0440\u0442 \u043d\u0430 WAN\n/ip/firewall/nat/add dstnat \u2014 \u043f\u0435\u0440\u0435\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 AWG-\u0442\u0440\u0430\u0444\u0438\u043a \u0432 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\n/ip/firewall/nat/add masquerade \u2014 NAT \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 + \u0442\u0443\u043d\u043d\u0435\u043b\u044f\n\n\u0421\u0442\u043e\u0440\u043e\u043d\u0430 B (\u0441\u0435\u0440\u0432\u0435\u0440) \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0434\u043e\u043f. \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0439.',
+            igTipFirewallSrv: 'Firewall\n\n/ip/firewall/filter/add accept \u2014 \u0440\u0430\u0437\u0440\u0435\u0448\u0430\u0435\u0442 AWG-\u043f\u043e\u0440\u0442 \u043d\u0430 WAN\n/ip/firewall/nat/add dstnat \u2014 \u043f\u0435\u0440\u0435\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 AWG-\u0442\u0440\u0430\u0444\u0438\u043a \u0432 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\n/ip/firewall/nat/add masquerade \u2014 NAT \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 + \u0442\u0443\u043d\u043d\u0435\u043b\u044f\n/ip/route/add \u2014 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u044b \u043a LAN \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0447\u0435\u0440\u0435\u0437 WG-\u043f\u0438\u0440\u044b\n\n\u0421\u0435\u0440\u0432\u0435\u0440 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043e\u0442 \u0432\u0441\u0435\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0438\u0440\u0443\u0435\u0442 \u043c\u0435\u0436\u0434\u0443 \u0438\u0445 \u0441\u0435\u0442\u044f\u043c\u0438.',
+            igTipRoutingS2S: '\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f\n\n/ip/route/add \u2014 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u044b \u043a \u0443\u0434\u0430\u043b\u0451\u043d\u043d\u043e\u0439 LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u0438 \u0447\u0435\u0440\u0435\u0437 WG-\u0442\u0443\u043d\u043d\u0435\u043b\u044c\n\n\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u044b A \u043c\u043e\u0433\u0443\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0430\u0442\u044c\u0441\u044f \u0434\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0441\u0442\u043e\u0440\u043e\u043d\u044b B \u0438 \u043d\u0430\u043e\u0431\u043e\u0440\u043e\u0442.',
+            igTipRoutingTunnel: '\u0420\u0443\u0447\u043d\u0430\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f\n\n\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.\n\u0414\u043e\u0431\u0430\u0432\u044c\u0442\u0435 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u044b \u0432\u0440\u0443\u0447\u043d\u0443\u044e: /ip/route/add dst-address=X.X.X.X/X gateway=wg-awg-proxy\n\n\u041d\u0435 \u0437\u0430\u0431\u0443\u0434\u044c\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c NAT \u0434\u043b\u044f \u0442\u0443\u043d\u043d\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0442\u0440\u0430\u0444\u0438\u043a\u0430:\n/ip/firewall/nat/add chain=srcnat action=masquerade out-interface=wg-*\n\n\u0414\u043b\u044f \u043e\u043f\u044b\u0442\u043d\u044b\u0445 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0445\u043e\u0442\u044f\u0442 \u043f\u043e\u043b\u043d\u044b\u0439 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044c.',
+            igTipManualConfig: '\u0420\u0443\u0447\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\n\n\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430. \u0412\u044b \u0432\u044b\u0431\u0438\u0440\u0430\u0435\u0442\u0435, \u043a\u0430\u043a \u0442\u0440\u0430\u0444\u0438\u043a \u043f\u043e\u043f\u0430\u0434\u0430\u0435\u0442 \u0432 \u0442\u0443\u043d\u043d\u0435\u043b\u044c:\n\n\u041c\u0430\u0440\u0448\u0440\u0443\u0442 \u0434\u043b\u044f \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0445 \u043f\u043e\u0434\u0441\u0435\u0442\u0435\u0439:\n/ip/route/add dst-address=X.X.X.X/X gateway=wg-*\n\n\u0412\u0435\u0441\u044c \u0442\u0440\u0430\u0444\u0438\u043a \u0447\u0435\u0440\u0435\u0437 \u0442\u0443\u043d\u043d\u0435\u043b\u044c:\n/ip/route/add dst-address=0.0.0.0/0 gateway=wg-* distance=1\n\nPolicy-\u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u0447\u0435\u0440\u0435\u0437 mangle-\u043c\u0435\u0442\u043a\u0438\n\n\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 address-list \u0434\u043b\u044f \u0438\u0437\u0431\u0438\u0440\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438',
+            igTipInternetWan: '\u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 (WAN)\n\n\u041f\u0440\u044f\u043c\u043e\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0447\u0435\u0440\u0435\u0437 WAN-\u043f\u043e\u0440\u0442 MikroTik.\n\u0422\u0440\u0430\u0444\u0438\u043a, \u043d\u0435 \u043e\u0442\u043c\u0435\u0447\u0435\u043d\u043d\u044b\u0439 \u0434\u043b\u044f VPN, \u0438\u0434\u0451\u0442 \u0441\u044e\u0434\u0430 \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e.',
+            igTipRoutingSrv: '\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f (L3)\n\n/ip/route/add \u2014 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u044b \u043a \u0443\u0434\u0430\u043b\u0451\u043d\u043d\u044b\u043c LAN-\u043f\u043e\u0434\u0441\u0435\u0442\u044f\u043c \u0447\u0435\u0440\u0435\u0437 WireGuard-\u043f\u0438\u0440\u044b\n/ip/firewall/filter/add \u2014 \u0440\u0430\u0437\u0440\u0435\u0448\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u0441\u044b\u043b\u043a\u0443 \u043c\u0435\u0436\u0434\u0443 \u0442\u0443\u043d\u043d\u0435\u043b\u0435\u043c \u0438 LAN\n\n\u0421\u0435\u0440\u0432\u0435\u0440 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0438\u0440\u0443\u0435\u0442 \u0442\u0440\u0430\u0444\u0438\u043a \u043c\u0435\u0436\u0434\u0443 \u0432\u0441\u0435\u043c\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0451\u043d\u043d\u044b\u043c\u0438 \u0441\u0435\u0442\u044f\u043c\u0438. \u041a\u0430\u0436\u0434\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 \u043a LAN \u0441\u0435\u0440\u0432\u0435\u0440\u0430. \u0414\u043b\u044f \u0441\u0432\u044f\u0437\u0438 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043c\u0435\u0436\u0434\u0443 \u0441\u043e\u0431\u043e\u0439 \u043d\u0443\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u044b \u0432\u0440\u0443\u0447\u043d\u0443\u044e.',
+            igTipLan: 'LAN-\u043a\u043b\u0438\u0435\u043d\u0442\u044b\n\n\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438. \u0418\u0445 \u0442\u0440\u0430\u0444\u0438\u043a \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442 \u0447\u0435\u0440\u0435\u0437 MikroTik, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0440\u0435\u0448\u0430\u0435\u0442: \u043d\u0430\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u0435\u0433\u043e \u0432 VPN-\u0442\u0443\u043d\u043d\u0435\u043b\u044c \u0438\u043b\u0438 \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e \u0432 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442.',
+            igTipInternet: '\u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\n\n\u041f\u0440\u044f\u043c\u043e\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0447\u0435\u0440\u0435\u0437 WAN. \u041d\u0435-VPN \u0442\u0440\u0430\u0444\u0438\u043a \u0438\u0434\u0451\u0442 \u0441\u044e\u0434\u0430. VPN-\u0442\u0440\u0430\u0444\u0438\u043a \u2014 \u0447\u0435\u0440\u0435\u0437 WG-\u0442\u0443\u043d\u043d\u0435\u043b\u044c \u043a AWG-\u0441\u0435\u0440\u0432\u0435\u0440\u0443.',
+            igTipAwgServer: 'AWG-\u0441\u0435\u0440\u0432\u0435\u0440\n\n\u0423\u0434\u0430\u043b\u0451\u043d\u043d\u044b\u0439 VPN-\u0441\u0435\u0440\u0432\u0435\u0440 \u0441 AmneziaWG. \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043f\u0430\u043a\u0435\u0442\u044b \u043e\u0442 awg-proxy \u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0438\u0440\u0443\u0435\u0442 \u0438\u0445 \u0432 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442.\n\n\u041f\u0443\u0442\u044c: \u041a\u043b\u0438\u0435\u043d\u0442 \u2192 MikroTik WG \u2192 awg-proxy \u2192 AWG-\u0441\u0435\u0440\u0432\u0435\u0440 \u2192 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442'
+        }
+    };
+
+    function t(key) { return (I18N[currentLang] && I18N[currentLang][key]) || I18N.en[key] || key; }
+
+    function setLang(lang) {
+        // Save all radio states before i18n update
+        var allRadios = document.querySelectorAll('input[type=radio]');
+        var radioState = {};
+        for (var i = 0; i < allRadios.length; i++) {
+            if (allRadios[i].checked) radioState[allRadios[i].name] = allRadios[i].value;
+        }
+        // Save select states
+        var allSelects = document.querySelectorAll('select');
+        var selectState = {};
+        for (var i = 0; i < allSelects.length; i++) {
+            if (allSelects[i].id) selectState[allSelects[i].id] = allSelects[i].value;
+        }
+        currentLang = lang;
+        applyI18n();
+        // Restore radio states
+        for (var name in radioState) {
+            var r = document.querySelector('input[type=radio][name="' + name + '"][value="' + radioState[name] + '"]');
+            if (r) r.checked = true;
+        }
+        // Restore select states
+        for (var id in selectState) {
+            var s = document.getElementById(id);
+            if (s) s.value = selectState[id];
+        }
+        onModeChange();
+        onScenarioChange();
+        if (getDeployMode() === 'server') onSrvNetChange();
+    }
+
+    function applyI18n() {
+        var els = document.querySelectorAll('[data-i18n]');
+        for (var i = 0; i < els.length; i++) {
+            els[i].textContent = t(els[i].getAttribute('data-i18n'));
+        }
+        var titles = document.querySelectorAll('[data-i18n-title]');
+        for (var i = 0; i < titles.length; i++) {
+            titles[i].title = t(titles[i].getAttribute('data-i18n-title'));
+        }
+        var phs = document.querySelectorAll('[data-i18n-ph]');
+        for (var i = 0; i < phs.length; i++) {
+            phs[i].placeholder = t(phs[i].getAttribute('data-i18n-ph'));
+        }
+        document.title = t('title');
+        document.documentElement.lang = currentLang;
+        document.getElementById('lang-toggle').textContent = currentLang === 'ru' ? 'EN' : 'RU';
+    }
+
+    // ============================================================
+    // .conf parser
+    // ============================================================
+    function parseConf(text) {
+        var result = {interface: {}, peer: {}};
+        var section = null;
+        var lines = text.split(/\r?\n/);
+        for (var i = 0; i < lines.length; i++) {
+            var line = lines[i].trim();
+            if (!line || line[0] === '#' || line[0] === ';') continue;
+            if (line === '[Interface]') {
+                section = 'interface';
+                continue;
+            }
+            if (line === '[Peer]') {
+                section = 'peer';
+                continue;
+            }
+            if (!section) continue;
+            var eq = line.indexOf('=');
+            if (eq < 0) continue;
+            result[section][line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
+        }
+        return result;
+    }
+
+    // ============================================================
+    // Field definitions
+    // ============================================================
+    var IFACE_REQUIRED = ['PrivateKey', 'Address', 'Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'H1', 'H2', 'H3', 'H4'];
+    // AWG 3.0 keys are optional: a v2 config stays valid, and a v3 config is
+    // rendered with its extra keys instead of being flagged as unknown.
+    var IFACE_OPTIONAL = ['DNS', 'S3', 'S4', 'I1', 'I2', 'I3', 'I4', 'I5',
+        'HeaderProtectionKey', 'ContentPaddingAddition', 'RekeyAfterTime',
+        'RekeyTimeout', 'RejectAfterTime', 'KeepaliveTimeout', 'MaxHandshakeAttempts'];
+    var PEER_REQUIRED = ['PublicKey', 'Endpoint', 'AllowedIPs'];
+    var PEER_OPTIONAL = ['PresharedKey', 'PersistentKeepalive'];
+
+    // ============================================================
+    // AWG 3.0 helpers
+    // ============================================================
+    // Keys that map 1:1 onto proxy env vars. The timing ones are accepted and
+    // logged as not emulated — the proxy forwards packets as they arrive — but
+    // they are carried over so the config survives a round trip intact.
+    var V3_ENV_MAP = [
+        ['HeaderProtectionKey',    'AWG_HEADER_PROTECTION_KEY'],
+        ['ContentPaddingAddition', 'AWG_CONTENT_PADDING'],
+        ['RekeyAfterTime',         'AWG_REKEY_AFTER'],
+        ['RekeyTimeout',           'AWG_REKEY_TIMEOUT'],
+        ['RejectAfterTime',        'AWG_REJECT_AFTER'],
+        ['KeepaliveTimeout',       'AWG_KEEPALIVE_TIMEOUT'],
+        ['MaxHandshakeAttempts',   'AWG_MAX_HANDSHAKE_ATTEMPTS']
+    ];
+
+    function v3EnvLines(prefix, iface) {
+        var e = [];
+        for (var k = 0; k < V3_ENV_MAP.length; k++) {
+            var v = iface[V3_ENV_MAP[k][0]];
+            if (v) e.push('/container/envs/add list=' + prefix + '-env key=' + V3_ENV_MAP[k][1] + ' value="' + v + '"');
+        }
+        return e;
+    }
+
+    function v3ExportLines(iface) {
+        var e = [];
+        for (var k = 0; k < V3_ENV_MAP.length; k++) {
+            var v = iface[V3_ENV_MAP[k][0]];
+            if (v) e.push('  :put "export ' + V3_ENV_MAP[k][1] + '=\\"' + v + '\\""');
+        }
+        return e;
+    }
+
+    // Outer datagram = IP hdr + 8 (UDP) + S4 + 16 (header) + round_up(MTU,16)
+    // + 16 (tag). Over IPv4 the header is 20 bytes, so the stock 1420 costs
+    // 1484 + S4 and anything up to S4 = 16 still fits a 1500-byte path. Over
+    // IPv6 the header is 40, which puts the same config at 1504 + S4 — already
+    // over the limit at S4 = 0, so there the MTU always has to come down.
+    function wgMtuForS4(s4, ipv6) {
+        var n = Number(s4) || 0;
+        if (!ipv6 && n <= 16) return 0; // no mtu= needed, RouterOS default is fine
+        var m = Math.floor(((ipv6 ? 1420 : 1440) - n) / 16) * 16;
+        if (m > 1420) m = 1420;
+        if (m < 1280) m = 1280;
+        return m;
+    }
+
+    function wgMtuOpt(iface) {
+        var m = wgMtuForS4(iface.S4, ipv6Enabled());
+        return m ? ' mtu=' + m : '';
+    }
+
+    // ---- IPv6 endpoints ----
+    // The proxy only speaks IPv6 towards AWG_REMOTE, and the whole difference
+    // it makes here is 20 bytes of MTU. No DNS is done in the browser: sending
+    // the user's server name to a third-party resolver is not ours to do, so a
+    // bare hostname is left to the router (see the :resolve block below).
+
+    // "2001:db8::1", "[2001:db8::1]" or "::1". Brackets are optional here —
+    // this answers "is this an address?", not "is this a valid endpoint?".
+    function isIPv6Literal(s) {
+        s = String(s || '').trim().replace(/^\[/, '').replace(/\]$/, '');
+        if (!s || s.indexOf(':') < 0) return false;
+        if ((s.match(/::/g) || []).length > 1) return false;   // at most one ::
+        var groups = s.split(':');
+        if (groups.length < 3 || groups.length > 8) return false;
+        for (var i = 0; i < groups.length; i++) {
+            var g = groups[i];
+            if (g === '') continue;                            // part of ::
+            // The last group may be a dotted quad (::ffff:192.0.2.1)
+            if (i === groups.length - 1 && g.indexOf('.') >= 0) {
+                if (!isValidIP(g)) return false;
+                continue;
+            }
+            if (!/^[0-9a-fA-F]{1,4}$/.test(g)) return false;
+        }
+        // Without "::" every one of the eight groups must be spelled out
+        return s.indexOf('::') >= 0 || groups.length === 8;
+    }
+
+    // Wrap a host in brackets when it needs them, so host + ':' + port stays
+    // unambiguous ("2001:db8::1:51820" parses as neither).
+    function endpointJoin(host, port) {
+        var h = String(host || '').trim();
+        if (isIPv6Literal(h) && h.charAt(0) !== '[') h = '[' + h + ']';
+        return h + ':' + port;
+    }
+
+    // Strip the port from an endpoint, keeping IPv6 brackets intact.
+    function endpointHost(s) {
+        s = String(s || '').trim();
+        var m = /^\[([^\]]+)\]:\d+$/.exec(s);
+        if (m) return m[1];
+        return s.replace(/:[\d]+$/, '');
+    }
+
+    function ipv6Enabled() {
+        var el = document.getElementById('ipv6-enable');
+        return !!(el && el.checked);
+    }
+
+    // Ticking the box is a fact, not a guess, when the endpoint is a literal.
+    // For a hostname there is nothing to go on here — resolving the user's
+    // server name from the browser would hand it to a third party — so the
+    // user's own choice stands and the router asks instead.
+    function syncIpv6FromEndpoint(host) {
+        var el = document.getElementById('ipv6-enable');
+        if (!el) return;
+        host = endpointHost(host);
+        if (isIPv6Literal(host)) el.checked = true;
+        else if (isValidIP(host)) el.checked = false;
+    }
+
+    // The AAAA question the browser refuses to ask, asked on the router. Older
+    // RouterOS 7.x may not accept type= on :resolve; on-error keeps the install
+    // going and simply leaves the IPv4 MTU in place.
+    function ipv6MtuProbeLines(prefix, endpoint, s4) {
+        var host = endpointHost(endpoint);
+        if (!host || isIPLiteral(host)) return []; // literal: already decided
+        var mtu = wgMtuForS4(s4, true);
+        return [
+            '',
+            '# AAAA check: over IPv6 the header costs 20 bytes more, so a',
+            '# full-size packet no longer fits 1500 and the MTU has to come down.',
+            '# Silently skipped if this RouterOS build has no :resolve type=.',
+            ':do {',
+            '  :local a6 [:resolve ' + host + ' type=ipv6]',
+            '  :if ([:len $a6] > 0) do={',
+            '    /interface/wireguard/set [find name=wg-' + prefix + '] mtu=' + mtu,
+            '    :put "' + host + ' has an AAAA record - wg-' + prefix + ' mtu set to ' + mtu + '"',
+            '  }',
+            '} on-error={}'
+        ];
+    }
+
+    // ============================================================
+    // Render parsed fields as tags
+    // ============================================================
+    function renderFields(parsed) {
+        var proto = detectProtocol(parsed.interface);
+        var badgeColors = {
+            'v1':   'background:#e5e7eb;color:#374151',
+            'v1.5': 'background:#fef3c7;color:#92400e',
+            'v2':   'background:#dbeafe;color:#1e40af',
+            'v3':   'background:#ede9fe;color:#5b21b6'
+        };
+        var protoBadge = '<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.75rem;font-weight:600;' + badgeColors[proto] + ';margin-left:8px">AWG ' + proto + '</span>';
+        var html = '<div style="margin-bottom:8px;font-size:0.8rem;color:var(--text-muted)">' + esc(t('ifaceSec')) + protoBadge + '</div>';
+        html += '<div class="fields-grid">';
+        IFACE_REQUIRED.concat(IFACE_OPTIONAL).forEach(function (k) {
+            var v = parsed.interface[k];
+            var ok = !!v;
+            var opt = IFACE_OPTIONAL.indexOf(k) >= 0;
+            var display = v ? (v.length > 18 ? v.slice(0, 15) + '...' : v) : 'missing';
+            html += '<div class="field-tag ' + (ok || opt ? 'ok' : 'miss') + '">' +
+                '<span class="fname">' + esc(k) + '</span>' +
+                '<span class="fval">' + esc(display) + '</span></div>';
+        });
+        html += '</div>';
+
+        html += '<div style="margin:14px 0 8px;font-size:0.8rem;color:var(--text-muted)">' + esc(t('peerSec')) + '</div>';
+        html += '<div class="fields-grid">';
+        PEER_REQUIRED.concat(PEER_OPTIONAL).forEach(function (k) {
+            var v = parsed.peer[k];
+            var ok = !!v;
+            var req = PEER_REQUIRED.indexOf(k) >= 0;
+            var display = v ? (v.length > 18 ? v.slice(0, 15) + '...' : v) : 'missing';
+            html += '<div class="field-tag ' + (ok || !req ? 'ok' : 'miss') + '">' +
+                '<span class="fname">' + esc(k) + '</span>' +
+                '<span class="fval">' + esc(display) + '</span></div>';
+        });
+        html += '</div>';
+        html += renderV3Notes(parsed.interface, proto);
+        return html;
+    }
+
+    // Honest account of what a v3 config gets and does not get once it runs
+    // through a proxy that has no session keys.
+    function renderV3Notes(iface, proto) {
+        if (proto !== 'v3') return '';
+        var html = '<div class="alert alert-info" style="margin-top:14px">' +
+            '<strong>' + esc(t('v3NotesTitle')) + '</strong><br>' +
+            esc(t('v3NotesOk')) + '<br>' + esc(t('v3NotesSkip')) + '</div>';
+
+        if (iface.RejectAfterTime) {
+            var lo = Number(String(iface.RejectAfterTime).split('-')[0]);
+            if (lo && lo < 150)
+                html += '<div class="alert alert-error" style="margin-top:8px">' +
+                    esc(t('v3RejectWarn')) + '</div>';
+        }
+
+        html += '<div class="alert alert-warning" style="margin-top:8px"><strong>' +
+            esc(t('v3UpdateTitle')) + '</strong> ' + esc(t('v3UpdateBody')) + '</div>';
+
+        var v6 = ipv6Enabled();
+        var mtu = wgMtuForS4(iface.S4, v6);
+        if (mtu)
+            html += '<div class="alert alert-warning" style="margin-top:8px">' +
+                esc(t(v6 ? 'ipv6MtuNote' : 'v3MtuNote')) + ' S4=' + esc(String(iface.S4 || 0)) +
+                ' → mtu=' + mtu + '.</div>';
+        return html;
+    }
+
+    function renderAWGParamsInline(awgParams) {
+        var keys = [
+            ['level', awgParams.level], ['Jc', awgParams.jc], ['Jmin', awgParams.jmin], ['Jmax', awgParams.jmax],
+            ['S1', awgParams.s1], ['S2', awgParams.s2],
+            ['S3', awgParams.s3 || undefined], ['S4', awgParams.s4 || undefined],
+            ['H1', awgParams.h1], ['H2', awgParams.h2], ['H3', awgParams.h3], ['H4', awgParams.h4],
+            ['HeaderProtectionKey', awgParams.hpKey ? awgParams.hpKey.slice(0, 8) + '...' : undefined],
+            ['fallback', (awgParams.chain && awgParams.chain.length)
+                ? awgParams.chain.map(function (c) { return c.level; }).join(' > ') : undefined]
+        ];
+        var html = '';
+        for (var i = 0; i < keys.length; i++) {
+            if (keys[i][1] === undefined) continue;
+            html += '<div class="field-tag ok"><span class="fname">' + esc(keys[i][0]) + '</span><span class="fval">' + esc(String(keys[i][1])) + '</span></div>';
+        }
+        return html;
+    }
+
+    function showS2SParams() {
+        var p = ensureS2SParams();
+        document.getElementById('s2s-awg-params').innerHTML = renderAWGParamsInline(p);
+    }
+
+    function showSrvParams() {
+        var p = ensureServerParams();
+        document.getElementById('srv-awg-params').innerHTML = renderAWGParamsInline(p);
+    }
+
+    // ============================================================
+    // Validation helpers
+    // ============================================================
+    function isValidBase64Key(s) {
+        if (!s || s.length !== 44) return false;
+        try { return atob(s).length === 32; } catch(e) { return false; }
+    }
+
+    function isNonNegativeInt(s) {
+        return /^(0|[1-9]\d*)$/.test(s);
+    }
+
+    function isUint32(s) {
+        if (!isNonNegativeInt(s)) return false;
+        var n = Number(s);
+        return n >= 0 && n <= 4294967295;
+    }
+
+    function isHValue(s) {
+        if (isUint32(s)) return true;
+        var parts = s.split('-');
+        return parts.length === 2 && isUint32(parts[0]) && isUint32(parts[1])
+               && Number(parts[0]) <= Number(parts[1]);
+    }
+
+    function isValidIPCIDR(s) {
+        var m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$/.exec(s);
+        if (!m) return false;
+        for (var i = 1; i <= 4; i++) { if (Number(m[i]) > 255) return false; }
+        return Number(m[5]) <= 32;
+    }
+
+    function isValidEndpoint(s) {
+        s = String(s || '').trim();
+        // "[2001:db8::1]:443" — the only unambiguous IPv6 form, since splitting
+        // a bare literal on the last colon would eat part of the address.
+        var m = /^\[([^\]]+)\]:(\d+)$/.exec(s);
+        if (m) return isIPv6Literal(m[1]) && Number(m[2]) > 0 && Number(m[2]) <= 65535;
+        if (s.indexOf(':') !== s.lastIndexOf(':')) return false; // bare IPv6, needs brackets
+        var idx = s.lastIndexOf(':');
+        if (idx < 1) return false;
+        var port = Number(s.slice(idx + 1));
+        return port > 0 && port <= 65535;
+    }
+
+    function isValidIPv6CIDR(s) {
+        var m = /^([0-9a-fA-F:]+)\/(1[0-2][0-9]|[0-9]{1,2})$/.exec(s);
+        if (!m) return false;
+        if (Number(m[2]) > 128) return false;
+        var addr = m[1];
+        // reject leading/trailing single colons (but allow :: anywhere)
+        if (/^:[^:]/.test(addr) || /[^:]:$/.test(addr)) return false;
+        // must contain at least one colon
+        if (addr.indexOf(':') < 0) return false;
+        // at most one :: group
+        if ((addr.match(/::/g) || []).length > 1) return false;
+        return true;
+    }
+
+    // Split a comma-separated AllowedIPs list into trimmed, non-empty tokens.
+    // Tolerates trailing/double commas, stray whitespace and newlines that are
+    // common in large split-tunnel lists (thousands of subnets concatenated by a
+    // script), which previously failed validation as bogus empty entries.
+    function splitAllowedIPs(s) {
+        var out = [];
+        var parts = (s || '').split(',');
+        for (var i = 0; i < parts.length; i++) {
+            var p = parts[i].trim();
+            if (p) out.push(p);
+        }
+        return out;
+    }
+
+    // Returns the first token that is neither a valid IPv4 nor IPv6 CIDR, or null
+    // if every token is valid. Empty list -> '' (nothing valid to use).
+    function firstInvalidAllowedIP(s) {
+        var parts = splitAllowedIPs(s);
+        if (parts.length === 0) return '';
+        for (var i = 0; i < parts.length; i++) {
+            if (!isValidIPCIDR(parts[i]) && !isValidIPv6CIDR(parts[i])) return parts[i];
+        }
+        return null;
+    }
+
+    function isValidAllowedIPs(s) {
+        return firstInvalidAllowedIP(s) === null;
+    }
+
+    // Normalize for RouterOS: drop empty entries, collapse whitespace around commas.
+    function normalizeAllowedIPs(s) {
+        return splitAllowedIPs(s).join(',');
+    }
+
+    function isValidIP(s) {
+        var m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(s);
+        if (!m) return false;
+        for (var i = 1; i <= 4; i++) { if (Number(m[i]) > 255) return false; }
+        return true;
+    }
+
+    // Either family. Deliberately separate from isValidIP(), which still means
+    // "IPv4" for everything that ends up in /ip/dns, /ip/address and friends.
+    function isIPLiteral(s) {
+        return isValidIP(s) || isIPv6Literal(s);
+    }
+
+    function isValidDNS(s) {
+        var parts = s.split(/\s*,\s*/);
+        for (var i = 0; i < parts.length; i++) {
+            if (!isValidIP(parts[i].trim())) return false;
+        }
+        return parts.length > 0;
+    }
+
+    function isPositiveInt(s) {
+        return /^[1-9]\d*$/.test(s);
+    }
+
+    // AWG 3.0 turned PersistentKeepalive into a range, but MikroTik takes a
+    // single value — pick one inside it. A bare 25 is AmneziaWG's default and
+    // therefore a fingerprint, so it keeps being replaced by a random 30-300.
+    function randomizeKeepalive(val) {
+        var m = /^(\d+)\s*-\s*(\d+)$/.exec(String(val).trim());
+        if (m) {
+            var lo = Number(m[1]), hi = Number(m[2]);
+            if (hi < lo) hi = lo;
+            return String(lo + Math.floor(Math.random() * (hi - lo + 1)));
+        }
+        if (parseInt(val, 10) === 25) return String(Math.floor(Math.random() * 271) + 30);
+        return val;
+    }
+
+    function isValidCPSTemplate(s) {
+        if (!s || !s.trim()) return false;
+        var i = 0;
+        var count = 0;
+        while (i < s.length) {
+            if (s[i] === ' ' || s[i] === '\t' || s[i] === '\n' || s[i] === '\r') { i++; continue; }
+            if (s[i] !== '<') return false;
+            var end = s.indexOf('>', i + 1);
+            if (end < 0) return false;
+            var tag = s.slice(i + 1, end).trim();
+            if (!tag) return false;
+            var kind = tag[0];
+            if (kind === 'b') {
+                var hex = tag.slice(1).trim();
+                if (hex.length < 3 || hex.slice(0, 2).toLowerCase() !== '0x') return false;
+                if (!/^[0-9a-fA-F]*$/.test(hex.slice(2)) || hex.length < 4 || (hex.length - 2) % 2 !== 0) return false;
+            } else if (kind === 'r') {
+                if (tag.length > 1 && (tag[1] === 'c' || tag[1] === 'd')) {
+                    var n = tag.slice(2).trim();
+                    if (!/^[1-9]\d*$/.test(n)) return false;
+                } else {
+                    var n = tag.slice(1).trim();
+                    if (!/^[1-9]\d*$/.test(n)) return false;
+                }
+            } else if (kind === 'd') {
+                // AWG 3.0 tags: <d> and <ds> emit nothing, <dz N> emits N bytes
+                if (tag === 'd' || tag === 'ds') {
+                    // no payload
+                } else if (tag[1] === 'z') {
+                    var nz = tag.slice(2).trim();
+                    if (!/^[1-9]\d*$/.test(nz)) return false;
+                } else {
+                    return false;
+                }
+            } else if (kind === 't' || kind === 'c') {
+                if (tag.trim().length !== 1) return false;
+            } else {
+                return false;
+            }
+            count++;
+            i = end + 1;
+        }
+        return count > 0;
+    }
+
+    function detectProtocol(iface) {
+        // v3 is defined by the header protection key alone. It must be checked
+        // first: a v3 config almost always carries the v2 markers as well.
+        if (iface.HeaderProtectionKey) return 'v3';
+        var hasHRange = (iface.H1 && iface.H1.indexOf('-') >= 0) ||
+            (iface.H2 && iface.H2.indexOf('-') >= 0) ||
+            (iface.H3 && iface.H3.indexOf('-') >= 0) ||
+            (iface.H4 && iface.H4.indexOf('-') >= 0);
+        if (iface.S3 || iface.S4 || hasHRange) return 'v2';
+        if (iface.I1 || iface.I2 || iface.I3 || iface.I4 || iface.I5) return 'v1.5';
+        return 'v1';
+    }
+
+    // ============================================================
+    // Validate
+    // ============================================================
+    function validate(parsed) {
+        var errors = [];
+        var iface = parsed.interface;
+        var peer = parsed.peer;
+
+        // Check presence first
+        IFACE_REQUIRED.forEach(function (k) {
+            if (!iface[k]) errors.push('[Interface] ' + k + ' -- missing');
+        });
+        PEER_REQUIRED.forEach(function (k) {
+            if (!peer[k]) errors.push('[Peer] ' + k + ' -- missing');
+        });
+        if (errors.length > 0) return errors;
+
+        // Normalize Address: filter out IPv6, default mask /32
+        if (iface.Address) {
+            var addrParts = iface.Address.split(/\s*,\s*/);
+            var ipv4Addr = '';
+            for (var ai = 0; ai < addrParts.length; ai++) {
+                var ap = addrParts[ai].trim();
+                if (ap && ap.indexOf(':') < 0) { ipv4Addr = ap; break; }
+            }
+            if (ipv4Addr && ipv4Addr.indexOf('/') < 0) ipv4Addr += '/32';
+            iface.Address = ipv4Addr;
+        }
+
+        // Normalize DNS: filter out IPv6 addresses
+        if (iface.DNS) {
+            var dnsParts = iface.DNS.split(/\s*,\s*/);
+            var ipv4Dns = [];
+            for (var di = 0; di < dnsParts.length; di++) {
+                var dp = dnsParts[di].trim();
+                if (dp && dp.indexOf(':') < 0) ipv4Dns.push(dp);
+            }
+            iface.DNS = ipv4Dns.join(', ');
+        }
+
+        // Format validation
+        if (!isValidBase64Key(iface.PrivateKey))
+            errors.push('[Interface] PrivateKey -- invalid base64 key (expected 44 chars, 32 bytes)');
+        if (!isValidIPCIDR(iface.Address))
+            errors.push('[Interface] Address -- invalid format (expected X.X.X.X/N)');
+        if (iface.DNS && !isValidDNS(iface.DNS))
+            errors.push('[Interface] DNS -- invalid format (expected comma-separated IPs)');
+
+        if (!isNonNegativeInt(iface.Jc))
+            errors.push('[Interface] Jc -- must be a non-negative integer');
+        if (!isNonNegativeInt(iface.Jmin))
+            errors.push('[Interface] Jmin -- must be a non-negative integer');
+        if (!isNonNegativeInt(iface.Jmax))
+            errors.push('[Interface] Jmax -- must be a non-negative integer');
+        if (isNonNegativeInt(iface.Jmin) && isNonNegativeInt(iface.Jmax) && Number(iface.Jmin) > Number(iface.Jmax))
+            errors.push('[Interface] Jmin must be <= Jmax');
+
+        if (!isNonNegativeInt(iface.S1))
+            errors.push('[Interface] S1 -- must be a non-negative integer');
+        if (!isNonNegativeInt(iface.S2))
+            errors.push('[Interface] S2 -- must be a non-negative integer');
+        if (iface.S3 && !isNonNegativeInt(iface.S3))
+            errors.push('[Interface] S3 -- must be a non-negative integer');
+        if (iface.S4 && !isNonNegativeInt(iface.S4))
+            errors.push('[Interface] S4 -- must be a non-negative integer');
+
+        if (!isHValue(iface.H1))
+            errors.push('[Interface] H1 -- must be uint32 or range MIN-MAX');
+        if (!isHValue(iface.H2))
+            errors.push('[Interface] H2 -- must be uint32 or range MIN-MAX');
+        if (!isHValue(iface.H3))
+            errors.push('[Interface] H3 -- must be uint32 or range MIN-MAX');
+        if (!isHValue(iface.H4))
+            errors.push('[Interface] H4 -- must be uint32 or range MIN-MAX');
+
+        if (!isValidBase64Key(peer.PublicKey))
+            errors.push('[Peer] PublicKey -- invalid base64 key (expected 44 chars, 32 bytes)');
+        if (!isValidEndpoint(peer.Endpoint))
+            errors.push('[Peer] Endpoint -- invalid format (expected host:port)');
+        var badAllowedIP = firstInvalidAllowedIP(peer.AllowedIPs);
+        if (badAllowedIP !== null)
+            errors.push(badAllowedIP === ''
+                ? '[Peer] AllowedIPs -- no valid CIDR entries found'
+                : '[Peer] AllowedIPs -- invalid entry "' + (badAllowedIP.length > 40 ? badAllowedIP.slice(0, 40) + '…' : badAllowedIP) + '" (expected X.X.X.X/N or IPv6/N)');
+        if (peer.PresharedKey && !isValidBase64Key(peer.PresharedKey))
+            errors.push('[Peer] PresharedKey -- invalid base64 key (expected 44 chars, 32 bytes)');
+        // AWG 3.0 turned PersistentKeepalive into a range, so a plain integer
+        // and A-B are both legal now.
+        if (peer.PersistentKeepalive && !isHValue(peer.PersistentKeepalive))
+            errors.push('[Peer] PersistentKeepalive -- must be a positive integer or range MIN-MAX');
+
+        // --- AWG 3.0 ---
+        if (iface.HeaderProtectionKey && !isValidBase64Key(iface.HeaderProtectionKey))
+            errors.push('[Interface] HeaderProtectionKey -- invalid base64 key (expected 44 chars, 32 bytes)');
+        if (iface.HeaderProtectionKey) {
+            // The nonce is taken from the first 12 padding bytes, so every S
+            // must be at least that long — the same rule amneziawg-go enforces
+            // in uapi.go (its README says 8, the code says 12).
+            ['S1', 'S2', 'S3', 'S4'].forEach(function (k) {
+                var v = iface[k];
+                if (!v || !isNonNegativeInt(v) || Number(v) < 12)
+                    errors.push('[Interface] ' + k + ' -- must be >= 12 when HeaderProtectionKey is set (ChaCha20 nonce)');
+            });
+        }
+        ['ContentPaddingAddition', 'RekeyAfterTime', 'RekeyTimeout',
+         'RejectAfterTime', 'KeepaliveTimeout', 'MaxHandshakeAttempts'].forEach(function (k) {
+            if (iface[k] && !isHValue(iface[k]))
+                errors.push('[Interface] ' + k + ' -- must be an integer or range MIN-MAX');
+        });
+
+        ['I1','I2','I3','I4','I5'].forEach(function(k) {
+            if (iface[k] && !isValidCPSTemplate(iface[k]))
+                errors.push('[Interface] ' + k + ' -- invalid CPS template (valid tags: <b 0xHEX>, <r N>, <rc N>, <rd N>, <t>, <c>, <d>, <ds>, <dz N>)');
+        });
+
+        return errors;
+    }
+
+    // ============================================================
+    // DNS helpers
+    // ============================================================
+    function parseDNSServers(s) {
+        if (!s) return [];
+        return s.split(/\s*,\s*/).filter(function(x) { return x; });
+    }
+
+    // ============================================================
+    // Services & exclusions data
+    // ============================================================
+    var SERVICES = {
+        claude:    {list:'Claude',    d:['claude.ai','anthropic.com'], r:[], on:true},
+        discord:   {list:'Discord',   d:['discord.com','discord.gg','discordapp.com','discordapp.net','dis.gd','discord.media'], r:['discord'], on:true},
+        gemini:    {list:'Gemini',    d:['gemini.google.com','aistudio.google.com','generativelanguage.googleapis.com','deepmind.google','deepmind.com'], r:[], on:false},
+        google:    {list:'GoogleWeb', d:['google.com','googleapis.com','gstatic.com','google-analytics.com','googleusercontent.com','googlesyndication.com','doubleclick.net','google.ru'], r:[], on:false},
+        instagram: {list:'Instagram', d:['ig.me','instagram.com','cdninstagram.com','instagram.net'], r:['instagram'], on:true},
+        intel:     {list:'Intel',     d:['intel.com','intel.co.uk','intel.co.jp','intel.de','intel.fr'], r:['intel'], on:false},
+        lg:        {list:'LG',        d:['lg.com','lgappstv.com','lge.com','lgsmartad.com'], r:[], on:false},
+        meta:      {list:'META',      d:['facebook.com','fbcdn.net','fb.com','fb.me','fbsbx.com','whatsapp.com','whatsapp.net','oculus.com','oculuscdn.com','instagram.com','cdninstagram.com'], r:['facebook','instagram','oculus','whatsapp'], on:false},
+        netflix:   {list:'NetFlix',   d:['netflix.com','netflix.net','nflximg.com','nflximg.net','nflxvideo.net','nflxso.net','nflxext.com','fast.com'], r:[], on:true},
+        openai:    {list:'OpenAI',    d:['openai.com','chatgpt.com','sora.com','oaiusercontent.com','oaistatic.com'], r:[], on:true},
+        rezka:     {list:'Rezka',     d:['stream.voidboost.cc','zerocdn.com'], r:['rezka'], on:false},
+        samsung:   {list:'Samsung',   d:['samsung.com','samsungcloud.com','samsungapps.com','samsungdm.com','samsungelectronics.com'], r:[], on:false},
+        spotify:   {list:'Spotify',   d:['spotify.com','spoti.fi','scdn.co','spotifycdn.com','spotifycdn.net','byspotify.com','pscdn.co','spotify-everywhere.com','spotify.design','spotifycharts.com','spotifycodes.com','spotifyforbrands.com','spotifyjobs.com'], r:[], on:true},
+        steam:     {list:'Steam',     d:['steampowered.com','steamcommunity.com','steamstatic.com','steamcdn-a.akamaihd.net','steamgames.com','steamusercontent.com','valve.software','valvesoftware.com'], r:[], on:false},
+        telegram:  {list:'Telegram',  d:['telegram.org','t.me','telesco.pe','tdesktop.com','telegram.me','telegram.dog','telegra.ph'], r:['telegram'], on:true},
+        tiktok:    {list:'TikTok',    d:['tiktok.com','tiktokcdn.com','tiktokv.com','musical.ly','tiktokcdn-us.com','tiktokd.org','byteoversea.com','ibyteimg.com','ibytedtos.com','byteimg.com'], r:[], on:true},
+        twitter:   {list:'Twitter',   d:['twitter.com','x.com','t.co','twimg.com','twitpic.com','vine.co'], r:[], on:true},
+        youtube:   {list:'YouTube',   d:['youtube.com','youtu.be','googlevideo.com','ytimg.com','yt3.ggpht.com','yt.be','youtube-nocookie.com','youtubekids.com','youtube-ui.l.google.com'], r:['youtube','googlevideo'], on:true}
+    };
+
+    var EXCLUSIONS = {
+        steam:   {list:'Steam',      label:'Steam',       d:['steampowered.com','steamcommunity.com','steamstatic.com','steamcdn-a.akamaihd.net','steamgames.com','steamusercontent.com','valve.software','valvesoftware.com'], r:[], on:true},
+        epic:    {list:'EpicGames',   label:'Epic Games',  d:['epicgames.com','unrealengine.com','fortnite.com','epicgames.dev'], r:[], on:false},
+        blizzard:{list:'Blizzard',    label:'Battle.net',  d:['blizzard.com','battle.net','blizzard.net'], r:[], on:false},
+        apple:   {list:'Apple',       label:'Apple',       d:['apple.com','icloud.com','mzstatic.com','apple-dns.net','cdn-apple.com'], r:[], on:false},
+        microsoft:{list:'Microsoft',  label:'Microsoft',   d:['microsoft.com','windowsupdate.com','live.com','office.com','office.net','msftconnecttest.com','azure.com'], r:[], on:false},
+        wot:      {list:'WoT',        label:'World of Tanks', d:['worldoftanks.eu','worldoftanks.com','worldoftanks.asia','wargaming.net','wotblitz.com','tanki.su'], r:[], ip:[
+            '92.223.1.0/24','92.223.4.0/24','92.223.5.0/24','92.223.6.0/24',
+            '92.223.8.0/24','92.223.9.0/24','92.223.10.0/24','92.223.12.0/24',
+            '92.223.14.0/24','92.223.16.0/24','92.223.18.0/24','92.223.19.0/24',
+            '92.223.20.0/24','92.223.36.0/24','92.223.37.0/24','92.223.38.0/24',
+            '92.223.54.0/24','92.223.56.0/24',
+            '185.12.240.0/24','185.12.241.0/24','185.12.242.0/24',
+            '185.12.213.0/24','185.12.214.0/24','185.12.215.0/24',
+            '88.204.200.192/26','83.229.15.0/24','45.82.31.0/24','121.78.67.0/24'
+        ], on:false}
+    };
+
+    var DOH_FORWARDERS = [
+        {doh:'https://dns.google/dns-query', name:'GoogleDNS'},
+        {doh:'https://cloudflare-dns.com/dns-query', name:'CloudFlare'},
+        {doh:'https://dns.comss.one/mikrotik', name:'Comss'},
+        {doh:'https://dns.quad9.net/dns-query', name:'Quad9'},
+        {doh:'https://common.dot.dns.yandex.net/dns-query', name:'Yandex'}
+    ];
+
+    var DNS_STATICS = [
+        {addr:'8.8.8.8', comment:'DNS GoogleDNS', name:'dns.google'},
+        {addr:'8.8.4.4', comment:'DNS GoogleDNS', name:'dns.google'},
+        {addr:'1.1.1.1', comment:'DNS CloudFlare', name:'cloudflare-dns.com'},
+        {addr:'1.0.0.1', comment:'DNS CloudFlare', name:'cloudflare-dns.com'},
+        {addr:'195.133.25.16', comment:'DNS Comss', name:'dns.comss.one'},
+        {addr:'9.9.9.9', comment:'DNS Quad9', name:'dns.quad9.net'},
+        {addr:'149.112.112.112', comment:'DNS Quad9', name:'dns.quad9.net'},
+        {addr:'77.88.8.8', comment:'DNS Yandex', name:'common.dot.dns.yandex.net'},
+        {addr:'77.88.8.1', comment:'DNS Yandex', name:'common.dot.dns.yandex.net'}
+    ];
+
+    // Map DNS server IPs to DoH forwarder names for auto-detection
+    var DNS_IP_TO_DOH = {
+        '8.8.8.8':'GoogleDNS', '8.8.4.4':'GoogleDNS',
+        '1.1.1.1':'CloudFlare', '1.0.0.1':'CloudFlare',
+        '195.133.25.16':'Comss',
+        '9.9.9.9':'Quad9', '149.112.112.112':'Quad9',
+        '77.88.8.8':'Yandex', '77.88.8.1':'Yandex'
+    };
+
+    function detectDohFromDns(dnsString) {
+        var servers = parseDNSServers(dnsString);
+        for (var i = 0; i < servers.length; i++) {
+            var name = DNS_IP_TO_DOH[servers[i]];
+            if (name) return name;
+        }
+        return null;
+    }
+
+    function autoSelectDoh(dnsString) {
+        var detected = detectDohFromDns(dnsString);
+        if (!detected) return;
+        var radios = document.getElementsByName('doh-fwd');
+        for (var i = 0; i < radios.length; i++) {
+            if (radios[i].value === detected) {
+                radios[i].checked = true;
+                document.getElementById('doh-custom-url').style.display = 'none';
+                break;
+            }
+        }
+    }
+
+    // ============================================================
+    // Scenario helpers
+    // ============================================================
+    function getScenario() {
+        var radios = document.getElementsByName('scenario');
+        for (var i = 0; i < radios.length; i++) {
+            if (radios[i].checked) return radios[i].value;
+        }
+        return 'dns-fwd';
+    }
+
+    function onScenarioChange() {
+        var mode = getDeployMode();
+        if (mode === 'server' || mode === 'site-to-site') return;
+        var s = getScenario();
+        document.getElementById('doh-panel').style.display = (s === 'dns-fwd' || s === 'non-ru') ? 'block' : 'none';
+        document.getElementById('services-panel').style.display = s === 'dns-fwd' ? 'block' : 'none';
+        document.getElementById('exclusions-panel').style.display = s === 'non-ru' ? 'block' : 'none';
+        document.getElementById('fasttrack-warn').style.display = (s === 'dns-fwd' || s === 'non-ru') ? 'block' : 'none';
+        document.getElementById('non-ru-warn').style.display = s === 'non-ru' ? 'block' : 'none';
+        document.getElementById('netwatch-row').style.display = (s === 'dns-fwd' || s === 'non-ru') ? 'block' : 'none';
+        var hintKey = s === 'dns-fwd' ? 'scenarioDnsHint' : s === 'non-ru' ? 'scenarioNonRuHint' : 'scenarioTunnelHint';
+        var hintEl = document.getElementById('scenario-hint');
+        hintEl.setAttribute('data-i18n', hintKey);
+        if (s === 'dns-fwd') {
+            hintEl.innerHTML = t(hintKey) + ' <span class="dns-info-icon" title="' + esc(t('dnsBrowserHint')).replace(/\n/g, '&#10;') + '" data-i18n-title="dnsBrowserHint">&#9432;</span>';
+        } else {
+            hintEl.textContent = t(hintKey);
+        }
+        _igRefresh();
+    }
+
+    function initServiceCheckboxes() {
+        var grid = document.getElementById('services-grid');
+        var html = '';
+        var keys = Object.keys(SERVICES);
+        for (var i = 0; i < keys.length; i++) {
+            var k = keys[i];
+            var s = SERVICES[k];
+            html += '<label><input type="checkbox" name="svc" value="' + k + '"' + (s.on ? ' checked' : '') + '> ' + esc(s.list) + '</label>';
+        }
+        grid.innerHTML = html;
+    }
+
+    function initExclusionCheckboxes() {
+        var grid = document.getElementById('exclusions-grid');
+        var html = '';
+        var keys = Object.keys(EXCLUSIONS);
+        for (var i = 0; i < keys.length; i++) {
+            var k = keys[i];
+            var ex = EXCLUSIONS[k];
+            html += '<label><input type="checkbox" name="excl" value="' + k + '"' + (ex.on ? ' checked' : '') + '> ' + esc(ex.label) + '</label>';
+        }
+        grid.innerHTML = html;
+    }
+
+    function toggleAllServices(on) {
+        var cbs = document.querySelectorAll('input[name="svc"]');
+        for (var i = 0; i < cbs.length; i++) cbs[i].checked = on;
+    }
+
+    function getSelectedServices() {
+        var cbs = document.querySelectorAll('input[name="svc"]:checked');
+        var keys = [];
+        for (var i = 0; i < cbs.length; i++) keys.push(cbs[i].value);
+        return keys;
+    }
+
+    function getSelectedExclusions() {
+        var cbs = document.querySelectorAll('input[name="excl"]:checked');
+        var keys = [];
+        for (var i = 0; i < cbs.length; i++) keys.push(cbs[i].value);
+        return keys;
+    }
+
+    // ============================================================
+    // Custom domains
+    // ============================================================
+    var customDomains = [];
+
+    function addCustomDomain() {
+        var input = document.getElementById('custom-domain-input');
+        var domain = input.value.trim().toLowerCase();
+        if (!domain || !/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*\.[a-z]{2,}$/.test(domain)) {
+            input.style.borderColor = 'var(--error)';
+            setTimeout(function() { input.style.borderColor = ''; }, 1500);
+            return;
+        }
+        if (customDomains.indexOf(domain) >= 0) {
+            input.value = '';
+            return;
+        }
+        customDomains.push(domain);
+        input.value = '';
+        renderCustomDomains();
+    }
+
+    function removeCustomDomain(domain) {
+        var idx = customDomains.indexOf(domain);
+        if (idx >= 0) customDomains.splice(idx, 1);
+        renderCustomDomains();
+    }
+
+    function renderCustomDomains() {
+        var list = document.getElementById('custom-domains-list');
+        var html = '';
+        for (var i = 0; i < customDomains.length; i++) {
+            html += '<span class="domain-tag">' + esc(customDomains[i]) +
+                '<button onclick="removeCustomDomain(\'' + esc(customDomains[i]) + '\')">&times;</button></span>';
+        }
+        list.innerHTML = html;
+    }
+
+    function getCustomDomains() {
+        return customDomains.slice();
+    }
+
+    // ============================================================
+    // Routing scenario builders
+    // ============================================================
+    function buildFwdEntries(prefix, serviceKeys, svcMap, forwarderName) {
+        var lines = [];
+        for (var i = 0; i < serviceKeys.length; i++) {
+            var svc = svcMap[serviceKeys[i]];
+            if (!svc) continue;
+            var domains = [];
+            for (var j = 0; j < svc.d.length; j++) domains.push('"' + svc.d[j] + '"');
+            lines.push(':foreach d in={' + domains.join(';') + '} do={');
+            lines.push('  :do { /ip/dns/static/add address-list=' + svc.list + ' forward-to=' + forwarderName + ' match-subdomain=yes type=FWD name=$d comment=' + prefix + '-dns-fwd } on-error={}');
+            lines.push('}');
+            if (svc.r) {
+                for (var j = 0; j < svc.r.length; j++) {
+                    lines.push(':do { /ip/dns/static/add address-list=' + svc.list + ' forward-to=' + forwarderName + ' type=FWD regexp=' + svc.r[j] + ' comment=' + prefix + '-dns-fwd } on-error={}');
+                }
+            }
+        }
+        return lines;
+    }
+
+    function buildIpEntries(prefix, serviceKeys, svcMap) {
+        var lines = [];
+        for (var i = 0; i < serviceKeys.length; i++) {
+            var svc = svcMap[serviceKeys[i]];
+            if (!svc || !svc.ip || svc.ip.length === 0) continue;
+            var nets = [];
+            for (var j = 0; j < svc.ip.length; j++) nets.push('"' + svc.ip[j] + '"');
+            lines.push(':foreach net in={' + nets.join(';') + '} do={');
+            lines.push('  :do { /ip/firewall/address-list/add list=' + svc.list + ' address=$net comment=' + prefix + '-excl-ip } on-error={}');
+            lines.push('}');
+        }
+        return lines;
+    }
+
+    function getPrefixDistance(prefix) {
+        var map = {
+            'awg-proxy-1': 1, 'awg-proxy': 1, 'awg-proxy-2': 2, 'awg-proxy-3': 3,
+            'awg-proxy-4': 4, 'awg-proxy-5': 5
+        };
+        return map[prefix] || 1;
+    }
+
+    function buildNetwatch(prefix, commentTag) {
+        // Пиннинг зонда через route-rule: пингуем 8.8.8.8 с src-address = адрес WG-интерфейса,
+        // а /routing rule по этому src уводит пробу в отдельную probe-таблицу с /32 через ЭТОТ
+        // туннель. Таблица/маршрут/правило netwatch'ем НИКОГДА не трогаются, поэтому после
+        // disable дефолт-роута зонд по-прежнему тестирует ИМЕННО этот туннель → failback честный
+        // (нет ни вечного DOWN, ни ложного UP через чужой живой путь). Один probe-target 8.8.8.8
+        // на все туннели (каждый в своей таблице), без «магических» разных IP. Комментарий
+        // '-probe' ≠ commentTag → down/up-script (find comment=commentTag) probe-объекты не задевают.
+        // Все объекты тегированы prefix → uninstall (comment~prefix) убирает их за собой.
+        var probeTable = prefix + '-probe';
+        var lines = [];
+        lines.push('# Netwatch: auto-failover for ' + prefix + ' (probe pinned via routing-rule -> ' + probeTable + ')');
+        lines.push('{');
+        // Guard: если на WG-интерфейсе нет IPv4 (битое/ручное состояние) — не роняем блок,
+        // просто логируем и пропускаем failover для этого туннеля. В штатной установке адрес
+        // ровно один. netwatch одной строкой (без \\-переносов) — безопасно внутри :if-блока.
+        lines.push(':local wgAddr ""');
+        lines.push(':do { :set wgAddr [/ip/address/get [find where interface=wg-' + prefix + '] address] } on-error={}');
+        lines.push(':if ([:len $wgAddr] > 0) do={');
+        lines.push('  :set wgAddr [:pick $wgAddr 0 [:find $wgAddr "/"]]');
+        lines.push('  :do { /routing/table/add name=' + probeTable + ' fib comment=' + commentTag + '-probe } on-error={}');
+        lines.push('  :do { /ip/route/add dst-address=8.8.8.8/32 gateway=wg-' + prefix + ' routing-table=' + probeTable + ' scope=10 comment=' + commentTag + '-probe } on-error={}');
+        lines.push('  :do { /routing/rule/add src-address=$wgAddr action=lookup table=' + probeTable + ' comment=' + commentTag + '-probe } on-error={}');
+        lines.push('  /tool/netwatch/add host=8.8.8.8 type=icmp src-address=$wgAddr interval=15s timeout=3s comment=' + commentTag + ' down-script="/ip/route/disable [find where comment=' + commentTag + ' and dst-address=0.0.0.0/0]; :log warning \\"' + prefix + ' tunnel DOWN - route disabled\\"" up-script="/ip/route/enable [find where comment=' + commentTag + ' and dst-address=0.0.0.0/0]; :log info \\"' + prefix + ' tunnel UP - route enabled\\""');
+        lines.push('} else={ :log warning ("failover: no IPv4 on wg-' + prefix + ', netwatch skipped") }');
+        lines.push('}');
+        return lines;
+    }
+
+    function buildPolicyRouting(prefix, tableName, commentTag) {
+        var distance = getPrefixDistance(prefix);
+        return [
+            ':do { /routing/table/add name=' + tableName + ' fib comment=' + commentTag + ' } on-error={}',
+            ':do { /ip/route/add dst-address=0.0.0.0/0 gateway=wg-' + prefix + ' distance=' + distance + ' routing-table=' + tableName + ' comment=' + commentTag + ' } on-error={}',
+            ':do { /routing/rule/add routing-mark=' + tableName + ' action=lookup table=' + tableName + ' comment=' + commentTag + ' } on-error={}'
+        ];
+    }
+
+    function buildFasttrackRestore(prefix) {
+        return [
+            '  :foreach rule in=[/ip/firewall/filter/find where comment~"disabled-by-' + prefix + '"] do={',
+            '    :local c [/ip/firewall/filter/get $rule comment]',
+            '    :local origComment [:pick $c ([:find $c "|"] + 1) [:len $c]]',
+            '    /ip/firewall/filter/set $rule disabled=no comment=$origComment',
+            '  }',
+            '  :foreach rule in=[/ip/firewall/filter/find where comment~"ft-modified-by-' + prefix + '"] do={',
+            '    :local c [/ip/firewall/filter/get $rule comment]',
+            '    :local origComment [:pick $c ([:find $c "|"] + 1) [:len $c]]',
+            '    /ip/firewall/filter/set $rule out-interface-list="" comment=$origComment',
+            '  }'
+        ];
+    }
+
+    function getSelectedDohForwarder() {
+        var radios = document.getElementsByName('doh-fwd');
+        for (var i = 0; i < radios.length; i++) {
+            if (radios[i].checked) return radios[i].value;
+        }
+        return 'GoogleDNS';
+    }
+
+    function getDohForwarderInfo(forwarderName) {
+        if (forwarderName === '__custom') {
+            var url = document.getElementById('doh-custom-url').value.trim();
+            if (url.indexOf('https://') === 0) {
+                return {doh: url, name: 'CustomDoH', isDoH: true};
+            }
+            return {addr: url, name: 'CustomDNS', isDoH: false};
+        }
+        for (var i = 0; i < DOH_FORWARDERS.length; i++) {
+            if (DOH_FORWARDERS[i].name === forwarderName) return {doh: DOH_FORWARDERS[i].doh, name: DOH_FORWARDERS[i].name, isDoH: true};
+        }
+        return {doh: DOH_FORWARDERS[0].doh, name: DOH_FORWARDERS[0].name, isDoH: true};
+    }
+
+    function getDnsStaticsForForwarder(forwarderName) {
+        return DNS_STATICS.filter(function(s) { return s.comment === 'DNS ' + forwarderName; });
+    }
+
+    function buildDnsRoutingScenario(prefix, selectedServices, forwarderName) {
+        var fwd = getDohForwarderInfo(forwarderName);
+        var lines = [];
+        lines.push('');
+        lines.push('# ============================================================');
+        lines.push('# DNS-based routing for selected services');
+        lines.push('# ============================================================');
+        lines.push('{');
+        if (fwd.isDoH) {
+            lines.push('# DoH forwarder');
+            lines.push(':do { /ip/dns/forwarders/add doh-servers=' + fwd.doh + ' name=' + fwd.name + ' comment=' + prefix + '-dns-fwd } on-error={}');
+            var statics = getDnsStaticsForForwarder(fwd.name);
+            if (statics.length > 0) {
+                lines.push('# DNS static entries for DoH resolver IPs');
+                for (var i = 0; i < statics.length; i++) {
+                    lines.push(':do { /ip/dns/static/add address=' + statics[i].addr + ' comment="' + prefix + '-dns-fwd" name=' + statics[i].name + ' type=A } on-error={}');
+                }
+            }
+        } else {
+            lines.push('# Custom DNS static entry');
+            lines.push(':do { /ip/dns/static/add address=' + fwd.addr + ' comment="' + prefix + '-dns-fwd" name=' + fwd.name + ' type=A } on-error={}');
+        }
+        var fwdName = fwd.name;
+        lines.push('# FWD entries');
+        lines = lines.concat(buildFwdEntries(prefix, selectedServices, SERVICES, fwdName));
+        var cDomains = getCustomDomains();
+        if (cDomains.length > 0) {
+            lines.push('# Custom domain FWD entries');
+            var domStrs = [];
+            for (var j = 0; j < cDomains.length; j++) domStrs.push('"' + cDomains[j] + '"');
+            lines.push(':foreach d in={' + domStrs.join(';') + '} do={');
+            lines.push('  :do { /ip/dns/static/add address-list=Custom forward-to=' + fwdName + ' match-subdomain=yes type=FWD name=$d comment=' + prefix + '-dns-fwd } on-error={}');
+            lines.push('}');
+        }
+        lines.push('# Policy routing');
+        lines = lines.concat(buildPolicyRouting(prefix, prefix + '-fwd-table', prefix + '-fwd'));
+        lines.push('# NAT for tunneled traffic');
+        lines.push('/ip/firewall/nat/add chain=srcnat action=masquerade out-interface=wg-' + prefix + ' comment=' + prefix + '-fwd');
+        lines.push('# Mangle rules');
+        var listNames = [];
+        for (var i = 0; i < selectedServices.length; i++) {
+            var svc = SERVICES[selectedServices[i]];
+            if (svc) listNames.push('"' + svc.list + '"');
+        }
+        if (cDomains.length > 0) listNames.push('"Custom"');
+        lines.push(':foreach l in={' + listNames.join(';') + '} do={');
+        lines.push('  /ip/firewall/mangle/add chain=prerouting action=mark-connection new-connection-mark=' + prefix + '-fwd-conn dst-address-list=$l in-interface-list=LAN passthrough=yes comment=' + prefix + '-fwd');
+        lines.push('}');
+        lines.push('/ip/firewall/mangle/add chain=prerouting action=mark-routing new-routing-mark=' + prefix + '-fwd-table connection-mark=' + prefix + '-fwd-conn in-interface-list=LAN passthrough=no comment=' + prefix + '-fwd');
+        lines.push('# MSS clamp for WireGuard (MTU 1420)');
+        lines.push('/ip/firewall/mangle/add chain=forward action=change-mss new-mss=clamp-to-pmtu passthrough=yes tcp-flags=syn protocol=tcp out-interface=wg-' + prefix + ' comment=' + prefix + '-mss');
+        lines.push('/ip/firewall/mangle/add chain=forward action=change-mss new-mss=clamp-to-pmtu passthrough=yes tcp-flags=syn protocol=tcp in-interface=wg-' + prefix + ' comment=' + prefix + '-mss');
+        lines.push('# Configure FastTrack: only for non-VPN traffic (out-interface-list=WAN)');
+        lines.push(':foreach rule in=[/ip/firewall/filter/find where action=fasttrack-connection] do={');
+        lines.push('  :local curOut [/ip/firewall/filter/get $rule out-interface-list]');
+        lines.push('  :if ($curOut = "") do={');
+        lines.push('    :local origComment [/ip/firewall/filter/get $rule comment]');
+        lines.push('    /ip/firewall/filter/set $rule out-interface-list=WAN disabled=no comment=("ft-modified-by-' + prefix + '|" . $origComment)');
+        lines.push('  }');
+        lines.push('}');
+        if (document.getElementById('netwatch-enable').checked) {
+            lines = lines.concat(buildNetwatch(prefix, prefix + '-fwd'));
+        }
+        lines.push('}');
+        return lines;
+    }
+
+    function buildNonRuScenario(prefix, exclusionKeys, forwarderName, serverHost, storageDisk, deployMode) {
+        var fwd = getDohForwarderInfo(forwarderName);
+        var isAuto = storageDisk === '__auto';
+        var isStandalone = deployMode === 'standalone';
+        var lines = [];
+        lines.push('');
+        lines.push('# ============================================================');
+        lines.push('# Route all non-RU traffic through tunnel');
+        lines.push('# ============================================================');
+
+        // DNS forwarder + statics (needed for DoH bootstrap in non-RU scenario)
+        lines.push('{');
+        if (fwd.isDoH) {
+            lines.push('# DoH forwarder');
+            lines.push(':do { /ip/dns/forwarders/add doh-servers=' + fwd.doh + ' name=' + fwd.name + ' comment=' + prefix + '-dns-fwd } on-error={}');
+            var statics = getDnsStaticsForForwarder(fwd.name);
+            if (statics.length > 0) {
+                lines.push('# DNS static entries for DoH resolver IPs');
+                for (var i = 0; i < statics.length; i++) {
+                    lines.push(':do { /ip/dns/static/add address=' + statics[i].addr + ' comment="' + prefix + '-dns-fwd" name=' + statics[i].name + ' type=A } on-error={}');
+                }
+            }
+        } else {
+            lines.push('# Custom DNS static entry');
+            lines.push(':do { /ip/dns/static/add address=' + fwd.addr + ' comment="' + prefix + '-dns-fwd" name=' + fwd.name + ' type=A } on-error={}');
+        }
+        if (exclusionKeys.length > 0) {
+            lines.push('# FWD entries for exclusions');
+            lines = lines.concat(buildFwdEntries(prefix, exclusionKeys, EXCLUSIONS, fwd.name));
+            lines = lines.concat(buildIpEntries(prefix, exclusionKeys, EXCLUSIONS));
+        }
+
+        lines.push('# Add private/special ranges to RU list (excluded from VPN routing via dst-address-list=!RU)');
+        lines.push(':foreach net in={"10.0.0.0/8";"172.16.0.0/12";"192.168.0.0/16";"100.64.0.0/10";"169.254.0.0/16";"224.0.0.0/4"} do={');
+        lines.push('  :if ([:len [/ip/firewall/address-list/find where list=RU address=$net comment=' + prefix + '-private]] = 0) do={');
+        lines.push('    /ip/firewall/address-list/add list=RU address=$net comment=' + prefix + '-private');
+        lines.push('  }');
+        lines.push('}');
+        lines.push('# Exclude NTP and DNS servers from tunnel (prevent time-sync deadlock after reboot)');
+        lines.push(':do {');
+        lines.push('  :foreach srv in=[/system/ntp/client/get servers] do={');
+        lines.push('    :do {');
+        lines.push('      :local ip [:resolve $srv]');
+        lines.push('      :if ([:len [/ip/firewall/address-list/find where list=RU address=$ip comment=' + prefix + '-ntp]] = 0) do={');
+        lines.push('        /ip/firewall/address-list/add list=RU address=$ip comment=' + prefix + '-ntp');
+        lines.push('      }');
+        lines.push('    } on-error={}');
+        lines.push('  }');
+        lines.push('} on-error={}');
+        lines.push(':do {');
+        lines.push('  :foreach srv in=[/ip/dns/get servers] do={');
+        lines.push('    :if ($srv ~ "^[0-9]") do={');
+        lines.push('      :if ([:len [/ip/firewall/address-list/find where list=RU address=$srv comment=' + prefix + '-dns]] = 0) do={');
+        lines.push('        /ip/firewall/address-list/add list=RU address=$srv comment=' + prefix + '-dns');
+        lines.push('      }');
+        lines.push('    }');
+        lines.push('  }');
+        lines.push('} on-error={}');
+
+        if (serverHost && isIPv6Literal(serverHost)) {
+            // An IPv6 server needs no loop guard here: everything below is
+            // IPv4-only policy routing (/ip/firewall/mangle + /ip/route), so
+            // traffic to a v6 endpoint never enters the tunnel in the first
+            // place. Putting the address into /ip/firewall/address-list would
+            // simply be rejected by RouterOS.
+            lines.push('# AWG server is IPv6 (' + serverHost + ') - no loop guard needed:');
+            lines.push('# the policy routing below only ever touches IPv4 traffic.');
+        } else if (serverHost) {
+            lines.push('# Accept AWG server (direct, prevent routing loop)');
+            lines.push(':do { /ip/firewall/address-list/add address=' + serverHost + ' list=' + prefix + '-vpn-server comment=' + prefix + '-non-ru } on-error={}');
+            lines.push('/ip/firewall/mangle/add chain=prerouting action=accept dst-address-list=' + prefix + '-vpn-server in-interface-list=LAN comment=' + prefix + '-non-ru');
+        }
+        lines.push('# Policy routing');
+        lines = lines.concat(buildPolicyRouting(prefix, prefix + '-non-ru-table', prefix + '-non-ru'));
+        lines.push('# NAT for tunneled traffic');
+        lines.push('/ip/firewall/nat/add chain=srcnat action=masquerade out-interface=wg-' + prefix + ' comment=' + prefix + '-non-ru');
+        if (exclusionKeys.length > 0) {
+            lines.push('# Exclusions: accept (bypass VPN)');
+            for (var ei = 0; ei < exclusionKeys.length; ei++) {
+                var exSvc = EXCLUSIONS[exclusionKeys[ei]];
+                if (exSvc) {
+                    lines.push('/ip/firewall/mangle/add chain=prerouting action=accept dst-address-list=' + exSvc.list + ' in-interface-list=LAN comment=' + prefix + '-non-ru');
+                }
+            }
+        }
+        lines.push('# Mark non-RU connections for tunnel');
+        lines.push('/ip/firewall/mangle/add chain=prerouting action=mark-connection new-connection-mark=' + prefix + '-non-ru connection-state=new dst-address-list=!RU in-interface-list=LAN passthrough=yes comment=' + prefix + '-non-ru');
+        lines.push('/ip/firewall/mangle/add chain=prerouting action=mark-routing new-routing-mark=' + prefix + '-non-ru-table connection-mark=' + prefix + '-non-ru in-interface-list=LAN passthrough=no comment=' + prefix + '-non-ru');
+        lines.push('# MSS clamp for WireGuard (MTU 1420)');
+        lines.push('/ip/firewall/mangle/add chain=forward action=change-mss new-mss=clamp-to-pmtu passthrough=yes tcp-flags=syn protocol=tcp out-interface=wg-' + prefix + ' comment=' + prefix + '-mss');
+        lines.push('/ip/firewall/mangle/add chain=forward action=change-mss new-mss=clamp-to-pmtu passthrough=yes tcp-flags=syn protocol=tcp in-interface=wg-' + prefix + ' comment=' + prefix + '-mss');
+        lines.push('# Disable FastTrack (incompatible with policy routing)');
+        lines.push(':foreach rule in=[/ip/firewall/filter/find where action=fasttrack-connection] do={');
+        lines.push('  :local origComment [/ip/firewall/filter/get $rule comment]');
+        lines.push('  :if ([:find $origComment "disabled-by-"] < 0) do={');
+        lines.push('    /ip/firewall/filter/set $rule disabled=yes comment=("disabled-by-' + prefix + '|" . $origComment)');
+        lines.push('  }');
+        lines.push('}');
+        lines.push('}');
+
+        var scriptSrc = [];
+        scriptSrc.push(':local disk \\"disk1\\"');
+        if (isStandalone) {
+            scriptSrc.push(':do {');
+            scriptSrc.push('  :local usbFree [/disk/get [find where mount-point=\\"usb1\\"] free]');
+            scriptSrc.push('  :if (\\$usbFree > 0) do={ :set disk \\"usb1\\" }');
+            scriptSrc.push('} on-error={}');
+        } else if (isAuto) {
+            scriptSrc.push(':do { :set disk [/container/envs/get [find where list=\\"' + prefix + '-env\\" key=\\"AWG_STORAGE\\"] value] } on-error={}');
+        } else {
+            scriptSrc.push(':set disk \\"' + storageDisk + '\\"');
+        }
+        scriptSrc.push(':if (\\$disk != \\"disk1\\") do={');
+        scriptSrc.push('  :local waited 0');
+        scriptSrc.push('  :while ([:len [/disk/find where mount-point=\\$disk]] = 0 && \\$waited < 60) do={');
+        scriptSrc.push('    :delay 5s');
+        scriptSrc.push('    :set waited (\\$waited + 5)');
+        scriptSrc.push('  }');
+        scriptSrc.push('  :if ([:len [/disk/find where mount-point=\\$disk]] = 0) do={');
+        scriptSrc.push('    :log warning (\\"RU list: disk \\" . \\$disk . \\" not mounted, falling back to disk1\\")');
+        scriptSrc.push('    :set disk \\"disk1\\"');
+        scriptSrc.push('  }');
+        scriptSrc.push('}');
+        scriptSrc.push(':if (\\$disk = \\"disk1\\") do={');
+        scriptSrc.push('  :local freeSpace [/system/resource/get free-hdd-space]');
+        scriptSrc.push('  :if (\\$freeSpace < 1572864) do={');
+        scriptSrc.push('    :log error \\"RU list: insufficient disk space on disk1, need USB\\"');
+        scriptSrc.push('    :error \\"Insufficient disk space on disk1. Connect USB drive and retry.\\"');
+        scriptSrc.push('  }');
+        scriptSrc.push('}');
+        scriptSrc.push(':local schedOk true');
+        scriptSrc.push(':do { :set schedOk [/system/device-mode/get scheduler] } on-error={}');
+        scriptSrc.push(':if ([:len [/system/scheduler/find where name=\\"' + prefix + '-ru-daily\\"]] = 0) do={ :set schedOk false }');
+        scriptSrc.push(':local rscName \\"ru_ranges_timeout.rsc\\"');
+        scriptSrc.push(':if (!\\$schedOk) do={ :set rscName \\"ru_ranges.rsc\\"; :log warning \\"RU list: scheduler unavailable, using permanent list\\" }');
+        scriptSrc.push(':local rscFile (\\$disk . \\"/\\" . \\$rscName)');
+        scriptSrc.push(':local hasCache ([:len [/file/find where name=\\$rscFile]] > 0)');
+        scriptSrc.push(':if (\\$hasCache) do={');
+        scriptSrc.push('  :do { /ip/firewall/address-list/remove [find where list=RU and comment=RIPE-RU] } on-error={}');
+        scriptSrc.push('  :delay 1s');
+        scriptSrc.push('  /import file=\\$rscFile');
+        scriptSrc.push('  :log info \\"RU list: imported from cache\\"');
+        scriptSrc.push('}');
+        scriptSrc.push(':local waitNet 0');
+        scriptSrc.push(':while (\\$waitNet < 60) do={');
+        scriptSrc.push('  :do { :resolve \\"github.com\\"; :set waitNet 99 } on-error={ :delay 5s; :set waitNet (\\$waitNet + 5) }');
+        scriptSrc.push('}');
+        scriptSrc.push(':if (\\$waitNet >= 99) do={');
+        scriptSrc.push('  :do {');
+        scriptSrc.push('    /tool/fetch url=(\\"' + RU_RANGES_BASE + '\\" . \\$rscName) dst-path=\\$rscFile mode=https http-header-field=\\"User-Agent: MikroTik\\" http-max-redirect-count=10');
+        scriptSrc.push('    :delay 5s');
+        scriptSrc.push('    :log info \\"RU list: downloaded\\"');
+        scriptSrc.push('    :do { /ip/firewall/address-list/remove [find where list=RU and comment=RIPE-RU] } on-error={}');
+        scriptSrc.push('    :delay 1s');
+        scriptSrc.push('    /import file=\\$rscFile');
+        scriptSrc.push('    :log info \\"RU list: fresh version imported\\"');
+        scriptSrc.push('    :local otherName \\"ru_ranges.rsc\\"');
+        scriptSrc.push('    :if (!\\$schedOk) do={ :set otherName \\"ru_ranges_timeout.rsc\\" }');
+        scriptSrc.push('    :do { /file/remove [find where name=(\\$disk . \\"/\\" . \\$otherName)] } on-error={}');
+        scriptSrc.push('  } on-error={');
+        scriptSrc.push('    :if (!\\$hasCache) do={');
+        scriptSrc.push('      :log error \\"RU list: download failed and no cached file available\\"');
+        scriptSrc.push('    } else={');
+        scriptSrc.push('      :log warning \\"RU list: download failed, keeping cached version\\"');
+        scriptSrc.push('    }');
+        scriptSrc.push('  }');
+        scriptSrc.push('} else={');
+        scriptSrc.push('  :log warning \\"RU list: no network after 60s, skipping download\\"');
+        scriptSrc.push('}');
+
+        lines.push('# RU address list update script');
+        lines.push('/system/script/add name=' + prefix + '-ru-update comment=' + prefix + ' source="' + scriptSrc.join('\\n') + '"');
+        lines.push('# Schedulers: on boot (interval=0 required for startup trigger) + daily at 04:00');
+        lines.push('# Skipped if scheduler is not allowed by device-mode (update script falls back to permanent list)');
+        lines.push('{');
+        lines.push('  :local schedOk true');
+        lines.push('  :do { :set schedOk [/system/device-mode/get scheduler] } on-error={}');
+        lines.push('  :if ($schedOk) do={');
+        lines.push('    /system/scheduler/add name=' + prefix + '-ru-startup on-event="/system/script/run ' + prefix + '-ru-update" start-time=startup interval=0 comment=' + prefix);
+        lines.push('    /system/scheduler/add name=' + prefix + '-ru-daily on-event="/system/script/run ' + prefix + '-ru-update" start-time=04:00:00 interval=1d comment=' + prefix);
+        lines.push('  } else={');
+        lines.push('    :put "WARNING: scheduler not allowed by device-mode. RU list will be imported as permanent entries (no auto-update)."');
+        lines.push('    :put "To enable auto-updates: /system/device-mode/update scheduler=yes"');
+        lines.push('  }');
+        lines.push('}');
+        lines.push('# First run (immediate)');
+        lines.push('/system/script/run ' + prefix + '-ru-update');
+
+        if (document.getElementById('netwatch-enable').checked) {
+            lines = lines.concat(buildNetwatch(prefix, prefix + '-non-ru'));
+        }
+
+        return lines;
+    }
+
+    // ============================================================
+    // Auto-storage builder
+    // ============================================================
+    function buildAutoStorage(prefix) {
+        return [
+            '# Auto-detect storage',
+            ':global awgDisk "disk1"',
+            '{',
+            '  :local usbFree 0',
+            '  :do { :set usbFree [/disk/get [find where mount-point="usb1"] free] } on-error={}',
+            '  :if ($usbFree > 0) do={',
+            '    :set awgDisk "usb1"',
+            '    :put ("Using USB: usb1 (" . ($usbFree / 1048576) . " MB free)")',
+            '  } else={',
+            '    :if ([:len [/disk/find where slot="usb1"]] > 0) do={',
+            '      :put "USB found but not mounted. Format as ext4?"',
+            "      :put \"Press 'y' to confirm:\"",
+            '      :local key [/terminal/inkey timeout=30]',
+            '      :if ($key = 121 || $key = 89) do={',
+            '        /disk/format-drive usb1 file-system=ext4 label=usb1',
+            '        :delay 3s',
+            '        :do { :set usbFree [/disk/get [find where mount-point="usb1"] free] } on-error={}',
+            '        :if ($usbFree > 0) do={',
+            '          :set awgDisk "usb1"',
+            '        } else={ :put "Format failed, using disk1" }',
+            '      } else={ :put "Using disk1" }',
+            '    } else={ :put "No USB, using disk1" }',
+            '  }',
+            '}'
+        ];
+    }
+
+    // ============================================================
+    // Generate uninstall script as /system/script/add command (container mode)
+    // ============================================================
+    function buildUninstallScriptSource(hasDNS, storageDisk, prefix, net, scenario) {
+        var isAuto = storageDisk === '__auto';
+        var src = [];
+        src.push('/system/script/add name=' + prefix + '-uninstall comment=' + prefix + ' source={');
+        src.push('  :put "Uninstallimg AmneziaWG..."');
+        src.push('  :log info "Uninstallimg AmneziaWG..."');
+        if (hasDNS) {
+            src.push('  :local prevDNS ""');
+            src.push('  :do { :set prevDNS [/container/envs/get [find where list="' + prefix + '-env" key="AWG_PREV_DNS"] value] } on-error={}');
+        }
+        if (isAuto) {
+            src.push('  :local storageDisk "disk1"');
+            src.push('  :do { :set storageDisk [/container/envs/get [find where list="' + prefix + '-env" key="AWG_STORAGE"] value] } on-error={}');
+            src.push('  :local prevTmpdir ""');
+            src.push('  :if ($storageDisk != "disk1") do={');
+            src.push('    :do { :set prevTmpdir [/container/envs/get [find where list="' + prefix + '-env" key="AWG_PREV_TMPDIR"] value] } on-error={}');
+            src.push('  }');
+        } else if (storageDisk !== 'disk1') {
+            src.push('  :local prevTmpdir ""');
+            src.push('  :do { :set prevTmpdir [/container/envs/get [find where list="' + prefix + '-env" key="AWG_PREV_TMPDIR"] value] } on-error={}');
+        }
+        src.push('  /ip/route/remove [find where comment=' + prefix + '-tunnel]');
+        if (hasDNS) {
+            src.push('  /ip/route/remove [find where comment=' + prefix + '-dns]');
+        }
+        if (scenario !== 'tunnel') {
+            // Health-check (netwatch) гасим ПЕРВЫМ, чтобы он не дёргал маршруты во время чистки.
+            // Probe-таблицу/маршрут/правило (comment=<tag>-probe) подхватит общий prefix-sweep
+            // ниже (ip/route + routing/rule + routing/table).
+            src.push('  :do { /tool/netwatch/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/route/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/dns/static/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/dns/forwarders/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/firewall/mangle/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /routing/rule/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /routing/table/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/firewall/address-list/remove [find where list=RU and comment=RIPE-RU] } on-error={}');
+            src.push('  :do { /ip/firewall/address-list/remove [find where comment~"' + prefix + '-private"] } on-error={}');
+            src.push('  :do { /ip/firewall/address-list/remove [find where list="' + prefix + '-vpn-server"] } on-error={}');
+            src.push('  :do { /ip/firewall/address-list/remove [find where comment~"' + prefix + '-excl-ip"] } on-error={}');
+            src.push('  :do { /system/scheduler/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /system/script/remove [find where comment~"' + prefix + '" and name!="' + prefix + '-uninstall"] } on-error={}');
+            src = src.concat(buildFasttrackRestore(prefix));
+        }
+        src.push('  /container/stop [find where interface=veth-' + prefix + ']');
+        src.push('  :delay 7s');
+        src.push('  /container/remove [find where interface=veth-' + prefix + ']');
+        src.push('  /container/envs/remove [find where list="' + prefix + '-env"]');
+        if (hasDNS) {
+            src.push('  :if ($prevDNS != "") do={');
+            src.push('    :do { /ip/dns/set servers="$prevDNS" } on-error={}');
+            src.push('    :put ("DNS restored to: $prevDNS")');
+            src.push('  }');
+        }
+        if (isAuto) {
+            src.push('  :if ($storageDisk != "disk1") do={');
+            src.push('    /container/config set tmpdir=$prevTmpdir');
+            src.push('    :put ("Container tmpdir restored to: $prevTmpdir")');
+            src.push('  }');
+        } else if (storageDisk !== 'disk1') {
+            src.push('  /container/config set tmpdir=$prevTmpdir');
+            src.push('  :put ("Container tmpdir restored to: $prevTmpdir")');
+        }
+        src.push('  /ip/firewall/nat/remove [find where out-interface="wg-' + prefix + '"]');
+        src.push('  /ip/firewall/nat/remove [find where src-address="' + net.natSrc + '"]');
+        src.push('  /ip/address/remove [find where address="' + net.hostAddr + '"]');
+        src = src.concat(containerV6UninstallLines(prefix, '  '));
+        src.push('  /interface/veth/remove [find where name="veth-' + prefix + '"]');
+        src.push('  /interface/wireguard/peers/remove [find where interface="wg-' + prefix + '"]');
+        src.push('  /ip/address/remove [find where interface="wg-' + prefix + '"]');
+        src.push('  /interface/wireguard/remove [find where name="wg-' + prefix + '"]');
+        src.push('  :do { /file/remove [find where name~"' + prefix + '.+tar"] } on-error={}');
+        if (isAuto) {
+            src.push('  :do { /file/remove [find where name=($storageDisk . "/' + prefix + '")] } on-error={}');
+            src.push('  :do { /file/remove [find where name=($storageDisk . "/ru_ranges_timeout.rsc")] } on-error={}');
+            src.push('  :do { /file/remove [find where name=($storageDisk . "/ru_ranges.rsc")] } on-error={}');
+            src.push('  :if ($storageDisk != "disk1") do={');
+            src.push('    :do { /file/remove [find where name~($storageDisk . "/pull")] } on-error={}');
+            src.push('  }');
+        } else {
+            src.push('  :do { /file/remove [find where name="' + storageDisk + '/' + prefix + '"] } on-error={}');
+            src.push('  :do { /file/remove [find where name="' + storageDisk + '/ru_ranges_timeout.rsc"] } on-error={}');
+            src.push('  :do { /file/remove [find where name="' + storageDisk + '/ru_ranges.rsc"] } on-error={}');
+            if (storageDisk !== 'disk1') {
+                src.push('  :do { /file/remove [find where name~"' + storageDisk + '/pull"] } on-error={}');
+            }
+        }
+        src.push('  :put "Uninstall AmneziaWG Proxy complete!"');
+        src.push('  :log info "Uninstall AmneziaWG Proxy complete!"');
+        src.push('  /system/script/remove [find where name=' + prefix + '-uninstall]');
+        src.push('}');
+        return src.join('\n');
+    }
+
+    // ============================================================
+    // Generate standalone uninstall script (no container/veth)
+    // ============================================================
+    function buildStandaloneUninstallScriptSource(hasDNS, prefix, scenario) {
+        var src = [];
+        src.push('/system/script/add name=' + prefix + '-uninstall comment=' + prefix + ' source={');
+        src.push('  :put "Uninstalling AmneziaWG Standalone..."');
+        src.push('  :log info "Uninstalling AmneziaWG Standalone..."');
+        if (hasDNS) {
+            src.push('  :local prevDNS ""');
+            src.push('  :do { :set prevDNS [/system/script/get [find where name=' + prefix + '-prev-dns] comment] } on-error={}');
+            src.push('  :if ([:len $prevDNS] > 0) do={');
+            src.push('    :do { /ip/dns/set servers="$prevDNS" } on-error={}');
+            src.push('    :put ("DNS restored to: $prevDNS")');
+            src.push('  } else={ :put "No previous DNS to restore." }');
+            src.push('  :do { /system/script/remove [find where name=' + prefix + '-prev-dns] } on-error={}');
+        }
+        src.push('  /ip/route/remove [find where comment=' + prefix + '-tunnel]');
+        if (hasDNS) {
+            src.push('  /ip/route/remove [find where comment=' + prefix + '-dns]');
+        }
+        if (scenario !== 'tunnel') {
+            // Health-check (netwatch) гасим ПЕРВЫМ, чтобы он не дёргал маршруты во время чистки.
+            // Probe-таблицу/маршрут/правило (comment=<tag>-probe) подхватит общий prefix-sweep
+            // ниже (ip/route + routing/rule + routing/table).
+            src.push('  :do { /tool/netwatch/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/route/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/dns/static/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/dns/forwarders/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/firewall/mangle/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /routing/rule/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /routing/table/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /ip/firewall/address-list/remove [find where list=RU and comment=RIPE-RU] } on-error={}');
+            src.push('  :do { /ip/firewall/address-list/remove [find where comment~"' + prefix + '-private"] } on-error={}');
+            src.push('  :do { /ip/firewall/address-list/remove [find where list="' + prefix + '-vpn-server"] } on-error={}');
+            src.push('  :do { /ip/firewall/address-list/remove [find where comment~"' + prefix + '-excl-ip"] } on-error={}');
+            src.push('  :do { /system/scheduler/remove [find where comment~"' + prefix + '"] } on-error={}');
+            src.push('  :do { /system/script/remove [find where comment~"' + prefix + '" and name!="' + prefix + '-uninstall"] } on-error={}');
+            src = src.concat(buildFasttrackRestore(prefix));
+        }
+        src.push('  /ip/firewall/nat/remove [find where out-interface="wg-' + prefix + '"]');
+        src.push('  /interface/wireguard/peers/remove [find where interface="wg-' + prefix + '"]');
+        src.push('  /ip/address/remove [find where interface="wg-' + prefix + '"]');
+        src.push('  /interface/wireguard/remove [find where name="wg-' + prefix + '"]');
+        src.push('  :do { /file/remove [find where name="disk1/ru_ranges_timeout.rsc"] } on-error={}');
+        src.push('  :do { /file/remove [find where name="usb1/ru_ranges_timeout.rsc"] } on-error={}');
+        src.push('  :do { /file/remove [find where name="disk1/ru_ranges.rsc"] } on-error={}');
+        src.push('  :do { /file/remove [find where name="usb1/ru_ranges.rsc"] } on-error={}');
+        src.push('  :put "Uninstall AmneziaWG Proxy Standalone complete!"');
+        src.push('  :log info "Uninstall AmneziaWG Proxy Standalone complete!"');
+        src.push('  /system/script/remove [find where name=' + prefix + '-uninstall]');
+        src.push('}');
+        return src.join('\n');
+    }
+
+    // ============================================================
+    // Generate MikroTik commands (container mode)
+    // ============================================================
+    function buildCommands(parsed, storageDisk, prefix, net, scenario) {
+        var i = parsed.interface;
+        var p = parsed.peer;
+        var dnsServers = parseDNSServers(i.DNS);
+        var hasDNS = dnsServers.length > 0;
+
+        var vethIP = net.vethAddr.replace(/\/.*/, '');
+        var peerAdd = '/interface/wireguard/peers/add interface=wg-' + prefix +
+            ' public-key="' + p.PublicKey + '"';
+        if (p.PresharedKey) peerAdd += ' preshared-key="' + p.PresharedKey + '"';
+        peerAdd += ' endpoint-address=' + vethIP + ' endpoint-port=51820' +
+            ' allowed-address=' + normalizeAllowedIPs(p.AllowedIPs);
+        if (p.PersistentKeepalive) peerAdd += ' persistent-keepalive=' + randomizeKeepalive(p.PersistentKeepalive);
+
+        var detectedProto = detectProtocol(i);
+        var lines = [
+            '# ============================================================',
+            '# AWG Proxy -- MikroTik RouterOS (protocol: ' + detectedProto + ')',
+            '# ' + new Date().toISOString().slice(0, 19).replace('T', ' '),
+            '# ============================================================',
+            '',
+            '# 0. Check prerequisites',
+            ':if ([:len [/system/package/find where name="container" disabled=no]] = 0) do={',
+            '  :put "Container package is not installed or disabled. Install it and reboot."',
+            '  :error "Usage: /system/device-mode/update container=yes"',
+            '}',
+            '',
+        ];
+
+        if (storageDisk === '__auto') {
+            lines = lines.concat(buildAutoStorage(prefix));
+            lines.push('');
+        } else if (storageDisk !== 'disk1') {
+            lines = lines.concat([
+                '# Check storage device',
+                '{',
+                '  :local targetDisk "' + storageDisk + '"',
+                '  :local diskFree 0',
+                '  :do { :set diskFree [/disk/get [find where mount-point=$targetDisk] free] } on-error={}',
+                '  :if ($diskFree = 0) do={',
+                '    :local baseDisk $targetDisk',
+                '    :local dashPos [:find $targetDisk "-part"]',
+                '    :if ([:typeof $dashPos] != "nil") do={ :set baseDisk [:pick $targetDisk 0 $dashPos] }',
+                '    :if ([:len [/disk/find where slot=$baseDisk]] > 0) do={',
+                '      :put ("Device $baseDisk found but $targetDisk is not available.")',
+                '      :put "Format $baseDisk as ext4? ALL DATA WILL BE ERASED!"',
+                "      :put \"Press 'y' to confirm, any other key to cancel:\"",
+                '      :local key [/terminal/inkey timeout=30]',
+                '      :if ($key = 121 || $key = 89) do={',
+                '        :put "Formatting $baseDisk as ext4..."',
+                '        /disk/format-drive $baseDisk file-system=ext4 label=$baseDisk',
+                '        :delay 3s',
+                '        :do { :set diskFree [/disk/get [find where mount-point=$targetDisk] free] } on-error={}',
+                '        :if ($diskFree = 0) do={',
+                '          :put ("ERROR: $targetDisk not found after formatting.")',
+                '          :put "Check mount point: /disk/print"',
+                '          :error ("Mount point $targetDisk not found after formatting")',
+                '        }',
+                '        :put ("Formatted OK. Free: " . ($diskFree / 1048576) . " MB")',
+                '      } else={',
+                '        :error "Formatting cancelled"',
+                '      }',
+                '    } else={',
+                '      :put ("ERROR: Storage device $targetDisk not found.")',
+                '      :put "Check available disks: /disk/print"',
+                '      :error ("Storage not found: " . $targetDisk)',
+                '    }',
+                '  } else={',
+                '    :put ("Storage OK: $targetDisk (" . ($diskFree / 1048576) . " MB free)")',
+                '  }',
+                '}',
+                '',
+            ]);
+        }
+
+        lines.push('# 1. Uninstall script');
+
+        lines.push(buildUninstallScriptSource(hasDNS, storageDisk, prefix, net, scenario));
+
+        var net6 = ipv6Enabled() ? getTunnelNetwork6(prefix) : null;
+        lines = lines.concat([
+            '',
+            '# 2. Network infrastructure',
+            '/interface/veth/add name=veth-' + prefix + ' ' + vethAddrArgs(net, net6),
+            '/ip/address/add address=' + net.hostAddr + ' interface=veth-' + prefix,
+            '/ip/firewall/nat/add chain=srcnat action=masquerade src-address=' + net.natSrc,
+        ]);
+        if (net6) {
+            lines.push('# The AWG server is IPv6: the container needs a way out over IPv6 too.');
+            lines = lines.concat(containerV6Lines(prefix, net6, true));
+        }
+        lines = lines.concat([
+            '',
+            '# 3. WireGuard interface (MikroTik derives the public key automatically)',
+            '/interface/wireguard/add name=wg-' + prefix + ' private-key="' + i.PrivateKey + '" listen-port=' + randomWgPort() + wgMtuOpt(i) + ' disabled=yes',
+            peerAdd,
+            '/ip/address/add address=' + i.Address + ' interface=wg-' + prefix,
+        ]);
+        lines = lines.concat(ipv6MtuProbeLines(prefix, p.Endpoint, i.S4));
+        lines = lines.concat([
+            '',
+            '# 4. Container environment variables',
+            '/container/envs/add list=' + prefix + '-env key=AWG_LISTEN value=":51820"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_REMOTE value="' + p.Endpoint + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_JC value="' + i.Jc + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_JMIN value="' + i.Jmin + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_JMAX value="' + i.Jmax + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_S1 value="' + i.S1 + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_S2 value="' + i.S2 + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_H1 value="' + i.H1 + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_H2 value="' + i.H2 + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_H3 value="' + i.H3 + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_H4 value="' + i.H4 + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_SERVER_PUB value="' + p.PublicKey + '"',
+            '/container/envs/add list=' + prefix + '-env key=AWG_CLIENT_PUB value=[/interface/wireguard/get [find name=wg-' + prefix + '] public-key]',
+        ]);
+
+        if (i.S3) lines.push('/container/envs/add list=' + prefix + '-env key=AWG_S3 value="' + i.S3 + '"');
+        if (i.S4) lines.push('/container/envs/add list=' + prefix + '-env key=AWG_S4 value="' + i.S4 + '"');
+        ['I1','I2','I3','I4','I5'].forEach(function(k) {
+            if (i[k]) lines.push('/container/envs/add list=' + prefix + '-env key=AWG_' + k + ' value="' + i[k] + '"');
+        });
+        lines = lines.concat(v3EnvLines(prefix, i));
+        if (dfEnabled()) lines.push('/container/envs/add list=' + prefix + '-env key=AWG_NO_DF value="1"');
+
+        if (hasDNS) {
+            var dnsStr = dnsServers.join(',');
+            lines.push('');
+            lines.push('# 5. DNS');
+            lines.push('{ :local rawDNS [/ip/dns/get servers]; :local prevDNS ""; :foreach s in=[:toarray $rawDNS] do={ :if ([:len $prevDNS] > 0) do={ :set prevDNS "$prevDNS,$s" } else={ :set prevDNS $s } }; /container/envs/add list=' + prefix + '-env key=AWG_PREV_DNS value="$prevDNS" }');
+            lines.push('# NOTE: replaces existing DNS configuration');
+            lines.push('/ip/dns/set servers=' + dnsStr);
+            for (var d = 0; d < dnsServers.length; d++) {
+                lines.push('/ip/route/add dst-address=' + dnsServers[d] + '/32 gateway=wg-' + prefix + ' distance=1 comment=' + prefix + '-dns');
+            }
+        }
+
+        lines = lines.concat([
+            '',
+            '# ' + (hasDNS ? '6' : '5') + '. Download, create and start container',
+        ]);
+        var isAuto = storageDisk === '__auto';
+        var isExternal = storageDisk !== 'disk1' && !isAuto;
+        if (isAuto) {
+            lines.push('/container/envs/add list=' + prefix + '-env key=AWG_STORAGE value=$awgDisk');
+            lines.push(':if ($awgDisk != "disk1") do={');
+            lines.push('  :local tmpPath ($awgDisk . "/pull")');
+            lines.push('  /container/envs/add list=' + prefix + '-env key=AWG_PREV_TMPDIR value=[/container/config/get tmpdir]');
+            lines.push('  /container/config set tmpdir=$tmpPath memory-high=200M');
+            lines.push('}');
+        } else if (isExternal) {
+            lines.push(':local prevTmpdir [/container/config/get tmpdir]');
+            lines.push('/container/envs/add list=' + prefix + '-env key=AWG_PREV_TMPDIR value=$prevTmpdir');
+            lines.push('/container/config set tmpdir=' + storageDisk + '/pull memory-high=200M');
+        }
+        lines = lines.concat([
+            '{',
+            '  :local arch [/system/resource/get architecture-name]',
+            '  :local ver [/system/resource/get version]',
+            '  :local dotPos [:find $ver "."]',
+            '  :local rest [:pick $ver ($dotPos + 1) [:len $ver]]',
+            '  :local endPos [:find $rest "."]',
+            '  :if ([:typeof $endPos] = "nil") do={ :set endPos [:find $rest " "] }',
+            '  :local minor [:tonum [:pick $rest 0 $endPos]]',
+            '  :local suffix ""',
+            '  :if ($minor <= 20) do={ :set suffix "-7.20-Docker" }',
+            '  :local file ""',
+            '  :if ($arch = "arm64") do={ :set file ("awg-proxy-arm64" . $suffix . ".tar.gz") }',
+            '  :if ($arch = "arm") do={',
+            '    :set file ("awg-proxy-arm" . $suffix . ".tar.gz")',
+            '    :local rbModel ""',
+            // /system/routerboard does not exist on CHR: RouterOS fails while parsing
+            // the script, before on-error can catch anything, and the whole block dies.
+            // :parse defers the lookup to runtime, so :do handles the missing menu.
+            '    :do { :set rbModel [[:parse ":return [/system/routerboard/get model]"]] } on-error={}',
+            '    :local bName [/system/resource/get board-name]',
+            '    :if ($rbModel~"E50UG" || $rbModel~"E60iUGS" || $bName~"E50UG" || $bName~"E60iUGS" || $bName~"^hEX") do={',
+            '      :set file ("awg-proxy-armv5" . $suffix . ".tar.gz")',
+            '    }',
+            '  }',
+            '  :if ($arch ~ "x86") do={ :set file ("awg-proxy-amd64" . $suffix . ".tar.gz") }',
+            '  :if ($file = "") do={ :error "Unsupported architecture: $arch" }',
+            '  :local url "' + REPO.dl + '$file"',
+        ]);
+        if (isAuto) {
+            lines.push('  :local filePath $file');
+            lines.push('  :if ($awgDisk != "disk1") do={');
+            lines.push('    :set filePath ($awgDisk . "/pull/" . $file)');
+            lines.push('    :do { /file/make-directory ($awgDisk . "/pull") } on-error={}');
+            lines.push('  }');
+        } else if (isExternal) {
+            lines.push('  :local filePath ("' + storageDisk + '/pull/" . $file)');
+            lines.push('  :do { /file/make-directory ' + storageDisk + '/pull } on-error={}');
+        } else {
+            lines.push('  :local filePath $file');
+        }
+        lines = lines.concat([
+            '  :if ([:len [/file/find where name=$filePath]] = 0) do={',
+            '    :local freeStorage 0',
+        ]);
+        if (isAuto) {
+            lines.push('    :if ($awgDisk = "disk1") do={');
+            lines.push('      :set freeStorage [/system/resource/get free-hdd-space]');
+            lines.push('    } else={');
+            lines.push('      :do { :set freeStorage [/disk/get [find where mount-point=$awgDisk] free] } on-error={}');
+            lines.push('    }');
+        } else if (storageDisk === 'disk1') {
+            lines.push('    :set freeStorage [/system/resource/get free-hdd-space]');
+        } else {
+            lines.push('    :do { :set freeStorage [/disk/get [find where mount-point="' + storageDisk + '"] free] } on-error={}');
+        }
+        lines = lines.concat([
+            '    :if ($freeStorage < 262144) do={',
+            '      :put ("WARNING: Low disk space (" . ($freeStorage / 1024) . "KB free). Need at least 256KB.")',
+            '      :put "See: ' + REPO.base + '#insufficient-disk-space"',
+            '      :error "Insufficient disk space"',
+            '    }',
+            '    :put ("Fetching: $url -> " . $filePath)',
+            '    /tool/fetch url=$url dst-path=$filePath http-max-redirect-count=10',
+            '  } else={',
+            '    :put ("File already exists: " . $filePath)',
+            '  }',
+        ]);
+        if (isAuto) {
+            lines.push('  :local rootDir ($awgDisk . "/' + prefix + '")');
+            lines.push('  /container/add file=$filePath interface=veth-' + prefix + ' envlist=' + prefix + '-env hostname=' + prefix + ' root-dir=$rootDir logging=no start-on-boot=yes comment=' + prefix);
+        } else {
+            lines.push('  /container/add file=$filePath interface=veth-' + prefix + ' envlist=' + prefix + '-env hostname=' + prefix + ' root-dir=' + storageDisk + '/' + prefix + ' logging=no start-on-boot=yes comment=' + prefix);
+        }
+        lines = lines.concat([
+            '  :if ($minor > 20) do={ [:parse "/container/set [find where interface=veth-' + prefix + '] shm-size=4M"] }',
+            '  /file/remove $filePath',
+            '  :local freeMem [/system/resource/get free-memory]',
+            '  :if ($freeMem < 16777216) do={',
+            '    :put ("WARNING: Low memory (" . ($freeMem / 1048576) . "MB free). 16MB+ recommended.")',
+            '  }',
+            '  /container/start [find where interface=veth-' + prefix + ']',
+            '  :do { /file/remove [find where name="console-dump.txt"] } on-error={}',
+            '  :put "Waiting for container to start..."',
+            '  :delay 5s',
+            '  /interface/wireguard/enable wg-' + prefix,
+            '  :put "WireGuard interface enabled"',
+            '  :put "Installation complete!"',
+        ]);
+
+        if (hasDNS) {
+            var firstDNS = dnsServers[0];
+            lines = lines.concat([
+                '  :put "Ping test: ' + firstDNS + ' via AmneziaWG tunnel. repeat 10 times"',
+                '  /ping ' + firstDNS + ' count=10 interval=1',
+            ]);
+        }
+
+        lines = lines.concat([
+            '  :put ""',
+            '  :put "======= NEXT STEPS ======="',
+        ]);
+
+        if (isOutdatedProto(detectedProto)) {
+            lines = lines.concat([
+                '  :put ""',
+                '  :put "  [WARNING]: Your config uses AWG ' + detectedProto + '. For better obfuscation,"',
+                '  :put "  update AmneziaVPN to the latest version and regenerate the config."',
+                '  :put "  Latest version supports AWG 3.0 with header protection."',
+                '  :put "  https://github.com/amnezia-vpn/amnezia-client/releases"',
+            ]);
+        }
+
+        lines = lines.concat([
+            '  :put ""',
+            '  :put "Route specific traffic through the AmneziaWG tunnel:"',
+            '  :put ""',
+            '  :put "  Route a single host:"',
+            '  :put "    /ip/route/add dst-address=8.8.8.8/32 gateway=wg-' + prefix + ' comment=' + prefix + '-tunnel"',
+            '  :put ""',
+            '  :put "  Route a subnet:"',
+            '  :put "    /ip/route/add dst-address=100.0.0.0/8 gateway=wg-' + prefix + ' comment=' + prefix + '-tunnel"',
+            '  :put ""',
+            '  :put "  Remove a route:"',
+            '  :put "    /ip/route/remove [find where comment=' + prefix + '-tunnel dst-address=8.8.8.8/32]"',
+            '  :put ""',
+            '  :put "  Show all tunnel routes:"',
+            '  :put "    /ip/route/print where comment=' + prefix + '-tunnel"',
+            '  :put ""',
+            '  :put "  Uninstall:"',
+            '  :put "    /system/script/run ' + prefix + '-uninstall"',
+            '  :put "========================="',
+            '}',
+            '',
+            '# ' + (hasDNS ? '7' : '6') + '. Verification',
+            '# /container/print',
+            '# /interface/wireguard/print',
+            '# /interface/wireguard/peers/print',
+        ]);
+
+        lines.push('');
+
+        return lines.join('\n');
+    }
+
+    // ============================================================
+    // Generate MikroTik commands (standalone mode)
+    // ============================================================
+    function buildStandaloneCommands(parsed, proxyIP, prefix, net, scenario) {
+        var i = parsed.interface;
+        var p = parsed.peer;
+        var dnsServers = parseDNSServers(i.DNS);
+        var hasDNS = dnsServers.length > 0;
+        var detectedProto = detectProtocol(i);
+
+        var peerAdd = '/interface/wireguard/peers/add interface=wg-' + prefix +
+            ' public-key="' + p.PublicKey + '"';
+        if (p.PresharedKey) peerAdd += ' preshared-key="' + p.PresharedKey + '"';
+        peerAdd += ' endpoint-address=' + proxyIP + ' endpoint-port=51820' +
+            ' allowed-address=' + normalizeAllowedIPs(p.AllowedIPs);
+        if (p.PersistentKeepalive) peerAdd += ' persistent-keepalive=' + randomizeKeepalive(p.PersistentKeepalive);
+
+        var lines = [
+            '# ============================================================',
+            '# AWG Proxy -- MikroTik RouterOS Standalone (protocol: ' + detectedProto + ')',
+            '# Proxy device: ' + proxyIP,
+            '# ' + new Date().toISOString().slice(0, 19).replace('T', ' '),
+            '# ============================================================',
+            '',
+            '# 1. Uninstall script',
+        ];
+
+        lines.push(buildStandaloneUninstallScriptSource(hasDNS, prefix, scenario));
+
+        lines = lines.concat([
+            '',
+            '# 2. WireGuard interface',
+            '/interface/wireguard/add name=wg-' + prefix + ' private-key="' + i.PrivateKey + '" listen-port=' + randomWgPort() + wgMtuOpt(i),
+            peerAdd,
+            '/ip/address/add address=' + i.Address + ' interface=wg-' + prefix,
+        ]);
+        lines = lines.concat(ipv6MtuProbeLines(prefix, p.Endpoint, i.S4));
+        lines.push('');
+
+        var stepN = 3;
+        if (hasDNS) {
+            var dnsStr = dnsServers.join(',');
+            lines.push('# ' + stepN + '. DNS');
+            lines.push('{ :local rawDNS [/ip/dns/get servers]; :local prevDNS ""; :foreach s in=[:toarray $rawDNS] do={ :if ([:len $prevDNS] > 0) do={ :set prevDNS "$prevDNS,$s" } else={ :set prevDNS $s } }; /system/script/add name=' + prefix + '-prev-dns comment="$prevDNS" source="" }');
+            lines.push('/ip/dns/set servers=' + dnsStr);
+            for (var d = 0; d < dnsServers.length; d++) {
+                lines.push('/ip/route/add dst-address=' + dnsServers[d] + '/32 gateway=wg-' + prefix + ' distance=1 comment=' + prefix + '-dns');
+            }
+            lines.push('');
+            stepN++;
+        }
+
+        lines.push('# ' + stepN + '. Enable WireGuard');
+        lines.push('/interface/wireguard/enable wg-' + prefix);
+        lines.push('');
+        stepN++;
+
+        lines.push('# ' + stepN + '. Proxy device setup instructions');
+        lines.push('{');
+        lines.push('  :put "======= AWG PROXY SETUP ======="');
+        lines.push('  :put ""');
+        lines.push('  :put "Run awg-proxy on your external device (' + proxyIP + ')."');
+        lines.push('  :put ""');
+        lines.push('  :put "1. Download the binary for your platform:"');
+        lines.push('  :put "   Linux arm64: ' + REPO.dl + 'awg-proxy-linux-arm64"');
+        lines.push('  :put "   Linux arm:   ' + REPO.dl + 'awg-proxy-linux-arm"');
+        lines.push('  :put "   Linux armv5: ' + REPO.dl + 'awg-proxy-linux-armv5"');
+        lines.push('  :put "   Linux amd64: ' + REPO.dl + 'awg-proxy-linux-amd64"');
+        lines.push('  :put ""');
+        lines.push('  :put "2. Set environment variables and run:"');
+        lines.push('  :put "export AWG_LISTEN=\\":51820\\""');
+        lines.push('  :put "export AWG_REMOTE=\\"' + p.Endpoint + '\\""');
+        lines.push('  :put "export AWG_JC=\\"' + i.Jc + '\\""');
+        lines.push('  :put "export AWG_JMIN=\\"' + i.Jmin + '\\""');
+        lines.push('  :put "export AWG_JMAX=\\"' + i.Jmax + '\\""');
+        lines.push('  :put "export AWG_S1=\\"' + i.S1 + '\\""');
+        lines.push('  :put "export AWG_S2=\\"' + i.S2 + '\\""');
+        lines.push('  :put "export AWG_H1=\\"' + i.H1 + '\\""');
+        lines.push('  :put "export AWG_H2=\\"' + i.H2 + '\\""');
+        lines.push('  :put "export AWG_H3=\\"' + i.H3 + '\\""');
+        lines.push('  :put "export AWG_H4=\\"' + i.H4 + '\\""');
+        lines.push('  :put "export AWG_SERVER_PUB=\\"' + p.PublicKey + '\\""');
+
+        lines.push('  :put ("export AWG_CLIENT_PUB=\\"" . [/interface/wireguard/get [find name=wg-' + prefix + '] public-key] . "\\"")');
+
+        if (i.S3) lines.push('  :put "export AWG_S3=\\"' + i.S3 + '\\""');
+        if (i.S4) lines.push('  :put "export AWG_S4=\\"' + i.S4 + '\\""');
+        ['I1','I2','I3','I4','I5'].forEach(function(k) {
+            if (i[k]) lines.push('  :put "export AWG_' + k + '=\\"' + i[k] + '\\""');
+        });
+        lines = lines.concat(v3ExportLines(i));
+        if (dfEnabled()) lines.push('  :put "export AWG_NO_DF=\\"1\\""');
+
+        lines.push('  :put ""');
+        lines.push('  :put "chmod +x awg-proxy-linux-* && ./awg-proxy-linux-<arch>"');
+        lines.push('  :put ""');
+        lines.push('  :put "3. (Optional) Create a systemd service for auto-start."');
+
+        if (isOutdatedProto(detectedProto)) {
+            lines.push('  :put ""');
+            lines.push('  :put "[WARNING]: Your config uses AWG ' + detectedProto + '. For better obfuscation,"');
+            lines.push('  :put "update AmneziaVPN to the latest version and regenerate the config."');
+            lines.push('  :put "https://github.com/amnezia-vpn/amnezia-client/releases"');
+        }
+
+        lines = lines.concat([
+            '  :put ""',
+            '  :put "Route specific traffic through the AmneziaWG tunnel:"',
+            '  :put ""',
+            '  :put "  Route a single host:"',
+            '  :put "    /ip/route/add dst-address=8.8.8.8/32 gateway=wg-' + prefix + ' comment=' + prefix + '-tunnel"',
+            '  :put ""',
+            '  :put "  Route a subnet:"',
+            '  :put "    /ip/route/add dst-address=100.0.0.0/8 gateway=wg-' + prefix + ' comment=' + prefix + '-tunnel"',
+            '  :put ""',
+            '  :put "  Remove a route:"',
+            '  :put "    /ip/route/remove [find where comment=' + prefix + '-tunnel dst-address=8.8.8.8/32]"',
+            '  :put ""',
+            '  :put "  Show all tunnel routes:"',
+            '  :put "    /ip/route/print where comment=' + prefix + '-tunnel"',
+            '  :put ""',
+            '  :put "  Uninstall:"',
+            '  :put "    /system/script/run ' + prefix + '-uninstall"',
+            '  :put "================================"',
+            '}',
+            '',
+        ]);
+
+        return lines.join('\n');
+    }
+
+    // ============================================================
+    // Syntax highlight
+    // ============================================================
+    function highlight(text) {
+        return esc(text)
+            .replace(/^(# .*)$/mg, '<span class="comment">$1</span>')
+            .replace(/^(\/.+?\/\w+\/\w+)/mg, '<span class="cmd">$1</span>');
+    }
+
+    function esc(s) {
+        return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+    }
+
+    // ============================================================
+    // Mode selector
+    // ============================================================
+    function getDeployMode() {
+        var radios = document.getElementsByName('deploy-mode');
+        for (var i = 0; i < radios.length; i++) {
+            if (radios[i].checked) return radios[i].value;
+        }
+        return 'container';
+    }
+
+    function dfEnabled() {
+        var el = document.getElementById('df-enable');
+        return !!(el && el.checked);
+    }
+
+    function onModeChange() {
+        var mode = getDeployMode();
+        var isS2S = mode === 'site-to-site';
+        var isSrv = mode === 'server';
+        var hideConf = isS2S || isSrv;
+        document.getElementById('storage-row').style.display = mode === 'container' ? 'block' : 'none';
+        document.getElementById('proxy-ip-row').style.display = mode === 'standalone' ? 'block' : 'none';
+        document.getElementById('s2s-panel').style.display = isS2S ? 'block' : 'none';
+        document.getElementById('srv-panel').style.display = isSrv ? 'block' : 'none';
+        document.getElementById('conf-input').style.display = hideConf ? 'none' : 'block';
+        document.querySelector('label[for="conf-input"]').style.display = hideConf ? 'none' : 'block';
+        document.getElementById('scenario-row').style.display = hideConf ? 'none' : 'block';
+        if (hideConf) {
+            document.getElementById('doh-panel').style.display = 'none';
+            document.getElementById('services-panel').style.display = 'none';
+            document.getElementById('exclusions-panel').style.display = 'none';
+            document.getElementById('fasttrack-warn').style.display = 'none';
+            document.getElementById('non-ru-warn').style.display = 'none';
+            document.getElementById('netwatch-row').style.display = 'none';
+        } else {
+            onScenarioChange();
+        }
+        document.getElementById('prefix-select').parentElement.style.display = 'block';
+        var hintKey = isSrv ? 'modeServerHint' : (isS2S ? 'modeSiteToSiteHint' : (mode === 'container' ? 'modeContainerHint' : 'modeStandaloneHint'));
+        document.getElementById('mode-hint').setAttribute('data-i18n', hintKey);
+        document.getElementById('mode-hint').textContent = t(hintKey);
+        // Show/hide hub prefix options
+        var prefixSel = document.getElementById('prefix-select');
+        var hubOpts = prefixSel.querySelectorAll('[value^="awg-server-"]');
+        var proxyOpts = prefixSel.querySelectorAll('[value^="awg-proxy"]');
+        for (var i = 0; i < hubOpts.length; i++) hubOpts[i].style.display = isSrv ? '' : 'none';
+        for (var i = 0; i < proxyOpts.length; i++) proxyOpts[i].style.display = isSrv ? 'none' : '';
+        if (isSrv && prefixSel.value.indexOf('awg-proxy') === 0) prefixSel.value = 'awg-server-1';
+        if (!isSrv && prefixSel.value.indexOf('awg-server') === 0) prefixSel.value = 'awg-proxy-1';
+        if (isS2S) ensureS2SParams();
+        if (isSrv) ensureServerParams();
+        _igRefresh();
+    }
+
+    // ============================================================
+    // Storage selector
+    // ============================================================
+    document.getElementById('storage-select').addEventListener('change', function() {
+        var custom = document.getElementById('storage-custom');
+        if (this.value === '__custom') {
+            custom.style.display = 'inline-block';
+            custom.focus();
+        } else {
+            custom.style.display = 'none';
+            custom.value = '';
+        }
+    });
+
+    function getStorageDisk() {
+        var sel = document.getElementById('storage-select').value;
+        if (sel === '__custom') {
+            var v = document.getElementById('storage-custom').value.trim();
+            return v || 'disk1';
+        }
+        return sel;
+    }
+
+    document.getElementById('prefix-select').addEventListener('change', function() {
+        var custom = document.getElementById('prefix-custom');
+        if (this.value === '__custom') {
+            custom.style.display = 'inline-block';
+            custom.focus();
+        } else {
+            custom.style.display = 'none';
+            custom.value = '';
+        }
+    });
+
+    function getPrefixName() {
+        var sel = document.getElementById('prefix-select').value;
+        if (sel === '__custom') {
+            var v = document.getElementById('prefix-custom').value.trim();
+            return v || 'awg-proxy-1';
+        }
+        return sel;
+    }
+
+    // Deterministically derives a unique /30 veth subnet from the tunnel name
+    // (prefix), so multiple tunnels on the same MikroTik don't collide. It has to
+    // stay deterministic: the uninstall script recomputes the same addresses from
+    // the prefix to clean them up. Uses a simple djb2-style hash spread across
+    // different bit ranges: bits 0-3 → 2nd octet (172.16-31.x.x), bits 4-11 →
+    // 3rd octet, bits 12-17 → 4th octet base (0-252, aligned to /30).
+    function prefixHash(prefix) {
+        var h = 0;
+        for (var i = 0; i < prefix.length; i++)
+            h = ((h * 31) + prefix.charCodeAt(i)) & 0xFFFFFFFF;
+        return h >>> 0;
+    }
+
+    function getTunnelNetwork(prefix) {
+        var h = prefixHash(prefix);
+        var oct2 = 16 + (h & 0xF);
+        var oct3 = (h >>> 4) & 0xFF;
+        var oct4base = ((h >>> 12) & 0x3F) * 4;
+        return {
+            vethAddr: '172.' + oct2 + '.' + oct3 + '.' + (oct4base + 2) + '/30',
+            hostAddr: '172.' + oct2 + '.' + oct3 + '.' + (oct4base + 1) + '/30',
+            vethGw:   '172.' + oct2 + '.' + oct3 + '.' + (oct4base + 1),
+            natSrc:   '172.' + oct2 + '.' + oct3 + '.' + oct4base + '/30'
+        };
+    }
+
+    // The veth carries only IPv4 by default, which leaves the container with a
+    // link-local address and nothing else — it cannot reach an IPv6 AWG server
+    // and cannot be reached over IPv6 either. When IPv6 is in play the veth
+    // gets a ULA too; the global address stays on the router, which NATs. The
+    // /64 is derived from the same prefix hash, so two tunnels on one router
+    // never land on the same one.
+    function getTunnelNetwork6(prefix) {
+        var h = prefixHash(prefix);
+        function hex(v, w) { var s = v.toString(16); while (s.length < w) s = '0' + s; return s; }
+        var net = 'fd' + hex((h >>> 24) & 0xFF, 2) + ':' + hex((h >>> 8) & 0xFFFF, 4) +
+                  ':' + hex(h & 0xFFFF, 4);
+        return {
+            vethAddr: net + '::2/64',
+            hostAddr: net + '::1/64',
+            vethAddrBare: net + '::2',
+            vethGw: net + '::1',
+            net: net + '::/64'
+        };
+    }
+
+    // "address=<v4>[,<v6>] gateway=<v4gw>[ gateway6=<v6gw>]" for /interface/veth/add
+    function vethAddrArgs(net, net6) {
+        if (!net6) return 'address=' + net.vethAddr + ' gateway=' + net.vethGw;
+        return 'address=' + net.vethAddr + ',' + net6.vethAddr +
+               ' gateway=' + net.vethGw + ' gateway6=' + net6.vethGw;
+    }
+
+    // Router side of the container's IPv6 leg. `outbound` also opens the way
+    // out: the container talks to the world through the router's own global
+    // address (NAT66), because a /128 from the ISP cannot be split further and
+    // a delegated prefix is not something we can count on.
+    //
+    // Both legs need a default route inside the container, and getting one took
+    // a live capture to pin down: RouterOS does NOT push the veth's `gateway6`
+    // into the container's network namespace. The container is left with an
+    // on-link /64 and nothing else, so anything aimed off-link dies inside it —
+    // an outbound leg never reaches the server, and a hub cannot answer a client
+    // whose address is (by definition) off-link. The router therefore has to
+    // advertise on the veth (RA), which is what installs `default via <router
+    // link-local>` in the namespace.
+    //
+    // Two details matter. `advertise=yes` alone is not enough: a router whose
+    // /ipv6/nd default is scoped to another interface (e.g. bridge) sends no RA
+    // on the veth, hence the explicit per-interface nd entry. And the prefix is
+    // advertised with `autonomous=no` so the container takes only the route and
+    // not an extra SLAAC address — it keeps sourcing from the static ::2 the
+    // veth already gave it, which is what the hub's dst-nat conntrack expects to
+    // see on the way back. With SLAAC on, replies leave from an unpredictable
+    // EUI-64 address and the translation never reverses.
+    function containerV6Lines(prefix, net6, outbound) {
+        var veth = 'veth-' + prefix;
+        var l = [
+            // Order matters, and only a live run shows why. The stock /ipv6/nd
+            // entry covers interface=all, so the moment an address on the veth
+            // says advertise=yes the router starts sending RAs and generates a
+            // *dynamic* prefix entry with autonomous=yes — after which adding
+            // the static one fails outright with "configuration for this prefix
+            // already exists". Declaring the prefix first leaves the router
+            // nothing to invent.
+            '/ipv6/nd/prefix/add interface=' + veth + ' prefix=' + net6.net + ' autonomous=no',
+            '/ipv6/address/add address=' + net6.hostAddr + ' interface=' + veth +
+                ' advertise=yes comment=' + prefix + '-v6',
+            '/ipv6/nd/add interface=' + veth + ' comment=' + prefix + '-v6',
+            // Both legs, and at the top of the chain. An outbound leg opens new
+            // connections from an interface that is not in LAN, which the
+            // defconf forward chain drops outright. A hub only ever answers, so
+            // on a stock router its replies already pass as established — but
+            // non-stock chains are common (force-ipv4 policies, geo-filters,
+            // "IPv6 only from LAN"), and anything sitting above the
+            // established-accept rejects those replies. The failure that causes
+            // is near-undiagnosable from either end: the handshake arrives, the
+            // hub answers, and the answer dies on the way out. The rule only
+            // ever passes traffic leaving our own container, which speaks to
+            // nobody but the clients in its session table.
+            '/ipv6/firewall/filter/add chain=forward action=accept in-interface=' + veth +
+                ' place-before=0 comment=' + prefix + '-v6-out'
+        ];
+        // Only an initiator masquerades. A hub must not: its replies have to
+        // keep the ::2 source that the inbound dst-nat recorded, or conntrack
+        // cannot translate them back to the router's global address.
+        if (outbound) {
+            l.push('/ipv6/firewall/nat/add chain=srcnat action=masquerade src-address=' + net6.net +
+                   ' out-interface-list=WAN comment=' + prefix + '-v6-nat');
+        }
+        return l;
+    }
+
+    // Undo of the above — the uninstall scripts are the only place that knows
+    // these objects exist, so they list them by comment.
+    function containerV6UninstallLines(prefix, indent) {
+        var p = indent || '';
+        return [
+            p + ':do { /ipv6/firewall/nat/remove [find where comment=' + prefix + '-v6-nat] } on-error={}',
+            p + ':do { /ipv6/firewall/nat/remove [find where comment=' + prefix + '-dstnat6] } on-error={}',
+            p + ':do { /ipv6/firewall/filter/remove [find where comment=' + prefix + '-v6-out] } on-error={}',
+            p + ':do { /ipv6/firewall/filter/remove [find where comment=' + prefix + '-awg-in6] } on-error={}',
+            p + ':do { /ipv6/nd/remove [find where interface=veth-' + prefix + '] } on-error={}',
+            p + ':do { /ipv6/nd/prefix/remove [find where interface=veth-' + prefix + '] } on-error={}',
+            p + ':do { /ipv6/address/remove [find where comment=' + prefix + '-v6] } on-error={}'
+        ];
+    }
+
+    // The WG listen-port used to come out of the same hash (bits 18+), but those
+    // bits don't change when only the trailing digit of the prefix does: every
+    // awg-proxy-1..5 got 25343 and every awg-server-1..2 got 25081, so a second
+    // tunnel on the same router could not bind its port. The port is local to the
+    // router and nothing recomputes it from the prefix, so draw it at random.
+    function randomWgPort() {
+        return randomInt(10000, 60000);
+    }
+
+    // Tunnel subnets used to ship as fixed defaults (10.10.10.0/24 for the hub,
+    // 10.0.0.0/30 for site-to-site), so a second tunnel on the same router landed
+    // on addresses the first one already held. Second octet starts at 20 to stay
+    // clear of the usual 10.0.x/10.1.x/10.10.x home LANs. The share link carries
+    // the subnet, so add-client runs still follow the server that issued it.
+    function randomTunnelSubnet(bits) {
+        var base = '10.' + randomInt(20, 250) + '.' + randomInt(0, 255) + '.';
+        return bits === 30 ? base + (randomInt(0, 62) * 4) + '/30' : base + '0/24';
+    }
+
+    // ============================================================
+    // Curve25519 key generation (Montgomery ladder, BigInt)
+    // ============================================================
+    function curve25519ScalarMult(scalar, point) {
+        var P = (1n << 255n) - 19n;
+        function mod(a) { a = a % P; return a < 0n ? a + P : a; }
+        function modPow(b, e, m) {
+            var r = 1n; b = ((b % m) + m) % m;
+            while (e > 0n) { if (e & 1n) r = r * b % m; e >>= 1n; b = b * b % m; }
+            return r;
+        }
+        function inv(a) { return modPow(a, P - 2n, P); }
+
+        var k = new Uint8Array(scalar);
+        k[0] &= 248; k[31] &= 127; k[31] |= 64;
+        var kn = 0n;
+        for (var i = 31; i >= 0; i--) kn = (kn << 8n) | BigInt(k[i]);
+
+        var u = 0n;
+        for (var i = 31; i >= 0; i--) u = (u << 8n) | BigInt(point[i]);
+
+        var x_1 = u, x_2 = 1n, z_2 = 0n, x_3 = u, z_3 = 1n, swap = 0n;
+        var A24 = 121665n;
+
+        for (var pos = 254; pos >= 0; pos--) {
+            var bit = (kn >> BigInt(pos)) & 1n;
+            swap ^= bit;
+            // cswap
+            var d1 = swap * (x_2 - x_3), d2 = swap * (z_2 - z_3);
+            x_2 = mod(x_2 - d1); x_3 = mod(x_3 + d1);
+            z_2 = mod(z_2 - d2); z_3 = mod(z_3 + d2);
+            swap = bit;
+
+            var A = mod(x_2 + z_2), AA = mod(A * A);
+            var B = mod(x_2 - z_2), BB = mod(B * B);
+            var E = mod(AA - BB);
+            var C = mod(x_3 + z_3), D = mod(x_3 - z_3);
+            var DA = mod(D * A), CB = mod(C * B);
+            x_3 = mod(mod(DA + CB) * mod(DA + CB));
+            z_3 = mod(x_1 * mod(mod(DA - CB) * mod(DA - CB)));
+            x_2 = mod(AA * BB);
+            z_2 = mod(E * mod(AA + mod(A24 * E)));
+        }
+        // final cswap
+        var d1 = swap * (x_2 - x_3), d2 = swap * (z_2 - z_3);
+        x_2 = mod(x_2 - d1); z_2 = mod(z_2 - d2);
+
+        var result = mod(x_2 * inv(z_2));
+        var out = new Uint8Array(32);
+        for (var i = 0; i < 32; i++) { out[i] = Number(result & 0xFFn); result >>= 8n; }
+        return out;
+    }
+
+    function generateWgKeyPair() {
+        var priv = new Uint8Array(32);
+        crypto.getRandomValues(priv);
+        var basepoint = new Uint8Array(32);
+        basepoint[0] = 9;
+        var pub = curve25519ScalarMult(priv, basepoint);
+        return {
+            privateKey: uint8ToBase64(priv),
+            publicKey: uint8ToBase64(pub)
+        };
+    }
+
+    function uint8ToBase64(arr) {
+        var s = '';
+        for (var i = 0; i < arr.length; i++) s += String.fromCharCode(arr[i]);
+        return btoa(s);
+    }
+
+    // ============================================================
+    // Random AWG parameter generation
+    // ============================================================
+    function randomInt(min, max) {
+        var range = max - min + 1;
+        var arr = new Uint32Array(1);
+        crypto.getRandomValues(arr);
+        return min + (arr[0] % range);
+    }
+
+    function randomUint32() {
+        var arr = new Uint32Array(1);
+        crypto.getRandomValues(arr);
+        return arr[0];
+    }
+
+    function randomKey32B64() {
+        var a = new Uint8Array(32);
+        crypto.getRandomValues(a);
+        var raw = '';
+        for (var i = 0; i < 32; i++) raw += String.fromCharCode(a[i]);
+        return btoa(raw);
+    }
+
+    // Obfuscation levels the generator can produce, strongest first.
+    var PROTO_LEVELS = ['v3', 'v2', 'v1.5', 'v1'];
+
+    // PROTO_LEVELS is newest-first, so a larger index means an older
+    // generation and index 0 is current. The guards below used to test
+    // against 'v2', which told owners of a genuine 3.0 config -- the case
+    // this page exists for -- to downgrade and lose header protection.
+    function isOutdatedProto(p) { return PROTO_LEVELS.indexOf(p) > 0; }
+
+    function randomHRange() {
+        var base = randomUint32();
+        var width = randomInt(8, 64);
+        if (base > 4294967295 - width) base -= width;
+        return { min: base, max: base + width };
+    }
+
+    function hRangesOverlap(a, b) {
+        return a.min <= b.max && b.min <= a.max;
+    }
+
+    // Four pairwise-disjoint H ranges (the proxy rejects overlapping ones).
+    function disjointHRanges() {
+        var rs;
+        for (var attempt = 0; attempt < 100; attempt++) {
+            rs = [randomHRange(), randomHRange(), randomHRange(), randomHRange()];
+            var ok = true;
+            for (var i = 0; i < 4 && ok; i++)
+                for (var j = i + 1; j < 4; j++)
+                    if (hRangesOverlap(rs[i], rs[j])) { ok = false; break; }
+            if (ok) break;
+        }
+        return [rs[0].min + '-' + rs[0].max, rs[1].min + '-' + rs[1].max,
+                rs[2].min + '-' + rs[2].max, rs[3].min + '-' + rs[3].max];
+    }
+
+    // CPS (I1-I5) templates: AmneziaWG signature-junk sent by the initiator
+    // (fixed magic header + random tail). The responder just drops them, so
+    // only the client side needs them.
+    function generateCPSTemplate() {
+        var nbytes = randomInt(2, 6);
+        var hex = '';
+        for (var i = 0; i < nbytes; i++) {
+            var b = randomInt(0, 255).toString(16);
+            hex += (b.length < 2 ? '0' : '') + b;
+        }
+        return '<b 0x' + hex + '><r ' + randomInt(4, 16) + '>';
+    }
+
+    function generateCPSTemplates(n) {
+        var a = [];
+        for (var i = 0; i < n; i++) a.push(generateCPSTemplate());
+        return a;
+    }
+
+    // One obfuscation profile at the requested level. avoidS1 keeps successive
+    // stages of a fallback chain apart, so a packet only ever matches one.
+    function generateObfLevel(level, avoidS1) {
+        var o = { level: level, s3: 0, s4: 0, hpKey: '', cps: null };
+        var lo = level === 'v3' ? 20 : 15;
+        do { o.s1 = randomInt(lo, 150); } while (avoidS1 && Math.abs(o.s1 - avoidS1) < 5);
+        o.s2 = randomInt(lo, 150);
+
+        if (level === 'v3' || level === 'v2') {
+            var rs = disjointHRanges();
+            o.h1 = rs[0]; o.h2 = rs[1]; o.h3 = rs[2]; o.h4 = rs[3];
+            o.s3 = randomInt(20, 100);
+        } else {
+            o.h1 = randomUint32(); o.h2 = randomUint32();
+            o.h3 = randomUint32(); o.h4 = randomUint32();
+        }
+
+        if (level === 'v3') {
+            // Header protection derives its nonce from the first 12 padding
+            // bytes, so every S must be >= 12 (S1/S2/S3 already are). S4 stays
+            // in 12..16 because the outer datagram is 1484 + S4 — at 16 that
+            // is exactly 1500, so the stock MTU never has to change.
+            o.s4 = randomInt(12, 16);
+            o.hpKey = randomKey32B64();
+        }
+        if (level === 'v3' || level === 'v1.5') o.cps = generateCPSTemplates(randomInt(1, 3));
+        return o;
+    }
+
+    function generateAWGParams(level) {
+        var o = generateObfLevel(level || 'v3', 0);
+        return {
+            jc: randomInt(3, 8),
+            jmin: randomInt(50, 250),
+            jmax: randomInt(500, 1000),
+            level: o.level,
+            s1: o.s1, s2: o.s2, s3: o.s3, s4: o.s4,
+            h1: o.h1, h2: o.h2, h3: o.h3, h4: o.h4,
+            hpKey: o.hpKey, cps: o.cps,
+            chain: [],
+            port: randomInt(10000, 60000),
+            wgListenPort: randomInt(10000, 60000)
+        };
+    }
+
+    // Fallback chain: every level below the primary, so an older container on
+    // the other side still comes up (the proxy probes the stages in order).
+    // Chain stages never carry header protection — there is a single key per
+    // device, and a second v3 stage would be indistinguishable from the first.
+    function attachFallbackChain(ap) {
+        var idx = PROTO_LEVELS.indexOf(ap.level);
+        if (idx < 0) idx = 0;
+        ap.chain = [];
+        var prevS1 = ap.s1;
+        for (var i = idx + 1; i < PROTO_LEVELS.length && ap.chain.length < 3; i++) {
+            var st = generateObfLevel(PROTO_LEVELS[i], prevS1);
+            prevS1 = st.s1;
+            ap.chain.push(st);
+        }
+        return ap;
+    }
+
+    // The level selector lives in whichever panel is visible (site-to-site or
+    // server); both write the same generator inputs.
+    function protoCtl(name) {
+        var srv = document.getElementById('srv-panel');
+        var inServer = srv && srv.style.display !== 'none';
+        return document.getElementById((inServer ? 'srv-' : 's2s-') + name);
+    }
+
+    function getProtoLevel() {
+        var el = protoCtl('proto-level');
+        return el && PROTO_LEVELS.indexOf(el.value) >= 0 ? el.value : 'v3';
+    }
+
+    function compatChainEnabled() {
+        var el = protoCtl('proto-compat');
+        return el ? el.checked : true;
+    }
+
+    function generateChainParams() {
+        var ap = generateAWGParams(getProtoLevel());
+        if (compatChainEnabled()) attachFallbackChain(ap);
+        return ap;
+    }
+
+    function generateS2SParams() {
+        return generateChainParams();
+    }
+
+    function onProtoLevelChange() {
+        // Regenerate whichever set the visible panel owns, so the preview and
+        // the emitted script never disagree about the level.
+        if (s2sParams) regenerateS2SParams();
+        if (serverParams) regenerateServerParams();
+    }
+
+    // Full obfuscation env block: primary profile plus the fallback chain.
+    // Used by every generated (non-.conf) scenario — site-to-site and server.
+    function s2sObfEnvLines(prefix, ap) {
+        var e = [];
+        function add(k, v) {
+            e.push('/container/envs/add list=' + prefix + '-env key=' + k + ' value="' + v + '"');
+        }
+        add('AWG_JC', ap.jc);
+        add('AWG_JMIN', ap.jmin);
+        add('AWG_JMAX', ap.jmax);
+        add('AWG_S1', ap.s1);
+        add('AWG_S2', ap.s2);
+        if (ap.s3) add('AWG_S3', ap.s3);
+        if (ap.s4) add('AWG_S4', ap.s4);
+        add('AWG_H1', ap.h1);
+        add('AWG_H2', ap.h2);
+        add('AWG_H3', ap.h3);
+        add('AWG_H4', ap.h4);
+        if (ap.hpKey) add('AWG_HEADER_PROTECTION_KEY', ap.hpKey);
+        if (ap.cps) for (var c = 0; c < ap.cps.length; c++) add('AWG_I' + (c + 1), ap.cps[c]);
+
+        var chain = ap.chain || [];
+        for (var i = 0; i < chain.length; i++) {
+            var pfx = 'AWG_FB' + (i === 0 ? '' : String(i + 1)) + '_';
+            var st = chain[i];
+            add(pfx + 'S1', st.s1);
+            add(pfx + 'S2', st.s2);
+            if (st.s3) add(pfx + 'S3', st.s3);
+            if (st.s4) add(pfx + 'S4', st.s4);
+            add(pfx + 'H1', st.h1);
+            add(pfx + 'H2', st.h2);
+            add(pfx + 'H3', st.h3);
+            add(pfx + 'H4', st.h4);
+            if (st.cps) for (var m = 0; m < st.cps.length; m++) add(pfx + 'I' + (m + 1), st.cps[m]);
+        }
+        return e;
+    }
+
+    var s2sParams = null;
+
+    function ensureS2SParams() {
+        if (!s2sParams) {
+            s2sParams = generateS2SParams();
+            document.getElementById('s2s-awg-port').value = s2sParams.port;
+            // Only on first init, never in regenerate*: a subnet typed by hand must
+            // survive a protocol-level change.
+            document.getElementById('s2s-tunnel-subnet').value = randomTunnelSubnet(30);
+            showS2SParams();
+        }
+        return s2sParams;
+    }
+
+    function regenerateS2SParams() {
+        s2sParams = generateS2SParams();
+        document.getElementById('s2s-awg-port').value = s2sParams.port;
+        showS2SParams();
+    }
+
+    // ============================================================
+    // Server mode (1:N hub)
+    // ============================================================
+    var serverParams = null;
+    var serverFromURL = null;
+    var lastAwgConf = null;
+    var lastAwgConfName = 'awg-client.conf';
+
+    function ensureServerParams() {
+        if (!serverParams) {
+            serverParams = generateChainParams();
+            document.getElementById('srv-awg-port').value = serverParams.port;
+            // Add-client runs overwrite this right after onModeChange() with the
+            // subnet from the share link (see the parseURLParams block on load).
+            document.getElementById('srv-tunnel-subnet').value = randomTunnelSubnet(24);
+            showSrvParams();
+        }
+        return serverParams;
+    }
+
+    function regenerateServerParams() {
+        serverParams = generateChainParams();
+        document.getElementById('srv-awg-port').value = serverParams.port;
+        showSrvParams();
+    }
+
+    function onSrvNetChange() {
+        var val = getSrvNetMode();
+        var isAwg = val === 'awg';
+        var hintKey = isAwg ? 'srvNetAwgHint' : (val === 'l3' ? 'srvNetL3Hint' : 'srvNetNoneHint');
+        var el = document.getElementById('srv-net-hint');
+        el.setAttribute('data-i18n', hintKey);
+        el.textContent = t(hintKey);
+        // LAN merging + client-side MikroTik storage are only relevant for site-to-site (none/l3).
+        // A direct AmneziaWG client (phone/laptop) has no LAN to merge and no client router.
+        document.getElementById('srv-lan-fields').style.display = isAwg ? 'none' : '';
+        var csWrap = document.getElementById('srv-storage-client-wrap');
+        if (csWrap) csWrap.style.display = isAwg ? 'none' : '';
+        if (!isAwg) {
+            // Update LAN labels: required for L3, optional for None
+            var req = val !== 'none';
+            var srvKey = req ? 'srvLanServerReq' : 'srvLanServerOpt';
+            var cliKey = req ? 'srvLanClientReq' : 'srvLanClientOpt';
+            var srvLabel = document.getElementById('srv-lan-server-label');
+            var cliLabel = document.getElementById('srv-lan-client-label');
+            srvLabel.setAttribute('data-i18n', srvKey);
+            srvLabel.textContent = t(srvKey);
+            cliLabel.setAttribute('data-i18n', cliKey);
+            cliLabel.textContent = t(cliKey);
+        }
+    }
+
+    function getSrvNetMode() {
+        var radios = document.getElementsByName('srv-net');
+        for (var i = 0; i < radios.length; i++) if (radios[i].checked) return radios[i].value;
+        return 'none';
+    }
+
+    function parseSlash24(cidr) {
+        var m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/24$/.exec(cidr);
+        if (!m) return null;
+        var octets = [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
+        for (var i = 0; i < 4; i++) if (octets[i] > 255) return null;
+        if (octets[3] !== 0) return null;
+        return {
+            network: cidr,
+            prefix: octets[0] + '.' + octets[1] + '.' + octets[2],
+            ip: function(n) { return octets[0] + '.' + octets[1] + '.' + octets[2] + '.' + n; }
+        };
+    }
+
+    // ============================================================
+    // Shareable URL encoding (version 4)
+    // ============================================================
+    //
+    // Version 4 is owned by this page. Links from the previous configurator
+    // (versions 2 and 3) are NOT read here — they belong to configurator.html,
+    // which is frozen. A link of the wrong generation gets a message pointing
+    // at the other page, in both directions.
+    //
+    // RULES FOR FUTURE CHANGES (backward compatibility contract):
+    //
+    // 1. NEVER change the fixed header layout (87 bytes). It is frozen.
+    //    If a new numeric param is needed, add it as a field, not to the header.
+    //
+    // 2. NEVER reorder or remove existing fields. Only APPEND new fields at the
+    //    end. Field indices are permanent.
+    //
+    // 3. Parser MUST handle missing trailing fields gracefully (field() returns '').
+    //
+    // 4. Parser MUST skip unknown trailing fields it doesn't understand.
+    //
+    // 5. Adding new fields does NOT require a version bump — fieldCount handles it.
+    //
+    // Format:
+    //   Fixed header (87 bytes, frozen):
+    //     [ver:1][jc:1][jmin:2][jmax:2][s1:2][s2:2][s3:2][s4:2]
+    //     [h1lo:4][h1hi:4][h2lo:4][h2hi:4][h3lo:4][h3hi:4][h4lo:4][h4hi:4]
+    //     [awgPort:2][wgPort:2][nextClient:1][tunOct:3][level:1][hpKey:32]
+    //   Fields section:
+    //     [fieldCount:1] followed by fieldCount length-prefixed fields [len:1][data:N]
+    //   Field indices (append-only, never reorder):
+    //     0 = wgPubKey (32 bytes, critical — without it nothing works)
+    //     1 = endpoint (critical — server address)
+    //     2 = prefix (important — tunnel name, has default fallback)
+    //     3 = netMode (1 byte: 0=none, 1=l3, 2=l2)
+    //     4 = serverLAN (optional, only needed for L3/L2)
+    //     5+ = reserved for future use
+    var SHARE_VERSION = 4;
+    var SHARE_HEADER_LEN = 87;
+
+    function strToBytes(s) { var a = []; for (var i = 0; i < s.length; i++) a.push(s.charCodeAt(i) & 0xFF); return a; }
+
+    // H is "N" or "LO-HI" in a config; the link always stores a lo/hi pair and
+    // collapses it back on read.
+    function hToPair(h) {
+        var parts = String(h).split('-');
+        var lo = Number(parts[0]) >>> 0;
+        var hi = parts.length > 1 ? Number(parts[1]) >>> 0 : lo;
+        return [lo, hi];
+    }
+
+    function pairToH(lo, hi) { return lo === hi ? String(lo) : (lo + '-' + hi); }
+
+    function buildShareableURL(awgParams, serverWgPubKey, endpoint, awgPort, wgPort, tunnelSubnet, prefix, nextClient, netMode, serverLAN) {
+        var tun = tunnelSubnet.replace(/\/24$/, '').split('.');
+        var keyRaw = atob(serverWgPubKey);
+        var netModeB = netMode === 'l3' ? 1 : (netMode === 'l2' ? 2 : 0);
+
+        var fields = [
+            strToBytes(keyRaw.slice(0, 32)),
+            strToBytes(endpoint),
+            strToBytes(prefix),
+            [netModeB],
+            strToBytes(serverLAN || '')
+        ];
+
+        var totalLen = SHARE_HEADER_LEN + 1;
+        for (var i = 0; i < fields.length; i++) totalLen += 1 + fields[i].length;
+        var buf = new Uint8Array(totalLen);
+        var o = 0;
+
+        function u16(v) { buf[o++] = (v >> 8) & 0xFF; buf[o++] = v & 0xFF; }
+        function u32(v) {
+            buf[o++] = (v >>> 24) & 0xFF; buf[o++] = (v >>> 16) & 0xFF;
+            buf[o++] = (v >>> 8) & 0xFF;  buf[o++] = v & 0xFF;
+        }
+
+        buf[o++] = SHARE_VERSION;
+        buf[o++] = awgParams.jc & 0xFF;
+        u16(awgParams.jmin); u16(awgParams.jmax);
+        u16(awgParams.s1); u16(awgParams.s2);
+        u16(awgParams.s3 || 0); u16(awgParams.s4 || 0);
+        ['h1', 'h2', 'h3', 'h4'].forEach(function (k) {
+            var pr = hToPair(awgParams[k]);
+            u32(pr[0]); u32(pr[1]);
+        });
+        u16(awgPort); u16(wgPort);
+        buf[o++] = nextClient & 0xFF;
+        buf[o++] = Number(tun[0]) & 0xFF; buf[o++] = Number(tun[1]) & 0xFF; buf[o++] = Number(tun[2]) & 0xFF;
+        var lvl = PROTO_LEVELS.indexOf(awgParams.level);
+        buf[o++] = lvl < 0 ? 0 : lvl;
+        var hpRaw = awgParams.hpKey ? atob(awgParams.hpKey) : '';
+        for (var b = 0; b < 32; b++) buf[o++] = b < hpRaw.length ? (hpRaw.charCodeAt(b) & 0xFF) : 0;
+
+        buf[o++] = fields.length & 0xFF;
+        for (var f = 0; f < fields.length; f++) {
+            buf[o++] = fields[f].length & 0xFF;
+            for (var i2 = 0; i2 < fields[f].length; i2++) buf[o++] = fields[f][i2];
+        }
+
+        var b64 = btoa(String.fromCharCode.apply(null, buf)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
+        return location.origin + location.pathname + '?c=' + b64;
+    }
+
+    function showWrongLinkGeneration() {
+        var warn = document.getElementById('link-version-warn');
+        if (!warn) return;
+        var href = document.getElementById('link-version-href');
+        if (href) href.href = 'configurator.html' + location.search;
+        warn.style.display = 'block';
+    }
+
+    function parseURLParams() {
+        var sp = new URLSearchParams(location.search);
+        var c = sp.get('c');
+        if (!c) return null;
+        var b64 = c.replace(/-/g, '+').replace(/_/g, '/');
+        while (b64.length % 4) b64 += '=';
+        try { var raw = atob(b64); } catch (e) { return null; }
+        var buf = new Uint8Array(raw.length);
+        for (var i = 0; i < raw.length; i++) buf[i] = raw.charCodeAt(i);
+        if (buf.length < SHARE_HEADER_LEN + 1) { showWrongLinkGeneration(); return null; }
+
+        var o = 0;
+        if (buf[o++] !== SHARE_VERSION) { showWrongLinkGeneration(); return null; }
+
+        function u16() { var v = (buf[o] << 8) | buf[o + 1]; o += 2; return v; }
+        function u32() {
+            var v = ((buf[o] << 24) | (buf[o + 1] << 16) | (buf[o + 2] << 8) | buf[o + 3]) >>> 0;
+            o += 4; return v;
+        }
+
+        var jc = buf[o++];
+        var jmin = u16(), jmax = u16();
+        var s1 = u16(), s2 = u16(), s3 = u16(), s4 = u16();
+        var hs = [];
+        for (var hn = 0; hn < 4; hn++) { var lo = u32(); var hh = u32(); hs.push(pairToH(lo, hh)); }
+        var awgPort = u16(), wgPort = u16();
+        var nextClient = buf[o++];
+        var tunOct = [buf[o++], buf[o++], buf[o++]];
+        var tunnelSubnet = tunOct[0] + '.' + tunOct[1] + '.' + tunOct[2] + '.0/24';
+        var level = PROTO_LEVELS[buf[o++]] || 'v3';
+        var hpRaw = '';
+        var hpNonZero = false;
+        for (var k = 0; k < 32; k++) { var bv = buf[o++]; if (bv) hpNonZero = true; hpRaw += String.fromCharCode(bv); }
+        var hpKey = hpNonZero ? btoa(hpRaw) : '';
+
+        var fields = [];
+        var fieldCount = buf[o++];
+        for (var f = 0; f < fieldCount && o < buf.length; f++) {
+            var flen = buf[o++];
+            var fdata = '';
+            for (var i3 = 0; i3 < flen && o < buf.length; i3++) fdata += String.fromCharCode(buf[o++]);
+            fields.push(fdata);
+        }
+
+        function field(idx) { return idx < fields.length ? fields[idx] : ''; }
+        var keyStr = field(0);
+        var serverWgPubKey = keyStr.length === 32 ? btoa(keyStr) : '';
+        var endpoint = field(1);
+        var prefix = field(2) || 'awg-server-1';
+        var netModeRaw = field(3);
+        var netModeVal = netModeRaw.length > 0 ? netModeRaw.charCodeAt(0) : 0;
+        var netMode = netModeVal === 1 ? 'l3' : (netModeVal === 2 ? 'l2' : 'none');
+        var serverLAN = field(4);
+
+        return {
+            mode: 'server',
+            jc: jc, jmin: jmin, jmax: jmax,
+            s1: s1, s2: s2, s3: s3, s4: s4,
+            h1: hs[0], h2: hs[1], h3: hs[2], h4: hs[3],
+            level: level, hpKey: hpKey,
+            serverWgPubKey: serverWgPubKey,
+            endpoint: endpoint,
+            awgPort: awgPort,
+            wgPort: wgPort,
+            tunnelSubnet: tunnelSubnet,
+            prefix: prefix,
+            nextClient: nextClient || 2,
+            netMode: netMode,
+            serverLAN: serverLAN
+        };
+    }
+
+    function copySrvURL() {
+        var el = document.getElementById('srv-share-url');
+        el.select();
+        document.execCommand('copy');
+        var btn = document.getElementById('srv-copy-url');
+        btn.textContent = '\u2713';
+        setTimeout(function() { btn.textContent = t('btnCopy'); }, 1500);
+    }
+
+    function buildServerUninstallSource(prefix, storageDisk, net) {
+        var isAuto = storageDisk === '__auto';
+        var src = [];
+        src.push('/system/script/add name=' + prefix + '-uninstall comment=' + prefix + ' source={');
+        src.push('  :put "Uninstalling AWG Proxy Server..."');
+        src.push('  :log info "Uninstalling AWG Proxy Server..."');
+        if (isAuto) {
+            src.push('  :local storageDisk "disk1"');
+            src.push('  :do { :set storageDisk [/container/envs/get [find where list="' + prefix + '-env" key="AWG_STORAGE"] value] } on-error={}');
+            src.push('  :local prevTmpdir ""');
+            src.push('  :if ($storageDisk != "disk1") do={');
+            src.push('    :do { :set prevTmpdir [/container/envs/get [find where list="' + prefix + '-env" key="AWG_PREV_TMPDIR"] value] } on-error={}');
+            src.push('  }');
+        } else if (storageDisk !== 'disk1') {
+            src.push('  :local prevTmpdir ""');
+            src.push('  :do { :set prevTmpdir [/container/envs/get [find where list="' + prefix + '-env" key="AWG_PREV_TMPDIR"] value] } on-error={}');
+        }
+        src.push('  /container/stop [find where interface=veth-' + prefix + ']');
+        src.push('  :delay 7s');
+        src.push('  /container/remove [find where interface=veth-' + prefix + ']');
+        src.push('  /container/envs/remove [find where list="' + prefix + '-env"]');
+        if (isAuto) {
+            src.push('  :if ($storageDisk != "disk1") do={');
+            src.push('    /container/config set tmpdir=$prevTmpdir');
+            src.push('  }');
+        } else if (storageDisk !== 'disk1') {
+            src.push('  /container/config set tmpdir=$prevTmpdir');
+        }
+        src.push('  :do { /ip/firewall/filter/remove [find where comment=' + prefix + '-awg-in] } on-error={}');
+        src.push('  :do { /ip/firewall/filter/remove [find where comment=' + prefix + '-wg-in] } on-error={}');
+        src.push('  :do { /ip/firewall/filter/remove [find where comment=' + prefix + '-wg-mgmt] } on-error={}');
+        src.push('  :do { /ip/firewall/nat/remove [find where comment=' + prefix + '-dstnat] } on-error={}');
+        src.push('  :do { /ip/firewall/nat/remove [find where comment=' + prefix + '-ft-nat] } on-error={}');
+        src = src.concat(containerV6UninstallLines(prefix, '  '));
+        src.push('  :do { /ip/firewall/filter/remove [find where comment=' + prefix + '-ft-fwd] } on-error={}');
+        src.push('  /ip/firewall/nat/remove [find where src-address="' + net.natSrc + '"]');
+        src.push('  :do { /ip/firewall/nat/remove [find where out-interface="wg-' + prefix + '"] } on-error={}');
+        src.push('  /ip/address/remove [find where address="' + net.hostAddr + '"]');
+        src.push('  /interface/veth/remove [find where name="veth-' + prefix + '"]');
+        src.push('  /interface/wireguard/peers/remove [find where interface="wg-' + prefix + '"]');
+        src.push('  /ip/address/remove [find where interface="wg-' + prefix + '"]');
+        src.push('  :do { /ip/route/remove [find where comment~"' + prefix + '"] } on-error={}');
+        src.push('  :do { /interface/eoip/remove [find where comment~"' + prefix + '"] } on-error={}');
+        src.push('  :do { /interface/bridge/port/remove [find where comment~"' + prefix + '"] } on-error={}');
+        src.push('  /interface/wireguard/remove [find where name="wg-' + prefix + '"]');
+        src.push('  :do { /file/remove [find where name~"' + prefix + '.+tar"] } on-error={}');
+        if (isAuto) {
+            src.push('  :do { /file/remove ($storageDisk . "/' + prefix + '") } on-error={}');
+        } else {
+            src.push('  :do { /file/remove "' + (storageDisk === 'disk1' ? '' : storageDisk + '/') + prefix + '" } on-error={}');
+        }
+        src.push('  /system/script/remove [find where name="' + prefix + '-uninstall"]');
+        src.push('  :put "Done."');
+        src.push('}');
+        return src.join('\n');
+    }
+
+    // Build a valid AmneziaWG .conf for a direct full-tunnel client (protocol v1: Jc/Jmin/Jmax/S1/S2/H1-H4).
+    // o: { clientPriv, serverPub, awgParams, endpoint, awgPort, tunClientIP, dnsServer }
+    function buildAwgConf(o) {
+        var p = o.awgParams;
+        var lines = [];
+        lines.push('[Interface]');
+        lines.push('PrivateKey = ' + o.clientPriv);
+        lines.push('Address = ' + o.tunClientIP + '/32');
+        if (o.dnsServer) lines.push('DNS = ' + o.dnsServer);
+        lines.push('Jc = ' + p.jc);
+        lines.push('Jmin = ' + p.jmin);
+        lines.push('Jmax = ' + p.jmax);
+        lines.push('S1 = ' + p.s1);
+        lines.push('S2 = ' + p.s2);
+        lines.push('H1 = ' + p.h1);
+        lines.push('H2 = ' + p.h2);
+        lines.push('H3 = ' + p.h3);
+        lines.push('H4 = ' + p.h4);
+        if (p.cps) {
+            for (var k = 0; k < p.cps.length; k++)
+                lines.push('I' + (k + 1) + ' = ' + p.cps[k]);
+        }
+        lines.push('');
+        lines.push('[Peer]');
+        lines.push('PublicKey = ' + o.serverPub);
+        lines.push('Endpoint = ' + endpointJoin(o.endpoint, o.awgPort));
+        lines.push('AllowedIPs = 0.0.0.0/0');
+        lines.push('PersistentKeepalive = 25');
+        return lines.join('\n');
+    }
+
+    function buildServerCommands(endpoint, awgParams, tunnelCidr, storageServer, storageClient, prefix, serverLAN, clientLAN, netMode, clientNum, dnsServer) {
+        var serverKey = generateWgKeyPair();
+        var clientKey = generateWgKeyPair();
+        // CPS templates for the client side (v1.5). Server ignores inbound CPS.
+        awgParams.cps = generateCPSTemplates(randomInt(2, 3));
+        var net = getTunnelNetwork(prefix);
+        var tun = parseSlash24(tunnelCidr);
+        var vethIP = net.vethAddr.replace(/\/.*/, '');
+        var awgPort = awgParams.port;
+        // Random per-generation port (see randomWgPort). It lives in awgParams and
+        // goes into the share link, so clients added from that link keep pointing
+        // at the same server port.
+        var wgPort = awgParams.wgListenPort || randomWgPort();
+        var tunServerIP = tun.ip(1);
+        var tunClientIP = tun.ip(clientNum + 1);
+
+        var serverAllowed = tunClientIP + '/32';
+        if (clientLAN) serverAllowed += ',' + clientLAN;
+
+        var clientAllowed = tun.network;
+        if (serverLAN) clientAllowed += ',' + serverLAN;
+
+        // Generate shareable URL before building commands (needed for env)
+        var shareURL = buildShareableURL(awgParams, serverKey.publicKey, endpoint, awgPort, wgPort, tunnelCidr, prefix, clientNum + 1, netMode, serverLAN);
+
+        // === SERVER CONFIG ===
+        var ls = [];
+        ls.push('# ============================================================');
+        ls.push('# AWG Proxy Server (1:N hub) \u2014 Head MikroTik');
+        ls.push('# ' + new Date().toISOString().slice(0, 19).replace('T', ' '));
+        ls.push('# ============================================================');
+        ls.push('');
+        ls.push('# 0. Check prerequisites');
+        ls.push(':if ([:len [/system/package/find where name="container" disabled=no]] = 0) do={');
+        ls.push('  :put "Container package is not installed or disabled. Install it and reboot."');
+        ls.push('  :error "Usage: /system/device-mode/update container=yes"');
+        ls.push('}');
+        ls.push('');
+
+        if (storageServer === '__auto') {
+            ls = ls.concat(buildAutoStorage(prefix));
+            ls.push('');
+        }
+
+        ls.push('# 1. Uninstall script');
+        ls.push(buildServerUninstallSource(prefix, storageServer, net));
+        ls.push('');
+
+        // The hub answers on IPv6 when the endpoint clients dial is IPv6. Its
+        // AWG socket then binds [::] with V6ONLY off, so one hub serves both
+        // families: an IPv4 client arrives on the same socket, v4-mapped.
+        var srvNet6 = ipv6Enabled() ? getTunnelNetwork6(prefix) : null;
+
+        ls.push('# 2. Network infrastructure');
+        ls.push('/interface/veth/add name=veth-' + prefix + ' ' + vethAddrArgs(net, srvNet6));
+        ls.push('/ip/address/add address=' + net.hostAddr + ' interface=veth-' + prefix);
+        ls.push('/ip/firewall/nat/add chain=srcnat action=masquerade src-address=' + net.natSrc);
+        if (srvNet6) {
+            ls.push('# Clients reach this hub over IPv6 - give the container an IPv6 leg.');
+            ls = ls.concat(containerV6Lines(prefix, srvNet6, false));
+        }
+        ls.push('');
+
+        ls.push('# 3. WireGuard server');
+        ls.push('/interface/wireguard/add name=wg-' + prefix + ' private-key="' + serverKey.privateKey + '" listen-port=' + wgPort + ' disabled=yes');
+        ls.push('/interface/wireguard/peers/add interface=wg-' + prefix + ' public-key="' + clientKey.publicKey + '" allowed-address=' + serverAllowed + ' comment=' + prefix + '-client-' + clientNum);
+        ls.push('/ip/address/add address=' + tunServerIP + '/24 interface=wg-' + prefix);
+        ls.push('/ip/firewall/nat/add chain=srcnat action=masquerade out-interface=wg-' + prefix);
+        ls.push('');
+
+        ls.push('# 4. Firewall: allow AWG port and forward to container');
+        ls.push('/ip/firewall/filter/add chain=forward action=accept protocol=udp dst-port=' + awgPort + ' in-interface-list=WAN place-before=0 comment=' + prefix + '-awg-in');
+        ls.push('/ip/firewall/filter/add chain=input action=accept protocol=udp src-address=' + vethIP + ' dst-port=' + wgPort + ' place-before=0 comment=' + prefix + '-wg-in');
+        ls.push('/ip/firewall/filter/add chain=input action=accept in-interface=wg-' + prefix + ' place-before=0 comment=' + prefix + '-wg-mgmt');
+        ls.push('/ip/firewall/nat/add chain=dstnat action=dst-nat protocol=udp dst-port=' + awgPort + ' in-interface-list=WAN to-addresses=' + vethIP + ' to-ports=' + awgPort + ' comment=' + prefix + '-dstnat');
+        if (srvNet6) {
+            // No dst-address here on purpose: an ISP-assigned IPv6 tends to
+            // change, and the rule has to survive that. Port plus WAN is
+            // specific enough.
+            ls.push('/ipv6/firewall/filter/add chain=forward action=accept protocol=udp dst-port=' + awgPort + ' in-interface-list=WAN place-before=0 comment=' + prefix + '-awg-in6');
+            ls.push('/ipv6/firewall/nat/add chain=dstnat action=dst-nat protocol=udp dst-port=' + awgPort + ' in-interface-list=WAN to-address=' + srvNet6.vethAddrBare + ' to-ports=' + awgPort + ' comment=' + prefix + '-dstnat6');
+        }
+        ls.push('# Full-tunnel egress for direct AmneziaWG clients (no-op for site-to-site peers that don\'t route 0.0.0.0/0)');
+        ls.push('/ip/firewall/nat/add chain=srcnat action=masquerade src-address=' + tunnelCidr + ' out-interface-list=WAN comment=' + prefix + '-ft-nat');
+        ls.push('/ip/firewall/filter/add chain=forward action=accept in-interface=wg-' + prefix + ' place-before=0 comment=' + prefix + '-ft-fwd');
+        ls.push('');
+
+        ls.push('# 5. Container environment variables');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_MODE value="server"');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_LISTEN value="' +
+                (srvNet6 ? '[::]:' : ':') + awgPort + '"');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_REMOTE value="' + net.vethGw + ':' + wgPort + '"');
+        ls = ls.concat(s2sObfEnvLines(prefix, awgParams));
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_SERVER_PUB value="' + serverKey.publicKey + '"');
+        ls.push('# AWG_CLIENT_PUB stays as legacy fallback for proxy-only clients.');
+        ls.push('# AWG_CLIENT_PUBS lists real direct AmneziaWG clients for multi-peer server mode.');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_CLIENT_PUB value="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_CLIENT_PUBS value="' + clientKey.publicKey + '"');
+        if (dnsServer) ls.push('/container/envs/add list=' + prefix + '-env key=AWG_DNS value="' + dnsServer + '"');
+        if (dfEnabled()) ls.push('/container/envs/add list=' + prefix + '-env key=AWG_NO_DF value="1"');
+        ls.push('/container/envs/add list=' + prefix + '-env key=AWG_CONFIG_URL value="' + shareURL + '"');
+        ls.push('');
+
+        var serverCompletion = [
+            '/interface/wireguard/enable wg-' + prefix,
+        ];
+        if (netMode === 'l3' && clientLAN) {
+            serverCompletion.push('# L3 routing to client LAN');
+            serverCompletion.push('/ip/route/add dst-address=' + clientLAN + ' gateway=wg-' + prefix + ' comment=' + prefix + '-client-' + clientNum);
+        }
+        serverCompletion.push(':put "Server installation complete!"');
+        ls.push('# 6. Download, create and start container');
+        ls = ls.concat(buildContainerDownload(prefix, storageServer, serverCompletion));
+        ls.push('');
+
+        // === CLIENT CONFIG ===
+        var lc = [];
+        lc.push('# ============================================================');
+        lc.push('# AWG Proxy Client ' + clientNum + ' \u2014 MikroTik');
+        lc.push('# ' + new Date().toISOString().slice(0, 19).replace('T', ' '));
+        lc.push('# ============================================================');
+        lc.push('');
+        lc.push('# 0. Check prerequisites');
+        lc.push(':if ([:len [/system/package/find where name="container" disabled=no]] = 0) do={');
+        lc.push('  :put "Container package is not installed or disabled. Install it and reboot."');
+        lc.push('  :error "Usage: /system/device-mode/update container=yes"');
+        lc.push('}');
+        lc.push('');
+
+        if (storageClient === '__auto') {
+            lc = lc.concat(buildAutoStorage(prefix));
+            lc.push('');
+        }
+
+        lc.push('# 1. Uninstall script');
+        lc.push(buildServerUninstallSource(prefix, storageClient, getTunnelNetwork(prefix)));
+        lc.push('');
+
+        lc.push('# 2. Network infrastructure');
+        var cnet = getTunnelNetwork(prefix);
+        var cvethIP = cnet.vethAddr.replace(/\/.*/, '');
+        // Client-side WG port is purely local (the peer talks to the container over
+        // veth), so nothing has to agree on it.
+        var clientWgPort = randomWgPort();
+        var cnet6 = ipv6Enabled() ? getTunnelNetwork6(prefix) : null;
+        lc.push('/interface/veth/add name=veth-' + prefix + ' ' + vethAddrArgs(cnet, cnet6));
+        lc.push('/ip/address/add address=' + cnet.hostAddr + ' interface=veth-' + prefix);
+        lc.push('/ip/firewall/nat/add chain=srcnat action=masquerade src-address=' + cnet.natSrc);
+        if (cnet6) {
+            lc.push('# The hub is reached over IPv6 - the container needs a way out over IPv6.');
+            lc = lc.concat(containerV6Lines(prefix, cnet6, true));
+        }
+        lc.push('');
+
+        lc.push('# 3. WireGuard client');
+        lc.push('/interface/wireguard/add name=wg-' + prefix + ' private-key="' + clientKey.privateKey + '" listen-port=' + clientWgPort + ' disabled=yes');
+        lc.push('/interface/wireguard/peers/add interface=wg-' + prefix + ' public-key="' + serverKey.publicKey + '" endpoint-address=' + cvethIP + ' endpoint-port=51820 allowed-address=' + clientAllowed + ' persistent-keepalive=' + randomInt(25, 55));
+        lc.push('/ip/address/add address=' + tunClientIP + '/24 interface=wg-' + prefix);
+        lc.push('/ip/firewall/filter/add chain=input action=accept in-interface=wg-' + prefix + ' place-before=0 comment=' + prefix + '-wg-mgmt');
+        lc.push('/ip/firewall/nat/add chain=srcnat action=masquerade out-interface=wg-' + prefix);
+        lc.push('');
+
+        lc.push('# 4. Container environment variables');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_LISTEN value=":51820"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_REMOTE value="' + endpointJoin(endpoint, awgPort) + '"');
+        lc = lc.concat(s2sObfEnvLines(prefix, awgParams));
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_SERVER_PUB value="' + serverKey.publicKey + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_CLIENT_PUB value="' + clientKey.publicKey + '"');
+        if (dnsServer) lc.push('/container/envs/add list=' + prefix + '-env key=AWG_DNS value="' + dnsServer + '"');
+        if (dfEnabled()) lc.push('/container/envs/add list=' + prefix + '-env key=AWG_NO_DF value="1"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_CONFIG_URL value="' + shareURL + '"');
+        lc.push('');
+
+        var clientCompletion = [
+            '/interface/wireguard/enable wg-' + prefix,
+        ];
+        if (netMode === 'l3' && serverLAN) {
+            clientCompletion.push('# L3 routing to server LAN');
+            clientCompletion.push('/ip/route/add dst-address=' + serverLAN + ' gateway=wg-' + prefix + ' comment=' + prefix + '-srv-route');
+        }
+        clientCompletion.push(':put "Client ' + clientNum + ' installation complete!"');
+        clientCompletion.push(':put "Ping test: ' + tunServerIP + ' (server WG address)"');
+        clientCompletion.push('/ping ' + tunServerIP + ' count=5');
+        lc.push('# 5. Download, create and start container');
+        lc = lc.concat(buildContainerDownload(prefix, storageClient, clientCompletion));
+        lc.push('');
+
+        return {
+            server: ls.join('\n'),
+            client: lc.join('\n'),
+            shareURL: shareURL,
+            serverKey: serverKey,
+            clientKey: clientKey,
+            awgConf: buildAwgConf({
+                clientPriv: clientKey.privateKey,
+                serverPub: serverKey.publicKey,
+                awgParams: awgParams,
+                endpoint: endpoint,
+                awgPort: awgPort,
+                tunClientIP: tunClientIP,
+                dnsServer: dnsServer
+            })
+        };
+    }
+
+    function buildAddClientCommands(urlParams, storageClient, clientLAN, netMode, dnsServer) {
+        var prefix = urlParams.prefix;
+        var clientNum = urlParams.nextClient;
+        var tunnelCidr = urlParams.tunnelSubnet;
+        var tun = parseSlash24(tunnelCidr);
+        var tunServerIP = tun.ip(1);
+        var tunClientIP = tun.ip(clientNum + 1);
+        var endpoint = urlParams.endpoint;
+        var awgPort = urlParams.awgPort;
+        var wgPort = urlParams.wgPort;
+        var serverWgPubKey = urlParams.serverWgPubKey;
+        var awgParams = {jc:urlParams.jc,jmin:urlParams.jmin,jmax:urlParams.jmax,s1:urlParams.s1,s2:urlParams.s2,s3:urlParams.s3,s4:urlParams.s4,h1:urlParams.h1,h2:urlParams.h2,h3:urlParams.h3,h4:urlParams.h4,level:urlParams.level,hpKey:urlParams.hpKey,cps:null,chain:[]};
+        // Fresh CPS templates for this client (v1.5); not carried in the URL.
+        awgParams.cps = generateCPSTemplates(randomInt(2, 3));
+
+        var clientKey = generateWgKeyPair();
+
+        var serverAllowed = tunClientIP + '/32';
+        if (clientLAN) serverAllowed += ',' + clientLAN;
+
+        var clientAllowed = tunnelCidr;
+        if (urlParams.serverLAN) clientAllowed += ',' + urlParams.serverLAN;
+
+        // Commands to run on SERVER
+        var lu = [];
+        lu.push('# ============================================================');
+        lu.push('# Add client ' + clientNum + ' to server \u2014 run on HEAD MikroTik');
+        lu.push('# ============================================================');
+        lu.push('/interface/wireguard/peers/add interface=wg-' + prefix + ' public-key="' + clientKey.publicKey + '" allowed-address=' + serverAllowed + ' comment=' + prefix + '-client-' + clientNum);
+        lu.push('# Keep AWG_CLIENT_PUB as legacy fallback; append this real client key to AWG_CLIENT_PUBS for direct AWG clients.');
+        lu.push('/container/envs/set [find where list="' + prefix + '-env" key="AWG_CLIENT_PUBS"] value=([/container/envs/get [find where list="' + prefix + '-env" key="AWG_CLIENT_PUBS"] value] . "," . "' + clientKey.publicKey + '")');
+        if (netMode === 'l3' && clientLAN) {
+            lu.push('/ip/route/add dst-address=' + clientLAN + ' gateway=wg-' + prefix + ' comment=' + prefix + '-client-' + clientNum);
+        }
+
+        // Full client config
+        var lc = [];
+        lc.push('# ============================================================');
+        lc.push('# AWG Proxy Client ' + clientNum + ' \u2014 MikroTik');
+        lc.push('# ' + new Date().toISOString().slice(0, 19).replace('T', ' '));
+        lc.push('# ============================================================');
+        lc.push('');
+        lc.push('# 0. Check prerequisites');
+        lc.push(':if ([:len [/system/package/find where name="container" disabled=no]] = 0) do={');
+        lc.push('  :put "Container package is not installed or disabled. Install it and reboot."');
+        lc.push('  :error "Usage: /system/device-mode/update container=yes"');
+        lc.push('}');
+        lc.push('');
+
+        if (storageClient === '__auto') {
+            lc = lc.concat(buildAutoStorage(prefix));
+            lc.push('');
+        }
+
+        lc.push('# 1. Uninstall script');
+        lc.push(buildServerUninstallSource(prefix, storageClient, getTunnelNetwork(prefix)));
+        lc.push('');
+
+        var cnet = getTunnelNetwork(prefix);
+        var cvethIP = cnet.vethAddr.replace(/\/.*/, '');
+        // Local-only port, same reasoning as in buildServerCommands.
+        var clientWgPort = randomWgPort();
+        var cnet6 = ipv6Enabled() ? getTunnelNetwork6(prefix) : null;
+        lc.push('# 2. Network infrastructure');
+        lc.push('/interface/veth/add name=veth-' + prefix + ' ' + vethAddrArgs(cnet, cnet6));
+        lc.push('/ip/address/add address=' + cnet.hostAddr + ' interface=veth-' + prefix);
+        lc.push('/ip/firewall/nat/add chain=srcnat action=masquerade src-address=' + cnet.natSrc);
+        if (cnet6) {
+            lc.push('# The hub is reached over IPv6 - the container needs a way out over IPv6.');
+            lc = lc.concat(containerV6Lines(prefix, cnet6, true));
+        }
+        lc.push('');
+
+        lc.push('# 3. WireGuard client');
+        lc.push('/interface/wireguard/add name=wg-' + prefix + ' private-key="' + clientKey.privateKey + '" listen-port=' + clientWgPort + ' disabled=yes');
+        lc.push('/interface/wireguard/peers/add interface=wg-' + prefix + ' public-key="' + serverWgPubKey + '" endpoint-address=' + cvethIP + ' endpoint-port=51820 allowed-address=' + clientAllowed + ' persistent-keepalive=' + randomInt(25, 55));
+        lc.push('/ip/address/add address=' + tunClientIP + '/24 interface=wg-' + prefix);
+        lc.push('/ip/firewall/filter/add chain=input action=accept in-interface=wg-' + prefix + ' place-before=0 comment=' + prefix + '-wg-mgmt');
+        lc.push('/ip/firewall/nat/add chain=srcnat action=masquerade out-interface=wg-' + prefix);
+        lc.push('');
+
+        lc.push('# 4. Container environment variables');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_LISTEN value=":51820"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_REMOTE value="' + endpointJoin(endpoint, awgPort) + '"');
+        lc = lc.concat(s2sObfEnvLines(prefix, awgParams));
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_SERVER_PUB value="' + serverWgPubKey + '"');
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_CLIENT_PUB value="' + clientKey.publicKey + '"');
+        if (dnsServer) lc.push('/container/envs/add list=' + prefix + '-env key=AWG_DNS value="' + dnsServer + '"');
+        if (dfEnabled()) lc.push('/container/envs/add list=' + prefix + '-env key=AWG_NO_DF value="1"');
+        var nextURL = buildShareableURL(awgParams, serverWgPubKey, endpoint, awgPort, wgPort, tunnelCidr, prefix, clientNum + 1, netMode, urlParams.serverLAN);
+        lc.push('/container/envs/add list=' + prefix + '-env key=AWG_CONFIG_URL value="' + nextURL + '"');
+        lc.push('');
+
+        var addClientCompletion = [
+            '/interface/wireguard/enable wg-' + prefix,
+        ];
+        if (netMode === 'l3' && urlParams.serverLAN) {
+            addClientCompletion.push('# L3 routing to server LAN');
+            addClientCompletion.push('/ip/route/add dst-address=' + urlParams.serverLAN + ' gateway=wg-' + prefix + ' comment=' + prefix + '-srv-route');
+        }
+        addClientCompletion.push(':put "Client ' + clientNum + ' installation complete!"');
+        addClientCompletion.push(':put "Ping test: ' + tunServerIP + ' (server WG address)"');
+        addClientCompletion.push('/ping ' + tunServerIP + ' count=5');
+        lc.push('# 5. Download, create and start container');
+        lc = lc.concat(buildContainerDownload(prefix, storageClient, addClientCompletion));
+        lc.push('');
+
+        return {
+            serverUpdate: lu.join('\n'),
+            client: lc.join('\n'),
+            shareURL: nextURL,
+            awgConf: buildAwgConf({
+                clientPriv: clientKey.privateKey,
+                serverPub: serverWgPubKey,
+                awgParams: awgParams,
+                endpoint: endpoint,
+                awgPort: awgPort,
+                tunClientIP: tunClientIP,
+                dnsServer: dnsServer
+            })
+        };
+    }
+
+    // Right-hand output column. For 'awg' mode it shows the importable .conf (plain text,
+    // copyable) + a visible "Download .conf" button; otherwise the client-side MikroTik script.
+    function renderClientPanel(result, netMode, clientNum) {
+        var pre = document.getElementById('srv-out-client');
+        var title = document.getElementById('srv-client-title');
+        var dlBtn = document.getElementById('srv-download-conf');
+        lastAwgConf = result.awgConf;
+        lastAwgConfName = 'awg-client-' + clientNum + '.conf';
+        if (netMode === 'awg') {
+            pre.dataset.plain = result.awgConf;
+            pre.textContent = result.awgConf;
+            title.textContent = t('srvAwgConfTitle') + ' ' + clientNum;
+            if (dlBtn) dlBtn.style.display = '';
+        } else {
+            pre.dataset.plain = result.client;
+            pre.innerHTML = highlight(result.client);
+            title.textContent = t('srvOutputClient') + ' ' + clientNum;
+            if (dlBtn) dlBtn.style.display = 'none';
+        }
+    }
+
+    function generateServer() {
+        var errs = [];
+        var mode = getDeployMode();
+        if (mode !== 'server') return;
+
+        var isAddClient = !!serverFromURL;
+        var endpoint = document.getElementById('srv-endpoint').value.trim();
+        var awgPortStr = document.getElementById('srv-awg-port').value.trim();
+        var tunnelSubnet = document.getElementById('srv-tunnel-subnet').value.trim();
+        var storageServer = document.getElementById('srv-storage-server').value;
+        var storageClient = document.getElementById('srv-storage-client').value;
+        var serverLAN = document.getElementById('srv-lan-server').value.trim();
+        var clientLAN = document.getElementById('srv-lan-client').value.trim();
+        var netMode = getSrvNetMode();
+        var clientNum = Number(document.getElementById('srv-client-num').value) || 1;
+        var dnsServer = document.getElementById('srv-dns').value.trim();
+        var prefix = getPrefixName();
+
+        // Direct AmneziaWG client: full-tunnel, no LAN merging — ignore LAN inputs entirely.
+        if (netMode === 'awg') { serverLAN = ''; clientLAN = ''; }
+
+        if (!endpoint) { errs.push(t('errSrvEndpoint')); }
+        var tun = parseSlash24(tunnelSubnet);
+        if (!tun) { errs.push(t('errSrvTunnelSubnet')); }
+        var awgPort = Number(awgPortStr);
+        if (awgPort < 1024 || awgPort > 65535) { errs.push(t('errAwgPort')); }
+        if (netMode === 'l3' && (!serverLAN || !clientLAN)) { errs.push(t('errSrvLanRequired')); }
+        if (serverLAN && !/^\d{1,3}(\.\d{1,3}){3}\/\d{1,2}$/.test(serverLAN)) { errs.push(t('errLanSubnet')); }
+        if (clientLAN && !/^\d{1,3}(\.\d{1,3}){3}\/\d{1,2}$/.test(clientLAN)) { errs.push(t('errLanSubnet')); }
+        // A direct AmneziaWG client resolves the endpoint hostname itself (via the device's
+        // system DNS, before the tunnel comes up) — no proxy-side DNS is needed. Only the
+        // client-side MikroTik container (none/l3) needs AWG_DNS to resolve a hostname endpoint.
+        var isHostname = endpoint && !/^\d{1,3}(\.\d{1,3}){3}$/.test(endpoint);
+        if (netMode !== 'awg' && isHostname && !dnsServer) { errs.push(t('errDnsRequired')); }
+
+        if (errs.length > 0) {
+            document.getElementById('errors-container').innerHTML = '<div class="alert alert-error">' + errs.map(esc).join('<br>') + '</div>';
+            return;
+        }
+        document.getElementById('errors-container').innerHTML = '';
+
+        if (isAddClient) {
+            serverFromURL.nextClient = clientNum;
+            var result = buildAddClientCommands(serverFromURL, storageClient, clientLAN, netMode, dnsServer);
+            document.getElementById('srv-out-server').style.display = 'none';
+            document.getElementById('srv-out-server').parentElement.style.display = 'none';
+            document.getElementById('srv-server-update').style.display = 'block';
+            document.getElementById('srv-out-update').dataset.plain = result.serverUpdate;
+            document.getElementById('srv-out-update').innerHTML = highlight(result.serverUpdate);
+            renderClientPanel(result, netMode, clientNum);
+            document.getElementById('srv-share-url').value = result.shareURL;
+        } else {
+            ensureServerParams();
+            serverParams.port = awgPort;
+            showSrvParams();
+            var result = buildServerCommands(endpoint, serverParams, tunnelSubnet, storageServer, storageClient, prefix, serverLAN, clientLAN, netMode, clientNum, dnsServer);
+            document.getElementById('srv-out-server').style.display = '';
+            document.getElementById('srv-out-server').parentElement.style.display = '';
+            document.getElementById('srv-server-update').style.display = 'none';
+            document.getElementById('srv-out-server').dataset.plain = result.server;
+            document.getElementById('srv-out-server').innerHTML = highlight(result.server);
+            renderClientPanel(result, netMode, clientNum);
+            document.getElementById('srv-share-url').value = result.shareURL;
+        }
+
+        document.getElementById('srv-output-section').style.display = 'block';
+        document.getElementById('output-section').style.display = 'none';
+        document.getElementById('s2s-output-section').style.display = 'none';
+        document.getElementById('srv-link-banner').style.display = 'block';
+    }
+
+    // ============================================================
+    // S2S subnet parser
+    // ============================================================
+    function parseSlash30(cidr) {
+        var m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/30$/.exec(cidr);
+        if (!m) return null;
+        var octets = [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
+        for (var i = 0; i < 4; i++) if (octets[i] > 255) return null;
+        var base = (octets[0] << 24 | octets[1] << 16 | octets[2] << 8 | octets[3]) >>> 0;
+        if (base % 4 !== 0) return null;
+        function toIP(n) { return ((n>>>24)&255)+'.'+((n>>>16)&255)+'.'+((n>>>8)&255)+'.'+(n&255); }
+        return {
+            network: cidr,
+            ip1: toIP(base + 1),
+            ip2: toIP(base + 2),
+            broadcast: toIP(base + 3),
+            addr1: toIP(base + 1) + '/30',
+            addr2: toIP(base + 2) + '/30'
+        };
+    }
+
+    // Host part of an endpoint, port excluded. An IPv6 literal is accepted with
+    // or without brackets; the brackets are put back by endpointJoin().
+    function isValidEndpointHost(s) {
+        if (!s || !s.trim()) return false;
+        s = s.trim();
+        if (isIPLiteral(s)) return true;
+        return /^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$/.test(s) && s.indexOf('..') < 0;
+    }
+
+    // ============================================================
+    // Build site-to-site commands
+    // ============================================================
+    function buildSiteToSiteCommands(endpointB, subnet, awgParams, storageA, storageB, prefix, dnsServer, lanA, lanB) {
+        var keyA = generateWgKeyPair();
+        var keyB = generateWgKeyPair();
+        var net = getTunnelNetwork(prefix);
+        var sn = parseSlash30(subnet);
+        var wgPort = awgParams.wgListenPort;
+
+        var result = {};
+
+        // === Side A (normal) ===
+        var la = [];
+        la.push('# ============================================================');
+        la.push('# AWG Proxy Site-to-Site -- Side A (client)');
+        la.push('# ' + new Date().toISOString().slice(0, 19).replace('T', ' '));
+        la.push('# ============================================================');
+        la.push('');
+        la.push('# 0. Check prerequisites');
+        la.push(':if ([:len [/system/package/find where name="container" disabled=no]] = 0) do={');
+        la.push('  :put "Container package is not installed or disabled. Install it and reboot."');
+        la.push('  :error "Usage: /system/device-mode/update container=yes"');
+        la.push('}');
+        la.push('');
+
+        if (storageA === '__auto') {
+            la = la.concat(buildAutoStorage(prefix));
+            la.push('');
+        }
+
+        // Uninstall script
+        la.push('# 1. Uninstall script');
+        la.push(buildS2SUninstallSource(prefix, storageA, net, false));
+        la.push('');
+
+        // Network
+        var net6 = ipv6Enabled() ? getTunnelNetwork6(prefix) : null;
+        la.push('# 2. Network infrastructure');
+        la.push('/interface/veth/add name=veth-' + prefix + ' ' + vethAddrArgs(net, net6));
+        la.push('/ip/address/add address=' + net.hostAddr + ' interface=veth-' + prefix);
+        la.push('/ip/firewall/nat/add chain=srcnat action=masquerade src-address=' + net.natSrc);
+        if (net6) {
+            la.push('# Side B is reached over IPv6 - the container needs a way out over IPv6.');
+            la = la.concat(containerV6Lines(prefix, net6, true));
+        }
+        la.push('');
+
+        // WireGuard
+        la.push('# 3. WireGuard interface');
+        la.push('/interface/wireguard/add name=wg-' + prefix + ' private-key="' + keyA.privateKey + '" listen-port=' + wgPort + ' disabled=yes');
+        la.push('/interface/wireguard/peers/add interface=wg-' + prefix + ' public-key="' + keyB.publicKey + '" endpoint-address=' + net.vethAddr.replace(/\/.*/, '') + ' endpoint-port=51820 allowed-address=' + subnet + (lanB ? ',' + lanB : '') + ' persistent-keepalive=' + randomInt(25, 55));
+        la.push('/ip/address/add address=' + sn.addr1 + ' interface=wg-' + prefix);
+        la.push('/ip/firewall/filter/add chain=input action=accept in-interface=wg-' + prefix + ' place-before=0 comment=' + prefix + '-wg-mgmt');
+        if (lanB) la.push('/ip/route/add dst-address=' + lanB + ' gateway=wg-' + prefix + ' comment=' + prefix + '-s2s-lan');
+        la.push('');
+
+        // Container env
+        la.push('# 4. Container environment variables');
+        la.push('/container/envs/add list=' + prefix + '-env key=AWG_LISTEN value=":51820"');
+        la.push('/container/envs/add list=' + prefix + '-env key=AWG_REMOTE value="' + endpointJoin(endpointB, awgParams.port) + '"');
+        la = la.concat(s2sObfEnvLines(prefix, awgParams));
+        la.push('/container/envs/add list=' + prefix + '-env key=AWG_SERVER_PUB value="' + keyB.publicKey + '"');
+        la.push('/container/envs/add list=' + prefix + '-env key=AWG_CLIENT_PUB value="' + keyA.publicKey + '"');
+        if (dnsServer) la.push('/container/envs/add list=' + prefix + '-env key=AWG_DNS value="' + dnsServer + '"');
+        if (dfEnabled()) la.push('/container/envs/add list=' + prefix + '-env key=AWG_NO_DF value="1"');
+        la.push('');
+
+        // Download + start
+        la.push('# 5. Download, create and start container');
+        la = la.concat(buildContainerDownload(prefix, storageA, [
+            '/interface/wireguard/enable wg-' + prefix,
+            ':put "Side A installation complete!"',
+            ':put "Ping test: ' + sn.ip2 + ' (Side B WG address)"',
+            '/ping ' + sn.ip2 + ' count=5',
+        ]));
+        la.push('');
+
+        // === Side B (reverse) ===
+        var lb = [];
+        lb.push('# ============================================================');
+        lb.push('# AWG Proxy Site-to-Site -- Side B (server)');
+        lb.push('# ' + new Date().toISOString().slice(0, 19).replace('T', ' '));
+        lb.push('# ============================================================');
+        lb.push('');
+        lb.push('# 0. Check prerequisites');
+        lb.push(':if ([:len [/system/package/find where name="container" disabled=no]] = 0) do={');
+        lb.push('  :put "Container package is not installed or disabled. Install it and reboot."');
+        lb.push('  :error "Usage: /system/device-mode/update container=yes"');
+        lb.push('}');
+        lb.push('');
+
+        if (storageB === '__auto') {
+            lb = lb.concat(buildAutoStorage(prefix));
+            lb.push('');
+        }
+
+        // Uninstall
+        lb.push('# 1. Uninstall script');
+        lb.push(buildS2SUninstallSource(prefix, storageB, net, true));
+        lb.push('');
+
+        // Network
+        lb.push('# 2. Network infrastructure');
+        lb.push('/interface/veth/add name=veth-' + prefix + ' ' + vethAddrArgs(net, net6));
+        lb.push('/ip/address/add address=' + net.hostAddr + ' interface=veth-' + prefix);
+        lb.push('/ip/firewall/nat/add chain=srcnat action=masquerade src-address=' + net.natSrc);
+        if (net6) {
+            lb.push('# Side A dials this side over IPv6 - give the container an IPv6 leg.');
+            lb = lb.concat(containerV6Lines(prefix, net6, false));
+        }
+        lb.push('');
+
+        // dst-nat + forward accept for AWG port
+        lb.push('# 3. Firewall: allow and forward AWG port');
+        lb.push('/ip/firewall/filter/add chain=forward action=accept protocol=udp dst-port=' + awgParams.port + ' in-interface-list=WAN place-before=0 comment=' + prefix + '-awg-in');
+        lb.push('/ip/firewall/filter/add chain=input action=accept protocol=udp src-address=' + net.vethAddr.replace(/\/.*/, '') + ' dst-port=' + wgPort + ' place-before=0 comment=' + prefix + '-wg-in');
+        lb.push('/ip/firewall/nat/add chain=dstnat action=dst-nat protocol=udp dst-port=' + awgParams.port + ' in-interface-list=WAN to-addresses=' + net.vethAddr.replace(/\/.*/, '') + ' to-ports=' + awgParams.port + ' comment=' + prefix + '-dstnat');
+        if (net6) {
+            lb.push('/ipv6/firewall/filter/add chain=forward action=accept protocol=udp dst-port=' + awgParams.port + ' in-interface-list=WAN place-before=0 comment=' + prefix + '-awg-in6');
+            lb.push('/ipv6/firewall/nat/add chain=dstnat action=dst-nat protocol=udp dst-port=' + awgParams.port + ' in-interface-list=WAN to-address=' + net6.vethAddrBare + ' to-ports=' + awgParams.port + ' comment=' + prefix + '-dstnat6');
+        }
+        lb.push('');
+
+        // WireGuard
+        lb.push('# 4. WireGuard interface');
+        lb.push('/interface/wireguard/add name=wg-' + prefix + ' private-key="' + keyB.privateKey + '" listen-port=' + wgPort + ' disabled=yes');
+        lb.push('/interface/wireguard/peers/add interface=wg-' + prefix + ' public-key="' + keyA.publicKey + '" allowed-address=' + subnet + (lanA ? ',' + lanA : ''));
+        lb.push('/ip/address/add address=' + sn.addr2 + ' interface=wg-' + prefix);
+        if (lanA) lb.push('/ip/route/add dst-address=' + lanA + ' gateway=wg-' + prefix + ' comment=' + prefix + '-s2s-lan');
+        lb.push('');
+
+        // Container env
+        lb.push('# 5. Container environment variables');
+        lb.push('/container/envs/add list=' + prefix + '-env key=AWG_LISTEN value="' + (net6 ? '[::]:' : ':') + awgParams.port + '"');
+        lb.push('/container/envs/add list=' + prefix + '-env key=AWG_REMOTE value="' + net.vethGw + ':' + wgPort + '"');
+        lb.push('/container/envs/add list=' + prefix + '-env key=AWG_MODE value="reverse"');
+        lb = lb.concat(s2sObfEnvLines(prefix, awgParams));
+        lb.push('/container/envs/add list=' + prefix + '-env key=AWG_SERVER_PUB value="' + keyB.publicKey + '"');
+        lb.push('/container/envs/add list=' + prefix + '-env key=AWG_CLIENT_PUB value="' + keyA.publicKey + '"');
+        if (dfEnabled()) lb.push('/container/envs/add list=' + prefix + '-env key=AWG_NO_DF value="1"');
+        lb.push('');
+
+        // Download + start
+        lb.push('# 6. Download, create and start container');
+        lb = lb.concat(buildContainerDownload(prefix, storageB, [
+            '/interface/wireguard/enable wg-' + prefix,
+            ':put "Side B installation complete!"',
+            ':put "Ping test: ' + sn.ip1 + ' (Side A WG address)"',
+            '/ping ' + sn.ip1 + ' count=5',
+        ]));
+        lb.push('');
+
+        result.sideA = la.join('\n');
+        result.sideB = lb.join('\n');
+        result.uninstallA = '# To uninstall Side A:\n/system/script/run ' + prefix + '-uninstall';
+        result.uninstallB = '# To uninstall Side B:\n/system/script/run ' + prefix + '-uninstall';
+        result.keyA = keyA;
+        result.keyB = keyB;
+        return result;
+    }
+
+    function buildS2SUninstallSource(prefix, storageDisk, net, hasNat) {
+        var isAuto = storageDisk === '__auto';
+        var src = [];
+        src.push('/system/script/add name=' + prefix + '-uninstall comment=' + prefix + ' source={');
+        src.push('  :put "Uninstalling AWG Proxy Site-to-Site..."');
+        src.push('  :log info "Uninstalling AWG Proxy Site-to-Site..."');
+        if (isAuto) {
+            src.push('  :local storageDisk "disk1"');
+            src.push('  :do { :set storageDisk [/container/envs/get [find where list="' + prefix + '-env" key="AWG_STORAGE"] value] } on-error={}');
+            src.push('  :local prevTmpdir ""');
+            src.push('  :if ($storageDisk != "disk1") do={');
+            src.push('    :do { :set prevTmpdir [/container/envs/get [find where list="' + prefix + '-env" key="AWG_PREV_TMPDIR"] value] } on-error={}');
+            src.push('  }');
+        } else if (storageDisk !== 'disk1') {
+            src.push('  :local prevTmpdir ""');
+            src.push('  :do { :set prevTmpdir [/container/envs/get [find where list="' + prefix + '-env" key="AWG_PREV_TMPDIR"] value] } on-error={}');
+        }
+        src.push('  /container/stop [find where interface=veth-' + prefix + ']');
+        src.push('  :delay 7s');
+        src.push('  /container/remove [find where interface=veth-' + prefix + ']');
+        src.push('  /container/envs/remove [find where list="' + prefix + '-env"]');
+        if (isAuto) {
+            src.push('  :if ($storageDisk != "disk1") do={');
+            src.push('    /container/config set tmpdir=$prevTmpdir');
+            src.push('  }');
+        } else if (storageDisk !== 'disk1') {
+            src.push('  /container/config set tmpdir=$prevTmpdir');
+        }
+        if (hasNat) {
+            src.push('  :do { /ip/firewall/filter/remove [find where comment=' + prefix + '-awg-in] } on-error={}');
+            src.push('  :do { /ip/firewall/filter/remove [find where comment=' + prefix + '-wg-in] } on-error={}');
+            src.push('  /ip/firewall/nat/remove [find where comment=' + prefix + '-dstnat]');
+        }
+        src.push('  :do { /ip/firewall/filter/remove [find where comment=' + prefix + '-wg-mgmt] } on-error={}');
+        src.push('  /ip/firewall/nat/remove [find where src-address="' + net.natSrc + '"]');
+        src.push('  /ip/address/remove [find where address="' + net.hostAddr + '"]');
+        src = src.concat(containerV6UninstallLines(prefix, '  '));
+        src.push('  /interface/veth/remove [find where name="veth-' + prefix + '"]');
+        src.push('  /interface/wireguard/peers/remove [find where interface="wg-' + prefix + '"]');
+        src.push('  /ip/address/remove [find where interface="wg-' + prefix + '"]');
+        src.push('  /interface/wireguard/remove [find where name="wg-' + prefix + '"]');
+        src.push('  :do { /file/remove [find where name~"' + prefix + '.+tar"] } on-error={}');
+        if (isAuto) {
+            src.push('  :do { /file/remove [find where name=($storageDisk . "/' + prefix + '")] } on-error={}');
+            src.push('  :if ($storageDisk != "disk1") do={');
+            src.push('    :do { /file/remove [find where name~($storageDisk . "/pull")] } on-error={}');
+            src.push('  }');
+        } else {
+            src.push('  :do { /file/remove [find where name="' + storageDisk + '/' + prefix + '"] } on-error={}');
+            if (storageDisk !== 'disk1') {
+                src.push('  :do { /file/remove [find where name~"' + storageDisk + '/pull"] } on-error={}');
+            }
+        }
+        src.push('  :put "Uninstall complete!"');
+        src.push('  :log info "Uninstall complete!"');
+        src.push('  /system/script/remove [find where name=' + prefix + '-uninstall]');
+        src.push('}');
+        return src.join('\n');
+    }
+
+    function buildContainerDownload(prefix, storageDisk, completionLines) {
+        var isAuto = storageDisk === '__auto';
+        var isExternal = storageDisk !== 'disk1' && !isAuto;
+        var lines = [];
+
+        if (isAuto) {
+            lines.push('/container/envs/add list=' + prefix + '-env key=AWG_STORAGE value=$awgDisk');
+            lines.push(':if ($awgDisk != "disk1") do={');
+            lines.push('  :local tmpPath ($awgDisk . "/pull")');
+            lines.push('  /container/envs/add list=' + prefix + '-env key=AWG_PREV_TMPDIR value=[/container/config/get tmpdir]');
+            lines.push('  /container/config set tmpdir=$tmpPath memory-high=200M');
+            lines.push('}');
+        } else if (isExternal) {
+            lines.push(':local prevTmpdir [/container/config/get tmpdir]');
+            lines.push('/container/envs/add list=' + prefix + '-env key=AWG_PREV_TMPDIR value=$prevTmpdir');
+            lines.push('/container/config set tmpdir=' + storageDisk + '/pull memory-high=200M');
+        }
+
+        lines.push('{');
+        lines.push('  :local arch [/system/resource/get architecture-name]');
+        lines.push('  :local ver [/system/resource/get version]');
+        lines.push('  :local dotPos [:find $ver "."]');
+        lines.push('  :local rest [:pick $ver ($dotPos + 1) [:len $ver]]');
+        lines.push('  :local endPos [:find $rest "."]');
+        lines.push('  :if ([:typeof $endPos] = "nil") do={ :set endPos [:find $rest " "] }');
+        lines.push('  :local minor [:tonum [:pick $rest 0 $endPos]]');
+        lines.push('  :local suffix ""');
+        lines.push('  :if ($minor <= 20) do={ :set suffix "-7.20-Docker" }');
+        lines.push('  :local file ""');
+        lines.push('  :if ($arch = "arm64") do={ :set file ("awg-proxy-arm64" . $suffix . ".tar.gz") }');
+        lines.push('  :if ($arch = "arm") do={');
+        lines.push('    :set file ("awg-proxy-arm" . $suffix . ".tar.gz")');
+        lines.push('    :local rbModel ""');
+        // See the note above: :parse keeps the CHR/x86 parser from choking on
+        // /system/routerboard, which only exists on RouterBOARD hardware.
+        lines.push('    :do { :set rbModel [[:parse ":return [/system/routerboard/get model]"]] } on-error={}');
+        lines.push('    :local bName [/system/resource/get board-name]');
+        lines.push('    :if ($rbModel~"E50UG" || $rbModel~"E60iUGS" || $bName~"E50UG" || $bName~"E60iUGS" || $bName~"^hEX") do={');
+        lines.push('      :set file ("awg-proxy-armv5" . $suffix . ".tar.gz")');
+        lines.push('    }');
+        lines.push('  }');
+        lines.push('  :if ($arch ~ "x86") do={ :set file ("awg-proxy-amd64" . $suffix . ".tar.gz") }');
+        lines.push('  :if ($file = "") do={ :error "Unsupported architecture: $arch" }');
+        lines.push('  :local url "' + REPO.dl + '$file"');
+
+        if (isAuto) {
+            lines.push('  :local filePath $file');
+            lines.push('  :if ($awgDisk != "disk1") do={');
+            lines.push('    :set filePath ($awgDisk . "/pull/" . $file)');
+            lines.push('    :do { /file/make-directory ($awgDisk . "/pull") } on-error={}');
+            lines.push('  }');
+        } else if (isExternal) {
+            lines.push('  :local filePath ("' + storageDisk + '/pull/" . $file)');
+            lines.push('  :do { /file/make-directory ' + storageDisk + '/pull } on-error={}');
+        } else {
+            lines.push('  :local filePath $file');
+        }
+
+        lines.push('  :if ([:len [/file/find where name=$filePath]] = 0) do={');
+        lines.push('    :local freeStorage 0');
+        if (isAuto) {
+            lines.push('    :if ($awgDisk = "disk1") do={');
+            lines.push('      :set freeStorage [/system/resource/get free-hdd-space]');
+            lines.push('    } else={');
+            lines.push('      :do { :set freeStorage [/disk/get [find where mount-point=$awgDisk] free] } on-error={}');
+            lines.push('    }');
+        } else if (storageDisk === 'disk1') {
+            lines.push('    :set freeStorage [/system/resource/get free-hdd-space]');
+        } else {
+            lines.push('    :do { :set freeStorage [/disk/get [find where mount-point="' + storageDisk + '"] free] } on-error={}');
+        }
+        lines.push('    :if ($freeStorage < 262144) do={');
+        lines.push('      :put ("WARNING: Low disk space (" . ($freeStorage / 1024) . "KB free). Need at least 256KB.")');
+        lines.push('      :error "Insufficient disk space"');
+        lines.push('    }');
+        lines.push('    :put ("Fetching: $url -> " . $filePath)');
+        lines.push('    /tool/fetch url=$url dst-path=$filePath http-max-redirect-count=10');
+        lines.push('  } else={');
+        lines.push('    :put ("File already exists: " . $filePath)');
+        lines.push('  }');
+
+        if (isAuto) {
+            lines.push('  :local rootDir ($awgDisk . "/' + prefix + '")');
+            lines.push('  /container/add file=$filePath interface=veth-' + prefix + ' envlist=' + prefix + '-env hostname=' + prefix + ' root-dir=$rootDir logging=no start-on-boot=yes comment=' + prefix);
+        } else {
+            lines.push('  /container/add file=$filePath interface=veth-' + prefix + ' envlist=' + prefix + '-env hostname=' + prefix + ' root-dir=' + storageDisk + '/' + prefix + ' logging=no start-on-boot=yes comment=' + prefix);
+        }
+
+        lines.push('  :if ($minor > 20) do={ [:parse "/container/set [find where interface=veth-' + prefix + '] shm-size=4M"] }');
+        lines.push('  /file/remove $filePath');
+        lines.push('  /container/start [find where interface=veth-' + prefix + ']');
+        lines.push('  :do { /file/remove [find where name="console-dump.txt"] } on-error={}');
+        lines.push('  :put "Waiting for container to start..."');
+        lines.push('  :delay 5s');
+        if (completionLines) {
+            for (var i = 0; i < completionLines.length; i++) {
+                lines.push('  ' + completionLines[i]);
+            }
+        }
+        lines.push('}');
+
+        return lines;
+    }
+
+    // ============================================================
+    // Main
+    // ============================================================
+    function generate() {
+        var mode = getDeployMode();
+        document.getElementById('errors-container').innerHTML = '';
+        document.getElementById('parsed-section').style.display = 'none';
+        document.getElementById('output-section').style.display = 'none';
+        document.getElementById('routing-section').style.display = 'none';
+        document.getElementById('uninstall-section').style.display = 'none';
+        document.getElementById('s2s-output-section').style.display = 'none';
+        document.getElementById('srv-output-section').style.display = 'none';
+
+        if (mode === 'site-to-site') {
+            generateS2S();
+            return;
+        }
+        if (mode === 'server') {
+            generateServer();
+            return;
+        }
+
+        var text = document.getElementById('conf-input').value;
+
+        if (!text.trim()) {
+            showError(t('errEmpty'));
+            return;
+        }
+
+        var parsed = parseConf(text);
+        var errors = validate(parsed);
+
+        // Auto-select DoH forwarder based on DNS servers in config
+        if (parsed.interface.DNS) autoSelectDoh(parsed.interface.DNS);
+        // An IPv6 endpoint settles the MTU question before anything is rendered
+        if (parsed.peer.Endpoint) syncIpv6FromEndpoint(parsed.peer.Endpoint);
+
+        var has = Object.keys(parsed.interface).length > 0 || Object.keys(parsed.peer).length > 0;
+        if (has) {
+            document.getElementById('fields-display').innerHTML = renderFields(parsed);
+            document.getElementById('parsed-section').style.display = 'block';
+        }
+
+        if (errors.length > 0) {
+            var html = '<div class="alert alert-error"><strong>' + esc(t('errTitle')) + '</strong><ul style="margin-top:6px;padding-left:18px">';
+            errors.forEach(function (e) {
+                html += '<li>' + esc(e) + '</li>';
+            });
+            html += '</ul></div>';
+            document.getElementById('errors-container').innerHTML = html;
+            return;
+        }
+
+        var prefix = getPrefixName();
+        if (!(/^[a-zA-Z0-9]$/.test(prefix) || /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,28}[a-zA-Z0-9]$/.test(prefix))) {
+            showError(t('errPrefix'));
+            return;
+        }
+
+        var scenario = getScenario();
+        var selectedServices = [];
+        var exclusions = [];
+        if (scenario === 'dns-fwd') {
+            selectedServices = getSelectedServices();
+            if (selectedServices.length === 0 && getCustomDomains().length === 0) {
+                showError(t('noServiceSelected'));
+                return;
+            }
+        } else if (scenario === 'non-ru') {
+            exclusions = getSelectedExclusions();
+        }
+
+        var mode = getDeployMode();
+        var net = getTunnelNetwork(prefix);
+        var commands;
+
+        if (mode === 'standalone') {
+            var proxyIP = document.getElementById('proxy-ip').value.trim();
+            if (!isValidIP(proxyIP)) {
+                showError(t('errProxyIp'));
+                return;
+            }
+            commands = buildStandaloneCommands(parsed, proxyIP, prefix, net, scenario);
+        } else {
+            commands = buildCommands(parsed, getStorageDisk(), prefix, net, scenario);
+        }
+
+        document.getElementById('output').dataset.plain = commands;
+        document.getElementById('output').innerHTML = highlight(commands);
+        document.getElementById('output-section').style.display = 'block';
+
+        var dohFwd = getSelectedDohForwarder();
+        var storageDisk = getStorageDisk();
+
+        // Routing section (separate for dns-fwd/non-ru to avoid terminal buffer overflow)
+        var routingSection = document.getElementById('routing-section');
+        if (scenario === 'dns-fwd' || scenario === 'non-ru') {
+            var routingCommands = '';
+            if (scenario === 'dns-fwd') {
+                routingCommands = buildDnsRoutingScenario(prefix, selectedServices, dohFwd).join('\n') + '\n';
+            } else {
+                var serverHost = parsed.peer && parsed.peer.Endpoint ? endpointHost(parsed.peer.Endpoint) : '';
+                routingCommands = buildNonRuScenario(prefix, exclusions, dohFwd, serverHost, storageDisk, mode).join('\n') + '\n';
+            }
+            document.getElementById('routing-output').dataset.plain = routingCommands;
+            document.getElementById('routing-output').innerHTML = highlight(routingCommands);
+            routingSection.style.display = 'block';
+            applyCollapse('routing-output', 'expand-routing');
+        } else {
+            routingSection.style.display = 'none';
+        }
+
+        var uninstall = [
+            '# To uninstall ' + prefix + ', run:',
+            '/system/script/run ' + prefix + '-uninstall',
+        ].join('\n');
+        document.getElementById('uninstall-output').dataset.plain = uninstall;
+        document.getElementById('uninstall-output').innerHTML = highlight(uninstall);
+        document.getElementById('uninstall-section').style.display = 'block';
+
+        applyCollapse('output', 'expand-output');
+        applyCollapse('uninstall-output', 'expand-uninstall');
+
+        setTimeout(function () {
+            document.getElementById('output-section').scrollIntoView({behavior: 'smooth', block: 'start'});
+        }, 80);
+    }
+
+    function generateS2S() {
+        var endpointB = document.getElementById('s2s-endpoint-b').value.trim();
+        var subnet = document.getElementById('s2s-tunnel-subnet').value.trim();
+        var portStr = document.getElementById('s2s-awg-port').value.trim();
+
+        if (!isValidEndpointHost(endpointB)) { showError(t('errEndpointB')); return; }
+        if (!parseSlash30(subnet)) { showError(t('errTunnelSubnet')); return; }
+
+        var port = parseInt(portStr, 10);
+        if (isNaN(port) || port < 1024 || port > 65535) { showError(t('errAwgPort')); return; }
+
+        var awgParams = ensureS2SParams();
+        awgParams.port = port;
+
+        var storageA = document.getElementById('s2s-storage-a').value;
+        var storageB = document.getElementById('s2s-storage-b').value;
+        var dnsServer = document.getElementById('s2s-dns').value.trim();
+        var lanA = document.getElementById('s2s-lan-a').value.trim();
+        var lanB = document.getElementById('s2s-lan-b').value.trim();
+
+        if (!isIPLiteral(endpointB) && !dnsServer) { showError(t('errDnsRequired')); return; }
+        if (lanA && !isValidIPCIDR(lanA)) { showError(t('errLanSubnet')); return; }
+        if (lanB && !isValidIPCIDR(lanB)) { showError(t('errLanSubnet')); return; }
+        var prefix = getPrefixName();
+
+        var result = buildSiteToSiteCommands(endpointB, subnet, awgParams, storageA, storageB, prefix, dnsServer, lanA, lanB);
+
+        document.getElementById('s2s-out-a').dataset.plain = result.sideA;
+        document.getElementById('s2s-out-a').innerHTML = highlight(result.sideA);
+        document.getElementById('s2s-out-b').dataset.plain = result.sideB;
+        document.getElementById('s2s-out-b').innerHTML = highlight(result.sideB);
+        document.getElementById('s2s-uninst-a').dataset.plain = result.uninstallA;
+        document.getElementById('s2s-uninst-a').innerHTML = highlight(result.uninstallA);
+        document.getElementById('s2s-uninst-b').dataset.plain = result.uninstallB;
+        document.getElementById('s2s-uninst-b').innerHTML = highlight(result.uninstallB);
+        document.getElementById('s2s-output-section').style.display = 'block';
+
+        setTimeout(function() {
+            document.getElementById('s2s-output-section').scrollIntoView({behavior:'smooth',block:'start'});
+        }, 80);
+    }
+
+    function showError(msg) {
+        document.getElementById('errors-container').innerHTML =
+            '<div class="alert alert-error">' + esc(msg) + '</div>';
+    }
+
+    function clearAll() {
+        document.getElementById('conf-input').value = '';
+        document.getElementById('errors-container').innerHTML = '';
+        document.getElementById('parsed-section').style.display = 'none';
+        document.getElementById('output-section').style.display = 'none';
+        document.getElementById('routing-section').style.display = 'none';
+        document.getElementById('uninstall-section').style.display = 'none';
+        document.getElementById('s2s-output-section').style.display = 'none';
+        document.getElementById('storage-select').value = '__auto';
+        document.getElementById('storage-custom').style.display = 'none';
+        document.getElementById('storage-custom').value = '';
+        document.getElementById('prefix-select').value = 'awg-proxy-1';
+        document.getElementById('netwatch-enable').checked = true;
+        document.getElementById('df-enable').checked = false;
+        document.getElementById('ipv6-enable').checked = false;
+        document.getElementById('prefix-custom').style.display = 'none';
+        document.getElementById('prefix-custom').value = '';
+        document.getElementById('proxy-ip').value = '';
+        document.getElementById('s2s-endpoint-b').value = '';
+        document.getElementById('s2s-tunnel-subnet').value = randomTunnelSubnet(30);
+        document.getElementById('s2s-storage-a').value = '__auto';
+        document.getElementById('s2s-storage-b').value = '__auto';
+        document.getElementById('s2s-dns').value = '';
+        document.getElementById('s2s-lan-a').value = '';
+        document.getElementById('s2s-lan-b').value = '';
+        s2sParams = null;
+        var radios = document.getElementsByName('deploy-mode');
+        radios[0].checked = true;
+        onModeChange();
+        var scenarioRadios = document.getElementsByName('scenario');
+        scenarioRadios[0].checked = true;
+        onScenarioChange();
+        initServiceCheckboxes();
+        initExclusionCheckboxes();
+        // Reset DoH forwarder
+        var dohRadios = document.getElementsByName('doh-fwd');
+        dohRadios[0].checked = true;
+        document.getElementById('doh-custom-url').style.display = 'none';
+        document.getElementById('doh-custom-url').value = '';
+        document.getElementById('conf-input').focus();
+    }
+
+    function downloadFile(name, content) {
+        var b = new Blob([content], { type: 'application/octet-stream' });
+        var u = URL.createObjectURL(b), a = document.createElement('a');
+        a.href = u; a.download = name;
+        document.body.appendChild(a); a.click(); document.body.removeChild(a);
+        setTimeout(function () { URL.revokeObjectURL(u); }, 1500);
+    }
+
+    function downloadAwgConf() {
+        if (lastAwgConf) downloadFile(lastAwgConfName, lastAwgConf);
+    }
+
+    function copyOutput(preId, btnId) {
+        var plain = document.getElementById(preId).dataset.plain || '';
+        if (!plain) return;
+        var btn = document.getElementById(btnId);
+        if (navigator.clipboard && navigator.clipboard.writeText) {
+            navigator.clipboard.writeText(plain).then(function () {
+                flash(btn);
+            }).catch(function () {
+                fallback(plain, btn);
+            });
+        } else {
+            fallback(plain, btn);
+        }
+    }
+
+    function fallback(text, btn) {
+        var ta = document.createElement('textarea');
+        ta.value = text;
+        ta.style.position = 'fixed';
+        ta.style.opacity = '0';
+        document.body.appendChild(ta);
+        ta.focus();
+        ta.select();
+        try {
+            document.execCommand('copy');
+            flash(btn);
+        } catch (e) {
+        }
+        document.body.removeChild(ta);
+    }
+
+    function flash(btn) {
+        var orig = btn.textContent;
+        btn.textContent = t('copied');
+        btn.classList.remove('btn-secondary');
+        btn.classList.add('btn-success');
+        setTimeout(function () {
+            btn.textContent = orig;
+            btn.classList.remove('btn-success');
+            btn.classList.add('btn-secondary');
+        }, 2000);
+    }
+
+    // ============================================================
+    // Collapsible script blocks
+    // ============================================================
+    function toggleCollapse(preId, btnId) {
+        var pre = document.getElementById(preId);
+        var btn = document.getElementById(btnId);
+        if (pre.classList.contains('collapsed')) {
+            pre.classList.remove('collapsed');
+            btn.textContent = t('btnCollapse');
+        } else {
+            pre.classList.add('collapsed');
+            btn.textContent = t('btnExpand');
+        }
+    }
+
+    function applyCollapse(preId, btnId) {
+        var pre = document.getElementById(preId);
+        var btn = document.getElementById(btnId);
+        var lineCount = (pre.textContent || '').split('\n').length;
+        if (lineCount > 18) {
+            pre.classList.add('collapsed');
+            btn.style.display = 'block';
+            btn.textContent = t('btnExpand');
+        } else {
+            pre.classList.remove('collapsed');
+            btn.style.display = 'none';
+        }
+    }
+
+    // ============================================================
+    // DoH selector handler
+    // ============================================================
+    document.getElementById('doh-selector').addEventListener('change', function(e) {
+        var custom = document.getElementById('doh-custom-url');
+        if (e.target.value === '__custom') {
+            custom.style.display = 'block';
+            custom.focus();
+        } else {
+            custom.style.display = 'none';
+        }
+    });
+
+    document.getElementById('conf-input').addEventListener('keydown', function (e) {
+        if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') generate();
+    });
+
+    // Tick the IPv6 box the moment an IPv6 endpoint is recognised, rather than
+    // waiting for Generate — the MTU note the user reads depends on it.
+    document.getElementById('conf-input').addEventListener('input', function () {
+        var m = /^\s*Endpoint\s*=\s*(\S+)\s*$/m.exec(this.value);
+        if (m) syncIpv6FromEndpoint(m[1]);
+    });
+    ['s2s-endpoint-b', 'srv-endpoint'].forEach(function (id) {
+        var el = document.getElementById(id);
+        if (el) el.addEventListener('input', function () { syncIpv6FromEndpoint(this.value); });
+    });
+
+    // Restore saved settings
+    // Initialize checkboxes
+    initServiceCheckboxes();
+    initExclusionCheckboxes();
+
+    // Apply i18n on load
+    applyI18n();
+    // Refresh scenario hint (restores info icon for dns-fwd)
+    onScenarioChange();
+
+    // Check URL params for server mode (add client)
+    (function() {
+        var urlData = parseURLParams();
+        if (!urlData) return;
+        serverFromURL = urlData;
+        // Switch to server mode
+        var radios = document.getElementsByName('deploy-mode');
+        for (var i = 0; i < radios.length; i++) {
+            if (radios[i].value === 'server') { radios[i].checked = true; break; }
+        }
+        onModeChange();
+        // Fill fields from URL
+        document.getElementById('srv-endpoint').value = urlData.endpoint;
+        document.getElementById('srv-awg-port').value = urlData.awgPort;
+        document.getElementById('srv-tunnel-subnet').value = urlData.tunnelSubnet;
+        document.getElementById('srv-client-num').value = urlData.nextClient;
+        // Set server LAN from URL
+        if (urlData.serverLAN) document.getElementById('srv-lan-server').value = urlData.serverLAN;
+        // Set net mode from URL
+        if (urlData.netMode) {
+            var netRadios = document.getElementsByName('srv-net');
+            for (var i = 0; i < netRadios.length; i++) {
+                if (netRadios[i].value === urlData.netMode) { netRadios[i].checked = true; break; }
+            }
+            onSrvNetChange();
+        }
+        // Set prefix
+        var prefixSel = document.getElementById('prefix-select');
+        var found = false;
+        for (var i = 0; i < prefixSel.options.length; i++) {
+            if (prefixSel.options[i].value === urlData.prefix) { prefixSel.value = urlData.prefix; found = true; break; }
+        }
+        if (!found) { prefixSel.value = '__custom'; document.getElementById('prefix-custom').value = urlData.prefix; document.getElementById('prefix-custom').style.display = 'block'; }
+        // Show add-client banner
+        document.getElementById('srv-add-client-banner').style.display = 'block';
+        document.getElementById('srv-add-client-info').textContent = urlData.endpoint + ':' + urlData.awgPort + ' \u2014 client #' + urlData.nextClient;
+        // Hide server storage (not needed for add-client)
+        document.getElementById('srv-storage-server').parentElement.style.display = 'none';
+    })();
+
+    // ============================================================
+    // Infographic — "How will this work?" SVG diagrams
+    // ============================================================
+
+    function _igEsc(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
+
+    function _igFormatTip(text) {
+        var lines = text.split('\n');
+        var html = '<div class="ig-tip-title">' + _igEsc(lines[0]) + '</div>';
+        for (var i = 1; i < lines.length; i++) {
+            var line = lines[i];
+            if (line.trim() === '') { html += '<br>'; continue; }
+            var escaped = _igEsc(line);
+            escaped = escaped.replace(/(\/[a-z][a-z0-9\/]*(?:\/[a-z][a-z0-9-]*)*)/g, '<code>$1</code>');
+            escaped = escaped.replace(/\b(dst-address-list=!?[A-Za-z0-9_-]+|src-address=[0-9.\/]+|out-interface=[a-z0-9*-]+|mark-(?:connection|routing)|change-mss|clamp-to-pmtu|masquerade|dstnat|accept|type=FWD)\b/g, '<code>$1</code>');
+            html += '<div>' + escaped + '</div>';
+        }
+        return html;
+    }
+
+    var IG_COLORS = {
+        wireguard:  { fill: '#dbeafe', stroke: '#3b82f6', dark: '#1e3a5f' },
+        container:  { fill: '#dcfce7', stroke: '#22c55e', dark: '#1a3a2a' },
+        dns:        { fill: '#ffedd5', stroke: '#f97316', dark: '#3a2a1a' },
+        routing:    { fill: '#ede9fe', stroke: '#8b5cf6', dark: '#2a1f4a' },
+        firewall:   { fill: '#fee2e2', stroke: '#ef4444', dark: '#3a1a1a' },
+        mangle:     { fill: '#fef9c3', stroke: '#eab308', dark: '#3a3a1a' },
+        external:   { fill: '#f3f4f6', stroke: '#9ca3af', dark: '#2a2a2a' },
+        rulist:     { fill: '#e0e7ff', stroke: '#6366f1', dark: '#1e1e4a' },
+        scheduler:  { fill: '#fce7f3', stroke: '#ec4899', dark: '#3a1a2a' },
+        manual:     { fill: '#fef3c7', stroke: '#f59e0b', dark: '#3a2f1a' }
+    };
+
+    var _igDark = function() { return window.matchMedia && window.matchMedia('(prefers-color-scheme:dark)').matches; };
+
+    function _igBlock(x, y, w, h, colorKey, labelKey, tipKey) {
+        var c = IG_COLORS[colorKey] || IG_COLORS.external;
+        var dark = _igDark();
+        return '<g class="ig-block" data-tip="' + _igEsc(t(tipKey)) + '">' +
+            '<rect x="'+x+'" y="'+y+'" width="'+w+'" height="'+h+'" rx="8" ' +
+            'fill="'+(dark?c.dark:c.fill)+'" stroke="'+c.stroke+'" stroke-width="1.5"/>' +
+            '<text x="'+(x+w/2)+'" y="'+(y+h/2)+'" text-anchor="middle" dominant-baseline="central" ' +
+            'font-size="12" font-weight="600" fill="'+(dark?'#e5e7eb':'#1f2937')+'">' +
+            _igEsc(t(labelKey)) + '</text></g>';
+    }
+
+    function _igArrow(x1, y1, x2, y2, dashed, bidir) {
+        var d;
+        if (x1 === x2 || y1 === y2) { d = 'M'+x1+','+y1+' L'+x2+','+y2; }
+        else { var m = y1+(y2-y1)/2; d = 'M'+x1+','+y1+' C'+x1+','+m+' '+x2+','+m+' '+x2+','+y2; }
+        var color = _igDark() ? '#888' : '#6b7280';
+        return '<path d="'+d+'" fill="none" stroke="'+color+'" stroke-width="1.5" ' +
+            (dashed ? 'stroke-dasharray="5,3" ' : '') +
+            'marker-end="url(#ig-arrow)"' + (bidir ? ' marker-start="url(#ig-arrow)"' : '') + '/>';
+    }
+
+    function _igDefs() {
+        var color = _igDark() ? '#888' : '#6b7280';
+        return '<defs><marker id="ig-arrow" viewBox="0 0 10 7" refX="9" refY="3.5" ' +
+            'markerWidth="8" markerHeight="6" orient="auto-start-reverse">' +
+            '<polygon points="0 0, 10 3.5, 0 7" fill="'+color+'"/></marker></defs>';
+    }
+
+    function _igSvg(w, h, content) {
+        return '<svg viewBox="0 0 '+w+' '+h+'" width="100%" style="max-width:'+w+'px" xmlns="http://www.w3.org/2000/svg" ' +
+            'font-family="-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif">' +
+            _igDefs() + content + '</svg>';
+    }
+
+    // Consistent grid layout for Container/Standalone diagrams:
+    //   Col L (x=10, w=150)   Col R (x=210, w=150)
+    //   Standard 5 rows: [10, 80, 150, 220, 290]
+    //   Non-RU 6 rows: [10, 80, 150, 220, 290, 360]
+    var CL = 10, CR = 210, BW = 150, BH = 44;
+    var RY = [10, 80, 150, 220, 290];
+    function _clc() { return CL + BW/2; }     // left col center = 85
+    function _crc() { return CR + BW/2; }     // right col center = 285
+
+    // --- Container + dns-fwd ---
+    function _igContainerDns() {
+        var s = '';
+        // Row 0: LAN + DNS
+        s += _igBlock(CL, RY[0], BW, BH, 'external', 'igLan', 'igTipLan');
+        s += _igBlock(CR, RY[0], BW, BH, 'dns', 'igDns', 'igTipDns');
+        s += _igArrow(_clc(), RY[0]+BH, _clc(), RY[1]);                    // LAN -> Mangle
+
+        // Row 1: Mangle + Routing, DNS feeds down to Mangle level then left into Mangle
+        s += _igBlock(CL, RY[1], BW, BH, 'mangle', 'igMangle', 'igTipMangle');
+        s += _igBlock(CR, RY[1], BW, BH, 'routing', 'igRouting', 'igTipRouting');
+        s += _igArrow(CL+BW, RY[1]+BH/2, CR, RY[1]+BH/2);                 // Mangle -> Routing
+        s += _igArrow(_crc(), RY[0]+BH, CL+BW, RY[1], true);              // DNS -> Mangle (dashed curve)
+
+        // Row 2: Firewall + WireGuard
+        s += _igBlock(CL, RY[2], BW, BH, 'firewall', 'igFirewall', 'igTipFirewall');
+        s += _igBlock(CR, RY[2], BW, BH, 'wireguard', 'igWireguard', 'igTipWireguard');
+        s += _igArrow(_clc(), RY[1]+BH, _clc(), RY[2]);                    // Mangle -> Firewall
+        s += _igArrow(_crc(), RY[1]+BH, _crc(), RY[2]);                    // Routing -> WireGuard
+
+        // Row 3: Internet (WAN) + Container on router
+        s += _igBlock(CL, RY[3], BW, BH, 'external', 'igInternetWan', 'igTipInternetWan');
+        s += _igBlock(CR, RY[3], BW, BH, 'container', 'igContainerOnRouter', 'igTipContainerOnRouter');
+        s += _igArrow(_clc(), RY[2]+BH, _clc(), RY[3]);                    // Firewall -> Internet
+        s += _igArrow(_crc(), RY[2]+BH, _crc(), RY[3]);                    // WG -> Container
+
+        // Row 4: AWG Server
+        s += _igBlock(CR, RY[4], BW, BH, 'external', 'igAwgServer', 'igTipAwgServer');
+        s += _igArrow(_crc(), RY[3]+BH, _crc(), RY[4]);                    // Container -> AWG
+
+        return _igSvg(CR+BW+10, RY[4]+BH+10, s);
+    }
+
+    // --- Container + tunnel ---
+    function _igContainerTunnel() {
+        var s = '';
+        // Row 0: LAN
+        s += _igBlock(CL, RY[0], BW, BH, 'external', 'igLan', 'igTipLan');
+        s += _igArrow(_clc(), RY[0]+BH, _clc(), RY[1]);                    // LAN -> Manual Config
+
+        // Row 1: Manual Config + Routing (manual)
+        s += _igBlock(CL, RY[1], BW, BH, 'manual', 'igManualConfig', 'igTipManualConfig');
+        s += _igBlock(CR, RY[1], BW, BH, 'routing', 'igRouting', 'igTipRoutingTunnel');
+        s += _igArrow(_clc(), RY[1]+BH, _clc(), RY[2]);                    // Manual -> Firewall
+        s += _igArrow(_crc(), RY[1]+BH, _crc(), RY[2]);                    // Routing -> WG
+
+        // Row 2: Firewall + WireGuard
+        s += _igBlock(CL, RY[2], BW, BH, 'firewall', 'igFirewall', 'igTipFirewall');
+        s += _igBlock(CR, RY[2], BW, BH, 'wireguard', 'igWireguard', 'igTipWireguard');
+
+        // Row 3: Internet (WAN) + Container on router
+        s += _igBlock(CL, RY[3], BW, BH, 'external', 'igInternetWan', 'igTipInternetWan');
+        s += _igBlock(CR, RY[3], BW, BH, 'container', 'igContainerOnRouter', 'igTipContainerOnRouter');
+        s += _igArrow(_clc(), RY[2]+BH, _clc(), RY[3]);                    // Firewall -> Internet
+        s += _igArrow(_crc(), RY[2]+BH, _crc(), RY[3]);                    // WG -> Container
+
+        // Row 4: AWG Server
+        s += _igBlock(CR, RY[4], BW, BH, 'external', 'igAwgServer', 'igTipAwgServer');
+        s += _igArrow(_crc(), RY[3]+BH, _crc(), RY[4]);                    // Container -> AWG
+
+        return _igSvg(CR+BW+10, RY[4]+BH+10, s);
+    }
+
+    // --- Container + non-ru ---
+    function _igContainerNonRu() {
+        var s = '';
+        // Layout: RU List + Scheduler left of main columns, connected to Mangle
+        // Extra column: x=10, main columns shifted right
+        var EX = 10, EW = 130;                  // extra col (RU List, Scheduler)
+        var ML = EX + EW + 20;                  // main left col x = 160
+        var MR = ML + BW + 50;                  // main right col x = 360
+        var mlc = ML + BW/2, mrc = MR + BW/2;  // centers
+
+        // Row 0: LAN
+        s += _igBlock(ML, RY[0], BW, BH, 'external', 'igLan', 'igTipLan');
+        s += _igArrow(mlc, RY[0]+BH, mlc, RY[1]);                          // LAN -> Mangle
+
+        // Row 0-1: Scheduler (row 0) -> RU List (row 1), left column
+        s += _igBlock(EX, RY[0], EW, BH, 'scheduler', 'igScheduler', 'igTipScheduler');
+        s += _igBlock(EX, RY[1], EW, BH, 'rulist', 'igRuList', 'igTipRuList');
+        s += _igArrow(EX+EW/2, RY[0]+BH, EX+EW/2, RY[1]);                 // Scheduler -> RU List (down)
+        s += _igArrow(EX+EW, RY[1]+BH/2, ML, RY[1]+BH/2, true);           // RU List -> Mangle (dashed, right)
+
+        // Row 1: Mangle + Routing
+        s += _igBlock(ML, RY[1], BW, BH, 'mangle', 'igMangle', 'igTipMangleNonRu');
+        s += _igBlock(MR, RY[1], BW, BH, 'routing', 'igRouting', 'igTipRouting');
+        s += _igArrow(ML+BW, RY[1]+BH/2, MR, RY[1]+BH/2);                 // Mangle -> Routing
+
+        // Row 2: Firewall + WireGuard
+        s += _igBlock(ML, RY[2], BW, BH, 'firewall', 'igFirewall', 'igTipFirewall');
+        s += _igBlock(MR, RY[2], BW, BH, 'wireguard', 'igWireguard', 'igTipWireguard');
+        s += _igArrow(mlc, RY[1]+BH, mlc, RY[2]);
+        s += _igArrow(mrc, RY[1]+BH, mrc, RY[2]);
+
+        // Row 3: Internet (WAN) + Container
+        s += _igBlock(ML, RY[3], BW, BH, 'external', 'igInternetWan', 'igTipInternetWan');
+        s += _igBlock(MR, RY[3], BW, BH, 'container', 'igContainerOnRouter', 'igTipContainerOnRouter');
+        s += _igArrow(mlc, RY[2]+BH, mlc, RY[3]);
+        s += _igArrow(mrc, RY[2]+BH, mrc, RY[3]);
+
+        // Row 4: AWG Server
+        s += _igBlock(MR, RY[4], BW, BH, 'external', 'igAwgServer', 'igTipAwgServer');
+        s += _igArrow(mrc, RY[3]+BH, mrc, RY[4]);
+
+        return _igSvg(MR+BW+10, RY[4]+BH+10, s);
+    }
+
+    // --- Standalone + dns-fwd ---
+    function _igStandaloneDns() {
+        var s = '';
+        // Row 0: LAN + DNS
+        s += _igBlock(CL, RY[0], BW, BH, 'external', 'igLan', 'igTipLan');
+        s += _igBlock(CR, RY[0], BW, BH, 'dns', 'igDns', 'igTipDns');
+        s += _igArrow(_clc(), RY[0]+BH, _clc(), RY[1]);
+
+        // Row 1: Mangle + Routing, DNS feeds down to Mangle
+        s += _igBlock(CL, RY[1], BW, BH, 'mangle', 'igMangle', 'igTipMangle');
+        s += _igBlock(CR, RY[1], BW, BH, 'routing', 'igRouting', 'igTipRouting');
+        s += _igArrow(CL+BW, RY[1]+BH/2, CR, RY[1]+BH/2);
+        s += _igArrow(_crc(), RY[0]+BH, CL+BW, RY[1], true);              // DNS -> Mangle (dashed curve)
+
+        // Row 2: Firewall + WireGuard
+        s += _igBlock(CL, RY[2], BW, BH, 'firewall', 'igFirewall', 'igTipFirewall');
+        s += _igBlock(CR, RY[2], BW, BH, 'wireguard', 'igWireguard', 'igTipWireguard');
+        s += _igArrow(_clc(), RY[1]+BH, _clc(), RY[2]);
+        s += _igArrow(_crc(), RY[1]+BH, _crc(), RY[2]);
+
+        // Row 3: Internet (WAN) + Standalone
+        s += _igBlock(CL, RY[3], BW, BH, 'external', 'igInternetWan', 'igTipInternetWan');
+        s += _igBlock(CR, RY[3], BW, BH, 'container', 'igStandalone', 'igTipStandalone');
+        s += _igArrow(_clc(), RY[2]+BH, _clc(), RY[3]);
+        s += _igArrow(_crc(), RY[2]+BH, _crc(), RY[3]);
+
+        // Row 4: AWG Server
+        s += _igBlock(CR, RY[4], BW, BH, 'external', 'igAwgServer', 'igTipAwgServer');
+        s += _igArrow(_crc(), RY[3]+BH, _crc(), RY[4]);
+
+        return _igSvg(CR+BW+10, RY[4]+BH+10, s);
+    }
+
+    // --- Standalone + tunnel ---
+    function _igStandaloneTunnel() {
+        var s = '';
+        // Row 0: LAN
+        s += _igBlock(CL, RY[0], BW, BH, 'external', 'igLan', 'igTipLan');
+        s += _igArrow(_clc(), RY[0]+BH, _clc(), RY[1]);
+
+        // Row 1: Manual Config + Routing (manual)
+        s += _igBlock(CL, RY[1], BW, BH, 'manual', 'igManualConfig', 'igTipManualConfig');
+        s += _igBlock(CR, RY[1], BW, BH, 'routing', 'igRouting', 'igTipRoutingTunnel');
+        s += _igArrow(_clc(), RY[1]+BH, _clc(), RY[2]);
+        s += _igArrow(_crc(), RY[1]+BH, _crc(), RY[2]);
+
+        // Row 2: Firewall + WireGuard
+        s += _igBlock(CL, RY[2], BW, BH, 'firewall', 'igFirewall', 'igTipFirewall');
+        s += _igBlock(CR, RY[2], BW, BH, 'wireguard', 'igWireguard', 'igTipWireguard');
+
+        // Row 3: Internet (WAN) + Standalone
+        s += _igBlock(CL, RY[3], BW, BH, 'external', 'igInternetWan', 'igTipInternetWan');
+        s += _igBlock(CR, RY[3], BW, BH, 'container', 'igStandalone', 'igTipStandalone');
+        s += _igArrow(_clc(), RY[2]+BH, _clc(), RY[3]);
+        s += _igArrow(_crc(), RY[2]+BH, _crc(), RY[3]);
+
+        // Row 4: AWG Server
+        s += _igBlock(CR, RY[4], BW, BH, 'external', 'igAwgServer', 'igTipAwgServer');
+        s += _igArrow(_crc(), RY[3]+BH, _crc(), RY[4]);
+
+        return _igSvg(CR+BW+10, RY[4]+BH+10, s);
+    }
+
+    // --- Standalone + non-ru ---
+    function _igStandaloneNonRu() {
+        var s = '';
+        var EX = 10, EW = 130;
+        var ML = EX + EW + 20, MR = ML + BW + 50;
+        var mlc = ML + BW/2, mrc = MR + BW/2;
+
+        // Row 0: LAN
+        s += _igBlock(ML, RY[0], BW, BH, 'external', 'igLan', 'igTipLan');
+        s += _igArrow(mlc, RY[0]+BH, mlc, RY[1]);
+
+        // RU List + Scheduler left of Mangle
+        s += _igBlock(EX, RY[0], EW, BH, 'scheduler', 'igScheduler', 'igTipScheduler');
+        s += _igBlock(EX, RY[1], EW, BH, 'rulist', 'igRuList', 'igTipRuList');
+        s += _igArrow(EX+EW/2, RY[0]+BH, EX+EW/2, RY[1]);
+        s += _igArrow(EX+EW, RY[1]+BH/2, ML, RY[1]+BH/2, true);
+
+        // Row 1: Mangle + Routing
+        s += _igBlock(ML, RY[1], BW, BH, 'mangle', 'igMangle', 'igTipMangleNonRu');
+        s += _igBlock(MR, RY[1], BW, BH, 'routing', 'igRouting', 'igTipRouting');
+        s += _igArrow(ML+BW, RY[1]+BH/2, MR, RY[1]+BH/2);
+
+        // Row 2: Firewall + WireGuard
+        s += _igBlock(ML, RY[2], BW, BH, 'firewall', 'igFirewall', 'igTipFirewall');
+        s += _igBlock(MR, RY[2], BW, BH, 'wireguard', 'igWireguard', 'igTipWireguard');
+        s += _igArrow(mlc, RY[1]+BH, mlc, RY[2]);
+        s += _igArrow(mrc, RY[1]+BH, mrc, RY[2]);
+
+        // Row 3: Internet (WAN) + Standalone
+        s += _igBlock(ML, RY[3], BW, BH, 'external', 'igInternetWan', 'igTipInternetWan');
+        s += _igBlock(MR, RY[3], BW, BH, 'container', 'igStandalone', 'igTipStandalone');
+        s += _igArrow(mlc, RY[2]+BH, mlc, RY[3]);
+        s += _igArrow(mrc, RY[2]+BH, mrc, RY[3]);
+
+        // Row 4: AWG Server
+        s += _igBlock(MR, RY[4], BW, BH, 'external', 'igAwgServer', 'igTipAwgServer');
+        s += _igArrow(mrc, RY[3]+BH, mrc, RY[4]);
+
+        return _igSvg(MR+BW+10, RY[4]+BH+10, s);
+    }
+
+    // --- Site-to-Site ---
+    function _igSiteToSite() {
+        var s = '';
+        var tc = _igDark() ? '#e5e7eb' : '#1f2937';
+        var SY = [24, 84, 144, 204, 264, 334];  // 6 rows: last = Internet
+        var bw = 150, bh = 40, gap = 20;
+        var ax = 10, ac = ax + bw/2;             // Side A
+        var bx = ax + bw + gap, bc = bx + bw/2;  // Side B
+        var totalW = bx + bw + 10;
+
+        // Side A (client)
+        s += '<text x="'+ac+'" y="14" text-anchor="middle" font-size="13" font-weight="700" fill="'+tc+'">' + _igEsc(t('igRouterA')) + '</text>';
+        s += _igBlock(ax, SY[0], bw, bh, 'external', 'igLan', 'igTipLan');
+        s += _igArrow(ac, SY[0]+bh, ac, SY[1], false, true);
+        s += _igBlock(ax, SY[1], bw, bh, 'routing', 'igRouting', 'igTipRoutingS2S');
+        s += _igArrow(ac, SY[1]+bh, ac, SY[2], false, true);
+        s += _igBlock(ax, SY[2], bw, bh, 'firewall', 'igFirewall', 'igTipFirewall');
+        s += _igArrow(ac, SY[2]+bh, ac, SY[3], false, true);
+        s += _igBlock(ax, SY[3], bw, bh, 'wireguard', 'igWireguard', 'igTipWireguard');
+        s += _igArrow(ac, SY[3]+bh, ac, SY[4], false, true);
+        s += _igBlock(ax, SY[4], bw, bh, 'container', 'igContainerClient', 'igTipContainer');
+
+        // Side B (server)
+        s += '<text x="'+bc+'" y="14" text-anchor="middle" font-size="13" font-weight="700" fill="'+tc+'">' + _igEsc(t('igRouterB')) + '</text>';
+        s += _igBlock(bx, SY[0], bw, bh, 'external', 'igLan', 'igTipLan');
+        s += _igArrow(bc, SY[0]+bh, bc, SY[1], false, true);
+        s += _igBlock(bx, SY[1], bw, bh, 'routing', 'igRouting', 'igTipRoutingS2S');
+        s += _igArrow(bc, SY[1]+bh, bc, SY[2], false, true);
+        s += _igBlock(bx, SY[2], bw, bh, 'firewall', 'igFirewall', 'igTipFirewallS2S');
+        s += _igArrow(bc, SY[2]+bh, bc, SY[3], false, true);
+        s += _igBlock(bx, SY[3], bw, bh, 'wireguard', 'igWireguard', 'igTipWireguard');
+        s += _igArrow(bc, SY[3]+bh, bc, SY[4], false, true);
+        s += _igBlock(bx, SY[4], bw, bh, 'container', 'igContainerServer', 'igTipContainer');
+
+        // Internet below both containers — 2 arrows down
+        var iw = 180;
+        var ix = (totalW - iw) / 2;
+        s += _igBlock(ix, SY[5], iw, bh, 'external', 'igInternet', 'igTipInternet');
+        s += _igArrow(ac, SY[4]+bh, ix+iw/2-40, SY[5]);                    // Client -> Internet
+        s += _igArrow(bc, SY[4]+bh, ix+iw/2+40, SY[5]);                    // Server -> Internet
+
+        return _igSvg(totalW, SY[5]+bh+10, s);
+    }
+
+    // --- Server (1:N Hub) ---
+    function _igServerHub() {
+        var s = '';
+        var tc = _igDark() ? '#e5e7eb' : '#1f2937';
+        var SY = [24, 84, 144, 204, 264, 334];  // 6 rows: last = Internet
+        var bw = 140, bh = 40, gap = 20;
+
+        // 3 columns: Server | Client#1 | Client#2
+        var sx = 10;                         // server x
+        var sc = sx + bw/2;                  // server center
+        var c1x = sx + bw + gap;             // client 1 x
+        var c1c = c1x + bw/2;               // client 1 center
+        var c2x = c1x + bw + gap;            // client 2 x
+        var c2c = c2x + bw/2;               // client 2 center
+        var totalW = c2x + bw + 10;
+
+        // Server (hub)
+        s += '<text x="'+sc+'" y="14" text-anchor="middle" font-size="13" font-weight="700" fill="'+tc+'">' + _igEsc(t('igServerHub')) + '</text>';
+        s += _igBlock(sx, SY[0], bw, bh, 'external', 'igLan', 'igTipLan');
+        s += _igArrow(sc, SY[0]+bh, sc, SY[1], false, true);
+        s += _igBlock(sx, SY[1], bw, bh, 'routing', 'igRouting', 'igTipRoutingSrv');
+        s += _igArrow(sc, SY[1]+bh, sc, SY[2], false, true);
+        s += _igBlock(sx, SY[2], bw, bh, 'firewall', 'igFirewall', 'igTipFirewallSrv');
+        s += _igArrow(sc, SY[2]+bh, sc, SY[3], false, true);
+        s += _igBlock(sx, SY[3], bw, bh, 'wireguard', 'igWireguard', 'igTipWireguard');
+        s += _igArrow(sc, SY[3]+bh, sc, SY[4], false, true);
+        s += _igBlock(sx, SY[4], bw, bh, 'container', 'igContainerServer', 'igTipContainer');
+
+        // Client #1
+        s += '<text x="'+c1c+'" y="14" text-anchor="middle" font-size="13" font-weight="700" fill="'+tc+'">' + _igEsc(t('igClientN')) + '</text>';
+        s += _igBlock(c1x, SY[0], bw, bh, 'external', 'igLan', 'igTipLan');
+        s += _igArrow(c1c, SY[0]+bh, c1c, SY[1], false, true);
+        s += _igBlock(c1x, SY[1], bw, bh, 'routing', 'igRouting', 'igTipRoutingS2S');
+        s += _igArrow(c1c, SY[1]+bh, c1c, SY[2], false, true);
+        s += _igBlock(c1x, SY[2], bw, bh, 'firewall', 'igFirewall', 'igTipFirewall');
+        s += _igArrow(c1c, SY[2]+bh, c1c, SY[3], false, true);
+        s += _igBlock(c1x, SY[3], bw, bh, 'wireguard', 'igWireguard', 'igTipWireguard');
+        s += _igArrow(c1c, SY[3]+bh, c1c, SY[4], false, true);
+        s += _igBlock(c1x, SY[4], bw, bh, 'container', 'igContainerClient', 'igTipContainer');
+
+        // Client #2
+        s += '<text x="'+c2c+'" y="14" text-anchor="middle" font-size="12" font-weight="700" fill="'+tc+'">' + _igEsc(t('igClient2')) + '</text>';
+        s += _igBlock(c2x, SY[0], bw, bh, 'external', 'igLan', 'igTipLan');
+        s += _igArrow(c2c, SY[0]+bh, c2c, SY[1], false, true);
+        s += _igBlock(c2x, SY[1], bw, bh, 'routing', 'igRouting', 'igTipRoutingS2S');
+        s += _igArrow(c2c, SY[1]+bh, c2c, SY[2], false, true);
+        s += _igBlock(c2x, SY[2], bw, bh, 'firewall', 'igFirewall', 'igTipFirewall');
+        s += _igArrow(c2c, SY[2]+bh, c2c, SY[3], false, true);
+        s += _igBlock(c2x, SY[3], bw, bh, 'wireguard', 'igWireguard', 'igTipWireguard');
+        s += _igArrow(c2c, SY[3]+bh, c2c, SY[4], false, true);
+        s += _igBlock(c2x, SY[4], bw, bh, 'container', 'igContainerClient', 'igTipContainer');
+
+        // Internet below all containers — 3 arrows down to it
+        var iw = 200;
+        var ix = (totalW - iw) / 2;          // centered
+        s += _igBlock(ix, SY[5], iw, bh, 'external', 'igInternet', 'igTipInternet');
+        s += _igArrow(sc, SY[4]+bh, ix+iw/2-60, SY[5]);                    // Server -> Internet
+        s += _igArrow(c1c, SY[4]+bh, ix+iw/2, SY[5]);                     // Client 1 -> Internet
+        s += _igArrow(c2c, SY[4]+bh, ix+iw/2+60, SY[5]);                  // Client 2 -> Internet
+
+        return _igSvg(totalW, SY[5]+bh+10, s);
+    }
+
+    // Tooltip: click to pin, click outside to dismiss
+    var _igPinned = null; // currently pinned block element
+    function _igShowTip(tip, block) {
+        var text = block.getAttribute('data-tip');
+        if (!text) return;
+        tip.innerHTML = _igFormatTip(text);
+        tip.classList.add('visible');
+        var r = block.getBoundingClientRect();
+        var left = r.left + r.width/2 - tip.offsetWidth/2;
+        left = Math.max(8, Math.min(left, window.innerWidth - tip.offsetWidth - 8));
+        tip.style.left = left + 'px';
+        var tipH = tip.offsetHeight;
+        var topAbove = r.top - tipH - 10;
+        if (topAbove >= 8) {
+            tip.style.top = topAbove + 'px';
+            tip.classList.add('above');
+            tip.classList.remove('below');
+        } else {
+            tip.style.top = (r.bottom + 10) + 'px';
+            tip.classList.remove('above');
+            tip.classList.add('below');
+        }
+        var arrowLeft = r.left + r.width/2 - left;
+        arrowLeft = Math.max(16, Math.min(arrowLeft, tip.offsetWidth - 16));
+        tip.style.setProperty('--arrow-left', arrowLeft + 'px');
+    }
+    function _igHideTip() {
+        var tip = document.getElementById('ig-tip');
+        if (tip) tip.classList.remove('visible');
+        _igPinned = null;
+    }
+    function _igBindTooltips(container) {
+        var tip = document.getElementById('ig-tip');
+        if (!tip) {
+            tip = document.createElement('div');
+            tip.id = 'ig-tip';
+            document.body.appendChild(tip);
+        }
+        var blocks = container.querySelectorAll('.ig-block');
+        for (var i = 0; i < blocks.length; i++) {
+            // Hover: show only if nothing is pinned
+            blocks[i].addEventListener('mouseenter', function() {
+                if (_igPinned) return;
+                _igShowTip(tip, this);
+            });
+            blocks[i].addEventListener('mouseleave', function() {
+                if (_igPinned) return;
+                tip.classList.remove('visible');
+            });
+            // Click: pin tooltip
+            blocks[i].addEventListener('click', function(e) {
+                e.stopPropagation();
+                if (_igPinned === this) { _igHideTip(); return; }
+                _igPinned = this;
+                _igShowTip(tip, this);
+            });
+        }
+        // Click outside dismisses pinned tooltip
+        document.addEventListener('click', function(e) {
+            if (!_igPinned) return;
+            if (tip.contains(e.target)) return;
+            _igHideTip();
+        });
+    }
+
+    function _igRefresh() {
+        var wrap = document.getElementById('infographic-container');
+        if (!wrap || !RY || !RY.length) return;
+        var svg = buildInfographic();
+        wrap.innerHTML = '<div class="infographic-wrap"><div class="ig-title" data-i18n="igTitle">' + t('igTitle') + '</div>' + svg + '</div>';
+        _igBindTooltips(wrap);
+    }
+
+    function buildInfographic() {
+        var mode = getDeployMode();
+        var scenario = getScenario();
+        if (mode === 'site-to-site') return _igSiteToSite();
+        if (mode === 'server') return _igServerHub();
+        if (mode === 'standalone') {
+            if (scenario === 'dns-fwd') return _igStandaloneDns();
+            if (scenario === 'non-ru') return _igStandaloneNonRu();
+            return _igStandaloneTunnel();
+        }
+        if (scenario === 'dns-fwd') return _igContainerDns();
+        if (scenario === 'non-ru') return _igContainerNonRu();
+        return _igContainerTunnel();
+    }
+
+    // Initial infographic render (after all constants are defined)
+    _igRefresh();
+</script>
+</body>
+</html>