├── .github └── workflows │ └── actions.yml ├── .gitignore ├── .vscode └── launch.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── bin ├── Cargo.toml └── src │ ├── args.rs │ ├── config.rs │ ├── gen.rs │ ├── logs.rs │ ├── main.rs │ ├── msg.rs │ ├── query.rs │ ├── sender.rs │ ├── shutdown.rs │ ├── stats.rs │ ├── store.rs │ └── util.rs ├── rust-toolchain └── rustfmt.toml /.github/workflows/actions.yml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/actions-rs/meta/blob/master/recipes/msrv.md 2 | 3 | on: [push, pull_request] 4 | 5 | name: Actions 6 | 7 | jobs: 8 | check: 9 | name: Check 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | rust: 14 | - stable 15 | - beta 16 | - nightly 17 | steps: 18 | - name: Checkout sources 19 | uses: actions/checkout@v2 20 | 21 | - name: Install toolchain 22 | uses: actions-rs/toolchain@v1 23 | with: 24 | toolchain: ${{ matrix.rust }} 25 | override: true 26 | 27 | - name: Run cargo check 28 | uses: actions-rs/cargo@v1 29 | with: 30 | command: check 31 | 32 | test: 33 | name: Test Suite 34 | runs-on: ubuntu-latest 35 | strategy: 36 | matrix: 37 | rust: 38 | - stable 39 | - beta 40 | - nightly 41 | steps: 42 | - name: Checkout sources 43 | uses: actions/checkout@v2 44 | 45 | - name: Install toolchain 46 | uses: actions-rs/toolchain@v1 47 | with: 48 | toolchain: ${{ matrix.rust }} 49 | override: true 50 | 51 | - name: Run cargo test 52 | uses: actions-rs/cargo@v1 53 | with: 54 | command: test 55 | 56 | fmt: 57 | name: Rustfmt 58 | runs-on: ubuntu-latest 59 | strategy: 60 | matrix: 61 | rust: 62 | - stable 63 | - beta 64 | steps: 65 | - name: Checkout sources 66 | uses: actions/checkout@v2 67 | 68 | - name: Install toolchain 69 | uses: actions-rs/toolchain@v1 70 | with: 71 | toolchain: ${{ matrix.rust }} 72 | override: true 73 | 74 | - name: Install rustfmt 75 | run: rustup component add rustfmt 76 | 77 | - name: Run cargo fmt 78 | uses: actions-rs/cargo@v1 79 | with: 80 | command: fmt 81 | args: --all -- --check 82 | 83 | clippy: 84 | name: Clippy 85 | runs-on: ubuntu-latest 86 | strategy: 87 | matrix: 88 | rust: 89 | - stable 90 | - beta 91 | - nightly 92 | steps: 93 | - name: Checkout sources 94 | uses: actions/checkout@v2 95 | 96 | - name: Install toolchain 97 | uses: actions-rs/toolchain@v1 98 | with: 99 | toolchain: ${{ matrix.rust }} 100 | override: true 101 | 102 | - name: Install clippy 103 | run: rustup component add clippy 104 | 105 | - name: Run cargo clippy 106 | uses: actions-rs/cargo@v1 107 | with: 108 | command: clippy 109 | args: -- -D warnings -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | file_test 3 | log -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'nailgun'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=nailgun", 15 | "--package=nailgun" 16 | ], 17 | "filter": { 18 | "name": "nailgun", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": [], 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | { 26 | "type": "lldb", 27 | "request": "launch", 28 | "name": "Debug unit tests in executable 'nailgun'", 29 | "cargo": { 30 | "args": [ 31 | "test", 32 | "--no-run", 33 | "--bin=nailgun", 34 | "--package=nailgun" 35 | ], 36 | "filter": { 37 | "name": "nailgun", 38 | "kind": "bin" 39 | } 40 | }, 41 | "args": [], 42 | "cwd": "${workspaceFolder}" 43 | }, 44 | { 45 | "type": "lldb", 46 | "request": "launch", 47 | "name": "Debug unit tests in library 'tokenbucket'", 48 | "cargo": { 49 | "args": [ 50 | "test", 51 | "--no-run", 52 | "--lib", 53 | "--package=tokenbucket" 54 | ], 55 | "filter": { 56 | "name": "tokenbucket", 57 | "kind": "lib" 58 | } 59 | }, 60 | "args": [], 61 | "cwd": "${workspaceFolder}" 62 | } 63 | ] 64 | } -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ansi_term" 7 | version = "0.12.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 10 | dependencies = [ 11 | "winapi", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.43" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf" 19 | 20 | [[package]] 21 | name = "async-trait" 22 | version = "0.1.51" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" 25 | dependencies = [ 26 | "proc-macro2", 27 | "quote", 28 | "syn", 29 | ] 30 | 31 | [[package]] 32 | name = "atty" 33 | version = "0.2.14" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 36 | dependencies = [ 37 | "hermit-abi", 38 | "libc", 39 | "winapi", 40 | ] 41 | 42 | [[package]] 43 | name = "autocfg" 44 | version = "1.1.0" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 47 | 48 | [[package]] 49 | name = "base64" 50 | version = "0.13.0" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 53 | 54 | [[package]] 55 | name = "bitflags" 56 | version = "1.2.1" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 59 | 60 | [[package]] 61 | name = "bumpalo" 62 | version = "3.7.0" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" 65 | 66 | [[package]] 67 | name = "bytes" 68 | version = "1.1.0" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" 71 | 72 | [[package]] 73 | name = "cc" 74 | version = "1.0.70" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" 77 | 78 | [[package]] 79 | name = "cfg-if" 80 | version = "1.0.0" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 83 | 84 | [[package]] 85 | name = "clap" 86 | version = "3.1.6" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "d8c93436c21e4698bacadf42917db28b23017027a4deccb35dbe47a7e7840123" 89 | dependencies = [ 90 | "atty", 91 | "bitflags", 92 | "clap_derive", 93 | "indexmap", 94 | "lazy_static", 95 | "os_str_bytes", 96 | "strsim", 97 | "termcolor", 98 | "textwrap", 99 | ] 100 | 101 | [[package]] 102 | name = "clap_derive" 103 | version = "3.1.7" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "a3aab4734e083b809aaf5794e14e756d1c798d2c69c7f7de7a09a2f5214993c1" 106 | dependencies = [ 107 | "heck", 108 | "proc-macro-error", 109 | "proc-macro2", 110 | "quote", 111 | "syn", 112 | ] 113 | 114 | [[package]] 115 | name = "crossbeam-channel" 116 | version = "0.5.1" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" 119 | dependencies = [ 120 | "cfg-if", 121 | "crossbeam-utils", 122 | ] 123 | 124 | [[package]] 125 | name = "crossbeam-utils" 126 | version = "0.8.8" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" 129 | dependencies = [ 130 | "cfg-if", 131 | "lazy_static", 132 | ] 133 | 134 | [[package]] 135 | name = "dashmap" 136 | version = "5.2.0" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "4c8858831f7781322e539ea39e72449c46b059638250c14344fec8d0aa6e539c" 139 | dependencies = [ 140 | "cfg-if", 141 | "num_cpus", 142 | "parking_lot", 143 | ] 144 | 145 | [[package]] 146 | name = "data-encoding" 147 | version = "2.3.2" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" 150 | 151 | [[package]] 152 | name = "enum-as-inner" 153 | version = "0.4.0" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73" 156 | dependencies = [ 157 | "heck", 158 | "proc-macro2", 159 | "quote", 160 | "syn", 161 | ] 162 | 163 | [[package]] 164 | name = "fnv" 165 | version = "1.0.7" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 168 | 169 | [[package]] 170 | name = "form_urlencoded" 171 | version = "1.0.1" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 174 | dependencies = [ 175 | "matches", 176 | "percent-encoding", 177 | ] 178 | 179 | [[package]] 180 | name = "futures" 181 | version = "0.3.15" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27" 184 | dependencies = [ 185 | "futures-channel", 186 | "futures-core", 187 | "futures-executor", 188 | "futures-io", 189 | "futures-sink", 190 | "futures-task", 191 | "futures-util", 192 | ] 193 | 194 | [[package]] 195 | name = "futures-channel" 196 | version = "0.3.15" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2" 199 | dependencies = [ 200 | "futures-core", 201 | "futures-sink", 202 | ] 203 | 204 | [[package]] 205 | name = "futures-core" 206 | version = "0.3.15" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1" 209 | 210 | [[package]] 211 | name = "futures-executor" 212 | version = "0.3.15" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79" 215 | dependencies = [ 216 | "futures-core", 217 | "futures-task", 218 | "futures-util", 219 | ] 220 | 221 | [[package]] 222 | name = "futures-io" 223 | version = "0.3.15" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1" 226 | 227 | [[package]] 228 | name = "futures-macro" 229 | version = "0.3.15" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121" 232 | dependencies = [ 233 | "autocfg", 234 | "proc-macro-hack", 235 | "proc-macro2", 236 | "quote", 237 | "syn", 238 | ] 239 | 240 | [[package]] 241 | name = "futures-sink" 242 | version = "0.3.15" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282" 245 | 246 | [[package]] 247 | name = "futures-task" 248 | version = "0.3.15" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae" 251 | 252 | [[package]] 253 | name = "futures-timer" 254 | version = "3.0.2" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" 257 | 258 | [[package]] 259 | name = "futures-util" 260 | version = "0.3.15" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967" 263 | dependencies = [ 264 | "autocfg", 265 | "futures-channel", 266 | "futures-core", 267 | "futures-io", 268 | "futures-macro", 269 | "futures-sink", 270 | "futures-task", 271 | "memchr", 272 | "pin-project-lite", 273 | "pin-utils", 274 | "proc-macro-hack", 275 | "proc-macro-nested", 276 | "slab", 277 | ] 278 | 279 | [[package]] 280 | name = "getrandom" 281 | version = "0.2.3" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" 284 | dependencies = [ 285 | "cfg-if", 286 | "libc", 287 | "wasi 0.10.2+wasi-snapshot-preview1", 288 | ] 289 | 290 | [[package]] 291 | name = "governor" 292 | version = "0.4.2" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "19775995ee20209163239355bc3ad2f33f83da35d9ef72dea26e5af753552c87" 295 | dependencies = [ 296 | "dashmap", 297 | "futures", 298 | "futures-timer", 299 | "no-std-compat", 300 | "nonzero_ext", 301 | "parking_lot", 302 | "quanta", 303 | "rand", 304 | "smallvec", 305 | ] 306 | 307 | [[package]] 308 | name = "h2" 309 | version = "0.3.4" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "d7f3675cfef6a30c8031cf9e6493ebdc3bb3272a3fea3923c4210d1830e6a472" 312 | dependencies = [ 313 | "bytes", 314 | "fnv", 315 | "futures-core", 316 | "futures-sink", 317 | "futures-util", 318 | "http", 319 | "indexmap", 320 | "slab", 321 | "tokio", 322 | "tokio-util 0.6.8", 323 | "tracing", 324 | ] 325 | 326 | [[package]] 327 | name = "hashbrown" 328 | version = "0.9.1" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" 331 | 332 | [[package]] 333 | name = "heck" 334 | version = "0.4.0" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 337 | 338 | [[package]] 339 | name = "hermit-abi" 340 | version = "0.1.18" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" 343 | dependencies = [ 344 | "libc", 345 | ] 346 | 347 | [[package]] 348 | name = "hostname" 349 | version = "0.3.1" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" 352 | dependencies = [ 353 | "libc", 354 | "match_cfg", 355 | "winapi", 356 | ] 357 | 358 | [[package]] 359 | name = "http" 360 | version = "0.2.4" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" 363 | dependencies = [ 364 | "bytes", 365 | "fnv", 366 | "itoa 0.4.7", 367 | ] 368 | 369 | [[package]] 370 | name = "idna" 371 | version = "0.2.3" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 374 | dependencies = [ 375 | "matches", 376 | "unicode-bidi", 377 | "unicode-normalization", 378 | ] 379 | 380 | [[package]] 381 | name = "indexmap" 382 | version = "1.6.2" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" 385 | dependencies = [ 386 | "autocfg", 387 | "hashbrown", 388 | ] 389 | 390 | [[package]] 391 | name = "ipconfig" 392 | version = "0.3.0" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "723519edce41262b05d4143ceb95050e4c614f483e78e9fd9e39a8275a84ad98" 395 | dependencies = [ 396 | "socket2", 397 | "widestring", 398 | "winapi", 399 | "winreg", 400 | ] 401 | 402 | [[package]] 403 | name = "ipnet" 404 | version = "2.3.0" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" 407 | 408 | [[package]] 409 | name = "itoa" 410 | version = "0.4.7" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" 413 | 414 | [[package]] 415 | name = "itoa" 416 | version = "1.0.1" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" 419 | 420 | [[package]] 421 | name = "js-sys" 422 | version = "0.3.53" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "e4bf49d50e2961077d9c99f4b7997d770a1114f087c3c2e0069b36c13fc2979d" 425 | dependencies = [ 426 | "wasm-bindgen", 427 | ] 428 | 429 | [[package]] 430 | name = "lazy_static" 431 | version = "1.4.0" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 434 | 435 | [[package]] 436 | name = "libc" 437 | version = "0.2.124" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "21a41fed9d98f27ab1c6d161da622a4fa35e8a54a8adc24bbf3ddd0ef70b0e50" 440 | 441 | [[package]] 442 | name = "linked-hash-map" 443 | version = "0.5.4" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" 446 | 447 | [[package]] 448 | name = "lock_api" 449 | version = "0.4.7" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" 452 | dependencies = [ 453 | "autocfg", 454 | "scopeguard", 455 | ] 456 | 457 | [[package]] 458 | name = "log" 459 | version = "0.4.14" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 462 | dependencies = [ 463 | "cfg-if", 464 | ] 465 | 466 | [[package]] 467 | name = "lru-cache" 468 | version = "0.1.2" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" 471 | dependencies = [ 472 | "linked-hash-map", 473 | ] 474 | 475 | [[package]] 476 | name = "mach" 477 | version = "0.3.2" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" 480 | dependencies = [ 481 | "libc", 482 | ] 483 | 484 | [[package]] 485 | name = "match_cfg" 486 | version = "0.1.0" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" 489 | 490 | [[package]] 491 | name = "matchers" 492 | version = "0.1.0" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 495 | dependencies = [ 496 | "regex-automata", 497 | ] 498 | 499 | [[package]] 500 | name = "matches" 501 | version = "0.1.8" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 504 | 505 | [[package]] 506 | name = "memchr" 507 | version = "2.4.0" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" 510 | 511 | [[package]] 512 | name = "mio" 513 | version = "0.8.2" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" 516 | dependencies = [ 517 | "libc", 518 | "log", 519 | "miow", 520 | "ntapi", 521 | "wasi 0.11.0+wasi-snapshot-preview1", 522 | "winapi", 523 | ] 524 | 525 | [[package]] 526 | name = "miow" 527 | version = "0.3.7" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" 530 | dependencies = [ 531 | "winapi", 532 | ] 533 | 534 | [[package]] 535 | name = "naildns" 536 | version = "0.2.0" 537 | dependencies = [ 538 | "anyhow", 539 | "async-trait", 540 | "bytes", 541 | "clap", 542 | "governor", 543 | "num-traits", 544 | "num_cpus", 545 | "parking_lot", 546 | "rand", 547 | "rustc-hash", 548 | "rustls", 549 | "tokio", 550 | "tokio-stream", 551 | "tokio-util 0.7.0", 552 | "tracing", 553 | "tracing-appender", 554 | "tracing-futures", 555 | "tracing-subscriber", 556 | "trust-dns-proto 0.21.2 (git+https://github.com/leshow/trust-dns?branch=mine_doh)", 557 | "trust-dns-resolver", 558 | "webpki-roots", 559 | ] 560 | 561 | [[package]] 562 | name = "no-std-compat" 563 | version = "0.4.1" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" 566 | 567 | [[package]] 568 | name = "nonzero_ext" 569 | version = "0.3.0" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" 572 | 573 | [[package]] 574 | name = "ntapi" 575 | version = "0.3.6" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" 578 | dependencies = [ 579 | "winapi", 580 | ] 581 | 582 | [[package]] 583 | name = "num-traits" 584 | version = "0.2.14" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 587 | dependencies = [ 588 | "autocfg", 589 | ] 590 | 591 | [[package]] 592 | name = "num_cpus" 593 | version = "1.13.1" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" 596 | dependencies = [ 597 | "hermit-abi", 598 | "libc", 599 | ] 600 | 601 | [[package]] 602 | name = "once_cell" 603 | version = "1.7.2" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" 606 | 607 | [[package]] 608 | name = "os_str_bytes" 609 | version = "6.0.0" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" 612 | dependencies = [ 613 | "memchr", 614 | ] 615 | 616 | [[package]] 617 | name = "parking_lot" 618 | version = "0.12.0" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" 621 | dependencies = [ 622 | "lock_api", 623 | "parking_lot_core", 624 | ] 625 | 626 | [[package]] 627 | name = "parking_lot_core" 628 | version = "0.9.2" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37" 631 | dependencies = [ 632 | "cfg-if", 633 | "libc", 634 | "redox_syscall", 635 | "smallvec", 636 | "windows-sys", 637 | ] 638 | 639 | [[package]] 640 | name = "percent-encoding" 641 | version = "2.1.0" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 644 | 645 | [[package]] 646 | name = "pin-project" 647 | version = "1.0.7" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "c7509cc106041c40a4518d2af7a61530e1eed0e6285296a3d8c5472806ccc4a4" 650 | dependencies = [ 651 | "pin-project-internal", 652 | ] 653 | 654 | [[package]] 655 | name = "pin-project-internal" 656 | version = "1.0.7" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "48c950132583b500556b1efd71d45b319029f2b71518d979fcc208e16b42426f" 659 | dependencies = [ 660 | "proc-macro2", 661 | "quote", 662 | "syn", 663 | ] 664 | 665 | [[package]] 666 | name = "pin-project-lite" 667 | version = "0.2.6" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" 670 | 671 | [[package]] 672 | name = "pin-utils" 673 | version = "0.1.0" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 676 | 677 | [[package]] 678 | name = "ppv-lite86" 679 | version = "0.2.10" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 682 | 683 | [[package]] 684 | name = "proc-macro-error" 685 | version = "1.0.4" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 688 | dependencies = [ 689 | "proc-macro-error-attr", 690 | "proc-macro2", 691 | "quote", 692 | "syn", 693 | "version_check", 694 | ] 695 | 696 | [[package]] 697 | name = "proc-macro-error-attr" 698 | version = "1.0.4" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 701 | dependencies = [ 702 | "proc-macro2", 703 | "quote", 704 | "version_check", 705 | ] 706 | 707 | [[package]] 708 | name = "proc-macro-hack" 709 | version = "0.5.19" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" 712 | 713 | [[package]] 714 | name = "proc-macro-nested" 715 | version = "0.1.7" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" 718 | 719 | [[package]] 720 | name = "proc-macro2" 721 | version = "1.0.29" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" 724 | dependencies = [ 725 | "unicode-xid", 726 | ] 727 | 728 | [[package]] 729 | name = "quanta" 730 | version = "0.9.3" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "20afe714292d5e879d8b12740aa223c6a88f118af41870e8b6196e39a02238a8" 733 | dependencies = [ 734 | "crossbeam-utils", 735 | "libc", 736 | "mach", 737 | "once_cell", 738 | "raw-cpuid", 739 | "wasi 0.10.2+wasi-snapshot-preview1", 740 | "web-sys", 741 | "winapi", 742 | ] 743 | 744 | [[package]] 745 | name = "quick-error" 746 | version = "1.2.3" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 749 | 750 | [[package]] 751 | name = "quote" 752 | version = "1.0.9" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 755 | dependencies = [ 756 | "proc-macro2", 757 | ] 758 | 759 | [[package]] 760 | name = "rand" 761 | version = "0.8.4" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" 764 | dependencies = [ 765 | "libc", 766 | "rand_chacha", 767 | "rand_core", 768 | "rand_hc", 769 | ] 770 | 771 | [[package]] 772 | name = "rand_chacha" 773 | version = "0.3.0" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" 776 | dependencies = [ 777 | "ppv-lite86", 778 | "rand_core", 779 | ] 780 | 781 | [[package]] 782 | name = "rand_core" 783 | version = "0.6.2" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" 786 | dependencies = [ 787 | "getrandom", 788 | ] 789 | 790 | [[package]] 791 | name = "rand_hc" 792 | version = "0.3.0" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" 795 | dependencies = [ 796 | "rand_core", 797 | ] 798 | 799 | [[package]] 800 | name = "raw-cpuid" 801 | version = "10.3.0" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "738bc47119e3eeccc7e94c4a506901aea5e7b4944ecd0829cbebf4af04ceda12" 804 | dependencies = [ 805 | "bitflags", 806 | ] 807 | 808 | [[package]] 809 | name = "redox_syscall" 810 | version = "0.2.8" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" 813 | dependencies = [ 814 | "bitflags", 815 | ] 816 | 817 | [[package]] 818 | name = "regex" 819 | version = "1.5.5" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" 822 | dependencies = [ 823 | "regex-syntax", 824 | ] 825 | 826 | [[package]] 827 | name = "regex-automata" 828 | version = "0.1.10" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 831 | dependencies = [ 832 | "regex-syntax", 833 | ] 834 | 835 | [[package]] 836 | name = "regex-syntax" 837 | version = "0.6.25" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 840 | 841 | [[package]] 842 | name = "resolv-conf" 843 | version = "0.7.0" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" 846 | dependencies = [ 847 | "hostname", 848 | "quick-error", 849 | ] 850 | 851 | [[package]] 852 | name = "ring" 853 | version = "0.16.20" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" 856 | dependencies = [ 857 | "cc", 858 | "libc", 859 | "once_cell", 860 | "spin", 861 | "untrusted", 862 | "web-sys", 863 | "winapi", 864 | ] 865 | 866 | [[package]] 867 | name = "rustc-hash" 868 | version = "1.1.0" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 871 | 872 | [[package]] 873 | name = "rustls" 874 | version = "0.20.4" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "4fbfeb8d0ddb84706bc597a5574ab8912817c52a397f819e5b614e2265206921" 877 | dependencies = [ 878 | "log", 879 | "ring", 880 | "sct", 881 | "webpki", 882 | ] 883 | 884 | [[package]] 885 | name = "rustls-pemfile" 886 | version = "1.0.0" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9" 889 | dependencies = [ 890 | "base64", 891 | ] 892 | 893 | [[package]] 894 | name = "ryu" 895 | version = "1.0.9" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" 898 | 899 | [[package]] 900 | name = "scopeguard" 901 | version = "1.1.0" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 904 | 905 | [[package]] 906 | name = "sct" 907 | version = "0.7.0" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" 910 | dependencies = [ 911 | "ring", 912 | "untrusted", 913 | ] 914 | 915 | [[package]] 916 | name = "serde" 917 | version = "1.0.126" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" 920 | 921 | [[package]] 922 | name = "serde_json" 923 | version = "1.0.75" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "c059c05b48c5c0067d4b4b2b4f0732dd65feb52daf7e0ea09cd87e7dadc1af79" 926 | dependencies = [ 927 | "itoa 1.0.1", 928 | "ryu", 929 | "serde", 930 | ] 931 | 932 | [[package]] 933 | name = "sharded-slab" 934 | version = "0.1.1" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "79c719719ee05df97490f80a45acfc99e5a30ce98a1e4fb67aee422745ae14e3" 937 | dependencies = [ 938 | "lazy_static", 939 | ] 940 | 941 | [[package]] 942 | name = "signal-hook-registry" 943 | version = "1.4.0" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 946 | dependencies = [ 947 | "libc", 948 | ] 949 | 950 | [[package]] 951 | name = "slab" 952 | version = "0.4.5" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" 955 | 956 | [[package]] 957 | name = "smallvec" 958 | version = "1.6.1" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" 961 | 962 | [[package]] 963 | name = "socket2" 964 | version = "0.4.4" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" 967 | dependencies = [ 968 | "libc", 969 | "winapi", 970 | ] 971 | 972 | [[package]] 973 | name = "spin" 974 | version = "0.5.2" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" 977 | 978 | [[package]] 979 | name = "strsim" 980 | version = "0.10.0" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 983 | 984 | [[package]] 985 | name = "syn" 986 | version = "1.0.76" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84" 989 | dependencies = [ 990 | "proc-macro2", 991 | "quote", 992 | "unicode-xid", 993 | ] 994 | 995 | [[package]] 996 | name = "termcolor" 997 | version = "1.1.2" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 1000 | dependencies = [ 1001 | "winapi-util", 1002 | ] 1003 | 1004 | [[package]] 1005 | name = "textwrap" 1006 | version = "0.15.0" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" 1009 | 1010 | [[package]] 1011 | name = "thiserror" 1012 | version = "1.0.29" 1013 | source = "registry+https://github.com/rust-lang/crates.io-index" 1014 | checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88" 1015 | dependencies = [ 1016 | "thiserror-impl", 1017 | ] 1018 | 1019 | [[package]] 1020 | name = "thiserror-impl" 1021 | version = "1.0.29" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c" 1024 | dependencies = [ 1025 | "proc-macro2", 1026 | "quote", 1027 | "syn", 1028 | ] 1029 | 1030 | [[package]] 1031 | name = "thread_local" 1032 | version = "1.1.4" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" 1035 | dependencies = [ 1036 | "once_cell", 1037 | ] 1038 | 1039 | [[package]] 1040 | name = "time" 1041 | version = "0.3.5" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad" 1044 | dependencies = [ 1045 | "itoa 0.4.7", 1046 | "libc", 1047 | ] 1048 | 1049 | [[package]] 1050 | name = "tinyvec" 1051 | version = "1.2.0" 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" 1053 | checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" 1054 | dependencies = [ 1055 | "tinyvec_macros", 1056 | ] 1057 | 1058 | [[package]] 1059 | name = "tinyvec_macros" 1060 | version = "0.1.0" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 1063 | 1064 | [[package]] 1065 | name = "tokio" 1066 | version = "1.17.0" 1067 | source = "registry+https://github.com/rust-lang/crates.io-index" 1068 | checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" 1069 | dependencies = [ 1070 | "bytes", 1071 | "libc", 1072 | "memchr", 1073 | "mio", 1074 | "num_cpus", 1075 | "once_cell", 1076 | "parking_lot", 1077 | "pin-project-lite", 1078 | "signal-hook-registry", 1079 | "socket2", 1080 | "tokio-macros", 1081 | "winapi", 1082 | ] 1083 | 1084 | [[package]] 1085 | name = "tokio-macros" 1086 | version = "1.7.0" 1087 | source = "registry+https://github.com/rust-lang/crates.io-index" 1088 | checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" 1089 | dependencies = [ 1090 | "proc-macro2", 1091 | "quote", 1092 | "syn", 1093 | ] 1094 | 1095 | [[package]] 1096 | name = "tokio-rustls" 1097 | version = "0.23.3" 1098 | source = "registry+https://github.com/rust-lang/crates.io-index" 1099 | checksum = "4151fda0cf2798550ad0b34bcfc9b9dcc2a9d2471c895c68f3a8818e54f2389e" 1100 | dependencies = [ 1101 | "rustls", 1102 | "tokio", 1103 | "webpki", 1104 | ] 1105 | 1106 | [[package]] 1107 | name = "tokio-stream" 1108 | version = "0.1.8" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" 1111 | dependencies = [ 1112 | "futures-core", 1113 | "pin-project-lite", 1114 | "tokio", 1115 | ] 1116 | 1117 | [[package]] 1118 | name = "tokio-util" 1119 | version = "0.6.8" 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" 1121 | checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd" 1122 | dependencies = [ 1123 | "bytes", 1124 | "futures-core", 1125 | "futures-sink", 1126 | "log", 1127 | "pin-project-lite", 1128 | "tokio", 1129 | ] 1130 | 1131 | [[package]] 1132 | name = "tokio-util" 1133 | version = "0.7.0" 1134 | source = "registry+https://github.com/rust-lang/crates.io-index" 1135 | checksum = "64910e1b9c1901aaf5375561e35b9c057d95ff41a44ede043a03e09279eabaf1" 1136 | dependencies = [ 1137 | "bytes", 1138 | "futures-core", 1139 | "futures-io", 1140 | "futures-sink", 1141 | "futures-util", 1142 | "log", 1143 | "pin-project-lite", 1144 | "slab", 1145 | "tokio", 1146 | ] 1147 | 1148 | [[package]] 1149 | name = "tracing" 1150 | version = "0.1.26" 1151 | source = "registry+https://github.com/rust-lang/crates.io-index" 1152 | checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" 1153 | dependencies = [ 1154 | "cfg-if", 1155 | "pin-project-lite", 1156 | "tracing-attributes", 1157 | "tracing-core", 1158 | ] 1159 | 1160 | [[package]] 1161 | name = "tracing-appender" 1162 | version = "0.2.0" 1163 | source = "registry+https://github.com/rust-lang/crates.io-index" 1164 | checksum = "94571df2eae3ed4353815ea5a90974a594a1792d8782ff2cbcc9392d1101f366" 1165 | dependencies = [ 1166 | "crossbeam-channel", 1167 | "time", 1168 | "tracing-subscriber", 1169 | ] 1170 | 1171 | [[package]] 1172 | name = "tracing-attributes" 1173 | version = "0.1.15" 1174 | source = "registry+https://github.com/rust-lang/crates.io-index" 1175 | checksum = "c42e6fa53307c8a17e4ccd4dc81cf5ec38db9209f59b222210375b54ee40d1e2" 1176 | dependencies = [ 1177 | "proc-macro2", 1178 | "quote", 1179 | "syn", 1180 | ] 1181 | 1182 | [[package]] 1183 | name = "tracing-core" 1184 | version = "0.1.21" 1185 | source = "registry+https://github.com/rust-lang/crates.io-index" 1186 | checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" 1187 | dependencies = [ 1188 | "lazy_static", 1189 | ] 1190 | 1191 | [[package]] 1192 | name = "tracing-futures" 1193 | version = "0.2.5" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" 1196 | dependencies = [ 1197 | "pin-project", 1198 | "tracing", 1199 | ] 1200 | 1201 | [[package]] 1202 | name = "tracing-log" 1203 | version = "0.1.2" 1204 | source = "registry+https://github.com/rust-lang/crates.io-index" 1205 | checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" 1206 | dependencies = [ 1207 | "lazy_static", 1208 | "log", 1209 | "tracing-core", 1210 | ] 1211 | 1212 | [[package]] 1213 | name = "tracing-serde" 1214 | version = "0.1.2" 1215 | source = "registry+https://github.com/rust-lang/crates.io-index" 1216 | checksum = "fb65ea441fbb84f9f6748fd496cf7f63ec9af5bca94dd86456978d055e8eb28b" 1217 | dependencies = [ 1218 | "serde", 1219 | "tracing-core", 1220 | ] 1221 | 1222 | [[package]] 1223 | name = "tracing-subscriber" 1224 | version = "0.3.6" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "77be66445c4eeebb934a7340f227bfe7b338173d3f8c00a60a5a58005c9faecf" 1227 | dependencies = [ 1228 | "ansi_term", 1229 | "lazy_static", 1230 | "matchers", 1231 | "regex", 1232 | "serde", 1233 | "serde_json", 1234 | "sharded-slab", 1235 | "smallvec", 1236 | "thread_local", 1237 | "tracing", 1238 | "tracing-core", 1239 | "tracing-log", 1240 | "tracing-serde", 1241 | ] 1242 | 1243 | [[package]] 1244 | name = "trust-dns-proto" 1245 | version = "0.21.2" 1246 | source = "registry+https://github.com/rust-lang/crates.io-index" 1247 | checksum = "9c31f240f59877c3d4bb3b3ea0ec5a6a0cff07323580ff8c7a605cd7d08b255d" 1248 | dependencies = [ 1249 | "async-trait", 1250 | "cfg-if", 1251 | "data-encoding", 1252 | "enum-as-inner", 1253 | "futures-channel", 1254 | "futures-io", 1255 | "futures-util", 1256 | "idna", 1257 | "ipnet", 1258 | "lazy_static", 1259 | "log", 1260 | "rand", 1261 | "smallvec", 1262 | "thiserror", 1263 | "tinyvec", 1264 | "tokio", 1265 | "url", 1266 | ] 1267 | 1268 | [[package]] 1269 | name = "trust-dns-proto" 1270 | version = "0.21.2" 1271 | source = "git+https://github.com/leshow/trust-dns?branch=mine_doh#c753a7bc07a2bc21a465c95241688351fbd18060" 1272 | dependencies = [ 1273 | "async-trait", 1274 | "bytes", 1275 | "cfg-if", 1276 | "data-encoding", 1277 | "enum-as-inner", 1278 | "futures-channel", 1279 | "futures-io", 1280 | "futures-util", 1281 | "h2", 1282 | "http", 1283 | "idna", 1284 | "ipnet", 1285 | "lazy_static", 1286 | "log", 1287 | "rand", 1288 | "rustls", 1289 | "rustls-pemfile", 1290 | "smallvec", 1291 | "thiserror", 1292 | "tinyvec", 1293 | "tokio", 1294 | "tokio-rustls", 1295 | "url", 1296 | "webpki", 1297 | "webpki-roots", 1298 | ] 1299 | 1300 | [[package]] 1301 | name = "trust-dns-resolver" 1302 | version = "0.21.2" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "e4ba72c2ea84515690c9fcef4c6c660bb9df3036ed1051686de84605b74fd558" 1305 | dependencies = [ 1306 | "cfg-if", 1307 | "futures-util", 1308 | "ipconfig", 1309 | "lazy_static", 1310 | "log", 1311 | "lru-cache", 1312 | "parking_lot", 1313 | "resolv-conf", 1314 | "smallvec", 1315 | "thiserror", 1316 | "tokio", 1317 | "trust-dns-proto 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)", 1318 | ] 1319 | 1320 | [[package]] 1321 | name = "unicode-bidi" 1322 | version = "0.3.5" 1323 | source = "registry+https://github.com/rust-lang/crates.io-index" 1324 | checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" 1325 | dependencies = [ 1326 | "matches", 1327 | ] 1328 | 1329 | [[package]] 1330 | name = "unicode-normalization" 1331 | version = "0.1.18" 1332 | source = "registry+https://github.com/rust-lang/crates.io-index" 1333 | checksum = "33717dca7ac877f497014e10d73f3acf948c342bee31b5ca7892faf94ccc6b49" 1334 | dependencies = [ 1335 | "tinyvec", 1336 | ] 1337 | 1338 | [[package]] 1339 | name = "unicode-xid" 1340 | version = "0.2.2" 1341 | source = "registry+https://github.com/rust-lang/crates.io-index" 1342 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 1343 | 1344 | [[package]] 1345 | name = "untrusted" 1346 | version = "0.7.1" 1347 | source = "registry+https://github.com/rust-lang/crates.io-index" 1348 | checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" 1349 | 1350 | [[package]] 1351 | name = "url" 1352 | version = "2.2.2" 1353 | source = "registry+https://github.com/rust-lang/crates.io-index" 1354 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 1355 | dependencies = [ 1356 | "form_urlencoded", 1357 | "idna", 1358 | "matches", 1359 | "percent-encoding", 1360 | ] 1361 | 1362 | [[package]] 1363 | name = "version_check" 1364 | version = "0.9.4" 1365 | source = "registry+https://github.com/rust-lang/crates.io-index" 1366 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1367 | 1368 | [[package]] 1369 | name = "wasi" 1370 | version = "0.10.2+wasi-snapshot-preview1" 1371 | source = "registry+https://github.com/rust-lang/crates.io-index" 1372 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 1373 | 1374 | [[package]] 1375 | name = "wasi" 1376 | version = "0.11.0+wasi-snapshot-preview1" 1377 | source = "registry+https://github.com/rust-lang/crates.io-index" 1378 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1379 | 1380 | [[package]] 1381 | name = "wasm-bindgen" 1382 | version = "0.2.76" 1383 | source = "registry+https://github.com/rust-lang/crates.io-index" 1384 | checksum = "8ce9b1b516211d33767048e5d47fa2a381ed8b76fc48d2ce4aa39877f9f183e0" 1385 | dependencies = [ 1386 | "cfg-if", 1387 | "wasm-bindgen-macro", 1388 | ] 1389 | 1390 | [[package]] 1391 | name = "wasm-bindgen-backend" 1392 | version = "0.2.76" 1393 | source = "registry+https://github.com/rust-lang/crates.io-index" 1394 | checksum = "cfe8dc78e2326ba5f845f4b5bf548401604fa20b1dd1d365fb73b6c1d6364041" 1395 | dependencies = [ 1396 | "bumpalo", 1397 | "lazy_static", 1398 | "log", 1399 | "proc-macro2", 1400 | "quote", 1401 | "syn", 1402 | "wasm-bindgen-shared", 1403 | ] 1404 | 1405 | [[package]] 1406 | name = "wasm-bindgen-macro" 1407 | version = "0.2.76" 1408 | source = "registry+https://github.com/rust-lang/crates.io-index" 1409 | checksum = "44468aa53335841d9d6b6c023eaab07c0cd4bddbcfdee3e2bb1e8d2cb8069fef" 1410 | dependencies = [ 1411 | "quote", 1412 | "wasm-bindgen-macro-support", 1413 | ] 1414 | 1415 | [[package]] 1416 | name = "wasm-bindgen-macro-support" 1417 | version = "0.2.76" 1418 | source = "registry+https://github.com/rust-lang/crates.io-index" 1419 | checksum = "0195807922713af1e67dc66132c7328206ed9766af3858164fb583eedc25fbad" 1420 | dependencies = [ 1421 | "proc-macro2", 1422 | "quote", 1423 | "syn", 1424 | "wasm-bindgen-backend", 1425 | "wasm-bindgen-shared", 1426 | ] 1427 | 1428 | [[package]] 1429 | name = "wasm-bindgen-shared" 1430 | version = "0.2.76" 1431 | source = "registry+https://github.com/rust-lang/crates.io-index" 1432 | checksum = "acdb075a845574a1fa5f09fd77e43f7747599301ea3417a9fbffdeedfc1f4a29" 1433 | 1434 | [[package]] 1435 | name = "web-sys" 1436 | version = "0.3.53" 1437 | source = "registry+https://github.com/rust-lang/crates.io-index" 1438 | checksum = "224b2f6b67919060055ef1a67807367c2066ed520c3862cc013d26cf893a783c" 1439 | dependencies = [ 1440 | "js-sys", 1441 | "wasm-bindgen", 1442 | ] 1443 | 1444 | [[package]] 1445 | name = "webpki" 1446 | version = "0.22.0" 1447 | source = "registry+https://github.com/rust-lang/crates.io-index" 1448 | checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" 1449 | dependencies = [ 1450 | "ring", 1451 | "untrusted", 1452 | ] 1453 | 1454 | [[package]] 1455 | name = "webpki-roots" 1456 | version = "0.22.3" 1457 | source = "registry+https://github.com/rust-lang/crates.io-index" 1458 | checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf" 1459 | dependencies = [ 1460 | "webpki", 1461 | ] 1462 | 1463 | [[package]] 1464 | name = "widestring" 1465 | version = "0.5.1" 1466 | source = "registry+https://github.com/rust-lang/crates.io-index" 1467 | checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" 1468 | 1469 | [[package]] 1470 | name = "winapi" 1471 | version = "0.3.9" 1472 | source = "registry+https://github.com/rust-lang/crates.io-index" 1473 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1474 | dependencies = [ 1475 | "winapi-i686-pc-windows-gnu", 1476 | "winapi-x86_64-pc-windows-gnu", 1477 | ] 1478 | 1479 | [[package]] 1480 | name = "winapi-i686-pc-windows-gnu" 1481 | version = "0.4.0" 1482 | source = "registry+https://github.com/rust-lang/crates.io-index" 1483 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1484 | 1485 | [[package]] 1486 | name = "winapi-util" 1487 | version = "0.1.5" 1488 | source = "registry+https://github.com/rust-lang/crates.io-index" 1489 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1490 | dependencies = [ 1491 | "winapi", 1492 | ] 1493 | 1494 | [[package]] 1495 | name = "winapi-x86_64-pc-windows-gnu" 1496 | version = "0.4.0" 1497 | source = "registry+https://github.com/rust-lang/crates.io-index" 1498 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1499 | 1500 | [[package]] 1501 | name = "windows-sys" 1502 | version = "0.34.0" 1503 | source = "registry+https://github.com/rust-lang/crates.io-index" 1504 | checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" 1505 | dependencies = [ 1506 | "windows_aarch64_msvc", 1507 | "windows_i686_gnu", 1508 | "windows_i686_msvc", 1509 | "windows_x86_64_gnu", 1510 | "windows_x86_64_msvc", 1511 | ] 1512 | 1513 | [[package]] 1514 | name = "windows_aarch64_msvc" 1515 | version = "0.34.0" 1516 | source = "registry+https://github.com/rust-lang/crates.io-index" 1517 | checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" 1518 | 1519 | [[package]] 1520 | name = "windows_i686_gnu" 1521 | version = "0.34.0" 1522 | source = "registry+https://github.com/rust-lang/crates.io-index" 1523 | checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" 1524 | 1525 | [[package]] 1526 | name = "windows_i686_msvc" 1527 | version = "0.34.0" 1528 | source = "registry+https://github.com/rust-lang/crates.io-index" 1529 | checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" 1530 | 1531 | [[package]] 1532 | name = "windows_x86_64_gnu" 1533 | version = "0.34.0" 1534 | source = "registry+https://github.com/rust-lang/crates.io-index" 1535 | checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" 1536 | 1537 | [[package]] 1538 | name = "windows_x86_64_msvc" 1539 | version = "0.34.0" 1540 | source = "registry+https://github.com/rust-lang/crates.io-index" 1541 | checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" 1542 | 1543 | [[package]] 1544 | name = "winreg" 1545 | version = "0.7.0" 1546 | source = "registry+https://github.com/rust-lang/crates.io-index" 1547 | checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" 1548 | dependencies = [ 1549 | "winapi", 1550 | ] 1551 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ "bin" ] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Evan Cameron 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nailgun 2 | 3 | `nailgun` is a DNS performance testing client written in Rust using `trust-dns-proto` and `tokio`. Open to PRs, issues, comments! 4 | 5 | `nailgun` is written as a hobby project first and foremost, it's inspired by flamethrower and copies some of its arguments. It is single-threaded by default but configurable to use many threads. You can specify both the number of traffic generators (with `tcount`) and the number of tokio worker threads (with `wcount`), `nailgun` will start `tcount * wcount` generators. Most of the time, this is not necessary as 1 concurrent generator and a single worker thread is more than enough. Testing against a local `dnsdist` instance configured to return instantly with `NXDOMAIN`, (yes, not a real-world benchmark) `nailgun` can do well over 250K QPS (1 worker thread & generator). 6 | 7 | `nailgun` uses `tracing` for logging so `RUST_LOG` can be used in order to control the logging output. 8 | 9 | ## Features 10 | 11 | ### Rate limiting 12 | 13 | You can specify a specific QPS with `-Q`, this allows you to set a desired QPS rate which will be divided up over all the senders. 14 | 15 | ### Output 16 | 17 | `--output` allows you to specify different logging formats, courtesy of `tracing`. "pretty", "json" and "debug" are currently supported with "pretty" as the default. You can use `RUST_LOG` to filter on the output (ex. `RUST_LOG="naildns=trace"` will print trace level from the `naildns` bin only). `nailgun` uses stdout by default, but will log to file if you accompany this with the `-o` flag and a path. 18 | 19 | Regardless of these options, a summary is printed to stdout sans-tracing after the run is over. 20 | 21 | ### Generators 22 | 23 | There are multiple generator types available with `-g`, the default is `static` but you can generate queries from file also or with some portion randomly generated. 24 | 25 | ### DoH (new!) 26 | 27 | `nailgun` can now send DoH messages! 28 | 29 | ``` 30 | naildns dns.google -Q 2 -P doh 31 | ``` 32 | 33 | ## Usage 34 | 35 | ``` 36 | naildns --help 37 | ``` 38 | 39 | By default, nailgun will spawn a single threaded tokio runtime and 1 traffic generator: 40 | 41 | ``` 42 | naildns 0.0.0.0 -p 1953 43 | ``` 44 | 45 | This can be scaled up: 46 | 47 | ``` 48 | naildns 0.0.0.0 -p 1953 -w 16 -t 1 49 | ``` 50 | 51 | Will spawn 16 OS threads (`w`/`wcount`) on the tokio runtime and 1 traffic generator (`t`/`tcount`) per thread spawned, for a total of 16\*1 = 16 traffic generators. 52 | 53 | **Note** If you want a quick server to test against, I've been spinning up `dnsdist` and adding `addAction(AllRule(), RCodeAction(3))` to its config so that all DNS messages immediately return with `NXDomain`. 54 | 55 | ### Building & Installing 56 | 57 | To build locally: 58 | 59 | ``` 60 | cargo build --release 61 | ``` 62 | 63 | `naildns` binary will be present in `target/release/naildns` 64 | 65 | #### From crates.io 66 | 67 | #### Local 68 | 69 | To install locally 70 | 71 | ``` 72 | cargo install 73 | ``` 74 | 75 | ### TODO 76 | 77 | - throttle based on # of timeouts 78 | - first batch of messages always seems to be double the set QPS (try `--limit-secs 5 -Q 10` for example, it will end up sending 60 queries) 79 | - some TODO cleanups left in the code to address 80 | - arg parsing for generator is very different from flamethrower because clap doesn't seem to support parsing it in the same style-- would be nice to figure this out 81 | - ~~combine logging output from all generators rather than having them run individually~~ 82 | - more generator types? 83 | - maybe leverage other parts of trustdns to make options for interesting types of queries. DOT? ~~DOH~~? 84 | - suggestions? bugs? 85 | -------------------------------------------------------------------------------- /bin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "naildns" 3 | version = "0.2.0" 4 | authors = ["Evan Cameron "] 5 | edition = "2021" 6 | description = """ 7 | nailgun (binary called `naildns`) is a small tool written in Rust that supports benchmarking and stress testing DNS servers. It supports IPv4 & IPv6, UDP & TCP and can generate different kinds of queries 8 | 9 | ex. naildns 8.8.8.8 -Q 1 (send 1 query per second to 8.8.8.8) 10 | """ 11 | categories = ["command-line-utilities", "asynchronous", "network-programming"] 12 | keywords = ["nailgun", "dns", "performance", "testing"] 13 | repository = "https://github.com/leshow/nailgun" 14 | license = "MIT" 15 | 16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 17 | 18 | [features] 19 | default = ["doh"] 20 | doh = ["trust-dns-proto/dns-over-https-rustls", "rustls", "webpki-roots"] 21 | 22 | [dependencies] 23 | async-trait = "0.1.51" 24 | anyhow = "1.0" 25 | clap = { version = "3.1.6", features = ["derive"] } 26 | bytes = "1.1" 27 | num_cpus = "1.13.0" 28 | tokio = { version = "1.17.0", features = ["full"] } 29 | tokio-util = { version = "0.7.0", features = ["full"] } 30 | tokio-stream = "0.1.8" 31 | trust-dns-proto = { git = "https://github.com/leshow/trust-dns", branch = "mine_doh" } 32 | trust-dns-resolver = "0.21.2" 33 | tracing = "0.1.26" 34 | tracing-subscriber = { version = "0.3.5", features = ["env-filter","json"] } 35 | tracing-appender = "0.2.0" 36 | tracing-futures = "0.2.5" 37 | parking_lot = "0.12.0" 38 | rustc-hash = "1.1.0" 39 | rustls = { version = "0.20.4", optional = true } 40 | rand = "0.8" 41 | governor = "0.4.2" 42 | num-traits = "0.2.14" 43 | webpki-roots = { version = "0.22.3", optional = true } -------------------------------------------------------------------------------- /bin/src/args.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, bail, Context, Error, Result}; 2 | use clap::Parser; 3 | use trust_dns_proto::rr::{DNSClass, Name, RecordType}; 4 | 5 | use std::{net::IpAddr, path::PathBuf, str::FromStr, time::Duration}; 6 | 7 | use crate::{config::Config, query::Source}; 8 | 9 | /// nailgun is a cli tool for stress testing and benchmarking DNS 10 | #[derive(Debug, Parser, Clone, PartialEq, Eq)] 11 | #[clap(author, about, version)] 12 | pub struct Args { 13 | /// IP or domain send traffic to 14 | pub target: String, 15 | /// IP address to bind to. If family not set will use 16 | /// [default: 0.0.0.0] 17 | #[clap(long, short = 'b')] 18 | pub bind_ip: Option, 19 | /// which port to nail 20 | #[clap(long, short = 'p', default_value = "53")] 21 | pub port: u16, 22 | /// which internet family to use, (inet/inet6) 23 | #[clap(long, short = 'F', default_value = "inet")] 24 | pub family: Family, 25 | /// the base record to use as the query for generators 26 | #[clap(long, short = 'r', default_value = "test.com.")] 27 | pub record: String, 28 | /// the query type to use for generators 29 | #[clap(long, short = 'T', default_value = "A")] 30 | pub qtype: RecordType, 31 | /// the query class to use 32 | #[clap(long, default_value = "IN")] 33 | pub class: DNSClass, 34 | /// query timeout in seconds 35 | #[clap(long, short = 't', default_value = "2")] 36 | pub timeout: u64, 37 | /// protocol to use (udp/tcp) 38 | #[clap(long, short = 'P', default_value = "udp")] 39 | pub protocol: Protocol, 40 | /// rate limit to a maximum queries per second, 0 is unlimited 41 | #[clap(long, short = 'Q', default_value = "0")] 42 | pub qps: u32, 43 | /// number of concurrent traffic generators per process 44 | #[clap(long, short = 'c', default_value = "1")] 45 | pub tcount: usize, 46 | /// number of tokio worker threads to spawn 47 | #[clap(long, short = 'w', default_value = "1")] 48 | pub wcount: usize, 49 | /// limits traffic generation to n seconds, 0 is unlimited 50 | #[clap(long, short = 'l', default_value = "0")] 51 | pub limit_secs: u64, 52 | /// log output format (pretty/json/debug) 53 | #[clap(long, default_value = "pretty")] 54 | pub output: LogStructure, 55 | /// query generator type (static/file/randompkt/randomqname) 56 | #[clap(subcommand)] 57 | pub generator: Option, 58 | /// output file for logs/metrics 59 | #[clap(short = 'o')] 60 | pub log_file: Option, 61 | } 62 | 63 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 64 | pub enum LogStructure { 65 | Debug, 66 | Pretty, 67 | Json, 68 | } 69 | 70 | impl FromStr for LogStructure { 71 | type Err = Error; 72 | fn from_str(s: &str) -> Result { 73 | match &s.to_ascii_lowercase()[..] { 74 | "json" => Ok(LogStructure::Json), 75 | "pretty" => Ok(LogStructure::Pretty), 76 | "debug" => Ok(LogStructure::Debug), 77 | _ => Err(anyhow!( 78 | "unknown log structure type: {:?} must be \"json\" or \"compact\" or \"pretty\"", 79 | s 80 | )), 81 | } 82 | } 83 | } 84 | 85 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 86 | pub enum Protocol { 87 | Udp, 88 | Tcp, 89 | #[cfg(feature = "doh")] 90 | DoH, 91 | } 92 | 93 | impl FromStr for Protocol { 94 | type Err = Error; 95 | fn from_str(s: &str) -> Result { 96 | match &s.to_ascii_lowercase()[..] { 97 | "udp" => Ok(Protocol::Udp), 98 | "tcp" => Ok(Protocol::Tcp), 99 | #[cfg(feature = "doh")] 100 | "doh" => Ok(Protocol::DoH), 101 | #[cfg(feature = "doh")] 102 | _ => Err(anyhow!( 103 | "unknown protocol type: {:?} must be \"udp\" or \"tcp\" \"doh\"", 104 | s 105 | )), 106 | #[cfg(not(feature = "doh"))] 107 | _ => Err(anyhow!( 108 | "unknown protocol type: {:?} must be \"udp\" or \"tcp\"", 109 | s 110 | )), 111 | } 112 | } 113 | } 114 | 115 | impl Protocol { 116 | #[allow(dead_code)] 117 | pub fn is_udp(&self) -> bool { 118 | matches!(*self, Protocol::Udp) 119 | } 120 | #[allow(dead_code)] 121 | pub fn is_tcp(&self) -> bool { 122 | matches!(self, Protocol::Tcp) 123 | } 124 | #[cfg(feature = "doh")] 125 | pub fn is_doh(&self) -> bool { 126 | matches!(*self, Protocol::DoH) 127 | } 128 | } 129 | 130 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 131 | pub enum Family { 132 | INet, 133 | INet6, 134 | } 135 | 136 | impl FromStr for Family { 137 | type Err = Error; 138 | fn from_str(s: &str) -> Result { 139 | match &s.to_ascii_lowercase()[..] { 140 | "inet" => Ok(Family::INet), 141 | "inet6" => Ok(Family::INet6), 142 | _ => Err(anyhow!( 143 | "unknown family type: {:?} must be \"inet\" or \"inet6\"", 144 | s 145 | )), 146 | } 147 | } 148 | } 149 | 150 | #[derive(Parser, Clone, PartialEq, Eq, Hash, Debug)] 151 | pub enum GenType { 152 | Static, 153 | #[clap(name = "randompkt")] 154 | RandomPkt { 155 | #[clap(default_value = "600")] 156 | size: usize, 157 | }, 158 | #[clap(name = "randomqname")] 159 | RandomQName { 160 | #[clap(default_value = "62")] 161 | size: usize, 162 | }, 163 | File { 164 | #[clap(parse(from_os_str))] 165 | path: PathBuf, 166 | }, 167 | } 168 | 169 | impl Args { 170 | pub async fn to_config(&self) -> Result { 171 | use trust_dns_resolver::{config::*, TokioAsyncResolver}; 172 | 173 | let query_src = match &self.generator { 174 | Some(GenType::File { path }) => Source::File(path.clone()), 175 | Some(GenType::Static) | None => Source::Static { 176 | name: Name::from_ascii(&self.record).map_err(|err| { 177 | anyhow!( 178 | "failed to parse record: {:?}. with error: {:?}", 179 | self.record, 180 | err 181 | ) 182 | })?, 183 | qtype: self.qtype, 184 | class: self.class, 185 | }, 186 | Some(GenType::RandomPkt { size }) => Source::RandomPkt { size: *size }, 187 | Some(GenType::RandomQName { size }) => Source::RandomQName { 188 | size: *size, 189 | qtype: self.qtype, 190 | }, 191 | }; 192 | 193 | let (target, name_server) = match self.target.parse::() { 194 | #[cfg(feature = "doh")] 195 | Ok(target) if self.protocol == self::Protocol::DoH => { 196 | bail!("found {}: need to use a domain name for DoH", target) 197 | } 198 | Ok(target) => (target, None), 199 | Err(_) => { 200 | // is not an IP, so see if we can resolve it over dns 201 | let resolver = 202 | TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default())?; 203 | let response = resolver.lookup_ip(self.target.clone()).await?; 204 | 205 | ( 206 | response 207 | .iter() 208 | .next() 209 | .context("Resolver failed to return an addr")?, 210 | Some(self.target.clone()), 211 | ) 212 | } 213 | }; 214 | Ok(Config { 215 | target: (target, self.port).into(), 216 | name_server, 217 | protocol: self.protocol, 218 | bind: self.bind_ip.context("Args::ip always has a value")?, 219 | query_src, 220 | qps: self.qps, 221 | timeout: Duration::from_secs(self.timeout), 222 | generators: self.tcount * self.wcount, 223 | }) 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /bin/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | net::{IpAddr, SocketAddr}, 3 | num::NonZeroU32, 4 | time::Duration, 5 | }; 6 | 7 | use governor::{ 8 | clock::DefaultClock, 9 | state::{InMemoryState, NotKeyed}, 10 | Quota, RateLimiter, 11 | }; 12 | 13 | use crate::{args::Protocol, query::Source}; 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct Config { 17 | pub target: SocketAddr, 18 | pub name_server: Option, 19 | pub protocol: Protocol, 20 | pub bind: IpAddr, 21 | pub qps: u32, 22 | pub timeout: Duration, 23 | pub generators: usize, 24 | pub query_src: Source, 25 | } 26 | 27 | impl Config { 28 | pub const fn batch_size(&self) -> u32 { 29 | 1_000 30 | } 31 | 32 | pub const fn rate_per_gen(&self) -> u32 { 33 | self.qps / self.generators as u32 34 | } 35 | 36 | pub const fn qps(&self) -> Qps { 37 | if self.qps == 0 { 38 | Qps::Unlimited 39 | } else { 40 | Qps::Limited(self.qps) 41 | } 42 | } 43 | 44 | pub fn rate_limiter(&self) -> Option> { 45 | if self.qps().is_limited() { 46 | Some(RateLimiter::direct(Quota::per_second( 47 | NonZeroU32::new(self.rate_per_gen()).expect("QPS is non-zero"), 48 | ))) 49 | } else { 50 | None 51 | } 52 | } 53 | } 54 | 55 | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)] 56 | pub enum Qps { 57 | Unlimited, 58 | Limited(u32), 59 | } 60 | 61 | impl Qps { 62 | /// Returns `true` if the qps is [`Unlimited`]. 63 | #[allow(dead_code)] 64 | pub fn is_unlimited(&self) -> bool { 65 | !self.is_limited() 66 | } 67 | 68 | /// Returns `true` if the qps is [`Limited`]. 69 | pub fn is_limited(&self) -> bool { 70 | matches!(self, Self::Limited(..)) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /bin/src/gen.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | net::SocketAddr, 4 | pin::Pin, 5 | sync::Arc, 6 | time::{Duration, Instant as StdInstant}, 7 | }; 8 | 9 | use anyhow::{Context, Result}; 10 | use async_trait::async_trait; 11 | use bytes::Bytes; 12 | use governor::{ 13 | clock::DefaultClock, 14 | state::{InMemoryState, NotKeyed}, 15 | RateLimiter, 16 | }; 17 | use parking_lot::Mutex; 18 | use tokio::{ 19 | net::{TcpStream, UdpSocket}, 20 | sync::mpsc, 21 | task::JoinHandle, 22 | time::{self, Instant}, 23 | }; 24 | use tokio_stream::{wrappers::ReceiverStream, Stream, StreamExt}; 25 | use tokio_util::{codec::FramedRead, udp::UdpFramed}; 26 | use tracing::{error, info, trace}; 27 | 28 | #[cfg(feature = "doh")] 29 | use crate::sender::doh::DohSender; 30 | use crate::{ 31 | args::Protocol, 32 | config::Config, 33 | msg::{BufMsg, BytesFreezeCodec, TcpDnsCodec}, 34 | query::{FileGen, RandomPkt, RandomQName, Source, StaticGen}, 35 | sender::{MsgSend, Sender}, 36 | shutdown::Shutdown, 37 | stats::{StatsInterval, StatsTracker}, 38 | store::{AtomicStore, Store}, 39 | }; 40 | 41 | #[derive(Debug)] 42 | pub struct Generator { 43 | pub config: Config, 44 | pub shutdown: Shutdown, 45 | pub stats_tx: mpsc::Sender, 46 | pub _shutdown_complete: mpsc::Sender<()>, 47 | } 48 | 49 | impl Generator { 50 | pub async fn run(&mut self) -> Result<()> { 51 | let store = Arc::new(Mutex::new(Store::new())); 52 | let mut stats = StatsTracker::default(); 53 | 54 | let bucket = self.config.rate_limiter(); 55 | 56 | let (mut reader, sender) = { 57 | let store = Arc::clone(&store); 58 | let atomic_store = Arc::clone(&stats.atomic_store); 59 | let config = self.config.clone(); 60 | match self.config.protocol { 61 | // TODO: Probably not realistic that each generator maintains one TcpStream. 62 | // Would need to drop this abstraction in order to have tcp gen open/close multiple 63 | // connections? 64 | Protocol::Tcp => TcpGen::build(store, atomic_store, config, bucket).await?, 65 | Protocol::Udp => UdpGen::build(store, atomic_store, config, bucket).await?, 66 | #[cfg(feature = "doh")] 67 | Protocol::DoH => DohGen::build(store, atomic_store, config, bucket).await?, 68 | } 69 | }; 70 | let mut sender = self.sender_task(sender)?; 71 | let mut cleanup = self.cleanup_task(Arc::clone(&store), Arc::clone(&stats.atomic_store)); 72 | 73 | // timers 74 | let sleep = time::sleep(Duration::from_secs(1)); 75 | tokio::pin!(sleep); 76 | let mut interval = Instant::now(); 77 | let mut total_duration = Duration::from_millis(0); 78 | 79 | let log = |interval| { 80 | let now = Instant::now(); 81 | let elapsed = now.duration_since(interval); 82 | let store = store.lock(); 83 | let in_flight = store.in_flight.len(); 84 | let ids = store.ids.len(); 85 | drop(store); 86 | (now, elapsed, in_flight, ids) 87 | }; 88 | while !self.shutdown.is_shutdown() { 89 | tokio::select! { 90 | res = reader.next() => { 91 | let frame = match res { 92 | Some(frame) => frame, 93 | None => return Ok(()) 94 | }; 95 | if let Ok((buf, addr)) = frame { 96 | let msg = BufMsg::new(buf, addr); 97 | let id = msg.msg_id(); 98 | let mut store = store.lock(); 99 | store.ids.push_back(id); 100 | if let Some(qinfo) = store.in_flight.remove(&id) { 101 | // TODO: there is a bug here were if we 102 | // never recv then nothing is removed, so there is no 103 | // sent buf_size 104 | stats.update(qinfo, &msg); 105 | } 106 | drop(store); 107 | } 108 | stats.recv += 1; 109 | }, 110 | () = &mut sleep => { 111 | let (now, elapsed, in_flight, ids) = log(interval); 112 | total_duration += elapsed; 113 | interval = now; 114 | // should complete immediately 115 | self.stats_tx.send(stats.interval(elapsed, total_duration, in_flight, ids)).await?; 116 | // reset the timer 117 | sleep.as_mut().reset(now + Duration::from_secs(1)); 118 | }, 119 | // if any handle returns or we recv shutdown-- exit generator 120 | // TODO: kinda gnarly. we could pass in `shutdown` 121 | // separately in `run` so we can have unique access. 122 | _ = async { 123 | tokio::select! { 124 | _ = self.shutdown.recv() => {}, 125 | _ = &mut sender => {}, 126 | _ = &mut cleanup => {} 127 | } 128 | } => { 129 | trace!("shutdown received"); 130 | // abort sender 131 | sender.abort(); 132 | // wait for remaining in flight messages or timeouts 133 | self.wait_in_flight(&store, &stats.atomic_store).await; 134 | // final log 135 | let (_, elapsed, in_flight, ids) = log(interval); 136 | total_duration += elapsed; 137 | self.stats_tx.send(stats.interval(elapsed, total_duration, in_flight, ids)).await?; 138 | // abort cleanup 139 | cleanup.abort(); 140 | // self.stats_tx.send(stats.totals()).await?; 141 | return Ok(()); 142 | } 143 | } 144 | } 145 | 146 | // we should only ever exit via shutdown.recv() 147 | Ok(()) 148 | } 149 | 150 | async fn wait_in_flight(&self, store: &Arc>, atomic_store: &Arc) { 151 | // add a little extra time to timeout any in flight queries 152 | let timeout = self.config.timeout; 153 | let mut interval = time::interval(Duration::from_millis(10)); 154 | let start_wait = StdInstant::now(); 155 | loop { 156 | interval.tick().await; 157 | let mut store = store.lock(); 158 | // could rely on cleanup_task? 159 | store.clear_timeouts(timeout, atomic_store); 160 | let empty = store.in_flight.is_empty(); 161 | let now = StdInstant::now(); 162 | if empty || (now - start_wait > timeout) { 163 | trace!("waited {:?} for cleanup", now - start_wait); 164 | return; 165 | } 166 | drop(store); 167 | } 168 | } 169 | 170 | // cleans up timed out message, returning them to in_flight queue 171 | fn cleanup_task( 172 | &self, 173 | store: Arc>, 174 | atomic_store: Arc, 175 | ) -> JoinHandle<()> { 176 | let timeout = self.config.timeout; 177 | tokio::spawn(async move { 178 | let mut sleep = time::interval(timeout); 179 | sleep.tick().await; 180 | loop { 181 | sleep.tick().await; 182 | let mut store = store.lock(); 183 | store.clear_timeouts(timeout, &atomic_store); 184 | drop(store); 185 | } 186 | }) 187 | } 188 | fn sender_task(&self, mut sender: Sender) -> Result> { 189 | // TODO: can we reduce the code duplication here without using 190 | // dynamic types? 191 | Ok(match &self.config.query_src { 192 | Source::File(path) => { 193 | info!(path = %path.display(), "using file gen"); 194 | let query_gen = FileGen::new(path)?; 195 | 196 | tokio::spawn(async move { 197 | if let Err(err) = sender.run(query_gen).await { 198 | error!(?err, "error in send task"); 199 | } 200 | }) 201 | } 202 | Source::Static { name, qtype, class } => { 203 | info!(%name, ?qtype, %class, "using static gen",); 204 | let query_gen = StaticGen::new(name.clone(), *qtype, *class); 205 | tokio::spawn(async move { 206 | if let Err(err) = sender.run(query_gen).await { 207 | error!(?err, "Error in send task"); 208 | } 209 | }) 210 | } 211 | Source::RandomPkt { size } => { 212 | info!(size, "using randompkt gen"); 213 | let query_gen = RandomPkt::new(*size); 214 | tokio::spawn(async move { 215 | if let Err(err) = sender.run(query_gen).await { 216 | error!(?err, "Error in send task"); 217 | } 218 | }) 219 | } 220 | Source::RandomQName { size, qtype } => { 221 | info!(size, ?qtype, "using randomqname gen"); 222 | let query_gen = RandomQName::new(*qtype, *size); 223 | tokio::spawn(async move { 224 | if let Err(err) = sender.run(query_gen).await { 225 | error!(?err, "Error in send task"); 226 | } 227 | }) 228 | } 229 | }) 230 | } 231 | } 232 | 233 | struct TcpGen; 234 | struct UdpGen; 235 | #[cfg(feature = "doh")] 236 | struct DohGen; 237 | 238 | #[async_trait] 239 | trait BuildGen { 240 | async fn build( 241 | store: Arc>, 242 | atomic_store: Arc, 243 | config: Config, 244 | bucket: Option>, 245 | ) -> Result<(BoxStream>, Sender)>; 246 | } 247 | 248 | type BoxStream = Pin + Send>>; 249 | 250 | #[async_trait] 251 | impl BuildGen for TcpGen { 252 | async fn build( 253 | store: Arc>, 254 | atomic_store: Arc, 255 | config: Config, 256 | bucket: Option>, 257 | ) -> Result<(BoxStream>, Sender)> { 258 | trace!("building TCP generator with target {}", config.target); 259 | // TODO: shutdown ? 260 | let (r, s) = TcpStream::connect(config.target).await?.into_split(); 261 | let reader = FramedRead::new( 262 | r, 263 | TcpDnsCodec { 264 | target: config.target, 265 | }, 266 | ); 267 | let sender = Sender { 268 | s: MsgSend::Tcp { s }, 269 | config, 270 | store, 271 | atomic_store, 272 | bucket, 273 | }; 274 | Ok((Box::pin(reader), sender)) 275 | } 276 | } 277 | 278 | #[async_trait] 279 | impl BuildGen for UdpGen { 280 | async fn build( 281 | store: Arc>, 282 | atomic_store: Arc, 283 | config: Config, 284 | bucket: Option>, 285 | ) -> Result<(BoxStream>, Sender)> { 286 | trace!("building UDP generator"); 287 | // bind to specified addr (will be 0.0.0.0 in most cases) 288 | let r = Arc::new(UdpSocket::bind((config.bind, 0)).await?); 289 | let s = Arc::clone(&r); 290 | 291 | let recv = UdpFramed::new(r, BytesFreezeCodec::new()); 292 | let sender = Sender { 293 | s: MsgSend::Udp { 294 | s, 295 | target: config.target, 296 | }, 297 | config, 298 | store, 299 | atomic_store, 300 | bucket, 301 | }; 302 | Ok((Box::pin(recv), sender)) 303 | } 304 | } 305 | 306 | #[cfg(feature = "doh")] 307 | #[async_trait] 308 | impl BuildGen for DohGen { 309 | async fn build( 310 | store: Arc>, 311 | atomic_store: Arc, 312 | config: Config, 313 | bucket: Option>, 314 | ) -> Result<(BoxStream>, Sender)> { 315 | let (mut doh, tx, resp_rx) = DohSender::new( 316 | config.target, 317 | config 318 | .name_server 319 | .clone() 320 | .context("doh must always have a dns name")?, 321 | ) 322 | .await?; 323 | // TODO: joinhandle & shutdown? 324 | tokio::spawn(async move { 325 | if let Err(err) = doh.run().await { 326 | error!(%err, "error in doh task"); 327 | } 328 | }); 329 | let sender = Sender { 330 | s: MsgSend::Doh { s: tx }, 331 | config, 332 | store, 333 | atomic_store, 334 | bucket, 335 | }; 336 | 337 | Ok((Box::pin(ReceiverStream::new(resp_rx)), sender)) 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /bin/src/logs.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use tracing_appender::non_blocking::WorkerGuard; 3 | use tracing_subscriber::{ 4 | fmt::{ 5 | self, 6 | format::{Format, PrettyFields}, 7 | }, 8 | prelude::__tracing_subscriber_SubscriberExt, 9 | util::SubscriberInitExt, 10 | EnvFilter, 11 | }; 12 | 13 | use crate::args::{Args, LogStructure}; 14 | 15 | // TODO: custom logging output format? 16 | // struct Output; 17 | // impl<'writer> FormatFields<'writer> for Output { 18 | // fn format_fields( 19 | // &self, 20 | // writer: &'writer mut dyn std::fmt::Write, 21 | // fields: R, 22 | // ) -> std::fmt::Result { 23 | // todo!() 24 | // } 25 | // } 26 | 27 | pub fn setup(args: &Args) -> Result> { 28 | let filter_layer = EnvFilter::try_from_default_env() 29 | .or_else(|_| EnvFilter::try_new("info"))? 30 | .add_directive("tokio_util=off".parse()?) 31 | .add_directive("h2=off".parse()?); 32 | let registry = tracing_subscriber::registry().with(filter_layer); 33 | 34 | // this code duplication is unfortunate-- but it looks 35 | // like tracing doesn't support dynamically configured subscribers 36 | Ok(match &args.log_file { 37 | Some(file) => { 38 | let appender = tracing_appender::rolling::never( 39 | file.parent() 40 | .with_context(|| format!("failed to start log on file {:#?}", file))?, 41 | file.file_name() 42 | .with_context(|| format!("failed to start log on file {:#?}", file))?, 43 | ); 44 | 45 | let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender); 46 | 47 | // dual layers will get registry to log to both stdout and file 48 | match args.output { 49 | LogStructure::Pretty => { 50 | registry 51 | .with( 52 | fmt::layer() 53 | .event_format( 54 | Format::default().pretty().with_source_location(false), 55 | ) 56 | .fmt_fields(PrettyFields::new()) 57 | .with_target(false), 58 | ) 59 | .with(fmt::layer().with_writer(non_blocking_appender)) 60 | .init(); 61 | } 62 | LogStructure::Debug => { 63 | registry 64 | .with(fmt::layer()) 65 | .with(fmt::layer().with_writer(non_blocking_appender)) 66 | .init(); 67 | } 68 | LogStructure::Json => { 69 | registry 70 | .with(fmt::layer().json()) 71 | .with(fmt::layer().json().with_writer(non_blocking_appender)) 72 | .init(); 73 | } 74 | } 75 | Some(_guard) 76 | } 77 | None => { 78 | match args.output { 79 | LogStructure::Pretty => { 80 | registry 81 | .with( 82 | fmt::layer() 83 | .event_format(Format::default().with_source_location(false)) 84 | .fmt_fields(PrettyFields::new()) 85 | .with_target(false), 86 | ) 87 | .init(); 88 | } 89 | LogStructure::Debug => { 90 | registry.with(fmt::layer()).init(); 91 | } 92 | LogStructure::Json => { 93 | registry.with(fmt::layer().json()).init(); 94 | } 95 | } 96 | None 97 | } 98 | }) 99 | } 100 | -------------------------------------------------------------------------------- /bin/src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn( 2 | missing_debug_implementations, 3 | // missing_docs, // TODO 4 | rust_2018_idioms, 5 | non_snake_case, 6 | non_upper_case_globals 7 | )] 8 | #![deny(rustdoc::broken_intra_doc_links)] 9 | #![allow(clippy::cognitive_complexity)] 10 | 11 | use std::{ 12 | net::{IpAddr, Ipv4Addr, Ipv6Addr}, 13 | time::Duration, 14 | }; 15 | 16 | use anyhow::{anyhow, bail, Result}; 17 | use clap::Parser; 18 | use tokio::{ 19 | runtime::Builder, 20 | signal, 21 | sync::{broadcast, mpsc}, 22 | time, 23 | }; 24 | use tracing::{error, info, trace}; 25 | 26 | mod args; 27 | mod config; 28 | mod gen; 29 | mod logs; 30 | mod msg; 31 | mod query; 32 | mod sender; 33 | mod shutdown; 34 | mod stats; 35 | mod store; 36 | mod util; 37 | 38 | use crate::{ 39 | args::{Args, Family}, 40 | gen::Generator, 41 | shutdown::Shutdown, 42 | stats::StatsRunner, 43 | }; 44 | 45 | fn main() -> Result<()> { 46 | let mut args = Args::parse(); 47 | // set default address for family if none provided 48 | if args.bind_ip.is_none() { 49 | match args.family { 50 | Family::INet6 => { 51 | args.bind_ip = Some(IpAddr::V6(Ipv6Addr::UNSPECIFIED)); 52 | } 53 | Family::INet => { 54 | args.bind_ip = Some(IpAddr::V4(Ipv4Addr::UNSPECIFIED)); 55 | } 56 | } 57 | } else if let Some(ip) = args.bind_ip { 58 | if ip.is_ipv4() && args.family == Family::INet6 { 59 | bail!("can't bind to ipv4 while in ipv6 mode"); 60 | } else if ip.is_ipv6() && args.family == Family::INet { 61 | bail!("can't bind to ipv6 while in ipv4 mode"); 62 | } 63 | } 64 | 65 | // change default port for doh 66 | #[cfg(feature = "doh")] 67 | if args.protocol.is_doh() && args.port == 53 { 68 | args.port = 443; 69 | } 70 | 71 | let _guard = logs::setup(&args); 72 | 73 | trace!("{:?}", args); 74 | let rt = Builder::new_multi_thread() 75 | .enable_all() 76 | .worker_threads(args.wcount) 77 | .build()?; 78 | trace!(?rt, "tokio runtime created"); 79 | 80 | // shutdown mechanism courtesy of https://github.com/tokio-rs/mini-redis 81 | rt.block_on(async move { 82 | tokio::spawn(async move { 83 | // When the provided `shutdown` future completes, we must send a shutdown 84 | // message to all active connections. We use a broadcast channel for this 85 | // purpose. The call below ignores the receiver of the broadcast pair, and when 86 | // a receiver is needed, the subscribe() method on the sender is used to create 87 | // one. 88 | let (notify_shutdown, _) = broadcast::channel(1); 89 | let (shutdown_complete_tx, shutdown_complete_rx) = mpsc::channel(1); 90 | 91 | let limit_secs = args.limit_secs; 92 | let mut runner = Runner { 93 | args, 94 | notify_shutdown, 95 | shutdown_complete_rx, 96 | shutdown_complete_tx, 97 | }; 98 | tokio::select! { 99 | res = runner.run() => { 100 | if let Err(err) = res { 101 | error!(?err, "nailgun exited with an error"); 102 | } 103 | }, 104 | res = sig() => { 105 | info!("caught signal handler-- exiting"); 106 | if let Err(err) = res { 107 | error!(?err); 108 | } 109 | }, 110 | _ = time::sleep(Duration::from_secs(limit_secs)), if limit_secs != 0 => { 111 | trace!("limit reached-- exiting"); 112 | } 113 | } 114 | let Runner { 115 | mut shutdown_complete_rx, 116 | shutdown_complete_tx, 117 | notify_shutdown, 118 | .. 119 | } = runner; 120 | trace!("sending shutdown signal"); 121 | // When `notify_shutdown` is dropped, all tasks which have `subscribe`d will 122 | // receive the shutdown signal and can exit 123 | drop(notify_shutdown); 124 | // Drop final `Sender` so the `Receiver` below can complete 125 | drop(shutdown_complete_tx); 126 | 127 | // Wait for all active connections to finish processing. As the `Sender` 128 | // handle held by the listener has been dropped above, the only remaining 129 | // `Sender` instances are held by connection handler tasks. When those drop, 130 | // the `mpsc` channel will close and `recv()` will return `None`. 131 | let _ = shutdown_complete_rx.recv().await; 132 | 133 | Ok::<(), anyhow::Error>(()) 134 | }) 135 | .await? 136 | })?; 137 | 138 | Ok(()) 139 | } 140 | 141 | async fn sig() -> Result<()> { 142 | signal::ctrl_c().await.map_err(|err| anyhow!(err)) 143 | } 144 | 145 | #[derive(Debug)] 146 | pub struct Runner { 147 | args: Args, 148 | notify_shutdown: broadcast::Sender<()>, 149 | shutdown_complete_rx: mpsc::Receiver<()>, 150 | shutdown_complete_tx: mpsc::Sender<()>, 151 | } 152 | 153 | impl Runner { 154 | pub async fn run(&mut self) -> Result<()> { 155 | let len = self.args.wcount * self.args.tcount; 156 | let mut handles = Vec::with_capacity(len); 157 | 158 | let (stats_tx, rx) = mpsc::channel(len); 159 | let mut stats = StatsRunner::new(rx, len); 160 | tokio::spawn(async move { stats.run().await }); 161 | let config = self.args.to_config().await?; 162 | trace!(?config); 163 | 164 | for i in 0..len { 165 | let mut gen = Generator { 166 | config: config.clone(), 167 | // Receive shutdown notifications. 168 | shutdown: Shutdown::new(self.notify_shutdown.subscribe()), 169 | // Notifies the receiver half once all clones are 170 | // dropped. 171 | _shutdown_complete: self.shutdown_complete_tx.clone(), 172 | // stats sender 173 | stats_tx: stats_tx.clone(), 174 | }; 175 | trace!( 176 | "spawning generator {} with QPS {}", 177 | i, 178 | gen.config.rate_per_gen() 179 | ); 180 | let handle = tokio::spawn(async move { 181 | if let Err(err) = gen.run().await { 182 | error!(?err, "generator exited with error"); 183 | } 184 | }); 185 | handles.push(handle); 186 | } 187 | 188 | for handle in handles { 189 | handle.await?; 190 | } 191 | Ok(()) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /bin/src/msg.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | 3 | use bytes::Bytes; 4 | use bytes::{Buf, BytesMut}; 5 | use tokio_util::codec::{BytesCodec, Decoder}; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct BufMsg { 9 | buf: Bytes, 10 | addr: SocketAddr, 11 | } 12 | 13 | impl BufMsg { 14 | /// Construct a new `BufMsg` with an address 15 | pub fn new(buf: Bytes, addr: SocketAddr) -> Self { 16 | Self { buf, addr } 17 | } 18 | 19 | /// Return a reference to internal bytes 20 | pub fn bytes(&self) -> &[u8] { 21 | &self.buf 22 | } 23 | 24 | /// Get associated address 25 | #[allow(dead_code)] 26 | pub fn addr(&self) -> SocketAddr { 27 | self.addr 28 | } 29 | 30 | /// Get DNS id from the buffer 31 | pub fn msg_id(&self) -> u16 { 32 | u16::from_be_bytes([self.buf[0], self.buf[1]]) 33 | } 34 | 35 | /// Get response code from byte buffer (not using edns) 36 | pub fn rcode(&self) -> u8 { 37 | 0x0F & u8::from_be_bytes([self.buf[3]]) 38 | } 39 | } 40 | 41 | impl From<(Bytes, SocketAddr)> for BufMsg { 42 | fn from((buf, addr): (Bytes, SocketAddr)) -> Self { 43 | Self { buf, addr } 44 | } 45 | } 46 | 47 | pub struct TcpDnsCodec { 48 | pub target: SocketAddr, 49 | } 50 | 51 | impl Decoder for TcpDnsCodec { 52 | type Item = (Bytes, SocketAddr); 53 | type Error = std::io::Error; 54 | 55 | fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { 56 | if src.len() < 2 { 57 | // Not enough data to read length marker. 58 | return Ok(None); 59 | } 60 | 61 | let mut len_buf = [0u8; 2]; 62 | len_buf.copy_from_slice(&src[..2]); 63 | let length = u16::from_be_bytes(len_buf) as usize; 64 | 65 | if src.len() < 2 + length { 66 | // buffer doesn't have all the data yet, so reserve 67 | // and read more 68 | src.reserve(2 + length - src.len()); 69 | return Ok(None); 70 | } 71 | 72 | src.advance(2); // advance over len 73 | let buf = src.split_to(length).freeze(); 74 | Ok(Some((buf, self.target))) 75 | } 76 | } 77 | 78 | /// BytesCodec that returns frozen Bytes 79 | pub struct BytesFreezeCodec(BytesCodec); 80 | 81 | impl BytesFreezeCodec { 82 | pub fn new() -> Self { 83 | Self(BytesCodec::new()) 84 | } 85 | } 86 | 87 | impl Decoder for BytesFreezeCodec { 88 | type Item = Bytes; 89 | type Error = std::io::Error; 90 | 91 | fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { 92 | Ok(self.0.decode(src)?.map(|b| b.freeze())) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /bin/src/query.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::{BufRead, BufReader, Lines}, 4 | path::PathBuf, 5 | str::FromStr, 6 | }; 7 | 8 | use anyhow::Result; 9 | use rand::Rng; 10 | use tracing::error; 11 | use trust_dns_proto::{ 12 | op::{Edns, Message, MessageType, Query}, 13 | rr::{DNSClass, Name, RecordType}, 14 | }; 15 | 16 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 17 | pub enum Source { 18 | File(PathBuf), 19 | Static { 20 | name: Name, 21 | qtype: RecordType, 22 | class: DNSClass, 23 | }, 24 | RandomPkt { 25 | size: usize, 26 | }, 27 | RandomQName { 28 | qtype: RecordType, 29 | size: usize, 30 | }, 31 | } 32 | 33 | pub trait QueryGen { 34 | fn next_msg(&mut self, id: u16) -> Option>; 35 | } 36 | 37 | #[derive(Debug)] 38 | pub struct FileGen { 39 | rdr: Lines>, 40 | // could reuse buf so we don't allocate a new string for each Message 41 | // buf: String, 42 | } 43 | 44 | impl FileGen { 45 | pub fn new(path: impl Into) -> Result { 46 | let path = path.into(); 47 | let rdr = BufReader::new(File::open(path)?).lines(); 48 | Ok(Self { rdr }) 49 | } 50 | } 51 | 52 | impl QueryGen for FileGen { 53 | // TODO: do we just exit when we run out of things to send? 54 | fn next_msg(&mut self, id: u16) -> Option> { 55 | let line = self 56 | .rdr 57 | .next()? 58 | .expect("FileGen encountered an error reading line"); 59 | let mut next = line.split_ascii_whitespace(); 60 | let name = Name::from_ascii(next.next()?) 61 | .expect("FileGen encountered an error parsing Name from line"); 62 | let qtype = RecordType::from_str(next.next()?) 63 | .expect("FileGen encountered an error parsing RecordType from line"); 64 | let mut msg = Message::new(); 65 | msg.set_id(id) 66 | .add_query(Query::query(name, qtype)) 67 | .set_message_type(MessageType::Query) 68 | .set_recursion_desired(true) 69 | .set_edns(Edns::new()); 70 | 71 | match msg.to_vec() { 72 | Ok(msg) => Some(msg), 73 | Err(err) => { 74 | error!(?err); 75 | None 76 | } 77 | } 78 | } 79 | } 80 | 81 | #[derive(Debug, Clone, PartialEq, Eq)] 82 | pub struct StaticGen { 83 | name: Name, 84 | qtype: RecordType, 85 | class: DNSClass, 86 | } 87 | 88 | impl QueryGen for StaticGen { 89 | /// generate a simple query using a given id, record and qtype 90 | fn next_msg(&mut self, id: u16) -> Option> { 91 | let mut msg = Message::new(); 92 | let mut query = Query::query(self.name.clone(), self.qtype); 93 | if self.class != DNSClass::IN { 94 | query.set_query_class(self.class); 95 | } 96 | msg.set_id(id) 97 | .add_query(query) 98 | .set_message_type(MessageType::Query) 99 | .set_recursion_desired(true) 100 | .set_edns(Edns::new()); 101 | match msg.to_vec() { 102 | Ok(msg) => Some(msg), 103 | Err(err) => { 104 | error!(?err); 105 | None 106 | } 107 | } 108 | } 109 | } 110 | 111 | impl StaticGen { 112 | pub fn new(name: Name, qtype: RecordType, class: DNSClass) -> Self { 113 | Self { name, qtype, class } 114 | } 115 | } 116 | 117 | #[derive(Debug, Clone, PartialEq, Eq)] 118 | pub struct RandomPkt { 119 | len: usize, 120 | } 121 | 122 | impl QueryGen for RandomPkt { 123 | /// generate a random message 124 | fn next_msg(&mut self, id: u16) -> Option> { 125 | let mut msg = id.to_be_bytes().to_vec(); 126 | msg.extend((0..self.len).map(|_| rand::random::())); 127 | Some(msg) 128 | } 129 | } 130 | 131 | impl RandomPkt { 132 | pub fn new(len: usize) -> Self { 133 | Self { len } 134 | } 135 | } 136 | 137 | #[derive(Debug, Clone, PartialEq, Eq)] 138 | pub struct RandomQName { 139 | len: usize, 140 | qtype: RecordType, 141 | } 142 | 143 | impl QueryGen for RandomQName { 144 | /// generate a random label only 145 | fn next_msg(&mut self, id: u16) -> Option> { 146 | use rand::distributions::Alphanumeric; 147 | let gen_label = |len| { 148 | let rng = rand::thread_rng(); 149 | rng.sample_iter(Alphanumeric) 150 | .map(char::from) 151 | .take(len) 152 | .collect::() 153 | }; 154 | 155 | let qname = match Name::from_str(&gen_label(self.len)) { 156 | Ok(qname) => Some(qname), 157 | Err(err) => { 158 | error!(?err); 159 | None 160 | } 161 | }?; 162 | 163 | let mut msg = Message::new(); 164 | msg.set_id(id) 165 | .add_query(Query::query(qname, self.qtype)) 166 | .set_message_type(MessageType::Query) 167 | .set_recursion_desired(true) 168 | .set_edns(Edns::new()); 169 | 170 | match msg.to_vec() { 171 | Ok(msg) => Some(msg), 172 | Err(err) => { 173 | error!(?err); 174 | None 175 | } 176 | } 177 | } 178 | } 179 | 180 | impl RandomQName { 181 | pub fn new(qtype: RecordType, len: usize) -> Self { 182 | Self { qtype, len } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /bin/src/sender.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | net::SocketAddr, 4 | sync::{atomic, Arc}, 5 | time::Instant, 6 | }; 7 | 8 | use anyhow::{Context, Result}; 9 | use governor::{ 10 | clock::DefaultClock, 11 | state::{InMemoryState, NotKeyed}, 12 | RateLimiter, 13 | }; 14 | use parking_lot::Mutex; 15 | use tokio::{ 16 | io::AsyncWriteExt, 17 | net::{tcp::OwnedWriteHalf, UdpSocket}, 18 | sync::mpsc, 19 | task, 20 | }; 21 | 22 | use crate::{ 23 | config::Config, 24 | query::QueryGen, 25 | store::{AtomicStore, QueryInfo, Store}, 26 | }; 27 | 28 | pub struct Sender { 29 | pub config: Config, 30 | pub s: MsgSend, 31 | pub store: Arc>, 32 | pub atomic_store: Arc, 33 | pub bucket: Option>, 34 | } 35 | 36 | impl Sender { 37 | pub async fn run(&mut self, mut query_gen: T) -> Result<()> { 38 | loop { 39 | let num = self.config.batch_size(); 40 | task::yield_now().await; 41 | let ids = { 42 | let mut store = self.store.lock(); 43 | (0..num) 44 | .flat_map(|_| match store.ids.pop_front() { 45 | Some(id) if !store.in_flight.contains_key(&id) => Some(id), 46 | _ => None, 47 | }) 48 | .collect::>() 49 | }; 50 | for next_id in ids { 51 | if let Some(bucket) = &self.bucket { 52 | bucket.until_ready().await; 53 | } 54 | let msg = query_gen 55 | .next_msg(next_id) 56 | .context("query gen ran out of msgs")?; 57 | // let msg = query::simple(next_id, self.config.record.clone(), self.config.qtype) 58 | // .to_vec()?; 59 | // TODO: better way to do this that locks less? 60 | { 61 | self.store.lock().in_flight.insert( 62 | next_id, 63 | QueryInfo { 64 | sent: Instant::now(), 65 | len: msg.len(), 66 | }, 67 | ); 68 | } 69 | // could be done with an async trait and Sender 70 | // but this just seems easier for now 71 | self.s.send(msg).await?; 72 | self.atomic_store 73 | .sent 74 | .fetch_add(1, atomic::Ordering::Relaxed); 75 | } 76 | } 77 | } 78 | } 79 | 80 | // a very simple DNS message sender 81 | pub enum MsgSend { 82 | Tcp { 83 | s: OwnedWriteHalf, 84 | }, 85 | Udp { 86 | s: Arc, 87 | target: SocketAddr, 88 | }, 89 | Doh { 90 | s: mpsc::Sender>, 91 | }, 92 | } 93 | 94 | impl MsgSend { 95 | async fn send(&mut self, msg: Vec) -> Result<()> { 96 | match self { 97 | MsgSend::Tcp { s } => { 98 | // write the message out 99 | s.write_u16(msg.len() as u16).await?; 100 | s.write_all(&msg[..]).await?; 101 | } 102 | MsgSend::Udp { s, target } => { 103 | s.send_to(&msg[..], *target).await?; 104 | } 105 | MsgSend::Doh { s } => { 106 | s.send(msg).await?; 107 | } 108 | } 109 | Ok(()) 110 | } 111 | } 112 | 113 | #[cfg(feature = "doh")] 114 | pub mod doh { 115 | use super::*; 116 | 117 | use bytes::Bytes; 118 | use rustls::{ClientConfig, KeyLogFile, OwnedTrustAnchor, RootCertStore}; 119 | use tokio::net::TcpStream as TokioTcpStream; 120 | use trust_dns_proto::iocompat::AsyncIoTokioAsStd; 121 | use trust_dns_proto::{ 122 | https::{HttpsClientStream, HttpsClientStreamBuilder}, 123 | xfer::FirstAnswer, 124 | }; 125 | 126 | fn client_config_tls12_webpki_roots() -> ClientConfig { 127 | let mut root_store = RootCertStore::empty(); 128 | root_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| { 129 | OwnedTrustAnchor::from_subject_spki_name_constraints( 130 | ta.subject, 131 | ta.spki, 132 | ta.name_constraints, 133 | ) 134 | })); 135 | 136 | let mut client_config = ClientConfig::builder() 137 | .with_safe_default_cipher_suites() 138 | .with_safe_default_kx_groups() 139 | .with_protocol_versions(&[&rustls::version::TLS12]) 140 | .unwrap() 141 | .with_root_certificates(root_store) 142 | .with_no_client_auth(); 143 | 144 | client_config.alpn_protocols = vec![b"h2".to_vec()]; 145 | client_config 146 | } 147 | 148 | pub struct DohSender { 149 | rx: mpsc::Receiver>, 150 | stream: HttpsClientStream, 151 | addr: SocketAddr, 152 | resp_tx: mpsc::Sender>, 153 | } 154 | 155 | impl DohSender { 156 | pub async fn new( 157 | target: SocketAddr, 158 | name_server: impl Into, 159 | ) -> Result<( 160 | Self, 161 | mpsc::Sender>, 162 | mpsc::Receiver>, 163 | )> { 164 | let (tx, rx) = mpsc::channel(100_000); 165 | let (resp_tx, resp_rx) = mpsc::channel(100_000); 166 | 167 | let mut client_config = client_config_tls12_webpki_roots(); 168 | client_config.key_log = Arc::new(KeyLogFile::new()); 169 | 170 | let https_builder = 171 | HttpsClientStreamBuilder::with_client_config(Arc::new(client_config)); 172 | let stream = https_builder 173 | .build::>(target, name_server.into()) 174 | .await?; 175 | 176 | Ok(( 177 | Self { 178 | rx, 179 | resp_tx, 180 | stream, 181 | addr: target, 182 | }, 183 | tx, 184 | resp_rx, 185 | )) 186 | } 187 | pub async fn run(&mut self) -> Result<()> { 188 | while let Some(msg) = self.rx.recv().await { 189 | let stream = self.stream.send_bytes(msg); 190 | let tx = self.resp_tx.clone(); 191 | // TODO: shutdown? 192 | let addr = self.addr; 193 | tokio::spawn(async move { 194 | let resp = stream.first_answer().await; 195 | if let Ok(resp) = resp { 196 | tx.send(Ok((resp.freeze(), addr))) 197 | .await 198 | .context("failed to send DOH response to gen")?; 199 | } 200 | Ok::<_, anyhow::Error>(()) 201 | }); 202 | } 203 | Ok(()) 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /bin/src/shutdown.rs: -------------------------------------------------------------------------------- 1 | use tokio::sync::broadcast; 2 | 3 | // shutdown code from https://github.com/tokio-rs/mini-redis 4 | /// Listens for the server shutdown signal. 5 | /// 6 | /// Shutdown is signalled using a `broadcast::Receiver`. Only a single value is 7 | /// ever sent. Once a value has been sent via the broadcast channel, the server 8 | /// should shutdown. 9 | /// 10 | /// The `Shutdown` struct listens for the signal and tracks that the signal has 11 | /// been received. Callers may query for whether the shutdown signal has been 12 | /// received or not. 13 | #[derive(Debug)] 14 | pub struct Shutdown { 15 | /// `true` if the shutdown signal has been received 16 | shutdown: bool, 17 | 18 | /// The receive half of the channel used to listen for shutdown. 19 | notify: broadcast::Receiver<()>, 20 | } 21 | 22 | impl Shutdown { 23 | /// Create a new `Shutdown` backed by the given `broadcast::Receiver`. 24 | pub(crate) fn new(notify: broadcast::Receiver<()>) -> Shutdown { 25 | Shutdown { 26 | shutdown: false, 27 | notify, 28 | } 29 | } 30 | 31 | /// Returns `true` if the shutdown signal has been received. 32 | pub(crate) fn is_shutdown(&self) -> bool { 33 | self.shutdown 34 | } 35 | 36 | /// Receive the shutdown notice, waiting if necessary. 37 | pub(crate) async fn recv(&mut self) { 38 | // If the shutdown signal has already been received, then return 39 | // immediately. 40 | if self.shutdown { 41 | return; 42 | } 43 | 44 | // Cannot receive a "lag error" as only one value is ever sent. 45 | let _ = self.notify.recv().await; 46 | 47 | // Remember that the signal has been received. 48 | self.shutdown = true; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /bin/src/stats.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | fmt::Display, 4 | io, 5 | sync::{atomic, Arc}, 6 | time::{Duration, Instant}, 7 | }; 8 | 9 | use anyhow::Result; 10 | use rustc_hash::FxHashMap; 11 | use tokio::sync::mpsc; 12 | use tracing::info; 13 | use trust_dns_proto::op::ResponseCode; 14 | 15 | use crate::{ 16 | msg::BufMsg, 17 | store::{AtomicStore, QueryInfo}, 18 | util::MovingAvg, 19 | }; 20 | 21 | #[derive(Debug)] 22 | pub struct StatsTracker { 23 | intervals: usize, 24 | pub recv: u64, 25 | ok_recv: u64, 26 | pub atomic_store: Arc, 27 | total_latency: Duration, 28 | min_latency: Duration, 29 | max_latency: Duration, 30 | rcodes: FxHashMap, 31 | recv_buf_size: usize, 32 | buf_size: usize, 33 | } 34 | 35 | impl Default for StatsTracker { 36 | fn default() -> Self { 37 | Self { 38 | total_latency: Duration::from_micros(0), 39 | min_latency: Duration::from_micros(u64::MAX), 40 | max_latency: Duration::from_micros(0), 41 | recv: 0, 42 | ok_recv: 0, 43 | atomic_store: Arc::new(AtomicStore::default()), 44 | rcodes: FxHashMap::default(), 45 | recv_buf_size: 0, 46 | buf_size: 0, 47 | intervals: 0, 48 | } 49 | } 50 | } 51 | 52 | impl StatsTracker { 53 | pub fn reset(&mut self) { 54 | self.min_latency = Duration::from_micros(u64::MAX); 55 | self.max_latency = Duration::from_micros(0); 56 | self.total_latency = Duration::from_micros(0); 57 | self.recv = 0; 58 | self.ok_recv = 0; 59 | self.recv_buf_size = 0; 60 | self.buf_size = 0; 61 | self.atomic_store.reset(); 62 | } 63 | 64 | pub fn update(&mut self, qinfo: QueryInfo, msg: &BufMsg) { 65 | if let Some(latency) = Instant::now().checked_duration_since(qinfo.sent) { 66 | self.total_latency += latency; 67 | self.min_latency = self.min_latency.min(latency); 68 | self.max_latency = self.max_latency.max(latency); 69 | } 70 | *self.rcodes.entry(msg.rcode()).or_insert(0) += 1; 71 | self.buf_size += qinfo.len; 72 | self.recv_buf_size += msg.bytes().len(); 73 | self.ok_recv += 1; 74 | } 75 | 76 | pub fn interval( 77 | &mut self, 78 | elapsed: Duration, 79 | total_duration: Duration, 80 | in_flight: usize, 81 | // not currently used, but we could log how many available ids there are 82 | _ids_len: usize, 83 | ) -> StatsInterval { 84 | let timeouts = self.atomic_store.timed_out.load(atomic::Ordering::Relaxed); 85 | let sent = self.atomic_store.sent.load(atomic::Ordering::Relaxed); 86 | let interval = StatsInterval { 87 | interval: self.intervals, 88 | duration: total_duration, 89 | elapsed, 90 | sent, 91 | recv: self.recv, 92 | ok_recv: self.ok_recv, 93 | timeouts, 94 | min_latency: self.min_latency, 95 | total_latency: self.total_latency, 96 | max_latency: self.max_latency, 97 | recv_buf_size: self.recv_buf_size, 98 | buf_size: self.buf_size, 99 | rcodes: std::mem::take(&mut self.rcodes), 100 | in_flight, 101 | }; 102 | self.intervals += 1; 103 | self.reset(); 104 | 105 | interval 106 | } 107 | } 108 | 109 | #[derive(Debug)] 110 | pub struct StatsInterval { 111 | duration: Duration, 112 | elapsed: Duration, 113 | interval: usize, 114 | sent: u64, 115 | recv: u64, 116 | ok_recv: u64, 117 | timeouts: u64, 118 | min_latency: Duration, 119 | total_latency: Duration, 120 | max_latency: Duration, 121 | rcodes: FxHashMap, 122 | recv_buf_size: usize, 123 | buf_size: usize, 124 | in_flight: usize, 125 | } 126 | 127 | impl Default for StatsInterval { 128 | fn default() -> Self { 129 | Self { 130 | duration: Duration::from_micros(0), 131 | total_latency: Duration::from_micros(0), 132 | min_latency: Duration::from_micros(u64::MAX), 133 | max_latency: Duration::from_micros(0), 134 | interval: 0, 135 | sent: 0, 136 | recv: 0, 137 | ok_recv: 0, 138 | rcodes: FxHashMap::default(), 139 | buf_size: 0, 140 | recv_buf_size: 0, 141 | elapsed: Duration::from_micros(0), 142 | timeouts: 0, 143 | in_flight: 0, 144 | } 145 | } 146 | } 147 | 148 | impl StatsInterval { 149 | fn min_latency(&self) -> f32 { 150 | if self.min_latency.as_micros() == u64::max_value() as u128 { 151 | 0. 152 | } else { 153 | self.min_latency.as_micros() as f32 / 1_000. 154 | } 155 | } 156 | fn max_latency(&self) -> f32 { 157 | self.max_latency.as_micros() as f32 / 1_000. 158 | } 159 | fn avg_latency(&self) -> f32 { 160 | if self.ok_recv != 0 { 161 | (self.total_latency.as_micros() / self.ok_recv as u128) as f32 / 1_000. 162 | } else { 163 | self.total_latency.as_micros() as f32 / 1_000. 164 | } 165 | } 166 | fn avg_latency_str(&self) -> Cow<'_, str> { 167 | if self.ok_recv != 0 { 168 | Cow::Owned(self.avg_latency().to_string()) 169 | } else { 170 | Cow::Borrowed("-") 171 | } 172 | } 173 | fn recv_avg_size(&self) -> usize { 174 | if self.ok_recv != 0 { 175 | self.recv_buf_size / self.ok_recv as usize 176 | } else { 177 | self.recv_buf_size 178 | } 179 | } 180 | fn avg_size(&self) -> usize { 181 | if self.sent != 0 { 182 | self.buf_size / self.sent as usize 183 | } else { 184 | self.buf_size 185 | } 186 | } 187 | 188 | pub fn aggregate(&mut self, agg: StatsInterval) { 189 | // destructuring like this makes sure we didn't miss any 190 | let Self { 191 | duration, 192 | elapsed, 193 | interval, 194 | sent, 195 | recv, 196 | ok_recv, 197 | timeouts, 198 | min_latency, 199 | total_latency, 200 | max_latency, 201 | rcodes, 202 | recv_buf_size, 203 | buf_size, 204 | in_flight, 205 | } = self; 206 | *interval = agg.interval; 207 | *duration = agg.duration; 208 | *elapsed += agg.elapsed; 209 | *sent += agg.sent; 210 | *recv += agg.recv; 211 | *ok_recv += agg.ok_recv; 212 | *timeouts += agg.timeouts; 213 | *min_latency = agg.min_latency.min(*min_latency); 214 | *max_latency = agg.max_latency.max(*max_latency); 215 | *total_latency += agg.total_latency; 216 | for (rcode, count) in agg.rcodes.into_iter() { 217 | *rcodes.entry(rcode).or_insert(0) += count; 218 | } 219 | *recv_buf_size += agg.recv_buf_size; 220 | *buf_size += agg.buf_size; 221 | *in_flight += agg.in_flight; 222 | } 223 | pub fn reset(&mut self) { 224 | self.min_latency = Duration::from_micros(u64::MAX); 225 | self.max_latency = Duration::from_micros(0); 226 | self.total_latency = Duration::from_micros(0); 227 | self.recv = 0; 228 | self.ok_recv = 0; 229 | self.recv_buf_size = 0; 230 | self.buf_size = 0; 231 | self.sent = 0; 232 | self.in_flight = 0; 233 | self.timeouts = 0; 234 | } 235 | 236 | pub fn log(&mut self) -> StatsInterval { 237 | info!( 238 | // elapsed = &PrettyDuration(elapsed), 239 | duration = %PrettyDuration(self.duration), 240 | send = self.sent, 241 | recv = self.recv, 242 | to = self.timeouts, 243 | min = %format!("{}ms", self.min_latency()), 244 | avg = %format!("{}ms", self.avg_latency_str()), 245 | max = %format!("{}ms", self.max_latency()), 246 | avg_size = self.avg_size(), 247 | recv_avg_size = self.recv_avg_size(), 248 | in_flight = self.in_flight, 249 | ); 250 | let interval = StatsInterval { 251 | interval: self.interval, 252 | duration: self.duration, 253 | elapsed: self.elapsed, 254 | sent: self.sent, 255 | recv: self.recv, 256 | ok_recv: self.ok_recv, 257 | timeouts: self.timeouts, 258 | min_latency: self.min_latency, 259 | total_latency: self.total_latency, 260 | max_latency: self.max_latency, 261 | recv_buf_size: self.recv_buf_size, 262 | buf_size: self.buf_size, 263 | rcodes: std::mem::take(&mut self.rcodes), 264 | in_flight: self.in_flight, 265 | }; 266 | self.reset(); 267 | interval 268 | } 269 | } 270 | 271 | #[derive(Debug)] 272 | pub struct StatsTotals { 273 | duration: Duration, 274 | elapsed: Duration, 275 | sent: u64, 276 | recv: u64, 277 | ok_recv: u64, 278 | timeouts: u64, 279 | min_latency: f32, 280 | avg_latency: MovingAvg, 281 | max_latency: f32, 282 | rcodes: FxHashMap, 283 | avg_recv_buf_size: MovingAvg, 284 | avg_buf_size: MovingAvg, 285 | in_flight: usize, 286 | } 287 | 288 | impl Default for StatsTotals { 289 | fn default() -> Self { 290 | Self { 291 | duration: Duration::from_micros(0), 292 | elapsed: Duration::from_micros(0), 293 | avg_latency: MovingAvg::new(0.), 294 | min_latency: f32::MAX, 295 | max_latency: 0., 296 | sent: 0, 297 | recv: 0, 298 | ok_recv: 0, 299 | rcodes: FxHashMap::default(), 300 | avg_buf_size: MovingAvg::new(0), 301 | avg_recv_buf_size: MovingAvg::new(0), 302 | timeouts: 0, 303 | in_flight: 0, 304 | } 305 | } 306 | } 307 | 308 | impl StatsTotals { 309 | pub fn update(&mut self, interval: StatsInterval) { 310 | self.duration = interval.duration; 311 | self.elapsed += interval.elapsed; 312 | self.sent += interval.sent; 313 | self.recv += interval.recv; 314 | self.ok_recv += interval.ok_recv; 315 | self.timeouts += interval.timeouts; 316 | self.in_flight += interval.in_flight; 317 | self.min_latency = if interval.min_latency() != 0. { 318 | interval.min_latency().min(self.min_latency) 319 | } else { 320 | self.min_latency 321 | }; 322 | self.max_latency = interval.max_latency().max(self.max_latency); 323 | if interval.avg_latency() != 0. { 324 | self.avg_latency.next(interval.avg_latency()); 325 | } 326 | if interval.buf_size != 0 && interval.sent != 0 { 327 | let avg = interval.buf_size / interval.sent as usize; 328 | self.avg_buf_size.next(avg); 329 | } 330 | if interval.recv_buf_size != 0 && interval.ok_recv != 0 { 331 | let avg = interval.recv_buf_size / interval.ok_recv as usize; 332 | self.avg_recv_buf_size.next(avg); 333 | } 334 | for (rcode, count) in interval.rcodes.into_iter() { 335 | *self.rcodes.entry(rcode).or_insert(0) += count; 336 | } 337 | } 338 | pub fn summary(&self) -> Result<()> { 339 | use std::io::Write; 340 | let runtime = PrettyDuration(self.duration); 341 | let to_percent = if self.sent == 0 || self.timeouts == 0 { 342 | 0. 343 | } else { 344 | (self.timeouts as f32 / self.sent as f32) * 100. 345 | }; 346 | info!( 347 | runtime = %runtime, 348 | total_sent = self.sent, 349 | total_rcvd = self.recv, 350 | min = %format!("{}ms", self.min_latency), 351 | avg = %format!("{}ms", self.avg_latency), 352 | max = %format!("{}ms", self.max_latency), 353 | sent_avg_size = %format!("{} bytes", self.avg_buf_size), 354 | rcvd_avg_size = %format!("{} bytes", self.avg_recv_buf_size), 355 | timeouts = %format!("{} ({}%)", self.timeouts, to_percent) 356 | ); 357 | // TODO: print this better? 358 | for (rcode, count) in self.rcodes.iter() { 359 | info!(responses = %format!("{:?}: {}", ResponseCode::from(0, *rcode), *count)); 360 | } 361 | // write it to stdout nicely 362 | let stdout = io::stdout(); 363 | let mut f = stdout.lock(); 364 | writeln!(f, "-----RESULTS-----")?; 365 | writeln!(f, "runtime {}", runtime)?; 366 | writeln!(f, "total sent {}", self.sent)?; 367 | writeln!(f, "total rcvd {}", self.recv)?; 368 | writeln!(f, "min latency {}ms", self.min_latency)?; 369 | writeln!(f, "avg latency {}ms", self.avg_latency)?; 370 | writeln!(f, "max latency {}ms", self.max_latency)?; 371 | writeln!(f, "sent avg size {} bytes", self.avg_buf_size)?; 372 | writeln!(f, "rcvd avg size {} bytes", self.avg_recv_buf_size)?; 373 | writeln!(f, "timeouts {} ({}%)", self.timeouts, to_percent)?; 374 | writeln!(f, "responses")?; 375 | for (rcode, count) in self.rcodes.iter() { 376 | writeln!(f, " {:?} {}", ResponseCode::from(0, *rcode), *count)?; 377 | } 378 | Ok(()) 379 | } 380 | } 381 | 382 | #[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] 383 | struct PrettyDuration(Duration); 384 | 385 | impl Display for PrettyDuration { 386 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 387 | let secs = self.0.as_secs_f32().to_string(); 388 | write!(f, "{}s", &secs[0..secs.len().min(5)]) 389 | } 390 | } 391 | 392 | #[derive(Debug)] 393 | pub struct StatsRunner { 394 | rx: mpsc::Receiver, 395 | len: u32, 396 | summary: StatsInterval, 397 | totals: StatsTotals, 398 | } 399 | 400 | impl StatsRunner { 401 | pub fn new(rx: mpsc::Receiver, len: usize) -> Self { 402 | Self { 403 | rx, 404 | len: len as u32, 405 | summary: StatsInterval::default(), 406 | totals: StatsTotals::default(), 407 | } 408 | } 409 | pub async fn run(&mut self) -> Result<()> { 410 | let mut generators = 0; 411 | while let Some(interval) = self.rx.recv().await { 412 | generators += 1; 413 | self.summary.aggregate(interval); 414 | // we've gathered all the generators' stats for this interval, so log 415 | if generators == self.len { 416 | // add this interval to the totals 417 | self.totals.update(self.summary.log()); 418 | generators = 0; 419 | } 420 | // recv returns None when all senders are dropped, 421 | // meaning our run is done 422 | } 423 | self.totals.summary()?; 424 | Ok(()) 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /bin/src/store.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::VecDeque, 3 | sync::{ 4 | atomic::{self, AtomicU64}, 5 | Arc, 6 | }, 7 | time::{Duration, Instant}, 8 | }; 9 | 10 | use rand::prelude::SliceRandom; 11 | use rustc_hash::FxHashMap; 12 | 13 | #[derive(Debug)] 14 | pub struct QueryInfo { 15 | pub sent: Instant, 16 | pub len: usize, 17 | } 18 | 19 | #[derive(Debug)] 20 | pub struct Store { 21 | pub ids: VecDeque, 22 | pub in_flight: FxHashMap, 23 | } 24 | 25 | impl Store { 26 | pub fn new() -> Self { 27 | Self { 28 | in_flight: FxHashMap::default(), 29 | ids: create_and_shuffle(), 30 | } 31 | } 32 | pub fn clear_timeouts(&mut self, timeout: Duration, atomic_store: &Arc) { 33 | let now = Instant::now(); 34 | let mut ids = Vec::new(); 35 | // remove all timed out ids from in_flight 36 | self.in_flight.retain(|id, info| { 37 | if now - info.sent >= timeout { 38 | ids.push(*id); 39 | false 40 | } else { 41 | true 42 | } 43 | }); 44 | atomic_store 45 | .timed_out 46 | .fetch_add(ids.len() as u64, atomic::Ordering::Relaxed); 47 | 48 | // add back the ids so they can be used 49 | self.ids.extend(ids); 50 | } 51 | } 52 | 53 | #[derive(Debug, Default)] 54 | pub struct AtomicStore { 55 | pub sent: AtomicU64, 56 | pub timed_out: AtomicU64, 57 | } 58 | 59 | impl AtomicStore { 60 | pub fn reset(&self) { 61 | self.sent.store(0, atomic::Ordering::Relaxed); 62 | self.timed_out.store(0, atomic::Ordering::Relaxed); 63 | } 64 | } 65 | 66 | // create a stack array of random u16's 67 | fn create_and_shuffle() -> VecDeque { 68 | let mut data: Vec = (0..u16::max_value()).collect(); 69 | data.shuffle(&mut rand::thread_rng()); 70 | VecDeque::from(data) 71 | } 72 | -------------------------------------------------------------------------------- /bin/src/util.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use num_traits::{AsPrimitive, Num}; 4 | 5 | pub struct MovingAvg { 6 | val: T, 7 | interval: usize, 8 | } 9 | 10 | impl MovingAvg { 11 | pub fn new(val: T) -> Self { 12 | Self { val, interval: 0 } 13 | } 14 | } 15 | 16 | impl MovingAvg 17 | where 18 | T: Num + AsPrimitive, 19 | usize: AsPrimitive, 20 | { 21 | pub fn next(&mut self, val: T) -> T { 22 | self.interval += 1; 23 | self.val = (self.val * (self.interval - 1).as_() + val) / (self.interval).as_(); 24 | self.val 25 | } 26 | } 27 | 28 | impl fmt::Display for MovingAvg { 29 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 30 | write!(f, "{}", self.val) 31 | } 32 | } 33 | 34 | impl fmt::Debug for MovingAvg { 35 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 36 | write!(f, "{:?}", self.val) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.58.0 -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | unstable_features = true 2 | merge_imports = true 3 | merge_derives = true 4 | reorder_impl_items = true 5 | use_field_init_shorthand = true 6 | use_try_shorthand = true 7 | wrap_comments = true 8 | --------------------------------------------------------------------------------