From 79fb8f3787674dfc91320029e9b5945dd08ff515 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Tue, 22 Mar 2022 19:25:52 -0400 Subject: [PATCH 1/3] Add internal-dns service --- Cargo.lock | 501 ++++++++++++++++++++++++++-- Cargo.toml | 2 + internal-dns-client/Cargo.toml | 12 + internal-dns-client/src/lib.rs | 18 + internal-dns/Cargo.toml | 36 ++ internal-dns/src/bin/apigen.rs | 27 ++ internal-dns/src/bin/dns-server.rs | 54 +++ internal-dns/src/dns_data.rs | 356 ++++++++++++++++++++ internal-dns/src/dns_server.rs | 185 ++++++++++ internal-dns/src/dropshot_server.rs | 73 ++++ internal-dns/src/lib.rs | 47 +++ internal-dns/tests/basic_test.rs | 188 +++++++++++ internal-dns/tests/openapi_test.rs | 27 ++ openapi/internal-dns.json | 237 +++++++++++++ 14 files changed, 1736 insertions(+), 27 deletions(-) create mode 100644 internal-dns-client/Cargo.toml create mode 100644 internal-dns-client/src/lib.rs create mode 100644 internal-dns/Cargo.toml create mode 100644 internal-dns/src/bin/apigen.rs create mode 100644 internal-dns/src/bin/dns-server.rs create mode 100644 internal-dns/src/dns_data.rs create mode 100644 internal-dns/src/dns_server.rs create mode 100644 internal-dns/src/dropshot_server.rs create mode 100644 internal-dns/src/lib.rs create mode 100644 internal-dns/tests/basic_test.rs create mode 100644 internal-dns/tests/openapi_test.rs create mode 100644 openapi/internal-dns.json diff --git a/Cargo.lock b/Cargo.lock index 28d12a1b65a..ff90b29d91e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,12 @@ dependencies = [ "syn", ] +[[package]] +name = "arc-swap" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d78ce20460b82d3fa150275ed9d55e21064fc7951177baacf86a145c4a4b1f" + [[package]] name = "array-init" version = "2.0.0" @@ -127,7 +133,7 @@ dependencies = [ "getrandom", "instant", "pin-project", - "rand", + "rand 0.8.5", "tokio", ] @@ -562,7 +568,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83bd3bb4314701c568e340cd8cf78c975aa0ca79e03d3f6d1677d5b0c9c0c03" dependencies = [ "generic-array 0.14.5", - "rand_core", + "rand_core 0.6.3", "subtle", "zeroize", ] @@ -644,6 +650,12 @@ dependencies = [ "syn", ] +[[package]] +name = "data-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" + [[package]] name = "db-macros" version = "0.1.0" @@ -801,7 +813,45 @@ dependencies = [ "base64", "bytes", "chrono", - "dropshot_endpoint", + "dropshot_endpoint 0.6.1-dev (git+https://github.com/oxidecomputer/dropshot?branch=main)", + "futures", + "hostname", + "http", + "hyper", + "indexmap", + "openapiv3", + "paste", + "percent-encoding", + "proc-macro2", + "rustls", + "rustls-pemfile", + "schemars", + "serde", + "serde_json", + "serde_urlencoded", + "slog", + "slog-async", + "slog-bunyan", + "slog-json", + "slog-term", + "tokio", + "tokio-rustls", + "toml", + "usdt 0.3.2", + "uuid", +] + +[[package]] +name = "dropshot" +version = "0.6.1-dev" +source = "git+https://github.com/oxidecomputer/dropshot#da1d2db1411e1edbbe0101cc1db855606e8dabfc" +dependencies = [ + "async-stream", + "async-trait", + "base64", + "bytes", + "chrono", + "dropshot_endpoint 0.6.1-dev (git+https://github.com/oxidecomputer/dropshot)", "futures", "hostname", "http", @@ -841,6 +891,18 @@ dependencies = [ "syn", ] +[[package]] +name = "dropshot_endpoint" +version = "0.6.1-dev" +source = "git+https://github.com/oxidecomputer/dropshot#da1d2db1411e1edbbe0101cc1db855606e8dabfc" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_tokenstream", + "syn", +] + [[package]] name = "dtrace-parser" version = "0.1.12" @@ -887,7 +949,7 @@ dependencies = [ "generic-array 0.14.5", "group", "pkcs8", - "rand_core", + "rand_core 0.6.3", "subtle", "zeroize", ] @@ -916,6 +978,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "enum-as-inner" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73" +dependencies = [ + "heck 0.4.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_logger" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "expectorate" version = "1.0.4" @@ -954,7 +1047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f" dependencies = [ "bitvec", - "rand_core", + "rand_core 0.6.3", "subtle", ] @@ -1040,6 +1133,22 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da1b8f89c5b5a5b7e59405cfcf0bb9588e5ed19f0b57a4cd542bbba3f164a6d" +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "funty" version = "1.2.0" @@ -1135,6 +1244,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "gateway-client" version = "0.1.0" @@ -1226,7 +1344,7 @@ checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912" dependencies = [ "byteorder", "ff", - "rand_core", + "rand_core 0.6.3", "subtle", ] @@ -1408,6 +1526,12 @@ dependencies = [ "syn", ] +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.17" @@ -1527,6 +1651,61 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "internal-dns" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap 3.1.6", + "dropshot 0.6.1-dev (git+https://github.com/oxidecomputer/dropshot)", + "expectorate", + "internal-dns-client", + "omicron-test-utils", + "openapi-lint", + "openapiv3", + "portpicker", + "pretty-hex", + "schemars", + "serde", + "serde_json", + "sled", + "slog", + "slog-async", + "slog-envlogger", + "slog-term", + "structopt", + "subprocess", + "tempdir", + "tokio", + "toml", + "trust-dns-proto", + "trust-dns-resolver", + "trust-dns-server", +] + +[[package]] +name = "internal-dns-client" +version = "0.1.0" +dependencies = [ + "progenitor", + "reqwest", + "serde", + "serde_json", + "slog", +] + +[[package]] +name = "ipconfig" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723519edce41262b05d4143ceb95050e4c614f483e78e9fd9e39a8275a84ad98" +dependencies = [ + "socket2", + "widestring", + "winapi", + "winreg 0.7.0", +] + [[package]] name = "ipnet" version = "2.4.0" @@ -1612,6 +1791,12 @@ version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "lock_api" version = "0.4.6" @@ -1630,6 +1815,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "macaddr" version = "1.0.1" @@ -1817,7 +2011,7 @@ dependencies = [ "anyhow", "bytes", "chrono", - "dropshot", + "dropshot 0.6.1-dev (git+https://github.com/oxidecomputer/dropshot?branch=main)", "headers", "http", "hyper", @@ -1845,6 +2039,15 @@ dependencies = [ "syn", ] +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -1923,7 +2126,7 @@ dependencies = [ "api_identity", "backoff", "chrono", - "dropshot", + "dropshot 0.6.1-dev (git+https://github.com/oxidecomputer/dropshot?branch=main)", "expectorate", "futures", "http", @@ -1932,7 +2135,7 @@ dependencies = [ "macaddr", "parse-display", "progenitor", - "rand", + "rand 0.8.5", "reqwest", "ring", "schemars", @@ -1956,7 +2159,7 @@ name = "omicron-gateway" version = "0.1.0" dependencies = [ "clap 3.1.6", - "dropshot", + "dropshot 0.6.1-dev (git+https://github.com/oxidecomputer/dropshot?branch=main)", "expectorate", "futures", "gateway-messages", @@ -1997,7 +2200,7 @@ dependencies = [ "db-macros", "diesel", "diesel-dtrace", - "dropshot", + "dropshot 0.6.1-dev (git+https://github.com/oxidecomputer/dropshot?branch=main)", "expectorate", "futures", "headers", @@ -2026,7 +2229,7 @@ dependencies = [ "oximeter-producer", "parse-display", "pq-sys", - "rand", + "rand 0.8.5", "ref-cast", "regex", "reqwest", @@ -2094,7 +2297,7 @@ dependencies = [ "cfg-if", "chrono", "crucible-agent-client", - "dropshot", + "dropshot 0.6.1-dev (git+https://github.com/oxidecomputer/dropshot?branch=main)", "expectorate", "futures", "http", @@ -2109,7 +2312,7 @@ dependencies = [ "percent-encoding", "progenitor", "propolis-client", - "rand", + "rand 0.8.5", "reqwest", "schemars", "serde", @@ -2141,7 +2344,7 @@ name = "omicron-test-utils" version = "0.1.0" dependencies = [ "anyhow", - "dropshot", + "dropshot 0.6.1-dev (git+https://github.com/oxidecomputer/dropshot?branch=main)", "expectorate", "futures", "libc", @@ -2322,7 +2525,7 @@ dependencies = [ name = "oximeter-collector" version = "0.1.0" dependencies = [ - "dropshot", + "dropshot 0.6.1-dev (git+https://github.com/oxidecomputer/dropshot?branch=main)", "expectorate", "nexus-client", "omicron-common", @@ -2352,7 +2555,7 @@ dependencies = [ "async-trait", "bytes", "chrono", - "dropshot", + "dropshot 0.6.1-dev (git+https://github.com/oxidecomputer/dropshot?branch=main)", "itertools", "omicron-test-utils", "oximeter", @@ -2376,7 +2579,7 @@ name = "oximeter-instruments" version = "0.1.0" dependencies = [ "chrono", - "dropshot", + "dropshot 0.6.1-dev (git+https://github.com/oxidecomputer/dropshot?branch=main)", "futures", "http", "oximeter", @@ -2398,7 +2601,7 @@ name = "oximeter-producer" version = "0.1.0" dependencies = [ "chrono", - "dropshot", + "dropshot 0.6.1-dev (git+https://github.com/oxidecomputer/dropshot?branch=main)", "nexus-client", "omicron-common", "oximeter", @@ -2709,6 +2912,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "portpicker" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be97d76faf1bfab666e1375477b23fde79eccf0276e9b63b92a39d676a889ba9" +dependencies = [ + "rand 0.8.5", +] + [[package]] name = "postgres-protocol" version = "0.6.3" @@ -2722,7 +2934,7 @@ dependencies = [ "hmac 0.12.1", "md-5", "memchr", - "rand", + "rand 0.8.5", "sha2 0.10.2", "stringprep", ] @@ -2792,6 +3004,12 @@ dependencies = [ "termtree", ] +[[package]] +name = "pretty-hex" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -2906,6 +3124,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.16" @@ -2932,6 +3156,29 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + [[package]] name = "rand" version = "0.8.5" @@ -2940,7 +3187,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.3", ] [[package]] @@ -2950,9 +3197,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", ] +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "rand_core" version = "0.6.3" @@ -2987,6 +3249,15 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "redox_syscall" version = "0.2.11" @@ -3098,7 +3369,17 @@ dependencies = [ "wasm-bindgen-futures", "web-sys", "webpki-roots", - "winreg", + "winreg 0.10.1", +] + +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", ] [[package]] @@ -3557,7 +3838,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2807892cfa58e081aa1f1111391c7a0649d4fa127a4ffbe34bcbfb35a1171a4" dependencies = [ "digest 0.9.0", - "rand_core", + "rand_core 0.6.3", ] [[package]] @@ -3572,6 +3853,22 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" +[[package]] +name = "sled" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" +dependencies = [ + "crc32fast", + "crossbeam-epoch", + "crossbeam-utils", + "fs2", + "fxhash", + "libc", + "log", + "parking_lot 0.11.2", +] + [[package]] name = "sled-agent-client" version = "0.1.0" @@ -3629,6 +3926,21 @@ dependencies = [ "usdt 0.2.1", ] +[[package]] +name = "slog-envlogger" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "906a1a0bc43fed692df4b82a5e2fbfc3733db8dad8bb514ab27a4f23ad04f5c0" +dependencies = [ + "log", + "regex", + "slog", + "slog-async", + "slog-scope", + "slog-stdlog", + "slog-term", +] + [[package]] name = "slog-json" version = "2.6.0" @@ -3641,6 +3953,28 @@ dependencies = [ "time 0.3.7", ] +[[package]] +name = "slog-scope" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f95a4b4c3274cd2869549da82b57ccc930859bdbf5bcea0424bc5f140b3c786" +dependencies = [ + "arc-swap", + "lazy_static", + "slog", +] + +[[package]] +name = "slog-stdlog" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8228ab7302adbf4fcb37e66f3cda78003feb521e7fd9e3847ec117a7784d0f5a" +dependencies = [ + "log", + "slog", + "slog-scope", +] + [[package]] name = "slog-term" version = "2.9.0" @@ -3707,7 +4041,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "dropshot", + "dropshot 0.6.1-dev (git+https://github.com/oxidecomputer/dropshot?branch=main)", "gateway-messages", "hex", "omicron-common", @@ -3726,7 +4060,7 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/spdm?rev=9742f6e#9742f6eae7b86cc8bc8bc2fb0feeb44f770a1fb6" dependencies = [ "bitflags", - "rand", + "rand 0.8.5", "ring", "webpki", ] @@ -3923,6 +4257,16 @@ dependencies = [ "xattr", ] +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + [[package]] name = "tempfile" version = "3.3.0" @@ -4294,6 +4638,94 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "trust-dns-client" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3be5f2ead860f0d3aabc01433bc6fff0fe5e86bfbe2dd16e32b9c79959310ad" +dependencies = [ + "cfg-if", + "data-encoding", + "futures-channel", + "futures-util", + "lazy_static", + "log", + "radix_trie", + "rand 0.8.5", + "thiserror", + "time 0.3.7", + "tokio", + "trust-dns-proto", +] + +[[package]] +name = "trust-dns-proto" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2861b3ed517888174d13909e675c4e94b3291867512068be59d76533e4d1270c" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "lazy_static", + "log", + "rand 0.8.5", + "smallvec", + "thiserror", + "tinyvec", + "tokio", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9e737a252a617bd4774649e245dbf705e207275db0893b9fa824d49f074fc1c" +dependencies = [ + "cfg-if", + "futures-util", + "ipconfig", + "lazy_static", + "log", + "lru-cache", + "parking_lot 0.12.0", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "trust-dns-proto", +] + +[[package]] +name = "trust-dns-server" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4058838790565ba870cb800008c7b3b8a3f154afaece824ad9a91a80a4b81dfb" +dependencies = [ + "async-trait", + "bytes", + "cfg-if", + "enum-as-inner", + "env_logger", + "futures-executor", + "futures-util", + "log", + "serde", + "thiserror", + "time 0.3.7", + "tokio", + "toml", + "trust-dns-client", + "trust-dns-proto", +] + [[package]] name = "try-lock" version = "0.2.3" @@ -4599,7 +5031,7 @@ dependencies = [ "ff", "group", "rand_chacha", - "rand_core", + "rand_core 0.6.3", "serde", "serde-big-array", "serde_cbor", @@ -4735,6 +5167,12 @@ dependencies = [ "webpki", ] +[[package]] +name = "widestring" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" + [[package]] name = "winapi" version = "0.3.9" @@ -4809,6 +5247,15 @@ version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi", +] + [[package]] name = "winreg" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index 6b68be368cd..d2be41fde94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,8 @@ members = [ "gateway", "gateway-client", "gateway-messages", + "internal-dns", + "internal-dns-client", "nexus", "nexus/src/db/db-macros", "nexus/test-utils", diff --git a/internal-dns-client/Cargo.toml b/internal-dns-client/Cargo.toml new file mode 100644 index 00000000000..af67e13d716 --- /dev/null +++ b/internal-dns-client/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "internal-dns-client" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[dependencies] +progenitor = { git = "https://github.com/oxidecomputer/progenitor" } +serde = { version = "1.0", features = [ "derive" ] } +serde_json = "1.0" +slog = { version = "2.5.0", features = [ "max_level_trace", "release_max_level_debug" ] } +reqwest = { version = "0.11", features = ["json", "rustls-tls", "stream"] } diff --git a/internal-dns-client/src/lib.rs b/internal-dns-client/src/lib.rs new file mode 100644 index 00000000000..49daa3d58ae --- /dev/null +++ b/internal-dns-client/src/lib.rs @@ -0,0 +1,18 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +progenitor::generate_api!( + spec = "../openapi/internal-dns.json", + inner_type = slog::Logger, + pre_hook = (|log: &slog::Logger, request: &reqwest::Request| { + slog::debug!(log, "client request"; + "method" => %request.method(), + "uri" => %request.url(), + "body" => ?&request.body(), + ); + }), + post_hook = (|log: &slog::Logger, result: &Result<_, _>| { + slog::debug!(log, "client response"; "result" => ?result); + }), +); diff --git a/internal-dns/Cargo.toml b/internal-dns/Cargo.toml new file mode 100644 index 00000000000..6abe17b75f0 --- /dev/null +++ b/internal-dns/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "internal-dns" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[dependencies] +anyhow = "1.0" +clap = { version = "3.1", features = [ "derive" ] } +dropshot = { git = "https://github.com/oxidecomputer/dropshot" } +pretty-hex = "0.2.1" +schemars = "0.8" +serde = { version = "1.0", features = [ "derive" ] } +serde_json = "1.0" +sled = "0.34" +slog = { version = "2.5.0", features = [ "max_level_trace", "release_max_level_debug" ] } +slog-term = "2.7" +slog-async = "2.7" +slog-envlogger = "2.2" +structopt = "0.3" +tempdir = "0.3" +tokio = { version = "1.17", features = [ "full" ] } +toml = "0.5" +trust-dns-proto = "0.21" +trust-dns-server = "0.21" + +[dev-dependencies] +expectorate = "1.0.4" +internal-dns-client = { path = "../internal-dns-client" } +omicron-test-utils = { path = "../test-utils" } +openapiv3 = "1.0" +openapi-lint = { git = "https://github.com/oxidecomputer/openapi-lint", branch = "main" } +portpicker = "0.1" +serde_json = "1.0" +subprocess = "0.2.8" +trust-dns-resolver = "0.21" diff --git a/internal-dns/src/bin/apigen.rs b/internal-dns/src/bin/apigen.rs new file mode 100644 index 00000000000..6f21201e4b0 --- /dev/null +++ b/internal-dns/src/bin/apigen.rs @@ -0,0 +1,27 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use anyhow::{bail, Result}; +use std::fs::File; +use std::io; +use internal_dns::dropshot_server::api; + +fn usage(args: &Vec) -> String { + format!("{} [output path]", args[0]) +} + +fn main() -> Result<()> { + let args: Vec = std::env::args().collect(); + + let mut out = match args.len() { + 1 => Box::new(io::stdout()) as Box, + 2 => Box::new(File::create(args[1].clone())?) as Box, + _ => bail!(usage(&args)), + }; + + let api = api(); + let openapi = api.openapi("Internal DNS", "v0.1.0"); + openapi.write(&mut out)?; + Ok(()) +} diff --git a/internal-dns/src/bin/dns-server.rs b/internal-dns/src/bin/dns-server.rs new file mode 100644 index 00000000000..12d4b4458f0 --- /dev/null +++ b/internal-dns/src/bin/dns-server.rs @@ -0,0 +1,54 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// See RFD 248 +// See https://github.com/oxidecomputer/omicron/issues/718 +// +// Milestones: +// - Dropshot server +// - Sqlite task +// - DNS task + +use anyhow::anyhow; +use anyhow::Context; +use clap::Parser; +use std::path::PathBuf; +use std::sync::Arc; + +#[derive(Parser, Debug)] +struct Args { + #[clap(long)] + config_file: PathBuf, +} + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + let args = Args::parse(); + let config_file = &args.config_file; + let config_file_contents = std::fs::read_to_string(config_file) + .with_context(|| format!("read config file {:?}", config_file))?; + let config: internal_dns::Config = toml::from_str(&config_file_contents) + .with_context(|| format!("parse config file {:?}", config_file))?; + eprintln!("{:?}", config); + + let log = + config.log.to_logger("internal-dns").context("failed to create logger")?; + + let db = Arc::new(sled::open(&config.data.storage_path)?); + + { + let db = db.clone(); + let log = log.clone(); + let config = config.dns.clone(); + + tokio::spawn( + async move { internal_dns::dns_server::run(log, db, config).await }, + ); + } + + let server = internal_dns::start_server(config, log, db).await?; + server + .await + .map_err(|error_message| anyhow!("server exiting: {}", error_message)) +} diff --git a/internal-dns/src/dns_data.rs b/internal-dns/src/dns_data.rs new file mode 100644 index 00000000000..0ddc2978365 --- /dev/null +++ b/internal-dns/src/dns_data.rs @@ -0,0 +1,356 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Manages DNS data (configured zone(s), records, etc.) + +use anyhow::Context; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use slog::{error, info, o, trace}; +use std::net::Ipv6Addr; +use std::sync::Arc; + +/// Configuration related to data model +#[derive(Deserialize, Debug)] +pub struct Config { + /// maximum number of channel messages to buffer + pub nmax_messages: usize, + + /// The path for the embedded kv store + pub storage_path: String, +} + +/// default maximum number of messages to buffer +const NMAX_MESSAGES_DEFAULT: usize = 16; + +impl Default for Config { + fn default() -> Self { + Config { + nmax_messages: NMAX_MESSAGES_DEFAULT, + storage_path: ".".into(), + } + } +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(rename = "Srv")] +pub struct SRV { + pub prio: u16, + pub weight: u16, + pub port: u16, + pub target: String, +} + +#[allow(clippy::upper_case_acronyms)] +#[derive(Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub enum DnsRecord { + AAAA(Ipv6Addr), + SRV(SRV), +} +#[derive(Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct DnsRecordKey { + name: String, +} +#[derive(Debug)] +pub struct DnsResponse { + tx: tokio::sync::oneshot::Sender, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename = "DnsKv")] +pub struct DnsKV { + key: DnsRecordKey, + record: DnsRecord, +} + +// XXX some refactors to help +// - each variant should have its own struct containing the data. This way we +// can pass it to functions as a bundle without them having to consume the +// whole enum (which might in principle be a different variant) +// - each variant's data should include some generic responder so that we can +// have common functions for logging and sending the T +#[derive(Debug)] +pub enum DnsCmd { + // XXX + // MakeExist(DnsRecord, DnsResponse<()>), + // MakeGone(DnsRecordKey, DnsResponse<()>), + Get(Option, DnsResponse>), + Set(Vec, DnsResponse<()>), + Delete(Vec, DnsResponse<()>), +} + +/// Data model client +/// +/// The Dropshot server has one of these to send commands to modify and update +/// the data model. +pub struct Client { + log: slog::Logger, + sender: tokio::sync::mpsc::Sender, +} + +impl Client { + pub fn new( + log: slog::Logger, + config: &Config, + db: Arc, + ) -> Client { + let (sender, receiver) = + tokio::sync::mpsc::channel(config.nmax_messages); + let server = Server { + log: log.new(o!("component" => "DataServer")), + receiver, + db, + }; + tokio::spawn(async move { data_server(server).await }); + Client { log, sender } + } + + // XXX error type needs to be rich enough for appropriate HTTP response + pub async fn get_records( + &self, + key: Option, + ) -> Result, anyhow::Error> { + slog::trace!(&self.log, "get_records"; "key" => ?key); + let (tx, rx) = tokio::sync::oneshot::channel(); + self.sender + .try_send(DnsCmd::Get(key, DnsResponse { tx })) + .context("send message")?; + rx.await.context("recv response") + } + + // XXX error type needs to be rich enough for appropriate HTTP response + pub async fn set_records( + &self, + records: Vec, + ) -> Result<(), anyhow::Error> { + slog::trace!(&self.log, "set_records"; "records" => ?records); + let (tx, rx) = tokio::sync::oneshot::channel(); + self.sender + .try_send(DnsCmd::Set(records, DnsResponse { tx })) + .context("send message")?; + rx.await.context("recv response") + } + + // XXX error type needs to be rich enough for appropriate HTTP response + pub async fn delete_records( + &self, + records: Vec, + ) -> Result<(), anyhow::Error> { + slog::trace!(&self.log, "delete_records"; "records" => ?records); + let (tx, rx) = tokio::sync::oneshot::channel(); + self.sender + .try_send(DnsCmd::Delete(records, DnsResponse { tx })) + .context("send message")?; + rx.await.context("recv response") + } +} + +/// Runs the body of the data model server event loop +async fn data_server(mut server: Server) { + let log = &server.log; + loop { + trace!(log, "waiting for message"); + let msg = match server.receiver.recv().await { + None => { + info!(log, "exiting due to channel close"); + break; + } + Some(m) => m, + }; + + trace!(log, "rx message"; "message" => ?msg); + match msg { + DnsCmd::Get(key, response) => { + server.cmd_get_records(key, response).await; + } + DnsCmd::Set(records, response) => { + server.cmd_set_records(records, response).await; + } + DnsCmd::Delete(records, response) => { + server.cmd_delete_records(records, response).await; + } + } + } +} + +/// Data model server +pub struct Server { + log: slog::Logger, + receiver: tokio::sync::mpsc::Receiver, + db: Arc, +} + +impl Server { + async fn cmd_get_records( + &self, + key: Option, + response: DnsResponse>, + ) { + // If a key is provided search just for that key. Otherwise return all + // the db entries. + if let Some(key) = key { + let bits = match self.db.get(key.name.as_bytes()) { + Ok(Some(bits)) => bits, + _ => { + match response.tx.send(Vec::new()) { + Ok(_) => {} + Err(e) => { + error!(self.log, "response tx: {:?}", e); + } + } + return; + } + }; + let record: DnsRecord = match serde_json::from_slice(bits.as_ref()) + { + Ok(r) => r, + Err(e) => { + error!(self.log, "deserialize record: {}", e); + match response.tx.send(Vec::new()) { + Ok(_) => {} + Err(e) => { + error!(self.log, "response tx: {:?}", e); + } + } + return; + } + }; + match response.tx.send(vec![DnsKV { key, record }]) { + Ok(_) => {} + Err(e) => { + error!(self.log, "response tx: {:?}", e); + } + } + } else { + let mut result = Vec::new(); + let mut iter = self.db.iter(); + loop { + match iter.next() { + Some(Ok((k, v))) => { + let record: DnsRecord = + match serde_json::from_slice(v.as_ref()) { + Ok(r) => r, + Err(e) => { + error!( + self.log, + "deserialize record: {}", e + ); + match response.tx.send(Vec::new()) { + Ok(_) => {} + Err(e) => { + error!( + self.log, + "response tx: {:?}", e + ); + } + } + return; + } + }; + let key = match std::str::from_utf8(k.as_ref()) { + Ok(s) => s.to_string(), + Err(e) => { + error!(self.log, "key encoding: {}", e); + match response.tx.send(Vec::new()) { + Ok(_) => {} + Err(e) => { + error!( + self.log, + "response tx: {:?}", e + ); + } + } + return; + } + }; + result.push(DnsKV { + key: DnsRecordKey { name: key }, + record, + }); + } + Some(Err(e)) => { + error!(self.log, "db iteration error: {}", e); + break; + } + None => break, + } + } + match response.tx.send(result) { + Ok(_) => {} + Err(e) => { + error!(self.log, "response tx: {:?}", e); + } + } + } + } + + async fn cmd_set_records( + &self, + records: Vec, + response: DnsResponse<()>, + ) { + for kv in records { + let bits = match serde_json::to_string(&kv.record) { + Ok(bits) => bits, + Err(e) => { + error!(self.log, "serialize record: {}", e); + match response.tx.send(()) { + Ok(_) => {} + Err(e) => { + error!(self.log, "response tx: {:?}", e); + } + } + return; + } + }; + match self.db.insert(kv.key.name.as_bytes(), bits.as_bytes()) { + Ok(_) => {} + Err(e) => { + error!(self.log, "db insert: {}", e); + match response.tx.send(()) { + Ok(_) => {} + Err(e) => { + error!(self.log, "response tx: {:?}", e); + } + } + return; + } + } + } + match response.tx.send(()) { + Ok(_) => {} + Err(e) => { + error!(self.log, "response tx: {:?}", e); + } + } + } + + async fn cmd_delete_records( + &self, + records: Vec, + response: DnsResponse<()>, + ) { + for k in records { + match self.db.remove(k.name.as_bytes()) { + Ok(_) => {} + Err(e) => { + error!(self.log, "db delete: {}", e); + match response.tx.send(()) { + Ok(_) => {} + Err(e) => { + error!(self.log, "response tx: {:?}", e); + } + } + return; + } + } + } + match response.tx.send(()) { + Ok(_) => {} + Err(e) => { + error!(self.log, "response tx: {:?}", e); + } + } + } +} diff --git a/internal-dns/src/dns_server.rs b/internal-dns/src/dns_server.rs new file mode 100644 index 00000000000..f6f5ed5209f --- /dev/null +++ b/internal-dns/src/dns_server.rs @@ -0,0 +1,185 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::io::Result; +use std::net::SocketAddr; +use std::str::FromStr; +use std::sync::Arc; + +use crate::dns_data::DnsRecord; +use pretty_hex::*; +use serde::Deserialize; +use slog::{error, Logger}; +use tokio::net::UdpSocket; +use trust_dns_proto::op::header::Header; +use trust_dns_proto::rr::rdata::SRV; +use trust_dns_proto::rr::record_data::RData; +use trust_dns_proto::rr::record_type::RecordType; +use trust_dns_proto::rr::{Name, Record}; +use trust_dns_proto::serialize::binary::{ + BinDecodable, BinDecoder, BinEncoder, +}; +use trust_dns_server::authority::{MessageRequest, MessageResponseBuilder}; + +/// Configuration related to the DNS server +#[derive(Deserialize, Debug, Clone)] +pub struct Config { + /// The address to listen for DNS requests on + pub bind_address: String, +} + +pub async fn run(log: Logger, db: Arc, config: Config) -> Result<()> { + let socket = Arc::new(UdpSocket::bind(config.bind_address).await?); + + loop { + let mut buf = vec![0u8; 16384]; + let (n, src) = socket.recv_from(&mut buf).await?; + buf.resize(n, 0); + + let socket = socket.clone(); + let log = log.clone(); + let db = db.clone(); + + tokio::spawn( + async move { handle_req(log, db, socket, src, buf).await }, + ); + } +} + +async fn handle_req<'a, 'b, 'c>( + log: Logger, + db: Arc, + socket: Arc, + src: SocketAddr, + buf: Vec, +) { + println!("{:?}", buf.hex_dump()); + + let mut dec = BinDecoder::new(&buf); + let mr = match MessageRequest::read(&mut dec) { + Ok(mr) => mr, + Err(e) => { + error!(log, "read message: {}", e); + return; + } + }; + + println!("{:#?}", mr); + + let rb = MessageResponseBuilder::from_message_request(&mr); + let header = Header::response_from_request(mr.header()); + + let name = mr.query().original().name().clone(); + let key = name.to_string(); + let key = key.trim_end_matches('.'); + + let bits = match db.get(key.as_bytes()) { + Ok(Some(bits)) => bits, + Err(e) => { + error!(log, "db get: {}", e); + nack(&log, &mr, &socket, &header, &src).await; + return; + } + _ => { + nack(&log, &mr, &socket, &header, &src).await; + return; + } + }; + + let record: crate::dns_data::DnsRecord = + match serde_json::from_slice(bits.as_ref()) { + Ok(r) => r, + Err(e) => { + error!(log, "deserialize record: {}", e); + return; + } + }; + + match record { + DnsRecord::AAAA(addr) => { + let mut aaaa = Record::new(); + aaaa.set_name(name) + .set_rr_type(RecordType::AAAA) + .set_data(Some(RData::AAAA(addr))); + + let mresp = rb.build(header, vec![&aaaa], vec![], vec![], vec![]); + + let mut resp_data = Vec::new(); + let mut enc = BinEncoder::new(&mut resp_data); + match mresp.destructive_emit(&mut enc) { + Ok(_) => {} + Err(e) => { + error!(log, "destructive emit: {}", e); + nack(&log, &mr, &socket, &header, &src).await; + return; + } + } + match socket.send_to(&resp_data, &src).await { + Ok(_) => {} + Err(e) => { + error!(log, "send: {}", e); + } + } + } + DnsRecord::SRV(crate::dns_data::SRV { prio, weight, port, target }) => { + let mut srv = Record::new(); + let tgt = match Name::from_str(&target) { + Ok(tgt) => tgt, + Err(e) => { + error!(log, "srv target: '{}' {}", target, e); + nack(&log, &mr, &socket, &header, &src).await; + return; + } + }; + srv.set_name(name) + .set_rr_type(RecordType::SRV) + .set_data(Some(RData::SRV(SRV::new(prio, weight, port, tgt)))); + + let mresp = rb.build(header, vec![&srv], vec![], vec![], vec![]); + + let mut resp_data = Vec::new(); + let mut enc = BinEncoder::new(&mut resp_data); + match mresp.destructive_emit(&mut enc) { + Ok(_) => {} + Err(e) => { + error!(log, "destructive emit: {}", e); + nack(&log, &mr, &socket, &header, &src).await; + return; + } + } + match socket.send_to(&resp_data, &src).await { + Ok(_) => {} + Err(e) => { + error!(log, "send: {}", e); + } + } + } + }; +} + +async fn nack( + log: &Logger, + mr: &MessageRequest, + socket: &UdpSocket, + header: &Header, + src: &SocketAddr, +) { + let rb = MessageResponseBuilder::from_message_request(mr); + let mresp = rb.build_no_records(*header); + let mut resp_data = Vec::new(); + let mut enc = BinEncoder::new(&mut resp_data); + match mresp.destructive_emit(&mut enc) { + Ok(_) => {} + Err(e) => { + error!(log, "destructive emit: {}", e); + return; + } + } + match socket.send_to(&resp_data, &src).await { + Ok(_) => {} + Err(e) => { + error!(log, "destructive emit: {}", e); + } + } +} diff --git a/internal-dns/src/dropshot_server.rs b/internal-dns/src/dropshot_server.rs new file mode 100644 index 00000000000..51d40e5053e --- /dev/null +++ b/internal-dns/src/dropshot_server.rs @@ -0,0 +1,73 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Dropshot server for configuring DNS namespace + +use crate::dns_data::{self, DnsKV, DnsRecordKey}; +use dropshot::endpoint; +use std::sync::Arc; + +pub struct Context { + client: dns_data::Client, +} + +impl Context { + pub fn new(client: dns_data::Client) -> Context { + Context { client } + } +} + +pub fn api() -> dropshot::ApiDescription> { + let mut api = dropshot::ApiDescription::new(); + + api.register(dns_records_get).expect("register dns_records_get"); + api.register(dns_records_set).expect("register dns_records_set"); + api.register(dns_records_delete).expect("register dns_records_delete"); + api +} + +#[endpoint( + method = GET, + path = "/get-records", +)] +async fn dns_records_get( + rqctx: Arc>>, +) -> Result>, dropshot::HttpError> { + let apictx = rqctx.context(); + // XXX record key + let records = apictx.client.get_records(None).await.map_err(|e| { + dropshot::HttpError::for_internal_error(format!("uh oh: {:?}", e)) + })?; + Ok(dropshot::HttpResponseOk(records)) +} + +#[endpoint( + method = PUT, + path = "/set-records", +)] +async fn dns_records_set( + rqctx: Arc>>, + rq: dropshot::TypedBody>, +) -> Result, dropshot::HttpError> { + let apictx = rqctx.context(); + apictx.client.set_records(rq.into_inner()).await.map_err(|e| { + dropshot::HttpError::for_internal_error(format!("uh oh: {:?}", e)) + })?; + Ok(dropshot::HttpResponseOk(())) +} + +#[endpoint( + method = PUT, + path = "/delete-records", +)] +async fn dns_records_delete( + rqctx: Arc>>, + rq: dropshot::TypedBody>, +) -> Result, dropshot::HttpError> { + let apictx = rqctx.context(); + apictx.client.delete_records(rq.into_inner()).await.map_err(|e| { + dropshot::HttpError::for_internal_error(format!("uh oh: {:?}", e)) + })?; + Ok(dropshot::HttpResponseOk(())) +} diff --git a/internal-dns/src/lib.rs b/internal-dns/src/lib.rs new file mode 100644 index 00000000000..d94684d75e5 --- /dev/null +++ b/internal-dns/src/lib.rs @@ -0,0 +1,47 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#![allow(clippy::type_complexity)] +#![allow(clippy::ptr_arg)] + +use anyhow::anyhow; +use serde::Deserialize; +use std::sync::Arc; + +pub mod dns_data; +pub mod dns_server; +pub mod dropshot_server; + +#[derive(Deserialize, Debug)] +pub struct Config { + pub log: dropshot::ConfigLogging, + pub dropshot: dropshot::ConfigDropshot, + pub data: dns_data::Config, + pub dns: dns_server::Config, +} + +pub async fn start_server( + config: Config, + log: slog::Logger, + db: Arc, +) -> Result>, anyhow::Error> +{ + let data_client = dns_data::Client::new( + log.new(slog::o!("component" => "DataClient")), + &config.data, + db, + ); + + let api = dropshot_server::api(); + let api_context = Arc::new(dropshot_server::Context::new(data_client)); + + Ok(dropshot::HttpServerStarter::new( + &config.dropshot, + api, + api_context, + &log, + ) + .map_err(|e| anyhow!("{}", e))? + .start()) +} diff --git a/internal-dns/tests/basic_test.rs b/internal-dns/tests/basic_test.rs new file mode 100644 index 00000000000..0363a696e6f --- /dev/null +++ b/internal-dns/tests/basic_test.rs @@ -0,0 +1,188 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::sync::Arc; + +use anyhow::{anyhow, Context, Result}; +use std::net::Ipv6Addr; +use internal_dns_client::{ + types::{DnsKv, DnsRecord, DnsRecordKey, Srv}, + Client, +}; +use trust_dns_resolver::config::{ + NameServerConfig, Protocol, ResolverConfig, ResolverOpts, +}; +use trust_dns_resolver::TokioAsyncResolver; + +#[tokio::test] +pub async fn aaaa_crud() -> Result<(), anyhow::Error> { + let (client, resolver) = init_client_server().await?; + + // records should initially be empty + let records = client.dns_records_get().await?; + assert!(records.is_empty()); + + // add an aaaa record + let name = DnsRecordKey { name: "devron.system".into() }; + let addr = Ipv6Addr::new(0xfd, 0, 0, 0, 0, 0, 0, 0x1); + let aaaa = DnsRecord::Aaaa(addr); + client + .dns_records_set(&vec![DnsKv { + key: name.clone(), + record: aaaa.clone(), + }]) + .await?; + + // read back the aaaa record + let records = client.dns_records_get().await?; + assert_eq!(1, records.len()); + assert_eq!(records[0].key.name, name.name); + match records[0].record { + DnsRecord::Aaaa(ra) => { + assert_eq!(ra, addr); + } + _ => { + panic!("expected aaaa record") + } + } + + // resolve the name + let response = resolver.lookup_ip(name.name + ".").await?; + let address = response.iter().next().expect("no addresses returned!"); + assert_eq!(address, addr); + + Ok(()) +} + +#[tokio::test] +pub async fn srv_crud() -> Result<(), anyhow::Error> { + let (client, resolver) = init_client_server().await?; + + // records should initially be empty + let records = client.dns_records_get().await?; + assert!(records.is_empty()); + + // add a srv record + let name = DnsRecordKey { name: "hromi.cluster".into() }; + let srv = + Srv { prio: 47, weight: 74, port: 99, target: "outpost47".into() }; + let rec = DnsRecord::Srv(srv.clone()); + client + .dns_records_set(&vec![DnsKv { + key: name.clone(), + record: rec.clone(), + }]) + .await?; + + // read back the srv record + let records = client.dns_records_get().await?; + assert_eq!(1, records.len()); + assert_eq!(records[0].key.name, name.name); + match records[0].record { + DnsRecord::Srv(ref rs) => { + assert_eq!(rs.prio, srv.prio); + assert_eq!(rs.weight, srv.weight); + assert_eq!(rs.port, srv.port); + assert_eq!(rs.target, srv.target); + } + _ => { + panic!("expected srv record") + } + } + + // resolve the srv + let response = resolver.srv_lookup(name.name).await?; + let srvr = response.iter().next().expect("no addresses returned!"); + assert_eq!(srvr.priority(), srv.prio); + assert_eq!(srvr.weight(), srv.weight); + assert_eq!(srvr.port(), srv.port); + assert_eq!(srvr.target().to_string(), srv.target + "."); + + Ok(()) +} + +async fn init_client_server( +) -> Result<(Client, TokioAsyncResolver), anyhow::Error> { + // initialize dns server config + let (config, dropshot_port, dns_port) = test_config()?; + let log = + config.log.to_logger("internal-dns").context("failed to create logger")?; + + // initialize dns server db + let db = Arc::new(sled::open(&config.data.storage_path)?); + db.clear()?; + + let client = Client::new( + &format!("http://127.0.0.1:{}", dropshot_port), + log.clone(), + ); + + let mut rc = ResolverConfig::new(); + rc.add_name_server(NameServerConfig { + socket_addr: SocketAddr::V4(SocketAddrV4::new( + Ipv4Addr::new(127, 0, 0, 1), + dns_port, + )), + protocol: Protocol::Udp, + tls_dns_name: None, + trust_nx_responses: false, + bind_addr: None, + }); + + let resolver = + TokioAsyncResolver::tokio(rc, ResolverOpts::default()).unwrap(); + + // launch a dns server + { + let db = db.clone(); + let log = log.clone(); + let config = config.dns.clone(); + + tokio::spawn( + async move { internal_dns::dns_server::run(log, db, config).await }, + ); + } + + // launch a dropshot server + tokio::spawn(async move { + let server = internal_dns::start_server(config, log, db).await?; + server.await.map_err(|error_message| { + anyhow!("server exiting: {}", error_message) + }) + }); + + // wait for server to start + tokio::time::sleep(tokio::time::Duration::from_millis(250)).await; + + Ok((client, resolver)) +} + +fn test_config() -> Result<(internal_dns::Config, u16, u16), anyhow::Error> { + let dropshot_port = portpicker::pick_unused_port().expect("pick port"); + let dns_port = portpicker::pick_unused_port().expect("pick port"); + let tmp_dir = tempdir::TempDir::new("internal-dns-test")?; + let mut storage_path = tmp_dir.path().to_path_buf(); + storage_path.push("test"); + let storage_path = storage_path.to_str().unwrap().into(); + + let config = internal_dns::Config { + log: dropshot::ConfigLogging::StderrTerminal { + level: dropshot::ConfigLoggingLevel::Info, + }, + dropshot: dropshot::ConfigDropshot { + bind_address: format!("127.0.0.1:{}", dropshot_port) + .parse() + .unwrap(), + request_body_max_bytes: 1024, + ..Default::default() + }, + data: internal_dns::dns_data::Config { nmax_messages: 16, storage_path }, + dns: internal_dns::dns_server::Config { + bind_address: format!("127.0.0.1:{}", dns_port).parse().unwrap(), + }, + }; + + Ok((config, dropshot_port, dns_port)) +} diff --git a/internal-dns/tests/openapi_test.rs b/internal-dns/tests/openapi_test.rs new file mode 100644 index 00000000000..3d6e6d56386 --- /dev/null +++ b/internal-dns/tests/openapi_test.rs @@ -0,0 +1,27 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use expectorate::assert_contents; +use subprocess::Exec; +use omicron_test_utils::dev::test_cmds::assert_exit_code; +use omicron_test_utils::dev::test_cmds::path_to_executable; +use omicron_test_utils::dev::test_cmds::run_command; +use omicron_test_utils::dev::test_cmds::EXIT_SUCCESS; +use openapiv3::OpenAPI; + +const CMD_API_GEN: &str = env!("CARGO_BIN_EXE_apigen"); + +#[test] +fn test_internal_dns_openapi() { + let exec = Exec::cmd(path_to_executable(CMD_API_GEN)); + let (exit_status, stdout, _) = run_command(exec); + assert_exit_code(exit_status, EXIT_SUCCESS); + + let spec: OpenAPI = serde_json::from_str(&stdout) + .expect("stdout was not valid OpenAPI"); + let errors = openapi_lint::validate(&spec); + assert!(errors.is_empty(), "{}", errors.join("\n\n")); + + assert_contents("../openapi/internal-dns.json", &stdout); +} diff --git a/openapi/internal-dns.json b/openapi/internal-dns.json new file mode 100644 index 00000000000..708983bd9cd --- /dev/null +++ b/openapi/internal-dns.json @@ -0,0 +1,237 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Internal DNS", + "version": "v0.1.0" + }, + "paths": { + "/delete-records": { + "put": { + "operationId": "dns_records_delete", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Array_of_DnsRecordKey", + "type": "array", + "items": { + "$ref": "#/components/schemas/DnsRecordKey" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Null", + "type": "string", + "enum": [ + null + ] + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/get-records": { + "get": { + "operationId": "dns_records_get", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_DnsKv", + "type": "array", + "items": { + "$ref": "#/components/schemas/DnsKv" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/set-records": { + "put": { + "operationId": "dns_records_set", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Array_of_DnsKv", + "type": "array", + "items": { + "$ref": "#/components/schemas/DnsKv" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Null", + "type": "string", + "enum": [ + null + ] + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + } + }, + "components": { + "responses": { + "Error": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "schemas": { + "DnsKv": { + "type": "object", + "properties": { + "key": { + "$ref": "#/components/schemas/DnsRecordKey" + }, + "record": { + "$ref": "#/components/schemas/DnsRecord" + } + }, + "required": [ + "key", + "record" + ] + }, + "DnsRecord": { + "oneOf": [ + { + "type": "object", + "properties": { + "AAAA": { + "type": "string", + "format": "ipv6" + } + }, + "required": [ + "AAAA" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "SRV": { + "$ref": "#/components/schemas/Srv" + } + }, + "required": [ + "SRV" + ], + "additionalProperties": false + } + ] + }, + "DnsRecordKey": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "Error": { + "description": "Error information from a response.", + "type": "object", + "properties": { + "error_code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "request_id": { + "type": "string" + } + }, + "required": [ + "message", + "request_id" + ] + }, + "Srv": { + "type": "object", + "properties": { + "port": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "prio": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "target": { + "type": "string" + }, + "weight": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "port", + "prio", + "target", + "weight" + ] + } + } + } +} \ No newline at end of file From 1a15d6c5a67a96c1ac8869b81d570b43b402f9c2 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Tue, 22 Mar 2022 19:26:31 -0400 Subject: [PATCH 2/3] fmt --- internal-dns/src/bin/apigen.rs | 2 +- internal-dns/src/bin/dns-server.rs | 12 +++++++----- internal-dns/tests/basic_test.rs | 19 ++++++++++++------- internal-dns/tests/openapi_test.rs | 6 +++--- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/internal-dns/src/bin/apigen.rs b/internal-dns/src/bin/apigen.rs index 6f21201e4b0..095291c9571 100644 --- a/internal-dns/src/bin/apigen.rs +++ b/internal-dns/src/bin/apigen.rs @@ -3,9 +3,9 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use anyhow::{bail, Result}; +use internal_dns::dropshot_server::api; use std::fs::File; use std::io; -use internal_dns::dropshot_server::api; fn usage(args: &Vec) -> String { format!("{} [output path]", args[0]) diff --git a/internal-dns/src/bin/dns-server.rs b/internal-dns/src/bin/dns-server.rs index 12d4b4458f0..505a42a7dc0 100644 --- a/internal-dns/src/bin/dns-server.rs +++ b/internal-dns/src/bin/dns-server.rs @@ -32,8 +32,10 @@ async fn main() -> Result<(), anyhow::Error> { .with_context(|| format!("parse config file {:?}", config_file))?; eprintln!("{:?}", config); - let log = - config.log.to_logger("internal-dns").context("failed to create logger")?; + let log = config + .log + .to_logger("internal-dns") + .context("failed to create logger")?; let db = Arc::new(sled::open(&config.data.storage_path)?); @@ -42,9 +44,9 @@ async fn main() -> Result<(), anyhow::Error> { let log = log.clone(); let config = config.dns.clone(); - tokio::spawn( - async move { internal_dns::dns_server::run(log, db, config).await }, - ); + tokio::spawn(async move { + internal_dns::dns_server::run(log, db, config).await + }); } let server = internal_dns::start_server(config, log, db).await?; diff --git a/internal-dns/tests/basic_test.rs b/internal-dns/tests/basic_test.rs index 0363a696e6f..24e5b11744e 100644 --- a/internal-dns/tests/basic_test.rs +++ b/internal-dns/tests/basic_test.rs @@ -6,11 +6,11 @@ use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::sync::Arc; use anyhow::{anyhow, Context, Result}; -use std::net::Ipv6Addr; use internal_dns_client::{ types::{DnsKv, DnsRecord, DnsRecordKey, Srv}, Client, }; +use std::net::Ipv6Addr; use trust_dns_resolver::config::{ NameServerConfig, Protocol, ResolverConfig, ResolverOpts, }; @@ -107,8 +107,10 @@ async fn init_client_server( ) -> Result<(Client, TokioAsyncResolver), anyhow::Error> { // initialize dns server config let (config, dropshot_port, dns_port) = test_config()?; - let log = - config.log.to_logger("internal-dns").context("failed to create logger")?; + let log = config + .log + .to_logger("internal-dns") + .context("failed to create logger")?; // initialize dns server db let db = Arc::new(sled::open(&config.data.storage_path)?); @@ -140,9 +142,9 @@ async fn init_client_server( let log = log.clone(); let config = config.dns.clone(); - tokio::spawn( - async move { internal_dns::dns_server::run(log, db, config).await }, - ); + tokio::spawn(async move { + internal_dns::dns_server::run(log, db, config).await + }); } // launch a dropshot server @@ -178,7 +180,10 @@ fn test_config() -> Result<(internal_dns::Config, u16, u16), anyhow::Error> { request_body_max_bytes: 1024, ..Default::default() }, - data: internal_dns::dns_data::Config { nmax_messages: 16, storage_path }, + data: internal_dns::dns_data::Config { + nmax_messages: 16, + storage_path, + }, dns: internal_dns::dns_server::Config { bind_address: format!("127.0.0.1:{}", dns_port).parse().unwrap(), }, diff --git a/internal-dns/tests/openapi_test.rs b/internal-dns/tests/openapi_test.rs index 3d6e6d56386..cf4cd7ff83f 100644 --- a/internal-dns/tests/openapi_test.rs +++ b/internal-dns/tests/openapi_test.rs @@ -3,12 +3,12 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use expectorate::assert_contents; -use subprocess::Exec; use omicron_test_utils::dev::test_cmds::assert_exit_code; use omicron_test_utils::dev::test_cmds::path_to_executable; use omicron_test_utils::dev::test_cmds::run_command; use omicron_test_utils::dev::test_cmds::EXIT_SUCCESS; use openapiv3::OpenAPI; +use subprocess::Exec; const CMD_API_GEN: &str = env!("CARGO_BIN_EXE_apigen"); @@ -18,8 +18,8 @@ fn test_internal_dns_openapi() { let (exit_status, stdout, _) = run_command(exec); assert_exit_code(exit_status, EXIT_SUCCESS); - let spec: OpenAPI = serde_json::from_str(&stdout) - .expect("stdout was not valid OpenAPI"); + let spec: OpenAPI = + serde_json::from_str(&stdout).expect("stdout was not valid OpenAPI"); let errors = openapi_lint::validate(&spec); assert!(errors.is_empty(), "{}", errors.join("\n\n")); From 8f373bd71f966f6d3218370646dbd91dbf4fc006 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Thu, 24 Mar 2022 11:02:14 -0400 Subject: [PATCH 3/3] Added dnsadm --- Cargo.lock | 7 ++ Cargo.toml | 2 + internal-dns-client/Cargo.toml | 7 ++ internal-dns-client/src/bin/dnsadm.rs | 117 ++++++++++++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 internal-dns-client/src/bin/dnsadm.rs diff --git a/Cargo.lock b/Cargo.lock index 7f2568b5357..67c3d4ef061 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1701,11 +1701,18 @@ dependencies = [ name = "internal-dns-client" version = "0.1.0" dependencies = [ + "anyhow", + "clap 3.1.6", "progenitor", "reqwest", "serde", "serde_json", "slog", + "slog-async", + "slog-envlogger", + "slog-term", + "structopt", + "tokio", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4dd7b1dbff3..9e8055be407 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,8 @@ default-members = [ "gateway", "gateway-client", "gateway-messages", + "internal-dns", + "internal-dns-client", "nexus", "nexus/src/db/db-macros", "package", diff --git a/internal-dns-client/Cargo.toml b/internal-dns-client/Cargo.toml index af67e13d716..70af97d5ac4 100644 --- a/internal-dns-client/Cargo.toml +++ b/internal-dns-client/Cargo.toml @@ -5,8 +5,15 @@ edition = "2021" license = "MPL-2.0" [dependencies] +anyhow = "1.0" +clap = { version = "3.1", features = [ "derive" ] } progenitor = { git = "https://github.com/oxidecomputer/progenitor" } serde = { version = "1.0", features = [ "derive" ] } serde_json = "1.0" slog = { version = "2.5.0", features = [ "max_level_trace", "release_max_level_debug" ] } +slog-term = "2.7" +slog-async = "2.7" +slog-envlogger = "2.2" +structopt = "0.3" +tokio = { version = "1.17", features = [ "full" ] } reqwest = { version = "0.11", features = ["json", "rustls-tls", "stream"] } diff --git a/internal-dns-client/src/bin/dnsadm.rs b/internal-dns-client/src/bin/dnsadm.rs new file mode 100644 index 00000000000..1c2d9a876fa --- /dev/null +++ b/internal-dns-client/src/bin/dnsadm.rs @@ -0,0 +1,117 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use anyhow::Result; +use internal_dns_client::{ + types::{DnsKv, DnsRecord, DnsRecordKey, Srv}, + Client, +}; +use slog::{Drain, Logger}; +use std::net::Ipv6Addr; +use structopt::{clap::AppSettings::*, StructOpt}; + +#[derive(Debug, StructOpt)] +#[structopt( + name = "dnsadm", + about = "Administer DNS records", + global_setting(ColorAuto), + global_setting(ColoredHelp) +)] +struct Opt { + #[structopt(short, long)] + address: Option, + + #[structopt(short, long)] + port: Option, + + #[structopt(subcommand)] + subcommand: SubCommand, +} + +#[derive(Debug, StructOpt)] +enum SubCommand { + ListRecords, + AddAAAA(AddAAAACommand), + AddSRV(AddSRVCommand), + DeleteRecord(DeleteRecordCommand), +} + +#[derive(Debug, StructOpt)] +struct AddAAAACommand { + name: String, + addr: Ipv6Addr, +} + +#[derive(Debug, StructOpt)] +struct AddSRVCommand { + name: String, + prio: u16, + weight: u16, + port: u16, + target: String, +} + +#[derive(Debug, StructOpt)] +struct DeleteRecordCommand { + name: String, +} + +#[tokio::main] +async fn main() -> Result<()> { + let opt = Opt::from_args(); + let log = init_logger(); + + let addr = match opt.address { + Some(a) => a, + None => "localhost".into(), + }; + let port = opt.port.unwrap_or(5353); + + let endpoint = format!("http://{}:{}", addr, port); + let client = Client::new(&endpoint, log.clone()); + + let opt = Opt::from_args(); + match opt.subcommand { + SubCommand::ListRecords => { + let records = client.dns_records_get().await?; + println!("{:#?}", records); + } + SubCommand::AddAAAA(cmd) => { + client + .dns_records_set(&vec![DnsKv { + key: DnsRecordKey { name: cmd.name }, + record: DnsRecord::Aaaa(cmd.addr), + }]) + .await?; + } + SubCommand::AddSRV(cmd) => { + client + .dns_records_set(&vec![DnsKv { + key: DnsRecordKey { name: cmd.name }, + record: DnsRecord::Srv(Srv { + prio: cmd.prio, + weight: cmd.weight, + port: cmd.port, + target: cmd.target, + }), + }]) + .await?; + } + SubCommand::DeleteRecord(cmd) => { + client + .dns_records_delete(&vec![DnsRecordKey { name: cmd.name }]) + .await?; + } + } + + Ok(()) +} + +fn init_logger() -> Logger { + let decorator = slog_term::TermDecorator::new().build(); + let drain = slog_term::FullFormat::new(decorator).build().fuse(); + let drain = slog_envlogger::new(drain).fuse(); + let drain = slog_async::Async::new(drain).chan_size(0x2000).build().fuse(); + slog::Logger::root(drain, slog::o!()) +}