erbium-core-1.0.5/.cargo_vcs_info.json0000644000000001600000000000100132450ustar { "git": { "sha1": "52c81e9577d5ab4bb78bc9d7eb90d19fe9ad7f4b" }, "path_in_vcs": "crates/erbium-core" }erbium-core-1.0.5/Cargo.lock0000644000001031330000000000100112240ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if", "once_cell", "version_check", ] [[package]] name = "aho-corasick" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] [[package]] name = "allocator-api2" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56fc6cf8dc8c4158eed8649f9b8b0ea1518eb62b544fe9490d66fa0b349eafe9" [[package]] name = "anyhow" version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "arbitrary" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e" dependencies = [ "derive_arbitrary", ] [[package]] name = "async-trait" version = "0.1.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "byteorder" version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cc" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cpufeatures" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" dependencies = [ "libc", ] [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "derive_arbitrary" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53e0efad4403bfc52dc201159c4b842a246a14b98c64b55dfd0f2d89729dfeb8" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[package]] name = "env_logger" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" dependencies = [ "humantime", "is-terminal", "log", "regex", "termcolor", ] [[package]] name = "erbium-core" version = "1.0.5" dependencies = [ "arbitrary", "async-trait", "byteorder", "bytes", "digest", "env_logger", "erbium-net", "futures", "hmac", "hyper", "lazy_static", "log", "prometheus", "rand", "rusqlite", "sha2", "tokio", "tokio-util", "vergen", "yaml-rust", ] [[package]] name = "erbium-net" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af50cd3527259cec275d05440e0d962dc060a88fd6b9bf18ba66e603c4d27bc4" dependencies = [ "bytes", "futures", "log", "mio", "netlink-packet-core", "netlink-packet-route", "netlink-sys", "nix", "tokio", ] [[package]] name = "errno" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", "windows-sys 0.48.0", ] [[package]] name = "errno-dragonfly" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" dependencies = [ "cc", "libc", ] [[package]] name = "fallible-iterator" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" [[package]] name = "fallible-streaming-iterator" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "futures" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-macro" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "gimli" version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" [[package]] name = "hashbrown" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" dependencies = [ "ahash", "allocator-api2", ] [[package]] name = "hashlink" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" dependencies = [ "hashbrown", ] [[package]] name = "hermit-abi" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ "digest", ] [[package]] name = "http" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http-body" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", "http", "pin-project-lite", ] [[package]] name = "httparse" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[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.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", "socket2", "tokio", "tower-service", "tracing", "want", ] [[package]] name = "io-lifetimes" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi", "libc", "windows-sys 0.48.0", ] [[package]] name = "is-terminal" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb" dependencies = [ "hermit-abi", "rustix 0.38.1", "windows-sys 0.48.0", ] [[package]] name = "itoa" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "libsqlite3-sys" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" dependencies = [ "cc", "pkg-config", "vcpkg", ] [[package]] name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" [[package]] name = "linux-raw-sys" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" [[package]] name = "lock_api" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memoffset" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ "autocfg", ] [[package]] name = "miniz_oxide" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] [[package]] name = "mio" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", "log", "wasi", "windows-sys 0.48.0", ] [[package]] name = "netlink-packet-core" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e5cf0b54effda4b91615c40ff0fd12d0d4c9a6e0f5116874f03941792ff535a" dependencies = [ "anyhow", "byteorder", "libc", "netlink-packet-utils", ] [[package]] name = "netlink-packet-route" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea993e32c77d87f01236c38f572ecb6c311d592e56a06262a007fd2a6e31253c" dependencies = [ "anyhow", "bitflags 1.3.2", "byteorder", "libc", "netlink-packet-core", "netlink-packet-utils", ] [[package]] name = "netlink-packet-utils" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" dependencies = [ "anyhow", "byteorder", "paste", "thiserror", ] [[package]] name = "netlink-sys" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6471bf08e7ac0135876a9581bf3217ef0333c191c128d34878079f42ee150411" dependencies = [ "bytes", "futures", "libc", "log", "tokio", ] [[package]] name = "nix" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", "memoffset", "pin-utils", "static_assertions", ] [[package]] name = "num_cpus" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "object" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.48.1", ] [[package]] name = "paste" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" [[package]] name = "pin-project-lite" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" dependencies = [ "unicode-ident", ] [[package]] name = "procfs" version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1de8dacb0873f77e6aefc6d71e044761fcc68060290f5b1089fcdf84626bb69" dependencies = [ "bitflags 1.3.2", "byteorder", "hex", "lazy_static", "rustix 0.36.14", ] [[package]] name = "prometheus" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "449811d15fbdf5ceb5c1144416066429cf82316e2ec8ce0c1f6f8a02e7bbcf8c" dependencies = [ "cfg-if", "fnv", "lazy_static", "libc", "memchr", "parking_lot", "procfs", "protobuf", "thiserror", ] [[package]] name = "protobuf" version = "2.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" [[package]] name = "quote" version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "redox_syscall" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "regex" version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "rusqlite" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ "bitflags 2.3.3", "fallible-iterator", "fallible-streaming-iterator", "hashlink", "libsqlite3-sys", "smallvec", ] [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" version = "0.36.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14e4d67015953998ad0eb82887a0eb0129e18a7e2f3b7b0f6c422fddcd503d62" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", "linux-raw-sys 0.1.4", "windows-sys 0.45.0", ] [[package]] name = "rustix" version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbc6396159432b5c8490d4e301d8c705f61860b8b6c863bf79942ce5401968f3" dependencies = [ "bitflags 2.3.3", "errno", "libc", "linux-raw-sys 0.4.3", "windows-sys 0.48.0", ] [[package]] name = "rustversion" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "sha2" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "signal-hook-registry" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] [[package]] name = "slab" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "socket2" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", ] [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "subtle" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" version = "2.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "termcolor" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" dependencies = [ "winapi-util", ] [[package]] name = "thiserror" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio" version = "1.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" dependencies = [ "autocfg", "backtrace", "bytes", "libc", "mio", "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-util" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", "tracing", ] [[package]] name = "tower-service" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", ] [[package]] name = "try-lock" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "typenum" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicode-ident" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" version = "8.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce38fc503fa57441ac2539c3e723b5adf76601eb4f1ad24025c6660d27f355b7" dependencies = [ "anyhow", "rustversion", ] [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "want" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ "try-lock", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ "windows-targets 0.42.2", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.1", ] [[package]] name = "windows-targets" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-targets" version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ "windows_aarch64_gnullvm 0.48.0", "windows_aarch64_msvc 0.48.0", "windows_i686_gnu 0.48.0", "windows_i686_msvc 0.48.0", "windows_x86_64_gnu 0.48.0", "windows_x86_64_gnullvm 0.48.0", "windows_x86_64_msvc 0.48.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "yaml-rust" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" dependencies = [ "linked-hash-map", ] erbium-core-1.0.5/Cargo.toml0000644000000043720000000000100112540ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "erbium-core" version = "1.0.5" authors = ["Perry Lorier "] description = "Network services for small/home networks - Core code" homepage = "https://github.com/isomer/erbium" readme = "README.md" license = "Apache-2.0" repository = "https://github.com/isomer/erbium.git" [lib] name = "erbium" path = "src/lib.rs" [[bin]] name = "erbium-dns" required-features = ["dns"] [[bin]] name = "erbium-dhcp" required-features = ["dhcp"] [[bin]] name = "erbium-lldp" [dependencies.arbitrary] version = "1.1" features = ["derive"] optional = true [dependencies.async-trait] version = "0.1.42" [dependencies.byteorder] version = "1.4.3" [dependencies.bytes] version = "1.0" [dependencies.digest] version = "0.10.3" [dependencies.env_logger] version = ">=0.9 ,<=0.10" [dependencies.erbium-net] version = "1.0.4" [dependencies.futures] version = "0.3.8" [dependencies.hmac] version = "0.12.1" [dependencies.hyper] version = "0.14.5" features = [ "server", "http1", "stream", "runtime", "tcp", ] optional = true [dependencies.lazy_static] version = "1.4" [dependencies.log] version = "0.4" [dependencies.prometheus] version = "0.13" features = ["process"] [dependencies.rand] version = "0.8" [dependencies.rusqlite] version = ">=0.28, <=0.29" [dependencies.sha2] version = "0.10" [dependencies.tokio] version = "1.8.4" features = ["full"] [dependencies.tokio-util] version = "0.7" features = ["codec"] [dependencies.yaml-rust] version = "0.4" [build-dependencies.vergen] version = ">=6,<=8" [features] default = [ "dhcp", "radv", "http", "dns", ] dhcp = [] dns = [] full = [ "dhcp", "radv", "http", "dns", ] fuzzing = ["dep:arbitrary"] http = [ "hyper", "dhcp", ] radv = [] static = ["rusqlite/bundled"] erbium-core-1.0.5/Cargo.toml.orig000064400000000000000000000030371046102023000147320ustar 00000000000000[package] name = "erbium-core" authors = ["Perry Lorier "] edition = "2021" description = "Network services for small/home networks - Core code" license = "Apache-2.0" readme = "README.md" repository = "https://github.com/isomer/erbium.git" homepage = "https://github.com/isomer/erbium" version.workspace = true [features] full=["dhcp", "radv", "http", "dns"] default=["dhcp", "radv", "http", "dns"] dhcp=[] dns=[] radv=[] http=["hyper", "dhcp"] # Currently can't compile http without dhcp. static=["rusqlite/bundled"] # Statically link dependencies. fuzzing=["dep:arbitrary"] # add arbitrary dependancy. [dependencies] arbitrary = { version = "1.1", features = ["derive"], optional=true} async-trait = { version = "0.1.42" } byteorder = "1.4.3" bytes = "1.0" digest = "0.10.3" env_logger = ">=0.9 ,<=0.10" erbium-net = { path = "../erbium-net", version="1.0.4" } futures = "0.3.8" hmac = "0.12.1" hyper = { version = "0.14.5", features=["server", "http1", "stream", "runtime", "tcp"], optional=true } lazy_static = "1.4" log = "0.4" prometheus = { version="0.13", features=["process"] } rand = "0.8" rusqlite = { version = ">=0.28, <=0.29" } sha2 = "0.10" tokio-util = { version="0.7", features= ["codec"] } tokio = { version = "1.8.4", features = ["full"] } yaml-rust = { version = "0.4" } [[bin]] name="erbium-dns" required-features=["dns"] [[bin]] name="erbium-dhcp" required-features=["dhcp"] [[bin]] name="erbium-lldp" #required-features=["lldp"] [lib] name = "erbium" path = "src/lib.rs" [build-dependencies] vergen = ">=6,<=8" erbium-core-1.0.5/README.md000064400000000000000000000002321046102023000133140ustar 00000000000000# Erbium Core This is the majority of the [erbium](crates.io/crates/erbium) functionality as a library that can be linked into tests, fuzzers and so on. erbium-core-1.0.5/src/acl.rs000064400000000000000000000316711046102023000137440ustar 00000000000000/* Copyright 2023 Perry Lorier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 * * ACL configuration * * ACL checks undergo two phases, the first phase checks if you are "authenticated", which means * "does this client match _any_ of the rules." If you do not match _any_ acl rules you are denied * with a NotAuthenticated error. * * The second phase is checking if the _first_ rule you matched contains the permission the client * is being checked against. If the client does not have this permission then they get a * NotAuthorised error, if they do have this permission then they are permitted. */ use crate::config::*; use erbium_net::addr::{NetAddr, NetAddrExt as _, WithPort as _, UNSPECIFIED6}; use yaml_rust::yaml; #[derive(Debug)] pub struct Permission { pub allow_dns_recursion: bool, pub allow_http: bool, pub allow_http_metrics: bool, pub allow_http_leases: bool, } pub struct Attributes { pub addr: NetAddr, } impl std::fmt::Display for Attributes { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.addr) } } // We implement Default so that when people define an Attributes, they can ..default() all the // parameters they don't care about. impl Default for Attributes { fn default() -> Self { Self { addr: UNSPECIFIED6.with_port(0), } } } #[derive(Debug)] pub struct Acl { pub subnet: Option>, pub unix: Option, pub permission: Permission, } fn check_subnet(attr: &Attributes, prefix: &Prefix) -> bool { match attr.addr.ip() { Some(sockaddr) => prefix.contains(sockaddr), _ => false, } } impl Acl { fn check(&self, attr: &Attributes) -> Option<&'_ Permission> { let mut ok = true; /* Check that the addr is contained within any of the subnets */ ok = ok && self .subnet .as_ref() .map(|ss| ss.iter().any(|s| check_subnet(attr, s))) .unwrap_or(true); /* Check that the addr is unix */ if let Some(unix) = self.unix { use erbium_net::addr::NetAddrExt as _; ok = ok && attr.addr.to_unix_addr().is_some() == unix; } if ok { Some(&self.permission) } else { None } } } pub enum PermissionType { DnsRecursion, Http, HttpLeases, HttpMetrics, } impl std::fmt::Display for PermissionType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use PermissionType::*; match self { DnsRecursion => write!(f, "DNS Recursion"), Http => write!(f, "HTTP"), HttpLeases => write!(f, "HTTP Leases"), HttpMetrics => write!(f, "HTTP Metrics"), } } } #[cfg_attr(test, derive(Debug))] #[derive(Eq, PartialEq)] pub enum AclError { NotAuthenticated, NotAuthorised(String), } impl std::fmt::Display for AclError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use AclError::*; match self { NotAuthenticated => write!(f, "Failed to match any ACLs"), NotAuthorised(perm) => write!(f, "Matched ACL does not have permission {}", perm), } } } /* This finds the first permission that matches, if no permissions match it returns * AclError::NotAuthenticated. */ fn check_authenticated<'v>(acl: &'v [Acl], attr: &Attributes) -> Result<&'v Permission, AclError> { acl.iter() .find_map(|a| a.check(attr)) .ok_or(AclError::NotAuthenticated) } /* This checks that the client matches an ACL ("NotAuthenticated"), and that the ACL has the * required permission ("NotAuthorised"). */ pub fn require_permission( acl: &[Acl], client: &Attributes, perm: PermissionType, ) -> Result<(), AclError> { fn check_permission(perm: bool, name: &str) -> Result<(), AclError> { if perm { Ok(()) } else { Err(AclError::NotAuthorised(name.into())) } } use PermissionType::*; match (check_authenticated(acl, client), perm) { (Ok(perms), DnsRecursion) => check_permission(perms.allow_dns_recursion, "dns-recursion"), (Ok(perms), Http) => check_permission(perms.allow_http, "http"), (Ok(perms), HttpLeases) => check_permission(perms.allow_http_leases, "http-leases"), (Ok(perms), HttpMetrics) => check_permission(perms.allow_http_metrics, "http-metrics"), (Err(err), perm) => { log::warn!("{}: {}: {}", client, perm, err); Err(err) } } } pub fn default_acls(addresses: &[Prefix]) -> Vec { vec![ Acl { /* Any address we hand out by DHCP we should also accept DNS requests from */ subnet: Some(addresses.to_vec()), unix: None, permission: Permission { allow_dns_recursion: true, allow_http_leases: true, allow_http_metrics: true, allow_http: true, }, }, Acl { /* Any v4/v6 localhost should also be able to accept DNS requests. */ subnet: Some(vec![ Prefix::V4(Prefix4 { addr: "127.0.0.0".parse().unwrap(), prefixlen: 8, }), Prefix::V6(Prefix6 { addr: "::1".parse().unwrap(), prefixlen: 128, }), ]), unix: None, permission: Permission { allow_dns_recursion: true, allow_http_leases: true, allow_http_metrics: true, allow_http: true, }, }, Acl { /* Allow API access over the unix domain socket */ subnet: None, unix: Some(true), permission: Permission { allow_dns_recursion: false, allow_http_leases: true, allow_http_metrics: true, allow_http: true, }, }, ] } pub(crate) fn parse_acl(name: &str, fragment: &yaml::Yaml) -> Result, Error> { match fragment { yaml::Yaml::Hash(h) => { let mut subnet = None; let mut unix = None; let mut accesses = vec![]; for (k, v) in h { match (k.as_str(), v) { (Some("match-subnets"), s) => { subnet = parse_array("match-subnets", s, parse_string_prefix)?; } (Some("match-unix"), s) => { unix = parse_boolean("match-unix", s)?; } (Some("apply-access"), a) => { accesses = parse_array("apply-access", a, parse_string)?.ok_or_else(|| { Error::InvalidConfig("apply-access cannot be null".into()) })?; } (Some(m), _) => { return Err(Error::InvalidConfig(format!("Unknown {} key {}", name, m))) } (None, _) => { return Err(Error::InvalidConfig(format!( "{} keys are expected to be strings", name ))) } } } let mut allow_dns_recursion = false; let mut allow_http = false; let mut allow_http_metrics = false; let mut allow_http_leases = false; for access in accesses { match access.as_str() { "dhcp-client" => { allow_dns_recursion = true; } "dns-recursion" => allow_dns_recursion = true, "http" => allow_http = true, "http-metrics" => allow_http_metrics = true, "http-leases" => allow_http_leases = true, "http-ro" => { allow_http = true; allow_http_metrics = true; allow_http_leases = true; } e => return Err(Error::InvalidConfig(format!("Unknown access {}", e))), } } Ok(Some(Acl { subnet, unix, permission: Permission { allow_dns_recursion, allow_http, allow_http_metrics, allow_http_leases, }, })) } e => Err(Error::InvalidConfig(format!( "Expected hash for {}, got {}", name, type_to_name(e) ))), } } #[test] fn acl_not_authenticated() { use erbium_net::addr::{Ipv4Addr, ToNetAddr as _, WithPort as _}; let test_acls = vec![Acl { subnet: Some(vec![Prefix::V4(Prefix4 { addr: "192.0.2.0".parse().unwrap(), prefixlen: 24, })]), unix: None, permission: Permission { allow_dns_recursion: true, allow_http: false, allow_http_leases: false, allow_http_metrics: false, }, }]; let ip = "192.168.0.1".parse::().unwrap().with_port(0); let client = Attributes { addr: ip.to_net_addr(), }; assert_eq!( require_permission(&test_acls, &client, PermissionType::DnsRecursion) .expect_err("Unexpected succeeded") .to_string(), "Failed to match any ACLs" ); } #[test] fn acl_not_authorized() { use erbium_net::addr::{Ipv4Addr, ToNetAddr as _, WithPort as _}; let test_acls = vec![Acl { subnet: Some(vec![Prefix::V4(Prefix4 { addr: "192.0.2.0".parse().unwrap(), prefixlen: 24, })]), unix: None, permission: Permission { allow_dns_recursion: false, allow_http: false, allow_http_leases: false, allow_http_metrics: false, }, }]; let ip = "192.0.2.1".parse::().unwrap().with_port(0); let client = Attributes { addr: ip.to_net_addr(), }; assert_eq!( require_permission(&test_acls, &client, PermissionType::DnsRecursion) .expect_err("Unexpected success") .to_string(), "Matched ACL does not have permission dns-recursion" ); } #[test] fn acl_allowed() { use erbium_net::addr::{Ipv4Addr, ToNetAddr as _, WithPort as _}; let test_acls = vec![Acl { subnet: Some(vec![Prefix::V4(Prefix4 { addr: "192.0.2.0".parse().unwrap(), prefixlen: 24, })]), unix: None, permission: Permission { allow_dns_recursion: true, allow_http: false, allow_http_leases: false, allow_http_metrics: false, }, }]; let ip = "192.0.2.1".parse::().unwrap().with_port(0); let client = Attributes { addr: ip.to_net_addr(), }; assert_eq!( require_permission(&test_acls, &client, PermissionType::DnsRecursion), Ok(()) ); } #[test] fn acl_parse() { load_config_from_string_for_test( "--- acls: - match-subnets: [192.0.2.0/24] apply-access: ['dns-recursion'] ", ) .expect("Failed to parse ACL configuration"); } #[test] fn acl_parse_fail() { assert_eq!( load_config_from_string_for_test( "--- acls: - match-subnets: [192.0.2.0/24] apply-access: ['not-a-permission'] ", ) .expect_err("Bad config unexpectedly successfully parsed") .to_string(), "Invalid Configuration: Unknown access not-a-permission" ); assert_eq!( load_config_from_string_for_test( "--- acls: - not-a-valid-key: 192.0.2.0/24 ", ) .expect_err("Bad config unexpectedly successfully parsed") .to_string(), "Invalid Configuration: Unknown acls key not-a-valid-key" ); assert_eq!( load_config_from_string_for_test( "--- acls: - match-subnets: [192.0.2.0/24] apply-access: null ", ) .expect_err("Bad config unexpectedly successfully parsed") .to_string(), "Invalid Configuration: apply-access cannot be null" ); } erbium-core-1.0.5/src/bin/erbium-conftest.rs000064400000000000000000000025651046102023000170630ustar 00000000000000/* Copyright 2023 Perry Lorier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 * * Dumps out all the configuration, as it's been parsed. * Primarily used for debugging and testing configurations. */ extern crate erbium; #[tokio::main] async fn main() -> Result<(), Box> { let args: Vec<_> = std::env::args_os().collect(); let config_file = match args.len() { 1 => std::path::Path::new("erbium.conf"), 2 => std::path::Path::new(&args[1]), _ => { println!("Usage: {} ", args[0].to_string_lossy()); return Ok(()); } }; println!("Loading config from {}", config_file.display()); let conf = erbium::config::load_config_from_path(config_file).await?; println!("Parse config: {:#?}", conf.read().await); Ok(()) } erbium-core-1.0.5/src/bin/erbium-dhcp.rs000064400000000000000000000046501046102023000161510ustar 00000000000000/* Copyright 2023 Perry Lorier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 * * Thin wrapper to start DHCP services only. */ use futures::StreamExt as _; extern crate erbium; use erbium::dhcp; enum Error { ConfigError(std::path::PathBuf, erbium::config::Error), ServiceError(String), } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match *self { Error::ConfigError(ref path, ref e) => write!( f, "Failed to load config from {}: {}", path.to_string_lossy(), e ), Error::ServiceError(ref e) => write!(f, "{}", e), } } } async fn go() -> Result<(), Error> { let args: Vec<_> = std::env::args_os().collect(); let config_file = match args.len() { 1 => std::path::Path::new("erbium.conf"), 2 => std::path::Path::new(&args[1]), _ => { println!("Usage: {} ", args[0].to_string_lossy()); return Ok(()); } }; let netinfo = erbium_net::netinfo::SharedNetInfo::new().await; let conf = erbium::config::load_config_from_path(config_file) .await .map_err(|e| Error::ConfigError(config_file.to_path_buf(), e))?; let mut services = futures::stream::FuturesUnordered::new(); let dhcp = std::sync::Arc::new( dhcp::DhcpService::new(netinfo.clone(), conf.clone()) .await .map_err(Error::ServiceError)?, ); services.push(tokio::spawn(async move { dhcp.run().await })); while let Some(x) = services.next().await { println!("Service complete: {:?}", x) } Ok(()) } #[tokio::main] async fn main() { match go().await { Ok(()) => (), Err(x) => { println!("Error: {}", x); std::process::exit(1); } } } erbium-core-1.0.5/src/bin/erbium-dns.rs000064400000000000000000000053531046102023000160200ustar 00000000000000/* Copyright 2023 Perry Lorier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 * * Thin wrapper to start DNS services only. */ extern crate erbium; #[cfg(feature = "dns")] use erbium::dns; #[cfg(feature = "dns")] enum Error { Config(erbium::config::Error), Dns(dns::Error), } #[cfg(feature = "dns")] impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use Error::*; match self { Config(e) => write!(f, "Failed to load config: {}", e), Dns(e) => write!(f, "Dns Error: {}", e), } } } #[cfg(feature = "dns")] async fn go() -> Result<(), Error> { use futures::StreamExt as _; let args: Vec<_> = std::env::args_os().collect(); let config_file = match args.len() { 1 => std::path::Path::new("erbium.conf"), 2 => std::path::Path::new(&args[1]), _ => { println!("Usage: {} ", args[0].to_string_lossy()); return Ok(()); } }; let mut services: futures::stream::FuturesUnordered< tokio::task::JoinHandle>, > = futures::stream::FuturesUnordered::new(); let netinfo = erbium_net::netinfo::SharedNetInfo::new().await; let dns = dns::DnsService::new( erbium::config::load_config_from_path(config_file) .await .map_err(Error::Config)?, &netinfo, ) .await .map_err(Error::Dns)?; services.push(tokio::spawn(async move { dns.run().await.map_err(|err| err.to_string()) })); while let Some(x) = services.next().await { println!("Service complete: {:?}", x) } Ok(()) } #[tokio::main] async fn main() { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); log::info!( "erbium-dns {}{}", env!("CARGO_PKG_VERSION"), option_env!("VERGEN_GIT_SHA") .map(|sha| format!(" ({})", sha)) .unwrap_or_else(|| "".into()) ); #[cfg(feature = "dns")] match go().await { Ok(()) => (), Err(x) => { println!("Error: {}", x); std::process::exit(1); } } } erbium-core-1.0.5/src/bin/erbium-lldp.rs000064400000000000000000000050071046102023000161630ustar 00000000000000/* Copyright 2023 Perry Lorier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 * * Thin wrapper to start DHCP services only. */ use futures::StreamExt as _; extern crate erbium; use erbium::lldp; enum Error { ConfigError(std::path::PathBuf, erbium::config::Error), ServiceError(String), } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match *self { Error::ConfigError(ref path, ref e) => write!( f, "Failed to load config from {}: {}", path.to_string_lossy(), e ), Error::ServiceError(ref e) => write!(f, "{}", e), } } } async fn go() -> Result<(), Error> { /* let args: Vec<_> = std::env::args_os().collect(); let config_file = match args.len() { 1 => std::path::Path::new("erbium.conf"), 2 => std::path::Path::new(&args[1]), _ => { println!("Usage: {} ", args[0].to_string_lossy()); return Ok(()); } }; let netinfo = erbium_net::netinfo::SharedNetInfo::new().await; let conf = erbium::config::load_config_from_path(config_file) .await .map_err(|e| Error::ConfigError(config_file.to_path_buf(), e))?; */ let mut services = futures::stream::FuturesUnordered::new(); let lldp = std::sync::Arc::new( lldp::LldpService::new().unwrap(), // netinfo.clone(), conf.clone()) //.await //.map_err(Error::ServiceError)?, ); services.push(tokio::spawn(async move { lldp.run().await })); while let Some(x) = services.next().await { println!("Service complete: {:?}", x) } Ok(()) } #[tokio::main] async fn main() { match go().await { Ok(()) => (), Err(x) => { println!("Error: {}", x); std::process::exit(1); } } } erbium-core-1.0.5/src/config.rs000064400000000000000000001075371046102023000144570ustar 00000000000000/* Copyright 2023 Perry Lorier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 * * Erbium Configuration parsing. */ use erbium_net::addr::*; use std::convert::TryFrom; use std::os::unix::fs::PermissionsExt as _; use tokio::io::AsyncReadExt as _; use yaml_rust::yaml; #[derive(Debug)] pub enum Error { IoError(std::io::Error), Utf8Error(std::string::FromUtf8Error), YamlError(yaml_rust::scanner::ScanError), MissingConfig, MultipleConfigs, ConfigProcessFailed, InvalidConfig(String), } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Error::IoError(e) => write!(f, "{}", e), Error::Utf8Error(e) => { write!(f, "UTF8 Decoding error reading configuration file: {}", e) } Error::YamlError(e) => write!(f, "Yaml parse error while reading configuration: {}", e), Error::MissingConfig => write!(f, "Configuration is empty/missing"), Error::MultipleConfigs => { write!(f, "Configuration file contains multiple configurations") } Error::ConfigProcessFailed => write!(f, "Configuration process failed"), Error::InvalidConfig(e) => write!(f, "Invalid Configuration: {}", e), } } } impl std::error::Error for Error {} impl Error { #[must_use] pub fn annotate(self, s: &str) -> Error { Error::InvalidConfig(format!("{}: {}", self, s)) } } #[derive(Default, Debug, Clone)] pub enum ConfigValue { #[default] NotSpecified, DontSet, Value(T), } impl ConfigValue { pub fn from_option(v: Option) -> Self { match v { None => ConfigValue::DontSet, Some(x) => ConfigValue::Value(x), } } /// Converts an ConfigValue into an Option, leaving DontSet as None. pub fn unwrap_or(&self, n: T) -> Option { match self { ConfigValue::NotSpecified => Some(n), ConfigValue::DontSet => None, ConfigValue::Value(v) => Some(v.clone()), } } pub fn or(&self, n: Option) -> Option { match self { ConfigValue::NotSpecified => n, ConfigValue::DontSet => None, ConfigValue::Value(v) => Some(v.clone()), } } pub fn as_ref(&self) -> ConfigValue<&T> { match self { ConfigValue::NotSpecified => ConfigValue::NotSpecified, ConfigValue::DontSet => ConfigValue::DontSet, ConfigValue::Value(v) => ConfigValue::Value(v), } } // Converts a ConfigValue into an Option, leaving NotSpecified as None. // This is useful if "don't set" has a default value that must be applied anyway. pub fn base_default(&self, n: T) -> Option { match self { ConfigValue::NotSpecified => None, ConfigValue::DontSet => Some(n), ConfigValue::Value(v) => Some(v.clone()), } } // This return T, setting the default, and unspecified to a value. // This is useful if the unspecified value has an obvious required default. pub fn always_unwrap_or(&self, n: T) -> T { match self { ConfigValue::NotSpecified => n, ConfigValue::DontSet => n, ConfigValue::Value(v) => v.clone(), } } #[must_use] pub fn apply_default(&self, n: ConfigValue) -> ConfigValue { match (self, n) { (ConfigValue::NotSpecified, ConfigValue::NotSpecified) => ConfigValue::NotSpecified, (ConfigValue::NotSpecified, ConfigValue::DontSet) => ConfigValue::DontSet, (ConfigValue::NotSpecified, ConfigValue::Value(v)) => ConfigValue::Value(v), (ConfigValue::DontSet, _) => ConfigValue::DontSet, (ConfigValue::Value(v), _) => ConfigValue::Value(v.clone()), } } } pub fn type_to_name(fragment: &yaml::Yaml) -> String { match fragment { yaml::Yaml::Real(_) => "Real".into(), yaml::Yaml::Integer(_) => "Integer".into(), yaml::Yaml::String(_) => "String".into(), yaml::Yaml::Boolean(_) => "Boolean".into(), yaml::Yaml::Array(a) => format!("Array of {}", type_to_name(&a[0])), yaml::Yaml::Hash(_) => "Hash".into(), yaml::Yaml::Alias(_) => "Alias".into(), yaml::Yaml::Null => "Null".into(), yaml::Yaml::BadValue => "Bad Value".into(), } } pub fn parse_i64(name: &str, fragment: &yaml::Yaml) -> Result, Error> { match fragment { yaml::Yaml::Null => Ok(None), yaml::Yaml::Integer(i) => Ok(Some(*i)), e => Err(Error::InvalidConfig(format!( "{} should be of type Integer, not {}", name, type_to_name(e) ))), } } pub fn parse_num>(name: &str, fragment: &yaml::Yaml) -> Result, Error> { match parse_i64(name, fragment) { Ok(None) => Ok(None), Err(e) => Err(e), Ok(Some(v)) => Ok(Some(N::try_from(v).map_err(|_| { Error::InvalidConfig(format!("{} out of range for {}", v, name)) })?)), } } pub fn parse_string(name: &str, fragment: &yaml::Yaml) -> Result, Error> { match fragment { yaml::Yaml::Null => Ok(None), yaml::Yaml::String(s) => Ok(Some(s.into())), e => Err(Error::InvalidConfig(format!( "{} should be of type String, not {}", name, type_to_name(e) ))), } } pub fn parse_boolean(name: &str, fragment: &yaml::Yaml) -> Result, Error> { match fragment { yaml::Yaml::Null => Ok(None), yaml::Yaml::Boolean(b) => Ok(Some(*b)), e => Err(Error::InvalidConfig(format!( "{} should be of type Boolean, not {}", name, type_to_name(e) ))), } } pub fn parse_array( name: &str, fragment: &yaml::Yaml, mut parser: F, ) -> Result>, Error> where F: FnMut(&str, &yaml::Yaml) -> Result, Error>, { match fragment { yaml::Yaml::Null => Ok(None), yaml::Yaml::Array(a) => { let mut v = a .iter() .map(|it| parser(name, it)) .collect::, _>>()? .drain(..) .map(|it| { it.ok_or_else(|| { Error::InvalidConfig(format!("Cannot have a Null value in array {}", name)) }) }) .collect::, _>>()?; v.shrink_to_fit(); Ok(Some(v)) } e => Err(Error::InvalidConfig(format!( "{} should be of type Array, not {}", name, type_to_name(e) ))), } } pub const INTERFACE4: std::net::IpAddr = std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)); pub const INTERFACE6: std::net::IpAddr = std::net::IpAddr::V6(std::net::Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)); fn str_ip(ost: Option) -> Result, Error> { match ost { Some(st) if st == "$self4" => Ok(Some(INTERFACE4)), Some(st) if st == "$self6" => Ok(Some(INTERFACE6)), Some(st) => Some( st.parse() .map_err(|e| Error::InvalidConfig(format!("{}", e))), ) .transpose(), None => Ok(None), } } fn str_ip4(ost: Option) -> Result, Error> { match str_ip(ost) { Err(e) => Err(e), Ok(None) => Ok(None), Ok(Some(std::net::IpAddr::V4(ip4))) => Ok(Some(ip4)), Ok(Some(std::net::IpAddr::V6(ip6))) => Err(Error::InvalidConfig(format!( "Expected v4 address, got v6 address ({})", ip6, ))), } } fn str_ip6(ost: Option) -> Result, Error> { match str_ip(ost) { Err(e) => Err(e), Ok(None) => Ok(None), Ok(Some(std::net::IpAddr::V6(ip6))) => Ok(Some(ip6)), Ok(Some(std::net::IpAddr::V4(ip4))) => Err(Error::InvalidConfig(format!( "Expected v6 address, got v4 address ({})", ip4, ))), } } #[derive(Debug)] enum HexError { InvalidDigit(u8), WrongLength, } impl std::fmt::Display for HexError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { HexError::InvalidDigit(x) => write!(f, "Unexpected hex digit {}", x), HexError::WrongLength => write!(f, "Hex byte is not two digits long"), } } } const fn hexdigit(c: u8) -> Result { match c { b'A'..=b'F' => Ok(c - b'A' + 10), b'a'..=b'f' => Ok(c - b'a' + 10), b'0'..=b'9' => Ok(c - b'0'), _ => Err(HexError::InvalidDigit(c)), } } fn hexbyte(st: &str) -> Result { let mut it = st.bytes(); if let Some(n1) = it.next() { if let Some(n2) = it.next() { if it.next().is_none() { return Ok((hexdigit(n1)? << 4) | hexdigit(n2)?); } } } Err(HexError::WrongLength) } fn str_hwaddr(ost: Option) -> Result>, Error> { ost.map(|st| { st.split(':') /* Vec */ .map(hexbyte) /* Vec> */ .collect() }) .transpose() .map_err(|e| Error::InvalidConfig(e.to_string())) } /// Parses a prefix of the form IP/prefixlen. /// IP can be v4 or v6. /// Currently no error handling on prefixlen is done. fn str_prefix(ost: Option) -> Result, Error> { Ok(ost .map(|st| { let sections = st.split('/').collect::>(); if sections.len() != 2 { Err(Error::InvalidConfig(format!( "Expected IP prefix, but '{}'", st ))) } else { let prefixlen = sections[1] .parse() .map_err(|x| Error::InvalidConfig(format!("{}", x)))?; match str_ip(Some(sections[0].into())) { Ok(Some(std::net::IpAddr::V4(ip4))) => Ok(Some(Prefix::V4(Prefix4 { addr: ip4, prefixlen, }))), Ok(Some(std::net::IpAddr::V6(ip6))) => Ok(Some(Prefix::V6(Prefix6 { addr: ip6, prefixlen, }))), Err(e) => Err(e), Ok(None) => Ok(None), } } }) .transpose()? .flatten()) } /// Parses a prefix of the form IPv4/prefixlen. /// Currently no error handling on prefixlen is done. fn str_prefix4(ost: Option) -> Result, Error> { Ok(ost .map(|st| { let sections = st.split('/').collect::>(); if sections.len() != 2 { Err(Error::InvalidConfig(format!( "Expected IPv4 prefix, but '{}'", st ))) } else { let prefixlen = sections[1] .parse() .map_err(|x| Error::InvalidConfig(format!("{}", x)))?; match str_ip4(Some(sections[0].into())) { Ok(Some(ip4)) => Ok(Some(Prefix4 { addr: ip4, prefixlen, })), Err(e) => Err(e), Ok(None) => Ok(None), } } }) .transpose()? .flatten()) } /// Parses a prefix of the form IPv6/prefixlen. /// Currently no error handling on prefixlen is done. fn str_prefix6(ost: Option) -> Result, Error> { Ok(ost .map(|st| { let sections = st.split('/').collect::>(); if sections.len() != 2 { Err(Error::InvalidConfig(format!( "Expected IPv6 prefix, but '{}'", st ))) } else { let prefixlen = sections[1] .parse() .map_err(|x| Error::InvalidConfig(format!("{}", x)))?; match str_ip6(Some(sections[0].into())) { Ok(Some(ip6)) => Ok(Some(Prefix6 { addr: ip6, prefixlen, })), Err(e) => Err(e), Ok(None) => Ok(None), } } }) .transpose()? .flatten()) } fn str_duration(ost: Option) -> Result, Error> { ost.map(|st| { let mut num = None; let mut ret = Default::default(); for c in st.chars() { match c { '0'..='9' => { if let Some(n) = num { num = Some(n * 10 + c as u64 - '0' as u64); } else { num = Some(c as u64 - '0' as u64); } } 's' => { ret += std::time::Duration::from_secs(num.take().unwrap()); } 'm' => { ret += std::time::Duration::from_secs(num.take().unwrap() * 60); } 'h' => { ret += std::time::Duration::from_secs(num.take().unwrap() * 3600); } 'd' => { ret += std::time::Duration::from_secs(num.take().unwrap() * 86400); } 'w' => { ret += std::time::Duration::from_secs(num.take().unwrap() * 7 * 86400); } x if x.is_whitespace() => (), '_' => (), _ => { return Err(Error::InvalidConfig(format!( "Unexpected {} in duration", c ))) } } } if let Some(n) = num { ret += std::time::Duration::from_secs(n); } Ok(ret) }) .transpose() } pub fn parse_string_hwaddr(name: &str, fragment: &yaml::Yaml) -> Result>, Error> { parse_string(name, fragment).and_then(str_hwaddr) } pub fn parse_string_ip( name: &str, fragment: &yaml::Yaml, ) -> Result, Error> { parse_string(name, fragment) .and_then(|s| str_ip(s).map_err(|e| Error::InvalidConfig(format!("{}: {}", name, e)))) } pub fn parse_string_ip4( name: &str, fragment: &yaml::Yaml, ) -> Result, Error> { parse_string(name, fragment) .and_then(|s| str_ip4(s).map_err(|e| Error::InvalidConfig(format!("{}: {}", name, e)))) } pub fn parse_string_ip6( name: &str, fragment: &yaml::Yaml, ) -> Result, Error> { parse_string(name, fragment) .and_then(|s| str_ip6(s).map_err(|e| Error::InvalidConfig(format!("{}: {}", name, e)))) } pub fn parse_string_prefix(name: &str, fragment: &yaml::Yaml) -> Result, Error> { parse_string(name, fragment) .and_then(|s| str_prefix(s).map_err(|e| Error::InvalidConfig(format!("{}: {}", name, e)))) } pub fn parse_string_prefix4(name: &str, fragment: &yaml::Yaml) -> Result, Error> { parse_string(name, fragment).and_then(str_prefix4) } pub fn parse_string_prefix6(name: &str, fragment: &yaml::Yaml) -> Result, Error> { parse_string(name, fragment) .and_then(|s| str_prefix6(s).map_err(|e| Error::InvalidConfig(format!("{}: {}", name, e)))) } pub fn str_sockaddr(ost: Option) -> Result, Error> { fn std_to_netaddr(src: std::net::SocketAddr) -> NetAddr { use std::net::SocketAddr::*; match src { V4(v4) => v4.into(), V6(v6) => v6.into(), } } ost.map(|st| match st.get(0..1) { Some("@") => UnixAddr::new_abstract(st[1..].as_bytes()) .map(to_net_addr) .map_err(|e| Error::InvalidConfig(format!("{} ({})", e, st))), Some(_) if st.contains('/') => UnixAddr::new(st.as_bytes()) .map(to_net_addr) .map_err(|e| Error::InvalidConfig(format!("{} ({})", e, st))), Some(_) => st .parse::() .map(std_to_netaddr) .map_err(|e| Error::InvalidConfig(format!("{} ({})", e, st))), None => Err(Error::InvalidConfig( "Invalid socket address, expected unix socket or ip socket".into(), )), }) .transpose() } pub fn parse_string_sockaddr(name: &str, fragment: &yaml::Yaml) -> Result, Error> { parse_string(name, fragment) .and_then(|s| str_sockaddr(s).map_err(|e| Error::InvalidConfig(format!("{}: {}", name, e)))) } pub fn parse_duration( name: &str, fragment: &yaml::Yaml, ) -> Result, Error> { if let yaml::Yaml::Integer(i) = fragment { Ok(Some(std::time::Duration::from_secs(*i as u64))) } else { parse_string(name, fragment).and_then(str_duration) } } pub trait PrefixOps { type Ip; fn network(&self) -> Self::Ip; fn netmask(&self) -> Self::Ip; fn broadcast(&self) -> Self::Ip; } pub trait Match { fn contains(&self, ip: Ip) -> bool; } #[derive(Debug, Eq, Clone)] pub struct Prefix4 { pub addr: std::net::Ipv4Addr, pub prefixlen: u8, } impl Prefix4 { pub fn new(addr: std::net::Ipv4Addr, prefixlen: u8) -> Self { assert!(prefixlen <= 32); Self { addr, prefixlen } } } impl PrefixOps for Prefix4 { type Ip = std::net::Ipv4Addr; fn network(&self) -> std::net::Ipv4Addr { (u32::from(self.addr) & u32::from(self.netmask())).into() } fn netmask(&self) -> std::net::Ipv4Addr { (!0xffffffff_u32 .checked_shr(self.prefixlen as u32) .unwrap_or(0)) .into() } fn broadcast(&self) -> std::net::Ipv4Addr { (u32::from(self.network()) | !u32::from(self.netmask())).into() } } impl Match for Prefix4 { fn contains(&self, ip: std::net::Ipv4Addr) -> bool { u32::from(ip) & u32::from(self.netmask()) == u32::from(self.addr) } } impl Match for Prefix4 { fn contains(&self, ip: std::net::Ipv6Addr) -> bool { match ip.octets() { // If this is a ::ffff:a.b.c.d address, check it against the v4 equivalent. [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, a, b, c, d] => { self.contains(std::net::Ipv4Addr::new(a, b, c, d)) } _ => false, } } } impl PartialEq for Prefix4 { fn eq(&self, other: &Self) -> bool { self.network() == other.network() && self.netmask() == other.netmask() } } #[derive(Debug, Eq, Clone)] pub struct Prefix6 { pub addr: std::net::Ipv6Addr, pub prefixlen: u8, } impl Prefix6 { pub fn new(addr: std::net::Ipv6Addr, prefixlen: u8) -> Self { assert!(prefixlen <= 128); Self { addr, prefixlen } } } impl PrefixOps for Prefix6 { type Ip = std::net::Ipv6Addr; fn network(&self) -> std::net::Ipv6Addr { (u128::from(self.addr) & u128::from(self.netmask())).into() } fn netmask(&self) -> std::net::Ipv6Addr { (!(0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffff_u128 .checked_shr(self.prefixlen as u32) .unwrap_or(0))) .into() } fn broadcast(&self) -> std::net::Ipv6Addr { /* v6 addresses don't have a "broadcast". * Perhaps this should be "all nodes multicast" instead. */ (u128::from(self.network()) | !u128::from(self.netmask())).into() } } impl Match for Prefix6 { fn contains(&self, ip: std::net::Ipv6Addr) -> bool { u128::from(ip) & u128::from(self.netmask()) == u128::from(self.addr) } } impl Match for Prefix6 { fn contains(&self, ip: std::net::Ipv4Addr) -> bool { match self.network().octets() { // If this is a ::ffff:a.b.c.d prefix, check it against the v4 equivalent. [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, a, b, c, d] => Prefix4::new( std::net::Ipv4Addr::new(a, b, c, d), self.prefixlen - (128 - 32), ) .contains(ip), _ => false, } } } impl PartialEq for Prefix6 { fn eq(&self, other: &Self) -> bool { self.network() == other.network() && self.netmask() == other.netmask() } } #[derive(Debug, Eq, PartialEq, Clone)] pub enum Prefix { V4(Prefix4), V6(Prefix6), } impl From for Prefix { fn from(p: Prefix4) -> Self { Self::V4(p) } } impl From for Prefix { fn from(p: Prefix6) -> Self { Self::V6(p) } } impl Prefix { pub fn new(ip: std::net::IpAddr, prefixlen: u8) -> Self { use std::net::IpAddr::*; match ip { V4(ip4) => Self::V4(Prefix4::new(ip4, prefixlen)), V6(ip6) => Self::V6(Prefix6::new(ip6, prefixlen)), } } } impl PrefixOps for Prefix { type Ip = std::net::IpAddr; fn network(&self) -> Self::Ip { use std::net::IpAddr::*; match self { Prefix::V4(p4) => V4(p4.network()), Prefix::V6(p6) => V6(p6.network()), } } fn netmask(&self) -> Self::Ip { use std::net::IpAddr::*; match self { Prefix::V4(p4) => V4(p4.netmask()), Prefix::V6(p6) => V6(p6.netmask()), } } fn broadcast(&self) -> Self::Ip { use std::net::IpAddr::*; match self { Prefix::V4(p4) => V4(p4.broadcast()), Prefix::V6(p6) => V6(p6.broadcast()), } } } impl Match for Prefix { fn contains(&self, ip: std::net::IpAddr) -> bool { match (self, ip) { (Prefix::V4(p4), std::net::IpAddr::V4(ip4)) => p4.contains(ip4), (Prefix::V6(p6), std::net::IpAddr::V6(ip6)) => p6.contains(ip6), // For ::ffff:a.b.c.d matches. (Prefix::V4(p4), std::net::IpAddr::V6(ip6)) => p4.contains(ip6), (Prefix::V6(p6), std::net::IpAddr::V4(ip4)) => p6.contains(ip4), } } } impl Match for Prefix { fn contains(&self, ip: std::net::Ipv4Addr) -> bool { self.contains(std::net::IpAddr::V4(ip)) } } impl Match for Prefix { fn contains(&self, ip: std::net::Ipv6Addr) -> bool { self.contains(std::net::IpAddr::V6(ip)) } } #[derive(Debug)] // Sometimes you want to only bind to interfaces that are necessary, rather than the unspecified // address. This allows multiple processes to cooperate in providing a particular service on // differing interfaces. // // To support this, we look at the "addresses" configuration option, and only bind to addresses of // interfaces that match a prefix there. // // If this heuristic is incorrect, then people can override this with "dns-listeners". pub enum AddressType { Addresses(Vec), BindInterface, } enum DefaultAddressType { Unspecified, Interface, } impl AddressType { pub async fn as_sockaddrs( &self, addresses: &[Prefix], netinfo: &erbium_net::netinfo::SharedNetInfo, port: u16, ) -> Vec { match self { AddressType::Addresses(addrs) => addrs.clone(), AddressType::BindInterface => find_listener_addresses(addresses, netinfo) .await .iter() .map(|ip| ip.with_port(port)) .collect(), } } } impl Default for AddressType { fn default() -> Self { AddressType::Addresses(vec![ std::net::SocketAddrV6::new(UNSPECIFIED6, 0, 0, 0).into() ]) } } #[derive(Debug, Default)] pub struct Config { #[cfg(feature = "dhcp")] pub dhcp: crate::dhcp::config::Config, pub ra: crate::radv::config::Config, pub dns_servers: Vec, pub dns_search: Vec, pub captive_portal: Option, pub addresses: Vec, pub listeners: Vec, pub dns_listeners: AddressType, #[cfg(feature = "dns")] pub dns_routes: Vec, pub acls: Vec, } pub type SharedConfig = std::sync::Arc>; async fn find_listener_addresses( addresses: &[Prefix], netinfo: &erbium_net::netinfo::SharedNetInfo, ) -> Vec { let mut ret = vec![]; for (ifaddr, _len) in netinfo.get_if_prefixes().await { for addr in addresses { use crate::config::Match as _; if addr.contains(ifaddr) { ret.push(ifaddr) } } } ret } fn load_config_from_string(cfg: &str) -> Result { let y = yaml::YamlLoader::load_from_str(cfg).map_err(Error::YamlError)?; match y.len() { 0 => return Err(Error::MissingConfig), 1 => (), _ => return Err(Error::MultipleConfigs), } if let Some(fragment) = y[0].as_hash() { let mut ra = None; #[cfg(feature = "dhcp")] let mut dhcp = None; #[cfg(feature = "dns")] let mut dns_servers = vec![INTERFACE4, INTERFACE6]; #[cfg(not(feature = "dns"))] let mut dns_servers = vec![]; let mut dns_search = vec![]; let mut captive_portal = None; let mut addresses = None; let mut listeners = None; let mut dns_listeners = None; #[cfg(feature = "dns")] let mut dns_routes = None; let mut default_listen_style = DefaultAddressType::Unspecified; let mut acls = None; for (k, v) in fragment { match (k.as_str(), v) { (Some("dhcp"), _) => return Err(Error::InvalidConfig("The dhcp section has been replaced with dhcp-policies section, please see the manpage for more details".into())), #[cfg(feature = "dhcp")] (Some("dhcp-policies"), d) => dhcp = crate::dhcp::config::Config::new(d) .map_err(|e| e.annotate("while parsing dhcp-policies"))?, #[cfg(not(feature = "dhcp"))] (Some("dhcp-policies"), _) => (), (Some("router-advertisements"), r) => ra = crate::radv::config::parse(r) .map_err(|e| e.annotate("while parsing router-advertisements"))?, (Some("dns-servers"), s) => { dns_servers = parse_array("dns-servers", s, parse_string_ip)? .ok_or_else(|| Error::InvalidConfig("dns-servers cannot be null".into()))? } (Some("dns-search"), s) => { dns_search = parse_array("dns-search", s, parse_string)? .ok_or_else(|| Error::InvalidConfig("dns-search cannot be null".into()))? } (Some("captive-portal"), s) => { captive_portal = parse_string("captive-portal", s)?; } (Some("addresses"), s) => { addresses = parse_array("addresses", s, parse_string_prefix)?; } (Some("api-listeners"), s) => { listeners = parse_array("api-listeners", s, parse_string_sockaddr)?; } (Some("dhcp-listeners"), _) => { return Err(Error::InvalidConfig("dhcp-listeners is deprecated, because it cannot work correclty".into())); } (Some("dns-listeners"), s) => { dns_listeners = parse_array("dns-listeners",s, parse_string_sockaddr)? .map(AddressType::Addresses); } (Some("default-listen-style"), s) => { match s.as_str() { None => return Err(Error::InvalidConfig(format!("invalid default-listen-style type: {}", type_to_name(s)))), Some("bind-addresses-interfaces") => default_listen_style = DefaultAddressType::Interface, Some("bind-unspecified") => default_listen_style = DefaultAddressType::Unspecified, Some(o) => return Err(Error::InvalidConfig(format!("invalid default-listen-style {}, expected bind-addresses-interfaces or bind-unspecified", o))), } }, (Some("acls"), s) => { acls = parse_array("acls", s, crate::acl::parse_acl)?; } (Some("dns-routes"), s) => { #[cfg(feature = "dns")] { dns_routes = crate::dns::config::parse_dns_routes("dns-routes", s)?; } } (Some(x), _) => { return Err(Error::InvalidConfig(format!( "Unknown configuration option {}", x ))) } (None, _) => { return Err(Error::InvalidConfig(format!( "Config should be keyed by String, not {}", type_to_name(k) ))) } } } let addresses = addresses.unwrap_or_default(); let conf = Config { #[cfg(feature = "dhcp")] dhcp: dhcp.unwrap_or_default(), ra: ra.unwrap_or_default(), dns_servers, dns_search, dns_listeners: dns_listeners.unwrap_or_else(|| match default_listen_style { DefaultAddressType::Unspecified => { AddressType::Addresses(vec![std::net::SocketAddrV6::new( UNSPECIFIED6, 53, 0, 0, ) .into()]) } DefaultAddressType::Interface => AddressType::BindInterface, }), #[cfg(feature = "dns")] dns_routes: dns_routes.unwrap_or_default(), captive_portal, listeners: listeners.unwrap_or_else(|| { vec![UnixAddr::new("/var/lib/erbium/control") .unwrap() .to_net_addr()] }), acls: acls.unwrap_or_else(|| crate::acl::default_acls(&addresses)), addresses, }; Ok(std::sync::Arc::new(tokio::sync::RwLock::new(conf))) } else { Err(Error::InvalidConfig( "Top level configuration should be a Hash".into(), )) } } #[cfg(test)] pub fn load_config_from_string_for_test(cfg: &str) -> Result { load_config_from_string(cfg) } /* We support reading configs from a yaml file, _or_ a program (eg a shell script?) that outputs * yaml on stdout. * * TODO: Implement reading a directory of configs. */ pub async fn load_config_from_path(path: &std::path::Path) -> Result { let metadata = std::fs::metadata(path).map_err(Error::IoError)?; let configdata = if metadata.permissions().mode() & 0o111 != 0 { let output = tokio::process::Command::new(path) .output() .await .map_err(Error::IoError)?; if !output.status.success() { return Err(Error::ConfigProcessFailed); } String::from_utf8(output.stdout).map_err(Error::Utf8Error)? } else { let mut contents = vec![]; tokio::fs::File::open(path) .await .map_err(Error::IoError)? .read_to_end(&mut contents) .await .map_err(Error::IoError)?; String::from_utf8(contents).map_err(Error::Utf8Error)? }; load_config_from_string(&configdata) } #[test] fn test_config_parse() -> Result<(), Error> { load_config_from_string( "--- dhcp-policies: - match-interface: eth0 apply-dns-servers: ['8.8.8.8', '8.8.4.4'] apply-subnet: 192.168.0.0/24 apply-time-offset: 3600 apply-domain-name: erbium.dev apply-forward: false apply-mtu: 1500 apply-broadcast: 192.168.255.255 apply-rebind-time: 120 apply-renewal-time: 90s apply-arp-timeout: 1w apply-routes: - prefix: 192.0.2.0/24 next-hop: 192.0.2.254 policies: - { match-host-name: myhost, apply-address: 192.168.0.1 } - match-interface: dmz apply-dns-servers: ['8.8.8.8'] apply-subnet: 192.0.2.0/24 # Reserve some space from the pool for servers policies: - apply-range: {start: 192.0.2.10, end: 192.0.2.20} # From the reserved pool, assign a static address. policies: - { match-hardware-address: 00:01:02:03:04:05, apply-address: 192.168.0.2 } # Reserve space for VPN endpoints - match-user-class: VPN apply-subnet: 192.0.2.128/25 router-advertisements: eth0: ", )?; Ok(()) } #[test] fn test_simple_config_parse() -> Result<(), Error> { load_config_from_string( "--- dns-servers: [$self4, $self6] dns-search: ['example.com'] addresses: [192.0.2.0/24, 2001:db8::/64] router-advertisements: eth0: lifetime: 1h ", )?; Ok(()) } #[test] fn test_listeners_parse() -> Result<(), Error> { load_config_from_string( "--- dns-search: ['example.com'] addresses: [192.0.2.0/24, 2001:db8::/64] dns-listeners: [192.0.2.0:53] ", )?; Ok(()) } #[test] fn test_duration() { assert_eq!( parse_duration("test", &yaml::Yaml::String("5s".into())).unwrap(), Some(std::time::Duration::from_secs(5)) ); assert_eq!( parse_duration("test", &yaml::Yaml::String("1w2d3h4m5s".into())).unwrap(), Some(std::time::Duration::from_secs( 7 * 86400 + 2 * 86400 + 3 * 3600 + 4 * 60 + 5 )) ); } #[test] fn test_prefix() { let p1 = "2001:db8::1".parse().unwrap(); let p2 = "2001:db8::".parse().unwrap(); assert_eq!(Prefix::new(p1, 64), Prefix::new(p2, 64)); } #[test] fn test_prefix6_contains() { let net1 = "::ffff:192.0.2.0".parse().unwrap(); let ip1: std::net::Ipv6Addr = "::ffff:192.0.2.1".parse().unwrap(); let ip2: std::net::Ipv6Addr = "::ffff:192.168.0.1".parse().unwrap(); assert!(Prefix6::new(net1, 120).contains(ip1)); assert!(!Prefix6::new(net1, 120).contains(ip2)); } #[test] fn test_cross_ip_version_contains() { let net6 = "::ffff:192.0.2.0".parse().unwrap(); let net4 = "192.0.2.0".parse().unwrap(); let ip6: std::net::Ipv6Addr = "::ffff:192.0.2.1".parse().unwrap(); let ip4: std::net::Ipv4Addr = "192.0.2.1".parse().unwrap(); let bad6: std::net::Ipv6Addr = "::ffff:10.0.0.1".parse().unwrap(); let bad4: std::net::Ipv6Addr = "::ffff:10.0.0.1".parse().unwrap(); assert!(Prefix6::new(net6, 120).contains(ip4)); assert!(Prefix6::new(net6, 120).contains(ip6)); assert!(Prefix4::new(net4, 24).contains(ip4)); assert!(Prefix4::new(net4, 24).contains(ip6)); assert!(!Prefix6::new(net6, 120).contains(bad4)); assert!(!Prefix6::new(net6, 120).contains(bad6)); assert!(!Prefix4::new(net4, 24).contains(bad4)); assert!(!Prefix4::new(net4, 24).contains(bad6)); assert!(Prefix::new(net6.into(), 120).contains(ip4)); assert!(Prefix::new(net6.into(), 120).contains(ip6)); assert!(Prefix::new(net4.into(), 24).contains(ip4)); assert!(Prefix::new(net4.into(), 24).contains(ip6)); assert!(!Prefix::new(net6.into(), 120).contains(bad4)); assert!(!Prefix::new(net6.into(), 120).contains(bad6)); assert!(!Prefix::new(net4.into(), 24).contains(bad4)); assert!(!Prefix::new(net4.into(), 24).contains(bad6)); } erbium-core-1.0.5/src/dhcp/config.rs000064400000000000000000000616461046102023000153750ustar 00000000000000/* Copyright 2023 Perry Lorier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 * * DHCP Configuration parsing. */ use super::dhcppkt; use std::convert::TryFrom as _; use std::ops::Sub; use yaml_rust::yaml; pub use crate::config::*; #[derive(Debug, Default)] pub struct Policy { pub match_all: bool, pub match_interface: Option>, pub match_chaddr: Option>, pub match_subnet: Option, pub match_other: std::collections::HashMap>, pub apply_address: Option, pub apply_default_lease: Option, pub apply_max_lease: Option, pub apply_other: std::collections::HashMap>, pub policies: Vec, pub(super) address_cache: std::sync::Mutex>>, } impl Clone for Policy { fn clone(&self) -> Self { Self { address_cache: Default::default(), match_interface: self.match_interface.clone(), match_chaddr: self.match_chaddr.clone(), match_other: self.match_other.clone(), apply_address: self.apply_address.clone(), apply_other: self.apply_other.clone(), policies: self.policies.clone(), ..*self } } } impl Policy { fn get_all_used_addresses(&self) -> std::collections::HashSet { /* We cache the result of this. */ if self.address_cache.lock().unwrap().borrow().is_none() { /* Cache is cold, heat it. */ let mut addrset: std::collections::HashSet = Default::default(); if let Some(address) = &self.apply_address { addrset.extend(address.iter()); } for p in &self.policies { addrset.extend(p.get_all_used_addresses().iter()); } self.address_cache.lock().unwrap().replace(Some(addrset)); } self.address_cache.lock().unwrap().borrow().clone().unwrap() } } #[derive(Debug, Default)] pub struct Config { pub policies: Vec, } impl Config { /// Function to provide a dummy policy for fuzzing. #[cfg(fuzzing)] pub fn get_fuzzing_config() -> Self { Self { policies: vec![Policy { match_subnet: Some( erbium_net::Ipv4Subnet::new("192.0.2.0".parse().unwrap(), 24).unwrap(), ), ..Default::default() }], } } pub fn get_all_used_addresses(&self) -> std::collections::HashSet { self.policies .iter() .map(Policy::get_all_used_addresses) .fold( std::collections::HashSet::::new(), |mut acc, e| { acc.extend(e); acc }, ) } fn parse_routes(fragment: &yaml::Yaml) -> Result>, Error> { match fragment { yaml::Yaml::Null => Ok(None), yaml::Yaml::Array(yroutes) => { let mut routes = Vec::new(); for route in yroutes { let mut prefix = None; let mut nexthop = None; if let Some(h) = route.as_hash() { for (k, v) in h { match k { yaml::Yaml::Null => { return Err(Error::InvalidConfig( "Routes cannot have null keys".into(), )) } yaml::Yaml::String(s) if s == "next-hop" => { nexthop = Some(parse_string_ip4("next-hop", v)?.ok_or_else( || Error::InvalidConfig("next-hop cannot be null".into()), )?); } yaml::Yaml::String(s) if s == "prefix" => match v { yaml::Yaml::Null => { return Err(Error::InvalidConfig( "prefix cannot be null".into(), )) } yaml::Yaml::String(s) => { let mut it = s.split('/'); let ip = it.next().unwrap().parse().map_err(|e| { Error::InvalidConfig(format!("{}", e)) })?; /* TODO: remove unwrap */ let prefixlen = it.next().unwrap().parse().unwrap(); prefix = Some( erbium_net::Ipv4Subnet::new(ip, prefixlen).map_err( |e| Error::InvalidConfig(format!("{}", e)), )?, ); } e => { return Err(Error::InvalidConfig(format!( "prefix has unexpected type {:?}", e, ))) } }, yaml::Yaml::String(s) => { return Err(Error::InvalidConfig(format!( "Unexpected key in route: {:?}", s ))); } e => { return Err(Error::InvalidConfig(format!( "Key has unexpected type: {:?}", e ))) } } } if let Some(prefix) = prefix { if let Some(nexthop) = nexthop { routes.push(dhcppkt::Route { prefix, nexthop }); } else { return Err(Error::InvalidConfig( "Missing next-hop: in route".into(), )); } } else { return Err(Error::InvalidConfig("Missing prefix: in route".into())); } } else { return Err(Error::InvalidConfig(format!( "Expected hash, got '{:?}'", route ))); } } Ok(Some(routes)) } e => Err(Error::InvalidConfig(format!( "Expected routes, got {:?}", e ))), } } fn parse_subnet(fragment: &yaml::Yaml) -> Result, Error> { match fragment { yaml::Yaml::Null => Ok(None), yaml::Yaml::String(s) => { let sections: Vec<&str> = s.split('/').collect(); if sections.len() != 2 { Err(Error::InvalidConfig(format!( "Expected IP prefix, but '{}'", s ))) } else { Ok(Some( erbium_net::Ipv4Subnet::new( sections[0] .parse() .map_err(|x| Error::InvalidConfig(format!("{}", x)))?, sections[1] .parse() .map_err(|x| Error::InvalidConfig(format!("{}", x)))?, ) .map_err(|_| { Error::InvalidConfig(format!("Prefix length too short {:?}", sections)) })?, )) } } e => Err(Error::InvalidConfig(format!( "Expected IP prefix as string, got {:?}", e ))), } } fn parse_number(value: &yaml::Yaml) -> Result, Error> { match value { yaml::Yaml::Null => Ok(None), yaml::Yaml::Integer(i) => Ok(Some(*i)), e => Err(Error::InvalidConfig(format!( "Expected Number, got '{:?}'", e ))), } } fn parse_generic( name: &str, value: &yaml::Yaml, ) -> Result<(dhcppkt::DhcpOption, Option), Error> { let maybe_opt = dhcppkt::name_to_option(name); if let Some(opt) = maybe_opt { use dhcppkt::*; Ok(( opt, match opt.get_type() { Some(DhcpOptionType::String) => { parse_string(name, value)?.map(DhcpOptionTypeValue::String) } Some(DhcpOptionType::IpList) => { parse_array(name, value, parse_string_ip4)?.map(DhcpOptionTypeValue::IpList) } Some(DhcpOptionType::Routes) => { Config::parse_routes(value)?.map(DhcpOptionTypeValue::Routes) } Some(DhcpOptionType::Ip) => { parse_string_ip4(name, value)?.map(DhcpOptionTypeValue::Ip) } Some(DhcpOptionType::I32) => Config::parse_number(value)? .map(|i| { i32::try_from(i).map_err(|_| { Error::InvalidConfig(format!("Integer {} out of range", i)) }) }) .transpose()? .map(DhcpOptionTypeValue::I32), Some(DhcpOptionType::U8) => Config::parse_number(value)? .map(|i| { u8::try_from(i).map_err(|_| { Error::InvalidConfig(format!("Integer {} out of range", i)) }) }) .transpose()? .map(DhcpOptionTypeValue::U8), Some(DhcpOptionType::U16) => Config::parse_number(value)? .map(|i| { u16::try_from(i).map_err(|_| { Error::InvalidConfig(format!("Integer {} out of range", i)) }) }) .transpose()? .map(DhcpOptionTypeValue::U16), Some(DhcpOptionType::U32) => Config::parse_number(value)? .map(|i| { u32::try_from(i).map_err(|_| { Error::InvalidConfig(format!("Integer {} out of range", i)) }) }) .transpose()? .map(DhcpOptionTypeValue::U32), Some(DhcpOptionType::Seconds16) => parse_duration(name, value)? .map(|i| { u16::try_from(i.as_secs()).map_err(|_| { Error::InvalidConfig(format!( "Duration {}s out of range", i.as_secs() )) }) }) .transpose()? .map(DhcpOptionTypeValue::U16), Some(DhcpOptionType::Seconds32) => parse_duration(name, value)? .map(|i| { u32::try_from(i.as_secs()).map_err(|_| { Error::InvalidConfig(format!( "Duration {}s out of range", i.as_secs() )) }) }) .transpose()? .map(DhcpOptionTypeValue::U32), Some(DhcpOptionType::Bool) => { parse_boolean(name, value)?.map(|b| DhcpOptionTypeValue::U8(b as u8)) } Some(DhcpOptionType::HwAddr) => { parse_string_hwaddr(name, value)?.map(DhcpOptionTypeValue::HwAddr) } Some(DhcpOptionType::DomainList) => { crate::config::parse_array(name, value, parse_string)? .map(DhcpOptionTypeValue::DomainList) } Some(DhcpOptionType::Unknown) => { return Err(Error::InvalidConfig(format!( "Option {} not supported", name ))) } None => { return Err(Error::InvalidConfig(format!( "Missing type information for {}", name ))) } }, )) } else { Err(Error::InvalidConfig(format!("Unknown option {}", name))) } } fn parse_policy(fragment: &yaml::Yaml) -> Result { if let Some(h) = fragment.as_hash() { let mut policy: Policy = Default::default(); let mut addresses: Option> = None; for (k, v) in h { match k.as_str() { Some("match-interface") => { policy.match_interface = Some( parse_string(k.as_str().unwrap(), v) .map_err(|x| x.annotate("Failed to parse match-interface"))?, ); } Some("match-hardware-address") => { if policy.match_chaddr.is_some() { return Err(Error::InvalidConfig( "match-hardware-address specified twice".into(), )); } policy.match_chaddr = parse_string_hwaddr("match-hardware-address", v) .map_err(|x| x.annotate("Failed to parse match-hardware-address"))?; } Some("match-subnet") => { if policy.match_subnet.is_some() { return Err(Error::InvalidConfig( "match-subnet specified twice".into(), )); } policy.match_subnet = Some( Config::parse_subnet(v) .map_err(|x| x.annotate("Failed to parse match-subnet"))? .ok_or_else(|| { Error::InvalidConfig("match-subnet cannot be nil".into()) })?, ); } Some(x) if x.starts_with("match-") => { let name = &x[6..]; let (opt, value) = Config::parse_generic(name, v) .map_err(|e| e.annotate(&format!("Failed to parse {}", x)))?; policy.match_other.insert(opt, value); } Some("apply-address") => { let addresses = addresses.get_or_insert_with(Vec::new); addresses.push( parse_string_ip4("apply-address", v) .map_err(|x| x.annotate("Failed to parse apply-address"))? .ok_or_else(|| { Error::InvalidConfig("apply-address cannot be nil".into()) })?, ); } Some("apply-default-lease") => { policy.apply_default_lease = Some( parse_duration("apply-default-lease", v) .map_err(|x| x.annotate("Failed to parse apply-default-lease"))? .ok_or_else(|| { Error::InvalidConfig("apply-default-lease cannot be nil".into()) })?, ); } Some("apply-max-lease") => { policy.apply_default_lease = Some( parse_duration("apply-max-lease", v) .map_err(|x| x.annotate("Failed to parse apply-max-lease"))? .ok_or_else(|| { Error::InvalidConfig("apply-max-lease cannot be nil".into()) })?, ); } Some("apply-range") => { if let Some(range) = v.as_hash() { let mut start: Option = None; let mut end: Option = None; for (rangek, rangev) in range { match rangek.as_str() { Some("start") => { start = Some( parse_string_ip4("start", rangev) .map_err(|x| { x.annotate("Failed to parse range start") })? .ok_or_else(|| { Error::InvalidConfig( "range start cannot be nil".into(), ) })?, ) } Some("end") => { end = Some( parse_string_ip4("end", rangev) .map_err(|x| { x.annotate("Failed to parse range end") })? .ok_or_else(|| { Error::InvalidConfig( "range end cannot be nil".into(), ) })?, ) } Some(e) => { return Err(Error::InvalidConfig(format!( "Unexpected key in range: {}", e ))) } None => { return Err(Error::InvalidConfig(format!( "range key is not a string, instead: '{:?}'", rangek ))) } } } let start = start.ok_or_else(|| { Error::InvalidConfig("Missing start in range".into()) })?; let end = end.ok_or_else(|| { Error::InvalidConfig("Missing end in range".into()) })?; let addresses = addresses.get_or_insert_with(Vec::new); for i in u32::from(start)..=u32::from(end) { addresses.push(i.into()); } } else { return Err(Error::InvalidConfig(format!( "Range should be a hash, not '{:?}'", v ))); } } Some("apply-subnet") => { let subnet = Config::parse_subnet(v) .map_err(|x| x.annotate("Failed to parse apply-subnet"))? .ok_or_else(|| { Error::InvalidConfig("apply-subnet cannot be nil".into()) })?; let base: u32 = subnet.network().into(); let addresses = addresses.get_or_insert_with(Vec::new); for i in 1..(((1 << (32 - subnet.prefixlen)) - 1) - 1) { addresses.push((base + i).into()) } } Some(x) if x.starts_with("apply-") => { let name = &x[6..]; let (opt, value) = Config::parse_generic(name, v) .map_err(|e| e.annotate(&format!("Failed to parse {}", x)))?; if policy.apply_other.insert(opt, value).is_some() { return Err(Error::InvalidConfig(format!( "Duplicate specification of {}", x ))); } } Some("policies") => { if !policy.policies.is_empty() { return Err(Error::InvalidConfig( "Can't specify policies twice in one policy".into(), )); } policy.policies = Config::parse_policies(v)?; } Some(x) => { return Err(Error::InvalidConfig(format!( "Policy contains unknown field '{}'", x ))) } None => return Err(Error::InvalidConfig("Policy is not hash".into())), } } /* If this Policy overrides addresses, then remove any addresses that are reserved for * sub policies */ if let Some(mut addrvec) = addresses { let mut addrset: std::collections::HashSet = Default::default(); addrset.extend(addrvec.drain(..)); for p in &policy.policies { addrset = addrset.sub(&p.get_all_used_addresses()); } policy.apply_address = Some(addrset); } Ok(policy) } else { Err(Error::InvalidConfig("Policy should be a hash".into())) } } fn parse_policies(fragment: &yaml::Yaml) -> Result, Error> { if let Some(l) = fragment.as_vec() { let mut policies = Vec::new(); for i in l { policies.push(Config::parse_policy(i)?); } Ok(policies) } else { Err(Error::InvalidConfig( "policies should be a list of policies".into(), )) } } pub fn new(y: &yaml::Yaml) -> Result, Error> { Ok(Some(Config { policies: Config::parse_policies(y)?, })) } } erbium-core-1.0.5/src/dhcp/dhcppkt.rs000064400000000000000000001152311046102023000155530ustar 00000000000000/* Copyright 2023 Perry Lorier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 * * Parsing/Serialisation for a DHCP Packet. */ use crate::pktparser; use std::collections; use std::fmt; use std::net; #[derive(Debug, PartialEq, Eq)] pub enum ParseError { UnexpectedEndOfInput, WrongMagic, InvalidPacket, } impl std::error::Error for ParseError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } } impl std::fmt::Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ParseError::UnexpectedEndOfInput => write!(f, "Unexpected End Of Input"), ParseError::WrongMagic => write!(f, "Wrong Magic"), ParseError::InvalidPacket => write!(f, "Invalid Packet"), } } } impl ParseError { pub const fn get_variant_name(&self) -> &'static str { use ParseError::*; match self { UnexpectedEndOfInput => "TRUNCATED_PACKET", WrongMagic => "WRONG_MAGIC", InvalidPacket => "INVALID_PACKET", } } } #[derive(PartialEq, Eq)] pub struct DhcpOp(u8); pub const OP_BOOTREQUEST: DhcpOp = DhcpOp(1); pub const OP_BOOTREPLY: DhcpOp = DhcpOp(2); impl ToString for DhcpOp { fn to_string(&self) -> String { match self { &OP_BOOTREQUEST => String::from("BOOTREQUEST"), &OP_BOOTREPLY => String::from("BOOTREPLY"), DhcpOp(x) => format!("#{}", x), } } } impl fmt::Debug for DhcpOp { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "DhcpOp({})", self.to_string()) } } #[derive(PartialEq, Eq)] pub struct HwType(u8); pub const HWTYPE_ETHERNET: HwType = HwType(1); impl ToString for HwType { fn to_string(&self) -> String { match self { &HWTYPE_ETHERNET => String::from("Ethernet"), HwType(x) => format!("#{}", x), } } } impl fmt::Debug for HwType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "HwType({})", self.to_string()) } } #[derive(Copy, Clone, PartialEq, Eq)] pub struct MessageType(u8); pub const DHCPDISCOVER: MessageType = MessageType(1); pub const DHCPOFFER: MessageType = MessageType(2); pub const DHCPREQUEST: MessageType = MessageType(3); pub const DHCPDECLINE: MessageType = MessageType(4); pub const DHCPACK: MessageType = MessageType(5); pub const DHCPNAK: MessageType = MessageType(6); pub const DHCPRELEASE: MessageType = MessageType(7); pub const DHCPINFORM: MessageType = MessageType(8); pub const DHCPFORCERENEW: MessageType = MessageType(9); impl ToString for MessageType { fn to_string(&self) -> String { match self { &DHCPDISCOVER => String::from("DHCPDISCOVER"), &DHCPOFFER => String::from("DHCPOFFER"), &DHCPREQUEST => String::from("DHCPREQUEST"), &DHCPDECLINE => String::from("DHCPDECLINE"), &DHCPACK => String::from("DHCPACK"), &DHCPNAK => String::from("DHCPNAK"), &DHCPRELEASE => String::from("DHCPRELEASE"), &DHCPINFORM => String::from("DHCPINFORM"), &DHCPFORCERENEW => String::from("DHCPFORCERENEW"), MessageType(x) => format!("#{}", x), } } } impl fmt::Debug for MessageType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.to_string()) } } impl std::default::Default for MessageType { fn default() -> Self { DHCPNAK } } #[derive(Copy, Clone, PartialEq, Eq, Hash)] pub struct DhcpOption(u8); pub const OPTION_NETMASK: DhcpOption = DhcpOption(1); pub const OPTION_TIMEOFFSET: DhcpOption = DhcpOption(2); pub const OPTION_ROUTERADDR: DhcpOption = DhcpOption(3); pub const OPTION_TIMESERVER: DhcpOption = DhcpOption(4); pub const OPTION_NAMESERVER: DhcpOption = DhcpOption(5); pub const OPTION_DOMAINSERVER: DhcpOption = DhcpOption(6); pub const OPTION_LOGSERVER: DhcpOption = DhcpOption(7); pub const OPTION_QUOTESERVER: DhcpOption = DhcpOption(8); pub const OPTION_LPRSERVER: DhcpOption = DhcpOption(9); pub const OPTION_IMPRESSSERVER: DhcpOption = DhcpOption(10); pub const OPTION_RLPSERVER: DhcpOption = DhcpOption(11); pub const OPTION_HOSTNAME: DhcpOption = DhcpOption(12); pub const OPTION_DOMAINNAME: DhcpOption = DhcpOption(15); pub const OPTION_ROOTPATH: DhcpOption = DhcpOption(17); pub const OPTION_EXTFILE: DhcpOption = DhcpOption(18); pub const OPTION_FORWARD: DhcpOption = DhcpOption(19); pub const OPTION_SRCRT: DhcpOption = DhcpOption(20); pub const OPTION_MAXDGASSM: DhcpOption = DhcpOption(21); pub const OPTION_TTL: DhcpOption = DhcpOption(23); pub const OPTION_MTUTMOUT: DhcpOption = DhcpOption(24); pub const OPTION_MTUIF: DhcpOption = DhcpOption(26); pub const OPTION_MTUSUB: DhcpOption = DhcpOption(27); pub const OPTION_BROADCAST: DhcpOption = DhcpOption(28); pub const OPTION_MASKDISCOVERY: DhcpOption = DhcpOption(29); pub const OPTION_MASKSUPPLIER: DhcpOption = DhcpOption(30); pub const OPTION_RTRDISCOVERY: DhcpOption = DhcpOption(31); pub const OPTION_RTRREQ: DhcpOption = DhcpOption(32); pub const OPTION_STATICROUTE: DhcpOption = DhcpOption(33); pub const OPTION_TRAILERS: DhcpOption = DhcpOption(34); pub const OPTION_ARPTIMEOUT: DhcpOption = DhcpOption(35); pub const OPTION_ETHERNET: DhcpOption = DhcpOption(36); pub const OPTION_TCPTTL: DhcpOption = DhcpOption(37); pub const OPTION_TCPKEEPALIVE: DhcpOption = DhcpOption(38); pub const OPTION_TCPKEEPALIVEGARBAGE: DhcpOption = DhcpOption(39); pub const OPTION_NISDOMAIN: DhcpOption = DhcpOption(40); pub const OPTION_NISSERVERS: DhcpOption = DhcpOption(41); pub const OPTION_NTPSERVERS: DhcpOption = DhcpOption(42); pub const OPTION_NETBIOSNAMESRV: DhcpOption = DhcpOption(44); pub const OPTION_NETBIOSDISTSRV: DhcpOption = DhcpOption(45); pub const OPTION_NETBIOSTYPE: DhcpOption = DhcpOption(46); pub const OPTION_NETBIOSSCOPE: DhcpOption = DhcpOption(47); pub const OPTION_XWFONTSRVS: DhcpOption = DhcpOption(48); pub const OPTION_XWDISPLAY: DhcpOption = DhcpOption(49); pub const OPTION_ADDRESSREQUEST: DhcpOption = DhcpOption(50); pub const OPTION_LEASETIME: DhcpOption = DhcpOption(51); pub const OPTION_MSGTYPE: DhcpOption = DhcpOption(53); pub const OPTION_SERVERID: DhcpOption = DhcpOption(54); pub const OPTION_PARAMLIST: DhcpOption = DhcpOption(55); pub const OPTION_MESSAGE: DhcpOption = DhcpOption(56); pub const OPTION_MAXMSGSIZE: DhcpOption = DhcpOption(57); pub const OPTION_RENEWALTIME: DhcpOption = DhcpOption(58); pub const OPTION_REBINDTIME: DhcpOption = DhcpOption(59); pub const OPTION_VENDOR_CLASS: DhcpOption = DhcpOption(60); pub const OPTION_CLIENTID: DhcpOption = DhcpOption(61); pub const OPTION_NIS3DOMAIN: DhcpOption = DhcpOption(64); pub const OPTION_NIS3SERVERS: DhcpOption = DhcpOption(65); pub const OPTION_HOMEAGENT: DhcpOption = DhcpOption(68); pub const OPTION_SMTP: DhcpOption = DhcpOption(69); pub const OPTION_POP3: DhcpOption = DhcpOption(70); pub const OPTION_NNTP: DhcpOption = DhcpOption(71); pub const OPTION_WWW: DhcpOption = DhcpOption(72); pub const OPTION_FINGER: DhcpOption = DhcpOption(73); pub const OPTION_IRC: DhcpOption = DhcpOption(74); pub const OPTION_STREETTALK: DhcpOption = DhcpOption(75); pub const OPTION_STDA: DhcpOption = DhcpOption(76); pub const OPTION_USERCLASS: DhcpOption = DhcpOption(77); /* RFC3004 */ pub const OPTION_FQDN: DhcpOption = DhcpOption(81); /* RFC4702 */ pub const OPTION_UUID: DhcpOption = DhcpOption(97); /* RFC4578 */ pub const OPTION_PCODE: DhcpOption = DhcpOption(100); /* RFC4833 */ pub const OPTION_TCODE: DhcpOption = DhcpOption(101); /* RFC4833 */ pub const OPTION_AUTOCONF: DhcpOption = DhcpOption(103); pub const OPTION_SUBNETSELECT: DhcpOption = DhcpOption(104); pub const OPTION_DOMAINSEARCH: DhcpOption = DhcpOption(119); pub const OPTION_SIPSERVERS: DhcpOption = DhcpOption(120); pub const OPTION_CIDRROUTE: DhcpOption = DhcpOption(121); pub const OPTION_CAPTIVEPORTAL: DhcpOption = DhcpOption(160); pub const OPTION_WPAD: DhcpOption = DhcpOption(252); const OPT_INFO: &[(&str, DhcpOption, DhcpOptionType)] = &[ ("netmask", OPTION_NETMASK, DhcpOptionType::Ip), ("time-offset", OPTION_TIMEOFFSET, DhcpOptionType::I32), ("routers", OPTION_ROUTERADDR, DhcpOptionType::IpList), ("time-servers", OPTION_TIMESERVER, DhcpOptionType::IpList), ("name-servers", OPTION_NAMESERVER, DhcpOptionType::IpList), ("dns-servers", OPTION_DOMAINSERVER, DhcpOptionType::IpList), ("log-servers", OPTION_LOGSERVER, DhcpOptionType::IpList), ("quote-servers", OPTION_QUOTESERVER, DhcpOptionType::IpList), ("lpr-servers", OPTION_LPRSERVER, DhcpOptionType::IpList), // 10 ( "impress-servers", OPTION_IMPRESSSERVER, DhcpOptionType::IpList, ), ("rlp-servers", OPTION_RLPSERVER, DhcpOptionType::IpList), ("host-name", OPTION_HOSTNAME, DhcpOptionType::String), //("bootfile-size", OPTION_BOOTFILESZ, DhcpOptionType::u16), //("merit-dump-file", OPTION_MRTDUMPF, ...) ("domain-name", OPTION_DOMAINNAME, DhcpOptionType::String), //("swap-server", OPTION_SWAPSRV, ...) ("root-path", OPTION_ROOTPATH, DhcpOptionType::String), ("extension-file", OPTION_EXTFILE, DhcpOptionType::String), ("forward", OPTION_FORWARD, DhcpOptionType::Bool), // 20 ("source-route", OPTION_SRCRT, DhcpOptionType::Bool), //("policy-filter", OPTION_POLICYFLT, DhcpOptionType::...), ( "max-reassembly", OPTION_MAXDGASSM, DhcpOptionType::Seconds16, ), ("default-ttl", OPTION_TTL, DhcpOptionType::U8), ("mtu-timeout", OPTION_MTUTMOUT, DhcpOptionType::Seconds32), //("mtu-plateu", OPTION_MTUPLATEAU, DhcpOptionType::...), [u16] ("mtu", OPTION_MTUIF, DhcpOptionType::U16), ("mtu-subnet", OPTION_MTUSUB, DhcpOptionType::Bool), ("broadcast", OPTION_BROADCAST, DhcpOptionType::Ip), ("mask-discovery", OPTION_MASKDISCOVERY, DhcpOptionType::Bool), // 30 ("mask-supplier", OPTION_MASKSUPPLIER, DhcpOptionType::Bool), ( "router-discovery", OPTION_RTRDISCOVERY, DhcpOptionType::Bool, ), ("router-request", OPTION_RTRREQ, DhcpOptionType::Ip), ( "classful-route", OPTION_STATICROUTE, DhcpOptionType::Unknown, ), // Needs special handling. -- DNI ("trailers", OPTION_TRAILERS, DhcpOptionType::Bool), ("arp-timeout", OPTION_ARPTIMEOUT, DhcpOptionType::Seconds32), ("ethernet", OPTION_ETHERNET, DhcpOptionType::Bool), ("tcp-ttl", OPTION_TCPTTL, DhcpOptionType::U16), ( "tcp-keepalive", OPTION_TCPKEEPALIVE, DhcpOptionType::Seconds32, ), ( "tcp-keepalive-garbage", OPTION_TCPKEEPALIVEGARBAGE, DhcpOptionType::Bool, ), // 40 ("nis-domain", OPTION_NISDOMAIN, DhcpOptionType::String), ("nis-servers", OPTION_NISSERVERS, DhcpOptionType::IpList), ("ntp-servers", OPTION_NTPSERVERS, DhcpOptionType::IpList), // vendor specific options should be handled specially. ( "netbios-namesrv", OPTION_NETBIOSNAMESRV, DhcpOptionType::IpList, ), ( "netbios-distsrv", OPTION_NETBIOSDISTSRV, DhcpOptionType::IpList, ), ("netbios-type", OPTION_NETBIOSTYPE, DhcpOptionType::U8), /* enum? */ ("netbios-scope", OPTION_NETBIOSSCOPE, DhcpOptionType::String), ( "xwindow-font-servers", OPTION_XWFONTSRVS, DhcpOptionType::IpList, ), ("xwindow-display", OPTION_XWDISPLAY, DhcpOptionType::IpList), // 50 ("address-request", OPTION_ADDRESSREQUEST, DhcpOptionType::Ip), ("lease-time", OPTION_LEASETIME, DhcpOptionType::Seconds32), // overload, handled specially. // dhcp msg type, handled specially. ("server-id", OPTION_SERVERID, DhcpOptionType::Ip), // parameter list, handled specially. ("message", OPTION_MESSAGE, DhcpOptionType::String), ("max-size", OPTION_MAXMSGSIZE, DhcpOptionType::U16), ( "renewal-time", OPTION_RENEWALTIME, DhcpOptionType::Seconds16, ), // seconds ("rebind-time", OPTION_REBINDTIME, DhcpOptionType::Seconds16), // seconds // 60 ("class-id", OPTION_VENDOR_CLASS, DhcpOptionType::String), ("client-id", OPTION_CLIENTID, DhcpOptionType::HwAddr), // netware // netware ("nisplus-domain", OPTION_NIS3DOMAIN, DhcpOptionType::String), ( "nisplus-servers", OPTION_NIS3SERVERS, DhcpOptionType::IpList, ), //("tftp-server", OPTION_TFTPSERVER, DhcpOptionType::String), handled specially. //bootfile name, handled specially. ( "home-agent-servers", OPTION_HOMEAGENT, DhcpOptionType::IpList, ), ("smtp-servers", OPTION_SMTP, DhcpOptionType::IpList), // 70 ("pop3-servers", OPTION_POP3, DhcpOptionType::IpList), ("nntp-servers", OPTION_NNTP, DhcpOptionType::IpList), ("www-servers", OPTION_WWW, DhcpOptionType::IpList), ("finger-servers", OPTION_FINGER, DhcpOptionType::IpList), ("irc-servers", OPTION_IRC, DhcpOptionType::IpList), ( "streettalk-servers", OPTION_STREETTALK, DhcpOptionType::IpList, ), ("stda-servers", OPTION_STDA, DhcpOptionType::IpList), ("user-class", OPTION_USERCLASS, DhcpOptionType::String), //("directory-agent", OPTION_DIRECTORY_AGENT, DhcpOptionType::Unknown), //("service-scope", OPTION_SERVICE_SCOPE // 80 //("rapid-commit", OPTION_RAPID_COMMIT ("fqdn", OPTION_FQDN, DhcpOptionType::String), // option 82 (relay agent information) needs special handling. // iSNS // NDS Servers // NDS Tree // NDS Context // BCMCS // BCMCS // 90 // Authentication, needs special handling // client-last-transaction-time, RFC4388 // associated-ip, RFC4388 // client-system, RFC4578 // client-ndi, RFC4578 // ldap, RFC3679 // ("uuid", OPTION_UUID, DhcpOptionType::Unknown), //RFC4578 // userauth, RFC2485 // geoconf civic, RFC4776 // 100 ("tz-rule", OPTION_PCODE, DhcpOptionType::String), ("tz-name", OPTION_TCODE, DhcpOptionType::String), // uuid/guid ("autoconfig", OPTION_AUTOCONF, DhcpOptionType::Bool), ("subnet-selection", OPTION_SUBNETSELECT, DhcpOptionType::Ip), // RFC3011 -- needs better support ( "dns-searches", OPTION_DOMAINSEARCH, DhcpOptionType::DomainList, ), // RFC3397 ("sip-servers", OPTION_SIPSERVERS, DhcpOptionType::Unknown), // RFC3361 ("routes", OPTION_CIDRROUTE, DhcpOptionType::Routes), //122: Cablelabs Client configuration, RFC3495 //123: GeoConf, RFC6225 //124: Vendor Identifying Vendor Class -- needs special support, RFC3925 //125: Vendor Identifying Vendor Specific Information -- needs special support, RFC3925 ( "captive-portal", OPTION_CAPTIVEPORTAL, DhcpOptionType::String, ), ("wpad-url", OPTION_WPAD, DhcpOptionType::String), ]; impl From for DhcpOption { fn from(v: u8) -> Self { DhcpOption(v) } } #[derive(Copy, Clone)] pub enum DhcpOptionType { String, Ip, IpList, I32, U8, U16, U32, Bool, Seconds16, Seconds32, HwAddr, Routes, DomainList, Unknown, } type IpList = Vec; type U8Str = Vec; impl DhcpOptionType { pub fn decode(&self, v: &[u8]) -> Option { match *self { DhcpOptionType::String => U8Str::parse_into(v) .map(|x| DhcpOptionTypeValue::String(String::from_utf8_lossy(&x).to_string())), DhcpOptionType::Ip => std::net::Ipv4Addr::parse_into(v).map(DhcpOptionTypeValue::Ip), DhcpOptionType::IpList => IpList::parse_into(v).map(DhcpOptionTypeValue::IpList), DhcpOptionType::I32 => i32::parse_into(v).map(DhcpOptionTypeValue::I32), DhcpOptionType::U8 => u8::parse_into(v).map(DhcpOptionTypeValue::U8), DhcpOptionType::U16 => u16::parse_into(v).map(DhcpOptionTypeValue::U16), DhcpOptionType::U32 => u32::parse_into(v).map(DhcpOptionTypeValue::U32), DhcpOptionType::Bool => u8::parse_into(v).map(DhcpOptionTypeValue::U8), // ? DhcpOptionType::Seconds16 => u16::parse_into(v).map(DhcpOptionTypeValue::U16), // ? DhcpOptionType::Seconds32 => u32::parse_into(v).map(DhcpOptionTypeValue::U32), // ? DhcpOptionType::HwAddr => U8Str::parse_into(v).map(DhcpOptionTypeValue::HwAddr), DhcpOptionType::Routes => Vec::::parse_into(v).map(DhcpOptionTypeValue::Routes), DhcpOptionType::DomainList => { Vec::::parse_into(v).map(DhcpOptionTypeValue::DomainList) } DhcpOptionType::Unknown => U8Str::parse_into(v).map(DhcpOptionTypeValue::Unknown), } } } #[derive(Debug, Clone)] pub enum DhcpOptionTypeValue { String(String), IpList(IpList), Ip(std::net::Ipv4Addr), I32(i32), U8(u8), U16(u16), U32(u32), HwAddr(Vec), Routes(Vec), DomainList(Vec), Unknown(Vec), } impl DhcpOptionTypeValue { pub fn as_bytes(&self) -> Vec { match self { DhcpOptionTypeValue::String(s) => s.as_bytes().to_vec(), DhcpOptionTypeValue::IpList(v) => { v.iter().map(|x| x.octets()).fold(vec![], |mut acc, v| { acc.extend(v.iter()); acc }) } DhcpOptionTypeValue::Ip(i) => i.octets().to_vec(), DhcpOptionTypeValue::I32(x) => x.to_be_bytes().to_vec(), DhcpOptionTypeValue::U8(x) => x.to_be_bytes().to_vec(), DhcpOptionTypeValue::U16(x) => x.to_be_bytes().to_vec(), DhcpOptionTypeValue::U32(x) => x.to_be_bytes().to_vec(), DhcpOptionTypeValue::HwAddr(x) => x.clone(), DhcpOptionTypeValue::Routes(v) => { let mut o = vec![]; for i in v { o.push(i.prefix.prefixlen); o.extend(i.prefix.addr.octets().iter()); o.extend(i.nexthop.octets().iter()); } o } DhcpOptionTypeValue::Unknown(v) => v.clone(), DhcpOptionTypeValue::DomainList(l) => { let mut o = vec![]; for domains in l.iter().map(|d| d.split('.')) { for label in domains { o.push(label.len() as u8); o.extend(label.as_bytes()); } o.push(0_u8) } o } } } } fn escape_char(&c: &u8) -> String { match c { b' '..=b'~' => char::from(c).to_string(), x => format!("\\x{:0>2x}", x), } } fn escape_str(c: &[u8]) -> String { c.iter().map(escape_char).collect::>().join("") } impl std::fmt::Display for DhcpOptionTypeValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { DhcpOptionTypeValue::String(s) => write!(f, "{}", escape_str(s.as_bytes())), DhcpOptionTypeValue::Ip(i) => i.fmt(f), DhcpOptionTypeValue::U8(i) => i.fmt(f), DhcpOptionTypeValue::U16(i) => i.fmt(f), DhcpOptionTypeValue::U32(i) => i.fmt(f), DhcpOptionTypeValue::I32(i) => i.fmt(f), DhcpOptionTypeValue::IpList(l) => write!( f, "{}", l.iter() .map(|i| format!("{}", i)) .collect::>() .join(",") ), DhcpOptionTypeValue::HwAddr(x) => write!( f, "{}", x.iter() .map(|b| format!("{:0>2x}", b)) .collect::>() .join(":") ), DhcpOptionTypeValue::Routes(l) => write!( f, "{}", l.iter() .map(|i| format!("{}->{}", i.prefix, i.nexthop)) .collect::>() .join(",") ), DhcpOptionTypeValue::Unknown(v) => write!( f, "{}", v.iter() .map(|b| format!("{:0>2x}", b)) .collect::>() .join("") ), DhcpOptionTypeValue::DomainList(v) => write!(f, "{}", v.join(",")), } } } impl DhcpOption { pub const fn new(opt: u8) -> Self { DhcpOption(opt) } pub fn get_type(&self) -> Option { for (_name, option, ty) in OPT_INFO { if option == self { return Some(*ty); } } None } } pub fn name_to_option(lookup_name: &str) -> Option { for (name, option, _ty) in OPT_INFO { if *name == lookup_name { return Some(*option); } } None } impl ToString for DhcpOption { fn to_string(&self) -> String { for (name, option, _ty) in OPT_INFO { if option == self { return (*name).into(); } } format!("#{}", self.0) } } impl fmt::Debug for DhcpOption { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.to_string()) } } pub trait DhcpParse { type Item; fn parse_into(v: &[u8]) -> Option; } #[derive(Clone, Debug)] pub struct Route { pub prefix: erbium_net::Ipv4Subnet, pub nexthop: std::net::Ipv4Addr, } fn parse_ip_from_iter(it: &mut I) -> Option where I: std::iter::Iterator, { let ip1 = it.next()?; let ip2 = it.next()?; let ip3 = it.next()?; let ip4 = it.next()?; Some(net::Ipv4Addr::new(ip1, ip2, ip3, ip4)) } impl DhcpParse for Vec { type Item = Self; fn parse_into(v: &[u8]) -> Option { let mut it = v.iter().copied(); let mut ret = vec![]; while let Some(prefixlen) = it.next() { let prefix = erbium_net::Ipv4Subnet::new(parse_ip_from_iter(&mut it)?, prefixlen).ok()?; let nexthop = parse_ip_from_iter(&mut it)?; ret.push(Route { prefix, nexthop }); } Some(ret) } } impl DhcpParse for std::net::Ipv4Addr { type Item = Self; fn parse_into(v: &[u8]) -> Option { if v.len() != 4 { None } else { Some(std::net::Ipv4Addr::new(v[0], v[1], v[2], v[3])) } } } impl DhcpParse for IpList { type Item = Self; fn parse_into(v: &[u8]) -> Option { let mut it = v.iter().copied(); let mut ret = vec![]; while let Some(o1) = it.next() { let o2 = it.next(); let o3 = it.next(); let o4 = it.next(); ret.push(std::net::Ipv4Addr::new(o1, o2?, o3?, o4?)); } Some(ret) } } /* HELP WANTED: I can't figure out how to make this a straight &[u8] -> Some(&[u8]) with no copies, * while preserving lifetimes etc. */ impl DhcpParse for Vec { type Item = Self; fn parse_into(v: &[u8]) -> Option { Some(v.to_vec()) } } /* This doesn't actually parse into a u64, this just parses as many bytes as it can find into a u64 */ impl DhcpParse for u64 { type Item = Self; fn parse_into(v: &[u8]) -> Option { Some(v.iter().fold(0_u64, |acc, &v| (acc << 8) + (v as Self))) } } impl DhcpParse for u32 { type Item = Self; fn parse_into(v: &[u8]) -> Option { Some(v.iter().fold(0_u32, |acc, &v| (acc << 8) + (v as Self))) } } impl DhcpParse for u16 { type Item = Self; fn parse_into(v: &[u8]) -> Option { Some(v.iter().fold(0_u16, |acc, &v| (acc << 8) + (v as Self))) } } impl DhcpParse for i32 { type Item = Self; fn parse_into(v: &[u8]) -> Option { Some(v.iter().fold(0_i32, |acc, &v| (acc << 8) + (v as Self))) } } impl DhcpParse for u8 { type Item = Self; fn parse_into(v: &[u8]) -> Option { if v.len() != 1 { None } else { v.first().copied() } } } impl DhcpParse for std::time::Duration { type Item = Self; fn parse_into(v: &[u8]) -> Option { u64::parse_into(v).map(std::time::Duration::from_secs) } } impl DhcpParse for MessageType { type Item = Self; fn parse_into(v: &[u8]) -> Option { if v.len() != 1 { None } else { Some(MessageType(v[0])) } } } impl DhcpParse for Vec { type Item = Self; fn parse_into(v: &[u8]) -> Option { let mut buf = crate::pktparser::Buffer::new(v); Some(buf.get_domains()?.iter().map(|d| d.join(".")).collect()) } } impl DhcpParse for String { type Item = Self; fn parse_into(v: &[u8]) -> Option { Some(String::from_utf8_lossy(v).to_string()) } } #[derive(Debug, Clone, PartialEq, Default, Eq)] pub struct DhcpOptions { pub other: collections::HashMap>, } impl DhcpOptions { pub fn get_raw_option(&self, option: &DhcpOption) -> Option<&[u8]> { self.other.get(option).map(|x| x.as_slice()) } pub fn get_option(&self, option: &DhcpOption) -> Option { self.get_raw_option(option).and_then(|x| T::parse_into(x)) } pub fn get_serverid(&self) -> Option { self.get_option::(&OPTION_SERVERID) } pub fn get_clientid(&self) -> Option> { self.get_option::>(&OPTION_CLIENTID) } pub fn get_address_request(&self) -> Option { self.get_option::(&OPTION_ADDRESSREQUEST) } pub fn get_messagetype(&self) -> Option { self.get_option::(&OPTION_MSGTYPE) } pub fn get_hostname(&self) -> Option { self.get_option::(&OPTION_HOSTNAME) } #[must_use] pub fn set_raw_option(mut self, option: &DhcpOption, value: &[u8]) -> Self { self.other.insert(*option, value.to_vec()); self } #[must_use] pub fn set_option(self, option: &DhcpOption, value: &T) -> Self { let mut v = Vec::new(); value.serialise(&mut v); self.set_raw_option(option, &v) } pub fn mutate_option(&mut self, option: &DhcpOption, value: &T) { let mut v = Vec::new(); value.serialise(&mut v); self.other.insert(*option, v); } pub fn mutate_option_value(&mut self, option: &DhcpOption, value: &DhcpOptionTypeValue) { self.other.insert(*option, value.as_bytes()); } #[must_use] pub fn maybe_set_option(self, option: &DhcpOption, value: Option<&T>) -> Self { if let Some(v) = value { self.set_option(option, v) } else { self } } #[must_use] pub fn remove_option(mut self, option: &DhcpOption) -> Self { self.other.remove(option); self } } #[derive(PartialEq, Eq)] pub struct Dhcp { pub op: DhcpOp, pub htype: HwType, pub hlen: u8, pub hops: u8, pub xid: u32, pub secs: u16, pub flags: u16, pub ciaddr: net::Ipv4Addr, pub yiaddr: net::Ipv4Addr, pub siaddr: net::Ipv4Addr, pub giaddr: net::Ipv4Addr, pub chaddr: Vec, pub sname: Vec, pub file: Vec, pub options: DhcpOptions, } impl std::fmt::Debug for Dhcp { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Dhcp") .field("op", &self.op) .field("htype", &self.htype) .field("hlen", &self.hlen) .field("hops", &self.hops) .field("xid", &self.xid) .field("secs", &self.secs) .field("flags", &self.flags) .field("ciaddr", &self.ciaddr) .field("yiaddr", &self.yiaddr) .field("siaddr", &self.siaddr) .field("giaddr", &self.giaddr) .field( "chaddr", &self .chaddr .iter() .map(|&x| format!("{:x?}", x)) .collect::>() .join(""), ) .field( "sname", &self .sname .iter() .map(|&x| format!("{:x?}", x)) .collect::>() .join(""), ) .field( "file", &String::from_utf8(self.file.clone()) .or_else::, _>(|_| Ok(format!("{:?}", self.file))) .unwrap(), ) .field("options", &self.options) .finish() } } fn null_terminated(mut v: Vec) -> Vec { for i in 0..v.len() { if v[i] == 0 { v.truncate(i); break; } } v } pub fn parse_options(mut buf: pktparser::Buffer) -> Result { let mut raw_options: collections::HashMap> = collections::HashMap::new(); loop { match buf.get_u8() { Some(0) => (), /* Pad byte */ Some(255) => break, /* End Field */ Some(x) => { let l = buf.get_u8().ok_or(ParseError::UnexpectedEndOfInput)?; raw_options .entry(DhcpOption(x)) .or_insert_with(Vec::new) .extend( buf.get_bytes(l as usize) .ok_or(ParseError::UnexpectedEndOfInput)?, ); } None => return Err(ParseError::UnexpectedEndOfInput), } } Ok(DhcpOptions { other: raw_options }) } pub fn parse(pkt: &[u8]) -> Result { let mut buf = pktparser::Buffer::new(pkt); let op = buf.get_u8().ok_or(ParseError::UnexpectedEndOfInput)?; let htype = buf.get_u8().ok_or(ParseError::UnexpectedEndOfInput)?; let hlen = buf.get_u8().ok_or(ParseError::UnexpectedEndOfInput)?; let hops = buf.get_u8().ok_or(ParseError::UnexpectedEndOfInput)?; let xid = buf.get_be32().ok_or(ParseError::UnexpectedEndOfInput)?; let secs = buf.get_be16().ok_or(ParseError::UnexpectedEndOfInput)?; let flags = buf.get_be16().ok_or(ParseError::UnexpectedEndOfInput)?; let ciaddr = buf.get_ipv4().ok_or(ParseError::UnexpectedEndOfInput)?; let yiaddr = buf.get_ipv4().ok_or(ParseError::UnexpectedEndOfInput)?; let siaddr = buf.get_ipv4().ok_or(ParseError::UnexpectedEndOfInput)?; let giaddr = buf.get_ipv4().ok_or(ParseError::UnexpectedEndOfInput)?; let chaddr = buf.get_vec(16).ok_or(ParseError::UnexpectedEndOfInput)?; if hlen as usize > chaddr.len() { return Err(ParseError::InvalidPacket); } let sname = null_terminated(buf.get_vec(64).ok_or(ParseError::UnexpectedEndOfInput)?); let file = null_terminated(buf.get_vec(128).ok_or(ParseError::UnexpectedEndOfInput)?); let magic = buf.get_be32().ok_or(ParseError::UnexpectedEndOfInput)?; if magic != 0x6382_5363 { return Err(ParseError::WrongMagic); } let options = parse_options(buf)?; Ok(Dhcp { op: DhcpOp(op), htype: HwType(htype), hlen, hops, xid, secs, flags, ciaddr, yiaddr, siaddr, giaddr, chaddr: chaddr[0..hlen as usize].to_vec(), sname, file, options, }) } pub trait Serialise { fn serialise(&self, v: &mut Vec); } impl Serialise for u8 { fn serialise(&self, v: &mut Vec) { v.push(*self); } } impl Serialise for u16 { fn serialise(&self, v: &mut Vec) { for b in self.to_be_bytes().iter() { b.serialise(v); } } } impl Serialise for u32 { fn serialise(&self, v: &mut Vec) { for b in self.to_be_bytes().iter() { b.serialise(v); } } } impl Serialise for net::Ipv4Addr { fn serialise(&self, v: &mut Vec) { for b in self.octets().iter() { b.serialise(v); } } } impl Serialise for DhcpOption { fn serialise(&self, v: &mut Vec) { self.0.serialise(v); } } impl Serialise for &[u8] { fn serialise(&self, v: &mut Vec) { v.extend(*self); } } impl Serialise for MessageType { fn serialise(&self, v: &mut Vec) { self.0.serialise(v); } } impl Serialise for Vec { fn serialise(&self, v: &mut Vec) { for i in self { i.serialise(v); } } } impl Serialise for String { fn serialise(&self, v: &mut Vec) { self.as_bytes().serialise(v) } } impl Serialise for i32 { fn serialise(&self, v: &mut Vec) { for i in self.to_be_bytes().iter() { i.serialise(v) } } } impl Serialise for DhcpOptionTypeValue { fn serialise(&self, v: &mut Vec) { v.extend(self.as_bytes().iter()); } } fn serialise_option(option: DhcpOption, bytes: &[T], v: &mut Vec) where T: Serialise, { option.serialise(v); (bytes.len() as u8).serialise(v); for i in bytes.iter() { i.serialise(v); } } impl Serialise for DhcpOptions { fn serialise(&self, v: &mut Vec) { for (o, p) in self.other.iter() { serialise_option(*o, p, v); } /* Add end of options marker */ (255_u8).serialise(v); } } fn serialise_fixed(out: &[u8], l: usize, v: &mut Vec) { let mut bytes = Vec::with_capacity(l); bytes.extend_from_slice(out); bytes.resize_with(l, Default::default); for b in &bytes { b.serialise(v); } } impl Dhcp { pub fn serialise(&self) -> Vec { let mut v: Vec = Vec::new(); self.op.0.serialise(&mut v); self.htype.0.serialise(&mut v); self.hlen.serialise(&mut v); self.hops.serialise(&mut v); self.xid.serialise(&mut v); self.secs.serialise(&mut v); self.flags.serialise(&mut v); self.ciaddr.serialise(&mut v); self.yiaddr.serialise(&mut v); self.siaddr.serialise(&mut v); self.giaddr.serialise(&mut v); serialise_fixed(&self.chaddr, 16, &mut v); serialise_fixed(&self.sname, 64, &mut v); serialise_fixed(&self.file, 128, &mut v); /* DHCP Magic */ 0x6382_5363_u32.serialise(&mut v); self.options.serialise(&mut v); v } pub fn get_client_id(&self) -> Vec { self.options .get_clientid() .unwrap_or_else(|| self.chaddr.clone()) } } #[cfg(test)] fn serialise_one_for_test(opt: DhcpOptionTypeValue) -> Vec { let mut v = vec![]; opt.serialise(&mut v); v } #[test] fn test_type_serialisation() { assert_eq!( serialise_one_for_test(DhcpOptionTypeValue::String("test".into())), vec![116, 101, 115, 116] ); assert_eq!( serialise_one_for_test(DhcpOptionTypeValue::Ip("192.0.2.0".parse().unwrap())), vec![192, 0, 2, 0] ); assert_eq!( serialise_one_for_test(DhcpOptionTypeValue::I32(16909060i32)), vec![1, 2, 3, 4] ); assert_eq!( serialise_one_for_test(DhcpOptionTypeValue::U8(42)), vec![42] ); assert_eq!( serialise_one_for_test(DhcpOptionTypeValue::U16(258)), vec![1, 2], ); assert_eq!( serialise_one_for_test(DhcpOptionTypeValue::U32(16909060)), vec![1, 2, 3, 4] ); assert_eq!( serialise_one_for_test(DhcpOptionTypeValue::HwAddr(vec![1, 2, 3, 4, 5, 6])), vec![1, 2, 3, 4, 5, 6] ); assert_eq!( serialise_one_for_test(DhcpOptionTypeValue::IpList(vec![ "192.0.2.0".parse().unwrap(), "192.0.2.1".parse().unwrap(), "192.0.2.2".parse().unwrap(), ])), vec![192, 0, 2, 0, 192, 0, 2, 1, 192, 0, 2, 2] ); assert_eq!( serialise_one_for_test(DhcpOptionTypeValue::Routes(vec![Route { prefix: erbium_net::Ipv4Subnet::new("192.0.2.0".parse().unwrap(), 24).unwrap(), nexthop: "192.0.2.254".parse().unwrap(), }])), vec![24, 192, 0, 2, 0, 192, 0, 2, 254] ); } #[test] fn test_parse() { assert_eq!( format!( "{}", DhcpOptionType::String .decode(&vec![116, 101, 115, 116]) .unwrap() ), "test" ); assert_eq!( format!( "{}", DhcpOptionType::Ip.decode(&vec![192, 0, 2, 42]).unwrap() ), "192.0.2.42" ); assert_eq!( format!( "{}", DhcpOptionType::IpList .decode(&vec![192, 0, 2, 12, 192, 0, 2, 17]) .unwrap() ), "192.0.2.12,192.0.2.17" ); assert_eq!( format!("{}", DhcpOptionType::I32.decode(&vec![1, 2, 3, 4]).unwrap()), "16909060", ); assert_eq!( format!("{}", DhcpOptionType::U8.decode(&vec![251]).unwrap()), "251", ); assert_eq!( format!("{}", DhcpOptionType::U16.decode(&vec![1, 2]).unwrap()), "258", ); assert_eq!( format!("{}", DhcpOptionType::U32.decode(&vec![1, 2, 3, 4]).unwrap()), "16909060", ); assert_eq!( format!("{}", DhcpOptionType::Bool.decode(&vec![0]).unwrap()), "0", ); assert_eq!( format!("{}", DhcpOptionType::Bool.decode(&vec![1]).unwrap()), "1", ); assert_eq!( format!( "{}", DhcpOptionType::Seconds16.decode(&vec![1, 0x2c]).unwrap() ), "300", ); assert_eq!( format!( "{}", DhcpOptionType::Seconds32 .decode(&vec![0, 1, 0x51, 0x80]) .unwrap() ), "86400", ); assert_eq!( format!( "{}", DhcpOptionType::HwAddr .decode(&vec![0, 1, 2, 3, 4, 5]) .unwrap() ), "00:01:02:03:04:05" ); assert_eq!( format!( "{}", DhcpOptionType::Routes .decode(&vec![ 24, 192, 0, 2, 0, 192, 0, 2, 254, 24, 198, 51, 100, 0, 192, 0, 2, 254 ]) .unwrap() ), "192.0.2.0/24->192.0.2.254,198.51.100.0/24->192.0.2.254" ); } erbium-core-1.0.5/src/dhcp/mod.rs000064400000000000000000001256651046102023000147110ustar 00000000000000/* Copyright 2023 Perry Lorier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 * * Main DHCP Code. */ use std::collections; use std::convert::TryInto as _; use std::net; use std::ops::Sub as _; use std::sync::Arc; use tokio::sync; use crate::dhcp::dhcppkt::Serialise; use erbium_net::addr::{NetAddr, ToNetAddr, WithPort as _, UNSPECIFIED4}; use erbium_net::packet; use erbium_net::raw; use erbium_net::udp; pub mod config; pub mod dhcppkt; pub mod pool; #[cfg(test)] mod test; type UdpSocket = udp::UdpSocket; type ServerIds = std::collections::HashSet; pub type SharedServerIds = Arc>; lazy_static::lazy_static! { static ref DHCP_RX_PACKETS: prometheus::IntCounter = prometheus::register_int_counter!("dhcp_received_packets", "Number of DHCP packets received") .unwrap(); static ref DHCP_TX_PACKETS: prometheus::IntCounter = prometheus::register_int_counter!("dhcp_sent_packets", "Number of DHCP packets sent") .unwrap(); static ref DHCP_ERRORS: prometheus::IntCounterVec = prometheus::register_int_counter_vec!( "dhcp_errors", "Counts of reasons that replies cannot be sent", &["reason"] ) .unwrap(); static ref DHCP_ALLOCATIONS: prometheus::IntCounterVec = prometheus::register_int_counter_vec!( "dhcp_allocations", "Counts of address allocation types", &["reason"] ) .unwrap(); static ref DHCP_ACTIVE_LEASES: prometheus::IntGauge = prometheus::register_int_gauge!( "dhcp_active_leases", "Counts of leases that are currently in use" ) .unwrap(); static ref DHCP_EXPIRED_LEASES: prometheus::IntGauge = prometheus::register_int_gauge!( "dhcp_expired_leases", "Counts of leases that are currently expired" ) .unwrap(); } #[derive(Debug, PartialEq, Eq)] pub enum DhcpError { UnknownMessageType(dhcppkt::MessageType), NoLeasesConfigured, ParseError(dhcppkt::ParseError), PoolError(pool::Error), InternalError(String), OtherServer(std::net::Ipv4Addr), NoPolicyConfigured, } impl std::error::Error for DhcpError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } } impl std::fmt::Display for DhcpError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { DhcpError::UnknownMessageType(m) => write!(f, "Unknown Message Type: {:?}", m), DhcpError::NoLeasesConfigured => write!(f, "No Leases Configured"), DhcpError::ParseError(e) => write!(f, "Parse Error: {:?}", e), DhcpError::InternalError(e) => write!(f, "Internal Error: {:?}", e), DhcpError::OtherServer(s) => write!(f, "Packet for a different DHCP server: {}", s), DhcpError::NoPolicyConfigured => write!(f, "No policy configured for client"), DhcpError::PoolError(p) => write!(f, "Pool Error: {:?}", p), } } } impl DhcpError { const fn get_variant_name(&self) -> &'static str { use DhcpError::*; match self { UnknownMessageType(_) => "UNKNOWN_MESSAGE_TYPE", NoLeasesConfigured => "NO_LEASES_CONFIGURED", ParseError(_) => "PARSE_ERROR", InternalError(_) => "INTERNAL_ERROR", OtherServer(_) => "OTHER_SERVER", NoPolicyConfigured => "NO_POLICY", PoolError(pool::Error::NoAssignableAddress) => "NO_ADDRESS", PoolError(pool::Error::RequestedAddressInUse) => "ADDRESS_IN_USE", PoolError(_) => "INTERNAL_POOL_ERROR", } } } #[derive(Debug)] pub struct DHCPRequest { /// The DHCP request packet. pub pkt: dhcppkt::Dhcp, /// The IP address that the request was received on. pub serverip: std::net::Ipv4Addr, /// The interface index that the request was received on. pub ifindex: u32, pub if_mtu: Option, pub if_router: Option, } #[cfg(test)] impl std::default::Default for DHCPRequest { fn default() -> Self { DHCPRequest { pkt: dhcppkt::Dhcp { op: dhcppkt::OP_BOOTREQUEST, htype: dhcppkt::HWTYPE_ETHERNET, hlen: 6, hops: 0, xid: 0, secs: 0, flags: 0, ciaddr: net::Ipv4Addr::UNSPECIFIED, yiaddr: net::Ipv4Addr::UNSPECIFIED, siaddr: net::Ipv4Addr::UNSPECIFIED, giaddr: net::Ipv4Addr::UNSPECIFIED, chaddr: vec![ 0x00, 0x00, 0x5E, 0x00, 0x53, 0x00, /* Reserved for documentation, per RFC7042 */ ], sname: vec![], file: vec![], options: Default::default(), }, serverip: "0.0.0.0".parse().unwrap(), ifindex: 0, if_mtu: None, if_router: None, } } } #[derive(Eq, PartialEq, Debug)] enum PolicyMatch { NoMatch, MatchFailed, MatchSucceeded, } fn check_policy(req: &DHCPRequest, policy: &config::Policy) -> PolicyMatch { let mut outcome = PolicyMatch::NoMatch; //if let Some(policy.match_interface ... if policy.match_all { outcome = PolicyMatch::MatchSucceeded; } if let Some(match_chaddr) = &policy.match_chaddr { outcome = PolicyMatch::MatchSucceeded; if req.pkt.chaddr != *match_chaddr { return PolicyMatch::MatchFailed; } } if let Some(match_subnet) = &policy.match_subnet { outcome = PolicyMatch::MatchSucceeded; if !match_subnet.contains(req.serverip) { return PolicyMatch::MatchFailed; } } for (k, m) in policy.match_other.iter() { if match (m, req.pkt.options.other.get(k)) { (None, None) => true, /* Required that option doesn't exist, option doesn't exist */ (None, Some(_)) => false, /* Required that option doesn't exist, option exists */ (Some(mat), Some(opt)) if &mat.as_bytes() == opt => true, /* Required it has value, and matches */ (Some(_), Some(_)) => false, /* Required it has a value, option has some other value */ (Some(_), None) => false, /* Required that option has a value, option doesn't exist */ } { /* If at least one thing matches, then this is a MatchSucceded */ outcome = PolicyMatch::MatchSucceeded; } else { /* If any fail, then fail everything */ return PolicyMatch::MatchFailed; } } outcome } fn apply_policy(req: &DHCPRequest, policy: &config::Policy, response: &mut Response) -> bool { /* Check if our policy should match. */ match check_policy(req, policy) { /* If the match failed, do not apply. */ PolicyMatch::MatchFailed => return false, /* If there are no matches applied for this policy, check if any subpolicies match, and if * so, apply this policy too, otherwise fail. */ PolicyMatch::NoMatch => { if !check_policies(req, &policy.policies) { return false; } } /* If there were matchers, and we matched them all, then continue with applying the policy. */ PolicyMatch::MatchSucceeded => (), } /* If there are addresses provided here, override any from the parent */ if let Some(address) = &policy.apply_address { response.address = Some(address.clone()); /* HELP: I tried to make the lifetimes worked, and failed */ } /* Now get the list of parameters we will apply from the parameter list from the client. */ // TODO: This should probably just be a u128 bitvector let pl: std::collections::HashSet< dhcppkt::DhcpOption, std::collections::hash_map::RandomState, > = req .pkt .options .get_option::>(&dhcppkt::OPTION_PARAMLIST) .unwrap_or_default() .iter() .copied() .map(dhcppkt::DhcpOption::from) .collect(); for (k, v) in &policy.apply_other { if pl.contains(k) { response.options.mutate_option(k, v.as_ref()); } } /* And check to see if a subpolicy also matches */ apply_policies(req, &policy.policies, response); /* Some of the defaults depend on what other options end up being set, so apply them here. */ if let Some(subnet) = &policy.match_subnet { if pl.contains(&dhcppkt::OPTION_NETMASK) { response .options .mutate_option_default(&dhcppkt::OPTION_NETMASK, &subnet.netmask()); } if pl.contains(&dhcppkt::OPTION_BROADCAST) { response .options .mutate_option_default(&dhcppkt::OPTION_BROADCAST, &subnet.broadcast()); } } true } fn check_policies(req: &DHCPRequest, policies: &[config::Policy]) -> bool { for policy in policies { match check_policy(req, policy) { PolicyMatch::MatchSucceeded => return true, PolicyMatch::MatchFailed => continue, PolicyMatch::NoMatch => { if check_policies(req, &policy.policies) { return true; } else { continue; } } } } false } fn apply_policies(req: &DHCPRequest, policies: &[config::Policy], response: &mut Response) -> bool { for policy in policies { if apply_policy(req, policy, response) { return true; } } false } #[derive(Default, Clone)] struct ResponseOptions { /* Options can be unset (not specified), set to "None" (do not send), or set to a specific * value. */ option: collections::HashMap>>, } impl ResponseOptions { fn set_raw_option(mut self, option: &dhcppkt::DhcpOption, value: &[u8]) -> Self { self.option.insert(*option, Some(value.to_vec())); self } fn set_option(self, option: &dhcppkt::DhcpOption, value: &T) -> Self { let mut v = Vec::new(); value.serialise(&mut v); self.set_raw_option(option, &v) } pub fn mutate_option( &mut self, option: &dhcppkt::DhcpOption, maybe_value: Option<&T>, ) { match maybe_value { Some(value) => { let mut v = Vec::new(); value.serialise(&mut v); self.option.insert(*option, Some(v)); } None => { self.option.insert(*option, None); } } } pub fn mutate_option_default( &mut self, option: &dhcppkt::DhcpOption, value: &T, ) { if self.option.get(option).is_none() { self.mutate_option(option, Some(value)); } } pub fn to_options(&self) -> dhcppkt::DhcpOptions { let mut opt = dhcppkt::DhcpOptions::default(); for (k, v) in &self.option { if let Some(d) = v { opt.other.insert(*k, d.to_vec()); } } opt } } #[derive(Default)] struct Response { options: ResponseOptions, address: Option, minlease: Option, maxlease: Option, } fn handle_discover( pools: &mut pool::Pool, req: &DHCPRequest, _serverids: &ServerIds, base: &[config::Policy], conf: &super::config::Config, ) -> Result { /* Build the default response we are about to reply with, it will be filled in later */ let mut response: Response = Response { options: ResponseOptions::default() .set_option(&dhcppkt::OPTION_MSGTYPE, &dhcppkt::DHCPOFFER) .set_option(&dhcppkt::OPTION_SERVERID, &req.serverip), ..Default::default() }; /* Now attempt to apply all the policies.*/ let base_policy = apply_policies(req, base, &mut response); let conf_policy = apply_policies(req, &conf.dhcp.policies, &mut response); if !base_policy && !conf_policy { /* If none of the policies applied at all, then provide a warning back to the caller */ Err(DhcpError::NoPolicyConfigured) } else if let Some(addresses) = response.address { /* At least one policy matched, and provided addresses. So now go allocate an address */ let mut raw_options = Vec::new(); req.pkt.options.serialise(&mut raw_options); match pools.allocate_address( &req.pkt.get_client_id(), req.pkt.options.get_address_request(), &addresses, response.minlease.unwrap_or(pool::DEFAULT_MIN_LEASE), response.maxlease.unwrap_or(pool::DEFAULT_MAX_LEASE), &raw_options, ) { /* Now we have an address, build the reply */ Ok(lease) => { DHCP_ALLOCATIONS .with_label_values(&[&format!("{:?}", lease.lease_type)]) .inc(); log::info!( "Allocated Lease: {} for {:?} ({:?})", lease.ip, lease.expire, lease.lease_type ); Ok(dhcppkt::Dhcp { op: dhcppkt::OP_BOOTREPLY, htype: dhcppkt::HWTYPE_ETHERNET, hlen: 6, hops: 0, xid: req.pkt.xid, secs: 0, flags: req.pkt.flags, ciaddr: net::Ipv4Addr::UNSPECIFIED, yiaddr: lease.ip, siaddr: net::Ipv4Addr::UNSPECIFIED, giaddr: req.pkt.giaddr, chaddr: req.pkt.chaddr.clone(), sname: vec![], file: vec![], options: response .options .clone() .set_option(&dhcppkt::OPTION_SERVERID, &req.serverip) .to_options(), }) } /* Some error occurred, document it. */ Err(e) => Err(DhcpError::PoolError(e)), } } else { /* There were no addresses assigned to this match */ Err(DhcpError::NoLeasesConfigured) } } fn handle_request( pools: &mut pool::Pool, req: &DHCPRequest, serverids: &ServerIds, base: &[config::Policy], conf: &super::config::Config, ) -> Result { if let Some(si) = req.pkt.options.get_serverid() { if !serverids.contains(&si) { return Err(DhcpError::OtherServer(si)); } } let mut response: Response = Response { options: ResponseOptions::default() .set_option(&dhcppkt::OPTION_MSGTYPE, &dhcppkt::DHCPOFFER) .set_option(&dhcppkt::OPTION_SERVERID, &req.serverip), ..Default::default() }; let base_policy = apply_policies(req, base, &mut response); let conf_policy = apply_policies(req, &conf.dhcp.policies, &mut response); if !base_policy && !conf_policy { Err(DhcpError::NoPolicyConfigured) } else if let Some(addresses) = response.address { let mut raw_options = Vec::new(); req.pkt.options.serialise(&mut raw_options); match pools.allocate_address( &req.pkt.get_client_id(), if !req.pkt.ciaddr.is_unspecified() { Some(req.pkt.ciaddr) } else { req.pkt.options.get_address_request() }, &addresses, response.minlease.unwrap_or(pool::DEFAULT_MIN_LEASE), response.maxlease.unwrap_or(pool::DEFAULT_MAX_LEASE), &raw_options, ) { Ok(lease) => { DHCP_ALLOCATIONS .with_label_values(&[&format!("{:?}", lease.lease_type)]) .inc(); log::info!( "Allocated Lease: {} for {:?} ({:?})", lease.ip, lease.expire, lease.lease_type ); Ok(dhcppkt::Dhcp { op: dhcppkt::OP_BOOTREPLY, htype: dhcppkt::HWTYPE_ETHERNET, hlen: 6, hops: 0, xid: req.pkt.xid, secs: 0, flags: req.pkt.flags, ciaddr: req.pkt.ciaddr, yiaddr: lease.ip, siaddr: net::Ipv4Addr::UNSPECIFIED, giaddr: req.pkt.giaddr, chaddr: req.pkt.chaddr.clone(), sname: vec![], file: vec![], options: response .options .set_option(&dhcppkt::OPTION_MSGTYPE, &dhcppkt::DHCPACK) .set_option( &dhcppkt::OPTION_SERVERID, &req.pkt.options.get_serverid().unwrap_or(req.serverip), ) .set_option(&dhcppkt::OPTION_LEASETIME, &(lease.expire.as_secs() as u32)) .to_options(), }) } Err(e) => Err(DhcpError::PoolError(e)), } } else { Err(DhcpError::NoLeasesConfigured) } } fn format_mac(v: &[u8]) -> String { v.iter() .map(|b| format!("{:0>2x}", b)) .collect::>() .join(":") } fn format_client(req: &dhcppkt::Dhcp) -> String { format!( "{} ({})", format_mac(&req.chaddr), String::from_utf8_lossy( &req.options .get_option::>(&dhcppkt::OPTION_HOSTNAME) .unwrap_or_default() ), ) } fn log_options(req: &dhcppkt::Dhcp) { log::info!( "{}: Options: {}", format_client(req), req.options .other .iter() // We already decode MSGTYPE and PARAMLIST elsewhere, so don't try and decode // them here. It just leads to confusing looking messages. .filter(|(&k, _)| k != dhcppkt::OPTION_MSGTYPE && k != dhcppkt::OPTION_PARAMLIST) .map(|(k, v)| format!( "{}({})", k.to_string(), k.get_type() .and_then(|x| x.decode(v)) .map(|x| format!("{}", x)) .unwrap_or_else(|| "".into()) )) .collect::>() .join(" "), ); } async fn log_pkt(request: &DHCPRequest, netinfo: &erbium_net::netinfo::SharedNetInfo) { use std::fmt::Write as _; let mut s = "".to_string(); write!( s, "{}: {} on {}", format_client(&request.pkt), request .pkt .options .get_messagetype() .map(|x| x.to_string()) .unwrap_or_else(|| "[unknown]".into()), netinfo.get_safe_name_by_ifidx(request.ifindex).await ) .unwrap(); if !request.serverip.is_unspecified() { write!(s, " ({})", request.serverip).unwrap(); } if !request.pkt.ciaddr.is_unspecified() { write!(s, ", using {}", request.pkt.ciaddr).unwrap(); } if !request.pkt.giaddr.is_unspecified() { write!( s, ", relayed via {} hops from {}", request.pkt.hops, request.pkt.giaddr ) .unwrap(); } log::info!("{}", s); log_options(&request.pkt); log::info!( "{}: Requested: {}", format_client(&request.pkt), request .pkt .options .get_option::>(&dhcppkt::OPTION_PARAMLIST) .map(|v| v .iter() .map(|&x| dhcppkt::DhcpOption::new(x)) .map(|o| o.to_string()) .collect::>() .join(" ")) .unwrap_or_else(|| "".into()) ); } /// Produce a default configuration. /// This builds a configuration that would look like: /// ```yaml /// # Always match the base configuration /// match-all: true /// # For top level settings, apply them. /// apply-dns-servers: [ip4s] /// apply-dns-search: [domains] /// apply-captive-portal: captiveportal /// policies: /// # For each IPv4 prefix provided in the addresses top level config: /// - match-subnet: prefix4 /// apply-subnet: prefix4 # with the current requestip removed. /// ``` pub fn build_default_config(conf: &crate::config::Config, request: &DHCPRequest) -> config::Policy { let mut default_policy = config::Policy { match_all: true, /* We always want this policy to match. */ ..Default::default() }; /* Add the default options from the top level configuration */ default_policy.apply_other.insert( dhcppkt::OPTION_DOMAINSERVER, Some(dhcppkt::DhcpOptionTypeValue::IpList( conf.dns_servers .iter() .filter_map(|ip| match ip { ip if *ip == config::INTERFACE4 => Some(request.serverip), std::net::IpAddr::V4(ip4) => Some(*ip4), _ => None, }) .collect(), )), ); default_policy.apply_other.insert( dhcppkt::OPTION_DOMAINSEARCH, Some(dhcppkt::DhcpOptionTypeValue::DomainList( conf.dns_search.clone(), )), ); default_policy.apply_other.insert( dhcppkt::OPTION_CAPTIVEPORTAL, conf.captive_portal .clone() .map(dhcppkt::DhcpOptionTypeValue::String), ); /* Now build some sub policies, for each address range */ let all_addrs = conf.dhcp.get_all_used_addresses(); default_policy.policies = conf .addresses .iter() .filter_map(|prefix| { if let super::config::Prefix::V4(p4) = prefix { use crate::config::Match as _; use crate::config::PrefixOps as _; let subnet = erbium_net::Ipv4Subnet::new(p4.network(), p4.prefixlen).ok()?; let mut ret = config::Policy { match_subnet: Some(subnet), apply_address: Some( (1..((1 << (32 - p4.prefixlen)) - 2)) .map(|offset| (u32::from(subnet.network()) + offset).into()) // TODO: This removes one IP from the list, it should also remove any // others found on the local machine. Probably fine for now, but // likely to cause confusion in the future. .filter(|ip4| *ip4 != request.serverip) .collect::() .sub(&all_addrs), ), ..Default::default() }; /* If this is the interface the request is coming in, then we can do extra stuff */ if p4.contains(request.serverip) { // Add the MTU // TODO: Perhaps don't send it if it's default? if let Some(mtu) = request.if_mtu { ret.apply_other.insert( dhcppkt::OPTION_MTUIF, Some(dhcppkt::DhcpOptionTypeValue::U16(mtu as u16)), ); } // Add the default route. if let Some(route) = request.if_router { ret.apply_other.insert( dhcppkt::OPTION_ROUTERADDR, Some(dhcppkt::DhcpOptionTypeValue::Ip(route)), ); } } Some(ret) } else { None } }) .collect(); default_policy } pub fn handle_pkt( pools: &mut pool::Pool, request: &DHCPRequest, serverids: ServerIds, conf: &super::config::Config, ) -> Result { match request.pkt.options.get_messagetype() { Some(dhcppkt::DHCPDISCOVER) => { let base = [build_default_config(conf, request)]; handle_discover(pools, request, &serverids, &base, conf) } Some(dhcppkt::DHCPREQUEST) => { let base = [build_default_config(conf, request)]; handle_request(pools, request, &serverids, &base, conf) } Some(x) => Err(DhcpError::UnknownMessageType(x)), None => Err(DhcpError::ParseError(dhcppkt::ParseError::InvalidPacket)), } } async fn send_raw(raw: Arc, buf: &[u8], intf: i32) -> Result<(), std::io::Error> { DHCP_TX_PACKETS.inc(); raw.send_msg( buf, &raw::ControlMessage::new(), raw::MsgFlags::empty(), Some( &erbium_net::addr::linkaddr_for_ifindex( intf.try_into().unwrap(), /* TODO: Push IfIndex type back through callstack */ ) .to_net_addr(), ), ) .await .map(|_| ()) } async fn get_serverids(s: &SharedServerIds) -> ServerIds { s.lock().await.clone() } fn to_array(mac: &[u8]) -> Option<[u8; 6]> { mac[0..6].try_into().ok() } enum RunError { ListenError(std::io::Error), RecvError(std::io::Error), Io(std::io::Error), PoolError(pool::Error), } impl ToString for RunError { fn to_string(&self) -> String { match self { RunError::Io(e) => format!("I/O Error in DHCP: {}", e), RunError::PoolError(e) => format!("DHCP Pool Error: {}", e), RunError::ListenError(e) => format!("Failed to listen on DHCP: {}", e), RunError::RecvError(e) => format!("Failed to receive a packet for DHCP: {}", e), } } } pub struct DhcpService { netinfo: erbium_net::netinfo::SharedNetInfo, conf: crate::config::SharedConfig, rawsock: std::sync::Arc, pool: std::sync::Arc>, serverids: SharedServerIds, listener: UdpSocket, } impl DhcpService { async fn recvdhcp(&self, pkt: &[u8], src: NetAddr, intf: u32) { let raw = self.rawsock.clone(); /* First, lets find the various metadata IP addresses */ let ip4 = src.as_sockaddr_in().unwrap(); let optional_dst = self.netinfo.get_ipv4_by_ifidx(intf).await; if optional_dst.is_none() { log::warn!( "No IPv4 found on interface {}", self.netinfo.get_safe_name_by_ifidx(intf).await ); DHCP_ERRORS .with_label_values(&["NO_IPV4_ON_INTERFACE"]) .inc(); return; } /* Now lets decode the packet, and if it fails decode, fail the function early */ let req = match dhcppkt::parse(pkt) { Err(e) => { log::warn!("Failed to parse packet: {}", e); DHCP_ERRORS.with_label_values(&[e.get_variant_name()]).inc(); return; } Ok(req) => req, }; /* Log what we've got */ let if_mtu = self.netinfo.get_mtu_by_ifidx(intf).await; let if_router = match self.netinfo.get_ipv4_default_route().await { /* If the default route points out a different interface, then this is the default route */ Some((_, Some(rtridx))) if rtridx != intf => Some(optional_dst.unwrap()), /* If it's the same interface, then the default router should be the nexthop */ Some((Some(nexthop), Some(rtridx))) if rtridx == intf => Some(nexthop), _ => None, }; let request = DHCPRequest { pkt: req, serverip: optional_dst.unwrap(), ifindex: intf, if_mtu, if_router, }; log_pkt(&request, &self.netinfo).await; /* Now, lets process the packet we've found */ let reply; { /* Limit the amount of time we have these locked to just handling the packet */ let mut pool = self.pool.lock().await; let lockedconf = self.conf.read().await; reply = match handle_pkt( &mut pool, &request, get_serverids(&self.serverids).await, &lockedconf, ) { Err(e) => { log::warn!( "{}: Failed to handle {}: {}", format_client(&request.pkt), request .pkt .options .get_messagetype() .map(|x| x.to_string()) .unwrap_or_else(|| "packet".into()), e ); DHCP_ERRORS.with_label_values(&[e.get_variant_name()]).inc(); return; } Ok(r) => r, }; } /* Now, we should have a packet ready to send */ /* First, if we're claiming to be particular IP, we should remember that as an IP that is one * of ours */ if let Some(si) = reply.options.get_serverid() { self.serverids.lock().await.insert(si); } /* Log what we're sending */ log::info!( "{}: Sending {} on {} with {} for {}", format_client(&reply), reply .options .get_messagetype() .map(|x| x.to_string()) .unwrap_or_else(|| "[unknown]".into()), self.netinfo .get_name_by_ifidx(intf) .await .unwrap_or_else(|| "".into()), reply.yiaddr, reply .options .get_option::(&dhcppkt::OPTION_LEASETIME) .unwrap_or(0) ); log_options(&reply); /* Collect metadata ready to send */ let srcll = if let Some(erbium_net::netinfo::LinkLayer::Ethernet(srcll)) = self.netinfo.get_linkaddr_by_ifidx(intf).await { srcll } else { log::warn!("{}: Not a usable LinkLayer?!", format_client(&reply)); DHCP_ERRORS.with_label_values(&["UNUSABLE_LINKLAYER"]).inc(); return; }; let chaddr = if let Some(chaddr) = to_array(&reply.chaddr) { chaddr } else { log::warn!( "{}: Cannot send reply to invalid client hardware addr {:?}", format_client(&reply), reply.chaddr ); DHCP_ERRORS.with_label_values(&["INVALID_CHADDR"]).inc(); return; }; /* Construct the raw packet from the reply to send */ let replybuf = reply.serialise(); let etherbuf = packet::Fragment::new_udp4( *request.serverip.with_port(67).as_sockaddr_in().unwrap(), &srcll, *ip4, &chaddr, packet::Tail::Payload(&replybuf), ) .flatten(); if let Err(e) = send_raw(raw, ðerbuf, intf.try_into().unwrap()).await { log::warn!("{}: Failed to send reply: {:?}", format_client(&reply), e); DHCP_ERRORS.with_label_values(&["SEND_ERROR"]).inc(); } } async fn new_internal( netinfo: erbium_net::netinfo::SharedNetInfo, conf: super::config::SharedConfig, ) -> Result { let rawsock = Arc::new(raw::RawSocket::new(raw::EthProto::ALL).map_err(RunError::ListenError)?); let pool = Arc::new(sync::Mutex::new( pool::Pool::new().map_err(RunError::PoolError)?, )); let serverids: SharedServerIds = Arc::new(sync::Mutex::new(std::collections::HashSet::new())); let listener = UdpSocket::bind(&[UNSPECIFIED4.with_port(67)]) .await .map_err(RunError::ListenError)?; listener .set_opt_ipv4_packet_info(true) .map_err(RunError::ListenError)?; listener .set_opt_reuse_port(true) .map_err(RunError::ListenError)?; log::info!( "Listening for DHCP on {}", listener.local_addr().map_err(RunError::Io)? ); Ok(Self { netinfo, conf, rawsock, pool, serverids, listener, }) } pub async fn new( netinfo: erbium_net::netinfo::SharedNetInfo, conf: super::config::SharedConfig, ) -> Result { match Self::new_internal(netinfo, conf).await { Ok(x) => Ok(x), Err(e) => Err(e.to_string()), } } async fn run_internal( self: &std::sync::Arc, listener: &UdpSocket, ) -> Result<(), RunError> { loop { let rm = match listener.recv_msg(65536, udp::MsgFlags::empty()).await { Ok(m) => m, Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue, Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue, Err(e) => return Err(RunError::RecvError(e)), }; DHCP_RX_PACKETS.inc(); let self2 = self.clone(); tokio::spawn(async move { self2 .recvdhcp( &rm.buffer, rm.address.unwrap(), rm.local_intf().unwrap().try_into().unwrap(), ) .await }); } } pub async fn run(self: std::sync::Arc) -> Result<(), String> { match self.run_internal(&self.listener).await { Ok(_) => Ok(()), Err(e) => Err(e.to_string()), } } pub async fn update_metrics(self: &std::sync::Arc) { match self.pool.lock().await.get_pool_metrics() { Ok((in_use, expired)) => { DHCP_ACTIVE_LEASES.set(in_use.into()); DHCP_EXPIRED_LEASES.set(expired.into()); } Err(e) => log::warn!("Failed to update metrics: {}", e), } } pub async fn get_leases(self: &std::sync::Arc) -> Vec { let ret = self.pool.lock().await.get_leases(); match ret { Ok(l) => l, Err(e) => { log::warn!("Failed to get leases: {}", e); Vec::new() } } } } #[test] fn test_policy() { let cfg = config::Policy { match_subnet: Some(erbium_net::Ipv4Subnet::new("192.0.2.0".parse().unwrap(), 24).unwrap()), ..Default::default() }; let req = DHCPRequest { serverip: "192.0.2.67".parse().unwrap(), ..Default::default() }; let mut resp = Default::default(); let policies = vec![cfg]; assert!(apply_policies(&req, policies.as_slice(), &mut resp)); } #[tokio::test] async fn test_config_parse() -> Result<(), Box> { let cfg = crate::config::load_config_from_string_for_test( "--- dhcp-policies: - match-subnet: 192.168.0.0/24 apply-dns-servers: ['8.8.8.8', '8.8.4.4'] apply-subnet: 192.168.0.0/24 apply-time-offset: 3600 apply-domain-name: erbium.dev apply-forward: false apply-mtu: 1500 apply-broadcast: 192.168.255.255 apply-rebind-time: 120 apply-renewal-time: 90s apply-arp-timeout: 1w policies: - { match-host-name: myhost, apply-address: 192.168.0.1 } - { match-hardware-address: 00:01:02:03:04:05, apply-address: 192.168.0.2 } - match-interface: dmz apply-dns-servers: ['8.8.8.8'] apply-subnet: 192.0.2.0/24 # Reserve some space from the pool for servers policies: - apply-range: {start: 192.0.2.10, end: 192.0.2.20} # From the reserved pool, assign a static address. policies: - { match-hardware-address: 00:01:02:03:04:05, apply-address: 192.168.0.2 } # Reserve space for VPN endpoints - match-user-class: VPN apply-subnet: 192.0.2.128/25 ", )?; let mut resp = Response { ..Default::default() }; if !apply_policies( &DHCPRequest { pkt: dhcppkt::Dhcp { op: dhcppkt::OP_BOOTREQUEST, htype: dhcppkt::HWTYPE_ETHERNET, hlen: 6, hops: 0, xid: 0, secs: 0, flags: 0, ciaddr: net::Ipv4Addr::UNSPECIFIED, yiaddr: net::Ipv4Addr::UNSPECIFIED, siaddr: net::Ipv4Addr::UNSPECIFIED, giaddr: net::Ipv4Addr::UNSPECIFIED, chaddr: vec![0, 1, 2, 3, 4, 5], sname: vec![], file: vec![], options: dhcppkt::DhcpOptions { ..Default::default() }, }, serverip: "192.168.0.67".parse().unwrap(), ifindex: 1, if_mtu: None, if_router: None, }, &cfg.read().await.dhcp.policies, &mut resp, ) { panic!("No policies applied"); } log::info!("{:?}", cfg.read().await); assert_eq!( resp.address, Some( [std::net::Ipv4Addr::new(192, 168, 0, 2)] .iter() .cloned() .collect() ) ); Ok(()) } #[test] fn test_format_client() { let req = dhcppkt::Dhcp { op: dhcppkt::OP_BOOTREQUEST, htype: dhcppkt::HWTYPE_ETHERNET, hlen: 6, hops: 0, xid: 0, secs: 0, flags: 0, ciaddr: net::Ipv4Addr::UNSPECIFIED, yiaddr: net::Ipv4Addr::UNSPECIFIED, siaddr: net::Ipv4Addr::UNSPECIFIED, giaddr: net::Ipv4Addr::UNSPECIFIED, chaddr: vec![0, 1, 2, 3, 4, 5], sname: vec![], file: vec![], options: dhcppkt::DhcpOptions { ..Default::default() }, }; assert_eq!(format_client(&req), "00:01:02:03:04:05 ()"); } #[tokio::test] async fn test_defaults() { let mut p = pool::Pool::new_in_memory().expect("Failed to create pool"); let mut pkt = test::mk_dhcp_request(); pkt.pkt.options.mutate_option( &dhcppkt::OPTION_PARAMLIST, &vec![ 6u8, /* Domain Server */ 119, /* Domain Search */ 160, /* Captive Portal */ ], ); let serverids: ServerIds = ServerIds::new(); let conf = crate::config::Config { dns_servers: vec![ "192.0.2.53".parse().unwrap(), "2001:db8::53".parse().unwrap(), ], dns_search: vec!["example.org".into()], captive_portal: Some("example.com".into()), ..test::mk_default_config() }; let base = [build_default_config(&conf, &pkt)]; println!("base={:?}", base); let resp = handle_discover(&mut p, &pkt, &serverids, &base, &conf).expect("Failed to handle request"); assert_eq!( resp.options .get_option::>(&dhcppkt::OPTION_DOMAINSERVER), Some(vec!["192.0.2.53".parse::().unwrap()]) ); println!( "{:?}", resp.options.get_raw_option(&dhcppkt::OPTION_CAPTIVEPORTAL) ); assert_eq!( resp.options .get_option::>(&dhcppkt::OPTION_CAPTIVEPORTAL), Some("example.com".as_bytes().to_vec()) ); assert_eq!( resp.options .get_option::>(&dhcppkt::OPTION_DOMAINSEARCH), Some(vec![String::from("example.org")]) ); } #[tokio::test] async fn test_base() { let mut pool = pool::Pool::new_in_memory().expect("Failed to create pool"); let mut pkt = test::mk_dhcp_request(); pkt.pkt.options.mutate_option( &dhcppkt::OPTION_PARAMLIST, &vec![ 6u8, /* Domain Server */ 119, /* Domain Search */ 160, /* Captive Portal */ ], ); let serverids: ServerIds = ServerIds::new(); let mut apply_address: pool::PoolAddresses = Default::default(); apply_address.insert("192.0.2.3".parse().unwrap()); let conf = crate::config::Config { dns_servers: vec![ "192.0.2.53".parse().unwrap(), "2001:db8::53".parse().unwrap(), ], dns_search: vec!["example.org".into()], captive_portal: Some("example.com".into()), addresses: vec![config::Prefix::V4(config::Prefix4 { addr: "192.0.2.0".parse().unwrap(), prefixlen: 24, })], dhcp: config::Config { policies: vec![config::Policy { match_chaddr: Some(vec![0x0, 0x1, 0x2, 0x3, 0x4, 0x5]), apply_address: Some(apply_address), ..Default::default() }], }, ..Default::default() }; let base = build_default_config(&conf, &pkt); /* The generated policy should not allocate 192.0.2.3, because that is allocated in the * custom dhcp policy provided. */ assert!(!base.policies[0] .apply_address .as_ref() .unwrap() .contains(&"192.0.2.3".parse().unwrap())); println!("base={:#?}", base); println!("pkt={:?}", pkt); let resp = handle_discover(&mut pool, &pkt, &serverids, &[base], &conf) .expect("Failed to handle request"); assert_eq!( resp.options .get_option::>(&dhcppkt::OPTION_DOMAINSERVER), Some(vec!["192.0.2.53".parse::().unwrap()]) ); println!( "{:?}", resp.options.get_raw_option(&dhcppkt::OPTION_CAPTIVEPORTAL) ); assert_eq!( resp.options .get_option::>(&dhcppkt::OPTION_CAPTIVEPORTAL), Some("example.com".as_bytes().to_vec()) ); assert_eq!( resp.options .get_option::>(&dhcppkt::OPTION_DOMAINSEARCH), Some(vec![String::from("example.org")]) ); } #[tokio::test] /* There was a bug that if the suffix bits in the prefix were not 0, then it would silently ignore * the address. Now we set them to 0 ourselves explicitly. */ async fn test_non_network_prefix() { let conf = crate::config::load_config_from_string_for_test( "--- addresses: [192.0.2.53/24] ", ) .unwrap(); let pkt = test::mk_dhcp_request(); let base = build_default_config(&*conf.read().await, &pkt); let network = erbium_net::Ipv4Subnet::new("192.0.2.0".parse().unwrap(), 24).unwrap(); assert_eq!(base.policies[0].match_subnet.unwrap().addr, network.addr); assert_eq!( base.policies[0].match_subnet.unwrap().prefixlen, network.prefixlen ); } erbium-core-1.0.5/src/dhcp/pool.rs000064400000000000000000000622511046102023000150720ustar 00000000000000/* Copyright 2023 Perry Lorier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 * * DHCP Pool Management. */ use rusqlite::OptionalExtension; use std::collections::hash_map::DefaultHasher; use std::hash::Hash; use std::hash::Hasher; pub const DEFAULT_MIN_LEASE: std::time::Duration = std::time::Duration::from_secs(300); pub const DEFAULT_MAX_LEASE: std::time::Duration = std::time::Duration::from_secs(86400); pub type PoolAddresses = std::collections::HashSet; #[derive(Debug)] pub enum LeaseType { NewAddress, ReusingLease, Requested, Revived, } #[derive(Debug)] pub struct Lease { pub ip: std::net::Ipv4Addr, pub expire: std::time::Duration, pub lease_type: LeaseType, } #[derive(Ord, PartialOrd, Eq, PartialEq)] pub struct LeaseInfo { pub ip: std::net::Ipv4Addr, pub client_id: Vec, pub start: u32, pub expire: u32, pub options: Vec, } pub struct Pool { conn: rusqlite::Connection, } #[derive(Debug, PartialEq, Eq)] pub enum Error { DbError(String), CorruptDatabase(String), NoAssignableAddress, RequestedAddressInUse, } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Error::DbError(reason) => write!(f, "{}", reason), Error::CorruptDatabase(s) => write!(f, "Corrupt Database: {}", s), Error::NoAssignableAddress => write!(f, "No Assignable Address"), Error::RequestedAddressInUse => write!(f, "Requested address is in use"), } } } impl std::error::Error for Error {} impl Error { fn emit(reason: &str, e: &rusqlite::Error) -> Error { Error::DbError(format!("{} ({})", reason, e)) } } fn calculate_hash(s: &S, t: &T) -> u64 { let mut h = DefaultHasher::new(); s.hash(&mut h); t.hash(&mut h); h.finish() } impl Pool { // - upgrade_schema_from_no_version should install the latest schema // directly so that new installations need not go through the // upgrade chain. // - upgrade_schema_from_version_* should perform upgrades one // version at a time since that is the only thing that is tested. fn upgrade_schema_from_no_version(&self) -> Result { if self .conn .query_row("SELECT 1 FROM leases LIMIT 1", rusqlite::params![], |_| { Ok(()) }) .optional() .err() .is_none() { // This is not a fresh new database but just one with // the schema_version missing. We know that this is the // same as version 0. return Ok(0); } // Else, probably a brand new database. Create it directly // with the latest version. self.conn .execute( "CREATE TABLE leases ( address TEXT NOT NULL, chaddr BLOB, clientid BLOB, start INTEGER NOT NULL, expiry INTEGER NOT NULL, options BLOB, PRIMARY KEY (address) )", rusqlite::params![], ) .map_err(|e| Error::emit("Creating table leases", &e))?; Ok(1) } fn upgrade_schema_from_version_0(&self) -> Result { self.conn .execute( "ALTER TABLE leases ADD COLUMN options BLOB", rusqlite::params![], ) .map_err(|e| Error::emit("Upgrading to schema version 1", &e))?; Ok(1) } fn setup_db(self) -> Result { // Dummy primary key for the schema_version table. // If the same sqlite database were used by another module, // that module could use a different key within the same schema_version // table to track the schema of its own table(s). const DB_SCHEMA_KEY: &str = "pool"; self.conn .execute( "CREATE TABLE IF NOT EXISTS schema_version ( key TEXT NOT NULL, version INTEGER NOT NULL, PRIMARY KEY (key) )", rusqlite::params![], ) .map_err(|e| Error::emit("Creating table schema_version", &e))?; loop { let upgraded_to_version = match self.conn .query_row( "SELECT version FROM schema_version WHERE key = ?1", rusqlite::params![DB_SCHEMA_KEY], |row| row.get(0), ) .optional() .map_err(|e| Error::emit("Querying schema version", &e))? { None => self.upgrade_schema_from_no_version()?, Some(0) => self.upgrade_schema_from_version_0()?, Some(1) => break, // up to date Some(v) => return Err(Error::DbError(format!( "Lease database has version {} which is newer than 1, the newest supported version", v ))), }; self.conn .execute( "INSERT OR REPLACE INTO schema_version (key, version) VALUES (?1, ?2)", rusqlite::params![DB_SCHEMA_KEY, upgraded_to_version], ) .map_err(|e| Error::emit("Creating updating schema version", &e))?; } Ok(self) } fn new_with_conn(conn: rusqlite::Connection) -> Result { Pool { conn }.setup_db() } //#[cfg(any(test, fuzzing))] pub fn new_in_memory() -> Result { let conn = rusqlite::Connection::open_in_memory() .map_err(|e| Error::emit("Creating database in memory database", &e))?; Self::new_with_conn(conn) } pub fn new() -> Result { let conn = rusqlite::Connection::open("/var/lib/erbium/leases.sqlite") .map_err(|e| Error::emit("Creating database /var/lib/erbium/leases.sqlite", &e))?; Self::new_with_conn(conn) } pub fn get_pool_metrics(&mut self) -> Result<(u32, u32), Error> { let ts: u32 = std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH) .expect("clock failure") .as_secs() as u32; self.conn .query_row( "SELECT SUM(CASE WHEN expiry < ?1 THEN 1 ELSE 0 END) as active, SUM(CASE WHEN expiry >= ?1 THEN 1 ELSE 0 END) as expired FROM leases", rusqlite::params![ts], |row| Ok((row.get(0)?, row.get(1)?)), ) .map_err(|e| Error::DbError(e.to_string())) } pub fn get_leases(&mut self) -> Result, Error> { self.conn .prepare_cached( "SELECT address, clientid, start, expiry, options FROM leases", ) .map_err(|e| Error::DbError(e.to_string()))? .query_map([], |row| { Ok(LeaseInfo { ip: row .get::<_, String>(0)? .parse::() .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?, client_id: row.get(1)?, start: row.get(2)?, expire: row.get(3)?, options: row.get::>>(4)?.unwrap_or_default(), }) }) .map_err(|e| Error::DbError(e.to_string()))? .collect::, _>>() .map_err(|e| Error::DbError(e.to_string())) } fn select_requested_address( &mut self, requested: std::net::Ipv4Addr, ts: u32, addresses: &PoolAddresses, ) -> Result { if !addresses.contains(&requested) { Err(Error::NoAssignableAddress) } else if self .conn .query_row( "SELECT true FROM leases WHERE expiry >= ?1 AND address = ?2", rusqlite::params![ts, requested.to_string()], |_row| Ok(Some(())), ) .or_else(map_no_row_to_none)? .is_none() { Ok(Lease { ip: requested, expire: std::time::Duration::from_secs(0), /* We rely on the min_lease_time below */ lease_type: LeaseType::Requested, }) } else { Err(Error::RequestedAddressInUse) } } fn select_new_address( &mut self, ts: u32, addresses: &PoolAddresses, clientid: &[u8], ) -> Result { /* This performs a consistent hash of the clientid and the IP addresses * then orders by the distance from the hash of the clientid */ let clienthash = calculate_hash(&0, &clientid); let mut addresses = addresses .iter() .map(|ip| (calculate_hash(&clienthash, ip), ip)) .collect::>(); addresses.sort_unstable(); let addresses = addresses .iter() .map(|(_dist, ip)| ip) .copied() .collect::>(); /* Now for each address, see if it's in use, and if so, return it */ for i in addresses { if self .conn .query_row( "SELECT true FROM leases WHERE expiry >= ?1 AND address = ?2", rusqlite::params![ts, i.to_string()], |_row| Ok(Some(())), ) .or_else(map_no_row_to_none)? .is_none() { return Ok(Lease { ip: *i, expire: std::time::Duration::from_secs(0), /* We rely on the min_lease_time below */ lease_type: LeaseType::NewAddress, }); } } Err(Error::NoAssignableAddress) } fn select_address( &mut self, clientid: &[u8], requested: Option, addresses: &PoolAddresses, ) -> Result { let ts = std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH) .expect("clock failure") .as_secs(); /* RFC2131 Section 4.3.1: * If an address is available, the new address SHOULD be chosen as follows: * * o The client's current address as recorded in the client's current * binding, ELSE */ if let Some(lease) = self .conn .query_row( "SELECT address, expiry, start FROM leases WHERE clientid = ?1 AND expiry > ?2 ORDER BY address=?3 DESC, expiry DESC LIMIT 1", rusqlite::params![ clientid, ts as u32, requested .map(|ip| ip.to_string()) .unwrap_or_else(|| "".into()) ], |row| { Ok(Some(( row.get::(0)?, row.get::(1)?, row.get::(2)?, ))) }, ) .or_else(map_no_row_to_none)? { if let Ok(ip) = lease.0.parse::() { if addresses.contains(&ip) { // We want leases to double in size. But normally you renew your // lease at ½ the duration. We don't want to always just double // the lease, because you can accidentally end up with a ridiculously // long lease if you renew rapidly. // So instead we just use 3*renew. let expiry = (ts as u32).saturating_sub(lease.2).saturating_mul(3); return Ok(Lease { ip, expire: std::time::Duration::from_secs(expiry.into()), lease_type: LeaseType::ReusingLease, }); } } } /* o The client's previous address as recorded in the client's (now * expired or released) binding, if that address is in the server's * pool of available addresses and not already allocated, ELSE */ if let Some(lease) = self .conn .query_row( "SELECT address, start, max(expiry) as expire_time FROM leases WHERE clientid = ?1 GROUP BY 1 ORDER BY address=?2 DESC, expire_time DESC LIMIT 1 ", rusqlite::params![ clientid, requested .map(|ip| ip.to_string()) .unwrap_or_else(|| "".into()) ], |row| { Ok(Some(( row.get::(0)?, row.get::(1)?, row.get::(2)?, ))) }, ) .or_else(map_no_row_to_none)? { if let Ok(ip) = lease.0.parse::() { if addresses.contains(&ip) { return Ok(Lease { ip, /* If a device is constantly asking for the same lease, we should double * the lease time. This means transient devices get short leases, and * devices that are more permanent get longer leases. */ expire: std::time::Duration::from_secs(2 * (lease.2 - lease.1) as u64), lease_type: LeaseType::Revived, }); } } } /* o The address requested in the 'Requested IP Address' option, if that * address is valid and not already allocated, ELSE */ if let Some(addr) = requested { match self.select_requested_address(addr, ts as u32, addresses) { Err(Error::NoAssignableAddress) => (), Err(Error::RequestedAddressInUse) => (), x => return x, } } /* o A new address allocated from the server's pool of available * addresses; the address is selected based on the subnet from which * the message was received (if 'giaddr' is 0) or on the address of * the relay agent that forwarded the message ('giaddr' when not 0). */ self.select_new_address(ts as u32, addresses, clientid) } pub fn allocate_address( &mut self, clientid: &[u8], requested: Option, addresses: &PoolAddresses, min_expire_time: std::time::Duration, max_expire_time: std::time::Duration, raw_options: &[u8], ) -> Result { let lease = self.select_address(clientid, requested, addresses)?; let lease = Lease { expire: std::cmp::min( std::cmp::max(lease.expire, min_expire_time), max_expire_time, ), ..lease }; let ts = std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH) .expect("clock failure") .as_secs(); self.conn .execute( "INSERT OR REPLACE INTO leases (address, clientid, start, expiry, options) VALUES (?1, ?2, ?3, ?4, ?5)", rusqlite::params![ lease.ip.to_string(), clientid, ts as u32, (ts + lease.expire.as_secs()) as u32, raw_options, ], ) .map_err(|e| Error::DbError(format!("Failed to update lease: {}", e)))?; Ok(lease) } #[cfg(test)] fn reserve_address_internal( &mut self, client_id: &[u8], addr: std::net::Ipv4Addr, expired: bool, ) { self.conn .execute( "INSERT INTO leases (address, clientid, start, expiry) VALUES (?1, ?2, ?3, ?4)", rusqlite::params![ addr.to_string(), client_id, 0, /* Reserved from the beginning of time */ if expired { 0 } else { 0xFFFFFFFFu32 /* Until the end of time */ } ], ) .expect("Failed to add existing lease to pool"); } #[cfg(test)] fn reserve_address(&mut self, client_id: &[u8], addr: std::net::Ipv4Addr) { self.reserve_address_internal(client_id, addr, false); } #[cfg(test)] fn reserve_expired_address(&mut self, client_id: &[u8], addr: std::net::Ipv4Addr) { self.reserve_address_internal(client_id, addr, true); } } fn map_no_row_to_none(e: rusqlite::Error) -> Result, Error> { if e == rusqlite::Error::QueryReturnedNoRows { Ok(None) } else { Err(Error::emit("Database query Error", &e)) } } #[test] fn schema_upgrade_test() { let conn = rusqlite::Connection::open_in_memory().expect("Failed to create in-memory sqlite database"); // Install a copy of the original unversioned schema and expect all // of the upgrades to happen one step at a time. That way, this // single test should invoke them all. conn.execute( "CREATE TABLE IF NOT EXISTS leases ( address TEXT NOT NULL, chaddr BLOB, clientid BLOB, start INTEGER NOT NULL, expiry INTEGER NOT NULL, PRIMARY KEY (address) )", rusqlite::params![], ) .expect("Failed to set up test database with old schema"); Pool::new_with_conn(conn).expect("setup_db failed"); } #[test] fn smoke_test() { let mut p = Pool::new_in_memory().expect("Failed to create in memory pools"); let mut addrpool: PoolAddresses = Default::default(); addrpool.insert("192.168.0.100".parse().unwrap()); addrpool.insert("192.168.0.101".parse().unwrap()); addrpool.insert("192.168.0.102".parse().unwrap()); p.allocate_address( b"client", None, &addrpool, DEFAULT_MIN_LEASE, DEFAULT_MAX_LEASE, b"\xff", ) .expect("Didn't get allocated an address?!"); let mut leases = p.get_leases().expect("error calling get_leases()"); assert_eq!(leases.len(), 1); let lease = leases.pop().unwrap(); assert_eq!(lease.client_id, b"client"); assert_eq!(lease.options, b"\xff"); } #[test] fn empty_pool() { let mut p = Pool::new_in_memory().expect("Failed to create in memory pools"); /* Deliberately don't add any addresses, so we'll fail when we try and allocate something */ let addrpool: PoolAddresses = Default::default(); assert_eq!( p.allocate_address( b"client", None, &addrpool, DEFAULT_MIN_LEASE, DEFAULT_MAX_LEASE, b"", ) .expect_err("Got allocated an address from an empty pool!"), Error::NoAssignableAddress ); } #[test] fn reacquire_lease() { /* o The client's current address as recorded in the client's current binding */ let requested = "192.168.0.100".parse().unwrap(); let mut p = Pool::new_in_memory().expect("Failed to create in memory pools"); p.reserve_address(b"client", requested); let mut addrpool: PoolAddresses = Default::default(); addrpool.insert("192.168.0.100".parse().unwrap()); let lease = p .allocate_address( b"client", Some(requested), &addrpool, DEFAULT_MIN_LEASE, DEFAULT_MAX_LEASE, b"", ) .expect("Failed to allocate address"); assert_eq!(lease.ip, requested); assert!(lease.expire > std::time::Duration::from_secs(0)); } #[test] fn reacquire_expired_lease() { /* o The client's previous address as recorded in the client's (now expired or released) * binding, if that address is in the server's pool of available addresses and not already * allocated */ let mut p = Pool::new_in_memory().expect("Failed to create in memory pools"); let requested = "192.168.0.100".parse().unwrap(); let mut addrpool: PoolAddresses = Default::default(); addrpool.insert("192.168.0.100".parse().unwrap()); addrpool.insert("192.168.0.101".parse().unwrap()); addrpool.insert("192.168.0.102".parse().unwrap()); p.reserve_expired_address(b"client", requested); let lease = p .allocate_address( b"client", Some(requested), &addrpool, DEFAULT_MIN_LEASE, DEFAULT_MAX_LEASE, b"", ) .expect("Failed to allocate address"); assert_eq!(lease.ip, requested); assert!(lease.expire > std::time::Duration::from_secs(0)); } #[test] fn acquire_requested_address_success() { /* o The address requested in the 'Requested IP Address' option, if that address is valid and * not already allocated, ELSE */ let mut p = Pool::new_in_memory().expect("Failed to create in memory pools"); let requested = "192.168.0.101".parse().unwrap(); let mut addrpool: PoolAddresses = Default::default(); addrpool.insert("192.168.0.100".parse().unwrap()); addrpool.insert("192.168.0.101".parse().unwrap()); addrpool.insert("192.168.0.102".parse().unwrap()); let lease = p .allocate_address( b"client", Some(requested), &addrpool, DEFAULT_MIN_LEASE, DEFAULT_MAX_LEASE, b"", ) .expect("Failed to allocate address"); assert_eq!(lease.ip, requested); } #[test] fn acquire_requested_address_in_use() { /* o The address requested in the 'Requested IP Address' option, if that address is valid and * not already allocated, ELSE */ let mut p = Pool::new_in_memory().expect("Failed to create in memory pools"); let requested = "192.168.0.101".parse().unwrap(); p.reserve_address(b"other-client", requested); let mut addrpool: PoolAddresses = Default::default(); addrpool.insert("192.168.0.1".parse().unwrap()); let lease = p .allocate_address( b"client", Some(requested), &addrpool, DEFAULT_MIN_LEASE, DEFAULT_MAX_LEASE, b"", ) .expect("Failed to allocate address"); /* Do not assigned the reserved address! */ assert_ne!(lease.ip, requested); } #[test] fn acquire_requested_address_invalid() { /* o The address requested in the 'Requested IP Address' option, if that address is valid and * not already allocated, ELSE */ let mut p = Pool::new_in_memory().expect("Failed to create in memory pools"); let mut addrpool: PoolAddresses = Default::default(); addrpool.insert("192.168.0.1".parse().unwrap()); let requested = "10.0.0.1".parse().unwrap(); let lease = p .select_address(b"client", Some(requested), &addrpool) .expect("Failed to allocate address"); /* Do not assigned the reserved address! */ assert_ne!(lease.ip, requested); } #[test] fn dont_hand_out_old_stale_lease() { /* If this client previously had an address that is no longer in the pool, * don't hand out the old address! Give them a new one! */ let mut p = Pool::new_in_memory().expect("Failed to create in memory pools"); let mut addrpool: PoolAddresses = Default::default(); let old_reserved = "192.168.0.101".parse().unwrap(); p.reserve_address(b"client", old_reserved); addrpool.insert("192.168.0.100".parse().unwrap()); let lease = p .select_address(b"client", Some(old_reserved), &addrpool) .expect("Failed to allocate address"); /* Do not assigned the old_reserved address! */ assert_ne!(lease.ip, old_reserved); } erbium-core-1.0.5/src/dhcp/test.rs000064400000000000000000000652611046102023000151040ustar 00000000000000/* Copyright 2023 Perry Lorier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 * * Sections quoted from RFCs are covered by the terms specified in RFC3978. * * Tests for DHCP functionality. */ use crate::dhcp; use crate::dhcp::dhcppkt; use crate::dhcp::pool; use rand::Rng; use std::net; use tokio::sync; const EXAMPLE_IP1: net::Ipv4Addr = net::Ipv4Addr::new(192, 0, 2, 1); /* Documentation prefix 1 */ const EXAMPLE_IP2: net::Ipv4Addr = net::Ipv4Addr::new(192, 0, 2, 2); /* Documentation prefix 2 */ const EXAMPLE_IP3: net::Ipv4Addr = net::Ipv4Addr::new(192, 0, 2, 3); /* Documentation prefix 3 */ const EXAMPLE_IP4: net::Ipv4Addr = net::Ipv4Addr::new(192, 0, 2, 4); /* Documentation prefix 4 */ const SERVER_IP: net::Ipv4Addr = EXAMPLE_IP1; const SERVER_IP2: net::Ipv4Addr = EXAMPLE_IP2; const NOT_SERVER_IP: net::Ipv4Addr = EXAMPLE_IP3; const CLIENTID: &[u8] = b"Client Identifier"; fn mk_dhcp_request_pkt() -> dhcppkt::Dhcp { dhcppkt::Dhcp { op: dhcppkt::OP_BOOTREQUEST, htype: dhcppkt::HWTYPE_ETHERNET, hlen: 6, hops: 0, xid: rand::thread_rng().gen(), secs: 0, flags: 0, ciaddr: net::Ipv4Addr::UNSPECIFIED, yiaddr: net::Ipv4Addr::UNSPECIFIED, siaddr: net::Ipv4Addr::UNSPECIFIED, giaddr: net::Ipv4Addr::UNSPECIFIED, chaddr: vec![ 0x00, 0x00, 0x5E, 0x00, 0x53, 0x00, /* Reserved for documentation, per RFC7042 */ ], sname: vec![], file: vec![], options: dhcppkt::DhcpOptions::default() .set_option(&dhcppkt::OPTION_MSGTYPE, &dhcppkt::DHCPREQUEST) .set_option(&dhcppkt::OPTION_HOSTNAME, &vec![dhcppkt::OPTION_HOSTNAME]) // TODO: is this correct? .set_option( &dhcppkt::OPTION_PARAMLIST, &vec![1u8, 3u8, 6u8, 15, 26, 28, 51, 58, 59, 43], ), } } pub fn mk_dhcp_request() -> dhcp::DHCPRequest { dhcp::DHCPRequest { pkt: mk_dhcp_request_pkt(), serverip: SERVER_IP, ifindex: 1, if_mtu: None, if_router: None, } } pub fn mk_default_config() -> crate::config::Config { let mut apply_address: pool::PoolAddresses = Default::default(); for i in 1..255 { apply_address .insert((u32::from("192.0.2.0".parse::().unwrap()) + i).into()); } crate::config::Config { dhcp: dhcp::config::Config { policies: vec![dhcp::config::Policy { match_subnet: Some( erbium_net::Ipv4Subnet::new("192.0.2.0".parse().unwrap(), 24).unwrap(), ), apply_address: Some(apply_address), ..Default::default() }], }, ..Default::default() } } #[test] fn test_parsing_inverse_serialising() { let mut orig_pkt = mk_dhcp_request(); orig_pkt.pkt.options = orig_pkt .pkt .options .set_option(&dhcppkt::OPTION_LEASETIME, &(321 as u32)) .set_option(&dhcppkt::OPTION_SERVERID, &SERVER_IP) .set_option(&dhcppkt::OPTION_CLIENTID, &CLIENTID) .set_option(&dhcppkt::OPTION_NTPSERVERS, &EXAMPLE_IP4); let bytes = orig_pkt.pkt.serialise(); let new_pkt = dhcppkt::parse(bytes.as_slice()).expect("Failed to parse DHCP packet"); assert!( orig_pkt.pkt.sname.len() <= 64, "sname={:?} ({} <= 64 is false", orig_pkt.pkt.sname, orig_pkt.pkt.sname.len() ); assert!( orig_pkt.pkt.chaddr.len() <= 16, "chaddr={:?} ({} <= 16 is false", orig_pkt.pkt.chaddr, orig_pkt.pkt.chaddr.len() ); assert_eq!(orig_pkt.pkt, new_pkt); } #[tokio::test] async fn test_handle_pkt() { let mut request = mk_dhcp_request(); request.pkt.options = request .pkt .options .set_option(&dhcppkt::OPTION_CLIENTID, &CLIENTID) .set_option(&dhcppkt::OPTION_LEASETIME, &321u32) .set_option(&dhcppkt::OPTION_SERVERID, &SERVER_IP); let mut p = pool::Pool::new_in_memory().expect("Failed to create pool"); let mut serverids: dhcp::ServerIds = dhcp::ServerIds::new(); serverids.insert(SERVER_IP); let conf = mk_default_config(); dhcp::handle_pkt(&mut p, &request, serverids, &conf).expect("Failed to handle request"); } #[test] fn truncated_pkt() { /* Check that truncated packets don't cause panics or other problems */ let mut orig_pkt = mk_dhcp_request(); orig_pkt.pkt.options = orig_pkt .pkt .options .set_option(&dhcppkt::OPTION_CLIENTID, &CLIENTID) .set_option(&dhcppkt::OPTION_LEASETIME, &(321u32)) .set_option(&dhcppkt::OPTION_SERVERID, &SERVER_IP); let bytes = orig_pkt.pkt.serialise(); for i in 0..(bytes.len() - 1) { match dhcppkt::parse(&bytes[0..i]) { Err(dhcppkt::ParseError::UnexpectedEndOfInput) => (), x => panic!("Unexpected response: {:?}", x), } } } /* rfc2131 Section 2: The 'client identifier' chosen by a DHCP client MUST be unique to that client * within the subnet to which the client is attached. * * Commentary: Only required by the client, not the server. */ /* rfc2131 Section 2: If the client uses a 'client identifier' in * one message, it MUST use that same identifier in all subsequent messages, to ensure that all * servers correctly identify the client. * * Commentary: Only required by the client, not the server. */ /* rfc2131 Section 2: A DHCP client must be prepared to receive DHCP messages with an 'options' * field of at least length 312 octets. * * Commentary: TODO: Check what happens when the server needs to send a huge reply to a client. * This is also not a requirement, as must is in lower case. */ /* rfc2131 Section 2: The remaining bits of the flags field are reserved for future use. They MUST * be set to zero by clients and ignored by servers and relay agents. * * Commentary: Check the bits are ignord by servers. */ #[tokio::test] async fn ignore_unused_flag_bits() { let mut p = pool::Pool::new_in_memory().expect("Failed to create pool"); let mut pkt = mk_dhcp_request(); pkt.pkt.flags = 0x7FFF; let serverids: dhcp::ServerIds = dhcp::ServerIds::new(); let conf = mk_default_config(); dhcp::handle_discover(&mut p, &pkt, &serverids, &[], &conf).expect("Failed to handle request"); } /* rfc2131 Section 3.1 Step 3: The client broadcasts a DHCPREQUEST message that MUST include the * 'server identifier' option to indicate which server it has selected, and that MAY include other * options specifying desired configuration values. * * Commentary: Client side behaviour. */ /* rfc2131 Section 3.1 Step 3: The 'requested IP address' option MUST be set to the value of * 'yiaddr' in the DHCPOFFER message from the server. * * Commentary: Client side behaviour, but we should check we set the yiaddr correctly. */ #[tokio::test] async fn confirm_yiaddr_set() { let mut p = pool::Pool::new_in_memory().expect("Failed to create pool"); let pkt = mk_dhcp_request(); let serverids: dhcp::ServerIds = dhcp::ServerIds::new(); let conf = mk_default_config(); let reply = dhcp::handle_discover(&mut p, &pkt, &serverids, &[], &conf) .expect("Failed to handle request"); assert_ne!( reply.yiaddr, net::Ipv4Addr::UNSPECIFIED, "yiaddr is not set on replies" ); } /* rfc2131 Section 3.1 Step 3: To help ensure that any BOOTP relay agents forward the DHCPREQUEST * message to the same set of DHCP servers that received the original DHCPDISCOVER message, the * DHCPREQUEST message MUST use the same value in the DHCP message header's 'secs' field and be * sent to the same IP broadcast address as the original DHCPDISCOVER message. * * Commentary: Client side behaviour. */ /* rfc2131 Section 3.1 Step 5: If the client detects that the address is already in use (e.g., * through the use of ARP), the client MUST send a DHCPDECLINE message to the server and restarts * the configuration process. * * Commentary: Client side behaviour. */ /* rfc2131 Section 3.1 Step 6: If the client used a 'client identifier' when it obtained the lease, * it MUST use the same 'client identifier' in the DHCPRELEASE message. * * Commentary: Client side behaviour. */ /* rfc2131 Section 3.2 Step 1: The server MUST broadcast the DHCPNAK message to the 0xffffffff broadcast address because the client may not have a correct network address or subnet mask, and the client may not be answering ARP requests. Otherwise, the server MUST send the DHCPNAK message to the IP address of the BOOTP relay agent, as recorded in 'giaddr'. */ fn broadcast_failed_renew() { /* TODO */ } /* rfc2131 Section 3.2 Step 3: If the client detects that the IP address in the DHCPACK message is * already in use, the client MUST send a DHCPDECLINE message to the server and restarts the * configuration process by requesting a new network address. * * Commentary: Client side behaviour. */ /* rfc2131 Section 3.4: The server SHOULD check the network address in a DHCPINFORM message for * consistency, but MUST NOT check for an existing lease. */ fn dhcpinform_dont_check_existing_lease() { /* TODO */ } /* rfc2131 Section 3.5: If the client includes a list of parameters in a DHCPDISCOVER message, it * MUST include that list in any subsequent DHCPREQUEST messages. * * Commentary: Client side behaviour. */ /* rfc2131 Section 4.1: A server with multiple network addresses MUST be prepared to to accept any * of its network addresses as identifying that server in a DHCP message. To accommodate * potentially incomplete network connectivity, a server MUST choose an address as a 'server * identifier' that, to the best of the server's knowledge, is reachable from the client. * * Commentary: The Server Identifier is set to the IP address of the interface the packet was * received on. We remember all ServerIdentifiers we've ever handed out, and if it's not for one * of them, we ignore the packet as being for a different server. */ #[tokio::test] async fn server_address_set() { let mut p = pool::Pool::new_in_memory().expect("Failed to create pool"); let pkt = mk_dhcp_request(); let serverids: dhcp::ServerIds = dhcp::ServerIds::new(); let conf = mk_default_config(); let reply = dhcp::handle_discover(&mut p, &pkt, &serverids, &[], &conf) .expect("Failed to handle request"); assert_ne!( reply .options .get_serverid() .expect("server identifier not set on repliy"), net::Ipv4Addr::UNSPECIFIED, "server identifier is not set on replies" ); } #[tokio::test] async fn ignore_other_request() { let pools = std::sync::Arc::new(sync::Mutex::new( pool::Pool::new_in_memory().expect("Failed to create pool"), )); let mut p = pools.lock().await; let mut pkt = mk_dhcp_request(); pkt.pkt.options = pkt .pkt .options .set_option(&dhcppkt::OPTION_SERVERID, &NOT_SERVER_IP); let mut serverids: dhcp::ServerIds = dhcp::ServerIds::new(); serverids.insert(SERVER_IP); serverids.insert(SERVER_IP2); let cfg = mk_default_config(); let reply = dhcp::handle_request(&mut p, &pkt, &serverids, &[], &cfg) .expect_err("Handled request not to me"); assert_eq!( reply, dhcp::DhcpError::OtherServer(NOT_SERVER_IP), "Packet to not-a-server-ip should be ignored." ); } /* RFC2131 Section 4.1: DHCP clients MUST use the IP address provided in the 'server identifier' * option for any unicast requests to the DHCP server. * * Commentary: Client side behaviour. */ /* RFC2131 Section 4.1: If the options in a DHCP message extend into the 'sname' and 'file' fields, * the 'option overload' option MUST appear in the 'options' field, with value 1, 2 or 3, as * specified in RFC 1533. * * Commentary: We don't yet support options in the sname or file fields. */ /* RFC2131 Section 4.1: If the 'option overload' option is present in the 'options' field, the * options in the 'options' field MUST be terminated by an 'end' option, and MAY contain one or * more 'pad' options to fill the options field. * * Commentary: We don't yet support the "option overload" option. */ /* RFC2131 Section 4.1: The options in the 'sname' and 'file' fields (if in use as indicated by the * 'options overload' option) MUST begin with the first octet of the field, MUST be terminated by * an 'end' option, and MUST be followed by 'pad' options to fill the remainder of the field. * * Commentary: We don't yet support the "option overload" option to have options in sname or file * fields. */ /* RFC2131 Section 4.1: Any individual option in the 'options', 'sname' and 'file' fields MUST be * entirely contained in that field. * * Commentary: We don't yet support the "option overload" option to have options in sname or file * fields. */ /* RFC2131 Section 4.1: The options in the 'options' field MUST be interpreted first, so that any * 'option overload' options may be interpreted. The 'file' field MUST be interpreted next (if the * 'option overload' option indicates that the 'file' field contains DHCP options), followed by the * 'sname' field. * * Commentary: We don't yet support the "option overload" option to have options in sname or file * fields. */ /* RFC2131 Section 4.1: The client MUST adopt a retransmission strategy that incorporates a * randomized exponential backoff algorithm to determine the delay between retransmissions. * * Commentary: Client side behaviour. */ /* RFC2131 Section 4.1: A DHCP client MUST choose 'xid's in such a way as to minimize the chance of * using an 'xid' identical to one used by another client. * * Commentary: Client side behaviour. */ /* RFC2131 Section 4.1: If the client supplies a 'client identifier', the client MUST use the same * 'client identifier' in all subsequent messages, and the server MUST use that identifier to * identify the client. * * Commentary: Client side behaviour. */ /* RFC2131 Section 4.1: If the client does not provide a 'client identifier' option, the server * MUST use the contents of the 'chaddr' field to identify the client. */ #[test] fn client_identifier_or_chaddr() { let mut ci = mk_dhcp_request(); ci.pkt.options = ci .pkt .options .set_option(&dhcppkt::OPTION_CLIENTID, &vec![1u8, 2, 3]); println!("{:?}", ci.pkt.options); assert_eq!( ci.pkt.get_client_id(), vec![1, 2, 3], "Did not use client identifier option!" ); let mut ch = mk_dhcp_request(); ch.pkt.options = ch.pkt.options.remove_option(&dhcppkt::OPTION_CLIENTID); assert_eq!( ch.pkt.get_client_id(), vec![0x00, 0x00, 0x5E, 0x00, 0x53, 0x00,], "Did not use chaddr" ); } /* RFC2131 Section 4.3.1: * Option DHCPOFFER DHCPACK DHCPNAK * ------ --------- ------- ------- * 'op' BOOTREPLY BOOTREPLY BOOTREPLY * 'htype' (From "Assigned Numbers" RFC) * 'hlen' (Hardware address length in octets) * 'hops' 0 0 0 * 'xid' 'xid' from client 'xid' from client 'xid' from client * DHCPDISCOVER DHCPREQUEST DHCPREQUEST * message message message * 'secs' 0 0 0 * 'ciaddr' 0 'ciaddr' from 0 * DHCPREQUEST or 0 * 'yiaddr' IP address offered IP address 0 * to client assigned to client * 'siaddr' IP address of next IP address of next 0 * bootstrap server bootstrap server * 'flags' 'flags' from 'flags' from 'flags' from * client DHCPDISCOVER client DHCPREQUEST client DHCPREQUEST * message message message * 'giaddr' 'giaddr' from 'giaddr' from 'giaddr' from * client DHCPDISCOVER client DHCPREQUEST client DHCPREQUEST * message message message * 'chaddr' 'chaddr' from 'chaddr' from 'chaddr' from * client DHCPDISCOVER client DHCPREQUEST client DHCPREQUEST * message message message * 'sname' Server host name Server host name (unused) * or options or options * 'file' Client boot file Client boot file (unused) * name or options name or options * Requested IP address MUST NOT MUST NOT MUST NOT * IP address lease time MUST MUST (DHCPREQUEST) MUST NOT * MUST NOT (DHCPINFORM) * Use 'file'/'sname' fields MAY MAY MUST NOT * DHCP message type DHCPOFFER DHCPACK DHCPNAK * Parameter request list MUST NOT MUST NOT MUST NOT * Message SHOULD SHOULD SHOULD * Client identifier MUST NOT MUST NOT MAY * Vendor class identifier MAY MAY MAY * Server identifier MUST MUST MUST * Maximum message size MUST NOT MUST NOT MUST NOT * All others MAY MAY MUST NOT */ #[tokio::test] async fn offer_required() { let mut request = mk_dhcp_request(); request.pkt.options = request .pkt .options .set_option(&dhcppkt::OPTION_CLIENTID, &CLIENTID) .set_option(&dhcppkt::OPTION_LEASETIME, &321u32) .set_option(&dhcppkt::OPTION_SERVERID, &SERVER_IP) .set_option(&dhcppkt::OPTION_MSGTYPE, &dhcppkt::DHCPDISCOVER); let mut p = pool::Pool::new_in_memory().expect("Failed to create pool"); let mut serverids: dhcp::ServerIds = dhcp::ServerIds::new(); serverids.insert(SERVER_IP); let conf = mk_default_config(); let reply = dhcp::handle_pkt(&mut p, &request, serverids, &conf).expect("Failed to handle request"); assert_eq!(reply.op, dhcppkt::OP_BOOTREPLY); assert_eq!(reply.htype, dhcppkt::HWTYPE_ETHERNET); assert_eq!(reply.hlen, 6); assert_eq!(reply.hops, 0); assert_eq!(reply.xid, request.pkt.xid); assert_eq!(reply.secs, 0); assert_eq!(reply.ciaddr, std::net::Ipv4Addr::UNSPECIFIED); assert_ne!(reply.yiaddr, std::net::Ipv4Addr::UNSPECIFIED); assert_eq!(reply.siaddr, std::net::Ipv4Addr::UNSPECIFIED); assert_eq!(reply.flags, request.pkt.flags); assert_eq!(reply.giaddr, request.pkt.giaddr); assert_eq!(reply.chaddr, request.pkt.chaddr); assert_eq!(reply.options.get_messagetype().unwrap(), dhcppkt::DHCPOFFER); assert!(reply .options .get_option::>(&dhcppkt::OPTION_ADDRESSREQUEST) .is_none()); assert!(reply .options .get_option::>(&dhcppkt::OPTION_PARAMLIST) .is_none()); assert!(reply .options .get_option::>(&dhcppkt::OPTION_SERVERID) .is_some()); assert!(reply .options .get_option::>(&dhcppkt::OPTION_MAXMSGSIZE) .is_none()); assert!(reply .options .get_option::>(&dhcppkt::OPTION_CLIENTID) .is_none()); } #[tokio::test] async fn ack_required() { let mut request = mk_dhcp_request(); request.pkt.options = request .pkt .options .set_option(&dhcppkt::OPTION_CLIENTID, &CLIENTID) .set_option(&dhcppkt::OPTION_LEASETIME, &321u32) .set_option(&dhcppkt::OPTION_SERVERID, &SERVER_IP) .set_option(&dhcppkt::OPTION_MSGTYPE, &dhcppkt::DHCPREQUEST); let mut p = pool::Pool::new_in_memory().expect("Failed to create pool"); let mut serverids: dhcp::ServerIds = dhcp::ServerIds::new(); serverids.insert(SERVER_IP); let conf = mk_default_config(); let reply = dhcp::handle_pkt(&mut p, &request, serverids, &conf).expect("Failed to handle request"); assert_eq!(reply.op, dhcppkt::OP_BOOTREPLY); assert_eq!(reply.htype, dhcppkt::HWTYPE_ETHERNET); assert_eq!(reply.hlen, 6); assert_eq!(reply.hops, 0); assert_eq!(reply.xid, request.pkt.xid); assert_eq!(reply.secs, 0); assert!( (reply.ciaddr == std::net::Ipv4Addr::UNSPECIFIED) || (reply.ciaddr == request.pkt.ciaddr) ); assert_ne!(reply.yiaddr, std::net::Ipv4Addr::UNSPECIFIED); assert_eq!(reply.siaddr, std::net::Ipv4Addr::UNSPECIFIED); assert_eq!(reply.flags, request.pkt.flags); assert_eq!(reply.giaddr, request.pkt.giaddr); assert_eq!(reply.chaddr, request.pkt.chaddr); assert_eq!(reply.options.get_messagetype().unwrap(), dhcppkt::DHCPACK); assert!(reply .options .get_option::>(&dhcppkt::OPTION_ADDRESSREQUEST) .is_none()); assert!(reply .options .get_option::>(&dhcppkt::OPTION_PARAMLIST) .is_none()); assert!(reply .options .get_option::>(&dhcppkt::OPTION_SERVERID) .is_some()); assert!(reply .options .get_option::>(&dhcppkt::OPTION_MAXMSGSIZE) .is_none()); assert!(reply .options .get_option::>(&dhcppkt::OPTION_CLIENTID) .is_none()); } #[tokio::test] async fn test_renew_unknown() { /* If the server is started and there is a client that tries to renew a lease we've not heard * about. If the lease is available, we should update our database and give it to them! */ let mut request = mk_dhcp_request(); request.pkt.options = request .pkt .options .set_option(&dhcppkt::OPTION_CLIENTID, &CLIENTID) .set_option(&dhcppkt::OPTION_MSGTYPE, &dhcppkt::DHCPREQUEST); request.pkt.ciaddr = EXAMPLE_IP2; let mut p = pool::Pool::new_in_memory().expect("Failed to create pool"); let mut serverids: dhcp::ServerIds = dhcp::ServerIds::new(); serverids.insert(SERVER_IP); let conf = mk_default_config(); let reply = dhcp::handle_pkt(&mut p, &request, serverids, &conf).expect("Failed to handle request"); assert_eq!(reply.yiaddr, EXAMPLE_IP2); } #[tokio::test] async fn test_full() { /* This is an end to end test, testing a sequence of packets that a client should send and * making sure that we handle them correctly. */ let mut p = pool::Pool::new_in_memory().expect("Failed to create pool"); let mut serverids: dhcp::ServerIds = dhcp::ServerIds::new(); let conf = mk_default_config(); let xid = rand::thread_rng().gen(); let secs = 0; /* Send DISCOVER */ let mut request = mk_dhcp_request(); request.pkt.xid = xid; request.pkt.secs = secs; request.pkt.options = request .pkt .options .set_option(&dhcppkt::OPTION_CLIENTID, &CLIENTID) .set_option(&dhcppkt::OPTION_MSGTYPE, &dhcppkt::DHCPDISCOVER); let offer = dhcp::handle_pkt(&mut p, &request, serverids.clone(), &conf) .expect("Failed to handle request"); serverids.insert(offer.options.get_serverid().unwrap()); assert_eq!(offer.options.get_messagetype(), Some(dhcppkt::DHCPOFFER)); /* Send REQUEST */ let mut request = mk_dhcp_request(); request.pkt.xid = xid; request.pkt.secs = secs; request.pkt.options = request .pkt .options .set_option(&dhcppkt::OPTION_CLIENTID, &CLIENTID) .set_option(&dhcppkt::OPTION_MSGTYPE, &dhcppkt::DHCPREQUEST) .set_option(&dhcppkt::OPTION_ADDRESSREQUEST, &offer.yiaddr); let ack = dhcp::handle_pkt(&mut p, &request, serverids.clone(), &conf) .expect("Failed to handle request"); assert_eq!(ack.options.get_messagetype(), Some(dhcppkt::DHCPACK)); assert_eq!(ack.yiaddr, offer.yiaddr); /* make sure we don't needlessly change our mind */ /* Time passes and now we want to renew */ let mut request = mk_dhcp_request(); /* xid and seconds are not copied from the previous requests */ request.pkt.secs = 0; request.pkt.ciaddr = offer.yiaddr; request.pkt.options = request .pkt .options .set_option(&dhcppkt::OPTION_CLIENTID, &CLIENTID) .set_option(&dhcppkt::OPTION_MSGTYPE, &dhcppkt::DHCPREQUEST); /* no server id */ let ack = dhcp::handle_pkt(&mut p, &request, serverids.clone(), &conf) .expect("Failed to handle request"); assert_eq!(ack.options.get_messagetype(), Some(dhcppkt::DHCPACK)); assert_eq!(ack.yiaddr, offer.yiaddr); /* Did we get back the same address? */ /* Okay, now it's time to RELEASE the address */ let mut request = mk_dhcp_request(); /* xid and seconds are not copied from the previous requests */ request.pkt.secs = 0; request.pkt.options = request .pkt .options .set_option(&dhcppkt::OPTION_CLIENTID, &CLIENTID) .set_option(&dhcppkt::OPTION_MSGTYPE, &dhcppkt::DHCPRELEASE); /* no server id */ /* release is not supported, so we expect an error here. But we shouldn't crash */ let _ack = dhcp::handle_pkt(&mut p, &request, serverids, &conf).expect_err("Failed to handle request"); } #[tokio::test] async fn test_defaults() { let mut p = pool::Pool::new_in_memory().expect("Failed to create pool"); let pkt = mk_dhcp_request(); let conf = crate::config::load_config_from_string_for_test( " dhcp-policies: - match-subnet: 192.0.2.0/24 apply-address: 192.0.2.1 apply-netmask: null ", ) .expect("Failed to parse test config"); let lockedconf = conf.read().await; let serverids: dhcp::ServerIds = dhcp::ServerIds::new(); let reply = dhcp::handle_discover(&mut p, &pkt, &serverids, &[], &lockedconf) .expect("Failed to handle request"); /* We've asked that netmask doesn't get set, so check it's not set */ assert_eq!( reply .options .get_option::(&dhcppkt::OPTION_NETMASK), None ); /* We've not specified what happens to the broadcast, so check it was defaulted correctly. */ assert_eq!( reply .options .get_option::(&dhcppkt::OPTION_BROADCAST), Some("192.0.2.255".parse().expect("Failed to parse IP")) ); } /* TODO: * 4. The servers receive the DHCPREQUEST broadcast from the client. Those servers not selected by * the DHCPREQUEST message use the message as notification that the client has declined that * server's offer. */ erbium-core-1.0.5/src/dns/acl.rs000064400000000000000000000034561046102023000145300ustar 00000000000000/* Copyright 2020 Perry Lorier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 * * ACL processing for incoming DNS packets. */ use super::dnspkt; use super::router; use crate::acl; use crate::config; use erbium_net::addr::NetAddrExt as _; use super::DnsMessage; use super::Error; pub(super) struct DnsAclHandler { config: config::SharedConfig, next: router::DnsRouteHandler, } impl DnsAclHandler { pub async fn new(config: config::SharedConfig) -> Self { Self { config: config.clone(), next: router::DnsRouteHandler::new(config).await, } } pub async fn handle_query(&self, msg: &DnsMessage) -> Result { acl::require_permission( &self.config.read().await.acls, &acl::Attributes { addr: msg.remote_addr, }, acl::PermissionType::DnsRecursion, ) .map_err(Error::RefusedByAcl)?; if msg.in_query.question.qtype == dnspkt::RR_ANY { return Err(Error::Denied("ANY queries are not allowed".into())); } else if msg.remote_addr.port() == Some(53) { return Err(Error::Denied("Invalid Source Port".into())); } self.next.handle_query(msg).await } } erbium-core-1.0.5/src/dns/bucket.rs000064400000000000000000000067511046102023000152470ustar 00000000000000/* Copyright 2023 Perry Lorier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 * * Token Bucket implementation */ /* The token bucket holds a timestamp of when the bucket last had 0 tokens remaining in it. * The current contents of the bucket can be calculated by taking how long ago that was, and * calculating how many tokens would have been deposited in the bucket since then. Updating * the bucket to deplete some tokens is handled similarly. */ type TokenCount = u32; pub type RealTimeClock = std::time::SystemTime; pub trait Clock: Send + Sync + 'static { fn now() -> u32; } impl Clock for RealTimeClock { fn now() -> u32 { RealTimeClock::now() .duration_since(RealTimeClock::UNIX_EPOCH) .unwrap() .as_secs() as u32 } } pub struct GenericTokenBucket(TokenCount); impl GenericTokenBucket { const MAX_TOKENS: u32 = 100; const TOKENS_PER_SECOND: u32 = 2; pub const fn new() -> Self { Self(0) } // If the bucket is currently "over full", then cap it at the maximum fullness. fn get_tokens_with_time(&self, now: u32) -> TokenCount { std::cmp::max(self.0, now - Self::MAX_TOKENS / Self::TOKENS_PER_SECOND) } fn get_tokens(&mut self) -> TokenCount { self.get_tokens_with_time(T::now()) } pub fn check(&self, tokens: u32) -> bool { let now = T::now(); let cur_tokens = self.get_tokens_with_time(now); let avail_tokens = (now as i64 - cur_tokens as i64) * (Self::TOKENS_PER_SECOND as i64); /* println!( "cur_tokens={} now={} (now-cur_tokens)={} tokens_requested={} tokens_avail={}", cur_tokens, now, now as i64 - cur_tokens as i64, tokens, avail_tokens ); */ tokens as i64 <= avail_tokens } // Remove some tokens pub fn deplete(&mut self, tokens: u32) { self.0 = self.get_tokens::() + (tokens + Self::TOKENS_PER_SECOND - 1) / Self::TOKENS_PER_SECOND; } // Add some tokens (independent of the passage of time) #[allow(dead_code)] pub fn refill(&mut self, tokens: u32) { self.0 = self.get_tokens::() - (tokens + Self::TOKENS_PER_SECOND - 1) / Self::TOKENS_PER_SECOND; } // Empty a bucket, ie: make the bucket has no available tokens. #[allow(dead_code)] pub fn empty(&mut self) { self.0 = T::now(); } } impl Default for GenericTokenBucket { fn default() -> Self { Self::new() } } #[test] fn test_tokens() { let mut bucket = GenericTokenBucket::new(); bucket.empty::(); bucket.refill::(20); assert_eq!(bucket.check::(10), true); bucket.deplete::(40); assert_eq!(bucket.check::(10), false); } erbium-core-1.0.5/src/dns/cache/mod.rs000064400000000000000000000240301046102023000156020ustar 00000000000000/* Copyright 2023 Perry Lorier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 * * Simple DNS cache. * Caching in Erbium is applied on the "out" side, not on the "in" side as might be more common. */ use super::Error; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use tokio::time::{Duration, Instant}; use crate::dns::dnspkt; use crate::dns::outquery; #[cfg(test)] mod test; lazy_static::lazy_static! { static ref DNS_CACHE: prometheus::IntCounterVec = prometheus::register_int_counter_vec!("dns_cache", "Cache statistics", &["result"]) .unwrap(); static ref DNS_CACHE_SIZE: prometheus::IntGauge = prometheus::register_int_gauge!("dns_cache_size", "Number of entries in the cache") .unwrap(); } #[derive(Eq, PartialEq, Hash)] #[cfg_attr(test, derive(Clone))] struct CacheKey { qname: dnspkt::Domain, qtype: dnspkt::Type, } struct CacheValue { reply: Result, birth: Instant, lifetime: Duration, } impl CacheValue { fn expiry(&self) -> Instant { self.birth + self.lifetime } } type Cache = HashMap; #[derive(Clone)] pub struct CacheHandler { next: outquery::OutQuery, cache: Arc>, } /* std::io::Error is not clonable (for good reason), but we want to clone it. * So instead, we do some mappings to remove the std::io::Error */ fn clone_out_reply(reply: &Result) -> Result { use outquery::Error as OutReplyError; use Error::*; match reply { Ok(out_reply) => Ok(out_reply.clone()), Err(NotAuthoritative) => Err(NotAuthoritative), Err(OutReply(OutReplyError::Timeout)) => Err(OutReply(OutReplyError::Timeout)), Err(OutReply(OutReplyError::FailedToSend(io))) => { Err(OutReply(OutReplyError::FailedToSendMsg(format!("{}", io)))) } Err(OutReply(OutReplyError::FailedToSendMsg(msg))) => { Err(OutReply(OutReplyError::FailedToSendMsg(msg.clone()))) } Err(OutReply(OutReplyError::FailedToRecv(io))) => { Err(OutReply(OutReplyError::FailedToRecvMsg(format!("{}", io)))) } Err(OutReply(OutReplyError::FailedToRecvMsg(msg))) => { Err(OutReply(OutReplyError::FailedToRecvMsg(msg.clone()))) } Err(OutReply(OutReplyError::TcpConnection(msg))) => { Err(OutReply(OutReplyError::TcpConnection(msg.clone()))) } Err(OutReply(OutReplyError::Parse(msg))) => { Err(OutReply(OutReplyError::Parse(msg.clone()))) } Err(OutReply(OutReplyError::Internal(msg))) => { Err(OutReply(OutReplyError::Internal(msg.clone()))) } Err(Denied(x)) => Err(Denied(x.clone())), Err(Blocked) => Err(Blocked), Err(NoRouteConfigured) => Err(NoRouteConfigured), /* These errors cannot occur */ Err(ListenError(..)) => unreachable!(), Err(AcceptError(..)) => unreachable!(), Err(RecvError(_)) => unreachable!(), Err(ParseError(_)) => unreachable!(), Err(RefusedByAcl(_)) => unreachable!(), } } /* std::io::Error is not clonable (for good reason), but we want to clone it. * So instead, we do some mappings to remove the std::io::Error */ fn clone_with_ttl_decrement_out_reply( reply: &Result, decrement: std::time::Duration, ) -> Result { match reply { Ok(out_reply) => Ok(out_reply.clone_with_ttl_decrement(decrement.as_secs() as u32)), err => clone_out_reply(err), } } impl CacheHandler { pub async fn new() -> Self { let cache = Arc::new(RwLock::new(Cache::new())); let cache_copy = cache.clone(); tokio::spawn(async move { Self::expire_thread(cache_copy).await; }); CacheHandler { next: outquery::OutQuery::new(), cache, } } /* Expires entries, returns the time for the next expiration run. */ fn expire(cache: &mut Cache, now: Instant) -> Instant { use std::convert::TryInto as _; /* We don't have any notification from the resolvers if this time needs to go down. * So if we get a spike of resolutions we might have to start doing expiries, so poll * at least every this time. */ let mut next_cycle = now + Duration::from_secs(1800); cache.retain(|_k, v| { if v.expiry() >= now { next_cycle = std::cmp::min(next_cycle, v.expiry()); true } else { false } }); /* Update the new cache size. */ DNS_CACHE_SIZE.set(cache.len().try_into().unwrap_or(i64::MAX)); /* Don't waste cpu cycling too often. If we have a lot of entries expiring at about * the same time, cap this to poll a bit more infrequently, it's more efficient to do * one run that collects multiple entries. * Again, we don't need to be too precise. */ std::cmp::max(next_cycle, Instant::now() + Duration::from_secs(30)) } async fn expire_thread(cache: Arc>) { loop { let next_cycle; /* Expire all the old entries */ { let mut rwcache = cache.write().await; next_cycle = Self::expire(&mut rwcache, Instant::now()); } /* Now wait until then. */ log::trace!( "Next cache expiry in {} seconds", (next_cycle - Instant::now()).as_secs(), ); tokio::time::sleep_until(next_cycle).await; } } fn get_entry( cache: &Cache, ck: &CacheKey, now: Instant, ) -> Option> { /* Check to see if we have a cache hit that is still valid, if so, return it */ if let Some(entry) = cache.get(ck) { if entry.expiry() >= now { let remaining = (entry.birth + entry.lifetime) - now; log::trace!("Cache hit ({:?} remaining)", remaining); DNS_CACHE.with_label_values(&["HIT"]).inc(); Some(clone_with_ttl_decrement_out_reply( &entry.reply, now - entry.birth, )) } else { log::trace!("Cache miss: Cache expired"); DNS_CACHE.with_label_values(&["EXPIRED"]).inc(); None } } else { log::trace!("Cache miss: Entry not present"); DNS_CACHE.with_label_values(&["MISS"]).inc(); None } } fn calculate_expiry(&self, out_result: &Result) -> Duration { match &out_result { /* If we got a packet, then use the expiry from the packet. */ Ok(out_reply) => out_reply.get_expiry(), /* If there was a problem sending the reply, then wait for at least as long * as exponential backoff would allow. */ Err(Error::OutReply(outquery::Error::Timeout)) | Err(Error::OutReply(outquery::Error::FailedToSend(_))) | Err(Error::OutReply(outquery::Error::FailedToRecv(_))) | Err(Error::OutReply(outquery::Error::TcpConnection(_))) | Err(Error::OutReply(outquery::Error::Parse(_))) => std::time::Duration::from_secs(8), /* Otherwise do not cache the error */ _ => std::time::Duration::from_secs(0), } } fn insert_cache_entry( &self, cache: &mut Cache, ck: CacheKey, out_result: &Result, expiry: Duration, ) { use std::convert::TryInto as _; cache.insert( ck, CacheValue { reply: clone_out_reply(out_result), birth: Instant::now(), lifetime: expiry, }, ); DNS_CACHE_SIZE.set(cache.len().try_into().unwrap_or(i64::MAX)); } pub async fn handle_query( &self, msg: &super::DnsMessage, addr: std::net::SocketAddr, ) -> Result { let q = &msg.in_query.question; /* Only do caching for IN queries */ if q.qclass != dnspkt::CLASS_IN { log::trace!("[{:x}] Not caching non-IN query", msg.in_query.qid); DNS_CACHE.with_label_values(&["UNCACHABLE_CLASS"]).inc(); return self.next.handle_query(msg, addr).await; } let ck = CacheKey { qname: q.qdomain.clone(), qtype: q.qtype, }; { let rocache = self.cache.read().await; if let Some(result) = Self::get_entry(&rocache, &ck, Instant::now()) { return result; } } /* Cache miss: Go attempt the resolve, and return the result */ let out_result = self.next.handle_query(msg, addr).await; let expiry = self.calculate_expiry(&out_result); /* Only insert into the cache if the duration is reasonable */ if expiry > Duration::from_secs(0) { let mut rwcache = self.cache.write().await; self.insert_cache_entry(&mut rwcache, ck, &out_result, expiry); } match &out_result { Ok(x) => log::trace!("[{:x}] OutReply: {:?}", msg.in_query.qid, x), Err(e) => log::trace!("[{:x}] OutReply: {}", msg.in_query.qid, e), }; out_result } } erbium-core-1.0.5/src/dns/cache/test.rs000064400000000000000000000073661046102023000160170ustar 00000000000000/* Copyright 2023 Perry Lorier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 * * Tests for caching. */ use super::*; use crate::dns::dnspkt::*; #[tokio::test] async fn test_expiry() { let handler = CacheHandler { next: outquery::OutQuery::new(), cache: Arc::new(RwLock::new(Cache::new())), }; let example_net: dnspkt::Domain = "example.net".parse().unwrap(); let ck = CacheKey { qname: example_net.clone(), qtype: RR_A, }; let mut now = Instant::now(); /* First verify the entry doesn't exist in an empty cache */ { let rocache = handler.cache.read().await; assert!(CacheHandler::get_entry(&rocache, &ck, now).is_none()); } let out_result = Ok(dnspkt::DNSPkt { qid: 1, rd: true, tc: false, aa: false, qr: true, opcode: dnspkt::OPCODE_QUERY, cd: false, ad: false, ra: false, rcode: dnspkt::NOERROR, bufsize: 512, edns_ver: Some(0), edns_do: false, question: dnspkt::Question { qdomain: example_net.clone(), qtype: RR_A, qclass: CLASS_IN, }, answer: vec![dnspkt::RR { domain: example_net.clone(), class: CLASS_IN, rrtype: RR_A, ttl: 600, rdata: dnspkt::RData::Other(vec![192, 0, 2, 1]), }], nameserver: vec![], additional: vec![], edns: None, }); let expiry = handler.calculate_expiry(&out_result); assert_eq!(expiry, Duration::from_secs(600)); /* Insert an entry */ { let mut rwcache = handler.cache.write().await; handler.insert_cache_entry(&mut rwcache, ck.clone(), &out_result, expiry); assert_eq!(rwcache.len(), 1); } /* Now test that it comes back 5s later */ now += Duration::from_secs(5); { let rocache = handler.cache.read().await; assert!(CacheHandler::get_entry(&rocache, &ck, now).is_some()); } /* Now run a GC after 60s and check the entry isn't removed */ now += Duration::from_secs(60); { let mut rwcache = handler.cache.write().await; let next = CacheHandler::expire(&mut rwcache, now); assert_eq!(rwcache.len(), 1); assert!(CacheHandler::get_entry(&rwcache, &ck, now).is_some()); assert!(next < now + Duration::from_secs(1800)); // We have an entry that is newer than that. assert!(next > now + Duration::from_secs(30)); // But not too frequently! } /* Test that it is expired after 15 minutes */ now += Duration::from_secs(900); { let rocache = handler.cache.read().await; assert!(CacheHandler::get_entry(&rocache, &ck, now).is_none()); } /* Test that after an hour, the expiry garbage collection removes the entry */ now += Duration::from_secs(3600); { let mut rwcache = handler.cache.write().await; let next = CacheHandler::expire(&mut rwcache, now); assert!(CacheHandler::get_entry(&rwcache, &ck, now).is_none()); assert_eq!(rwcache.len(), 0); assert!(next >= now + Duration::from_secs(1800)); // There are no entries left, so re-run infrequently. } } erbium-core-1.0.5/src/dns/config.rs000064400000000000000000000102321046102023000152240ustar 00000000000000/* Copyright 2023 Perry Lorier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 * * DNS Configuration parsing. */ use crate::config::*; use yaml_rust::yaml; #[derive(Debug)] pub enum Handler { Forward(Vec), ForgeNxDomain, } enum HandlerType { Forward, ForgeNxDomain, } #[derive(Debug)] pub struct Route { pub suffixes: Vec, pub dest: Handler, } pub fn parse_dns_route(name: &str, fragment: &yaml::Yaml) -> Result, Error> { if let Some(h) = fragment.as_hash() { let mut suffixes = None; let mut servers = None; let mut handler = None; for (k, v) in h { match k.as_str() { Some("domain-suffixes") => { suffixes = parse_array("domain-suffixes", v, parse_string)? } Some("dns-servers") => servers = parse_array("domain-servers", v, parse_string_ip)?, Some("type") => match parse_string("type", v)? { Some(t) if t == "forward" => handler = Some(HandlerType::Forward), Some(t) if t == "forge-nxdomain" => handler = Some(HandlerType::ForgeNxDomain), Some(kw) => { return Err(Error::InvalidConfig(format!( "{} type {} not supported", name, kw, ))) } None => return Err(Error::InvalidConfig(format!("{} cannot be null", name))), }, Some(opt) => { return Err(Error::InvalidConfig(format!( "Unknown {} keyword {}", name, opt ))) } None => { return Err(Error::InvalidConfig(format!( "Expected string in {}, not {:?}", name, k ))) } } } let suffix_domains: Vec = suffixes .unwrap_or_default() .iter() .map(|d| d.parse()) .collect::>() .map_err(|m| Error::InvalidConfig(m.into()))?; let servers = servers.unwrap_or_default(); if servers.len() > 1 { return Err(Error::InvalidConfig( "Multiple DNS servers for a prefix not yet implemented.".into(), // TODO )); } match handler { Some(HandlerType::Forward) | None => { return Ok(Some(Route { suffixes: suffix_domains, dest: Handler::Forward( servers .iter() .map(|ip| std::net::SocketAddr::new(*ip, 53)) .collect(), ), })); } Some(HandlerType::ForgeNxDomain) => { return Ok(Some(Route { suffixes: suffix_domains, dest: Handler::ForgeNxDomain, })) } } } Ok(None) } pub fn parse_dns_routes(name: &str, fragment: &yaml::Yaml) -> Result>, Error> { parse_array(name, fragment, parse_dns_route) } #[test] fn test_dns_config() -> Result<(), Error> { use crate::config; config::load_config_from_string_for_test( "--- dns-routes: - domain-suffixes: ['invalid'] type: forge-nxdomain - domain-suffixes: [''] type: forward dns-servers: [2001:4860:4860::8888] ", )?; Ok(()) } erbium-core-1.0.5/src/dns/dnspkt.rs000064400000000000000000001201611046102023000152650ustar 00000000000000/* Copyright 2023 Perry Lorier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 * * Datastructures and serialisation of DNS packets. */ #[cfg(fuzzing)] use arbitrary::Arbitrary; use std::fmt; #[derive(Eq, Ord, PartialOrd, PartialEq, Clone, Copy)] #[cfg_attr(fuzzing, derive(Arbitrary))] pub struct Class(pub u16); pub const CLASS_IN: Class = Class(1); /* Internet */ pub const CLASS_CH: Class = Class(3); /* ChaosNet */ impl fmt::Display for Class { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { &CLASS_IN => write!(f, "IN"), &CLASS_CH => write!(f, "CH"), Class(x) => write!(f, "Class#{}", x), } } } impl fmt::Debug for Class { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Class({})", self) } } #[derive(Ord, PartialOrd, PartialEq, Eq, Clone, Hash, Copy)] #[cfg_attr(fuzzing, derive(Arbitrary))] pub struct Type(pub u16); pub const RR_A: Type = Type(1); pub const RR_NS: Type = Type(2); pub const RR_CNAME: Type = Type(5); pub const RR_SOA: Type = Type(6); pub const RR_PTR: Type = Type(12); pub const RR_MX: Type = Type(15); pub const RR_RP: Type = Type(17); pub const RR_AFSDB: Type = Type(18); pub const RR_RT: Type = Type(21); pub const RR_NAPTR: Type = Type(35); pub const RR_OPT: Type = Type(41); pub const RR_NSEC: Type = Type(47); pub const RR_NSEC3: Type = Type(50); pub const RR_ANY: Type = Type(255); impl fmt::Display for Type { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { &RR_A => write!(f, "A"), &RR_NS => write!(f, "NS"), &RR_CNAME => write!(f, "CNAME"), &RR_SOA => write!(f, "SOA"), &RR_PTR => write!(f, "PTR"), &RR_NAPTR => write!(f, "NAPTR"), &RR_OPT => write!(f, "OPT"), &RR_NSEC => write!(f, "NSEC"), &RR_NSEC3 => write!(f, "NSEC3"), Type(x) => write!(f, "Type#{}", x), } } } impl fmt::Debug for Type { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Type({})", self) } } #[derive(Ord, PartialOrd, PartialEq, Eq, Clone, Copy)] #[cfg_attr(fuzzing, derive(Arbitrary))] pub struct RCode(pub u16); pub const NOERROR: RCode = RCode(0); pub const FORMERR: RCode = RCode(1); pub const SERVFAIL: RCode = RCode(2); pub const NXDOMAIN: RCode = RCode(3); pub const NOTIMP: RCode = RCode(4); pub const REFUSED: RCode = RCode(5); pub const YXDOMAIN: RCode = RCode(6); pub const YXRRSET: RCode = RCode(7); pub const NXRRSET: RCode = RCode(8); pub const NOTAUTH: RCode = RCode(9); pub const NOTZONE: RCode = RCode(10); pub const DSOTYPENI: RCode = RCode(11); pub const BADVERS: RCode = RCode(16); pub const BADSIG: RCode = RCode(16); /* Yes, this is a dupe */ pub const BADKEY: RCode = RCode(17); pub const BADTIME: RCode = RCode(18); pub const BADMODE: RCode = RCode(19); pub const BADNAME: RCode = RCode(20); pub const BADALG: RCode = RCode(21); pub const BADTRUNC: RCode = RCode(22); pub const BADCOOKIE: RCode = RCode(23); impl fmt::Display for RCode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { &NOERROR => write!(f, "NOERROR"), &FORMERR => write!(f, "FORMERR"), &SERVFAIL => write!(f, "SERVFAIL"), &NXDOMAIN => write!(f, "NXDOMAIN"), &NOTIMP => write!(f, "NOTIMP"), &REFUSED => write!(f, "REFUSED"), &YXDOMAIN => write!(f, "YXDOMAIN"), &YXRRSET => write!(f, "YXRRSET"), &NXRRSET => write!(f, "NXRRSET"), &NOTAUTH => write!(f, "NOTAUTH"), &NOTZONE => write!(f, "NOTZONE"), &DSOTYPENI => write!(f, "DSOTYPENI"), &BADVERS => write!(f, "BADVERS/BADSIG"), &BADKEY => write!(f, "BADKEY"), &BADTIME => write!(f, "BADTIME"), &BADMODE => write!(f, "BADMODE"), &BADNAME => write!(f, "BADNAME"), &BADALG => write!(f, "BADALG"), &BADTRUNC => write!(f, "BADTRUNC"), &BADCOOKIE => write!(f, "BADCOOKIE"), RCode(x) => write!(f, "RCode#{}", x), } } } impl fmt::Debug for RCode { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "RCode({})", self) } } fn display_byte(b: u8) -> String { match b { n @ 32..=127 => char::from(n).to_string(), n => format!("\\{}", n), } } #[derive(Ord, Clone, PartialEq, Eq, PartialOrd, Hash, Debug)] pub struct Label(Vec); impl From> for Label { fn from(mut v: Vec) -> Self { assert!(!v.is_empty()); v.shrink_to_fit(); Label(v) } } impl fmt::Display for Label { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}", self.0.iter().map(|&b| display_byte(b)).collect::() ) } } #[cfg(fuzzing)] impl<'a> Arbitrary<'a> for Label { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { /* Labels cannot be empty. */ loop { let v = Vec::::arbitrary(u)?; if v.len() > 0 && v.len() < 64 { return Ok(Self(v)); } } } } #[derive(Clone, PartialEq, Eq, PartialOrd, Hash)] #[cfg_attr(fuzzing, derive(Arbitrary))] pub struct Domain(Vec