Add WiFi Audit, API Fuzzer, Cloud Scanner, Threat Intel, Log Correlator, Steganography, Anti-Forensics, BLE Scanner, Forensics, RFID/NFC, Malware Sandbox, Password Toolkit, Web Scanner, Report Engine, Net Mapper, and C2 Framework. Each module includes CLI interface, Flask routes, and web UI template. Also includes Go DNS server source + binary, IP Capture service, SYN Flood, Gone Fishing mail server, and hack hijack modules from v2.0 work. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1608 lines
82 KiB
HTML
1608 lines
82 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}DNS Server — AUTARCH{% endblock %}
|
|
{% block content %}
|
|
<h1>DNS Server</h1>
|
|
<p style="color:var(--text-secondary);margin-bottom:1.5rem">
|
|
Authoritative DNS & nameserver with zone management, DNSSEC, import/export, and mail record automation.
|
|
</p>
|
|
|
|
<!-- Server Controls -->
|
|
<div class="card" style="padding:1rem;margin-bottom:1.25rem;display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
|
|
<div id="dns-status" style="font-size:0.9rem">Status: <span style="color:var(--text-muted)">Checking...</span></div>
|
|
<button class="btn btn-primary" onclick="startDNS()">Start Server</button>
|
|
<button class="btn btn-danger" onclick="stopDNS()">Stop Server</button>
|
|
<button class="btn" onclick="checkDNS()">Refresh</button>
|
|
<div id="dns-metrics" style="margin-left:auto;font-size:0.82rem;color:var(--text-secondary)"></div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="tabs" style="display:flex;gap:0;border-bottom:2px solid var(--border);margin-bottom:1.5rem">
|
|
<button class="tab-btn active" onclick="dnsTab('zones',this)">Zones</button>
|
|
<button class="tab-btn" onclick="dnsTab('records',this)">Records</button>
|
|
<button class="tab-btn" onclick="dnsTab('ezlocal',this)">EZ-Local</button>
|
|
<button class="tab-btn" onclick="dnsTab('reverseproxy',this)">Reverse Proxy</button>
|
|
<button class="tab-btn" onclick="dnsTab('import-export',this)">Import / Export</button>
|
|
<button class="tab-btn" onclick="dnsTab('templates',this)">Templates</button>
|
|
<button class="tab-btn" onclick="dnsTab('config',this)">Config</button>
|
|
</div>
|
|
|
|
<!-- ═══════════════════ ZONES TAB ═══════════════════ -->
|
|
<div id="tab-zones" class="tab-pane">
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.25rem">
|
|
<div class="card" style="padding:1.25rem">
|
|
<h3 style="margin-bottom:1rem">Create Zone</h3>
|
|
<label class="form-label">Domain</label>
|
|
<input id="z-domain" class="form-input" placeholder="example.local">
|
|
<button class="btn btn-primary" style="margin-top:0.75rem;width:100%" onclick="createZone()">Create Zone</button>
|
|
<div id="z-create-status" style="font-size:0.82rem;margin-top:0.5rem"></div>
|
|
|
|
<hr style="border-color:var(--border);margin:1.25rem 0">
|
|
<h4 style="margin-bottom:0.5rem">Clone Zone</h4>
|
|
<p style="font-size:0.78rem;color:var(--text-secondary);margin-bottom:0.5rem">Duplicate an existing zone to a new domain name.</p>
|
|
<label class="form-label">Source Zone</label>
|
|
<select id="z-clone-src" class="form-input zone-select"></select>
|
|
<label class="form-label" style="margin-top:0.5rem">New Domain</label>
|
|
<input id="z-clone-dst" class="form-input" placeholder="new-domain.local">
|
|
<button class="btn btn-primary" style="margin-top:0.75rem;width:100%" onclick="cloneZone()">Clone Zone</button>
|
|
<div id="z-clone-status" style="font-size:0.82rem;margin-top:0.5rem"></div>
|
|
|
|
<hr style="border-color:var(--border);margin:1.25rem 0">
|
|
<h4 style="margin-bottom:0.5rem">Quick Mail Setup</h4>
|
|
<p style="font-size:0.78rem;color:var(--text-secondary);margin-bottom:0.5rem">Auto-create MX, SPF, DKIM, DMARC records for a zone.</p>
|
|
<label class="form-label">Zone</label>
|
|
<select id="z-mail-zone" class="form-input zone-select"></select>
|
|
<label class="form-label" style="margin-top:0.5rem">MX Host</label>
|
|
<input id="z-mail-mx" class="form-input" placeholder="mail.example.local">
|
|
<label class="form-label" style="margin-top:0.5rem">SPF Allow</label>
|
|
<input id="z-mail-spf" class="form-input" value="ip4:127.0.0.1" placeholder="ip4:192.168.1.100">
|
|
<button class="btn btn-primary" style="margin-top:0.75rem;width:100%" onclick="mailSetup()">Setup Mail Records</button>
|
|
<div id="z-mail-status" style="font-size:0.82rem;margin-top:0.5rem"></div>
|
|
</div>
|
|
|
|
<div class="card" style="padding:1.25rem">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
|
|
<h3>Zones</h3>
|
|
<button class="btn btn-small" onclick="loadZones()">Refresh</button>
|
|
</div>
|
|
<div id="z-list" style="font-size:0.85rem">Loading...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════════════ RECORDS TAB ═══════════════════ -->
|
|
<div id="tab-records" class="tab-pane" style="display:none">
|
|
<div class="card" style="padding:1.25rem">
|
|
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1rem">
|
|
<h3>Records for:</h3>
|
|
<select id="r-zone" class="form-input zone-select" style="width:auto;min-width:200px" onchange="loadRecords()"></select>
|
|
<button class="btn btn-small" onclick="loadRecords()">Refresh</button>
|
|
</div>
|
|
|
|
<!-- Add record form -->
|
|
<div style="display:grid;grid-template-columns:100px 1fr 1fr 80px 80px auto;gap:0.5rem;align-items:end;margin-bottom:1rem">
|
|
<div>
|
|
<label class="form-label">Type</label>
|
|
<select id="r-type" class="form-input">
|
|
<option>A</option><option>AAAA</option><option>CNAME</option>
|
|
<option>MX</option><option>TXT</option><option>NS</option>
|
|
<option>SRV</option><option>PTR</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Name</label>
|
|
<input id="r-name" class="form-input" placeholder="subdomain.example.local.">
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Value</label>
|
|
<input id="r-value" class="form-input" placeholder="192.168.1.100">
|
|
</div>
|
|
<div>
|
|
<label class="form-label">TTL</label>
|
|
<input id="r-ttl" class="form-input" value="300" type="number">
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Priority</label>
|
|
<input id="r-priority" class="form-input" value="0" type="number">
|
|
</div>
|
|
<button class="btn btn-primary" onclick="addRecord()">Add</button>
|
|
</div>
|
|
<div id="r-add-status" style="font-size:0.82rem;margin-bottom:0.75rem"></div>
|
|
|
|
<!-- Bulk add toggle -->
|
|
<div style="margin-bottom:1rem">
|
|
<button class="btn btn-small" onclick="toggleBulkAdd()">Bulk Add Records</button>
|
|
<div id="bulk-add-panel" style="display:none;margin-top:0.75rem">
|
|
<p style="font-size:0.78rem;color:var(--text-secondary);margin-bottom:0.5rem">
|
|
Add multiple records at once. JSON array format:
|
|
</p>
|
|
<textarea id="bulk-json" class="form-input" style="height:140px;font-family:monospace;font-size:0.78rem" placeholder='[
|
|
{"type":"A","name":"www","value":"192.168.1.100","ttl":300},
|
|
{"type":"A","name":"mail","value":"192.168.1.101","ttl":300},
|
|
{"type":"MX","name":"@","value":"mail.example.local.","ttl":300,"priority":10}
|
|
]'></textarea>
|
|
<button class="btn btn-primary" style="margin-top:0.5rem" onclick="bulkAddRecords()">Add All</button>
|
|
<span id="bulk-add-status" style="font-size:0.82rem;margin-left:0.5rem"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Records table -->
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
|
|
<div style="display:flex;gap:0.5rem;align-items:center">
|
|
<label class="form-label" style="margin:0">Filter:</label>
|
|
<select id="r-filter-type" class="form-input" style="width:auto;padding:0.3rem 0.5rem;font-size:0.8rem" onchange="filterRecords()">
|
|
<option value="">All Types</option>
|
|
<option>A</option><option>AAAA</option><option>CNAME</option>
|
|
<option>MX</option><option>TXT</option><option>NS</option>
|
|
<option>SRV</option><option>PTR</option><option>SOA</option>
|
|
</select>
|
|
<input id="r-filter-search" class="form-input" style="width:200px;padding:0.3rem 0.5rem;font-size:0.8rem" placeholder="Search name/value..." oninput="filterRecords()">
|
|
</div>
|
|
<span id="r-count" style="font-size:0.78rem;color:var(--text-muted)"></span>
|
|
</div>
|
|
<table style="width:100%;font-size:0.82rem;border-collapse:collapse">
|
|
<thead>
|
|
<tr style="border-bottom:2px solid var(--border);text-align:left">
|
|
<th style="padding:6px;cursor:pointer" onclick="sortRecords('type')">Type</th>
|
|
<th style="padding:6px;cursor:pointer" onclick="sortRecords('name')">Name</th>
|
|
<th style="padding:6px;cursor:pointer" onclick="sortRecords('value')">Value</th>
|
|
<th style="padding:6px">TTL</th>
|
|
<th style="padding:6px">Pri</th>
|
|
<th style="padding:6px">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="r-table"></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- DNSSEC -->
|
|
<div class="card" style="padding:1.25rem;margin-top:1.25rem">
|
|
<h3 style="margin-bottom:0.75rem">DNSSEC</h3>
|
|
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:0.75rem">
|
|
Enable DNSSEC signing for a zone to protect against DNS spoofing.
|
|
</p>
|
|
<div style="display:flex;gap:0.75rem;align-items:center">
|
|
<select id="dnssec-zone" class="form-input zone-select" style="width:auto;min-width:200px"></select>
|
|
<button class="btn btn-primary" onclick="enableDNSSEC()">Enable DNSSEC</button>
|
|
<button class="btn btn-danger" onclick="disableDNSSEC()">Disable DNSSEC</button>
|
|
<span id="dnssec-status" style="font-size:0.82rem"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════════════ EZ-LOCAL TAB ═══════════════════ -->
|
|
<div id="tab-ezlocal" class="tab-pane" style="display:none">
|
|
<div class="card" style="padding:1.25rem;margin-bottom:1.25rem;border:2px solid var(--accent);background:linear-gradient(135deg,var(--bg-card) 0%,rgba(99,102,241,0.05) 100%)">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.75rem">
|
|
<div>
|
|
<h3 style="margin:0">EZ-Local Setup</h3>
|
|
<p style="font-size:0.82rem;color:var(--text-secondary);margin-top:0.25rem">
|
|
One-click local intranet domain. Network is auto-scanned — just review and deploy.
|
|
</p>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="ezScanNetwork()">Scan Network</button>
|
|
</div>
|
|
<div id="ez-scan-status" style="font-size:0.82rem"></div>
|
|
</div>
|
|
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.25rem">
|
|
<!-- Domain Setup -->
|
|
<div class="card" style="padding:1.25rem">
|
|
<h3 style="margin-bottom:1rem">Domain Configuration</h3>
|
|
|
|
<label class="form-label">Local Domain Name</label>
|
|
<div style="display:flex;gap:0.5rem;align-items:center">
|
|
<input id="ez-domain" class="form-input" value="home.local" placeholder="mynet.local" style="flex:1">
|
|
<select id="ez-tld" class="form-input" style="width:auto" onchange="ezUpdateDomain()">
|
|
<option value=".local">.local</option>
|
|
<option value=".lan">.lan</option>
|
|
<option value=".internal">.internal</option>
|
|
<option value=".home">.home</option>
|
|
<option value="">(custom)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<label class="form-label" style="margin-top:0.75rem">Nameserver IP (this machine)</label>
|
|
<input id="ez-ns-ip" class="form-input" placeholder="Auto-detected...">
|
|
|
|
<label class="form-label" style="margin-top:0.75rem">Gateway / Router IP</label>
|
|
<input id="ez-gateway" class="form-input" placeholder="Auto-detected...">
|
|
|
|
<label class="form-label" style="margin-top:0.75rem">Subnet</label>
|
|
<input id="ez-subnet" class="form-input" placeholder="192.168.1.0/24">
|
|
|
|
<hr style="border-color:var(--border);margin:1.25rem 0">
|
|
|
|
<h4 style="margin-bottom:0.5rem">Auto-Create Records</h4>
|
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.85rem;margin-bottom:0.4rem">
|
|
<input type="checkbox" id="ez-ns-record" checked> NS record (this machine as nameserver)
|
|
</label>
|
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.85rem;margin-bottom:0.4rem">
|
|
<input type="checkbox" id="ez-gateway-record" checked> gateway.domain → router IP
|
|
</label>
|
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.85rem;margin-bottom:0.4rem">
|
|
<input type="checkbox" id="ez-server-record" checked> server.domain → this machine
|
|
</label>
|
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.85rem;margin-bottom:0.4rem">
|
|
<input type="checkbox" id="ez-dashboard-record" checked> dashboard.domain → AUTARCH web UI
|
|
</label>
|
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.85rem;margin-bottom:0.4rem">
|
|
<input type="checkbox" id="ez-wildcard-record"> *.domain → this machine (catch-all)
|
|
</label>
|
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.85rem;margin-bottom:0.4rem">
|
|
<input type="checkbox" id="ez-reverse-zone"> Create reverse DNS zone (PTR records)
|
|
</label>
|
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.85rem;margin-bottom:0.4rem">
|
|
<input type="checkbox" id="ez-arp-hosts" checked> Add records for discovered ARP hosts
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Discovered Hosts -->
|
|
<div class="card" style="padding:1.25rem">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
|
|
<h3>Discovered Hosts</h3>
|
|
<span id="ez-host-count" style="font-size:0.78rem;color:var(--text-muted)"></span>
|
|
</div>
|
|
<p style="font-size:0.78rem;color:var(--text-secondary);margin-bottom:0.75rem">
|
|
Hosts found on your network via ARP scan. Edit hostnames to use as DNS names.
|
|
</p>
|
|
<div id="ez-hosts" style="max-height:400px;overflow-y:auto;font-size:0.82rem">
|
|
<div style="color:var(--text-muted)">Click "Scan Network" to discover hosts</div>
|
|
</div>
|
|
|
|
<hr style="border-color:var(--border);margin:1rem 0">
|
|
<h4 style="margin-bottom:0.5rem">Custom Hosts</h4>
|
|
<p style="font-size:0.78rem;color:var(--text-secondary);margin-bottom:0.5rem">Add hosts not found by scan:</p>
|
|
<div style="display:flex;gap:0.5rem;margin-bottom:0.5rem">
|
|
<input id="ez-custom-name" class="form-input" placeholder="hostname" style="flex:1">
|
|
<input id="ez-custom-ip" class="form-input" placeholder="192.168.1.x" style="width:140px">
|
|
<button class="btn btn-small" onclick="ezAddCustomHost()">Add</button>
|
|
</div>
|
|
<div id="ez-custom-list"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preview & Deploy -->
|
|
<div class="card" style="padding:1.25rem;margin-top:1.25rem">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
|
|
<h3>Preview & Deploy</h3>
|
|
<div style="display:flex;gap:0.5rem">
|
|
<button class="btn" onclick="ezPreview()">Preview Records</button>
|
|
<button class="btn btn-primary" onclick="ezDeploy()">Deploy Zone</button>
|
|
</div>
|
|
</div>
|
|
<div id="ez-preview" style="display:none">
|
|
<pre id="ez-preview-text" style="background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius);padding:1rem;font-size:0.78rem;max-height:300px;overflow-y:auto;white-space:pre-wrap;font-family:monospace"></pre>
|
|
</div>
|
|
<div id="ez-deploy-status" style="font-size:0.82rem;margin-top:0.5rem"></div>
|
|
|
|
<hr style="border-color:var(--border);margin:1.25rem 0">
|
|
<h4>Client Configuration</h4>
|
|
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:0.5rem">
|
|
After deploying, configure your devices to use this machine as their DNS server:
|
|
</p>
|
|
<div id="ez-client-instructions" style="background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius);padding:1rem;font-family:monospace;font-size:0.8rem">
|
|
Set DNS server to: <strong id="ez-my-dns-ip">127.0.0.1</strong>
|
|
<div style="margin-top:0.75rem;font-size:0.78rem;color:var(--text-secondary)">
|
|
<div><strong>Windows:</strong> Settings → Network → Change adapter options → IPv4 → DNS = <span class="ez-dns-ip"></span></div>
|
|
<div><strong>Linux:</strong> Edit /etc/resolv.conf → nameserver <span class="ez-dns-ip"></span></div>
|
|
<div><strong>macOS:</strong> System Preferences → Network → Advanced → DNS = <span class="ez-dns-ip"></span></div>
|
|
<div><strong>Router:</strong> Set DHCP DNS to <span class="ez-dns-ip"></span> for entire network</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════════════ REVERSE PROXY TAB ═══════════════════ -->
|
|
<div id="tab-reverseproxy" class="tab-pane" style="display:none">
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.25rem">
|
|
<!-- DDNS Setup -->
|
|
<div class="card" style="padding:1.25rem">
|
|
<h3 style="margin-bottom:0.5rem">Dynamic DNS (DDNS)</h3>
|
|
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:1rem">
|
|
Keep a DNS record updated with your changing public IP. AUTARCH will auto-detect your IP and update the zone record periodically.
|
|
</p>
|
|
|
|
<label class="form-label">Zone</label>
|
|
<select id="rp-zone" class="form-input zone-select"></select>
|
|
<label class="form-label" style="margin-top:0.5rem">Hostname</label>
|
|
<input id="rp-hostname" class="form-input" placeholder="home" value="@">
|
|
<label class="form-label" style="margin-top:0.5rem">Current Public IP</label>
|
|
<div style="display:flex;gap:0.5rem;align-items:center">
|
|
<input id="rp-public-ip" class="form-input" placeholder="Detecting..." style="flex:1" readonly>
|
|
<button class="btn btn-small" onclick="rpDetectIP()">Detect</button>
|
|
</div>
|
|
<label class="form-label" style="margin-top:0.5rem">Update Interval (minutes)</label>
|
|
<input id="rp-interval" class="form-input" type="number" value="5" min="1" max="60">
|
|
|
|
<div style="display:flex;gap:0.5rem;margin-top:1rem">
|
|
<button class="btn btn-primary" onclick="rpUpdateDDNS()">Update Now</button>
|
|
<button class="btn" onclick="rpStartDDNS()">Start Auto-Update</button>
|
|
<button class="btn btn-danger" onclick="rpStopDDNS()">Stop</button>
|
|
</div>
|
|
<div id="rp-ddns-status" style="font-size:0.82rem;margin-top:0.5rem"></div>
|
|
</div>
|
|
|
|
<!-- Reverse Proxy Config -->
|
|
<div class="card" style="padding:1.25rem">
|
|
<h3 style="margin-bottom:0.5rem">Reverse Proxy Generator</h3>
|
|
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:1rem">
|
|
Generate reverse proxy configs for nginx, Caddy, or Apache to route traffic from a domain to your local services.
|
|
</p>
|
|
|
|
<label class="form-label">Domain</label>
|
|
<input id="rp-domain" class="form-input" placeholder="mysite.example.com">
|
|
<label class="form-label" style="margin-top:0.5rem">Backend (local service)</label>
|
|
<input id="rp-backend" class="form-input" placeholder="http://127.0.0.1:8080" value="http://127.0.0.1:8080">
|
|
<label class="form-label" style="margin-top:0.5rem">SSL/TLS</label>
|
|
<select id="rp-ssl" class="form-input">
|
|
<option value="letsencrypt">Let's Encrypt (auto)</option>
|
|
<option value="selfsigned">Self-Signed</option>
|
|
<option value="none">None (HTTP only)</option>
|
|
</select>
|
|
<label class="form-label" style="margin-top:0.5rem">Proxy Type</label>
|
|
<select id="rp-type" class="form-input" onchange="rpGenerate()">
|
|
<option value="nginx">nginx</option>
|
|
<option value="caddy">Caddy</option>
|
|
<option value="apache">Apache</option>
|
|
</select>
|
|
|
|
<button class="btn btn-primary" style="margin-top:0.75rem;width:100%" onclick="rpGenerate()">Generate Config</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Generated Config -->
|
|
<div id="rp-config-panel" class="card" style="padding:1.25rem;margin-top:1.25rem;display:none">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.75rem">
|
|
<h3>Generated Configuration</h3>
|
|
<div style="display:flex;gap:0.5rem">
|
|
<button class="btn btn-small" onclick="rpCopyConfig()">Copy</button>
|
|
<button class="btn btn-small" onclick="rpDownloadConfig()">Download</button>
|
|
</div>
|
|
</div>
|
|
<pre id="rp-config-output" style="background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius);padding:1rem;font-size:0.8rem;white-space:pre-wrap;font-family:monospace;max-height:400px;overflow-y:auto"></pre>
|
|
</div>
|
|
|
|
<!-- Port Forwarding Guide -->
|
|
<div class="card" style="padding:1.25rem;margin-top:1.25rem">
|
|
<h3 style="margin-bottom:0.75rem">Port Forwarding Requirements</h3>
|
|
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:0.75rem">
|
|
For external access with a DHCP/dynamic WAN IP, you need:
|
|
</p>
|
|
<div style="display:grid;grid-template-columns:auto 1fr;gap:0.5rem 1rem;font-size:0.82rem">
|
|
<span style="font-weight:600;color:var(--accent)">1.</span>
|
|
<span><strong>Port forwarding</strong> on your router: Forward ports 80 and 443 (and 53 for DNS) to this machine's LAN IP (<span id="rp-lan-ip" style="font-family:monospace">detecting...</span>)</span>
|
|
|
|
<span style="font-weight:600;color:var(--accent)">2.</span>
|
|
<span><strong>Dynamic DNS</strong> — Use the DDNS panel above to keep your DNS record pointing to your changing IP</span>
|
|
|
|
<span style="font-weight:600;color:var(--accent)">3.</span>
|
|
<span><strong>Reverse proxy</strong> — nginx/Caddy/Apache on this machine to route incoming traffic to your local services</span>
|
|
|
|
<span style="font-weight:600;color:var(--accent)">4.</span>
|
|
<span><strong>Firewall</strong> — Allow ports 80, 443, 53 through your OS firewall</span>
|
|
</div>
|
|
|
|
<hr style="border-color:var(--border);margin:1rem 0">
|
|
<h4 style="margin-bottom:0.5rem">UPnP Auto-Forward</h4>
|
|
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:0.5rem">
|
|
If your router supports UPnP, AUTARCH can auto-create port forwarding rules.
|
|
</p>
|
|
<div style="display:flex;gap:0.5rem;flex-wrap:wrap">
|
|
<button class="btn" onclick="rpUPnPForward(80)">Forward Port 80</button>
|
|
<button class="btn" onclick="rpUPnPForward(443)">Forward Port 443</button>
|
|
<button class="btn" onclick="rpUPnPForward(53)">Forward Port 53 (DNS)</button>
|
|
</div>
|
|
<div id="rp-upnp-status" style="font-size:0.82rem;margin-top:0.5rem"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════════════ IMPORT / EXPORT TAB ═══════════════════ -->
|
|
<div id="tab-import-export" class="tab-pane" style="display:none">
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.25rem">
|
|
<!-- Export -->
|
|
<div class="card" style="padding:1.25rem">
|
|
<h3 style="margin-bottom:1rem">Export Zone File</h3>
|
|
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:1rem">
|
|
Export a zone in BIND format for backup or transfer to another DNS server.
|
|
</p>
|
|
<label class="form-label">Zone</label>
|
|
<select id="exp-zone" class="form-input zone-select"></select>
|
|
<div style="display:flex;gap:0.5rem;margin-top:0.75rem">
|
|
<button class="btn btn-primary" onclick="exportZone()">Export</button>
|
|
<button class="btn" onclick="copyExport()">Copy to Clipboard</button>
|
|
<button class="btn" onclick="downloadExport()">Download .zone</button>
|
|
</div>
|
|
<div id="exp-output" style="display:none;margin-top:1rem">
|
|
<textarea id="exp-text" class="form-input" style="height:300px;font-family:monospace;font-size:0.78rem;white-space:pre" readonly></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import -->
|
|
<div class="card" style="padding:1.25rem">
|
|
<h3 style="margin-bottom:1rem">Import Zone File</h3>
|
|
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:1rem">
|
|
Import a BIND-format zone file. The zone must already exist — records will be added.
|
|
</p>
|
|
<label class="form-label">Target Zone</label>
|
|
<select id="imp-zone" class="form-input zone-select"></select>
|
|
<label class="form-label" style="margin-top:0.75rem">Zone File Contents</label>
|
|
<textarea id="imp-text" class="form-input" style="height:250px;font-family:monospace;font-size:0.78rem;white-space:pre" placeholder="$ORIGIN example.local.
|
|
$TTL 300
|
|
|
|
@ IN SOA ns1.example.local. admin.example.local. (
|
|
2024010101 ; serial
|
|
3600 ; refresh
|
|
900 ; retry
|
|
604800 ; expire
|
|
86400 ; minimum
|
|
)
|
|
|
|
@ IN NS ns1.example.local.
|
|
@ IN A 192.168.1.100
|
|
www IN A 192.168.1.100
|
|
mail IN A 192.168.1.101
|
|
@ IN MX 10 mail.example.local."></textarea>
|
|
<div style="display:flex;gap:0.5rem;margin-top:0.75rem">
|
|
<button class="btn btn-primary" onclick="importZone()">Import</button>
|
|
<label class="btn" style="cursor:pointer;margin:0">
|
|
Upload .zone File
|
|
<input type="file" id="imp-file" accept=".zone,.txt,.db" style="display:none" onchange="loadZoneFile(this)">
|
|
</label>
|
|
</div>
|
|
<div id="imp-status" style="font-size:0.82rem;margin-top:0.5rem"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Zone File Editor -->
|
|
<div class="card" style="padding:1.25rem;margin-top:1.25rem">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
|
|
<h3>Zone File Editor</h3>
|
|
<div style="display:flex;gap:0.5rem;align-items:center">
|
|
<select id="edit-zone" class="form-input zone-select" style="width:auto;min-width:200px"></select>
|
|
<button class="btn" onclick="loadZoneEditor()">Load</button>
|
|
<button class="btn btn-primary" onclick="saveZoneEditor()">Save</button>
|
|
</div>
|
|
</div>
|
|
<p style="font-size:0.78rem;color:var(--text-secondary);margin-bottom:0.75rem">
|
|
Edit zone records directly in BIND format. Changes are parsed and applied on save.
|
|
</p>
|
|
<div style="position:relative">
|
|
<div id="editor-line-nums" style="position:absolute;left:0;top:0;bottom:0;width:35px;background:var(--bg-card);border-right:1px solid var(--border);font-family:monospace;font-size:0.78rem;color:var(--text-muted);padding:0.5rem 0;text-align:right;overflow:hidden;user-select:none"></div>
|
|
<textarea id="zone-editor" class="form-input" style="height:350px;font-family:monospace;font-size:0.78rem;white-space:pre;padding-left:45px;resize:vertical" oninput="updateLineNums()" onscroll="syncLineScroll()"></textarea>
|
|
</div>
|
|
<div id="editor-status" style="font-size:0.82rem;margin-top:0.5rem"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════════════ TEMPLATES TAB ═══════════════════ -->
|
|
<div id="tab-templates" class="tab-pane" style="display:none">
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.25rem">
|
|
<!-- Web Server Template -->
|
|
<div class="card" style="padding:1.25rem">
|
|
<h3 style="margin-bottom:0.5rem">Web Server</h3>
|
|
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:1rem">
|
|
Create records for a typical web server setup (A, www CNAME, optional AAAA).
|
|
</p>
|
|
<label class="form-label">Zone</label>
|
|
<select id="tpl-web-zone" class="form-input zone-select"></select>
|
|
<label class="form-label" style="margin-top:0.5rem">Server IP (v4)</label>
|
|
<input id="tpl-web-ip" class="form-input" placeholder="192.168.1.100">
|
|
<label class="form-label" style="margin-top:0.5rem">Server IP (v6, optional)</label>
|
|
<input id="tpl-web-ip6" class="form-input" placeholder="2001:db8::1">
|
|
<label style="display:flex;align-items:center;gap:0.5rem;margin-top:0.75rem;font-size:0.85rem">
|
|
<input id="tpl-web-www" type="checkbox" checked> Add www CNAME
|
|
</label>
|
|
<label style="display:flex;align-items:center;gap:0.5rem;margin-top:0.4rem;font-size:0.85rem">
|
|
<input id="tpl-web-wildcard" type="checkbox"> Add wildcard *.domain
|
|
</label>
|
|
<button class="btn btn-primary" style="margin-top:0.75rem;width:100%" onclick="applyWebTemplate()">Apply Web Server Records</button>
|
|
<div id="tpl-web-status" style="font-size:0.82rem;margin-top:0.5rem"></div>
|
|
</div>
|
|
|
|
<!-- Mail Server Template -->
|
|
<div class="card" style="padding:1.25rem">
|
|
<h3 style="margin-bottom:0.5rem">Mail Server</h3>
|
|
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:1rem">
|
|
Full mail setup: A, MX, SPF, DKIM, DMARC, and optional autoconfig records.
|
|
</p>
|
|
<label class="form-label">Zone</label>
|
|
<select id="tpl-mail-zone" class="form-input zone-select"></select>
|
|
<label class="form-label" style="margin-top:0.5rem">Mail Server IP</label>
|
|
<input id="tpl-mail-ip" class="form-input" placeholder="192.168.1.101">
|
|
<label class="form-label" style="margin-top:0.5rem">Mail Hostname</label>
|
|
<input id="tpl-mail-host" class="form-input" placeholder="mail.example.local">
|
|
<label class="form-label" style="margin-top:0.5rem">SPF Additional Sources</label>
|
|
<input id="tpl-mail-spf" class="form-input" placeholder="ip4:10.0.0.0/24" value="">
|
|
<label class="form-label" style="margin-top:0.5rem">DKIM Public Key (optional)</label>
|
|
<textarea id="tpl-mail-dkim" class="form-input" style="height:60px;font-family:monospace;font-size:0.75rem" placeholder="v=DKIM1; k=rsa; p=MIGfMA0G..."></textarea>
|
|
<button class="btn btn-primary" style="margin-top:0.75rem;width:100%" onclick="applyMailTemplate()">Apply Mail Records</button>
|
|
<div id="tpl-mail-status" style="font-size:0.82rem;margin-top:0.5rem"></div>
|
|
</div>
|
|
|
|
<!-- Reverse DNS Template -->
|
|
<div class="card" style="padding:1.25rem">
|
|
<h3 style="margin-bottom:0.5rem">Reverse DNS (PTR)</h3>
|
|
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:1rem">
|
|
Create reverse lookup zone and PTR records for an IP range.
|
|
</p>
|
|
<label class="form-label">Network</label>
|
|
<input id="tpl-ptr-net" class="form-input" placeholder="192.168.1">
|
|
<label class="form-label" style="margin-top:0.5rem">Hostnames (one per line: IP FQDN)</label>
|
|
<textarea id="tpl-ptr-hosts" class="form-input" style="height:100px;font-family:monospace;font-size:0.78rem" placeholder="100 server.example.local
|
|
101 mail.example.local
|
|
102 ns1.example.local"></textarea>
|
|
<button class="btn btn-primary" style="margin-top:0.75rem;width:100%" onclick="applyPTRTemplate()">Create PTR Zone + Records</button>
|
|
<div id="tpl-ptr-status" style="font-size:0.82rem;margin-top:0.5rem"></div>
|
|
</div>
|
|
|
|
<!-- Subdomain Delegation Template -->
|
|
<div class="card" style="padding:1.25rem">
|
|
<h3 style="margin-bottom:0.5rem">Subdomain Delegation</h3>
|
|
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:1rem">
|
|
Delegate a subdomain to another nameserver.
|
|
</p>
|
|
<label class="form-label">Parent Zone</label>
|
|
<select id="tpl-del-zone" class="form-input zone-select"></select>
|
|
<label class="form-label" style="margin-top:0.5rem">Subdomain</label>
|
|
<input id="tpl-del-sub" class="form-input" placeholder="lab">
|
|
<label class="form-label" style="margin-top:0.5rem">NS Server</label>
|
|
<input id="tpl-del-ns" class="form-input" placeholder="ns1.lab.example.local">
|
|
<label class="form-label" style="margin-top:0.5rem">NS Server IP (glue)</label>
|
|
<input id="tpl-del-ip" class="form-input" placeholder="10.0.0.1">
|
|
<button class="btn btn-primary" style="margin-top:0.75rem;width:100%" onclick="applyDelegationTemplate()">Apply Delegation Records</button>
|
|
<div id="tpl-del-status" style="font-size:0.82rem;margin-top:0.5rem"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════════════ CONFIG TAB ═══════════════════ -->
|
|
<div id="tab-config" class="tab-pane" style="display:none">
|
|
<!-- Network Settings -->
|
|
<div class="card" style="padding:1.25rem;max-width:700px">
|
|
<h3 style="margin-bottom:1rem">Network Settings</h3>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.75rem 1rem">
|
|
<div>
|
|
<label class="form-label">DNS Listen Address</label>
|
|
<input id="cfg-dns-listen" class="form-input" value="0.0.0.0:53">
|
|
</div>
|
|
<div>
|
|
<label class="form-label">API Listen Address</label>
|
|
<input id="cfg-api-listen" class="form-input" value="127.0.0.1:5380">
|
|
</div>
|
|
<div style="grid-column:span 2">
|
|
<label class="form-label">Upstream DNS Servers (comma-separated, leave empty for pure recursive)</label>
|
|
<input id="cfg-upstream" class="form-input" value="" placeholder="Optional fallback: 8.8.8.8:53, 1.1.1.1:53">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cache & Performance -->
|
|
<div class="card" style="padding:1.25rem;max-width:700px;margin-top:1rem">
|
|
<h3 style="margin-bottom:1rem">Cache & Performance</h3>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:0.75rem 1rem">
|
|
<div>
|
|
<label class="form-label">Cache TTL (seconds)</label>
|
|
<input id="cfg-cache-ttl" class="form-input" type="number" value="300">
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Negative Cache TTL (NXDOMAIN)</label>
|
|
<input id="cfg-neg-cache-ttl" class="form-input" type="number" value="60">
|
|
</div>
|
|
<div>
|
|
<label class="form-label">SERVFAIL Cache TTL</label>
|
|
<input id="cfg-servfail-ttl" class="form-input" type="number" value="30">
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Query Log Max Entries</label>
|
|
<input id="cfg-querylog-max" class="form-input" type="number" value="1000">
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Max UDP Size (bytes)</label>
|
|
<input id="cfg-max-udp" class="form-input" type="number" value="1232">
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Rate Limit (queries/sec/IP)</label>
|
|
<input id="cfg-rate-limit" class="form-input" type="number" value="100" placeholder="0 = unlimited">
|
|
</div>
|
|
</div>
|
|
<label style="display:flex;align-items:center;gap:0.5rem;margin-top:0.75rem;font-size:0.85rem">
|
|
<input id="cfg-prefetch" type="checkbox"> Prefetch expiring cache entries
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Security -->
|
|
<div class="card" style="padding:1.25rem;max-width:700px;margin-top:1rem">
|
|
<h3 style="margin-bottom:1rem">Security</h3>
|
|
<div style="display:flex;flex-wrap:wrap;gap:1rem 2rem">
|
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.85rem">
|
|
<input id="cfg-log-queries" type="checkbox" checked> Log all queries
|
|
</label>
|
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.85rem">
|
|
<input id="cfg-refuse-any" type="checkbox" checked> Refuse ANY queries (anti-amplification)
|
|
</label>
|
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.85rem">
|
|
<input id="cfg-minimal" type="checkbox" checked> Minimal responses (hide server info)
|
|
</label>
|
|
</div>
|
|
<div style="margin-top:0.75rem">
|
|
<label class="form-label">Allowed Zone Transfer IPs (comma-separated, empty = deny all)</label>
|
|
<input id="cfg-allow-transfer" class="form-input" value="" placeholder="Leave empty to deny all AXFR/IXFR">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Encryption -->
|
|
<div class="card" style="padding:1.25rem;max-width:700px;margin-top:1rem">
|
|
<h3 style="margin-bottom:1rem">Encryption (Upstream)</h3>
|
|
<p style="font-size:0.78rem;color:var(--text-secondary);margin-bottom:0.75rem">
|
|
When upstream forwarders are configured, encrypt queries using DoT or DoH.
|
|
Recursive resolution from root hints always uses plain DNS (root servers don't support encryption).
|
|
</p>
|
|
<div style="display:flex;flex-wrap:wrap;gap:1rem 2rem">
|
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.85rem">
|
|
<input id="cfg-enable-doh" type="checkbox" checked> DNS-over-HTTPS (DoH)
|
|
</label>
|
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.85rem">
|
|
<input id="cfg-enable-dot" type="checkbox" checked> DNS-over-TLS (DoT)
|
|
</label>
|
|
</div>
|
|
<p style="font-size:0.72rem;color:var(--text-muted);margin-top:0.5rem">
|
|
Priority: DoH (if available) > DoT > Plain. Auto-detected for Google, Cloudflare, Quad9, OpenDNS, AdGuard.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Hosts File -->
|
|
<div class="card" style="padding:1.25rem;max-width:700px;margin-top:1rem">
|
|
<h3 style="margin-bottom:1rem">Hosts File</h3>
|
|
<div style="display:grid;grid-template-columns:1fr auto;gap:0.75rem 1rem;align-items:end">
|
|
<div>
|
|
<label class="form-label">Hosts File Path (loaded on startup)</label>
|
|
<input id="cfg-hosts-file" class="form-input" value="" placeholder="/etc/hosts or C:\Windows\System32\drivers\etc\hosts">
|
|
</div>
|
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.85rem;padding-bottom:0.25rem">
|
|
<input id="cfg-hosts-autoload" type="checkbox"> Auto-load on start
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Save Button -->
|
|
<div style="max-width:700px;margin-top:1rem">
|
|
<button class="btn btn-primary" style="font-size:1rem;padding:0.6rem 2rem" onclick="saveConfig()">Save All Settings</button>
|
|
<div id="cfg-status" style="font-size:0.82rem;margin-top:0.5rem;display:inline-block;margin-left:1rem"></div>
|
|
</div>
|
|
|
|
<!-- Resolver Mode Info -->
|
|
<div class="card" style="padding:1.25rem;max-width:700px;margin-top:1.25rem">
|
|
<h3 style="margin-bottom:0.75rem">Resolver Mode</h3>
|
|
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:0.75rem">
|
|
AUTARCH DNS operates as a fully recursive resolver by default, walking from the 13 IANA root servers through
|
|
the delegation chain to resolve any domain independently. No upstream forwarders are required.
|
|
</p>
|
|
<div style="display:flex;gap:1rem">
|
|
<div class="card" style="padding:0.75rem;flex:1;border:2px solid var(--accent)">
|
|
<strong style="font-size:0.85rem;color:var(--accent)">Recursive (Default)</strong>
|
|
<p style="font-size:0.78rem;color:var(--text-secondary);margin-top:0.25rem">
|
|
Full independence. Resolves from root hints. Zero reliance on third-party DNS.
|
|
</p>
|
|
</div>
|
|
<div class="card" style="padding:0.75rem;flex:1">
|
|
<strong style="font-size:0.85rem">Hybrid</strong>
|
|
<p style="font-size:0.78rem;color:var(--text-secondary);margin-top:0.25rem">
|
|
Recursive first, falls back to upstream forwarders on failure. Add upstream servers above.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.tab-btn{padding:0.6rem 1.2rem;background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:0.9rem;border-bottom:2px solid transparent;margin-bottom:-2px;transition:all 0.2s}
|
|
.tab-btn:hover{color:var(--text-primary)}
|
|
.tab-btn.active{color:var(--accent);border-bottom-color:var(--accent);font-weight:600}
|
|
.form-label{display:block;font-size:0.78rem;color:var(--text-secondary);margin-bottom:0.25rem;font-weight:600;text-transform:uppercase;letter-spacing:0.04em}
|
|
.form-input{width:100%;padding:0.5rem 0.65rem;background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius);color:var(--text-primary);font-size:0.85rem}
|
|
.form-input:focus{outline:none;border-color:var(--accent)}
|
|
.btn-danger{background:var(--danger);color:#fff}
|
|
.zone-card{background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;margin-bottom:0.5rem;transition:border-color 0.2s}
|
|
.zone-card:hover{border-color:var(--accent)}
|
|
</style>
|
|
|
|
<script>
|
|
let _currentZone = '';
|
|
let _allRecords = [];
|
|
let _sortField = '';
|
|
let _sortAsc = true;
|
|
|
|
function dnsTab(name, btn) {
|
|
document.querySelectorAll('.tab-pane').forEach(p => p.style.display = 'none');
|
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
document.getElementById('tab-' + name).style.display = '';
|
|
btn.classList.add('active');
|
|
if (name === 'zones') loadZones();
|
|
if (name === 'records') loadRecords();
|
|
if (name === 'config') loadConfig();
|
|
}
|
|
|
|
// ── Server control ──
|
|
function checkDNS() {
|
|
fetch('/dns/status').then(r => r.json()).then(d => {
|
|
const el = document.getElementById('dns-status');
|
|
if (d.running) {
|
|
el.innerHTML = `Status: <span style="color:#4ade80;font-weight:600">RUNNING</span> • DNS: ${d.listen_dns} • API: ${d.listen_api}`;
|
|
if (d.queries !== undefined) {
|
|
document.getElementById('dns-metrics').textContent = `Queries: ${d.queries} | Zones: ${d.zones || 0}`;
|
|
}
|
|
} else {
|
|
el.innerHTML = 'Status: <span style="color:var(--text-muted);font-weight:600">STOPPED</span>';
|
|
document.getElementById('dns-metrics').textContent = '';
|
|
}
|
|
});
|
|
}
|
|
|
|
function startDNS() {
|
|
document.getElementById('dns-status').innerHTML = 'Status: <span style="color:var(--accent)">Starting...</span>';
|
|
fetch('/dns/start', {method:'POST'}).then(r => r.json()).then(d => {
|
|
if (!d.ok) alert(d.error);
|
|
checkDNS();
|
|
});
|
|
}
|
|
|
|
function stopDNS() {
|
|
fetch('/dns/stop', {method:'POST'}).then(r => r.json()).then(() => checkDNS());
|
|
}
|
|
|
|
// ── Zones ──
|
|
function loadZones() {
|
|
fetch('/dns/zones').then(r => r.json()).then(d => {
|
|
const el = document.getElementById('z-list');
|
|
const selects = document.querySelectorAll('.zone-select');
|
|
|
|
if (!d.ok || !d.zones || !d.zones.length) {
|
|
el.textContent = 'No zones. Create one to get started.';
|
|
selects.forEach(s => s.innerHTML = '<option value="">No zones</option>');
|
|
return;
|
|
}
|
|
|
|
el.innerHTML = d.zones.map(z => `<div class="zone-card">
|
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
<strong>${z.domain}</strong>
|
|
<div style="display:flex;gap:0.4rem;align-items:center">
|
|
${z.dnssec ? '<span style="color:#4ade80;font-size:0.75rem;font-weight:600">DNSSEC</span>' : ''}
|
|
<span style="color:var(--text-muted);font-size:0.8rem">${z.records || 0} records</span>
|
|
<button class="btn btn-small" onclick="_currentZone='${z.domain}';dnsTab('records',document.querySelectorAll('.tab-btn')[1])">Edit</button>
|
|
<button class="btn btn-small" onclick="quickExport('${z.domain}')">Export</button>
|
|
<button class="btn btn-small btn-danger" onclick="deleteZone('${z.domain}')">Delete</button>
|
|
</div>
|
|
</div>
|
|
</div>`).join('');
|
|
|
|
const opts = d.zones.map(z => `<option value="${z.domain}">${z.domain}</option>`).join('');
|
|
selects.forEach(s => {
|
|
s.innerHTML = opts;
|
|
if (_currentZone) s.value = _currentZone;
|
|
});
|
|
});
|
|
}
|
|
|
|
function createZone() {
|
|
const domain = document.getElementById('z-domain').value.trim();
|
|
if (!domain) return;
|
|
const el = document.getElementById('z-create-status');
|
|
fetch('/dns/zones', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({domain})})
|
|
.then(r => r.json()).then(d => {
|
|
el.innerHTML = d.ok ? `<span style="color:#4ade80">Zone ${domain} created</span>` : `<span style="color:var(--danger)">${d.error}</span>`;
|
|
if (d.ok) { document.getElementById('z-domain').value = ''; loadZones(); }
|
|
});
|
|
}
|
|
|
|
function deleteZone(domain) {
|
|
if (!confirm(`Delete zone ${domain} and all its records?`)) return;
|
|
fetch(`/dns/zones/${domain}`, {method:'DELETE'}).then(() => loadZones());
|
|
}
|
|
|
|
function cloneZone() {
|
|
const src = document.getElementById('z-clone-src').value;
|
|
const dst = document.getElementById('z-clone-dst').value.trim();
|
|
if (!src || !dst) return;
|
|
const el = document.getElementById('z-clone-status');
|
|
el.innerHTML = '<span style="color:var(--text-muted)">Cloning...</span>';
|
|
fetch('/dns/zone-clone', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({source: src, target: dst})})
|
|
.then(r => r.json()).then(d => {
|
|
el.innerHTML = d.ok ? `<span style="color:#4ade80">Cloned ${src} → ${dst}</span>` : `<span style="color:var(--danger)">${d.error}</span>`;
|
|
if (d.ok) { document.getElementById('z-clone-dst').value = ''; loadZones(); }
|
|
});
|
|
}
|
|
|
|
function mailSetup() {
|
|
const zone = document.getElementById('z-mail-zone').value;
|
|
if (!zone) return;
|
|
const data = {
|
|
mx_host: document.getElementById('z-mail-mx').value,
|
|
spf_allow: document.getElementById('z-mail-spf').value,
|
|
};
|
|
const el = document.getElementById('z-mail-status');
|
|
fetch(`/dns/zones/${zone}/mail-setup`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data)})
|
|
.then(r => r.json()).then(d => {
|
|
el.innerHTML = d.ok ? `<span style="color:#4ade80">${d.message}</span>` : `<span style="color:var(--danger)">${d.error}</span>`;
|
|
});
|
|
}
|
|
|
|
// ── Records ──
|
|
function loadRecords() {
|
|
const zone = document.getElementById('r-zone').value || _currentZone;
|
|
if (!zone) { document.getElementById('r-table').innerHTML = '<tr><td colspan="6" style="padding:10px;color:var(--text-muted)">Select a zone</td></tr>'; return; }
|
|
_currentZone = zone;
|
|
fetch(`/dns/zones/${zone}/records`).then(r => r.json()).then(d => {
|
|
_allRecords = (d.ok && d.records) ? d.records : [];
|
|
filterRecords();
|
|
});
|
|
}
|
|
|
|
function filterRecords() {
|
|
const typeFilter = document.getElementById('r-filter-type').value;
|
|
const search = document.getElementById('r-filter-search').value.toLowerCase();
|
|
let recs = _allRecords;
|
|
|
|
if (typeFilter) recs = recs.filter(r => r.type === typeFilter);
|
|
if (search) recs = recs.filter(r => (r.name || '').toLowerCase().includes(search) || (r.value || '').toLowerCase().includes(search));
|
|
|
|
if (_sortField) {
|
|
recs = [...recs].sort((a, b) => {
|
|
const va = (a[_sortField] || '').toString().toLowerCase();
|
|
const vb = (b[_sortField] || '').toString().toLowerCase();
|
|
return _sortAsc ? va.localeCompare(vb) : vb.localeCompare(va);
|
|
});
|
|
}
|
|
|
|
const zone = _currentZone;
|
|
document.getElementById('r-count').textContent = `${recs.length} of ${_allRecords.length} records`;
|
|
const el = document.getElementById('r-table');
|
|
if (!recs.length) {
|
|
el.innerHTML = '<tr><td colspan="6" style="padding:10px;color:var(--text-muted)">No records</td></tr>';
|
|
return;
|
|
}
|
|
el.innerHTML = recs.map(r => `<tr style="border-bottom:1px solid var(--border)">
|
|
<td style="padding:5px"><span style="background:var(--bg-input);padding:2px 6px;border-radius:3px;font-size:0.75rem;font-weight:600">${r.type}</span></td>
|
|
<td style="padding:5px;font-family:monospace;font-size:0.8rem">${r.name}</td>
|
|
<td style="padding:5px;font-family:monospace;font-size:0.8rem;max-width:300px;overflow:hidden;text-overflow:ellipsis" title="${esc(r.value)}">${r.value}</td>
|
|
<td style="padding:5px">${r.ttl}</td>
|
|
<td style="padding:5px">${r.priority || ''}</td>
|
|
<td style="padding:5px"><button class="btn btn-small btn-danger" onclick="deleteRecord('${zone}','${r.id}')">Delete</button></td>
|
|
</tr>`).join('');
|
|
}
|
|
|
|
function sortRecords(field) {
|
|
if (_sortField === field) _sortAsc = !_sortAsc;
|
|
else { _sortField = field; _sortAsc = true; }
|
|
filterRecords();
|
|
}
|
|
|
|
function addRecord() {
|
|
const zone = document.getElementById('r-zone').value || _currentZone;
|
|
if (!zone) return;
|
|
const data = {
|
|
type: document.getElementById('r-type').value,
|
|
name: document.getElementById('r-name').value,
|
|
value: document.getElementById('r-value').value,
|
|
ttl: parseInt(document.getElementById('r-ttl').value),
|
|
priority: parseInt(document.getElementById('r-priority').value),
|
|
};
|
|
const el = document.getElementById('r-add-status');
|
|
fetch(`/dns/zones/${zone}/records`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data)})
|
|
.then(r => r.json()).then(d => {
|
|
el.innerHTML = d.ok ? '<span style="color:#4ade80">Record added</span>' : `<span style="color:var(--danger)">${d.error}</span>`;
|
|
if (d.ok) loadRecords();
|
|
});
|
|
}
|
|
|
|
function deleteRecord(zone, id) {
|
|
fetch(`/dns/zones/${zone}/records/${id}`, {method:'DELETE'}).then(() => loadRecords());
|
|
}
|
|
|
|
function toggleBulkAdd() {
|
|
const p = document.getElementById('bulk-add-panel');
|
|
p.style.display = p.style.display === 'none' ? '' : 'none';
|
|
}
|
|
|
|
function bulkAddRecords() {
|
|
const zone = document.getElementById('r-zone').value || _currentZone;
|
|
if (!zone) return;
|
|
let records;
|
|
try { records = JSON.parse(document.getElementById('bulk-json').value); }
|
|
catch(e) { document.getElementById('bulk-add-status').innerHTML = `<span style="color:var(--danger)">Invalid JSON: ${e.message}</span>`; return; }
|
|
const el = document.getElementById('bulk-add-status');
|
|
el.innerHTML = '<span style="color:var(--text-muted)">Adding...</span>';
|
|
fetch(`/dns/zone-bulk-records/${zone}`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({records})})
|
|
.then(r => r.json()).then(d => {
|
|
el.innerHTML = d.ok ? `<span style="color:#4ade80">${d.added || records.length} records added</span>` : `<span style="color:var(--danger)">${d.error}</span>`;
|
|
if (d.ok) loadRecords();
|
|
});
|
|
}
|
|
|
|
// ── DNSSEC ──
|
|
function enableDNSSEC() {
|
|
const zone = document.getElementById('dnssec-zone').value;
|
|
if (!zone) return;
|
|
fetch(`/dns/zones/${zone}/dnssec/enable`, {method:'POST'}).then(r => r.json()).then(d => {
|
|
document.getElementById('dnssec-status').innerHTML = d.ok ? `<span style="color:#4ade80">${d.message}</span>` : `<span style="color:var(--danger)">${d.error}</span>`;
|
|
loadZones();
|
|
});
|
|
}
|
|
|
|
function disableDNSSEC() {
|
|
const zone = document.getElementById('dnssec-zone').value;
|
|
if (!zone) return;
|
|
fetch(`/dns/zones/${zone}/dnssec/disable`, {method:'POST'}).then(r => r.json()).then(d => {
|
|
document.getElementById('dnssec-status').innerHTML = d.ok ? `<span style="color:#4ade80">${d.message}</span>` : `<span style="color:var(--danger)">${d.error}</span>`;
|
|
loadZones();
|
|
});
|
|
}
|
|
|
|
// ── Import / Export ──
|
|
function exportZone() {
|
|
const zone = document.getElementById('exp-zone').value;
|
|
if (!zone) return;
|
|
fetch(`/dns/zone-export/${zone}`).then(r => r.json()).then(d => {
|
|
if (!d.ok) { alert(d.error); return; }
|
|
document.getElementById('exp-output').style.display = '';
|
|
document.getElementById('exp-text').value = d.zone_file || d.data || JSON.stringify(d, null, 2);
|
|
});
|
|
}
|
|
|
|
function quickExport(domain) {
|
|
_currentZone = domain;
|
|
dnsTab('import-export', document.querySelectorAll('.tab-btn')[2]);
|
|
document.getElementById('exp-zone').value = domain;
|
|
setTimeout(() => exportZone(), 100);
|
|
}
|
|
|
|
function copyExport() {
|
|
const text = document.getElementById('exp-text').value;
|
|
navigator.clipboard.writeText(text).then(() => alert('Copied!'));
|
|
}
|
|
|
|
function downloadExport() {
|
|
const text = document.getElementById('exp-text').value;
|
|
const zone = document.getElementById('exp-zone').value || 'zone';
|
|
const blob = new Blob([text], {type: 'text/plain'});
|
|
const a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = zone + '.zone';
|
|
a.click();
|
|
}
|
|
|
|
function importZone() {
|
|
const zone = document.getElementById('imp-zone').value;
|
|
if (!zone) return;
|
|
const text = document.getElementById('imp-text').value;
|
|
if (!text.trim()) return;
|
|
const el = document.getElementById('imp-status');
|
|
el.innerHTML = '<span style="color:var(--text-muted)">Importing...</span>';
|
|
fetch(`/dns/zone-import/${zone}`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({zone_file: text})})
|
|
.then(r => r.json()).then(d => {
|
|
el.innerHTML = d.ok ? `<span style="color:#4ade80">${d.message || 'Import successful'}</span>` : `<span style="color:var(--danger)">${d.error}</span>`;
|
|
if (d.ok) loadZones();
|
|
});
|
|
}
|
|
|
|
function loadZoneFile(input) {
|
|
const file = input.files[0];
|
|
if (!file) return;
|
|
const reader = new FileReader();
|
|
reader.onload = () => { document.getElementById('imp-text').value = reader.result; };
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
// ── Zone File Editor ──
|
|
function loadZoneEditor() {
|
|
const zone = document.getElementById('edit-zone').value;
|
|
if (!zone) return;
|
|
fetch(`/dns/zone-export/${zone}`).then(r => r.json()).then(d => {
|
|
if (!d.ok) { alert(d.error); return; }
|
|
document.getElementById('zone-editor').value = d.zone_file || d.data || '';
|
|
updateLineNums();
|
|
});
|
|
}
|
|
|
|
function saveZoneEditor() {
|
|
const zone = document.getElementById('edit-zone').value;
|
|
if (!zone) return;
|
|
const text = document.getElementById('zone-editor').value;
|
|
const el = document.getElementById('editor-status');
|
|
el.innerHTML = '<span style="color:var(--text-muted)">Saving...</span>';
|
|
fetch(`/dns/zone-import/${zone}`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({zone_file: text})})
|
|
.then(r => r.json()).then(d => {
|
|
el.innerHTML = d.ok ? `<span style="color:#4ade80">Saved</span>` : `<span style="color:var(--danger)">${d.error}</span>`;
|
|
if (d.ok) loadZones();
|
|
});
|
|
}
|
|
|
|
function updateLineNums() {
|
|
const ta = document.getElementById('zone-editor');
|
|
const lines = ta.value.split('\n').length;
|
|
const nums = Array.from({length: lines}, (_, i) => i + 1).join('\n');
|
|
document.getElementById('editor-line-nums').textContent = nums;
|
|
}
|
|
|
|
function syncLineScroll() {
|
|
const ta = document.getElementById('zone-editor');
|
|
document.getElementById('editor-line-nums').scrollTop = ta.scrollTop;
|
|
}
|
|
|
|
// ── Templates ──
|
|
function _bulkAdd(zone, records, statusEl) {
|
|
const el = document.getElementById(statusEl);
|
|
el.innerHTML = '<span style="color:var(--text-muted)">Applying...</span>';
|
|
fetch(`/dns/zone-bulk-records/${zone}`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({records})})
|
|
.then(r => r.json()).then(d => {
|
|
el.innerHTML = d.ok ? `<span style="color:#4ade80">${d.added || records.length} records created</span>` : `<span style="color:var(--danger)">${d.error}</span>`;
|
|
if (d.ok) loadZones();
|
|
});
|
|
}
|
|
|
|
function applyWebTemplate() {
|
|
const zone = document.getElementById('tpl-web-zone').value;
|
|
const ip = document.getElementById('tpl-web-ip').value.trim();
|
|
if (!zone || !ip) return;
|
|
const ip6 = document.getElementById('tpl-web-ip6').value.trim();
|
|
const www = document.getElementById('tpl-web-www').checked;
|
|
const wild = document.getElementById('tpl-web-wildcard').checked;
|
|
|
|
const recs = [{type:'A', name:'@', value:ip, ttl:300}];
|
|
if (ip6) recs.push({type:'AAAA', name:'@', value:ip6, ttl:300});
|
|
if (www) recs.push({type:'CNAME', name:'www', value:zone + '.', ttl:300});
|
|
if (wild) recs.push({type:'A', name:'*', value:ip, ttl:300});
|
|
_bulkAdd(zone, recs, 'tpl-web-status');
|
|
}
|
|
|
|
function applyMailTemplate() {
|
|
const zone = document.getElementById('tpl-mail-zone').value;
|
|
const ip = document.getElementById('tpl-mail-ip').value.trim();
|
|
const host = document.getElementById('tpl-mail-host').value.trim();
|
|
if (!zone || !ip || !host) return;
|
|
const spf = document.getElementById('tpl-mail-spf').value.trim();
|
|
const dkim = document.getElementById('tpl-mail-dkim').value.trim();
|
|
|
|
const recs = [
|
|
{type:'A', name:'mail', value:ip, ttl:300},
|
|
{type:'MX', name:'@', value:host + '.', ttl:300, priority:10},
|
|
{type:'TXT', name:'@', value:`v=spf1 a mx ip4:${ip} ${spf} -all`, ttl:300},
|
|
{type:'TXT', name:'_dmarc', value:'v=DMARC1; p=quarantine; rua=mailto:dmarc@' + zone, ttl:300},
|
|
];
|
|
if (dkim) recs.push({type:'TXT', name:'default._domainkey', value:dkim, ttl:300});
|
|
// Autoconfig
|
|
recs.push({type:'CNAME', name:'autoconfig', value:host + '.', ttl:300});
|
|
recs.push({type:'SRV', name:'_submission._tcp', value:host + '.', ttl:300, priority:0});
|
|
recs.push({type:'SRV', name:'_imap._tcp', value:host + '.', ttl:300, priority:0});
|
|
|
|
_bulkAdd(zone, recs, 'tpl-mail-status');
|
|
}
|
|
|
|
function applyPTRTemplate() {
|
|
const net = document.getElementById('tpl-ptr-net').value.trim();
|
|
const hosts = document.getElementById('tpl-ptr-hosts').value.trim();
|
|
if (!net || !hosts) return;
|
|
const el = document.getElementById('tpl-ptr-status');
|
|
|
|
// Create reverse zone first
|
|
const parts = net.split('.');
|
|
const revZone = parts.reverse().join('.') + '.in-addr.arpa';
|
|
el.innerHTML = '<span style="color:var(--text-muted)">Creating reverse zone...</span>';
|
|
|
|
fetch('/dns/zones', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({domain: revZone})})
|
|
.then(r => r.json()).then(d => {
|
|
if (!d.ok && !d.error?.includes('exists')) { el.innerHTML = `<span style="color:var(--danger)">${d.error}</span>`; return; }
|
|
// Now add PTR records
|
|
const recs = hosts.split('\n').filter(l => l.trim()).map(line => {
|
|
const [lastOctet, ...fqdnParts] = line.trim().split(/\s+/);
|
|
return {type:'PTR', name:lastOctet, value:fqdnParts.join(' ') + '.', ttl:300};
|
|
});
|
|
_bulkAdd(revZone, recs, 'tpl-ptr-status');
|
|
});
|
|
}
|
|
|
|
function applyDelegationTemplate() {
|
|
const zone = document.getElementById('tpl-del-zone').value;
|
|
const sub = document.getElementById('tpl-del-sub').value.trim();
|
|
const ns = document.getElementById('tpl-del-ns').value.trim();
|
|
const ip = document.getElementById('tpl-del-ip').value.trim();
|
|
if (!zone || !sub || !ns) return;
|
|
|
|
const recs = [{type:'NS', name:sub, value:ns + '.', ttl:300}];
|
|
if (ip) recs.push({type:'A', name:ns.split('.')[0], value:ip, ttl:300});
|
|
_bulkAdd(zone, recs, 'tpl-del-status');
|
|
}
|
|
|
|
// ── Config ──
|
|
function loadConfig() {
|
|
fetch('/dns/config').then(r => r.json()).then(d => {
|
|
if (!d.ok) return;
|
|
const c = d.config;
|
|
// Network
|
|
document.getElementById('cfg-dns-listen').value = c.listen_dns || '0.0.0.0:53';
|
|
document.getElementById('cfg-api-listen').value = c.listen_api || '127.0.0.1:5380';
|
|
document.getElementById('cfg-upstream').value = (c.upstream || []).join(', ');
|
|
// Cache & Performance
|
|
document.getElementById('cfg-cache-ttl').value = c.cache_ttl || 300;
|
|
document.getElementById('cfg-neg-cache-ttl').value = c.negative_cache_ttl || 60;
|
|
document.getElementById('cfg-servfail-ttl').value = c.servfail_cache_ttl || 30;
|
|
document.getElementById('cfg-querylog-max').value = c.querylog_max || 1000;
|
|
document.getElementById('cfg-max-udp').value = c.max_udp_size || 1232;
|
|
document.getElementById('cfg-rate-limit').value = c.rate_limit || 100;
|
|
document.getElementById('cfg-prefetch').checked = c.prefetch_enabled === true;
|
|
// Security
|
|
document.getElementById('cfg-log-queries').checked = c.log_queries !== false;
|
|
document.getElementById('cfg-refuse-any').checked = c.refuse_any !== false;
|
|
document.getElementById('cfg-minimal').checked = c.minimal_responses !== false;
|
|
document.getElementById('cfg-allow-transfer').value = (c.allow_transfer || []).join(', ');
|
|
// Encryption
|
|
document.getElementById('cfg-enable-doh').checked = c.enable_doh !== false;
|
|
document.getElementById('cfg-enable-dot').checked = c.enable_dot !== false;
|
|
// Hosts
|
|
document.getElementById('cfg-hosts-file').value = c.hosts_file || '';
|
|
document.getElementById('cfg-hosts-autoload').checked = c.hosts_auto_load === true;
|
|
});
|
|
}
|
|
|
|
function saveConfig() {
|
|
const data = {
|
|
// Network
|
|
listen_dns: document.getElementById('cfg-dns-listen').value,
|
|
listen_api: document.getElementById('cfg-api-listen').value,
|
|
upstream: document.getElementById('cfg-upstream').value.split(',').map(s => s.trim()).filter(Boolean),
|
|
// Cache & Performance
|
|
cache_ttl: parseInt(document.getElementById('cfg-cache-ttl').value) || 300,
|
|
negative_cache_ttl: parseInt(document.getElementById('cfg-neg-cache-ttl').value) || 60,
|
|
servfail_cache_ttl: parseInt(document.getElementById('cfg-servfail-ttl').value) || 30,
|
|
querylog_max: parseInt(document.getElementById('cfg-querylog-max').value) || 1000,
|
|
max_udp_size: parseInt(document.getElementById('cfg-max-udp').value) || 1232,
|
|
rate_limit: parseInt(document.getElementById('cfg-rate-limit').value) || 0,
|
|
prefetch_enabled: document.getElementById('cfg-prefetch').checked,
|
|
// Security
|
|
log_queries: document.getElementById('cfg-log-queries').checked,
|
|
refuse_any: document.getElementById('cfg-refuse-any').checked,
|
|
minimal_responses: document.getElementById('cfg-minimal').checked,
|
|
allow_transfer: document.getElementById('cfg-allow-transfer').value.split(',').map(s => s.trim()).filter(Boolean),
|
|
// Encryption
|
|
enable_doh: document.getElementById('cfg-enable-doh').checked,
|
|
enable_dot: document.getElementById('cfg-enable-dot').checked,
|
|
// Hosts
|
|
hosts_file: document.getElementById('cfg-hosts-file').value.trim(),
|
|
hosts_auto_load: document.getElementById('cfg-hosts-autoload').checked,
|
|
};
|
|
fetch('/dns/config', {method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data)})
|
|
.then(r => r.json()).then(d => {
|
|
document.getElementById('cfg-status').innerHTML = d.ok ? '<span style="color:#4ade80">All settings saved</span>' : `<span style="color:var(--danger)">${d.error}</span>`;
|
|
});
|
|
}
|
|
|
|
// ── EZ-Local ──
|
|
let _ezHosts = [];
|
|
let _ezCustomHosts = [];
|
|
let _ezNetInfo = {};
|
|
|
|
function ezScanNetwork() {
|
|
document.getElementById('ez-scan-status').innerHTML = '<span style="color:var(--accent)">Scanning network...</span>';
|
|
fetch('/dns/network-info').then(r => r.json()).then(d => {
|
|
_ezNetInfo = d;
|
|
document.getElementById('ez-ns-ip').value = d.default_ip || '';
|
|
document.getElementById('ez-gateway').value = d.gateway || '';
|
|
document.getElementById('ez-subnet').value = d.subnet || '';
|
|
|
|
// Update client instructions
|
|
document.getElementById('ez-my-dns-ip').textContent = d.default_ip || '127.0.0.1';
|
|
document.querySelectorAll('.ez-dns-ip').forEach(el => el.textContent = d.default_ip || '127.0.0.1');
|
|
|
|
// Suggest domain based on hostname
|
|
if (d.hostname && !document.getElementById('ez-domain').value.includes('.')) {
|
|
document.getElementById('ez-domain').value = d.hostname.toLowerCase().split('.')[0] + '.local';
|
|
}
|
|
|
|
// Show discovered hosts
|
|
_ezHosts = (d.hosts || []).map(h => ({
|
|
ip: h.ip,
|
|
mac: h.mac || '',
|
|
name: h.name || '',
|
|
hostname: h.name ? h.name.split('.')[0].toLowerCase() : '',
|
|
include: true
|
|
}));
|
|
ezRenderHosts();
|
|
|
|
document.getElementById('ez-scan-status').innerHTML = `<span style="color:#4ade80">Found ${_ezHosts.length} hosts on ${d.subnet || 'network'}</span>`;
|
|
}).catch(e => {
|
|
document.getElementById('ez-scan-status').innerHTML = `<span style="color:var(--danger)">Scan failed: ${e.message}</span>`;
|
|
});
|
|
}
|
|
|
|
function ezRenderHosts() {
|
|
const el = document.getElementById('ez-hosts');
|
|
document.getElementById('ez-host-count').textContent = `${_ezHosts.filter(h => h.include).length} of ${_ezHosts.length} selected`;
|
|
if (!_ezHosts.length) { el.innerHTML = '<div style="color:var(--text-muted)">No hosts discovered</div>'; return; }
|
|
|
|
el.innerHTML = _ezHosts.map((h, i) => `<div style="display:grid;grid-template-columns:auto 130px 1fr 120px;gap:0.5rem;align-items:center;padding:4px 0;border-bottom:1px solid var(--border)">
|
|
<input type="checkbox" ${h.include ? 'checked' : ''} onchange="_ezHosts[${i}].include=this.checked;ezRenderHosts()">
|
|
<span style="font-family:monospace;font-size:0.8rem">${h.ip}</span>
|
|
<input class="form-input" style="padding:0.25rem 0.4rem;font-size:0.8rem" value="${esc(h.hostname)}" onchange="_ezHosts[${i}].hostname=this.value" placeholder="hostname">
|
|
<span style="font-size:0.72rem;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis" title="${esc(h.mac)}">${h.mac || ''}</span>
|
|
</div>`).join('');
|
|
}
|
|
|
|
function ezAddCustomHost() {
|
|
const name = document.getElementById('ez-custom-name').value.trim();
|
|
const ip = document.getElementById('ez-custom-ip').value.trim();
|
|
if (!name || !ip) return;
|
|
_ezCustomHosts.push({hostname: name, ip});
|
|
document.getElementById('ez-custom-name').value = '';
|
|
document.getElementById('ez-custom-ip').value = '';
|
|
const el = document.getElementById('ez-custom-list');
|
|
el.innerHTML = _ezCustomHosts.map((h, i) => `<div style="display:flex;justify-content:space-between;padding:3px 0;font-size:0.82rem">
|
|
<span><span style="font-family:monospace">${esc(h.hostname)}</span> → ${h.ip}</span>
|
|
<button class="btn btn-small" onclick="_ezCustomHosts.splice(${i},1);ezAddCustomHost.__render()" style="font-size:0.7rem;padding:1px 4px">x</button>
|
|
</div>`).join('');
|
|
ezAddCustomHost.__render = () => { document.getElementById('ez-custom-list').innerHTML = _ezCustomHosts.map((h, i) => `<div style="display:flex;justify-content:space-between;padding:3px 0;font-size:0.82rem"><span><span style="font-family:monospace">${esc(h.hostname)}</span> → ${h.ip}</span><button class="btn btn-small" onclick="_ezCustomHosts.splice(${i},1);ezAddCustomHost.__render()" style="font-size:0.7rem;padding:1px 4px">x</button></div>`).join(''); };
|
|
}
|
|
|
|
function ezUpdateDomain() {
|
|
const tld = document.getElementById('ez-tld').value;
|
|
if (tld) {
|
|
const current = document.getElementById('ez-domain').value;
|
|
const base = current.split('.')[0] || 'home';
|
|
document.getElementById('ez-domain').value = base + tld;
|
|
}
|
|
}
|
|
|
|
function _ezBuildRecords() {
|
|
const domain = document.getElementById('ez-domain').value.trim();
|
|
const nsIp = document.getElementById('ez-ns-ip').value.trim();
|
|
const gwIp = document.getElementById('ez-gateway').value.trim();
|
|
const records = [];
|
|
|
|
if (document.getElementById('ez-ns-record').checked && nsIp) {
|
|
records.push({type:'A', name:'ns1', value:nsIp, ttl:300});
|
|
records.push({type:'NS', name:'@', value:`ns1.${domain}.`, ttl:300});
|
|
}
|
|
if (document.getElementById('ez-gateway-record').checked && gwIp) {
|
|
records.push({type:'A', name:'gateway', value:gwIp, ttl:300});
|
|
records.push({type:'A', name:'router', value:gwIp, ttl:300});
|
|
}
|
|
if (document.getElementById('ez-server-record').checked && nsIp) {
|
|
records.push({type:'A', name:'server', value:nsIp, ttl:300});
|
|
records.push({type:'A', name:(_ezNetInfo.hostname || 'autarch').toLowerCase().split('.')[0], value:nsIp, ttl:300});
|
|
}
|
|
if (document.getElementById('ez-dashboard-record').checked && nsIp) {
|
|
records.push({type:'A', name:'dashboard', value:nsIp, ttl:300});
|
|
records.push({type:'A', name:'autarch', value:nsIp, ttl:300});
|
|
}
|
|
if (document.getElementById('ez-wildcard-record').checked && nsIp) {
|
|
records.push({type:'A', name:'*', value:nsIp, ttl:300});
|
|
}
|
|
|
|
// ARP-discovered hosts
|
|
if (document.getElementById('ez-arp-hosts').checked) {
|
|
_ezHosts.filter(h => h.include && h.hostname).forEach(h => {
|
|
records.push({type:'A', name:h.hostname, value:h.ip, ttl:300});
|
|
});
|
|
}
|
|
|
|
// Custom hosts
|
|
_ezCustomHosts.forEach(h => {
|
|
records.push({type:'A', name:h.hostname, value:h.ip, ttl:300});
|
|
});
|
|
|
|
return {domain, records};
|
|
}
|
|
|
|
function ezPreview() {
|
|
const {domain, records} = _ezBuildRecords();
|
|
if (!domain) return;
|
|
|
|
let text = `; Zone: ${domain}\n; Generated by AUTARCH EZ-Local\n;\n$ORIGIN ${domain}.\n$TTL 300\n\n`;
|
|
records.forEach(r => {
|
|
const pad = ' '.repeat(Math.max(1, 16 - r.name.length));
|
|
text += `${r.name}${pad}IN ${r.type.padEnd(6)} ${r.value}\n`;
|
|
});
|
|
|
|
if (document.getElementById('ez-reverse-zone').checked) {
|
|
const prefix = document.getElementById('ez-subnet').value.split('.').slice(0, 3);
|
|
text += `\n; Reverse zone: ${prefix.reverse().join('.')}.in-addr.arpa\n`;
|
|
records.filter(r => r.type === 'A').forEach(r => {
|
|
const lastOctet = r.value.split('.')[3];
|
|
text += `${lastOctet} IN PTR ${r.name}.${domain}.\n`;
|
|
});
|
|
}
|
|
|
|
document.getElementById('ez-preview').style.display = '';
|
|
document.getElementById('ez-preview-text').textContent = text;
|
|
}
|
|
|
|
function ezDeploy() {
|
|
const {domain, records} = _ezBuildRecords();
|
|
if (!domain || !records.length) return;
|
|
const el = document.getElementById('ez-deploy-status');
|
|
el.innerHTML = '<span style="color:var(--accent)">Creating zone...</span>';
|
|
|
|
// Create zone, then bulk add records
|
|
fetch('/dns/zones', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({domain})})
|
|
.then(r => r.json()).then(d => {
|
|
if (!d.ok && !d.error?.includes('exists')) { el.innerHTML = `<span style="color:var(--danger)">${d.error}</span>`; return; }
|
|
el.innerHTML = '<span style="color:var(--accent)">Adding records...</span>';
|
|
return fetch(`/dns/zone-bulk-records/${domain}`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({records})});
|
|
})
|
|
.then(r => r?.json())
|
|
.then(d => {
|
|
if (!d) return;
|
|
if (d.ok) {
|
|
el.innerHTML = `<span style="color:#4ade80">Zone ${domain} deployed with ${records.length} records!</span>`;
|
|
loadZones();
|
|
|
|
// Handle reverse zone
|
|
if (document.getElementById('ez-reverse-zone').checked) {
|
|
const prefix = document.getElementById('ez-subnet').value.split('.').slice(0, 3);
|
|
const revZone = prefix.slice().reverse().join('.') + '.in-addr.arpa';
|
|
const ptrRecs = records.filter(r => r.type === 'A').map(r => ({
|
|
type:'PTR', name:r.value.split('.')[3], value:`${r.name}.${domain}.`, ttl:300
|
|
}));
|
|
fetch('/dns/zones', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({domain: revZone})})
|
|
.then(() => fetch(`/dns/zone-bulk-records/${revZone}`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({records: ptrRecs})}))
|
|
.then(() => { el.innerHTML += ' + reverse zone created'; });
|
|
}
|
|
} else {
|
|
el.innerHTML = `<span style="color:var(--danger)">${d.error}</span>`;
|
|
}
|
|
});
|
|
}
|
|
|
|
// ── Reverse Proxy ──
|
|
let _rpDDNSInterval = null;
|
|
|
|
function rpDetectIP() {
|
|
document.getElementById('rp-public-ip').value = 'Detecting...';
|
|
fetch('https://api.ipify.org?format=json').then(r => r.json()).then(d => {
|
|
document.getElementById('rp-public-ip').value = d.ip || '';
|
|
}).catch(() => { document.getElementById('rp-public-ip').value = 'Detection failed'; });
|
|
}
|
|
|
|
function rpUpdateDDNS() {
|
|
const zone = document.getElementById('rp-zone').value;
|
|
const hostname = document.getElementById('rp-hostname').value.trim() || '@';
|
|
const el = document.getElementById('rp-ddns-status');
|
|
el.innerHTML = '<span style="color:var(--text-muted)">Detecting IP and updating...</span>';
|
|
|
|
fetch('https://api.ipify.org?format=json').then(r => r.json()).then(d => {
|
|
const ip = d.ip;
|
|
document.getElementById('rp-public-ip').value = ip;
|
|
if (!zone || !ip) { el.innerHTML = '<span style="color:var(--danger)">Zone and IP required</span>'; return; }
|
|
|
|
// Add/update A record in zone
|
|
return fetch(`/dns/zones/${zone}/records`, {method:'POST', headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({type:'A', name:hostname, value:ip, ttl:60})
|
|
}).then(r => r.json()).then(d => {
|
|
const now = new Date().toLocaleTimeString();
|
|
el.innerHTML = `<span style="color:#4ade80">Updated ${hostname}.${zone} → ${ip} at ${now}</span>`;
|
|
});
|
|
}).catch(e => {
|
|
el.innerHTML = `<span style="color:var(--danger)">Failed: ${e.message}</span>`;
|
|
});
|
|
}
|
|
|
|
function rpStartDDNS() {
|
|
const interval = parseInt(document.getElementById('rp-interval').value) || 5;
|
|
rpUpdateDDNS();
|
|
_rpDDNSInterval = setInterval(rpUpdateDDNS, interval * 60 * 1000);
|
|
document.getElementById('rp-ddns-status').innerHTML += ' — <span style="color:var(--accent)">Auto-update active</span>';
|
|
}
|
|
|
|
function rpStopDDNS() {
|
|
if (_rpDDNSInterval) { clearInterval(_rpDDNSInterval); _rpDDNSInterval = null; }
|
|
document.getElementById('rp-ddns-status').innerHTML = '<span style="color:var(--text-muted)">Auto-update stopped</span>';
|
|
}
|
|
|
|
function rpGenerate() {
|
|
const domain = document.getElementById('rp-domain').value.trim();
|
|
const backend = document.getElementById('rp-backend').value.trim();
|
|
const ssl = document.getElementById('rp-ssl').value;
|
|
const type = document.getElementById('rp-type').value;
|
|
if (!domain || !backend) return;
|
|
|
|
let config = '';
|
|
if (type === 'nginx') {
|
|
if (ssl === 'letsencrypt') {
|
|
config = `# nginx reverse proxy — ${domain}
|
|
# Requires: certbot --nginx -d ${domain}
|
|
|
|
server {
|
|
listen 80;
|
|
server_name ${domain};
|
|
return 301 https://$host$request_uri;
|
|
}
|
|
|
|
server {
|
|
listen 443 ssl http2;
|
|
server_name ${domain};
|
|
|
|
ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
|
|
location / {
|
|
proxy_pass ${backend};
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
}
|
|
}`;
|
|
} else if (ssl === 'selfsigned') {
|
|
config = `# nginx reverse proxy — ${domain} (self-signed)
|
|
# Generate cert: openssl req -x509 -nodes -days 365 -newkey rsa:2048 \\
|
|
# -keyout /etc/ssl/private/${domain}.key \\
|
|
# -out /etc/ssl/certs/${domain}.crt \\
|
|
# -subj "/CN=${domain}"
|
|
|
|
server {
|
|
listen 80;
|
|
server_name ${domain};
|
|
return 301 https://$host$request_uri;
|
|
}
|
|
|
|
server {
|
|
listen 443 ssl http2;
|
|
server_name ${domain};
|
|
|
|
ssl_certificate /etc/ssl/certs/${domain}.crt;
|
|
ssl_certificate_key /etc/ssl/private/${domain}.key;
|
|
|
|
location / {
|
|
proxy_pass ${backend};
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
}`;
|
|
} else {
|
|
config = `# nginx reverse proxy — ${domain} (HTTP only)
|
|
|
|
server {
|
|
listen 80;
|
|
server_name ${domain};
|
|
|
|
location / {
|
|
proxy_pass ${backend};
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
}
|
|
}`;
|
|
}
|
|
} else if (type === 'caddy') {
|
|
if (ssl === 'none') {
|
|
config = `# Caddyfile — ${domain} (HTTP only)
|
|
|
|
http://${domain} {
|
|
reverse_proxy ${backend}
|
|
}`;
|
|
} else if (ssl === 'selfsigned') {
|
|
config = `# Caddyfile — ${domain} (self-signed TLS)
|
|
|
|
${domain} {
|
|
tls internal
|
|
reverse_proxy ${backend}
|
|
}`;
|
|
} else {
|
|
config = `# Caddyfile — ${domain} (auto Let's Encrypt)
|
|
# Caddy handles TLS automatically
|
|
|
|
${domain} {
|
|
reverse_proxy ${backend}
|
|
|
|
header {
|
|
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
|
X-Content-Type-Options "nosniff"
|
|
X-Frame-Options "SAMEORIGIN"
|
|
}
|
|
|
|
log {
|
|
output file /var/log/caddy/${domain}.log
|
|
}
|
|
}`;
|
|
}
|
|
} else if (type === 'apache') {
|
|
if (ssl === 'letsencrypt') {
|
|
config = `# Apache reverse proxy — ${domain}
|
|
# Requires: certbot --apache -d ${domain}
|
|
# Enable modules: a2enmod proxy proxy_http ssl headers
|
|
|
|
<VirtualHost *:80>
|
|
ServerName ${domain}
|
|
Redirect permanent / https://${domain}/
|
|
</VirtualHost>
|
|
|
|
<VirtualHost *:443>
|
|
ServerName ${domain}
|
|
|
|
SSLEngine on
|
|
SSLCertificateFile /etc/letsencrypt/live/${domain}/fullchain.pem
|
|
SSLCertificateKeyFile /etc/letsencrypt/live/${domain}/privkey.pem
|
|
|
|
ProxyPreserveHost On
|
|
ProxyPass / ${backend}/
|
|
ProxyPassReverse / ${backend}/
|
|
|
|
RequestHeader set X-Forwarded-Proto "https"
|
|
RequestHeader set X-Real-IP "%{REMOTE_ADDR}e"
|
|
</VirtualHost>`;
|
|
} else {
|
|
config = `# Apache reverse proxy — ${domain}
|
|
# Enable modules: a2enmod proxy proxy_http headers
|
|
|
|
<VirtualHost *:80>
|
|
ServerName ${domain}
|
|
|
|
ProxyPreserveHost On
|
|
ProxyPass / ${backend}/
|
|
ProxyPassReverse / ${backend}/
|
|
|
|
RequestHeader set X-Real-IP "%{REMOTE_ADDR}e"
|
|
</VirtualHost>`;
|
|
}
|
|
}
|
|
|
|
document.getElementById('rp-config-panel').style.display = '';
|
|
document.getElementById('rp-config-output').textContent = config;
|
|
}
|
|
|
|
function rpCopyConfig() {
|
|
navigator.clipboard.writeText(document.getElementById('rp-config-output').textContent).then(() => alert('Copied!'));
|
|
}
|
|
|
|
function rpDownloadConfig() {
|
|
const text = document.getElementById('rp-config-output').textContent;
|
|
const type = document.getElementById('rp-type').value;
|
|
const domain = document.getElementById('rp-domain').value.trim() || 'site';
|
|
const ext = type === 'caddy' ? 'Caddyfile' : (type + '.conf');
|
|
const blob = new Blob([text], {type:'text/plain'});
|
|
const a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = `${domain}.${ext}`;
|
|
a.click();
|
|
}
|
|
|
|
function rpUPnPForward(port) {
|
|
const el = document.getElementById('rp-upnp-status');
|
|
el.innerHTML = `<span style="color:var(--text-muted)">Requesting UPnP forward for port ${port}...</span>`;
|
|
// This would call a UPnP backend — placeholder for now
|
|
fetch('/api/upnp/forward', {method:'POST', headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({external_port: port, internal_port: port, protocol:'TCP'})
|
|
}).then(r => r.json()).then(d => {
|
|
el.innerHTML = d.ok ? `<span style="color:#4ade80">Port ${port} forwarded via UPnP</span>` : `<span style="color:var(--danger)">${d.error || 'UPnP not available'}</span>`;
|
|
}).catch(() => {
|
|
el.innerHTML = `<span style="color:var(--danger)">UPnP forward failed — configure manually on your router</span>`;
|
|
});
|
|
}
|
|
|
|
function esc(s) {
|
|
return s ? String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"') : '';
|
|
}
|
|
|
|
// Init
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
checkDNS();
|
|
loadZones();
|
|
// Auto-detect LAN IP for reverse proxy panel
|
|
fetch('/dns/network-info').then(r => r.json()).then(d => {
|
|
const lanEl = document.getElementById('rp-lan-ip');
|
|
if (lanEl) lanEl.textContent = d.default_ip || '127.0.0.1';
|
|
}).catch(() => {});
|
|
rpDetectIP();
|
|
});
|
|
</script>
|
|
{% endblock %}
|