From 27d89b12544704e193ad16a4bdbc2e6d2ebc41ae Mon Sep 17 00:00:00 2001 From: JakeBreath Date: Sun, 16 Aug 2026 19:15:26 -0500 Subject: [PATCH] Implemented public DNS resolution checks --- checks.py | 43 +++++++++++++++++++++++++++++++++++++++++++ main.py | 21 ++++++++++++++++----- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/checks.py b/checks.py index 8881844..51e00b6 100644 --- a/checks.py +++ b/checks.py @@ -1,6 +1,7 @@ import asyncio import json import time +from urllib.parse import urlparse import aiohttp @@ -24,6 +25,11 @@ TARGETS = [ GITEA_VERSION_URL = 'https://gitea.rainbow-herring.ts.net/api/v1/version' +DOH_RESOLVERS = { + 'Cloudflare': 'https://cloudflare-dns.com/dns-query', + 'Google': 'https://dns.google/resolve', +} + async def _get(session, url, timeout): started = time.monotonic() @@ -37,6 +43,38 @@ async def _get(session, url, timeout): return resp.status, latency_ms, body +async def _resolve_public(session, resolver_url, host, timeout): + try: + async with session.get( + resolver_url, + params={'name': host, 'type': 'A'}, + headers={'Accept': 'application/dns-json'}, + timeout=aiohttp.ClientTimeout(total=timeout), + ) as resp: + if resp.status != 200: + return {'ok': False, 'ips': [], 'error': f'HTTP {resp.status}'} + body = await resp.text() + data = json.loads(body) + except asyncio.TimeoutError: + return {'ok': False, 'ips': [], 'error': 'timeout'} + except aiohttp.ClientError: + return {'ok': False, 'ips': [], 'error': 'error'} + except Exception: + return {'ok': False, 'ips': [], 'error': 'error'} + + status = data.get('Status') + if status == 3: + return {'ok': False, 'ips': [], 'error': 'NXDOMAIN'} + if status == 2: + return {'ok': False, 'ips': [], 'error': 'SERVFAIL'} + if status != 0: + return {'ok': False, 'ips': [], 'error': f'status {status}'} + answers = [a for a in data.get('Answer', []) if a.get('type') == 1] + if not answers: + return {'ok': False, 'ips': [], 'error': 'no A record'} + return {'ok': True, 'ips': [a['data'] for a in answers], 'error': ''} + + async def check_target(session, target, timeout): result = { 'name': target['name'], @@ -48,6 +86,7 @@ async def check_target(session, target, timeout): 'db': None, 'version': None, 'detail': '', + 'dns': {}, } try: status, latency_ms, body = await _get(session, target['url'], timeout) @@ -80,6 +119,10 @@ async def check_target(session, target, timeout): except Exception: pass + host = urlparse(target['url']).hostname + for label, resolver_url in DOH_RESOLVERS.items(): + result['dns'][label] = await _resolve_public(session, resolver_url, host, timeout) + return result diff --git a/main.py b/main.py index 830a912..ac04b4a 100644 --- a/main.py +++ b/main.py @@ -72,14 +72,25 @@ class HealthBot(discord.Client): emoji = '🟢' if r['up'] else '🔴' code = r['status_code'] if r['status_code'] is not None else '—' latency = f"{r['latency_ms']} ms" if r['latency_ms'] is not None else '—' - value = f'{emoji} `HTTP {code}` · {latency}' + hc = f'{emoji} `HTTP {code}` · {latency}' if r.get('db') is not None: - value += f' · db {r["db"]}' + hc += f' · db {r["db"]}' if r.get('version'): - value += f' · v{r["version"]}' + hc += f' · v{r["version"]}' if r.get('detail'): - value += f'\n*{r["detail"]}*' - embed.add_field(name=r['name'], value=value, inline=False) + hc += f' · *{r["detail"]}*' + + lines = [f'**healthcheck** {hc}'] + for label in ('Cloudflare', 'Google'): + d = r.get('dns', {}).get(label) + if d and d.get('ok'): + val = '✓ ' + ', '.join(d['ips']) + else: + err = d.get('error') if d else 'unknown' + val = f'✗ {err}' + lines.append(f'**DNS resolution ({label})** {val}') + + embed.add_field(name=r['name'], value='\n'.join(lines), inline=False) embed.set_footer(text=f'Updates every {config.CHECK_INTERVAL}s') return embed