2025-06-15 12:51:56 +00:00
|
|
|
{ lib, pkgs, ... }:
|
|
|
|
|
|
|
|
let
|
|
|
|
dnstapSocket = "/var/run/vector/dnstap.sock";
|
|
|
|
in
|
|
|
|
{
|
|
|
|
name = "vector-dnstap";
|
|
|
|
meta.maintainers = [ pkgs.lib.maintainers.happysalada ];
|
|
|
|
|
|
|
|
nodes = {
|
2025-06-15 13:14:56 +00:00
|
|
|
clickhouse =
|
|
|
|
{ config, pkgs, ... }:
|
|
|
|
{
|
|
|
|
networking.firewall.allowedTCPPorts = [ 6000 ];
|
|
|
|
|
|
|
|
services.vector = {
|
|
|
|
enable = true;
|
|
|
|
|
|
|
|
settings = {
|
|
|
|
sources = {
|
|
|
|
vector_dnstap_source = {
|
|
|
|
type = "vector";
|
|
|
|
address = "[::]:6000";
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
sinks = {
|
|
|
|
clickhouse = {
|
|
|
|
type = "clickhouse";
|
|
|
|
inputs = [
|
|
|
|
"vector_dnstap_source"
|
|
|
|
];
|
|
|
|
endpoint = "http://localhost:8123";
|
|
|
|
database = "dnstap";
|
|
|
|
table = "records";
|
|
|
|
date_time_best_effort = true;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
services.clickhouse.enable = true;
|
|
|
|
};
|
|
|
|
|
2025-07-01 11:30:18 +00:00
|
|
|
knot =
|
|
|
|
{
|
|
|
|
config,
|
|
|
|
nodes,
|
|
|
|
pkgs,
|
|
|
|
...
|
|
|
|
}:
|
|
|
|
let
|
|
|
|
exampleZone = pkgs.writeTextDir "example.com.zone" ''
|
|
|
|
@ SOA ns.example.com. noc.example.com. 2019031301 86400 7200 3600000 172800
|
|
|
|
@ NS ns1
|
|
|
|
@ NS ns2
|
|
|
|
ns1 A 192.168.0.1
|
|
|
|
ns1 AAAA fd00::1
|
|
|
|
ns2 A 192.168.0.2
|
|
|
|
ns2 AAAA fd00::2
|
|
|
|
www A 192.0.2.1
|
|
|
|
www AAAA 2001:DB8::1
|
|
|
|
sub NS ns.example.com.
|
|
|
|
'';
|
|
|
|
|
|
|
|
knotZonesEnv = pkgs.buildEnv {
|
|
|
|
name = "knot-zones";
|
|
|
|
paths = [
|
|
|
|
exampleZone
|
|
|
|
];
|
|
|
|
};
|
|
|
|
in
|
|
|
|
{
|
|
|
|
networking.firewall.allowedUDPPorts = [ 53 ];
|
|
|
|
|
|
|
|
services.vector = {
|
|
|
|
enable = true;
|
|
|
|
|
|
|
|
settings = {
|
|
|
|
sources = {
|
|
|
|
dnstap = {
|
|
|
|
type = "dnstap";
|
|
|
|
multithreaded = true;
|
|
|
|
mode = "unix";
|
|
|
|
lowercase_hostnames = true;
|
|
|
|
socket_file_mode = 504;
|
|
|
|
socket_path = "${dnstapSocket}";
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
sinks = {
|
|
|
|
vector_dnstap_sink = {
|
|
|
|
type = "vector";
|
|
|
|
inputs = [ "dnstap" ];
|
|
|
|
address = "clickhouse:6000";
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
systemd.services.vector.serviceConfig = {
|
|
|
|
RuntimeDirectory = "vector";
|
|
|
|
RuntimeDirectoryMode = "0770";
|
|
|
|
};
|
|
|
|
|
|
|
|
services.knot = {
|
|
|
|
enable = true;
|
|
|
|
settings = {
|
|
|
|
server = {
|
|
|
|
listen = [
|
|
|
|
"0.0.0.0@53"
|
|
|
|
"::@53"
|
|
|
|
];
|
|
|
|
automatic-acl = true;
|
|
|
|
};
|
|
|
|
template.default = {
|
|
|
|
storage = knotZonesEnv;
|
|
|
|
dnssec-signing = false;
|
|
|
|
# Input-only zone files
|
|
|
|
# https://www.knot-dns.cz/docs/2.8/html/operation.html#example-3
|
|
|
|
# prevents modification of the zonefiles, since the zonefiles are immutable
|
|
|
|
zonefile-sync = -1;
|
|
|
|
zonefile-load = "difference";
|
|
|
|
journal-content = "changes";
|
|
|
|
global-module = "mod-dnstap/capture_all";
|
|
|
|
};
|
|
|
|
zone = {
|
|
|
|
"example.com".file = "example.com.zone";
|
|
|
|
};
|
|
|
|
|
|
|
|
mod-dnstap = [
|
|
|
|
{
|
|
|
|
id = "capture_all";
|
|
|
|
sink = "unix:${dnstapSocket}";
|
|
|
|
}
|
|
|
|
];
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
systemd.services.knot = {
|
|
|
|
after = [ "vector.service" ];
|
|
|
|
wants = [ "vector.service" ];
|
|
|
|
serviceConfig = {
|
|
|
|
# DNSTAP access
|
|
|
|
ReadWritePaths = [ "/var/run/vector" ];
|
|
|
|
SupplementaryGroups = [ "vector" ];
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2025-06-15 12:51:56 +00:00
|
|
|
unbound =
|
2025-07-01 11:30:18 +00:00
|
|
|
{
|
|
|
|
config,
|
|
|
|
nodes,
|
|
|
|
pkgs,
|
|
|
|
...
|
|
|
|
}:
|
2025-06-15 12:51:56 +00:00
|
|
|
{
|
|
|
|
networking.firewall.allowedUDPPorts = [ 53 ];
|
|
|
|
|
|
|
|
services.vector = {
|
|
|
|
enable = true;
|
|
|
|
|
|
|
|
settings = {
|
|
|
|
sources = {
|
|
|
|
dnstap = {
|
|
|
|
type = "dnstap";
|
|
|
|
multithreaded = true;
|
|
|
|
mode = "unix";
|
|
|
|
lowercase_hostnames = true;
|
|
|
|
socket_file_mode = 504;
|
|
|
|
socket_path = "${dnstapSocket}";
|
2024-12-10 20:26:33 +01:00
|
|
|
};
|
2025-06-15 12:51:56 +00:00
|
|
|
};
|
2024-12-10 20:26:33 +01:00
|
|
|
|
2025-06-15 12:51:56 +00:00
|
|
|
sinks = {
|
|
|
|
file = {
|
|
|
|
type = "file";
|
|
|
|
inputs = [ "dnstap" ];
|
|
|
|
path = "/var/lib/vector/logs.log";
|
|
|
|
encoding = {
|
|
|
|
codec = "json";
|
2024-12-10 20:26:33 +01:00
|
|
|
};
|
|
|
|
};
|
2025-06-15 13:14:56 +00:00
|
|
|
|
|
|
|
vector_dnstap_sink = {
|
|
|
|
type = "vector";
|
|
|
|
inputs = [ "dnstap" ];
|
|
|
|
address = "clickhouse:6000";
|
|
|
|
};
|
2024-05-21 17:21:57 +01:00
|
|
|
};
|
|
|
|
};
|
2025-06-15 12:51:56 +00:00
|
|
|
};
|
2024-05-21 17:21:57 +01:00
|
|
|
|
2025-06-15 12:51:56 +00:00
|
|
|
systemd.services.vector.serviceConfig = {
|
|
|
|
RuntimeDirectory = "vector";
|
|
|
|
RuntimeDirectoryMode = "0770";
|
|
|
|
};
|
2024-12-10 20:26:33 +01:00
|
|
|
|
2025-06-15 12:51:56 +00:00
|
|
|
services.unbound = {
|
|
|
|
enable = true;
|
|
|
|
enableRootTrustAnchor = false;
|
|
|
|
package = pkgs.unbound-full;
|
|
|
|
settings = {
|
|
|
|
server = {
|
|
|
|
interface = [
|
|
|
|
"0.0.0.0"
|
|
|
|
"::"
|
|
|
|
];
|
|
|
|
access-control = [
|
|
|
|
"192.168.0.0/24 allow"
|
|
|
|
"::/0 allow"
|
|
|
|
];
|
|
|
|
|
|
|
|
domain-insecure = "local";
|
|
|
|
private-domain = "local";
|
|
|
|
|
|
|
|
local-zone = "local. static";
|
|
|
|
local-data = [
|
|
|
|
''"test.local. 10800 IN A 192.168.123.5"''
|
|
|
|
];
|
2024-12-10 20:26:33 +01:00
|
|
|
};
|
2024-05-21 17:21:57 +01:00
|
|
|
|
2025-07-01 11:30:18 +00:00
|
|
|
forward-zone = [
|
|
|
|
{
|
|
|
|
name = "example.com.";
|
|
|
|
forward-addr = [
|
|
|
|
nodes.knot.networking.primaryIPv6Address
|
|
|
|
nodes.knot.networking.primaryIPAddress
|
|
|
|
];
|
|
|
|
}
|
|
|
|
];
|
|
|
|
|
2025-06-15 12:51:56 +00:00
|
|
|
dnstap = {
|
|
|
|
dnstap-enable = "yes";
|
|
|
|
dnstap-socket-path = "${dnstapSocket}";
|
|
|
|
dnstap-send-identity = "yes";
|
|
|
|
dnstap-send-version = "yes";
|
|
|
|
dnstap-log-client-query-messages = "yes";
|
|
|
|
dnstap-log-client-response-messages = "yes";
|
2024-05-21 17:21:57 +01:00
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
2024-12-10 20:26:33 +01:00
|
|
|
|
2025-06-15 12:51:56 +00:00
|
|
|
systemd.services.unbound = {
|
|
|
|
after = [ "vector.service" ];
|
|
|
|
wants = [ "vector.service" ];
|
|
|
|
serviceConfig = {
|
|
|
|
# DNSTAP access
|
|
|
|
ReadWritePaths = [ "/var/run/vector" ];
|
|
|
|
SupplementaryGroups = [ "vector" ];
|
|
|
|
};
|
2024-05-21 17:21:57 +01:00
|
|
|
};
|
2025-06-15 12:51:56 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
dnsclient =
|
|
|
|
{ config, pkgs, ... }:
|
|
|
|
{
|
|
|
|
environment.systemPackages = [ pkgs.dig ];
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2025-06-15 13:14:56 +00:00
|
|
|
testScript =
|
|
|
|
let
|
|
|
|
# work around quote/substitution complexity by Nix, Perl, bash and SQL.
|
|
|
|
databaseDDL = pkgs.writeText "database.sql" "CREATE DATABASE IF NOT EXISTS dnstap";
|
|
|
|
|
|
|
|
tableDDL = pkgs.writeText "table.sql" ''
|
|
|
|
CREATE TABLE IF NOT EXISTS dnstap.records (
|
|
|
|
timestamp DateTime64(6),
|
|
|
|
dataType LowCardinality(String),
|
|
|
|
dataTypeId UInt8,
|
|
|
|
messageType LowCardinality(String),
|
|
|
|
messageTypeId UInt8,
|
|
|
|
requestData Nullable(JSON),
|
|
|
|
responseData Nullable(JSON),
|
|
|
|
responsePort UInt16,
|
|
|
|
serverId LowCardinality(String),
|
|
|
|
serverVersion LowCardinality(String),
|
|
|
|
socketFamily LowCardinality(String),
|
|
|
|
socketProtocol LowCardinality(String),
|
|
|
|
sourceAddress String,
|
|
|
|
sourcePort UInt16,
|
|
|
|
)
|
|
|
|
ENGINE = MergeTree()
|
|
|
|
ORDER BY (serverId, timestamp)
|
|
|
|
PARTITION BY toYYYYMM(timestamp)
|
|
|
|
'';
|
|
|
|
|
|
|
|
tableView = pkgs.writeText "view.sql" ''
|
|
|
|
CREATE MATERIALIZED VIEW dnstap.domains_view (
|
|
|
|
timestamp DateTime64(6),
|
|
|
|
serverId LowCardinality(String),
|
|
|
|
domain String,
|
|
|
|
record_type LowCardinality(String)
|
|
|
|
)
|
|
|
|
ENGINE = MergeTree()
|
|
|
|
PARTITION BY toYYYYMM(timestamp)
|
2025-07-01 11:30:18 +00:00
|
|
|
ORDER BY (serverId, toStartOfHour(timestamp), domain, timestamp)
|
2025-06-15 13:14:56 +00:00
|
|
|
POPULATE AS
|
|
|
|
SELECT
|
|
|
|
timestamp,
|
|
|
|
serverId,
|
|
|
|
JSONExtractString(requestData.question[1]::String, 'domainName') as domain,
|
|
|
|
JSONExtractString(requestData.question[1]::String, 'questionType') as record_type
|
|
|
|
FROM dnstap.records
|
|
|
|
WHERE messageTypeId = 5 # ClientQuery
|
|
|
|
'';
|
|
|
|
|
2025-07-01 11:30:18 +00:00
|
|
|
selectDomainCountQuery = pkgs.writeText "select-domain-count.sql" ''
|
2025-06-15 13:14:56 +00:00
|
|
|
SELECT
|
|
|
|
domain,
|
|
|
|
count(domain)
|
|
|
|
FROM dnstap.domains_view
|
|
|
|
GROUP BY domain
|
|
|
|
'';
|
2025-07-01 11:30:18 +00:00
|
|
|
|
|
|
|
selectAuthResponseQuery = pkgs.writeText "select-auth-response.sql" ''
|
|
|
|
SELECT
|
|
|
|
*
|
|
|
|
FROM dnstap.records
|
|
|
|
WHERE messageType = 'AuthResponse'
|
|
|
|
'';
|
2025-06-15 13:14:56 +00:00
|
|
|
in
|
|
|
|
''
|
|
|
|
clickhouse.wait_for_unit("clickhouse")
|
|
|
|
clickhouse.wait_for_open_port(6000)
|
|
|
|
clickhouse.wait_for_open_port(8123)
|
|
|
|
|
|
|
|
clickhouse.succeed(
|
|
|
|
"cat ${databaseDDL} | clickhouse-client",
|
|
|
|
"cat ${tableDDL} | clickhouse-client",
|
|
|
|
"cat ${tableView} | clickhouse-client",
|
|
|
|
)
|
|
|
|
|
2025-07-01 11:30:18 +00:00
|
|
|
knot.wait_for_unit("knot")
|
2025-06-15 13:14:56 +00:00
|
|
|
unbound.wait_for_unit("unbound")
|
|
|
|
|
2025-07-01 11:30:18 +00:00
|
|
|
for machine in knot, unbound:
|
|
|
|
machine.wait_for_unit("vector")
|
2025-06-15 13:14:56 +00:00
|
|
|
|
2025-07-01 11:30:18 +00:00
|
|
|
machine.wait_until_succeeds(
|
|
|
|
"journalctl -o cat -u vector.service | grep 'Socket permissions updated to 0o770'"
|
|
|
|
)
|
|
|
|
machine.wait_until_succeeds(
|
|
|
|
"journalctl -o cat -u vector.service | grep 'component_type=dnstap' | grep 'Listening... path=\"${dnstapSocket}\"'"
|
|
|
|
)
|
|
|
|
|
|
|
|
machine.wait_for_file("${dnstapSocket}")
|
|
|
|
machine.succeed("test 770 -eq $(stat -c '%a' ${dnstapSocket})")
|
2025-06-15 13:14:56 +00:00
|
|
|
|
|
|
|
dnsclient.systemctl("start network-online.target")
|
|
|
|
dnsclient.wait_for_unit("network-online.target")
|
|
|
|
dnsclient.succeed(
|
2025-07-01 11:30:18 +00:00
|
|
|
"dig @unbound test.local",
|
|
|
|
"dig @unbound www.example.com"
|
2025-06-15 13:14:56 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
unbound.wait_for_file("/var/lib/vector/logs.log")
|
|
|
|
|
|
|
|
unbound.wait_until_succeeds(
|
|
|
|
"grep ClientQuery /var/lib/vector/logs.log | grep '\"domainName\":\"test.local.\"' | grep '\"rcodeName\":\"NoError\"'"
|
|
|
|
)
|
|
|
|
unbound.wait_until_succeeds(
|
|
|
|
"grep ClientResponse /var/lib/vector/logs.log | grep '\"domainName\":\"test.local.\"' | grep '\"rData\":\"192.168.123.5\"'"
|
|
|
|
)
|
|
|
|
|
|
|
|
clickhouse.log(clickhouse.wait_until_succeeds(
|
2025-07-01 11:30:18 +00:00
|
|
|
"cat ${selectDomainCountQuery} | clickhouse-client | grep 'test.local.'"
|
|
|
|
))
|
|
|
|
|
|
|
|
clickhouse.log(clickhouse.wait_until_succeeds(
|
|
|
|
"cat ${selectDomainCountQuery} | clickhouse-client | grep 'www.example.com.'"
|
|
|
|
))
|
|
|
|
|
|
|
|
clickhouse.log(clickhouse.wait_until_succeeds(
|
|
|
|
"cat ${selectAuthResponseQuery} | clickhouse-client | grep 'Knot DNS ${pkgs.knot-dns.version}'"
|
2025-06-15 13:14:56 +00:00
|
|
|
))
|
|
|
|
'';
|
2025-06-15 12:51:56 +00:00
|
|
|
}
|