277 lines
9.6 KiB
Python
277 lines
9.6 KiB
Python
import json
|
|
import os
|
|
import threading
|
|
from datetime import datetime
|
|
import config_utils
|
|
import factory
|
|
import mod_dns_queries
|
|
from pages.ddns.view import public_ip_info
|
|
from pages.dhcpleases.view import live_dhcp_leases
|
|
|
|
METRICS_DB = config_utils.DNS_METRICS_DB
|
|
|
|
|
|
def _fmt_since(since_str):
|
|
try:
|
|
dt = datetime.strptime(since_str, '%Y-%m-%d')
|
|
now = datetime.now()
|
|
rel = config_utils.relative_time(int(dt.timestamp()), int(now.timestamp()))
|
|
if dt.date() == now.date():
|
|
return 'Today'
|
|
return f'{dt.strftime("%Y-%m-%d")} ({rel} ago)'
|
|
except Exception:
|
|
return since_str
|
|
|
|
|
|
def _fmt_updated(updated_ts):
|
|
try:
|
|
now = datetime.now()
|
|
rel = config_utils.relative_time(int(updated_ts), int(now.timestamp()))
|
|
return f'{rel} ago'
|
|
except Exception:
|
|
return '-'
|
|
|
|
|
|
def _dns_providers_table(servers):
|
|
if not servers:
|
|
return '<p class="text-muted" style="margin:0.5rem 0 0">No upstream server data recorded yet.</p>'
|
|
rows = []
|
|
for s in servers:
|
|
latency = f'{s["avg_latency_ms"]} ms' if s.get("avg_latency_ms") else '-'
|
|
rows.append(
|
|
f'<tr>'
|
|
f'<td class="table-cell">{factory.e(s.get("address", "-"))}</td>'
|
|
f'<td class="table-cell">{s.get("queries_sent", 0):,}</td>'
|
|
f'<td class="table-cell">{s.get("retried", 0):,}</td>'
|
|
f'<td class="table-cell">{s.get("failed", 0):,}</td>'
|
|
f'<td class="table-cell">{s.get("nxdomain", 0):,}</td>'
|
|
f'<td class="table-cell">{latency}</td>'
|
|
f'</tr>'
|
|
)
|
|
return (
|
|
'<table class="data-table" style="margin-top:0.75rem"><thead><tr>'
|
|
'<th class="table-header">Upstream DNS Provider</th>'
|
|
'<th class="table-header">Queries Sent</th>'
|
|
'<th class="table-header">Retried</th>'
|
|
'<th class="table-header">Failed</th>'
|
|
'<th class="table-header">NXDOMAIN</th>'
|
|
'<th class="table-header">Avg Latency</th>'
|
|
'</tr></thead><tbody>'
|
|
+ ''.join(rows)
|
|
+ '</tbody></table>'
|
|
)
|
|
|
|
|
|
def load_dns_metrics(period=0):
|
|
import sqlite3
|
|
empty = {
|
|
'queries': '-', 'hits': '-', 'hit_rate': '-', 'forwarded': '-',
|
|
'auth': '-', 'tcp_peak': '-', 'cache_evictions': '-',
|
|
'updated': '-', 'since': '-', 'servers': [],
|
|
}
|
|
try:
|
|
where = (
|
|
f"WHERE date >= date('now','localtime','-{period - 1} days')"
|
|
if period and period > 0 else ''
|
|
)
|
|
con = sqlite3.connect(METRICS_DB, timeout=5)
|
|
con.execute('PRAGMA journal_mode=WAL')
|
|
row = con.execute(f'''
|
|
SELECT
|
|
MIN(date), MAX(last_updated),
|
|
SUM(queries_forwarded), SUM(queries_answered_locally),
|
|
SUM(queries_authoritative), SUM(cache_reused), MAX(tcp_hwm)
|
|
FROM daily_totals {where}
|
|
''').fetchone()
|
|
srv_rows = con.execute(f'''
|
|
SELECT
|
|
ds.address,
|
|
SUM(ds.queries_sent),
|
|
SUM(ds.retried),
|
|
SUM(ds.failed),
|
|
SUM(ds.nxdomain),
|
|
(SELECT avg_latency_ms FROM daily_servers d2
|
|
WHERE d2.address = ds.address AND d2.avg_latency_ms > 0
|
|
ORDER BY d2.date DESC LIMIT 1)
|
|
FROM daily_servers ds {where}
|
|
GROUP BY ds.address
|
|
ORDER BY SUM(ds.queries_sent) DESC
|
|
''').fetchall()
|
|
con.close()
|
|
|
|
if not row or row[0] is None:
|
|
return empty
|
|
|
|
since_raw, updated_raw, fwd, hits, auth, reused, tcp_hwm = row
|
|
fwd = fwd or 0
|
|
hits = hits or 0
|
|
total = fwd + hits
|
|
|
|
servers = [
|
|
{
|
|
'address': r[0],
|
|
'queries_sent': r[1] or 0,
|
|
'retried': r[2] or 0,
|
|
'failed': r[3] or 0,
|
|
'nxdomain': r[4] or 0,
|
|
'avg_latency_ms': r[5] or 0,
|
|
}
|
|
for r in srv_rows
|
|
]
|
|
|
|
return {
|
|
'queries': f'{total:,}' if total else '-',
|
|
'hits': f'{hits:,}' if hits else '-',
|
|
'hit_rate': f'{hits / total * 100:.0f}%' if total > 0 else '-',
|
|
'forwarded': f'{fwd:,}' if fwd else '-',
|
|
'auth': f'{(auth or 0):,}',
|
|
'tcp_peak': str(tcp_hwm or 0),
|
|
'cache_evictions': f'{(reused or 0):,}',
|
|
'updated': _fmt_updated(updated_raw),
|
|
'since': _fmt_since(since_raw),
|
|
'servers': servers,
|
|
}
|
|
except Exception:
|
|
return empty
|
|
|
|
|
|
DNS_QUERIES_DB = f'{config_utils.CONFIGS_DIR}/dns-queries.db'
|
|
|
|
|
|
def has_query_logging(cfg):
|
|
return any(v.get('dnsmasq_log_queries') for v in cfg.get('vlans', []))
|
|
|
|
|
|
def blocked_domains_table():
|
|
no_data = '<p class="text-muted" style="margin:0.5rem 0 0">No query data collected yet.</p>'
|
|
try:
|
|
import sqlite3
|
|
if not os.path.exists(DNS_QUERIES_DB):
|
|
return no_data
|
|
con = sqlite3.connect(DNS_QUERIES_DB)
|
|
rows = con.execute(
|
|
'SELECT domain, COUNT(*) as cnt FROM dns_queries WHERE blocked=1 '
|
|
'GROUP BY domain ORDER BY cnt DESC LIMIT 10'
|
|
).fetchall()
|
|
con.close()
|
|
if not rows:
|
|
return no_data
|
|
trs = ''.join(
|
|
f'<tr><td class="table-cell">{factory.e(r[0])}</td>'
|
|
f'<td class="table-cell">{r[1]:,}</td></tr>'
|
|
for r in rows
|
|
)
|
|
return (
|
|
'<table class="data-table"><thead><tr>'
|
|
'<th class="table-header">Domain</th>'
|
|
'<th class="table-header">Times Blocked</th>'
|
|
'</tr></thead><tbody>' + trs + '</tbody></table>'
|
|
)
|
|
except Exception:
|
|
return no_data
|
|
|
|
|
|
def client_activity_table():
|
|
no_data = '<p class="text-muted" style="margin:0.5rem 0 0">No query data collected yet.</p>'
|
|
try:
|
|
import sqlite3
|
|
if not os.path.exists(DNS_QUERIES_DB):
|
|
return no_data
|
|
con = sqlite3.connect(DNS_QUERIES_DB)
|
|
rows = con.execute(
|
|
'SELECT client_ip, COUNT(*) as total, SUM(blocked) as blocked '
|
|
'FROM dns_queries GROUP BY client_ip ORDER BY total DESC LIMIT 10'
|
|
).fetchall()
|
|
con.close()
|
|
if not rows:
|
|
return no_data
|
|
trs = []
|
|
for client_ip, total, blocked in rows:
|
|
pct = f'{blocked / total * 100:.0f}%' if total else '0%'
|
|
trs.append(
|
|
f'<tr><td class="table-cell">{factory.e(client_ip)}</td>'
|
|
f'<td class="table-cell">{total:,}</td>'
|
|
f'<td class="table-cell">{int(blocked):,} ({pct})</td></tr>'
|
|
)
|
|
return (
|
|
'<table class="data-table"><thead><tr>'
|
|
'<th class="table-header">Client</th>'
|
|
'<th class="table-header">Total Queries</th>'
|
|
'<th class="table-header">Blocked</th>'
|
|
'</tr></thead><tbody>' + ''.join(trs) + '</tbody></table>'
|
|
)
|
|
except Exception:
|
|
return no_data
|
|
|
|
|
|
def all_time_blocked_display():
|
|
try:
|
|
import sqlite3
|
|
if not os.path.exists(DNS_QUERIES_DB):
|
|
return '-'
|
|
con = sqlite3.connect(DNS_QUERIES_DB)
|
|
row = con.execute(
|
|
'SELECT SUM(blocked), COUNT(*) FROM dns_queries'
|
|
).fetchone()
|
|
con.close()
|
|
blocked, total = row
|
|
if not blocked:
|
|
return '-'
|
|
pct = f'{blocked / total * 100:.0f}' if total else '0'
|
|
return f'{int(blocked):,} blocked ({pct}%)'
|
|
except Exception:
|
|
return '-'
|
|
|
|
|
|
def count_blocked_domains():
|
|
try:
|
|
total = sum(
|
|
int(factory.run(f'wc -l < "{config_utils.BLOCKLISTS_DIR}/{f}"') or 0)
|
|
for f in os.listdir(config_utils.BLOCKLISTS_DIR) if f.endswith('.con')
|
|
)
|
|
return str(total)
|
|
except Exception:
|
|
return '-'
|
|
|
|
|
|
def bl_last_update():
|
|
try:
|
|
mtime = max(
|
|
os.path.getmtime(f'{config_utils.BLOCKLISTS_DIR}/{f}')
|
|
for f in os.listdir(config_utils.BLOCKLISTS_DIR) if f.endswith('.con')
|
|
)
|
|
return config_utils.fmt_timestamp(int(mtime))
|
|
except Exception:
|
|
return '-'
|
|
|
|
|
|
def collect_tokens(cfg):
|
|
if has_query_logging(cfg):
|
|
threading.Thread(target=mod_dns_queries.collect, args=(cfg,), daemon=True).start()
|
|
|
|
tokens = config_utils.collect_layout_tokens(cfg)
|
|
non_vpn_vlans = [v for v in cfg.get('vlans', []) if not v.get('is_vpn')]
|
|
dns = cfg.get('upstream_dns', {})
|
|
dns_stats = load_dns_metrics()
|
|
ddns = factory.load_ddns()
|
|
ip_str, domains_sub, _ = public_ip_info(ddns)
|
|
|
|
lease_count = len(live_dhcp_leases())
|
|
tokens['STAT_LEASE_COUNT'] = str(lease_count)
|
|
tokens['STAT_LEASES_LINK'] = f'<a href="/dhcpleases">{lease_count} active lease{"s" if lease_count != 1 else ""}</a>'
|
|
tokens['STAT_VLAN_COUNT'] = str(len(non_vpn_vlans))
|
|
tokens['STAT_PUBLIC_IP'] = ip_str
|
|
tokens['STAT_DDNS_HOSTNAME'] = domains_sub
|
|
|
|
tokens['DNS_CACHE_SIZE'] = str(dns.get('cache_size', '-'))
|
|
tokens['DNS_STAT_QUERIES'] = dns_stats['queries']
|
|
tokens['DNS_STAT_HITS'] = dns_stats['hits']
|
|
tokens['DNS_STAT_HIT_RATE'] = dns_stats['hit_rate']
|
|
tokens['DNS_STAT_CACHE_EVICTIONS'] = dns_stats['cache_evictions']
|
|
tokens['DNS_METRICS_SINCE'] = dns_stats['since']
|
|
|
|
tokens['STAT_BLOCKED_ALLTIME'] = all_time_blocked_display()
|
|
tokens['HAS_QUERY_LOGGING'] = '1' if has_query_logging(cfg) else ''
|
|
tokens['BLOCKED_DOMAINS_TABLE'] = blocked_domains_table()
|
|
tokens['CLIENT_ACTIVITY_TABLE'] = client_activity_table()
|
|
return tokens
|