--- /dev/null
+<!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;
+ }
+
+ .promo {
+ --promo: #15803d;
+ --promo-line: #4ade80;
+ margin-top: 14px;
+ padding: 12px 14px;
+ border: 1px solid #bbf7d0;
+ border-left: 3px solid var(--promo-line);
+ border-radius: 8px;
+ background: #f3fdf6;
+ font-size: 0.85rem;
+ line-height: 1.55;
+ }
+
+ .promo-title {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ font-weight: 700;
+ font-size: 0.92rem;
+ color: var(--promo);
+ cursor: pointer;
+ list-style: none;
+ }
+
+ .promo-title::-webkit-details-marker {
+ display: none;
+ }
+
+ .promo-title::before {
+ content: "\1F33F";
+ font-size: 1.05rem;
+ line-height: 1;
+ }
+
+ .promo-title::after {
+ content: "\25BE";
+ font-size: 0.75rem;
+ opacity: 0.7;
+ }
+
+ .promo[open] .promo-title::after {
+ content: "\25B4";
+ }
+
+ .promo-title:hover {
+ opacity: 0.8;
+ }
+
+ .promo[open] .promo-title {
+ margin-bottom: 6px;
+ }
+
+ .promo p {
+ margin: 2px 0;
+ }
+
+ .promo a {
+ color: var(--promo);
+ }
+
+ .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; }
+ .promo { --promo: #86efac; --promo-line: #4ade80; background: #14231a; border-color: #1f3d29; }
+ .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 — 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] PrivateKey = ... Address = 10.1.1.1/32 DNS = 1.1.1.1 Jc = 2 Jmin = 1 Jmax = 500 S1 = 123 S2 = 12 H1 = 12345 ... [Peer] PublicKey = ... Endpoint = 1.2.3.4:12345 AllowedIPs = 0.0.0.0/0"></textarea>
+ <details class="promo">
+ <summary class="promo-title" data-i18n="promoTitle">VPS без ограничений по трафику</summary>
+ <p>
+ <span data-i18n="promoIntro">Разместить сервер AmneziaWG можно у</span>
+ <a href="https://freakhosting.com/clientarea/aff.php?aff=730&redirectto=https://freakhosting.com/budget-vps/" target="_blank" rel="noopener">Freakhosting</a><span data-i18n="promoSection">, раздел Budget VPS</span>.
+ </p>
+ <p><b data-i18n="promoPlanLabel">Рекомендуемый тариф:</b> BVPS-2 — <span data-i18n="promoSpecs">2 vCPU, 2 ГБ RAM, 40 ГБ NVMe SSD</span>.</p>
+ <p><b data-i18n="promoAdvLabel">Ключевое преимущество:</b> <span data-i18n="promoAdv">порт 10 Гбит/с с безлимитным трафиком — идеально для VPN.</span></p>
+ <p><b data-i18n="promoPriceLabel">Цена:</b> <span data-i18n="promoPrice">всего €25 в год (около 2500 руб.; на момент проверки, цена может меняться)</span>.</p>
+ </details>
+ <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">ⓘ</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 — traffic falls back to the next tunnel (by distance) or goes direct. Re-enables the route when the tunnel recovers.</div>
+ </div>
+ <div id="v6block-row" style="margin-top:8px">
+ <label><input type="checkbox" id="v6block-enable" checked>
+ <span data-i18n="v6blockLabel">Force IPv4 (block native IPv6 past the tunnel)</span></label>
+ <div class="hint" data-i18n="v6blockHint">Only IPv4 goes into the tunnel. If the ISP hands the router native IPv6, every IPv6 flow leaves straight out of WAN and the routing policy never sees it: the site opens past the VPN, or hangs for about 10 seconds until the client falls back to IPv4. These rules reject such connections (TCP with a reset, so the fallback is instant). Anything that must keep speaking IPv6 directly goes into the ...-v6-direct address list.</div>
+ </div>
+ <div id="autoupdate-row" style="margin-top:8px">
+ <label><input type="checkbox" id="autoupdate-enable" checked>
+ <span data-i18n="autoUpdateLabel">Check for container updates daily</span></label>
+ <div class="hint" data-i18n="autoUpdateHint">Adds a scheduler that runs the <code><prefix>-update</code> script at 04:30. The script compares the published release with the installed one and only calls <code>/container/repull</code> when the version actually changed — a pull restarts the container and the tunnel is down for about ten seconds. Needs RouterOS 7.22+ and an install from the registry; the script itself is always created, so you can update by hand with <code>/system/script/run <prefix>-update</code>.</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’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 — 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) — 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="extratime-row" style="margin-top:8px">
+ <label><input type="checkbox" id="extratime-enable">
+ <span data-i18n="extraTimeLabel">Hold DNS address-list entries past their TTL</span></label>
+ <input type="text" id="extratime-value" value="30m" style="max-width:6em;margin-left:6px">
+ <div class="hint" data-i18n="extraTimeHint">Sets <code>/ip/dns address-list-extra-time</code>. Services behind Cloudflare and other CDNs hand out very short TTLs, so an address can drop out of the list while the browser is still using it and the connection leaves the tunnel. This is a <strong>router-wide</strong> DNS setting, not a per-list one: it is only applied if the router still has the default <code>0s</code>, and the uninstall script only resets it if it is still exactly the value set here.</div>
+ </div>
+ <div id="offline-row" style="margin-top:8px">
+ <label><input type="checkbox" id="offline-enable">
+ <span data-i18n="offlineLabel">Offline mode</span></label>
+ <div class="hint" data-i18n="offlineHint">Adds a step 0 to the output: a script that changes nothing and prints exactly which files to download and where to put them. Download them elsewhere, upload them to the router, then run the install. Nothing is fetched from GitHub — no registry pull, no release download, and the RU list stops waiting a minute for a name that will never resolve.</div>
+ </div>
+ <div id="dns-row" style="margin-top:8px">
+ <label for="awg-dns" data-i18n="awgDnsLabel">Container DNS server (required if the endpoint is a domain)</label>
+ <input type="text" id="awg-dns" placeholder="e.g. 192.168.88.1">
+ <div class="hint" data-i18n="awgDnsHint">Written to the container as AWG_DNS. The container image is scratch and has no resolver of its own, so a hostname endpoint cannot be resolved without this. It has to be reachable from the container <strong>without</strong> the tunnel — the router itself or a public resolver, not the DNS from the config, which usually lives inside the tunnel.</div>
+ </div>
+ <div id="path-mtu-row" style="margin-top:8px">
+ <label for="path-mtu" data-i18n="pathMtuLabel">Underlay path MTU</label>
+ <input type="number" id="path-mtu" value="1500" min="1280" max="1500" step="1">
+ <div class="hint" data-i18n="pathMtuHint">The MTU of the link the outer UDP datagram has to cross: 1500 for plain Ethernet, <strong>1492 for PPPoE</strong>, lower for PPPoE over VLAN.</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/ → disk1/)</option>
+ <option value="disk1" data-i18n="sDisk1">disk1/ — internal storage (default)</option>
+ <option value="usb1" data-i18n="sUsb1">usb1/ — USB flash drive (common)</option>
+ <option value="usb1-part1" data-i18n="sUsb1p">usb1-part1/ — USB partition</option>
+ <option value="usb2" data-i18n="sUsb2">usb2/ — USB flash drive (2nd)</option>
+ <option value="sd1" data-i18n="sSd1">sd1/ — SD / microSD card</option>
+ <option value="nvme1" data-i18n="sNvme1">nvme1/ — NVMe SSD</option>
+ <option value="sata1" data-i18n="sSata1">sata1/ — 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 — 1st tunnel (default)</option>
+ <option value="awg-proxy-2" data-i18n="pAwg2">awg-proxy-2 — 2nd tunnel</option>
+ <option value="awg-proxy-3" data-i18n="pAwg3">awg-proxy-3 — 3rd tunnel</option>
+ <option value="awg-proxy-4" data-i18n="pAwg4">awg-proxy-4 — 4th tunnel</option>
+ <option value="awg-proxy-5" data-i18n="pAwg5">awg-proxy-5 — 5th tunnel</option>
+ <option value="awg-server-1" data-i18n="pHub1" style="display:none">awg-server-1 — 1st server (default)</option>
+ <option value="awg-server-2" data-i18n="pHub2" style="display:none">awg-server-2 — 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/ → 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/ → 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/ → 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/ → 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="offline-section" class="card" style="display:none">
+ <div class="output-header">
+ <div class="card-title" style="margin-bottom:0" data-i18n="offlineTitle">Step 0: What to download</div>
+ <button class="btn btn-secondary" id="copy-offline-btn" onclick="copyOutput('offline-output','copy-offline-btn')" data-i18n="btnCopyOffline">Copy step 0</button>
+ </div>
+ <div class="alert alert-warning" data-i18n="offlineNote">Run this FIRST. It only prints file names. Download the files it names, upload them to the router, and only then run step 1.</div>
+ <pre id="offline-output"></pre>
+ </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">⚠️ 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 — 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/';
+ // Container registry and release API only exist on the GitHub side; a GitLab
+ // mirror keeps the archive path and loses the /container/repull shortcut.
+ return {
+ base: base, dl: dl,
+ ghcr: m[2] === 'github' ? 'ghcr.io/' + m[1] + '/awg-proxy' : '',
+ api: m[2] === 'github' ? 'https://api.github.com/repos/' + m[1] + '/' + p + '/releases/latest' : ''
+ };
+ }
+ return {
+ base: 'https://github.com/timbrs/amneziawg-mikrotik-c',
+ dl: 'https://github.com/timbrs/amneziawg-mikrotik-c/releases/latest/download/',
+ ghcr: 'ghcr.io/timbrs/awg-proxy',
+ api: 'https://api.github.com/repos/timbrs/amneziawg-mikrotik-c/releases/latest'
+ };
+ })();
+
+ 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.',
+ mtuNoteConf: 'The MTU from the config is kept as is — it is lower than the ceiling this transport allows.',
+ mtuNotePath: 'The WireGuard MTU is lowered to fit the underlay path MTU of',
+ extraTimeLabel: 'Hold DNS address-list entries past their TTL',
+ extraTimeHint: 'Sets /ip/dns address-list-extra-time. Services behind Cloudflare and other CDNs hand out very short TTLs, so an address can drop out of the list while the browser is still using it and the connection leaves the tunnel. This is a router-wide DNS setting, not a per-list one: it is only applied if the router still has the default 0s, and the uninstall script only resets it if it is still exactly the value set here.',
+ offlineLabel: 'Offline mode',
+ offlineHint: 'Adds a step 0 to the output: a script that changes nothing and prints exactly which files to download and where to put them. Download them elsewhere, upload them to the router, then run the install. Nothing is fetched from GitHub — no registry pull, no release download, and the RU list stops waiting a minute for a name that will never resolve.',
+ offlineTitle: 'Step 0: What to download',
+ btnCopyOffline: 'Copy step 0',
+ offlineNote: 'Run this FIRST. It only prints file names. Download the files it names, upload them to the router, and only then run step 1.',
+ awgDnsLabel: 'Container DNS server (required if the endpoint is a domain)',
+ awgDnsHint: 'Written to the container as AWG_DNS. The container image is scratch and has no resolver of its own, so a hostname endpoint cannot be resolved without this. It has to be reachable from the container without the tunnel — the router itself or a public resolver, not the DNS from the config, which usually lives inside the tunnel.',
+ pathMtuLabel: 'Underlay path MTU',
+ pathMtuHint: 'The MTU of the link the outer UDP datagram has to cross: 1500 for plain Ethernet, 1492 for PPPoE, lower for PPPoE over VLAN. With S4 set, a 1500-byte assumption on a 1492-byte path fragments every full-size packet, and a TCP MSS clamp does not help — the overflow is on the outer UDP datagram and hits QUIC too.',
+ 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.',
+ v6blockLabel: 'Force IPv4 (block native IPv6 past the tunnel)',
+ v6blockHint: 'Only IPv4 goes into the tunnel. If the ISP hands the router native IPv6, every IPv6 flow leaves straight out of WAN and the routing policy never sees it: the site opens past the VPN, or hangs for about 10 seconds until the client falls back to IPv4. These rules reject such connections (TCP with a reset, so the fallback is instant). Anything that must keep speaking IPv6 directly goes into the ...-v6-direct address list.',
+ autoUpdateLabel: 'Check for container updates daily',
+ autoUpdateHint: 'Adds a scheduler that runs the <prefix>-update script at 04:30. The script compares the published release with the installed one and only calls /container/repull when the version actually changed \u2014 a pull restarts the container and the tunnel is down for about ten seconds. Needs RouterOS 7.22+ and an install from the registry; the script itself is always created, so you can update by hand with /system/script/run <prefix>-update.',
+ 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.',
+ promoTitle: 'VPS with unlimited traffic',
+ promoIntro: 'You can host an AmneziaWG server at',
+ promoSection: ', section Budget VPS',
+ promoPlanLabel: 'Recommended plan:',
+ promoSpecs: '2 vCPU, 2 GB RAM, 40 GB NVMe SSD',
+ promoAdvLabel: 'Key advantage:',
+ promoAdv: '10 Gbps port with unlimited traffic \u2014 perfect for VPN.',
+ promoPriceLabel: 'Price:',
+ promoPrice: 'just \u20ac25 per year (about 2500 RUB; as of the last check, the price may change)',
+ 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 понижается, чтобы не было фрагментации.',
+ mtuNoteConf: 'MTU из конфига оставлен как есть — он ниже потолка, который допускает этот транспорт.',
+ mtuNotePath: 'MTU WireGuard понижен, чтобы уложиться в MTU нижележащего канала',
+ extraTimeLabel: 'Держать записи DNS address-list дольше TTL',
+ extraTimeHint: 'Задаёт /ip/dns address-list-extra-time. У сервисов за Cloudflare и другими CDN очень короткий TTL, поэтому адрес успевает выпасть из списка, пока браузер им ещё пользуется, и соединение уходит мимо туннеля. Это настройка на весь роутер, а не на конкретный список: она применяется, только если там всё ещё дефолтные 0s, а скрипт удаления сбрасывает её, только если значение осталось ровно тем, что задано здесь.',
+ offlineLabel: 'Офлайн-режим',
+ offlineHint: 'Добавляет в вывод шаг 0: скрипт, который ничего не меняет и печатает, какие именно файлы скачать и куда их положить. Скачайте их там, где есть доступ, загрузите на роутер и только потом запускайте установку. С GitHub ничего не качается — ни образ из реестра, ни релизный архив, а обновление RU-списка перестаёт ждать минуту имя, которое всё равно не зарезолвится.',
+ offlineTitle: 'Шаг 0: что скачать',
+ btnCopyOffline: 'Копировать шаг 0',
+ offlineNote: 'Запустите ЭТО первым. Скрипт только печатает имена файлов. Скачайте названные файлы, загрузите их на роутер и только после этого запускайте шаг 1.',
+ awgDnsLabel: 'DNS-сервер для контейнера (обязателен, если endpoint — домен)',
+ awgDnsHint: 'Записывается в контейнер как AWG_DNS. Образ контейнера — scratch, своего резолвера в нём нет, поэтому без этого hostname в endpoint не резолвится. Адрес должен быть доступен из контейнера без туннеля — сам роутер или публичный резолвер, но не DNS из конфига: тот обычно живёт внутри туннеля.',
+ pathMtuLabel: 'MTU нижележащего канала',
+ pathMtuHint: 'MTU канала, по которому идёт внешняя UDP-датаграмма: 1500 для обычного Ethernet, 1492 для PPPoE, меньше для PPPoE поверх VLAN. При заданном S4 расчёт на 1500 в канале с 1492 фрагментирует каждый полноразмерный пакет, и TCP MSS clamp тут не помогает — переполняется внешняя UDP-датаграмма, задевая в том числе QUIC.',
+ 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.',
+ v6blockLabel: '\u0424\u043e\u0440\u0441\u0438\u0440\u043e\u0432\u0430\u0442\u044c IPv4 (\u043d\u0435 \u043f\u0443\u0441\u043a\u0430\u0442\u044c IPv6 \u043c\u0438\u043c\u043e \u0442\u0443\u043d\u043d\u0435\u043b\u044f)',
+ v6blockHint: '\u0412 \u0442\u0443\u043d\u043d\u0435\u043b\u044c \u0437\u0430\u0432\u043e\u0434\u0438\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e IPv4. \u0415\u0441\u043b\u0438 \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u0432\u044b\u0434\u0430\u043b \u0440\u043e\u0443\u0442\u0435\u0440\u0443 \u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 IPv6, \u0432\u0435\u0441\u044c IPv6-\u0442\u0440\u0430\u0444\u0438\u043a \u0443\u0445\u043e\u0434\u0438\u0442 \u043f\u0440\u044f\u043c\u043e \u0432 WAN \u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0435\u0433\u043e \u043d\u0435 \u0432\u0438\u0434\u044f\u0442: \u0441\u0430\u0439\u0442 \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u043c\u0438\u043c\u043e VPN \u0438\u043b\u0438 \u0432\u0438\u0441\u043d\u0435\u0442 \u043e\u043a\u043e\u043b\u043e 10 \u0441\u0435\u043a\u0443\u043d\u0434, \u043f\u043e\u043a\u0430 \u043a\u043b\u0438\u0435\u043d\u0442 \u043d\u0435 \u043e\u0442\u043a\u0430\u0442\u0438\u0442\u0441\u044f \u043d\u0430 IPv4. \u041f\u0440\u0430\u0432\u0438\u043b\u0430 \u043e\u0442\u0431\u0438\u0432\u0430\u044e\u0442 \u0442\u0430\u043a\u0438\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f (TCP \u2014 \u0447\u0435\u0440\u0435\u0437 reset, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043e\u0442\u043a\u0430\u0442 \u043c\u0433\u043d\u043e\u0432\u0435\u043d\u043d\u044b\u0439). \u0422\u043e, \u0447\u0442\u043e \u0434\u043e\u043b\u0436\u043d\u043e \u0445\u043e\u0434\u0438\u0442\u044c \u043f\u043e IPv6 \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e, \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0439\u0442\u0435 \u0432 \u0430\u0434\u0440\u0435\u0441-\u043b\u0438\u0441\u0442 ...-v6-direct.',
+ autoUpdateLabel: '\u0415\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430',
+ autoUpdateHint: '\u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 scheduler, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432 04:30 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 \u0441\u043a\u0440\u0438\u043f\u0442 <prefix>-update. \u0421\u043a\u0440\u0438\u043f\u0442 \u0441\u0432\u0435\u0440\u044f\u0435\u0442 \u043e\u043f\u0443\u0431\u043b\u0438\u043a\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0440\u0435\u043b\u0438\u0437 \u0441 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044b\u043c \u0438 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 /container/repull \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0433\u0434\u0430 \u0432\u0435\u0440\u0441\u0438\u044f \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0441\u043c\u0435\u043d\u0438\u043b\u0430\u0441\u044c \u2014 \u043f\u0435\u0440\u0435\u043a\u0430\u0447\u043a\u0430 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440, \u0442\u0443\u043d\u043d\u0435\u043b\u044c \u043b\u0435\u0436\u0438\u0442 \u043e\u043a\u043e\u043b\u043e \u0434\u0435\u0441\u044f\u0442\u0438 \u0441\u0435\u043a\u0443\u043d\u0434. \u041d\u0443\u0436\u0435\u043d RouterOS 7.22+ \u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0438\u0437 \u0440\u0435\u0435\u0441\u0442\u0440\u0430; \u0441\u0430\u043c \u0441\u043a\u0440\u0438\u043f\u0442 \u0441\u043e\u0437\u0434\u0430\u0451\u0442\u0441\u044f \u0432\u0441\u0435\u0433\u0434\u0430, \u0442\u0430\u043a \u0447\u0442\u043e \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u043c\u043e\u0436\u043d\u043e \u043a\u043e\u043c\u0430\u043d\u0434\u043e\u0439 /system/script/run <prefix>-update.',
+ 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.',
+ promoTitle: 'VPS \u0431\u0435\u0437 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0439 \u043f\u043e \u0442\u0440\u0430\u0444\u0438\u043a\u0443',
+ promoIntro: '\u0420\u0430\u0437\u043c\u0435\u0441\u0442\u0438\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 AmneziaWG \u043c\u043e\u0436\u043d\u043e \u0443',
+ promoSection: ', \u0440\u0430\u0437\u0434\u0435\u043b Budget VPS',
+ promoPlanLabel: '\u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c\u044b\u0439 \u0442\u0430\u0440\u0438\u0444:',
+ promoSpecs: '2 vCPU, 2 \u0413\u0411 RAM, 40 \u0413\u0411 NVMe SSD',
+ promoAdvLabel: '\u041a\u043b\u044e\u0447\u0435\u0432\u043e\u0435 \u043f\u0440\u0435\u0438\u043c\u0443\u0449\u0435\u0441\u0442\u0432\u043e:',
+ promoAdv: '\u043f\u043e\u0440\u0442 10 \u0413\u0431\u0438\u0442/\u0441 \u0441 \u0431\u0435\u0437\u043b\u0438\u043c\u0438\u0442\u043d\u044b\u043c \u0442\u0440\u0430\u0444\u0438\u043a\u043e\u043c \u2014 \u0438\u0434\u0435\u0430\u043b\u044c\u043d\u043e \u0434\u043b\u044f VPN.',
+ promoPriceLabel: '\u0426\u0435\u043d\u0430:',
+ promoPrice: '\u0432\u0441\u0435\u0433\u043e \u20ac25 \u0432 \u0433\u043e\u0434 (\u043e\u043a\u043e\u043b\u043e 2500 \u0440\u0443\u0431.; \u043d\u0430 \u043c\u043e\u043c\u0435\u043d\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438, \u0446\u0435\u043d\u0430 \u043c\u043e\u0436\u0435\u0442 \u043c\u0435\u043d\u044f\u0442\u044c\u0441\u044f)',
+ 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();
+ // The band of server ports this client may use rides in a comment:
+ // the .conf still has to import into any WireGuard client as is,
+ // and only awg-proxy knows what to do with a list.
+ var ap = /^[#;]\s*AllowedPorts\s*=\s*(.+)$/i.exec(line);
+ if (ap) result.allowedPorts = ap[1].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', 'MTU', 'S3', 'S4', 'I1', 'I2', 'I3', 'I4', 'I5',
+ 'HeaderProtectionKey', 'ContentPaddingAddition', 'RekeyAfterTime',
+ 'RekeyTimeout', 'RejectAfterTime', 'KeepaliveTimeout', 'MaxHandshakeAttempts',
+ 'RandomTrailers', 'DisableCookies'];
+ 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'],
+ // AWG 3.1 — both are on-the-wire behaviour the proxy does implement.
+ ['RandomTrailers', 'AWG_RANDOM_TRAILERS'],
+ ['DisableCookies', 'AWG_DISABLE_COOKIES']
+ ];
+
+ 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), so the WireGuard MTU that still fits a given path is
+ // path - (20|40) - 8 - S4 - 32, rounded down to a multiple of 16.
+ // Over IPv4 on a 1500-byte path the stock 1420 costs 1484 + S4 and anything
+ // up to S4 = 16 still fits. Over IPv6 the header is 40, which puts the same
+ // config at 1504 + S4 — already over at S4 = 0, so there the MTU always has
+ // to come down. PPPoE is the other way this goes wrong: on a 1492-byte path
+ // S4 = 12 already needs 1408, and returning "default is fine" there is what
+ // fragments every full-size packet.
+ var DEFAULT_PATH_MTU = 1500;
+
+ // The largest WireGuard MTU whose outer datagram still fits the path.
+ // Always a number; 1420 is the practical ceiling (RouterOS default).
+ function wgMtuCeiling(s4, ipv6, pathMtu) {
+ var n = Number(s4) || 0;
+ var path = Number(pathMtu) || DEFAULT_PATH_MTU;
+ var m = Math.floor((path - (ipv6 ? 40 : 20) - 8 - n - 32) / 16) * 16;
+ if (m > 1420) m = 1420;
+ if (m < 1280) m = 1280;
+ return m;
+ }
+
+ // 0 means "leave mtu= off": the RouterOS default is already correct.
+ function wgMtuForS4(s4, ipv6, pathMtu) {
+ var m = wgMtuCeiling(s4, ipv6, pathMtu);
+ return m >= 1420 ? 0 : m;
+ }
+
+ // The underlay the outer UDP datagram has to cross. 1500 for plain
+ // Ethernet, 1492 for PPPoE, lower for PPPoE-over-VLAN and the like.
+ function pathMtuValue() {
+ var el = document.getElementById('path-mtu');
+ var v = el ? Number(el.value) : 0;
+ if (!v || v < 1280 || v > 1500) return DEFAULT_PATH_MTU;
+ return v;
+ }
+
+ // The resolver the container itself uses for AWG_REMOTE. The image is
+ // scratch: without this there is no /etc/resolv.conf and a hostname
+ // endpoint never resolves, which is the whole of #54.
+ function awgDnsValue() {
+ var el = document.getElementById('awg-dns');
+ return el ? el.value.trim() : '';
+ }
+
+ // /ip dns address-list-extra-time keeps a DNS-generated address-list entry
+ // alive past its TTL. Services behind a CDN publish TTLs of a few seconds,
+ // so an address can expire out of the list while a browser is still using
+ // it and the next connection leaves the tunnel (#49).
+ //
+ // It is a single router-wide setting, so it cannot simply be set and later
+ // reset: another tool, or the user, may be relying on it. Rather than
+ // stashing the old value somewhere and hoping it survives, it is only ever
+ // changed when the router still has the RouterOS default of 0s -- which
+ // makes 0s the only value an uninstall would ever restore, and makes the
+ // whole snapshot unnecessary.
+ function extraTimeEnabled() {
+ var el = document.getElementById('extratime-enable');
+ return !!(el && el.checked);
+ }
+
+ function extraTimeValue() {
+ var el = document.getElementById('extratime-value');
+ var v = el ? el.value.trim() : '';
+ return /^[0-9]+[smhd]$/.test(v) ? v : '30m';
+ }
+
+ function extraTimeSetLines(prefix) {
+ if (!extraTimeEnabled()) return [];
+ var v = extraTimeValue();
+ return [
+ '# Keep DNS address-list entries ' + v + ' past their TTL (short-TTL CDNs).',
+ '# Router-wide setting: only touched if it is still at the default.',
+ '{',
+ ' :local cur [/ip/dns/get address-list-extra-time]',
+ ' :if ($cur = 0s) do={',
+ ' /ip/dns/set address-list-extra-time=' + v,
+ ' :put "DNS address-list-extra-time set to ' + v + '"',
+ ' } else={',
+ ' :put ("DNS address-list-extra-time is already " . $cur . " - left alone")',
+ ' }',
+ '}'
+ ];
+ }
+
+ // Only undo our own change, and only if it is still ours.
+ function extraTimeResetLines(indent) {
+ if (!extraTimeEnabled()) return [];
+ var v = extraTimeValue();
+ return [
+ indent + ':if ([/ip/dns/get address-list-extra-time] = ' + v + ') do={',
+ indent + ' /ip/dns/set address-list-extra-time=0s',
+ indent + '}'
+ ];
+ }
+
+ // A firewall rule that has to sit above whatever is already there.
+ // place-before=0 needs an existing rule to place before: on an empty
+ // /ip/firewall/filter RouterOS rejects the command outright, and since the
+ // script keeps running past a failed line the install ends up half applied
+ // (#64). So the parameter is only used when there is something to place
+ // before.
+ //
+ // Adding the rule and then moving it to the top looks tidier and is wrong:
+ // with fasttrack on, index 0 is a dynamic builtin ("special dummy rule to
+ // show fasttrack counters") and /move refuses with "cannot move builtin" --
+ // an on-error would swallow that while the rule stayed at the bottom, which
+ // is a worse failure than the one being fixed. Verified on RouterOS 7.24.2:
+ // place-before=0 succeeds there, /move does not.
+ function firewallTopRule(table, args, comment) {
+ var add = table + '/add ' + args + ' comment=' + comment;
+ return [
+ ':if ([:len [' + table + '/find]] > 0) do={',
+ ' ' + add + ' place-before=0',
+ '} else={',
+ ' ' + add,
+ '}'
+ ].join('\n');
+ }
+
+ // RouterOS rejects in-interface-list=<name> when the list does not exist,
+ // one command at a time, so a missing WAN or LAN leaves half a
+ // configuration behind instead of stopping (#17, #64). Checked before the
+ // first command that changes anything.
+ function interfaceListPreflightLines(names) {
+ var lines = [];
+ for (var i = 0; i < names.length; i++) {
+ var n = names[i];
+ lines.push('# Required interface list: ' + n);
+ lines.push(':if ([:len [/interface/list/find where name="' + n + '"]] = 0) do={');
+ lines.push(' :put "ERROR: interface-list ' + n + ' does not exist - nothing has been changed."');
+ lines.push(' :put "Create it and add the matching interfaces, then run this script again:"');
+ lines.push(' :put " /interface/list/add name=' + n + '"');
+ lines.push(' :put " /interface/list/member/add list=' + n +
+ ' interface=<your ' + (n === 'WAN' ? 'uplink' : 'LAN') + ' interface>"');
+ lines.push(' :error "interface-list ' + n + ' not found"');
+ lines.push('}');
+ }
+ return lines;
+ }
+
+ // A config may carry its own MTU. That value is the server operator's
+ // choice and has to survive the import; the S4/transport ceiling may only
+ // lower it further, never raise it back up.
+ function explicitMtu(iface) {
+ var v = Number(iface && iface.MTU) || 0;
+ return (v >= 576 && v <= 1500) ? v : 0;
+ }
+
+ function effectiveWgMtu(iface, ipv6) {
+ if (ipv6 === undefined) ipv6 = ipv6Enabled();
+ var m = Math.min(wgMtuCeiling(iface.S4, ipv6, pathMtuValue()),
+ explicitMtu(iface) || 1420);
+ return m >= 1420 ? 0 : m;
+ }
+
+ function wgMtuOpt(iface) {
+ var m = effectiveWgMtu(iface);
+ 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]+$/, '');
+ }
+
+ // The port half of AWG_REMOTE may name a whole band: "443", "443,8080",
+ // "20150-20299,21500-21649". Same grammar the proxy parses — 1-65535, no
+ // reversed range, at most 32 tokens — so anything this accepts starts.
+ function isValidPortSpec(s) {
+ var toks = String(s || '').split(',');
+ if (toks.length > 32) return false;
+ for (var i = 0; i < toks.length; i++) {
+ var m = /^(\d{1,5})(?:-(\d{1,5}))?$/.exec(toks[i]);
+ if (!m) return false;
+ var lo = Number(m[1]);
+ var hi = m[2] === undefined ? lo : Number(m[2]);
+ if (lo < 1 || hi > 65535 || hi < lo) return false;
+ }
+ return true;
+ }
+
+ // Endpoint with its single port replaced by a whole spec, brackets kept.
+ function endpointWithPorts(endpoint, spec) {
+ var host = endpointHost(endpoint);
+ return host ? endpointJoin(host, spec) : String(endpoint || '');
+ }
+
+ // What AWG_REMOTE gets: the allowed band when the .conf carried a valid
+ // one, the plain endpoint otherwise. Everything else — the route that
+ // excludes the server from the tunnel, the AAAA probe — keeps reading
+ // Endpoint, which stays an ordinary host:port.
+ function remoteEndpoint(parsed) {
+ var spec = parsed.allowedPorts;
+ return (spec && isValidPortSpec(spec))
+ ? endpointWithPorts(parsed.peer.Endpoint, spec)
+ : parsed.peer.Endpoint;
+ }
+
+ 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, iface) {
+ var host = endpointHost(endpoint);
+ if (!host || isIPLiteral(host)) return []; // literal: already decided
+ var mtu = effectiveWgMtu(iface, 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);
+ html += renderMtuNote(parsed.interface);
+ 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>';
+
+ return html;
+ }
+
+ // Why the generated WireGuard interface gets the MTU it gets. Shown for
+ // every protocol, not just v3: an explicit MTU in the config and a PPPoE
+ // path both lower it on a plain v1 profile too.
+ function renderMtuNote(iface) {
+ var v6 = ipv6Enabled();
+ var path = pathMtuValue();
+ var mtu = effectiveWgMtu(iface, v6);
+ if (!mtu) return '';
+ var explicit = explicitMtu(iface);
+ var reason;
+ if (explicit && explicit <= wgMtuCeiling(iface.S4, v6, path)) reason = t('mtuNoteConf');
+ else if (path < DEFAULT_PATH_MTU) reason = t('mtuNotePath') + ' ' + path + '.';
+ else reason = t(v6 ? 'ipv6MtuNote' : 'v3MtuNote');
+ return '<div class="alert alert-warning" style="margin-top:8px">' + esc(reason) +
+ ' S4=' + esc(String(iface.S4 || 0)) +
+ (explicit ? ', MTU=' + esc(String(explicit)) : '') +
+ ' → mtu=' + mtu + '.</div>';
+ }
+
+ 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');
+ });
+
+ // --- AWG 3.1 --- awg-tools writes these as on/off, and accepts 0/1.
+ ['RandomTrailers', 'DisableCookies'].forEach(function (k) {
+ if (iface[k] && !/^(on|off|0|1|true|false)$/i.test(iface[k].trim()))
+ errors.push('[Interface] ' + k + ' -- must be on/off (or 0/1)');
+ });
+
+ ['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},
+ // MTProto dials Telegram's own ranges by IP, with no DNS lookup for the
+ // router to see, so the domains alone never route a running client (#41).
+ // Published set for AS62041/AS59930.
+ telegram: {list:'Telegram', d:['telegram.org','t.me','telesco.pe','tdesktop.com','telegram.me','telegram.dog','telegra.ph'], r:['telegram'],
+ ip:['91.108.4.0/22','91.108.8.0/22','91.108.12.0/22','91.108.16.0/22',
+ '91.108.20.0/22','91.108.56.0/22','149.154.160.0/20','185.76.151.0/24'], 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;
+ }
+
+ // Автоподбор двигает переключатель, только пока пользователь сам его не
+ // трогал: иначе Generate заново вычитывает DNS из конфига и возвращает
+ // провайдера, которого только что сменили руками.
+ var dohUserPicked = false;
+
+ function autoSelectDoh(dnsString) {
+ if (dohUserPicked) return;
+ 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';
+ document.getElementById('v6block-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, ' ') + '" data-i18n-title="dnsBrowserHint">ⓘ</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]) + '\')">×</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 v6BlockEnabled() {
+ var el = document.getElementById('v6block-enable');
+ return !!(el && el.checked);
+ }
+
+ // Нативный IPv6 провайдера обходит туннель стороной. Внутрь wg заводится
+ // только IPv4 (на интерфейсе один /ip/address), поэтому ::/0 у роутера
+ // смотрит в WAN, и весь IPv6-трафик LAN уходит напрямую — вся политика
+ // маршрутизации выше его попросту не видит. Симптом обманчивый: ICMPv6 до
+ // цели проходит, а TCP висит ~10 секунд, пока клиент не откатится на IPv4,
+ // из-за чего это выглядит как «тормозит VPN», а не как утечка мимо него.
+ //
+ // reject, а не drop: RST/admin-prohibited возвращает клиента на IPv4
+ // мгновенно, drop заставляет его досидеть таймаут. Правила дописываются в
+ // конец цепочки, то есть ПОСЛЕ defconf-accept established,related — рвутся
+ // только новые соединения. Матч по out-interface-list=WAN, поэтому соседние
+ // туннели того же роутера и veth контейнера не задеты; всё остальное, что
+ // должно ходить по IPv6 напрямую, кладётся в список <prefix>-v6-direct.
+ // Комментарии правил — латиницей: кириллица в comment роняет команду в
+ // RouterOS молча, правило просто не создаётся.
+ function buildV6ForceIpv4(prefix) {
+ var tag = prefix + '-v6-force4';
+ var list = prefix + '-v6-direct';
+ return [
+ '# Force IPv4: the tunnel carries no IPv6, so native ISP IPv6 would bypass it',
+ // v6net, а не net: в non-ru-сценарии выше уже есть свой :foreach net
+ // по приватным диапазонам, и в одном блоке имя лучше не переиспользовать.
+ ':foreach v6net in={"fc00::/7";"fe80::/10";"ff00::/8"} do={',
+ ' :do { /ipv6/firewall/address-list/add list=' + list + ' address=$v6net comment=' + tag + ' } on-error={}',
+ '}',
+ '/ipv6/firewall/filter/add chain=forward action=accept dst-address-list=' + list + ' in-interface-list=LAN comment=' + tag,
+ '/ipv6/firewall/filter/add chain=forward action=reject reject-with=tcp-reset protocol=tcp in-interface-list=LAN out-interface-list=WAN comment=' + tag,
+ '/ipv6/firewall/filter/add chain=forward action=reject reject-with=icmp-admin-prohibited in-interface-list=LAN out-interface-list=WAN comment=' + tag
+ ];
+ }
+
+ 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; });
+ }
+
+ // Plain-IP резолверы провайдера: DNS_STATICS держит их как A-записи для
+ // bootstrap DoH, они же годятся в /ip/dns/set servers=.
+ function dnsIpsForForwarder(forwarderName) {
+ if (forwarderName === '__custom') {
+ var url = document.getElementById('doh-custom-url').value.trim();
+ // Только публичный IP: адрес из /ip/dns/set пинится маршрутом в
+ // туннель, и LAN-резолвер, уехавший туда, отвечать перестанет.
+ return (isValidIP(url) && isPublicIPv4(url)) ? [url] : [];
+ }
+ return getDnsStaticsForForwarder(forwarderName).map(function(st) { return st.addr; });
+ }
+
+ function isPublicIPv4(ip) {
+ var o = ip.split('.');
+ if (o.length !== 4) return false;
+ var a = Number(o[0]), b = Number(o[1]);
+ if (a === 0 || a === 10 || a === 127 || a >= 224) return false;
+ if (a === 172 && b >= 16 && b <= 31) return false;
+ if (a === 192 && b === 168) return false;
+ if (a === 169 && b === 254) return false;
+ if (a === 100 && b >= 64 && b <= 127) return false;
+ return true;
+ }
+
+ // Серверы для /ip/dns/set: берутся из конфига, но если там публичный
+ // резолвер (1.1.1.1 и т.п.), а в панели DoH выбран другой провайдер — в
+ // скрипт уходит выбранный. Приватный DNS из конфига (10.x и прочее) не
+ // трогаем: он живёт за туннелем, подменять его провайдером нельзя.
+ function dnsServersForScript(dnsString, scenario) {
+ var servers = parseDNSServers(dnsString);
+ if (scenario !== 'dns-fwd' && scenario !== 'non-ru') return servers;
+ var detected = detectDohFromDns(dnsString);
+ if (!detected) return servers;
+ var selected = getSelectedDohForwarder();
+ if (selected === detected) return servers;
+ var repl = dnsIpsForForwarder(selected);
+ if (!repl.length) return servers;
+ var out = [], done = false;
+ for (var i = 0; i < servers.length; i++) {
+ if (DNS_IP_TO_DOH[servers[i]]) {
+ if (!done) { out = out.concat(repl); done = true; }
+ } else {
+ out.push(servers[i]);
+ }
+ }
+ return out;
+ }
+
+ 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('# ============================================================');
+ // Worth saying out loud in the script itself: this whole scheme routes on
+ // addresses RouterOS learned from DNS queries it saw. A client using DoH,
+ // Private DNS, its own resolver or a cached address never produces one,
+ // and its traffic quietly goes around the tunnel (#41).
+ lines.push('# NOTE: this routes what RouterOS learns from DNS queries it handles.');
+ lines.push('# Clients must use this router as their DNS server. A client on DoH,');
+ lines.push('# Android Private DNS, a hardcoded 8.8.8.8 or a cached address will');
+ lines.push('# not appear in these lists and will bypass the tunnel.');
+ lines.push('# To check: /ip/firewall/address-list/print where list=<Service>');
+ lines.push('# while the service is in use - an empty list means DNS, not AWG.');
+ // Both mangle chains below match on in-interface-list=LAN, and the
+ // FastTrack rework matches out-interface-list=WAN. Neither exists on
+ // every router (CHR in particular), and RouterOS fails those commands
+ // one by one while the rest of the script carries on (#17, #64).
+ lines = lines.concat(interfaceListPreflightLines(['LAN', 'WAN']));
+ 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));
+ // Services that are also reachable by bare IP (Telegram/MTProto) need
+ // their ranges seeded statically: no DNS query means no address-list
+ // entry, and the mangle rule below never matches. Tagged -excl-ip so the
+ // uninstall script sweeps them up with the rest.
+ var svcIps = buildIpEntries(prefix, selectedServices, SERVICES);
+ if (svcIps.length > 0) {
+ lines.push('# Static IP ranges for services that bypass DNS');
+ lines = lines.concat(svcIps);
+ }
+ lines = lines.concat(extraTimeSetLines(prefix));
+ 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');
+ // clamp-to-pmtu, а не число: MTU интерфейса зависит от S4 и транспорта
+ // (wgMtuForS4), захардкоженное значение с ним разъезжается. Пары
+ // /ipv6/firewall/mangle тут нет намеренно — в туннель заводится только
+ // IPv4, клемпить по IPv6 нечего; за IPv6 отвечает buildV6ForceIpv4.
+ lines.push('# MSS clamp for the tunnel (IPv4 only - nothing IPv6 is routed into it)');
+ 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');
+ if (v6BlockEnabled()) lines = lines.concat(buildV6ForceIpv4(prefix));
+ 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 offline = offlineEnabled();
+ 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('# ============================================================');
+
+ // Both mangle chains below match on in-interface-list=LAN, and the
+ // FastTrack rework matches out-interface-list=WAN. Neither exists on
+ // every router (CHR in particular), and RouterOS fails those commands
+ // one by one while the rest of the script carries on (#17, #64).
+ lines = lines.concat(interfaceListPreflightLines(['LAN', 'WAN']));
+
+ // 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 = lines.concat(extraTimeSetLines(prefix));
+ }
+
+ 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');
+ // clamp-to-pmtu, а не число: MTU интерфейса зависит от S4 и транспорта
+ // (wgMtuForS4), захардкоженное значение с ним разъезжается. Пары
+ // /ipv6/firewall/mangle тут нет намеренно — в туннель заводится только
+ // IPv4, клемпить по IPv6 нечего; за IPv6 отвечает buildV6ForceIpv4.
+ lines.push('# MSS clamp for the tunnel (IPv4 only - nothing IPv6 is routed into it)');
+ 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');
+ if (v6BlockEnabled()) lines = lines.concat(buildV6ForceIpv4(prefix));
+ 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)');
+ if (offline) {
+ // Which of the two lists the script wants depends on device-mode,
+ // which the user cannot easily predict when downloading by hand.
+ // Offline, take whichever one is actually on the router.
+ scriptSrc.push(':if (!\\$hasCache) do={');
+ scriptSrc.push(' :local altName \\"ru_ranges.rsc\\"');
+ scriptSrc.push(' :if (!\\$schedOk) do={ :set altName \\"ru_ranges_timeout.rsc\\" }');
+ scriptSrc.push(' :local altFile (\\$disk . \\"/\\" . \\$altName)');
+ scriptSrc.push(' :if ([:len [/file/find where name=\\$altFile]] > 0) do={');
+ scriptSrc.push(' :set rscFile \\$altFile');
+ scriptSrc.push(' :set hasCache true');
+ scriptSrc.push(' }');
+ scriptSrc.push('}');
+ }
+ 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('}');
+ // Offline: github.com will never resolve, and retrying it for a
+ // minute on every boot and every daily run buys nothing (#51).
+ scriptSrc.push(':local waitNet 0');
+ if (offline) {
+ scriptSrc.push(':if (!\\$hasCache) do={ :log error \\"RU list: offline install and no list file on the router\\" }');
+ } else {
+ 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('}');
+ }
+ // Offline: nothing to download, and leaving the block in would log a
+ // misleading "no network after 60s" on every boot and every daily run.
+ if (!offline) {
+ 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)
+ // ============================================================
+ // ============================================================
+ // Update script (/container/repull)
+ // ============================================================
+ // Two jobs in one script, and the order between them is the whole point.
+ //
+ // 1. Health first. A container can be alive-but-bricked: an interrupted pull
+ // leaves it with no image-id, and RouterOS can also leave root-dir empty
+ // after a "skip importing same version" pull, so it starts, dies on execvpe
+ // and stops. If the version check ran first it would answer "already on
+ // vX - nothing to pull" and walk away from a dead tunnel, every night,
+ // forever. So a container that is supposed to be running and is not gets
+ // repaired regardless of what version it claims to be.
+ // 2. Version second. repull restarts the container every single time, even
+ // when the digest has not moved, and the tunnel is down for ~10s while it
+ // does - so a healthy container is left alone unless the release moved.
+ //
+ // Nothing here touches netwatch, routes or the WireGuard interface: while the
+ // container is down the netwatch failover is what keeps the router online, and
+ // it has to keep working through every repair attempt.
+ // opts lets an existing installation - where the veth and env list were named by
+ // an older configurator - reuse this exact script instead of a hand-written copy.
+ function buildUpdateScriptSource(prefix, opts) {
+ opts = opts || {};
+ var veth = opts.veth || ('veth-' + prefix);
+ var env = opts.envlist || (prefix + '-env');
+ var name = opts.name || (prefix + '-update');
+ var comment = opts.comment || prefix;
+ var addArgs = 'interface=' + veth + ' envlist=' + env + ' hostname=' + prefix +
+ ' root-dir=$rootDirOrig logging=no start-on-boot=yes comment=' + comment;
+ var src = [];
+ src.push('/system/script/add name=' + name + ' comment=' + comment + ' source={');
+ // An installation upgraded in place keeps the previous container on the same
+ // veth, stopped, as a fallback - so "the container" is the running one, or
+ // failing that the one meant to boot, and only then whatever is left. The
+ // newest id wins the tie: that is the one a reinstall added last.
+ src.push(' :local cids [/container/find where interface=' + veth + ' and !stopped]');
+ src.push(' :if ([:len $cids] = 0) do={ :set cids [/container/find where interface=' + veth + ' and start-on-boot=yes] }');
+ src.push(' :if ([:len $cids] = 0) do={ :set cids [/container/find where interface=' + veth + '] }');
+ src.push(' :if ([:len $cids] = 0) do={');
+ src.push(' :put "Container ' + prefix + ' not found"');
+ src.push(' :log warning "' + name + ': container not found"');
+ src.push(' } else={');
+ src.push(' :local cid [:pick $cids ([:len $cids] - 1)]');
+ src.push(' :local img [/container/get $cid remote-image]');
+ src.push(' :local ver [/system/resource/get version]');
+ src.push(' :local dotPos [:find $ver "."]');
+ src.push(' :local rest [:pick $ver ($dotPos + 1) [:len $ver]]');
+ src.push(' :local endPos [:find $rest "."]');
+ src.push(' :if ([:typeof $endPos] = "nil") do={ :set endPos [:find $rest " "] }');
+ src.push(' :local minor [:tonum [:pick $rest 0 $endPos]]');
+
+ // --- how the container is doing right now -------------------------------
+ src.push(' :local wantRunning true');
+ src.push(' :do { :if ([/container/get $cid start-on-boot] != true) do={ :set wantRunning false } } on-error={}');
+ src.push(' :local healthy true');
+ src.push(' :if ([:len [/container/get $cid image-id]] = 0) do={ :set healthy false }');
+ src.push(' :if ($wantRunning) do={');
+ src.push(' :do { :if ([/container/get $cid stopped] = true) do={ :set healthy false } } on-error={}');
+ src.push(' }');
+ // A plain start is the cheapest repair and costs nothing when it works.
+ src.push(' :if (!$healthy && $wantRunning && [:len [/container/get $cid image-id]] > 0) do={');
+ src.push(' :put "Container is down - trying to start it"');
+ src.push(' :do { /container/start $cid } on-error={}');
+ src.push(' :delay 8s');
+ src.push(' :set healthy true');
+ src.push(' :do { :if ([/container/get $cid stopped] = true) do={ :set healthy false } } on-error={}');
+ src.push(' :if ($healthy) do={');
+ src.push(' :put "Container is back up"');
+ src.push(' :log info "' + prefix + ': container restarted by the update script"');
+ src.push(' }');
+ src.push(' }');
+
+ // --- a file install can still be repaired, and that repair is the migration
+ src.push(' :if ($img = "" && $minor >= 22 && !$healthy) do={');
+ src.push(' :local imgTag "latest"');
+ src.push(' :local rbModel ""');
+ src.push(' :do { :set rbModel [[:parse ":return [/system/routerboard/get model]"]] } on-error={}');
+ src.push(' :local bName [/system/resource/get board-name]');
+ src.push(' :if ($rbModel~"E50UG" || $rbModel~"E60iUGS" || $bName~"E50UG" || $bName~"E60iUGS" || $bName~"^hEX") do={');
+ src.push(' :set imgTag "latest-armv5"');
+ src.push(' }');
+ src.push(' :set img ("' + REPO.ghcr + ':" . $imgTag)');
+ src.push(' :put ("Container is down and was installed from a file - pointing it at " . $img)');
+ src.push(' :log warning "' + name + ': broken file install, switching to the registry"');
+ src.push(' :do { /container/set $cid remote-image=$img } on-error={}');
+ src.push(' }');
+
+ src.push(' :if ($img = "") do={');
+ src.push(' :put "Installed from a local image file - /container/repull only updates registry installs."');
+ src.push(' :put "Reinstall with the configurator on RouterOS 7.22+ to switch to one-command updates."');
+ src.push(' } else={');
+ src.push(' :if ($minor < 22) do={');
+ src.push(' :put ("/container/repull needs RouterOS 7.22+, this router runs " . $ver)');
+ src.push(' :if (!$healthy) do={ :log error ("' + name + ': container is down, and " . $ver . " has no /container/repull") }');
+ src.push(' } else={');
+ // Layers are unpacked through tmpdir, so it has to live on the same disk as
+ // root-dir. On a 16MB-flash router with the container on USB, the default
+ // (internal flash) has no room at all.
+ src.push(' :local rootDir [/container/get $cid root-dir]');
+ src.push(' :if ([:pick $rootDir 0 1] = "/") do={ :set rootDir [:pick $rootDir 1 [:len $rootDir]] }');
+ src.push(' :local rootDirOrig $rootDir');
+ src.push(' :local slashPos [:find $rootDir "/"]');
+ src.push(' :local disk $rootDir');
+ src.push(' :if ([:typeof $slashPos] != "nil") do={ :set disk [:pick $rootDir 0 $slashPos] }');
+ src.push(' :local verPath "awg-update-check.json"');
+ src.push(' :if ($disk != "disk1") do={');
+ src.push(' :do { /file/make-directory ($disk . "/pull") } on-error={}');
+ src.push(' /container/config set tmpdir=($disk . "/pull")');
+ src.push(' :set verPath ($disk . "/pull/awg-update-check.json")');
+ src.push(' }');
+ src.push(' :local newVer ""');
+ if (REPO.api) {
+ src.push(' :do {');
+ src.push(' /tool/fetch url="' + REPO.api + '" dst-path=$verPath http-max-redirect-count=10');
+ src.push(' :local relJson [/file/get [find where name=$verPath] contents]');
+ src.push(' :local tagPos [:find $relJson "\\"tag_name\\":\\""]');
+ src.push(' :if ([:typeof $tagPos] != "nil") do={');
+ src.push(' :local tagRest [:pick $relJson ($tagPos + 12) [:len $relJson]]');
+ src.push(' :set newVer [:pick $tagRest 0 [:find $tagRest "\\""]]');
+ src.push(' }');
+ src.push(' :do { /file/remove [find where name=$verPath] } on-error={}');
+ src.push(' } on-error={ :put "Could not read the published version" }');
+ }
+ src.push(' :local curVer ""');
+ src.push(' :do { :set curVer [/container/envs/get [find where list="' + env + '" key="AWG_IMAGE_VER"] value] } on-error={}');
+ // A healthy container is only ever pulled against a version we actually
+ // confirmed. If the release API is unreachable or rate-limited, doing it
+ // anyway would restart the tunnel nightly for nothing and hammer the
+ // registry with anonymous pulls from every router at once.
+ // The health flag is what overrides all of this: a bricked container is
+ // pulled again even when it claims to be on the newest release.
+ src.push(' :local doPull true');
+ src.push(' :if ($healthy) do={');
+ src.push(' :if ($newVer = "") do={');
+ src.push(' :set doPull false');
+ src.push(' :put "Published version unknown - leaving the healthy container alone"');
+ src.push(' }');
+ src.push(' :if ($newVer != "" && $newVer = $curVer) do={');
+ src.push(' :set doPull false');
+ src.push(' :put ("Already on " . $curVer . " - nothing to pull, tunnel untouched")');
+ src.push(' }');
+ src.push(' }');
+ src.push(' :if ($doPull) do={');
+ src.push(' :if (!$healthy) do={');
+ src.push(' :put "Container is not healthy - pulling the image again"');
+ src.push(' :log warning "' + name + ': container unhealthy, forcing a pull"');
+ src.push(' }');
+ src.push(' :put ("Updating " . $img . " ...")');
+ src.push(' :log info "' + prefix + ': repull started"');
+ // Stale .backup copies from an earlier pull confuse the rename dance RouterOS
+ // does around root-dir: it can move the live directory aside and never put it
+ // back, which is exactly how a container ends up bricked.
+ src.push(' :do { /file/remove [find where name~($disk . "/' + prefix + '.backup")] } on-error={}');
+ src.push(' :do { [:parse "/container/set $cid ignore-remote-image-change=yes"] } on-error={}');
+ src.push(' /container/repull $cid');
+ src.push(' :local waited 0');
+ src.push(' :while ([:len [/container/get $cid image-id]] = 0 && $waited < 180) do={');
+ src.push(' :delay 3s');
+ src.push(' :set waited ($waited + 3)');
+ src.push(' }');
+ src.push(' :if ([:len [/container/get $cid image-id]] = 0) do={');
+ src.push(' :put "Pull failed - retrying once in 15s"');
+ src.push(' :delay 15s');
+ src.push(' /container/repull $cid');
+ src.push(' :set waited 0');
+ src.push(' :while ([:len [/container/get $cid image-id]] = 0 && $waited < 180) do={');
+ src.push(' :delay 3s');
+ src.push(' :set waited ($waited + 3)');
+ src.push(' }');
+ src.push(' }');
+ src.push(' :local backUp false');
+ src.push(' :if ([:len [/container/get $cid image-id]] > 0) do={');
+ src.push(' :set backUp true');
+ src.push(' :if ($wantRunning) do={');
+ src.push(' :delay 3s');
+ src.push(' :do { /container/start $cid } on-error={}');
+ src.push(' :delay 8s');
+ src.push(' :set backUp false');
+ src.push(' :do { :if ([/container/get $cid stopped] != true) do={ :set backUp true } } on-error={}');
+ src.push(' }');
+ src.push(' }');
+ // Last resort: rebuild it from the registry with the same parameters. Ten
+ // more seconds of downtime beats a tunnel that stays dead until someone looks.
+ src.push(' :if (!$backUp) do={');
+ src.push(' :put "Container did not come back up - rebuilding it from the registry"');
+ src.push(' :log warning "' + name + ': container did not come back, rebuilding"');
+ src.push(' :do { /container/stop $cid } on-error={}');
+ src.push(' :delay 5s');
+ src.push(' :do { /container/remove $cid } on-error={}');
+ src.push(' :delay 3s');
+ src.push(' :do { /file/remove [find where name~($disk . "/' + prefix + '.backup")] } on-error={}');
+ src.push(' :do { /container/add remote-image=$img ' + addArgs + ' } on-error={}');
+ src.push(' :set waited 0');
+ src.push(' :local newCids [/container/find where interface=' + veth + ' and start-on-boot=yes]');
+ src.push(' :if ([:len $newCids] = 0) do={ :set newCids [/container/find where interface=' + veth + '] }');
+ src.push(' :while ([:len $newCids] > 0 && [:len [/container/get [:pick $newCids ([:len $newCids] - 1)] image-id]] = 0 && $waited < 180) do={');
+ src.push(' :delay 3s');
+ src.push(' :set waited ($waited + 3)');
+ src.push(' }');
+ src.push(' :if ([:len $newCids] > 0 && [:len [/container/get [:pick $newCids ([:len $newCids] - 1)] image-id]] > 0) do={');
+ src.push(' :do { /container/set [:pick $newCids ([:len $newCids] - 1)] shm-size=4M } on-error={}');
+ src.push(' :do { [:parse "/container/set [find where interface=' + veth + '] ignore-remote-image-change=yes"] } on-error={}');
+ src.push(' :do { [:parse "/container/set [find where interface=' + veth + '] restart-policy=on-failure restart-interval=30s restart-max-count=10"] } on-error={}');
+ src.push(' :do { /container/start [:pick $newCids ([:len $newCids] - 1)] } on-error={}');
+ src.push(' :delay 8s');
+ src.push(' :do { :if ([/container/get [:pick $newCids ([:len $newCids] - 1)] stopped] != true) do={ :set backUp true } } on-error={}');
+ src.push(' }');
+ src.push(' :if ($backUp) do={');
+ src.push(' :put "Container rebuilt"');
+ src.push(' :log info "' + prefix + ': container rebuilt after update"');
+ src.push(' } else={');
+ src.push(' :put "Rebuild failed - the tunnel is down. Netwatch keeps the router online meanwhile."');
+ src.push(' :put "This script runs again on the next schedule and will retry."');
+ src.push(' :log error "' + name + ': rebuild failed, tunnel is down"');
+ src.push(' }');
+ src.push(' }');
+ // Only record the version once the container is actually up. Writing it after
+ // a failed repair would make the next nightly run say "already up to date"
+ // and leave the tunnel dead.
+ src.push(' :if ($backUp && $newVer != "") do={');
+ src.push(' :do { /container/envs/remove [find where list="' + env + '" key="AWG_IMAGE_VER"] } on-error={}');
+ src.push(' :do { /container/envs/add list=' + env + ' key=AWG_IMAGE_VER value=$newVer } on-error={}');
+ src.push(' }');
+ src.push(' :do { /file/remove [find where name~($disk . "/' + prefix + '.backup")] } on-error={}');
+ src.push(' :if ($backUp) do={');
+ src.push(' :put ("Update done: " . $img)');
+ src.push(' :log info "' + prefix + ': update done"');
+ src.push(' }');
+ src.push(' }');
+ src.push(' }');
+ src.push(' }');
+ src.push(' }');
+ src.push('}');
+ return src.join('\n');
+ }
+
+ // Daily check. repull with no new version costs two HTTP requests and leaves the
+ // container alone, so the schedule is cheap; 04:30 keeps it clear of the
+ // <prefix>-ru-daily job at 04:00.
+ function buildUpdateSchedulerLines(prefix) {
+ if (!isAutoUpdateEnabled()) return [];
+ // Offline install: the update script fetches the release list from
+ // api.github.com, which is exactly what this router cannot reach. A
+ // daily scheduler would fail every night forever, so it is not created.
+ // The script itself still exists and can be run by hand once the router
+ // does have a way out.
+ if (offlineEnabled()) return [
+ '# Daily update check skipped: offline install cannot reach the release API.',
+ '# Run /system/script/run ' + prefix + '-update by hand when the router is online.'
+ ];
+ // device-mode can forbid /system/scheduler entirely, and a bare add would
+ // abort the whole install script right here. Same guard the RU list uses.
+ return [
+ '# Daily update check. Skipped if scheduler is not allowed by device-mode',
+ '{',
+ ' :local schedOk true',
+ ' :do { :set schedOk [/system/device-mode/get scheduler] } on-error={}',
+ ' :if ($schedOk) do={',
+ ' /system/scheduler/add name=' + prefix + '-update on-event="/system/script/run ' + prefix + '-update" start-time=04:30:00 interval=1d comment=' + prefix,
+ ' } else={',
+ ' :put "WARNING: scheduler not allowed by device-mode, no daily update check."',
+ ' :put "Update by hand: /system/script/run ' + prefix + '-update"',
+ ' :put "To allow it: /system/device-mode/update scheduler=yes"',
+ ' }',
+ '}',
+ ];
+ }
+
+ 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]');
+ }
+ 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={}');
+ 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={}');
+ extraTimeResetLines(' ').forEach(function (l) { src.push(l); });
+ 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={}');
+ // Список RU — общий на весь роутер: он ничей, его наполняет <prefix>-ru-update,
+ // и на него смотрят правила ВСЕХ установок (dst-address-list=!RU). Снести его
+ // при удалении одной установки — значит отправить весь российский трафик в
+ // туннель у всех остальных, причём восстановить сможет только тот, у кого
+ // остался скрипт обновления списка. Записи, действительно принадлежащие
+ // этой установке, помечены её префиксом и убираются ниже.
+ 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={}');
+ // Форс IPv4 (buildV6ForceIpv4) живёт в /ipv6 — общий prefix-sweep по /ip его не видит.
+ src.push(' :do { /ipv6/firewall/filter/remove [find where comment~"' + prefix + '-v6-force4"] } on-error={}');
+ src.push(' :do { /ipv6/firewall/address-list/remove [find where list="' + prefix + '-v6-direct"] } 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]');
+ }
+ 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={}');
+ 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={}');
+ extraTimeResetLines(' ').forEach(function (l) { src.push(l); });
+ 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={}');
+ // Список RU — общий на весь роутер: он ничей, его наполняет <prefix>-ru-update,
+ // и на него смотрят правила ВСЕХ установок (dst-address-list=!RU). Снести его
+ // при удалении одной установки — значит отправить весь российский трафик в
+ // туннель у всех остальных, причём восстановить сможет только тот, у кого
+ // остался скрипт обновления списка. Записи, действительно принадлежащие
+ // этой установке, помечены её префиксом и убираются ниже.
+ 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={}');
+ // Форс IPv4 (buildV6ForceIpv4) живёт в /ipv6 — общий prefix-sweep по /ip его не видит.
+ src.push(' :do { /ipv6/firewall/filter/remove [find where comment~"' + prefix + '-v6-force4"] } on-error={}');
+ src.push(' :do { /ipv6/firewall/address-list/remove [find where list="' + prefix + '-v6-direct"] } 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 = dnsServersForScript(i.DNS, scenario);
+ 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 and update scripts');
+
+ lines.push(buildUninstallScriptSource(hasDNS, storageDisk, prefix, net, scenario));
+ lines.push(buildUpdateScriptSource(prefix));
+ lines = lines.concat(buildUpdateSchedulerLines(prefix));
+
+ 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));
+ 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="' + remoteEndpoint(parsed) + '"',
+ '/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"');
+ // Without a resolver the container cannot turn a hostname endpoint into
+ // an address; generate() makes this mandatory whenever the endpoint is
+ // not a literal.
+ var awgDns = awgDnsValue();
+ if (awgDns) lines.push('/container/envs/add list=' + prefix + '-env key=AWG_DNS value="' + awgDns + '"');
+
+ // Pin the two forwarding threads to their own cores. The receive buffer
+ // of a container socket is capped by net.core.rmem_max and cannot be
+ // raised from inside, so a thread that loses its core to something else
+ // for a couple of milliseconds loses packets outright. Measured on a
+ // hAP ax3: pinned 1/2 dropped 0.92% against 2.11% unpinned. Only worth
+ // doing where there are cores to spare, hence the cpu-count guard.
+ lines.push('# Perf: give each forwarding thread its own core (4+ core routers)');
+ lines.push('{ :if ([/system/resource/get cpu-count] >= 4) do={' +
+ ' /container/envs/add list=' + prefix + '-env key=AWG_CPU_C2S value="1";' +
+ ' /container/envs/add list=' + prefix + '-env key=AWG_CPU_S2C value="2" } }');
+ lines.push('# NOTE on MTU: the value above assumes this tunnel is the last hop.');
+ lines.push('# If the server forwards your traffic into another tunnel, subtract');
+ lines.push('# that tunnel\'s overhead too, or every packet leaves in two fragments.');
+
+ 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');
+ if (dnsStr !== parseDNSServers(i.DNS).join(','))
+ lines.push('# NOTE: servers follow the DNS-forwarder picked in the configurator, not the .conf');
+ lines.push('/ip/dns/set servers=' + dnsStr);
+ for (var d = 0; d < dnsServers.length; d++) {
+ // Никакого /32 через wg-интерфейс: такой маршрут переживает смерть туннеля
+ // (WG-интерфейс не уходит в down) и уводит резолв в никуда. DNS идёт общим
+ // путём - политикой маршрутизации в активный туннель, а при падении всех
+ // туннелей по default-маршруту, как и остальной трафик.
+ lines.push('# DNS ' + dnsServers[d] + ' идёт общим маршрутом (в активный туннель / default),');
+ lines.push('# без привязки к wg-' + prefix + ': иначе падение туннеля уносит с собой резолв.');
+ }
+ }
+
+ lines = lines.concat([
+ '',
+ '# ' + (hasDNS ? '6' : '5') + '. Download, create and start container',
+ ]);
+ lines = lines.concat(buildImageSetupLines(prefix, storageDisk, {
+ leaveOpen: true, memCheck: true, docLink: true,
+ completionLines: [
+ '/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 " Update the container:"',
+ ' :put " /system/script/run ' + prefix + '-update"',
+ ' :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 = dnsServersForScript(i.DNS, scenario);
+ 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 and update scripts',
+ ];
+
+ 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));
+ 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="" }');
+ if (dnsStr !== parseDNSServers(i.DNS).join(','))
+ lines.push('# NOTE: servers follow the DNS-forwarder picked in the configurator, not the .conf');
+ lines.push('/ip/dns/set servers=' + dnsStr);
+ for (var d = 0; d < dnsServers.length; d++) {
+ // Никакого /32 через wg-интерфейс: такой маршрут переживает смерть туннеля
+ // (WG-интерфейс не уходит в down) и уводит резолв в никуда. DNS идёт общим
+ // путём - политикой маршрутизации в активный туннель, а при падении всех
+ // туннелей по default-маршруту, как и остальной трафик.
+ lines.push('# DNS ' + dnsServers[d] + ' идёт общим маршрутом (в активный туннель / default),');
+ lines.push('# без привязки к wg-' + prefix + ': иначе падение туннеля уносит с собой резолв.');
+ }
+ 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=\\"' + remoteEndpoint(parsed) + '\\""');
+ 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\\""');
+ var saDns = awgDnsValue();
+ if (saDns) lines.push(' :put "export AWG_DNS=\\"' + saDns + '\\""');
+
+ 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 " Update the container:"',
+ ' :put " /system/script/run ' + prefix + '-update"',
+ ' :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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
+ }
+
+ // ============================================================
+ // 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 isAutoUpdateEnabled() {
+ var el = document.getElementById('autoupdate-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';
+ document.getElementById('v6block-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.
+ firewallTopRule('/ipv6/firewall/filter',
+ 'chain=forward action=accept in-interface=' + veth, 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..."');
+ 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={}');
+ 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');
+ }
+
+ // Every protocol-significant field of a generated AWG profile, in the order
+ // amneziawg-tools' showconf writes them. It lives in one place on purpose:
+ // the container env, the share link and a direct client's .conf all have to
+ // describe the same profile, and the way that breaks is a field being added
+ // to one of the three and forgotten in the others. Keys are the generator's
+ // own lowercase shape (see generateAWGParams), not parsed-.conf casing.
+ function awgProfileConfLines(p) {
+ var lines = [
+ 'Jc = ' + p.jc,
+ 'Jmin = ' + p.jmin,
+ 'Jmax = ' + p.jmax,
+ 'S1 = ' + p.s1,
+ 'S2 = ' + p.s2
+ ];
+ // v2/v3 only: awg-tools omits S3/S4 rather than writing 0.
+ if (p.s3) lines.push('S3 = ' + p.s3);
+ if (p.s4) lines.push('S4 = ' + p.s4);
+ lines.push('H1 = ' + p.h1);
+ lines.push('H2 = ' + p.h2);
+ lines.push('H3 = ' + p.h3);
+ lines.push('H4 = ' + p.h4);
+ // v3: without the key the client speaks an unprotected header and the
+ // server drops every packet, which is what made #66 look like a
+ // handshake problem rather than a missing line.
+ if (p.hpKey) lines.push('HeaderProtectionKey = ' + p.hpKey);
+ // v3.1
+ if (p.randomTrailers) lines.push('RandomTrailers = ' + p.randomTrailers);
+ if (p.disableCookies) lines.push('DisableCookies = ' + p.disableCookies);
+ // v1.5/v3 CPS templates
+ if (p.cps) {
+ for (var k = 0; k < p.cps.length; k++)
+ lines.push('I' + (k + 1) + ' = ' + p.cps[k]);
+ }
+ return lines;
+ }
+
+ // Build a valid AmneziaWG .conf for a direct full-tunnel client.
+ // o: { clientPriv, serverPub, awgParams, endpoint, awgPort, tunClientIP, dnsServer }
+ function buildAwgConf(o) {
+ 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 = lines.concat(awgProfileConfLines(o.awgParams));
+ 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('}');
+ // The firewall rules below match in-interface-list=WAN. Checked here,
+ // before anything is created: RouterOS fails those adds one at a time
+ // and the script would otherwise finish half applied (#64).
+ ls = ls.concat(interfaceListPreflightLines(['WAN']));
+ 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(buildUpdateScriptSource(prefix));
+ ls = ls.concat(buildUpdateSchedulerLines(prefix));
+ 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(firewallTopRule('/ip/firewall/filter',
+ 'chain=forward action=accept protocol=udp dst-port=' + awgPort + ' in-interface-list=WAN',
+ prefix + '-awg-in'));
+ ls.push(firewallTopRule('/ip/firewall/filter',
+ 'chain=input action=accept protocol=udp src-address=' + vethIP + ' dst-port=' + wgPort,
+ prefix + '-wg-in'));
+ ls.push(firewallTopRule('/ip/firewall/filter',
+ 'chain=input action=accept in-interface=wg-' + prefix, 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(firewallTopRule('/ipv6/firewall/filter',
+ 'chain=forward action=accept protocol=udp dst-port=' + awgPort + ' in-interface-list=WAN',
+ 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(firewallTopRule('/ip/firewall/filter',
+ 'chain=forward action=accept in-interface=wg-' + prefix, 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(buildImageSetupLines(prefix, storageServer, { completionLines: 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(buildUpdateScriptSource(prefix));
+ lc = lc.concat(buildUpdateSchedulerLines(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(firewallTopRule('/ip/firewall/filter',
+ 'chain=input action=accept in-interface=wg-' + prefix, 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(buildImageSetupLines(prefix, storageClient, { completionLines: 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(buildUpdateScriptSource(prefix));
+ lc = lc.concat(buildUpdateSchedulerLines(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(firewallTopRule('/ip/firewall/filter',
+ 'chain=input action=accept in-interface=wg-' + prefix, 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(buildImageSetupLines(prefix, storageClient, { completionLines: 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(buildUpdateScriptSource(prefix));
+ la = la.concat(buildUpdateSchedulerLines(prefix));
+ 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(firewallTopRule('/ip/firewall/filter',
+ 'chain=input action=accept in-interface=wg-' + prefix, 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(buildImageSetupLines(prefix, storageA, { completionLines: [
+ '/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('}');
+ // The firewall rules below match in-interface-list=WAN. Checked here,
+ // before anything is created: RouterOS fails those adds one at a time
+ // and the script would otherwise finish half applied (#64).
+ lb = lb.concat(interfaceListPreflightLines(['WAN']));
+ 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(buildUpdateScriptSource(prefix));
+ lb = lb.concat(buildUpdateSchedulerLines(prefix));
+ 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(firewallTopRule('/ip/firewall/filter',
+ 'chain=forward action=accept protocol=udp dst-port=' + awgParams.port + ' in-interface-list=WAN',
+ prefix + '-awg-in'));
+ lb.push(firewallTopRule('/ip/firewall/filter',
+ 'chain=input action=accept protocol=udp src-address=' + net.vethAddr.replace(/\/.*/, '') + ' dst-port=' + wgPort,
+ 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(firewallTopRule('/ipv6/firewall/filter',
+ 'chain=forward action=accept protocol=udp dst-port=' + awgParams.port + ' in-interface-list=WAN',
+ 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(buildImageSetupLines(prefix, storageB, { completionLines: [
+ '/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..."');
+ 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={}');
+ 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');
+ }
+
+ // ============================================================
+ // Container image: one generator for every scenario
+ // ============================================================
+ // Three sources, in order: an archive already on the router (offline, kept in
+ // place), the GHCR registry (RouterOS 7.22+, the only path /container/repull can
+ // update later), and finally the release archive over /tool/fetch.
+ // opts: { completionLines, leaveOpen, memCheck, docLink }
+ // ============================================================
+ // Offline install
+ // ============================================================
+ // GitHub is unreachable from a good number of the routers this is aimed at,
+ // which used to mean you needed a second router with a working tunnel just
+ // to install the first one (#51). The container archive and the RU list can
+ // both be put on the router by hand, and the installer already prefers a
+ // local file when it finds one -- what was missing is knowing which file.
+ //
+ // The name cannot be worked out in the browser: it depends on the
+ // architecture, the RouterOS version (7.20 needs the classic-Docker build)
+ // and, on arm, the board model. So instead of guessing, the prep script
+ // below asks the router and prints the answer.
+ function offlineEnabled() {
+ var el = document.getElementById('offline-enable');
+ return !!(el && el.checked);
+ }
+
+ // Release file name for this router, left in $file (plus $imgTag, $suffix
+ // and $minor). Shared by the installer and the prep script so the two can
+ // never disagree about which archive to look for.
+ function imageFileDetectLines(ind) {
+ var lines = [];
+ lines.push(ind + ':local arch [/system/resource/get architecture-name]');
+ lines.push(ind + ':local ver [/system/resource/get version]');
+ lines.push(ind + ':local dotPos [:find $ver "."]');
+ lines.push(ind + ':local rest [:pick $ver ($dotPos + 1) [:len $ver]]');
+ lines.push(ind + ':local endPos [:find $rest "."]');
+ lines.push(ind + ':if ([:typeof $endPos] = "nil") do={ :set endPos [:find $rest " "] }');
+ lines.push(ind + ':local minor [:tonum [:pick $rest 0 $endPos]]');
+ lines.push(ind + ':local suffix ""');
+ lines.push(ind + ':if ($minor <= 20) do={ :set suffix "-7.20-Docker" }');
+ lines.push(ind + ':local file ""');
+ // armv5 has no place in the multi-arch index (arm32v5/debian only), so it
+ // rides its own registry tag.
+ lines.push(ind + ':local imgTag "latest"');
+ lines.push(ind + ':if ($arch = "arm64") do={ :set file ("awg-proxy-arm64" . $suffix . ".tar.gz") }');
+ lines.push(ind + ':if ($arch = "arm") do={');
+ lines.push(ind + ' :set file ("awg-proxy-arm" . $suffix . ".tar.gz")');
+ lines.push(ind + ' :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.
+ lines.push(ind + ' :do { :set rbModel [[:parse ":return [/system/routerboard/get model]"]] } on-error={}');
+ lines.push(ind + ' :local bName [/system/resource/get board-name]');
+ lines.push(ind + ' :if ($rbModel~"E50UG" || $rbModel~"E60iUGS" || $bName~"E50UG" || $bName~"E60iUGS" || $bName~"^hEX") do={');
+ lines.push(ind + ' :set file ("awg-proxy-armv5" . $suffix . ".tar.gz")');
+ lines.push(ind + ' :set imgTag "latest-armv5"');
+ lines.push(ind + ' }');
+ lines.push(ind + '}');
+ lines.push(ind + ':if ($arch ~ "x86") do={ :set file ("awg-proxy-amd64" . $suffix . ".tar.gz") }');
+ lines.push(ind + ':if ($file = "") do={ :error "Unsupported architecture: $arch" }');
+ return lines;
+ }
+
+ // Step 0 of an offline install: read-only, changes nothing, prints exactly
+ // which files to download and where to put them. Run this first, fetch the
+ // files on a machine that can reach GitHub, upload them, then run the
+ // install script.
+ function buildOfflinePrepScript(prefix, storageDisk, needsRuList) {
+ var isAuto = storageDisk === '__auto';
+ var lines = [
+ '# ============================================================',
+ '# Offline install, step 0 of 2 -- what to download',
+ '# This script changes nothing. It only prints file names.',
+ '# ============================================================',
+ '{'
+ ];
+ lines = lines.concat(imageFileDetectLines(' '));
+
+ // Where the installer will look. disk1 is the plain files list; an
+ // external disk keeps its own root. Read-only probe: unlike the
+ // installer, this must never offer to format anything.
+ lines.push(' :local disk "disk1"');
+ if (isAuto) {
+ lines.push(' :do {');
+ lines.push(' :local usbFree [/disk/get [find where mount-point="usb1"] free]');
+ lines.push(' :if ($usbFree > 0) do={ :set disk "usb1" }');
+ lines.push(' } on-error={}');
+ } else if (storageDisk !== 'disk1') {
+ lines.push(' :set disk "' + storageDisk + '"');
+ }
+ lines.push(' :local dest $file');
+ lines.push(' :if ($disk != "disk1") do={ :set dest ($disk . "/" . $file) }');
+
+ lines.push(' :put "============================================================"');
+ lines.push(' :put "Offline install: download these on a machine with internet,"');
+ lines.push(' :put "then upload them to this router (Files, or drag into WinBox)."');
+ lines.push(' :put "============================================================"');
+ lines.push(' :put ""');
+ lines.push(' :put "1. Container image"');
+ lines.push(' :put (" file: " . $file)');
+ lines.push(' :put (" url: ' + REPO.dl + '" . $file)');
+ lines.push(' :put (" put it here: " . $dest)');
+
+ if (needsRuList) {
+ // The RU list comes in two flavours and which one the update script
+ // wants depends on whether device-mode allows schedulers. Print the
+ // one it will ask for; the installer accepts the other as well.
+ lines.push(' :local schedOk true');
+ lines.push(' :do { :set schedOk [/system/device-mode/get scheduler] } on-error={}');
+ lines.push(' :local rscName "ru_ranges_timeout.rsc"');
+ lines.push(' :if (!$schedOk) do={ :set rscName "ru_ranges.rsc" }');
+ lines.push(' :put ""');
+ lines.push(' :put "2. RU address list"');
+ lines.push(' :put (" file: " . $rscName)');
+ lines.push(' :put (" url: ' + RU_RANGES_BASE + '" . $rscName)');
+ lines.push(' :put (" put it here: " . $disk . "/" . $rscName)');
+ }
+
+ lines.push(' :put ""');
+ lines.push(' :put "Then run the install script from step 1."');
+ lines.push('}');
+ return lines.join('\n');
+ }
+
+ function buildImageSetupLines(prefix, storageDisk, opts) {
+ opts = opts || {};
+ var offline = offlineEnabled();
+ var isAuto = storageDisk === '__auto';
+ var isExternal = storageDisk !== 'disk1' && !isAuto;
+ var addArgs = 'interface=veth-' + prefix + ' envlist=' + prefix + '-env hostname=' +
+ prefix + ' root-dir=$rootDir logging=no start-on-boot=yes comment=' + prefix;
+ var find = '[/container/find where interface=veth-' + prefix + ']';
+ 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 = lines.concat(imageFileDetectLines(' '));
+ lines.push(' :local url "' + REPO.dl + '$file"');
+
+ if (isAuto) {
+ lines.push(' :local filePath $file');
+ lines.push(' :local altPath ("disk1/" . $file)');
+ lines.push(' :local rootDir ($awgDisk . "/' + prefix + '")');
+ lines.push(' :if ($awgDisk != "disk1") do={');
+ lines.push(' :set filePath ($awgDisk . "/pull/" . $file)');
+ lines.push(' :set altPath ($awgDisk . "/" . $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(' :local altPath ("' + storageDisk + '/" . $file)');
+ lines.push(' :local rootDir "' + storageDisk + '/' + prefix + '"');
+ lines.push(' :do { /file/make-directory ' + storageDisk + '/pull } on-error={}');
+ } else {
+ lines.push(' :local filePath $file');
+ lines.push(' :local altPath ("disk1/" . $file)');
+ lines.push(' :local rootDir "' + storageDisk + '/' + prefix + '"');
+ }
+ lines.push(' :if ([:len [/file/find where name=$filePath]] = 0 && [:len [/file/find where name=$altPath]] > 0) do={');
+ lines.push(' :set filePath $altPath');
+ lines.push(' }');
+
+ lines.push(' :local added false');
+ lines.push(' :local fetched false');
+
+ // 1. Offline: the archive is already here. It belongs to the user, so it stays.
+ lines.push(' :if ([:len [/file/find where name=$filePath]] > 0) do={');
+ lines.push(' :put ("Using image already on the router: " . $filePath)');
+ lines.push(' /container/add file=$filePath ' + addArgs);
+ lines.push(' :set added true');
+ lines.push(' }');
+
+ // 2. Registry. /container/add returns before the pull even starts and never
+ // raises, so :do/on-error sees nothing: the only signal is image-id filling in
+ // (success) or staying empty while the container flips to download-failed.
+ if (REPO.ghcr && !offline) {
+ lines.push(' :if (!$added && $minor >= 22) do={');
+ lines.push(' :local img ("' + REPO.ghcr + ':" . $imgTag)');
+ lines.push(' :put ("Pulling " . $img)');
+ lines.push(' /container/add remote-image=$img ' + addArgs);
+ lines.push(' :local cids ' + find);
+ lines.push(' :local waited 0');
+ lines.push(' :while ([:len $cids] > 0 && [:len [/container/get [:pick $cids 0] image-id]] = 0 && $waited < 120) do={');
+ lines.push(' :delay 2s');
+ lines.push(' :set waited ($waited + 2)');
+ lines.push(' }');
+ lines.push(' :if ([:len $cids] > 0 && [:len [/container/get [:pick $cids 0] image-id]] > 0) do={');
+ lines.push(' :set added true');
+ lines.push(' :put ("Pulled from registry in " . $waited . "s")');
+ // Remember what the registry served, so <prefix>-update can tell whether a
+ // repull is worth a restart. Left empty on failure: the update script then
+ // just repulls, which is what it would have done anyway.
+ if (REPO.api) {
+ lines.push(' :do {');
+ lines.push(' :local verPath ($filePath . ".rel")');
+ lines.push(' /tool/fetch url="' + REPO.api + '" dst-path=$verPath http-max-redirect-count=10');
+ lines.push(' :local relJson [/file/get [find where name=$verPath] contents]');
+ lines.push(' :local tagPos [:find $relJson "\\"tag_name\\":\\""]');
+ lines.push(' :local tagRest [:pick $relJson ($tagPos + 12) [:len $relJson]]');
+ lines.push(' /container/envs/add list=' + prefix + '-env key=AWG_IMAGE_VER value=[:pick $tagRest 0 [:find $tagRest "\\""]]');
+ lines.push(' /file/remove [find where name=$verPath]');
+ lines.push(' } on-error={}');
+ }
+ lines.push(' } else={');
+ lines.push(' :put "Registry unavailable, falling back to the release archive"');
+ lines.push(' :do { /container/remove $cids } on-error={}');
+ lines.push(' :delay 2s');
+ lines.push(' }');
+ lines.push(' }');
+ }
+
+ // 3. Release archive. This one we downloaded, so this one we clean up.
+ lines.push(' :if (!$added) 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.")');
+ if (opts.docLink) {
+ lines.push(' :put "See: ' + REPO.base + '#insufficient-disk-space"');
+ }
+ lines.push(' :error "Insufficient disk space"');
+ lines.push(' }');
+ if (offline) {
+ // Offline install: step 0 printed this exact name. Nothing has been
+ // downloaded, so the only honest outcome is to say which file is
+ // missing and stop.
+ lines.push(' :put ("Image not found on the router: " . $filePath)');
+ lines.push(' :put ("Download it and upload it there, then run this script again:")');
+ lines.push(' :put (" ' + REPO.dl + '" . $file)');
+ lines.push(' :error "Offline install: image file missing"');
+ lines.push(' }');
+ } else {
+ lines.push(' :put ("Fetching: $url -> " . $filePath)');
+ lines.push(' /tool/fetch url=$url dst-path=$filePath http-max-redirect-count=10');
+ lines.push(' /container/add file=$filePath ' + addArgs);
+ lines.push(' :set fetched true');
+ lines.push(' }');
+ }
+
+ lines.push(' :if ($minor > 20) do={ [:parse "/container/set [find where interface=veth-' + prefix + '] shm-size=4M"] }');
+ // Параметр появился в 7.23, на 7.22 и ниже его нет - :parse откладывает разбор
+ // до выполнения, иначе скрипт не прочитается целиком на старых версиях.
+ lines.push(' :do { [:parse "/container/set [find where interface=veth-' + prefix + '] ignore-remote-image-change=yes"] } on-error={}');
+ lines.push(' :do { [:parse "/container/set [find where interface=veth-' + prefix + '] restart-policy=on-failure restart-interval=30s restart-max-count=10"] } on-error={}');
+ // Only remove what this script downloaded: an archive the user dropped on the
+ // router by hand is their offline copy.
+ lines.push(' :if ($fetched) do={ /file/remove $filePath }');
+ if (opts.memCheck) {
+ lines.push(' :local freeMem [/system/resource/get free-memory]');
+ lines.push(' :if ($freeMem < 16777216) do={');
+ lines.push(' :put ("WARNING: Low memory (" . ($freeMem / 1048576) . "MB free). 16MB+ recommended.")');
+ lines.push(' }');
+ }
+ 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 (opts.completionLines) {
+ for (var i = 0; i < opts.completionLines.length; i++) {
+ lines.push(' ' + opts.completionLines[i]);
+ }
+ }
+ if (!opts.leaveOpen) {
+ 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('offline-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;
+ }
+
+ // A hostname endpoint is useless to the container without a resolver:
+ // the image is scratch and writes /etc/resolv.conf from AWG_DNS alone.
+ // Failing here beats a container that starts and never connects.
+ var endpointHostName = endpointHost(parsed.peer.Endpoint);
+ if (endpointHostName && !isIPLiteral(endpointHostName) && !awgDnsValue()) {
+ showError(t('errDnsRequired'));
+ 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();
+
+ // Step 0 of an offline install. Rendered above the install script and
+ // before the routing one, because that is the order it has to be run in.
+ var offlineSection = document.getElementById('offline-section');
+ if (offlineEnabled() && mode !== 'standalone') {
+ var prep = buildOfflinePrepScript(prefix, storageDisk, scenario === 'non-ru');
+ document.getElementById('offline-output').dataset.plain = prep;
+ document.getElementById('offline-output').innerHTML = highlight(prep);
+ offlineSection.style.display = 'block';
+ } else {
+ offlineSection.style.display = 'none';
+ }
+
+ // 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('autoupdate-enable').checked = true;
+ 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
+ dohUserPicked = false;
+ 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) {
+ dohUserPicked = true;
+ 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]);
+ // DoH-провайдера подсвечиваем тоже сразу, а не на Generate: иначе
+ // менять его пришлось бы уже после того, как скрипт собран.
+ var d = /^\s*DNS\s*=\s*(.+)$/m.exec(this.value);
+ if (d) autoSelectDoh(d[1].trim());
+ });
+ ['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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
+
+ 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>