Files
Berwn 54f607d063 Add blackbox exporter for outside-in DNS probes
control runs blackbox_exporter on loopback, probing each nameserver's
public v4+v6 address for every zone: SOA (zone served) and DNSKEY (still
signed, since blackbox has no DO-bit option). Probe definitions are
shared between the exporter config and the VictoriaMetrics scrape jobs
so they can't drift. Verified live against ns1/ns2 over v4 and v6.
2026-06-17 15:37:45 +07:00

109 lines
3.1 KiB
Nix

# Blackbox DNS probe definitions, shared between the exporter module
# (modules/monitoring/blackbox.nix, which renders these into the blackbox
# config) and the scraper (modules/monitoring/server.nix, which turns them into
# VictoriaMetrics scrape jobs). Kept in one place so the module list and the
# scrape jobs can never drift apart.
#
# These query the nameservers' PUBLIC addresses, i.e. the path a real internet
# resolver takes, not the mesh — the whole point is to catch outside-in
# resolution failures the Knot stats can't see. For each zone we run two probes
# per endpoint: an SOA query (is the zone being served at all?) and a DNSKEY
# query (is it still DNSSEC-signed?). Blackbox has no DO-bit option, so we ask
# for DNSKEY directly — an authoritative signed zone returns it without EDNS0,
# and its absence means signing has broken.
{ lib }:
let
domains = import ../dns/domains.nix;
blackboxAddr = "127.0.0.1:9115";
# Public endpoints of the authoritative nameservers. The v4 addresses also
# appear in the `internet` instance in clan.nix; the v6 ones in each ns
# machine's cnx.staticIPv6. IPv6 literals are bracketed for host:port.
endpoints = [
{
instance = "ns1 v4";
target = "46.224.170.206:53";
}
{
instance = "ns1 v6";
target = "[2a01:4f8:c014:b5c5::1]:53";
}
{
instance = "ns2 v4";
target = "157.180.70.82:53";
}
{
instance = "ns2 v6";
target = "[2a01:4f9:c014:6d87::1]:53";
}
];
queries = [
{
name = "soa";
type = "SOA";
}
{
name = "dnskey";
type = "DNSKEY";
}
];
sanitize = lib.replaceStrings [ "." ] [ "_" ];
moduleName = zone: q: "dns_${q.name}_${sanitize zone}";
modules = lib.listToAttrs (
lib.concatMap (
zone:
map (
q:
lib.nameValuePair (moduleName zone q) {
prober = "dns";
timeout = "5s";
dns = {
query_name = "${zone}.";
query_type = q.type;
valid_rcodes = [ "NOERROR" ];
# Fail unless at least one answer RR of the queried type is present:
# a NOERROR with an empty answer (or a missing DNSKEY) still fails.
validate_answer_rrs.fail_if_not_matches_regexp = [ "\\s${q.type}\\s" ];
};
}
) queries
) domains
);
scrapeConfigs = lib.concatMap (
zone:
map (q: {
job_name = "blackbox_${moduleName zone q}";
metrics_path = "/probe";
params.module = [ (moduleName zone q) ];
static_configs = map (e: {
targets = [ e.target ];
labels = {
instance = e.instance;
zone = zone;
query = q.type;
};
}) endpoints;
# Hand the real DNS server to blackbox as ?target=, then point the scrape
# at the exporter itself.
relabel_configs = [
{
source_labels = [ "__address__" ];
target_label = "__param_target";
}
{
target_label = "__address__";
replacement = blackboxAddr;
}
];
}) queries
) domains;
in
{
inherit modules scrapeConfigs blackboxAddr;
}