Health-Check: DNS-over-HTTPS-Rückfall bei nicht auflösbarem Hostnamen, DNS-Problem des Admin-Servers nur Warnung (außer strikte Rollouts); COST_INGEST_KEY optional (Schema-getrieben)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
60
app/services/netcheck.py
Normal file
60
app/services/netcheck.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""HTTP-GET mit DNS-Rückfall für Health-Checks.
|
||||
|
||||
Der Resolver des Admin-Servers (Coolify/Docker) cacht „Name nicht gefunden“ für neue Hostnamen oft lange – obwohl der Name öffentlich
|
||||
längst existiert. Schlägt die Auflösung fehl, fragen wir die Adresse per DNS-over-HTTPS (Cloudflare, dann Google) ab und verbinden uns
|
||||
direkt zur IP (Host-Header + SNI = Hostname, Zertifikat wird normal geprüft).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
def _is_dns_error(exc: BaseException) -> bool:
|
||||
seen = set()
|
||||
while exc is not None and id(exc) not in seen:
|
||||
seen.add(id(exc))
|
||||
if isinstance(exc, socket.gaierror):
|
||||
return True
|
||||
exc = exc.__cause__ or exc.__context__
|
||||
return False
|
||||
|
||||
|
||||
def is_dns_failure_text(msg: str) -> bool:
|
||||
m = msg.lower()
|
||||
return "name or service not known" in m or "temporary failure in name resolution" in m or "nodename nor servname" in m or "gaierror" in m
|
||||
|
||||
|
||||
def doh_resolve(host: str, timeout: float = 5.0) -> list[str]:
|
||||
for url in ("https://cloudflare-dns.com/dns-query", "https://dns.google/resolve"):
|
||||
try:
|
||||
r = httpx.get(url, params={"name": host, "type": "A"}, headers={"accept": "application/dns-json"}, timeout=timeout)
|
||||
ips = [a["data"] for a in r.json().get("Answer", []) if a.get("type") == 1]
|
||||
if ips:
|
||||
return ips
|
||||
except Exception: # noqa: BLE001
|
||||
continue
|
||||
return []
|
||||
|
||||
|
||||
def get_json(url: str, timeout: float = 8.0) -> tuple[int, dict]:
|
||||
"""GET mit DNS-Rückfall → (status, json-dict oder {}). Bei Rückfall enthält das Dict `_dns_fallback: true`."""
|
||||
def parse(r):
|
||||
return r.status_code, (r.json() if r.headers.get("content-type", "").startswith("application/json") else {})
|
||||
try:
|
||||
return parse(httpx.get(url, timeout=timeout, follow_redirects=True))
|
||||
except httpx.ConnectError as exc:
|
||||
if not _is_dns_error(exc):
|
||||
raise
|
||||
parts = urlsplit(url)
|
||||
for ip in doh_resolve(parts.hostname or ""):
|
||||
try:
|
||||
r = httpx.get(f"{parts.scheme}://{ip}{parts.path or '/'}", headers={"Host": parts.hostname}, extensions={"sni_hostname": parts.hostname},
|
||||
timeout=timeout, follow_redirects=True)
|
||||
status, body = parse(r)
|
||||
return status, {**body, "_dns_fallback": True}
|
||||
except Exception: # noqa: BLE001
|
||||
continue
|
||||
raise
|
||||
Reference in New Issue
Block a user