at master 11 kB view raw
1{ lib, pkgs, ... }: 2 3let 4 dnstapSocket = "/var/run/vector/dnstap.sock"; 5in 6{ 7 name = "vector-dnstap"; 8 meta.maintainers = [ pkgs.lib.maintainers.happysalada ]; 9 10 nodes = { 11 clickhouse = 12 { config, pkgs, ... }: 13 { 14 networking.firewall.allowedTCPPorts = [ 6000 ]; 15 16 services.vector = { 17 enable = true; 18 19 settings = { 20 sources = { 21 vector_dnstap_source = { 22 type = "vector"; 23 address = "[::]:6000"; 24 }; 25 }; 26 27 sinks = { 28 clickhouse = { 29 type = "clickhouse"; 30 inputs = [ 31 "vector_dnstap_source" 32 ]; 33 endpoint = "http://localhost:8123"; 34 database = "dnstap"; 35 table = "records"; 36 date_time_best_effort = true; 37 }; 38 }; 39 }; 40 }; 41 42 services.clickhouse.enable = true; 43 }; 44 45 knot = 46 { 47 config, 48 nodes, 49 pkgs, 50 ... 51 }: 52 let 53 exampleZone = pkgs.writeTextDir "example.com.zone" '' 54 @ SOA ns.example.com. noc.example.com. 2019031301 86400 7200 3600000 172800 55 @ NS ns1 56 @ NS ns2 57 ns1 A 192.168.0.1 58 ns1 AAAA fd00::1 59 ns2 A 192.168.0.2 60 ns2 AAAA fd00::2 61 www A 192.0.2.1 62 www AAAA 2001:DB8::1 63 sub NS ns.example.com. 64 ''; 65 66 knotZonesEnv = pkgs.buildEnv { 67 name = "knot-zones"; 68 paths = [ 69 exampleZone 70 ]; 71 }; 72 in 73 { 74 networking.firewall.allowedUDPPorts = [ 53 ]; 75 76 services.vector = { 77 enable = true; 78 79 settings = { 80 sources = { 81 dnstap = { 82 type = "dnstap"; 83 multithreaded = true; 84 mode = "unix"; 85 lowercase_hostnames = true; 86 socket_file_mode = 504; 87 socket_path = "${dnstapSocket}"; 88 }; 89 }; 90 91 sinks = { 92 vector_dnstap_sink = { 93 type = "vector"; 94 inputs = [ "dnstap" ]; 95 address = "clickhouse:6000"; 96 }; 97 }; 98 }; 99 }; 100 101 systemd.services.vector.serviceConfig = { 102 RuntimeDirectory = "vector"; 103 RuntimeDirectoryMode = "0770"; 104 }; 105 106 services.knot = { 107 enable = true; 108 settings = { 109 server = { 110 listen = [ 111 "0.0.0.0@53" 112 "::@53" 113 ]; 114 automatic-acl = true; 115 }; 116 template.default = { 117 storage = knotZonesEnv; 118 dnssec-signing = false; 119 # Input-only zone files 120 # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-3 121 # prevents modification of the zonefiles, since the zonefiles are immutable 122 zonefile-sync = -1; 123 zonefile-load = "difference"; 124 journal-content = "changes"; 125 global-module = "mod-dnstap/capture_all"; 126 }; 127 zone = { 128 "example.com".file = "example.com.zone"; 129 }; 130 131 mod-dnstap = [ 132 { 133 id = "capture_all"; 134 sink = "unix:${dnstapSocket}"; 135 } 136 ]; 137 }; 138 }; 139 140 systemd.services.knot = { 141 after = [ "vector.service" ]; 142 wants = [ "vector.service" ]; 143 serviceConfig = { 144 # DNSTAP access 145 ReadWritePaths = [ "/var/run/vector" ]; 146 SupplementaryGroups = [ "vector" ]; 147 }; 148 }; 149 }; 150 151 unbound = 152 { 153 config, 154 nodes, 155 pkgs, 156 ... 157 }: 158 { 159 networking.firewall.allowedUDPPorts = [ 53 ]; 160 161 services.vector = { 162 enable = true; 163 164 settings = { 165 sources = { 166 dnstap = { 167 type = "dnstap"; 168 multithreaded = true; 169 mode = "unix"; 170 lowercase_hostnames = true; 171 socket_file_mode = 504; 172 socket_path = "${dnstapSocket}"; 173 }; 174 }; 175 176 sinks = { 177 file = { 178 type = "file"; 179 inputs = [ "dnstap" ]; 180 path = "/var/lib/vector/logs.log"; 181 encoding = { 182 codec = "json"; 183 }; 184 }; 185 186 vector_dnstap_sink = { 187 type = "vector"; 188 inputs = [ "dnstap" ]; 189 address = "clickhouse:6000"; 190 }; 191 }; 192 }; 193 }; 194 195 systemd.services.vector.serviceConfig = { 196 RuntimeDirectory = "vector"; 197 RuntimeDirectoryMode = "0770"; 198 }; 199 200 services.unbound = { 201 enable = true; 202 enableRootTrustAnchor = false; 203 package = pkgs.unbound-full; 204 settings = { 205 server = { 206 interface = [ 207 "0.0.0.0" 208 "::" 209 ]; 210 access-control = [ 211 "192.168.0.0/24 allow" 212 "::/0 allow" 213 ]; 214 215 domain-insecure = "local"; 216 private-domain = "local"; 217 218 local-zone = "local. static"; 219 local-data = [ 220 ''"test.local. 10800 IN A 192.168.123.5"'' 221 ]; 222 }; 223 224 forward-zone = [ 225 { 226 name = "example.com."; 227 forward-addr = [ 228 nodes.knot.networking.primaryIPv6Address 229 nodes.knot.networking.primaryIPAddress 230 ]; 231 } 232 ]; 233 234 dnstap = { 235 dnstap-enable = "yes"; 236 dnstap-socket-path = "${dnstapSocket}"; 237 dnstap-send-identity = "yes"; 238 dnstap-send-version = "yes"; 239 dnstap-log-client-query-messages = "yes"; 240 dnstap-log-client-response-messages = "yes"; 241 }; 242 }; 243 }; 244 245 systemd.services.unbound = { 246 after = [ "vector.service" ]; 247 wants = [ "vector.service" ]; 248 serviceConfig = { 249 # DNSTAP access 250 ReadWritePaths = [ "/var/run/vector" ]; 251 SupplementaryGroups = [ "vector" ]; 252 }; 253 }; 254 }; 255 256 dnsclient = 257 { config, pkgs, ... }: 258 { 259 environment.systemPackages = [ pkgs.dig ]; 260 }; 261 }; 262 263 testScript = 264 let 265 # work around quote/substitution complexity by Nix, Perl, bash and SQL. 266 databaseDDL = pkgs.writeText "database.sql" "CREATE DATABASE IF NOT EXISTS dnstap"; 267 268 tableDDL = pkgs.writeText "table.sql" '' 269 CREATE TABLE IF NOT EXISTS dnstap.records ( 270 timestamp DateTime64(6), 271 dataType Enum('Message' = 1), 272 messageType Enum( 273 'AuthQuery' = 1, 274 'AuthResponse' = 2, 275 'ResolverQuery' = 3, 276 'ResolverResponse' = 4, 277 'ClientQuery' = 5, 278 'ClientResponse' = 6, 279 'ForwarderQuery' = 7, 280 'ForwarderResponse' = 8, 281 'StubQuery' = 9, 282 'StubResponse' = 10, 283 'ToolQuery' = 11, 284 'ToolResponse' = 12, 285 'UpdateQuery' = 13, 286 'UpdateResponse' = 14 287 ), 288 queryZone Nullable(String), 289 requestData Nullable(JSON), 290 responseAddress String, 291 responseData Nullable(JSON), 292 responsePort UInt16, 293 serverId LowCardinality(String), 294 serverVersion LowCardinality(String), 295 socketFamily Enum('INET' = 1, 'INET6' = 2), 296 socketProtocol Enum( 297 'UDP' = 1, 298 'TCP' = 2, 299 'DOT' = 3, 300 'DOH' = 4, 301 'DNSCryptUDP' = 5, 302 'DNSCryptTCP' = 6, 303 'DOQ' = 7 304 ), 305 sourceAddress String, 306 sourcePort UInt16, 307 ) 308 ENGINE = MergeTree() 309 ORDER BY (serverId, timestamp) 310 PARTITION BY toYYYYMM(timestamp) 311 ''; 312 313 tableView = pkgs.writeText "view.sql" '' 314 CREATE MATERIALIZED VIEW dnstap.domains_view ( 315 timestamp DateTime64(6), 316 serverId LowCardinality(String), 317 domain String, 318 record_type LowCardinality(String) 319 ) 320 ENGINE = MergeTree() 321 PARTITION BY toYYYYMM(timestamp) 322 ORDER BY (serverId, toStartOfHour(timestamp), domain, timestamp) 323 POPULATE AS 324 SELECT 325 timestamp, 326 serverId, 327 JSONExtractString(requestData.question[1]::String, 'domainName') as domain, 328 JSONExtractString(requestData.question[1]::String, 'questionType') as record_type 329 FROM dnstap.records 330 WHERE messageType = 'ClientQuery' 331 ''; 332 333 selectDomainCountQuery = pkgs.writeText "select-domain-count.sql" '' 334 SELECT 335 domain, 336 count(domain) 337 FROM dnstap.domains_view 338 GROUP BY domain 339 ''; 340 341 selectAuthResponseQuery = pkgs.writeText "select-auth-response.sql" '' 342 SELECT 343 * 344 FROM dnstap.records 345 WHERE messageType = 'AuthResponse' 346 ''; 347 in 348 '' 349 clickhouse.wait_for_unit("clickhouse") 350 clickhouse.wait_for_open_port(6000) 351 clickhouse.wait_for_open_port(8123) 352 353 clickhouse.succeed( 354 "cat ${databaseDDL} | clickhouse-client", 355 "cat ${tableDDL} | clickhouse-client", 356 "cat ${tableView} | clickhouse-client", 357 ) 358 359 knot.wait_for_unit("knot") 360 unbound.wait_for_unit("unbound") 361 362 for machine in knot, unbound: 363 machine.wait_for_unit("vector") 364 365 machine.wait_until_succeeds( 366 "journalctl -o cat -u vector.service | grep 'Socket permissions updated to 0o770'" 367 ) 368 machine.wait_until_succeeds( 369 "journalctl -o cat -u vector.service | grep 'component_type=dnstap' | grep 'Listening... path=\"${dnstapSocket}\"'" 370 ) 371 372 machine.wait_for_open_unix_socket("${dnstapSocket}") 373 374 dnsclient.systemctl("start network-online.target") 375 dnsclient.wait_for_unit("network-online.target") 376 dnsclient.succeed( 377 "dig @unbound test.local", 378 "dig @unbound www.example.com" 379 ) 380 381 unbound.wait_for_file("/var/lib/vector/logs.log") 382 383 unbound.wait_until_succeeds( 384 "grep ClientQuery /var/lib/vector/logs.log | grep '\"domainName\":\"test.local.\"' | grep '\"rcodeName\":\"NoError\"'" 385 ) 386 unbound.wait_until_succeeds( 387 "grep ClientResponse /var/lib/vector/logs.log | grep '\"domainName\":\"test.local.\"' | grep '\"rData\":\"192.168.123.5\"'" 388 ) 389 390 clickhouse.log(clickhouse.wait_until_succeeds( 391 "cat ${selectDomainCountQuery} | clickhouse-client | grep 'test.local.'" 392 )) 393 394 clickhouse.log(clickhouse.wait_until_succeeds( 395 "cat ${selectDomainCountQuery} | clickhouse-client | grep 'www.example.com.'" 396 )) 397 398 clickhouse.log(clickhouse.wait_until_succeeds( 399 "cat ${selectAuthResponseQuery} | clickhouse-client | grep 'Knot DNS ${pkgs.knot-dns.version}'" 400 )) 401 ''; 402}