mozim-0.2.7/.cargo/config.toml000064400000000000000000000001241046102023000142750ustar 00000000000000[target.x86_64-unknown-linux-gnu] runner = 'sudo -E' [env] RUST_TEST_THREADS = "1" mozim-0.2.7/.cargo_vcs_info.json0000644000000001360000000000100121750ustar { "git": { "sha1": "a9a7053604bf26e506550f3e9b7e7e851bc1efe7" }, "path_in_vcs": "" }mozim-0.2.7/.github/workflows/build.yml000064400000000000000000000013321046102023000162030ustar 00000000000000name: Build on: pull_request: types: [opened, synchronize, reopened] push: branches: - main jobs: build: name: Build runs-on: ubuntu-latest strategy: fail-fast: true matrix: include: - rust_target: "i686-unknown-linux-gnu" - rust_target: "x86_64-unknown-linux-gnu" - rust_target: "loongarch64-unknown-linux-gnu" steps: - uses: actions/checkout@v3 - name: Install Rust Stable run: | rustup override set stable rustup update stable rustup target add ${{ matrix.rust_target }} - name: Build test for ${{ matrix.rust_target }} run: cargo build --target ${{ matrix.rust_target }} mozim-0.2.7/.github/workflows/main.yaml000064400000000000000000000022251046102023000161730ustar 00000000000000name: CI on: pull_request: types: [opened, synchronize, reopened] jobs: rust_lint: strategy: fail-fast: true matrix: include: - rust_version: "stable" - rust_version: "nightly" runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install Rust ${{ matrix.rust_version }} run: | rustup override set ${{ matrix.rust_version }} rustup update ${{ matrix.rust_version }} rustup component add rustfmt clippy - name: Check fmt if: matrix.rust_version == 'stable' run: cargo fmt -- --check - name: Check clippy if: matrix.rust_version == 'nightly' run: cargo clippy -- -D warnings rust_integ: runs-on: ubuntu-latest steps: - name: Install packages run: sudo apt-get -y install dnsmasq - uses: actions/checkout@v3 - name: Install Rust stable run: rustup default stable - name: Run test env: # Needed for the `link::test::create_get_delete_w` test to pass. CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER: "sudo -E" run: cargo test -- --test-threads=1 --show-output mozim-0.2.7/.gitignore000064400000000000000000000000631046102023000127540ustar 00000000000000/target *.swp Cargo.lock tags vendor/ *.orig *.rej mozim-0.2.7/.rustfmt.toml000064400000000000000000000001421046102023000134410ustar 00000000000000max_width = 80 reorder_imports = true edition = "2021" format_strings = true wrap_comments = true mozim-0.2.7/CHANGELOG000064400000000000000000000032421046102023000122000ustar 00000000000000# Changelog ## [0.2.7] - 2025-08-15 ### Breaking changes - N/A ### New features - DHCPv4: Support classless route. (28e050c) ### Bug fixes - N/A ## [0.2.6] - 2025-05-28 ### Breaking changes - N/A ### New features - Support DHCPv6. (7f1dd30) - Support release DHCP lease in async API. (8f97403) ### Bug fixes - dhcpv4: Remove `todo!()`. (f0d93aa) - dhcpv4: Replace the DiscoveryTimeout timer when discovery done. (e8dd9f7) - async: Quit the thread when client drooped. (d5131ca) ## [0.2.5] - 2024-07-10 ### Breaking changes - N/A ### New features - Support arbitrary client ID via `DhcpV4Config.set_client_id()`. (5f9a606) ### Bug fixes - epoll: do not close twice. (bff0a0c) ## [0.2.4] - 2024-07-10 ### Breaking changes - N/A ### New features - N/A ### Bug fixes - Apply BPF filter right after raw socket been created. (e34bc25) - Ignore invalid DHCP message during discovery and request stage. (fed292c) ## [0.2.3] - 2024-02-19 ### Breaking changes - N/A ### New features - Support LoongArch. (c6d8e32) ### Bug fixes - N/A ## [0.2.2] - 2023-03-24 ### Breaking changes - N/A ### New features - N/A ### Bug fixes - Fix DHCP Server Identifier(54) option. (f267f10) ## [0.2.1] - 2023-02-17 ### Breaking changes - N/A ### New features - N/A ### Bug fixes - Fix build failuer on i686. (7e877da) - Improve ASYNC API performance. (341dfba) ## [0.2.0] - 2023-02-12 ### Breaking changes - Change the `DhcpV4Config::new()` to return `DhcpV4Config` instead of result. (ab07930) ### New features - Add async API. (05c04d9) ### Bug fixes - N/A ## [0.1.0] - 2022-12-01 ### Breaking changes - N/A ### New features - Initial release ### Bug fixes - N/A mozim-0.2.7/Cargo.lock0000644000001140660000000000100101600ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "addr2line" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "anstream" version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys 0.60.2", ] [[package]] name = "anyhow" version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "async-trait" version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", "syn 2.0.105", ] [[package]] name = "backtrace" version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", "windows-targets 0.52.6", ] [[package]] name = "bitflags" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cfg-if" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "data-encoding" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "dhcproto" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6794294f2c4665aae452e950c2803a1e487c5672dc8448f0bfa3f52ff67e270" dependencies = [ "dhcproto-macros", "hex", "ipnet", "rand", "thiserror 1.0.69", "trust-dns-proto", "url", ] [[package]] name = "dhcproto-macros" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7993efb860416547839c115490d4951c6d0f8ec04a3594d9dd99d50ed7ec170" [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn 2.0.105", ] [[package]] name = "enum-as-inner" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" dependencies = [ "heck", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "env_filter" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", "jiff", "log", ] [[package]] name = "etherparse" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "827292ea592108849932ad8e30218f8b1f21c0dfd0696698a18b5d0aed62d990" dependencies = [ "arrayvec", ] [[package]] name = "ethtool" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5479e15c34374ec0240622c15f2152647ba8726bc6e15f33335a83e309f7b1a5" dependencies = [ "anyhow", "byteorder", "futures", "genetlink", "log", "netlink-packet-core", "netlink-packet-generic", "netlink-packet-utils", "netlink-proto", "netlink-sys", "thiserror 1.0.69", "tokio", ] [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn 2.0.105", ] [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "genetlink" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f890076c1faa1298bf747ce3694a8d9e0d2cc4b06fe293f12dd95742bfd079f" dependencies = [ "futures", "log", "netlink-packet-core", "netlink-packet-generic", "netlink-packet-utils", "netlink-proto", "thiserror 1.0.69", ] [[package]] name = "getrandom" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "icu_collections" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locale_core" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_normalizer" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", "zerotrie", "zerovec", ] [[package]] name = "idna" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" dependencies = [ "matches", "unicode-bidi", "unicode-normalization", ] [[package]] name = "idna" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "io-uring" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" dependencies = [ "bitflags", "cfg-if", "libc", ] [[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", "serde", ] [[package]] name = "jiff-static" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", "syn 2.0.105", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "litemap" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "matches" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "memchr" version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "wasi", "windows-sys 0.59.0", ] [[package]] name = "mozim" version = "0.2.7" dependencies = [ "byteorder", "dhcproto", "env_logger", "etherparse", "futures", "ipnet", "libc", "log", "nispor", "nix", "rand", "tokio", ] [[package]] name = "mptcp-pm" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eafa8fc63dce407b75e336f9a22f18cf5510a3a5c3a5d83262688eb5cca42d5" dependencies = [ "anyhow", "byteorder", "futures", "genetlink", "log", "netlink-packet-core", "netlink-packet-generic", "netlink-packet-utils", "netlink-proto", "netlink-sys", "thiserror 1.0.69", "tokio", ] [[package]] name = "netlink-packet-core" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" dependencies = [ "anyhow", "byteorder", "netlink-packet-utils", ] [[package]] name = "netlink-packet-generic" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd7eb8ad331c84c6b8cb7f685b448133e5ad82e1ffd5acafac374af4a5a308b" dependencies = [ "anyhow", "byteorder", "netlink-packet-core", "netlink-packet-utils", ] [[package]] name = "netlink-packet-route" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0e7987b28514adf555dc1f9a5c30dfc3e50750bbaffb1aec41ca7b23dcd8e4" dependencies = [ "anyhow", "bitflags", "byteorder", "libc", "log", "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 1.0.69", ] [[package]] name = "netlink-proto" version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" dependencies = [ "bytes", "futures", "log", "netlink-packet-core", "netlink-sys", "thiserror 2.0.14", ] [[package]] name = "netlink-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" dependencies = [ "bytes", "futures", "libc", "log", "tokio", ] [[package]] name = "nispor" version = "1.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1cbce36b23360c765ebd1419a5e51dda6996dbd1614f4f11247668d4f748421" dependencies = [ "ethtool", "futures", "libc", "log", "mptcp-pm", "rtnetlink", "serde", "serde_json", "tokio", "wl-nl80211", ] [[package]] name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags", "cfg-if", "cfg_aliases", "libc", ] [[package]] name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "portable-atomic" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" dependencies = [ "zerovec", ] [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro2" version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 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 = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rtnetlink" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cb5850b5aa2c9c0ae44f157694bbe85107a2e13d76eb3178d0e3ee96c410f57" dependencies = [ "futures", "log", "netlink-packet-core", "netlink-packet-route", "netlink-packet-utils", "netlink-proto", "netlink-sys", "nix", "thiserror 1.0.69", "tokio", ] [[package]] name = "rustc-demangle" version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "serde" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", "syn 2.0.105", ] [[package]] name = "serde_json" version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "slab" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "synstructure" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", "syn 2.0.105", ] [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" dependencies = [ "thiserror-impl 2.0.14", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn 2.0.105", ] [[package]] name = "thiserror-impl" version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" dependencies = [ "proc-macro2", "quote", "syn 2.0.105", ] [[package]] name = "tinystr" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tinyvec" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "io-uring", "libc", "mio", "pin-project-lite", "slab", "socket2", "tokio-macros", "windows-sys 0.59.0", ] [[package]] name = "tokio-macros" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", "syn 2.0.105", ] [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", "syn 2.0.105", ] [[package]] name = "tracing-core" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", ] [[package]] name = "trust-dns-proto" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26" dependencies = [ "async-trait", "cfg-if", "data-encoding", "enum-as-inner", "futures-channel", "futures-io", "futures-util", "idna 0.2.3", "ipnet", "lazy_static", "rand", "smallvec", "thiserror 1.0.69", "tinyvec", "tracing", "url", ] [[package]] name = "unicode-bidi" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-normalization" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "url" version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna 1.0.3", "percent-encoding", ] [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "windows-link" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets 0.53.3", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-targets" version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", "windows_i686_gnullvm 0.53.0", "windows_i686_msvc 0.53.0", "windows_x86_64_gnu 0.53.0", "windows_x86_64_gnullvm 0.53.0", "windows_x86_64_msvc 0.53.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "wl-nl80211" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cffcf1e1dca38467779e22768bfc7f294f1b7b3bd99727edf13280eb2429789" dependencies = [ "anyhow", "bitflags", "byteorder", "futures", "genetlink", "log", "netlink-packet-core", "netlink-packet-generic", "netlink-packet-utils", "netlink-proto", "netlink-sys", "thiserror 1.0.69", "tokio", ] [[package]] name = "writeable" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "yoke" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", "syn 2.0.105", "synstructure", ] [[package]] name = "zerocopy" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", "syn 2.0.105", ] [[package]] name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn 2.0.105", "synstructure", ] [[package]] name = "zerotrie" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" dependencies = [ "displaydoc", "yoke", "zerofrom", ] [[package]] name = "zerovec" version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", "syn 2.0.105", ] mozim-0.2.7/Cargo.toml0000644000000031360000000000100101760ustar # 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 = "mozim" version = "0.2.7" build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "DHCP Client Library" readme = "README.md" license = "Apache-2.0" repository = "https://github.com/nispor/mozim" [lib] name = "mozim" path = "src/lib.rs" [[example]] name = "mozim_dhcpv4_async" path = "examples/mozim_dhcpv4_async.rs" [[example]] name = "mozim_dhcpv6_sync" path = "examples/mozim_dhcpv6_sync.rs" [dependencies.byteorder] version = "1.4.3" [dependencies.dhcproto] version = "0.12.0" [dependencies.etherparse] version = "0.13.0" [dependencies.futures] version = "0.3" features = ["std"] default-features = false [dependencies.ipnet] version = "2.11.0" [dependencies.libc] version = "0.2.132" [dependencies.log] version = "0.4.17" [dependencies.nispor] version = "1.2.17" [dependencies.nix] version = "0.29.0" features = [ "poll", "time", "event", ] [dependencies.rand] version = "0.8.5" default-features = false [dev-dependencies.env_logger] version = "0.11.0" [dev-dependencies.tokio] version = "1.19" features = [ "macros", "rt", ] mozim-0.2.7/Cargo.toml.orig0000644000000012130000000000100111270ustar [package] name = "mozim" version = "0.2.7" description = "DHCP Client Library" license = "Apache-2.0" repository = "https://github.com/nispor/mozim" edition = "2021" [lib] name = "mozim" path = "src/lib.rs" [dependencies] rand = { version = "0.8.5", default-features = false } libc = "0.2.132" byteorder = "1.4.3" dhcproto = "0.12.0" log = "0.4.17" etherparse = "0.13.0" nix = { version = "0.29.0", features = ["poll", "time", "event"] } nispor = "1.2.17" futures = { version = "0.3", default-features = false, features = ["std"] } ipnet = "2.11.0" [dev-dependencies] tokio = { version = "1.19", features = ["macros", "rt"] } env_logger = "0.11.0" mozim-0.2.7/Cargo.toml.orig000064400000000000000000000012131046102023000136510ustar 00000000000000[package] name = "mozim" version = "0.2.7" description = "DHCP Client Library" license = "Apache-2.0" repository = "https://github.com/nispor/mozim" edition = "2021" [lib] name = "mozim" path = "src/lib.rs" [dependencies] rand = { version = "0.8.5", default-features = false } libc = "0.2.132" byteorder = "1.4.3" dhcproto = "0.12.0" log = "0.4.17" etherparse = "0.13.0" nix = { version = "0.29.0", features = ["poll", "time", "event"] } nispor = "1.2.17" futures = { version = "0.3", default-features = false, features = ["std"] } ipnet = "2.11.0" [dev-dependencies] tokio = { version = "1.19", features = ["macros", "rt"] } env_logger = "0.11.0" mozim-0.2.7/LICENSE000064400000000000000000000261351046102023000120010ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. mozim-0.2.7/README.md000064400000000000000000000021251046102023000122440ustar 00000000000000# Mozim -- DHCP Client Library Still doing code sign, no real work this project can do yet. Check again in 2022. DONE: * raw socket with BPF applied and accepting all mac address. * DHCP discovery and request. * Renew, rebind. * DHCP IP apply via cli tool `mzc`. * Route * Timeout and retry TODO: * Verify XID. * Handle vendor difference: https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/848 * Support multiple DHCP servers with `DHCPNAK` reply. * Support DHCPNAK * Support `DHCPDECLINE`: Client to server indicating network address is already in use. * Support `DHCPINFORM`: Client to server, asking only for local configuration parameters; client already has externally configured network address. * Rate control -- Token bucket (RFC 2698) * Initial sleep before discovery/solicit(need check RFC) # Try out ```bash # Below script will create veth eth1/eth1.ep. # The `eth1.ep` is DHCP server interface running dnsmasq in `mozim` network # namespace. sudo ./utils/test_env_mozim & cargo run --example mozim_dhcpv4_async cargo run --example mozim_dhcpv6_sync ``` mozim-0.2.7/doc/dhcp_discovery_with_lease.pcapng000064400000000000000000000034701046102023000201410ustar 00000000000000 ¬M<+ÿÿÿÿÿÿÿÿ6Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz (with SSE4.2)Linux 5.10.79-1-lts4Dumpcap (Wireshark) 3.4.10 (Git commit 733b3a137c2b)¬@eth1  Linux 5.10.79-1-lts@x ºywžKVVÿÿÿÿÿÿ6//xEH€9–ÿÿÿÿDC4Óè,Êz6//xc‚Sc52À 7 =6//xÿxx º>פKVV6//xÂ‡ÙÆ7¥EÀH>]@´zÀÀ CD4‡S,ÊzÀ À6//xc‚Sc56À3x:<;iÿÿÿÀÿÀÀÿxx ºN+©KVVÿÿÿÿÿÿ6//xEH€9–ÿÿÿÿDC4Ëï,Êz6//xc‚Sc56À2À 7 =6//xÿxx º}¬LVV6//xÂ‡ÙÆ7¥EÀH>^@´yÀÀ CD4‡S,ÊzÀ À6//xc‚Sc56À3x:<;iÿÿÿÀÿÀÀÿxlkÑ–ðgCounters provided by dumpcapkÑéjNgkÑÏŒðglmozim-0.2.7/doc/dhcp_discovery_without_lease.pcapng000064400000000000000000000034701046102023000206710ustar 00000000000000 ¬M<+ÿÿÿÿÿÿÿÿ6Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz (with SSE4.2)Linux 5.10.79-1-lts4Dumpcap (Wireshark) 3.4.10 (Git commit 733b3a137c2b)¬@eth1  Linux 5.10.79-1-lts@xO ºoRVVÿÿÿÿÿÿ6//xEH€9–ÿÿÿÿDC49ò“Q6//xc‚Sc57 =6//xÿxxO ºÛp’RVV6//xÂ‡ÙÆ7¥EÀHü­@ö)ÀÀ CD4‡Sò“QÀ À6//xc‚Sc56À3x:<;iÿÿÿÀÿÀÀÿxxO ºÁ–RVVÿÿÿÿÿÿ6//xEH€9–ÿÿÿÿDC4íJò“Q6//xc‚Sc56À2À 7 =6//xÿxxO º„ÌSVV6//xÂ‡ÙÆ7¥EÀHü®@ö(ÀÀ CD4‡Sò“QÀ À6//xc‚Sc56À3x:<;iÿÿÿÀÿÀÀÿxlkÑED–™Counters provided by dumpcapkÑÐ&™kÑ¡C–™lmozim-0.2.7/doc/dhcpv6_notes.md000064400000000000000000000145071046102023000144650ustar 00000000000000## RFC 8415 Clients and servers exchange DHCP messages using UDP (see [RFC768] and BCP 145 [RFC8085]). The client uses a link-local address or addresses determined through other mechanisms for transmitting and receiving DHCP messages. `All_DHCP_Relay_Agents_and_Servers`: ff02::1:2 `All_DHCP_Servers`: ff05::1:3 Clients listen for DHCP messages on UDP port 546. Servers and relay agents listen for DHCP messages on UDP port 547. Server Server (not selected) Client (selected) v v v | | | | Begins initialization | | | | start of | _____________/|\_____________ | 4-message |/ Solicit | Solicit \| exchange | | | Determines | Determines configuration | configuration | | | |\ | ____________/| | \________ | /Advertise | | Advertise\ |/ | | \ | | | Collects Advertises | | \ | | | Selects configuration | | | | | _____________/|\_____________ | |/ Request | Request \| | | | | | Commits configuration | | | end of | | _____________/| 4-message | |/ Reply | exchange | | | | Initialization complete | | | | . . . . . . | T1 (renewal) timer expires | | | | 2-message | _____________/|\_____________ | exchange |/ Renew | Renew \| | | | | | Commits extended lease(s) | | | | | _____________/| | |/ Reply | . . . . . . | | | | Graceful shutdown | | | | 2-message | _____________/|\_____________ | exchange |/ Release | Release \| | | | | | Discards lease(s) | | | | | _____________/| | |/ Reply | | | | v v v The IAID uniquely identifies the IA and MUST be chosen to be unique among the IAIDs for that IA type on the client (e.g., an IA_NA with an IAID of 0 and an IA_PD with an IAID of 0 are each considered unique). The IAID is chosen by the client. For any given use of an IA by the client, the IAID for that IA MUST be consistent across restarts of the DHCP client. 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | msg-type | transaction-id | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | . options . . (variable number and length) . | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Figure 2: Client/Server Message Format 16.2. Solicit Message Clients MUST discard any received Solicit messages. Servers MUST discard any Solicit messages that do not include a Client Identifier option or that do include a Server Identifier option. 16.3. Advertise Message Clients MUST discard any received Advertise message that meets any of the following conditions: - the message does not include a Server Identifier option (see Section 21.3). - the message does not include a Client Identifier option (see Section 21.2). - the contents of the Client Identifier option do not match the client's DUID. - the "transaction-id" field value does not match the value the client used in its Solicit message. Servers and relay agents MUST discard any received Advertise messages. 16.4. Request Message Clients MUST discard any received Request messages. Servers MUST discard any received Request message that meets any of the following conditions: - the message does not include a Server Identifier option (see Section 21.3). - the contents of the Server Identifier option do not match the server's DUID. - the message does not include a Client Identifier option (see Section 21.2). 16.6. Renew Message Clients MUST discard any received Renew messages. Servers MUST discard any received Renew message that meets any of the following conditions: - the message does not include a Server Identifier option (see Section 21.3). - the contents of the Server Identifier option do not match the server's identifier. - the message does not include a Client Identifier option (see Section 21.2). 16.7. Rebind Message Clients MUST discard any received Rebind messages. Servers MUST discard any received Rebind messages that do not include a Client Identifier option (see Section 21.2) or that do include a Server Identifier option (see Section 21.3). mozim-0.2.7/examples/mozim_dhcpv4_async.rs000064400000000000000000000017401046102023000167530ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use futures::stream::StreamExt; use mozim::{DhcpV4ClientAsync, DhcpV4Config}; const TEST_NIC: &str = "dhcpcli"; #[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), Box> { enable_log(); let mut config = DhcpV4Config::new(TEST_NIC); config.set_host_name("mozim-test"); config.use_host_name_as_client_id(); config.set_timeout(60); let mut cli = DhcpV4ClientAsync::init(config, None).unwrap(); loop { if let Some(Ok(lease)) = cli.next().await { // You need to code to apply the IP address in lease to this NIC, so // follow up renew can work. println!("Got lease {lease:?}"); cli.release(&lease)?; return Ok(()); } } } fn enable_log() { env_logger::Builder::new() .filter(Some("nispor"), log::LevelFilter::Debug) .filter(Some("mozim"), log::LevelFilter::Debug) .init(); } mozim-0.2.7/examples/mozim_dhcpv6_sync.rs000064400000000000000000000015531046102023000166160ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use mozim::{DhcpV6Client, DhcpV6Config, DhcpV6IaType}; const TEST_NIC: &str = "dhcpcli"; const POLL_WAIT_TIME: u32 = 5; fn main() -> Result<(), Box> { enable_log(); let mut config = DhcpV6Config::new(TEST_NIC, DhcpV6IaType::NonTemporaryAddresses); config.set_timeout(60); let mut cli = DhcpV6Client::init(config, None).unwrap(); loop { for event in cli.poll(POLL_WAIT_TIME)? { if let Some(lease) = cli.process(event)? { println!("Got DHCPv6 lease {:?}", lease); cli.release(&lease)?; return Ok(()); } } } } fn enable_log() { env_logger::Builder::new() .filter(Some("nispor"), log::LevelFilter::Debug) .filter(Some("mozim"), log::LevelFilter::Debug) .init(); } mozim-0.2.7/src/bpf.rs000064400000000000000000000061611046102023000126750ustar 00000000000000use crate::{DhcpError, ErrorKind}; const DHCP_BPF_LEN: u16 = 11; // libc are setting these constant as u32 which make our life worse // as libc::sock_filter code is u16. const BPF_B: u16 = 0x10; const BPF_H: u16 = 0x08; const BPF_ABS: u16 = 0x20; const BPF_IND: u16 = 0x40; const BPF_MSH: u16 = 0xa0; const BPF_JEQ: u16 = 0x10; const BPF_JSET: u16 = 0x40; const BPF_K: u16 = 0x00; const BPF_LD: u16 = 0x00; const BPF_LDX: u16 = 0x01; const BPF_JMP: u16 = 0x05; const BPF_RET: u16 = 0x06; const ETHERTYPE_IP: u32 = 0x0800; const IPPROTO_UDP: u32 = 17; const DHCPV4_DST_PORT: u32 = 68; const ETHER_TYPE_POS: u32 = 12; const IP_PROTO_POS: u32 = 23; const IP_FRAGMENT_POS: u32 = 20; const IP_HEADER_LEN_POS: u32 = 14; const ETHER_HEADER_LEN: u32 = 14; const DST_PORT_IN_IP_POS: u32 = 2; const BPF_FILTER_RAW: [(u16, u8, u8, u32); DHCP_BPF_LEN as usize] = [ // Load protocol type to A (BPF_LD | BPF_H | BPF_ABS, 0, 0, ETHER_TYPE_POS), // Move on if ETHERTYPE_IP, otherwise drop package (BPF_JMP | BPF_JEQ | BPF_K, 0, 8, ETHERTYPE_IP), // Load IPv4 protocol type to A (BPF_LD | BPF_B | BPF_ABS, 0, 0, IP_PROTO_POS), // Move on if UDP, otherwise drop package (BPF_JMP | BPF_JEQ | BPF_K, 0, 6, IPPROTO_UDP), // Load IPv4 flag and fragment offset (BPF_LD | BPF_H | BPF_ABS, 0, 0, IP_FRAGMENT_POS), // Drop package which has MF(more fragment) set is 1 or is fragment (BPF_JMP | BPF_JSET | BPF_K, 4, 0, 0x1fff), // Store IP header length to X (BPF_LDX | BPF_B | BPF_MSH, 0, 0, IP_HEADER_LEN_POS), // Load UDP destination port number to A ( BPF_LD | BPF_H | BPF_IND, 0, 0, ETHER_HEADER_LEN + DST_PORT_IN_IP_POS, ), // Check whether destination port is DHCPV4_DST_PORT (BPF_JMP | BPF_JEQ | BPF_K, 0, 1, DHCPV4_DST_PORT), // Accept this package (BPF_RET, 0, 0, u32::MAX), // Drop this package (BPF_RET, 0, 0, 0x00000000), ]; pub(crate) fn apply_dhcp_bpf(fd: libc::c_int) -> Result<(), DhcpError> { let mut raw_filters = [libc::sock_filter { code: 0, jt: 0, jf: 0, k: 0, }; DHCP_BPF_LEN as usize]; for (i, (code, jt, jf, k)) in BPF_FILTER_RAW.iter().enumerate() { raw_filters[i].code = *code; raw_filters[i].jt = *jt; raw_filters[i].jf = *jf; raw_filters[i].k = *k; log::debug!( "Registering BPF filter {code:#04x}, {jt}, {jf}, {k:#010x}" ); } let bpf_filter = libc::sock_fprog { len: DHCP_BPF_LEN, filter: raw_filters.as_ptr() as *mut _, }; let rc = unsafe { libc::setsockopt( fd, libc::SOL_SOCKET, libc::SO_ATTACH_FILTER, (&bpf_filter as *const _) as *const libc::c_void, std::mem::size_of::() as libc::socklen_t, ) }; if rc != 0 { let e = DhcpError::new( ErrorKind::Bug, format!( "Failed to apply socket BPF filter, error: {:?}", nix::errno::Errno::last() ), ); log::error!("{e}"); Err(e) } else { Ok(()) } } mozim-0.2.7/src/client_async.rs000064400000000000000000000172651046102023000146100ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::os::fd::BorrowedFd; use std::os::unix::io::{AsRawFd, RawFd}; use std::pin::Pin; use std::sync::{Arc, Mutex}; use futures::{ task::{Context, Poll, Waker}, Stream, }; use nix::poll::{PollFd, PollFlags}; use crate::{ DhcpError, DhcpV4Client, DhcpV4Config, DhcpV4Lease, DhcpV6Client, DhcpV6Config, DhcpV6Lease, ErrorKind, }; const POLL_TIMEOUT: u16 = 1000; // milliseconds #[derive(Debug)] struct ShareState { waker: Option, } #[derive(Debug)] pub struct DhcpV4ClientAsync { client: DhcpV4Client, share_state: Arc>, } impl DhcpV4ClientAsync { /// Release the lease acquired from DHCPv4 server. pub fn release(&mut self, lease: &DhcpV4Lease) -> Result<(), DhcpError> { self.client.release(lease) } } impl Stream for DhcpV4ClientAsync { type Item = Result; fn poll_next( mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll> { // Poll without wait match self.client.poll(0) { Ok(events) => { for event in events { match self.client.process(event) { Ok(Some(lease)) => { return Poll::Ready(Some(Ok(lease))); } Ok(None) => (), Err(e) => { return Poll::Ready(Some(Err(e))); } } } } Err(e) => { log::error!("DHCP client poll error: {e}"); return Poll::Ready(Some(Err(e))); } } let mut share_state = match self.share_state.lock() { Ok(s) => s, Err(e) => { return Poll::Ready(Some(Err(DhcpError::new( ErrorKind::Bug, format!( "BUG: DhcpV4ClientAsync::poll_next() Failed to \ acquire lock on share_state {e}", ), )))); } }; if share_state.waker.is_none() { share_state.waker = Some(cx.waker().clone()); drop(share_state); let fd = self.client.as_raw_fd(); let share_state = self.share_state.clone(); std::thread::spawn(move || poll_thread(fd, share_state)); } else { share_state.waker = Some(cx.waker().clone()); drop(share_state); } Poll::Pending } } impl DhcpV4ClientAsync { pub fn init( config: DhcpV4Config, lease: Option, ) -> Result { Ok(Self { client: DhcpV4Client::init(config, lease)?, share_state: Arc::new(Mutex::new(ShareState { waker: None })), }) } } impl std::ops::Drop for DhcpV4ClientAsync { fn drop(&mut self) { if let Ok(mut s) = self.share_state.lock() { // Signal `poll_thread()` to quit s.waker = None; } } } // This function will be invoked in a thread to notify the async executor // via `Waker::wake()`. Will quit when `poll()` failed (except EAGAIN). fn poll_thread(fd: RawFd, share_state: Arc>) { let fd = unsafe { BorrowedFd::borrow_raw(fd) }; let mut poll_fds = [PollFd::new( fd, PollFlags::POLLIN | PollFlags::POLLOUT | PollFlags::POLLHUP | PollFlags::POLLERR, )]; loop { if share_state.lock().map(|s| s.waker.is_none()).ok() == Some(true) { std::thread::sleep(std::time::Duration::from_millis( POLL_TIMEOUT as u64, )); // Waker is None means DHCP client quit. return; } else { match nix::poll::poll(&mut poll_fds, POLL_TIMEOUT) { // Timeout, let's check whether waker is None(DHCP client quit); Ok(0) => { continue; } Ok(_) => match share_state.lock() { Ok(mut s) => { if let Some(waker) = s.waker.take() { log::debug!("poll_thread got event"); waker.wake(); } else { log::debug!( "poll_thread got event but Waker is None" ); } } Err(e) => { log::error!( "BUG: poll_thread() Failed to acquire lock: {e}" ); return; } }, Err(e) => { if e == nix::errno::Errno::EAGAIN { continue; } else { log::error!( "BUG: poll_thread() got error from poll(): {e}" ); return; } } } } } } #[derive(Debug)] pub struct DhcpV6ClientAsync { client: DhcpV6Client, share_state: Arc>, } impl Stream for DhcpV6ClientAsync { type Item = Result; fn poll_next( mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll> { // Poll without wait match self.client.poll(0) { Ok(events) => { for event in events { match self.client.process(event) { Ok(Some(lease)) => { return Poll::Ready(Some(Ok(lease))); } Ok(None) => (), Err(e) => { return Poll::Ready(Some(Err(e))); } } } } Err(e) => { log::error!("DHCP client poll error: {e}"); return Poll::Ready(Some(Err(e))); } } let mut share_state = match self.share_state.lock() { Ok(s) => s, Err(e) => { return Poll::Ready(Some(Err(DhcpError::new( ErrorKind::Bug, format!( "BUG: DhcpV6ClientAsync::poll_next() Failed to \ acquire lock on share_state {e}", ), )))); } }; if share_state.waker.is_none() { share_state.waker = Some(cx.waker().clone()); drop(share_state); let fd = self.client.as_raw_fd(); let share_state = self.share_state.clone(); std::thread::spawn(move || poll_thread(fd, share_state)); } else { share_state.waker = Some(cx.waker().clone()); drop(share_state); } Poll::Pending } } impl DhcpV6ClientAsync { pub fn init( config: DhcpV6Config, lease: Option, ) -> Result { Ok(Self { client: DhcpV6Client::init(config, lease)?, share_state: Arc::new(Mutex::new(ShareState { waker: None })), }) } } impl std::ops::Drop for DhcpV6ClientAsync { fn drop(&mut self) { if let Ok(mut s) = self.share_state.lock() { // Signal `poll_thread()` to quit s.waker = None; } } } impl DhcpV6ClientAsync { /// Release the lease acquired from DHCPv6 server. pub fn release(&mut self, lease: &DhcpV6Lease) -> Result<(), DhcpError> { self.client.release(lease) } } mozim-0.2.7/src/dhcpv4/client.rs000064400000000000000000000505611046102023000145770ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::os::unix::io::{AsRawFd, RawFd}; use std::time::Duration; use rand::Rng; use super::{ event::DhcpV4Event, time::{gen_dhcp_request_delay, gen_renew_rebind_times}, }; use crate::{ event::DhcpEventPool, socket::{DhcpRawSocket, DhcpSocket, DhcpUdpSocket}, DhcpError, DhcpV4Config, DhcpV4Lease, DhcpV4Message, DhcpV4MessageType, ErrorKind, }; // RFC 2131 suggests four times(60 seconds) retry before fallback to // discovery phase const MAX_REQUEST_RETRY_COUNT: u32 = 4; const NOT_RETRY: bool = false; const IS_RETRY: bool = true; #[derive(Debug, PartialEq, Clone, Copy)] enum DhcpV4Phase { Done, Discovery, Request, Renew, Rebind, } impl Default for DhcpV4Phase { fn default() -> Self { Self::Discovery } } impl std::fmt::Display for DhcpV4Phase { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { Self::Done => "done", Self::Discovery => "discovery", Self::Request => "request", Self::Renew => "renew", Self::Rebind => "rebind", } ) } } #[derive(Debug)] pub struct DhcpV4Client { config: DhcpV4Config, event_pool: DhcpEventPool, lease: Option, phase: DhcpV4Phase, raw_socket: Option, retry_count: u32, udp_socket: Option, xid: u32, } impl AsRawFd for DhcpV4Client { fn as_raw_fd(&self) -> RawFd { self.event_pool.epoll.as_raw_fd() } } impl DhcpV4Client { pub fn init( mut config: DhcpV4Config, lease: Option, ) -> Result { config.init()?; let mut event_pool = DhcpEventPool::new()?; event_pool.add_timer( Duration::from_secs(config.timeout.into()), DhcpV4Event::Timeout, )?; let raw_socket = DhcpRawSocket::new(&config)?; event_pool .add_socket(raw_socket.as_raw_fd(), DhcpV4Event::RawPackageIn)?; let xid: u32 = rand::thread_rng().gen(); let (dhcp_msg, phase) = if let Some(lease) = &lease { event_pool.add_timer( Duration::from_secs(gen_dhcp_request_delay(0).into()), DhcpV4Event::RequestTimeout, )?; let mut dhcp_msg = DhcpV4Message::new(&config, DhcpV4MessageType::Request, xid); dhcp_msg.load_lease(lease.clone()); (dhcp_msg, DhcpV4Phase::Request) } else { event_pool.add_timer( Duration::from_secs(gen_dhcp_request_delay(0).into()), DhcpV4Event::DiscoveryTimeout, )?; ( DhcpV4Message::new(&config, DhcpV4MessageType::Discovery, xid), DhcpV4Phase::Discovery, ) }; raw_socket.send(&dhcp_msg.to_eth_pkg_broadcast()?)?; Ok(Self { config, event_pool, lease, phase, xid, raw_socket: Some(raw_socket), retry_count: 0, udp_socket: None, }) } fn clean_up(&mut self) { self.lease = None; self.retry_count = 0; self.phase = DhcpV4Phase::Done; self.event_pool.remove_all_event(); self.raw_socket = None; self.udp_socket = None; } pub fn poll(&self, wait_time: u32) -> Result, DhcpError> { self.event_pool.poll(wait_time) } fn gen_discovery_pkg(&self) -> DhcpV4Message { DhcpV4Message::new(&self.config, DhcpV4MessageType::Discovery, self.xid) } fn gen_request_pkg(&self, lease: &DhcpV4Lease) -> DhcpV4Message { let mut dhcp_msg = DhcpV4Message::new( &self.config, DhcpV4MessageType::Request, self.xid, ); dhcp_msg.load_lease(lease.clone()); dhcp_msg } fn process_discovery(&mut self) -> Result, DhcpError> { let socket = if let Some(s) = self.raw_socket.as_ref() { s } else { self.clean_up(); let e = DhcpError::new( ErrorKind::Bug, "process_discovery(): No Raw socket".to_string(), ); log::error!("{e}"); return Err(e); }; let lease = match recv_dhcp_msg(socket, DhcpV4MessageType::Offer, self.xid) { Ok(Some(l)) => l, Ok(None) => return Ok(None), Err(e) => { log::info!("Ignoring invalid DHCP package: {e}"); return Ok(None); } }; socket.send(&self.gen_request_pkg(&lease).to_eth_pkg_broadcast()?)?; self.lease = Some(lease); self.event_pool.add_timer( Duration::from_secs(gen_dhcp_request_delay(0).into()), DhcpV4Event::RequestTimeout, )?; self.retry_count = 0; self.event_pool.del_timer(DhcpV4Event::DiscoveryTimeout)?; self.phase = DhcpV4Phase::Request; Ok(None) } fn set_renew_rebind_timer( &mut self, lease: &DhcpV4Lease, ) -> Result<(), DhcpError> { let t = gen_renew_rebind_times(lease.t1, lease.t2, lease.lease_time); self.event_pool .add_timer(Duration::from_secs(t[0].into()), DhcpV4Event::Renew)?; self.event_pool.add_timer( Duration::from_secs(t[1].into()), DhcpV4Event::RenewRetry, )?; self.event_pool .add_timer(Duration::from_secs(t[2].into()), DhcpV4Event::Rebind)?; self.event_pool.add_timer( Duration::from_secs(t[3].into()), DhcpV4Event::RebindRetry, )?; self.event_pool.add_timer( Duration::from_secs(lease.lease_time.into()), DhcpV4Event::LeaseExpired, )?; Ok(()) } fn process_request(&mut self) -> Result, DhcpError> { let socket = if let Some(s) = self.raw_socket.as_ref() { s } else { self.clean_up(); let e = DhcpError::new( ErrorKind::Bug, "process_request(): No Raw socket".to_string(), ); log::error!("{e}"); return Err(e); }; let lease = match recv_dhcp_msg(socket, DhcpV4MessageType::Ack, self.xid) { Ok(Some(l)) => l, Ok(None) => return Ok(None), Err(e) => { log::info!("Ignoring invalid DHCP package: {e}"); return Ok(None); } }; self.clean_up(); self.lease = Some(lease.clone()); self.event_pool.del_timer(DhcpV4Event::RequestTimeout)?; self.set_renew_rebind_timer(&lease)?; Ok(Some(lease)) } // RFC 2131 suggests four times(60 seconds) retry before fallback to // discovery phase fn process_request_timeout( &mut self, ) -> Result, DhcpError> { self.event_pool.del_timer(DhcpV4Event::RequestTimeout)?; if self.retry_count >= MAX_REQUEST_RETRY_COUNT { self.retry_count = 0; self.phase = DhcpV4Phase::Discovery; self.event_pool.add_timer( Duration::from_secs( gen_dhcp_request_delay(self.retry_count).into(), ), DhcpV4Event::DiscoveryTimeout, )?; if let Some(raw_socket) = &self.raw_socket { raw_socket .send(&self.gen_discovery_pkg().to_eth_pkg_broadcast()?)?; Ok(None) } else { self.clean_up(); let e = DhcpError::new(ErrorKind::Bug, "No RAW socket".to_string()); log::error!("{e}"); Err(e) } } else { self.retry_count += 1; self.event_pool.add_timer( Duration::from_secs( gen_dhcp_request_delay(self.retry_count).into(), ), DhcpV4Event::RequestTimeout, )?; if let Some(raw_socket) = &self.raw_socket { if let Some(lease) = &self.lease { raw_socket.send( &self.gen_request_pkg(lease).to_eth_pkg_broadcast()?, )?; Ok(None) } else { self.clean_up(); let e = DhcpError::new( ErrorKind::Bug, "No lease in request timeout process".to_string(), ); log::error!("{e}"); Err(e) } } else { let e = DhcpError::new(ErrorKind::Bug, "No RAW socket".to_string()); log::error!("{e}"); Err(e) } } } fn process_discovery_timeout( &mut self, ) -> Result, DhcpError> { self.event_pool.del_timer(DhcpV4Event::RequestTimeout)?; self.retry_count += 1; self.event_pool.add_timer( Duration::from_secs( gen_dhcp_request_delay(self.retry_count).into(), ), DhcpV4Event::DiscoveryTimeout, )?; if let Some(raw_socket) = &self.raw_socket { raw_socket .send(&self.gen_discovery_pkg().to_eth_pkg_broadcast()?)?; Ok(None) } else { self.clean_up(); let e = DhcpError::new(ErrorKind::Bug, "No RAW socket".to_string()); log::error!("{e}"); Err(e) } } fn process_timeout(&mut self) -> Result, DhcpError> { self.clean_up(); let e = DhcpError::new(ErrorKind::Timeout, "Timeout".to_string()); log::error!("{e}"); Err(e) } // Unicast to DHCP server requesting lease extension, with ciaddr field // and empty server identifier. fn process_renew( &mut self, is_retry: bool, ) -> Result, DhcpError> { if is_retry { self.event_pool.del_timer(DhcpV4Event::RenewRetry)?; } else { self.event_pool.del_timer(DhcpV4Event::Renew)?; } // The renew require unicast to DHCP server which hard(need // ARP) to do in raw socket for proxy mode. // TODO: For now, we just skip renew stage and let the lease // been refreshed in rebind stage. if self.config.is_proxy { log::debug!("Proxy mode has no renew support yet, ignoring"); return Ok(None); } let lease = if let Some(l) = self.lease.as_ref() { l } else { self.clean_up(); let e = DhcpError::new( ErrorKind::Bug, "process_renew(): No lease".to_string(), ); log::error!("{e}"); return Err(e); }; let udp_socket = DhcpUdpSocket::new( self.config.iface_name.as_str(), &lease.yiaddr, &lease.siaddr, self.config.socket_timeout, )?; let mut dhcp_msg = DhcpV4Message::new( &self.config, DhcpV4MessageType::Request, self.xid, ); dhcp_msg.load_lease(lease.clone()); dhcp_msg.renew_or_rebind(true); udp_socket.send(&dhcp_msg.to_dhcp_pkg()?)?; self.event_pool .add_socket(udp_socket.as_raw_fd(), DhcpV4Event::UdpPackageIn)?; self.udp_socket = Some(udp_socket); self.phase = DhcpV4Phase::Renew; self.retry_count = u32::from(is_retry); Ok(None) } fn process_renew_recv(&mut self) -> Result, DhcpError> { let socket = if let Some(s) = self.udp_socket.as_ref() { s } else { self.clean_up(); let e = DhcpError::new( ErrorKind::Bug, "process_renew_recv(): No UDP socket".to_string(), ); log::error!("{e}"); return Err(e); }; match recv_dhcp_msg(socket, DhcpV4MessageType::Ack, self.xid) { Ok(Some(lease)) => { self.clean_up(); self.lease = Some(lease.clone()); self.set_renew_rebind_timer(&lease)?; Ok(Some(lease)) } Ok(None) => Ok(None), Err(e) => { if self.retry_count == 0 { log::warn!("DHCP renew failed: {e}, will try"); } else { log::warn!("DHCP renew failed twice: {e}, will rebind"); } Ok(None) } } } // Broadcast to all DHCP servers requesting lease extension with ciaddr. fn process_rebind( &mut self, is_retry: bool, ) -> Result, DhcpError> { if is_retry { self.event_pool.del_timer(DhcpV4Event::RebindRetry)?; } else { self.event_pool.del_timer(DhcpV4Event::Rebind)?; } let lease = if let Some(l) = self.lease.as_ref() { l } else { self.clean_up(); let e = DhcpError::new( ErrorKind::Bug, "process_rebind(): no lease".to_string(), ); log::error!("{e}"); return Err(e); }; let raw_socket = DhcpRawSocket::new(&self.config)?; let mut dhcp_msg = DhcpV4Message::new( &self.config, DhcpV4MessageType::Request, self.xid, ); dhcp_msg.load_lease(lease.clone()); dhcp_msg.renew_or_rebind(true); raw_socket.send(&dhcp_msg.to_eth_pkg_broadcast()?)?; self.event_pool .add_socket(raw_socket.as_raw_fd(), DhcpV4Event::RawPackageIn)?; self.raw_socket = Some(raw_socket); self.phase = DhcpV4Phase::Rebind; self.retry_count = u32::from(is_retry); Ok(None) } fn process_rebind_recv( &mut self, ) -> Result, DhcpError> { let socket = if let Some(s) = self.raw_socket.as_ref() { s } else { self.clean_up(); let e = DhcpError::new( ErrorKind::Bug, "process_rebind_recv(): No RAW socket".to_string(), ); log::error!("{e}"); return Err(e); }; match recv_dhcp_msg(socket, DhcpV4MessageType::Ack, self.xid) { Ok(Some(lease)) => { self.clean_up(); self.lease = Some(lease.clone()); self.set_renew_rebind_timer(&lease)?; Ok(Some(lease)) } Ok(None) => Ok(None), Err(e) => { if self.retry_count == 0 { log::warn!("DHCP rebind failed: {e}, will try"); } else { log::warn!( "DHCP rebind failed twice: {e}, will request new lease" ); } Ok(None) } } } // Instead raise error to user, we should try the whole DHCP discovery // again with specific timeout fn process_lease_expired( &mut self, ) -> Result, DhcpError> { self.clean_up(); self.event_pool.add_timer( Duration::from_secs(self.config.timeout.into()), DhcpV4Event::Timeout, )?; let raw_socket = DhcpRawSocket::new(&self.config)?; self.event_pool .add_socket(raw_socket.as_raw_fd(), DhcpV4Event::RawPackageIn)?; self.event_pool.add_timer( Duration::from_secs(gen_dhcp_request_delay(0).into()), DhcpV4Event::DiscoveryTimeout, )?; let dhcp_msg = DhcpV4Message::new( &self.config, DhcpV4MessageType::Discovery, self.xid, ); raw_socket.send(&dhcp_msg.to_eth_pkg_broadcast()?)?; self.raw_socket = Some(raw_socket); self.phase = DhcpV4Phase::Discovery; Ok(None) } pub fn process( &mut self, event: DhcpV4Event, ) -> Result, DhcpError> { log::debug!("Processing event {event:?}"); match event { DhcpV4Event::RawPackageIn => match self.phase { DhcpV4Phase::Discovery => self.process_discovery(), DhcpV4Phase::Request => self.process_request(), DhcpV4Phase::Rebind => self.process_rebind_recv(), _ => { log::error!( "BUG: Got in-coming packet on raw socket with \ unexpected phase {}", self.phase ); Ok(None) } }, DhcpV4Event::UdpPackageIn => match self.phase { DhcpV4Phase::Renew => self.process_renew_recv(), _ => { log::error!( "BUG: Got in-coming packet on UDP socket with \ unexpected phase {}", self.phase ); Ok(None) } }, DhcpV4Event::RequestTimeout => self.process_request_timeout(), DhcpV4Event::DiscoveryTimeout => self.process_discovery_timeout(), DhcpV4Event::Timeout => self.process_timeout(), DhcpV4Event::Renew => self.process_renew(NOT_RETRY), DhcpV4Event::RenewRetry => self.process_renew(IS_RETRY), DhcpV4Event::Rebind => self.process_rebind(NOT_RETRY), DhcpV4Event::RebindRetry => self.process_rebind(IS_RETRY), DhcpV4Event::LeaseExpired => self.process_lease_expired(), } } /// Release the DHCPv4 lease. /// To request new lease once released, please create new instance of /// [DhcpV4Client]. pub fn release(&mut self, lease: &DhcpV4Lease) -> Result<(), DhcpError> { let mut dhcp_msg = DhcpV4Message::new( &self.config, DhcpV4MessageType::Release, self.xid, ); dhcp_msg.load_lease(lease.clone()); if self.config.is_proxy { let raw_socket = DhcpRawSocket::new(&self.config)?; raw_socket.send(&dhcp_msg.to_proxy_eth_pkg_unicast()?)?; } else { // Cannot create UDP socket when interface does not have DHCP IP // assigned, so we fallback to RAW socket match DhcpUdpSocket::new( self.config.iface_name.as_str(), &lease.yiaddr, &lease.siaddr, self.config.socket_timeout, ) { Ok(udp_socket) => { udp_socket.send(&dhcp_msg.to_dhcp_pkg()?)?; } Err(e) => { log::debug!( "Failed to create UDP socket to release lease {e}, \ fallback to RAW socket" ); let raw_socket = DhcpRawSocket::new(&self.config)?; raw_socket.send(&dhcp_msg.to_proxy_eth_pkg_unicast()?)?; } } } self.clean_up(); Ok(()) } } fn recv_dhcp_msg( socket: &impl DhcpSocket, expected: DhcpV4MessageType, xid: u32, ) -> Result, DhcpError> { let buffer: Vec = socket.recv()?; let reply_dhcp_msg = if socket.is_raw() { DhcpV4Message::from_eth_pkg(&buffer)? } else { DhcpV4Message::from_dhcp_pkg(&buffer)? }; if reply_dhcp_msg.xid != xid { log::debug!( "Dropping DHCP message due to xid miss-match. Expecting {}, got {}", xid, reply_dhcp_msg.xid ); return Ok(None); } if reply_dhcp_msg.msg_type != expected { log::debug!( "Dropping DHCP message due to type miss-match. Expecting {}, got {}", expected, reply_dhcp_msg.msg_type ); return Ok(None); } if let Some(lease) = reply_dhcp_msg.lease { Ok(Some(lease)) } else { log::debug!( "No lease found in the reply from DHCP server {reply_dhcp_msg:?}" ); Ok(None) } } mozim-0.2.7/src/dhcpv4/config.rs000064400000000000000000000112231046102023000145560ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use dhcproto::v4::OptionCode; use super::option::V4_OPT_CODE_MS_CLASSLESS_STATIC_ROUTE; use crate::{ mac::mac_str_to_u8_array, nispor::get_nispor_iface, socket::DEFAULT_SOCKET_TIMEOUT, DhcpError, }; // https://www.iana.org/assignments/arp-parameters/arp-parameters.xhtml#arp-parameters-2 const ARP_HW_TYPE_ETHERNET: u8 = 1; const DEFAULT_TIMEOUT: u32 = 120; #[derive(Debug, PartialEq, Eq, Clone)] pub struct DhcpV4Config { pub(crate) iface_name: String, pub(crate) iface_index: u32, pub(crate) src_mac: String, pub(crate) client_id: Vec, pub(crate) host_name: String, // TODO: Support allow list and deny list for DHCP servers. pub(crate) timeout: u32, pub(crate) socket_timeout: u32, pub(crate) is_proxy: bool, pub(crate) request_opts: Vec, } impl Default for DhcpV4Config { fn default() -> Self { Self { iface_name: String::new(), iface_index: 0, src_mac: String::new(), client_id: Vec::new(), host_name: String::new(), timeout: DEFAULT_TIMEOUT, socket_timeout: DEFAULT_SOCKET_TIMEOUT, is_proxy: false, request_opts: vec![ OptionCode::Hostname, OptionCode::SubnetMask, OptionCode::Router, OptionCode::DomainNameServer, OptionCode::DomainName, OptionCode::InterfaceMtu, OptionCode::NtpServers, OptionCode::ClasslessStaticRoute, OptionCode::Unknown(V4_OPT_CODE_MS_CLASSLESS_STATIC_ROUTE), ], } } } impl DhcpV4Config { pub fn new(iface_name: &str) -> Self { Self { iface_name: iface_name.to_string(), ..Default::default() } } // Check whether interface exists and resolve iface_index and MAC pub(crate) fn init(&mut self) -> Result<(), DhcpError> { let np_iface = get_nispor_iface(self.iface_name.as_str(), false)?; self.iface_index = np_iface.index; if !self.is_proxy { self.src_mac = np_iface.mac_address; } Ok(()) } pub fn new_proxy(out_iface_name: &str, proxy_mac: &str) -> Self { Self { iface_name: out_iface_name.to_string(), src_mac: proxy_mac.to_string(), is_proxy: true, ..Default::default() } } // Set timeout in seconds pub fn set_timeout(&mut self, timeout: u32) -> &mut Self { self.timeout = timeout; self } pub fn set_host_name(&mut self, host_name: &str) -> &mut Self { self.host_name = host_name.to_string(); self } pub fn use_mac_as_client_id(&mut self) -> &mut Self { self.client_id = vec![ARP_HW_TYPE_ETHERNET]; self.client_id .append(&mut mac_str_to_u8_array(&self.src_mac)); self } pub fn use_host_name_as_client_id(&mut self) -> &mut Self { if !self.host_name.is_empty() { // RFC 2132: 9.14. Client-identifier // Type 0 is used when not using hardware address // The RFC never mentioned the NULL terminator for string. // TODO: Need to check with dnsmasq implementation let host_name = self.host_name.clone(); self.set_client_id(0, host_name.as_bytes()); } self } pub fn set_client_id( &mut self, client_id_type: u8, client_id: &[u8], ) -> &mut Self { // RFC 2132: 9.14. Client-identifier self.client_id = vec![client_id_type]; self.client_id.extend_from_slice(client_id); self } /// By default, these DHCP options will be requested from DHCP server: /// * Hostname (12) /// * Subnet Mask (1) /// * Router (3) /// * Domain Name Server (6) /// * Domain Name (15) /// * Interface MTU (26) /// * NTP Servers (42) /// * Classless Static Route (121) /// * Microsoft Classless Static Route (249) /// /// This function will append specified DHCP option to above list. pub fn request_extra_dhcp_opts(&mut self, opts: &[u8]) -> &mut Self { for opt in opts { self.request_opts.push((*opt).into()); } self.request_opts.sort_unstable(); self.request_opts.dedup(); self } /// Specify arbitrary DHCP options to request. pub fn override_request_dhcp_opts(&mut self, opts: &[u8]) -> &mut Self { self.request_opts = opts.iter().map(|c| OptionCode::from(*c)).collect(); self.request_opts.sort_unstable(); self.request_opts.dedup(); self } } mozim-0.2.7/src/dhcpv4/event.rs000064400000000000000000000045151046102023000144400ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::convert::TryFrom; use crate::{event::DhcpEvent, DhcpError, ErrorKind}; #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] pub enum DhcpV4Event { RawPackageIn = 1, UdpPackageIn, DiscoveryTimeout, RequestTimeout, Timeout, Renew, RenewRetry, Rebind, RebindRetry, LeaseExpired, } impl From for u64 { fn from(v: DhcpV4Event) -> u64 { v as u64 } } impl TryFrom for DhcpV4Event { type Error = DhcpError; fn try_from(v: u64) -> Result { match v { x if x == Self::RawPackageIn as u64 => Ok(Self::RawPackageIn), x if x == Self::UdpPackageIn as u64 => Ok(Self::UdpPackageIn), x if x == Self::DiscoveryTimeout as u64 => { Ok(Self::DiscoveryTimeout) } x if x == Self::RequestTimeout as u64 => Ok(Self::RequestTimeout), x if x == Self::Timeout as u64 => Ok(Self::Timeout), x if x == Self::Renew as u64 => Ok(Self::Renew), x if x == Self::RenewRetry as u64 => Ok(Self::RenewRetry), x if x == Self::Rebind as u64 => Ok(Self::Rebind), x if x == Self::RebindRetry as u64 => Ok(Self::RebindRetry), x if x == Self::LeaseExpired as u64 => Ok(Self::LeaseExpired), _ => { let e = DhcpError::new( ErrorKind::Bug, format!("Got unexpected event ID {v}"), ); log::error!("{e}"); Err(e) } } } } impl std::fmt::Display for DhcpV4Event { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { Self::RawPackageIn => "RawPackageIn", Self::UdpPackageIn => "UdpPackageIn", Self::DiscoveryTimeout => "DiscoveryTimeout", Self::RequestTimeout => "RequestTimeout", Self::Timeout => "Timeout", Self::Renew => "Renew", Self::RenewRetry => "RenewRetry", Self::Rebind => "Rebind", Self::RebindRetry => "RebindRetry", Self::LeaseExpired => "LeaseExpired", } ) } } impl DhcpEvent for DhcpV4Event {} mozim-0.2.7/src/dhcpv4/lease.rs000064400000000000000000000110401046102023000143770ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::net::Ipv4Addr; use dhcproto::{v4, v4::DhcpOption, Encodable}; use super::option::{DhcpV4Options, V4_OPT_CODE_MS_CLASSLESS_STATIC_ROUTE}; use crate::{DhcpError, DhcpV4ClasslessRoute}; #[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] pub struct DhcpV4Lease { // Required for sending DHCPRELEASE in proxy mode pub(crate) srv_mac: [u8; 6], pub siaddr: Ipv4Addr, pub yiaddr: Ipv4Addr, pub t1: u32, pub t2: u32, pub lease_time: u32, pub srv_id: Ipv4Addr, pub subnet_mask: Ipv4Addr, pub broadcast_addr: Option, pub dns_srvs: Option>, pub gateways: Option>, pub ntp_srvs: Option>, pub mtu: Option, pub host_name: Option, pub domain_name: Option, pub classless_routes: Option>, dhcp_opts: DhcpV4Options, } impl Default for DhcpV4Lease { fn default() -> Self { Self { srv_mac: [u8::MAX; 6], siaddr: Ipv4Addr::new(0, 0, 0, 0), yiaddr: Ipv4Addr::new(0, 0, 0, 0), t1: 0, t2: 0, lease_time: 0, srv_id: Ipv4Addr::new(0, 0, 0, 0), subnet_mask: Ipv4Addr::new(0, 0, 0, 0), broadcast_addr: None, dns_srvs: None, gateways: None, ntp_srvs: None, mtu: None, host_name: None, domain_name: None, classless_routes: None, dhcp_opts: DhcpV4Options::default(), } } } impl std::convert::TryFrom<&v4::Message> for DhcpV4Lease { type Error = DhcpError; fn try_from(v4_dhcp_msg: &v4::Message) -> Result { let mut ret = Self { siaddr: v4_dhcp_msg.siaddr(), yiaddr: v4_dhcp_msg.yiaddr(), dhcp_opts: DhcpV4Options::new(v4_dhcp_msg.opts().iter()), ..Default::default() }; for (code, dhcp_opt) in v4_dhcp_msg.opts().iter() { match dhcp_opt { DhcpOption::MessageType(_) => (), DhcpOption::Renewal(v) => { ret.t1 = *v; } DhcpOption::Rebinding(v) => { ret.t2 = *v; } DhcpOption::InterfaceMtu(v) => { ret.mtu = Some(*v); } DhcpOption::ServerIdentifier(v) => { ret.srv_id = *v; } DhcpOption::AddressLeaseTime(v) => { ret.lease_time = *v; } DhcpOption::SubnetMask(v) => { ret.subnet_mask = *v; } DhcpOption::BroadcastAddr(v) => { ret.broadcast_addr = Some(*v); } DhcpOption::DomainNameServer(v) => { ret.dns_srvs = Some(v.clone()); } DhcpOption::Router(v) => { ret.gateways = Some(v.clone()); } DhcpOption::NtpServers(v) => { ret.ntp_srvs = Some(v.clone()); } DhcpOption::Hostname(v) => { ret.host_name = Some(v.to_string()); } DhcpOption::DomainName(v) => { ret.domain_name = Some(v.to_string()); } DhcpOption::ClasslessStaticRoute(v) => { ret.classless_routes = Some(DhcpV4ClasslessRoute::parse(v)); } DhcpOption::Unknown(v) => { if *code == v4::OptionCode::Unknown( V4_OPT_CODE_MS_CLASSLESS_STATIC_ROUTE, ) && ret.classless_routes.is_none() { if let Some(routes) = v .to_vec() .ok() .and_then(DhcpV4ClasslessRoute::parse_raw) { ret.classless_routes = Some(routes); } } } _ => (), } } // TODO: Validate T1 < T2 < lease_time. Ok(ret) } } impl DhcpV4Lease { /// Return the raw data of specified DHCP option containing /// leading code and length(if available) also. pub fn get_option_raw(&self, code: u8) -> Option<&[u8]> { self.dhcp_opts.get_data_raw(code) } } mozim-0.2.7/src/dhcpv4/mod.rs000064400000000000000000000005451046102023000140750ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 mod client; mod config; mod event; mod lease; mod msg; mod option; mod time; pub use self::client::DhcpV4Client; pub use self::config::DhcpV4Config; pub use self::event::DhcpV4Event; pub use self::lease::DhcpV4Lease; pub use self::msg::{DhcpV4Message, DhcpV4MessageType}; pub use self::option::DhcpV4ClasslessRoute; mozim-0.2.7/src/dhcpv4/msg.rs000064400000000000000000000235331046102023000141060ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::net::Ipv4Addr; use dhcproto::{v4, Decodable, Decoder, Encodable}; use crate::{ mac::{ mac_address_to_eth_mac_bytes, mac_str_to_u8_array, BROADCAST_MAC_ADDRESS, }, DhcpError, DhcpV4Config, DhcpV4Lease, ErrorKind, }; const DEFAULT_TTL: u8 = 128; #[derive(Debug, PartialEq, Eq, Clone)] pub enum DhcpV4MessageType { Discovery, Offer, Request, Ack, Nack, Decline, Release, Inform, Unknown, } impl Default for DhcpV4MessageType { fn default() -> Self { Self::Unknown } } impl std::fmt::Display for DhcpV4MessageType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{:?}", match self { Self::Discovery => "discovery", Self::Offer => "offer", Self::Request => "request", Self::Ack => "ack", Self::Nack => "nack", Self::Decline => "decline", Self::Release => "release", Self::Inform => "inform", Self::Unknown => "unknown", } ) } } #[derive(Debug, PartialEq, Eq, Clone, Default)] pub struct DhcpV4Message { pub msg_type: DhcpV4MessageType, pub lease: Option, pub config: DhcpV4Config, renew_or_rebind: bool, pub(crate) xid: u32, } impl DhcpV4Message { pub fn new( config: &DhcpV4Config, msg_type: DhcpV4MessageType, xid: u32, ) -> Self { Self { msg_type, config: config.clone(), lease: None, renew_or_rebind: false, xid, } } pub fn load_lease(&mut self, lease: DhcpV4Lease) -> &mut Self { self.lease = Some(lease); self } pub(crate) fn renew_or_rebind(&mut self, value: bool) -> &mut Self { self.renew_or_rebind = value; self } pub(crate) fn to_dhcp_pkg(&self) -> Result, DhcpError> { let mut dhcp_msg = v4::Message::default(); dhcp_msg.set_flags(v4::Flags::default()); dhcp_msg.set_xid(self.xid); if !self.config.host_name.is_empty() { dhcp_msg.set_sname_str(self.config.host_name.clone()); } if !self.config.src_mac.is_empty() { dhcp_msg .set_chaddr(&mac_str_to_u8_array(self.config.src_mac.as_str())); } if self.msg_type == DhcpV4MessageType::Discovery { dhcp_msg .opts_mut() .insert(v4::DhcpOption::MessageType(v4::MessageType::Discover)); dhcp_msg .opts_mut() .insert(v4::DhcpOption::ParameterRequestList( self.config.request_opts.clone(), )); } else if self.msg_type == DhcpV4MessageType::Request { dhcp_msg .opts_mut() .insert(v4::DhcpOption::MessageType(v4::MessageType::Request)); if let Some(lease) = self.lease.as_ref() { if self.renew_or_rebind { dhcp_msg.set_ciaddr(lease.yiaddr); } else { if lease.srv_id != Ipv4Addr::new(0, 0, 0, 0) { dhcp_msg.opts_mut().insert( v4::DhcpOption::ServerIdentifier(lease.srv_id), ); } else { dhcp_msg.opts_mut().insert( v4::DhcpOption::ServerIdentifier(lease.siaddr), ); } dhcp_msg.opts_mut().insert( v4::DhcpOption::RequestedIpAddress(lease.yiaddr), ); } } else { let e = DhcpError::new( ErrorKind::InvalidArgument, "No DHCP lease found for DHCP request, please run \ DhcpV4Message::load_lease() first" .to_string(), ); log::error!("{e}"); return Err(e); } dhcp_msg .opts_mut() .insert(v4::DhcpOption::ParameterRequestList( self.config.request_opts.clone(), )); } else if self.msg_type == DhcpV4MessageType::Release { if let Some(lease) = self.lease.as_ref() { dhcp_msg.set_ciaddr(lease.yiaddr); dhcp_msg.opts_mut().insert(v4::DhcpOption::MessageType( v4::MessageType::Release, )); if lease.srv_id != Ipv4Addr::new(0, 0, 0, 0) { dhcp_msg .opts_mut() .insert(v4::DhcpOption::ServerIdentifier(lease.srv_id)); } else { dhcp_msg .opts_mut() .insert(v4::DhcpOption::ServerIdentifier(lease.siaddr)); } } else { return Err(DhcpError::new( ErrorKind::Bug, format!("Got no lease for RELEASE message: {self:?}"), )); } } else { let e = DhcpError::new( ErrorKind::InvalidArgument, format!("Unsupported DHCP message type {:?}", self.msg_type), ); log::error!("{e}"); return Err(e); } dhcp_msg.opts_mut().insert(v4::DhcpOption::ClientIdentifier( self.config.client_id.clone(), )); if !self.config.host_name.is_empty() { dhcp_msg.opts_mut().insert(v4::DhcpOption::Hostname( self.config.host_name.clone(), )); } log::debug!("DHCP message {dhcp_msg:?}"); let mut dhcp_msg_buff = Vec::new(); let mut e = v4::Encoder::new(&mut dhcp_msg_buff); dhcp_msg.encode(&mut e)?; Ok(dhcp_msg_buff) } pub(crate) fn from_dhcp_pkg(payload: &[u8]) -> Result { let v4_dhcp_msg = v4::Message::decode(&mut Decoder::new(payload)) .map_err(|decode_error| { let e = DhcpError::new( ErrorKind::InvalidDhcpServerReply, format!( "Failed to parse DHCP message from payload of pkg \ {payload:?}: {decode_error}" ), ); log::error!("{e}"); e })?; let msg_type = match v4_dhcp_msg.opts().get(v4::OptionCode::MessageType) { Some(v4::DhcpOption::MessageType(v4::MessageType::Offer)) => { DhcpV4MessageType::Offer } Some(v4::DhcpOption::MessageType(v4::MessageType::Ack)) => { DhcpV4MessageType::Ack } Some(t) => { log::debug!("Unknown dhcp message type {t:?}"); DhcpV4MessageType::Unknown } None => { log::debug!("Got no dhcp message type"); DhcpV4MessageType::Unknown } }; let ret = Self { lease: Some(DhcpV4Lease::try_from(&v4_dhcp_msg)?), msg_type, xid: v4_dhcp_msg.xid(), ..Default::default() }; log::debug!("Got reply DHCP message {ret:?}"); Ok(ret) } pub(crate) fn to_eth_pkg_broadcast(&self) -> Result, DhcpError> { let dhcp_msg_buff = self.to_dhcp_pkg()?; gen_eth_pkg( &mac_address_to_eth_mac_bytes(&self.config.src_mac)?, &BROADCAST_MAC_ADDRESS, &Ipv4Addr::new(0, 0, 0, 0), &Ipv4Addr::new(255, 255, 255, 255), dhcproto::v4::CLIENT_PORT, dhcproto::v4::SERVER_PORT, &dhcp_msg_buff, ) } pub(crate) fn to_proxy_eth_pkg_unicast( &self, ) -> Result, DhcpError> { if let Some(lease) = self.lease.as_ref() { let dhcp_msg_buff = self.to_dhcp_pkg()?; gen_eth_pkg( &mac_address_to_eth_mac_bytes(&self.config.src_mac)?, &lease.srv_mac, &lease.yiaddr, &lease.siaddr, dhcproto::v4::CLIENT_PORT, dhcproto::v4::SERVER_PORT, &dhcp_msg_buff, ) } else { Err(DhcpError::new( ErrorKind::Bug, "No lease found for `to_proxy_eth_pkg_unicast()`".to_string(), )) } } pub(crate) fn from_eth_pkg(data: &[u8]) -> Result { let pkg = match etherparse::SlicedPacket::from_ethernet(data) { Err(error) => { let e = DhcpError::new( ErrorKind::InvalidDhcpServerReply, format!( "Failed to parse ethernet package to Dhcpv4Offer: \ {error}" ), ); log::error!("{e}"); return Err(e); } Ok(v) => v, }; let mut ret = Self::from_dhcp_pkg(pkg.payload)?; if let Some(eth_header) = pkg.link.map(|l| l.to_header()) { if let Some(lease) = ret.lease.as_mut() { lease.srv_mac = eth_header.source; } } Ok(ret) } } fn gen_eth_pkg( src_mac: &[u8; 6], dst_mac: &[u8; 6], src_ip: &Ipv4Addr, dst_ip: &Ipv4Addr, src_port: u16, dst_port: u16, payload: &[u8], ) -> Result, DhcpError> { let builder = etherparse::PacketBuilder::ethernet2(*src_mac, *dst_mac) .ipv4(src_ip.octets(), dst_ip.octets(), DEFAULT_TTL) .udp(src_port, dst_port); let mut pkg = Vec::::with_capacity(builder.size(payload.len())); builder.write(&mut pkg, payload)?; Ok(pkg) } mozim-0.2.7/src/dhcpv4/option.rs000064400000000000000000000035631046102023000146310ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use std::net::Ipv4Addr; use dhcproto::{ v4::{DhcpOption, OptionCode}, Decodable, Encodable, }; // Microsoft Classless Static Route Option, data format is identical to // RFC 3442: Classless Static Route Option(121) pub(crate) const V4_OPT_CODE_MS_CLASSLESS_STATIC_ROUTE: u8 = 249; #[derive(Debug, PartialEq, Eq, Clone, Default)] pub(crate) struct DhcpV4Options { data: HashMap>, } impl DhcpV4Options { pub(crate) fn new<'a, T>(opts: T) -> Self where T: Iterator, { let mut data = HashMap::new(); for (code, opt) in opts { if let Ok(raw) = opt.to_vec() { data.insert(u8::from(*code), raw); } } Self { data } } pub(crate) fn get_data_raw(&self, code: u8) -> Option<&[u8]> { self.data.get(&code).map(|v| v.as_slice()) } } #[derive(Debug, PartialEq, Eq, Clone)] pub struct DhcpV4ClasslessRoute { pub destination: Ipv4Addr, pub prefix_length: u8, pub router: Ipv4Addr, } impl DhcpV4ClasslessRoute { pub(crate) fn parse_raw(mut raw: Vec) -> Option> { if !raw.is_empty() { raw[0] = OptionCode::ClasslessStaticRoute.into(); if let Ok(DhcpOption::ClasslessStaticRoute(v)) = DhcpOption::decode(&mut dhcproto::Decoder::new(raw.as_slice())) { return Some(Self::parse(&v)); } } None } pub(crate) fn parse(rts: &[(ipnet::Ipv4Net, Ipv4Addr)]) -> Vec { let mut ret = Vec::new(); for (dst, router) in rts { ret.push(Self { destination: dst.addr(), prefix_length: dst.prefix_len(), router: *router, }); } ret } } mozim-0.2.7/src/dhcpv4/time.rs000064400000000000000000000017761046102023000142630ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::time::Duration; use rand::Rng; // The T1/T2 randomization is done by server side according to RFC 2131: // Times T1 and T2 SHOULD be chosen with some random "fuzz" around a fixed // value, to avoid synchronization of client reacquisition. pub(crate) fn gen_renew_rebind_times(t1: u32, t2: u32, lease: u32) -> [u32; 4] { [t1, t1 + (t2 - t1) / 2, t2, t2 + (lease - t2) / 2] } // RFC 2131, section 4.1 "Constructing and sending DHCP messages" has // retransmission guideline. // It should be starting with 4 seconds and double of previous delay, up to 64 // seconds. Delay should be randomized from range -1 to 1; pub(crate) fn gen_dhcp_request_delay(retry_count: u32) -> u32 { let mut base = 2u64.pow(retry_count + 2) - 1; if base > 62 { base = 62; } let ms: u64 = rand::thread_rng().gen_range(0..2000); (Duration::from_secs(base) + Duration::from_millis(ms)) .as_secs() .try_into() .unwrap_or(u32::MAX) } mozim-0.2.7/src/dhcpv6/client.rs000064400000000000000000000423251046102023000146000ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::net::Ipv6Addr; use std::os::fd::{AsRawFd, RawFd}; use std::time::{Duration, Instant}; use rand::Rng; use super::{ msg::{DhcpV6Message, DhcpV6MessageType}, time::{ gen_rebind_wait_time, gen_renew_wait_time, gen_request_wait_time, gen_solicit_wait_time, }, }; use crate::{ event::DhcpEventPool, socket::{DhcpSocket, DhcpUdpSocket}, DhcpError, DhcpV6Config, DhcpV6Event, DhcpV6IaType, DhcpV6Lease, ErrorKind, }; const DHCPV6_REPLAY_AND_SRVS: Ipv6Addr = Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 1, 2); #[derive(Debug, PartialEq, Clone, Copy)] enum DhcpV6Phase { Done, PreSolicit, Solicit, PreRequest, Request, Renew, Rebind, } impl std::fmt::Display for DhcpV6Phase { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { Self::Done => "done", Self::PreSolicit => "pre_solicit", Self::PreRequest => "pre_request", Self::Solicit => "solicit", Self::Request => "request", Self::Renew => "renew", Self::Rebind => "rebind", } ) } } #[derive(Debug)] #[non_exhaustive] pub struct DhcpV6Client { config: DhcpV6Config, event_pool: DhcpEventPool, lease: Option, phase: DhcpV6Phase, udp_socket: Option, xid: [u8; 3], retrans_timeout: Duration, retrans_count: u32, trans_begin_time: Option, trans_dhcp_msg: Option, } impl AsRawFd for DhcpV6Client { fn as_raw_fd(&self) -> RawFd { self.event_pool.epoll.as_raw_fd() } } impl DhcpV6Client { fn clean_up(&mut self) { self.lease = None; self.retrans_count = 0; self.phase = DhcpV6Phase::Done; self.event_pool.remove_all_event(); self.udp_socket = None; } pub fn init( mut config: DhcpV6Config, lease: Option, ) -> Result { config.init()?; let mut event_pool = DhcpEventPool::new()?; event_pool.add_timer( Duration::from_secs(config.timeout.into()), DhcpV6Event::Timeout, )?; // In RFC 8415, the `transaction-id` is a 3-octet field let mut xid: [u8; 3] = [0; 3]; xid.copy_from_slice( &rand::thread_rng().gen::().to_le_bytes()[..3], ); let mut ret = Self { config, event_pool, lease, phase: DhcpV6Phase::Done, xid, udp_socket: None, retrans_timeout: Duration::new(0, 0), retrans_count: 0, trans_begin_time: None, trans_dhcp_msg: None, }; if ret.lease.is_some() { ret.process_renew()?; } else { ret.process_solicit()?; } Ok(ret) } fn clean_trans_counters(&mut self) { self.trans_dhcp_msg = None; self.retrans_count = 0; self.retrans_timeout = Duration::new(0, 0); self.trans_begin_time = None; } pub fn poll(&self, wait_time: u32) -> Result, DhcpError> { self.event_pool.poll(wait_time) } pub fn process( &mut self, event: DhcpV6Event, ) -> Result, DhcpError> { log::debug!("Processing event {event:?}"); match event { DhcpV6Event::TransmitWait => { self.process_transmit()?; Ok(None) } DhcpV6Event::UdpPackageIn => match self.phase { DhcpV6Phase::Solicit => { self.process_advertise()?; Ok(None) } DhcpV6Phase::Request | DhcpV6Phase::Renew | DhcpV6Phase::Rebind => self.process_reply(), _ => Err(DhcpError::new( ErrorKind::Bug, format!( "Cannot process unsupported phase {} in UdpPackageIn", self.phase ), )), }, DhcpV6Event::Renew => { self.process_renew()?; Ok(None) } DhcpV6Event::LeaseExpired => { self.process_solicit()?; Ok(None) } DhcpV6Event::Rebind => { self.process_rebind()?; Ok(None) } _ => Err(DhcpError::new( ErrorKind::Bug, format!("Cannot process unsupported event {event}"), )), } } /// The RFC 8415: /// Implementations SHOULD retransmit one or more times but MAY choose /// to terminate the retransmission procedure early. /// So here we decided not to wait reply from DHCPv6 server. /// To request new release, you need to create new instance of /// [DhcpV6Client]. pub fn release(&mut self, lease: &DhcpV6Lease) -> Result<(), DhcpError> { if self.udp_socket.is_none() { let socket = DhcpUdpSocket::new_v6( self.config.iface_index, &self.config.src_ip, self.config.socket_timeout, )?; self.udp_socket = Some(socket); } let socket = self.udp_socket.as_ref().unwrap(); let mut dhcp_msg = DhcpV6Message::new( &self.config, DhcpV6MessageType::RELEASE, self.xid, ); dhcp_msg.load_lease(lease.clone())?; let dst = if lease.srv_ip.is_unspecified() { &DHCPV6_REPLAY_AND_SRVS } else { &lease.srv_ip }; socket.send_to_v6(dst, &dhcp_msg.to_dhcp_pkg()?)?; self.clean_up(); Ok(()) } fn process_solicit(&mut self) -> Result<(), DhcpError> { self.phase = DhcpV6Phase::PreSolicit; self.lease = None; self.retrans_timeout = gen_solicit_wait_time(Instant::now(), 0, Duration::new(0, 0))?; self.trans_dhcp_msg = Some(DhcpV6Message::new( &self.config, DhcpV6MessageType::SOLICIT, self.xid, )); self.event_pool .add_timer(self.retrans_timeout, DhcpV6Event::TransmitWait) } fn process_advertise(&mut self) -> Result<(), DhcpError> { self.event_pool.del_timer(DhcpV6Event::Timeout)?; let socket = match self.udp_socket.as_ref() { Some(s) => s, None => { return Err(DhcpError::new( ErrorKind::Bug, format!("Got NULL socket for process_solicit {self:?}"), )); } }; let lease = match recv_dhcp_msg( socket, DhcpV6MessageType::ADVERTISE, self.xid, )? { Some(l) => l, None => return Ok(()), }; let mut dhcp_msg = DhcpV6Message::new( &self.config, DhcpV6MessageType::REQUEST, self.xid, ); if let Err(e) = dhcp_msg.load_lease(lease.clone()) { log::warn!("Invalid DHCPv6 lease: {e}, will retry later"); return Ok(()); } self.event_pool.del_timer(DhcpV6Event::TransmitWait)?; self.clean_trans_counters(); self.retrans_timeout = gen_request_wait_time(Instant::now(), 0, Duration::new(0, 0))?; self.trans_dhcp_msg = Some(dhcp_msg); self.event_pool .add_timer(self.retrans_timeout, DhcpV6Event::TransmitWait)?; self.phase = DhcpV6Phase::PreRequest; Ok(()) } // TODO: Handle sever reply with valid_life with 0(indicate requested // IA is invalid) fn process_reply(&mut self) -> Result, DhcpError> { let socket = match self.udp_socket.as_ref() { Some(s) => s, None => { return Err(DhcpError::new( ErrorKind::Bug, format!("Got NULL socket for process_solicit {self:?}"), )); } }; let lease = match recv_dhcp_msg(socket, DhcpV6MessageType::REPLY, self.xid)? { Some(l) => l, None => return Ok(None), }; self.phase = DhcpV6Phase::Done; self.event_pool.del_socket(DhcpV6Event::UdpPackageIn)?; self.udp_socket = None; self.event_pool.del_timer(DhcpV6Event::TransmitWait)?; self.lease = Some(lease.clone()); self.clean_trans_counters(); self.schedule_renew_rebind_restart()?; Ok(Some(lease)) } // TODO: rate control fn process_transmit(&mut self) -> Result<(), DhcpError> { self.event_pool.del_timer(DhcpV6Event::TransmitWait)?; self.schedule_next_retransmit()?; // The RFC 8415 said // A client is not expected to listen for a response during the // entire RT period and may turn off listening capabilities after // waiting at least the shorter of RT and MAX_WAIT_TIME due to // power consumption saving or other reasons. Of course, a client // MUST listen for a Reconfigure if it has negotiated for its use // with the server. // Hence it is OK to create UDP socket when actual transmitting happens. if self.udp_socket.is_none() { let socket = DhcpUdpSocket::new_v6( self.config.iface_index, &self.config.src_ip, self.config.socket_timeout, )?; self.event_pool .add_socket(socket.as_raw_fd(), DhcpV6Event::UdpPackageIn)?; self.udp_socket = Some(socket); } let socket = self.udp_socket.as_ref().unwrap(); let dhcp_msg = match self.trans_dhcp_msg.as_mut() { Some(p) => p, None => { return Err(DhcpError::new( ErrorKind::Bug, format!( "Got NULL DHCP package for process_transmit {self:?}" ), )); } }; if self.retrans_count > 1 { // We are safe to use unwrap as `schedule_next_retransmit()` // already confirmed so. dhcp_msg.add_elapsed_time(self.trans_begin_time.unwrap()); } // TODO Support unicast to server socket.send_to_v6(&DHCPV6_REPLAY_AND_SRVS, &dhcp_msg.to_dhcp_pkg()?)?; match self.phase { DhcpV6Phase::PreSolicit => self.phase = DhcpV6Phase::Solicit, DhcpV6Phase::PreRequest => self.phase = DhcpV6Phase::Request, _ => (), } Ok(()) } fn schedule_next_retransmit(&mut self) -> Result<(), DhcpError> { self.retrans_count += 1; if self.trans_begin_time.is_none() { self.trans_begin_time = Some(Instant::now()); } self.retrans_timeout = match self.phase { DhcpV6Phase::PreSolicit | DhcpV6Phase::Solicit => { gen_solicit_wait_time( self.trans_begin_time.unwrap(), self.retrans_count, self.retrans_timeout, )? } DhcpV6Phase::PreRequest | DhcpV6Phase::Request => { gen_request_wait_time( self.trans_begin_time.unwrap(), self.retrans_count, self.retrans_timeout, )? } DhcpV6Phase::Renew => { if let Some(lease) = self.lease.as_ref() { gen_rebind_wait_time( self.trans_begin_time.unwrap(), self.retrans_count, self.retrans_timeout, Duration::from_secs(lease.t2.into()), )? } else { return Err(DhcpError::new( ErrorKind::Bug, format!( "Got NULL lease for DhcpV6Phase::Rebind in \ schedule_next_retransmit(): {self:?}" ), )); } } DhcpV6Phase::Rebind => { if let Some(lease) = self.lease.as_ref() { gen_rebind_wait_time( self.trans_begin_time.unwrap(), self.retrans_count, self.retrans_timeout, Duration::from_secs(lease.valid_life.into()), )? } else { return Err(DhcpError::new( ErrorKind::Bug, format!( "Got NULL lease for DhcpV6Phase::Rebind in \ schedule_next_retransmit(): {self:?}" ), )); } } _ => { return Err(DhcpError::new( ErrorKind::Bug, format!( "Got invalid phase {:?} for \ `schedule_next_retransmit()`: {:?}", self.phase, self ), )); } }; self.event_pool .add_timer(self.retrans_timeout, DhcpV6Event::TransmitWait) } fn schedule_renew_rebind_restart(&mut self) -> Result<(), DhcpError> { if let Some(lease) = self.lease.as_ref() { self.event_pool.add_timer( Duration::from_secs(lease.valid_life.into()), DhcpV6Event::LeaseExpired, )?; if lease.ia_type != DhcpV6IaType::TemporaryAddresses { self.event_pool.add_timer( Duration::from_secs(lease.t1.into()), DhcpV6Event::Renew, )?; self.event_pool.add_timer( Duration::from_secs(lease.t2.into()), DhcpV6Event::Rebind, )?; } Ok(()) } else { Err(DhcpError::new( ErrorKind::Bug, format!( "Got NULL lease for `schedule_renew_rebind()`: {self:?}" ), )) } } fn process_renew(&mut self) -> Result<(), DhcpError> { self.event_pool.del_timer(DhcpV6Event::Renew)?; self.phase = DhcpV6Phase::Renew; if let Some(lease) = self.lease.as_ref() { self.retrans_timeout = gen_renew_wait_time( Instant::now(), 0, Duration::new(0, 0), Duration::from_secs(lease.t2.into()), )?; let mut dhcp_msg = DhcpV6Message::new( &self.config, DhcpV6MessageType::RENEW, self.xid, ); dhcp_msg.load_lease(lease.clone())?; self.trans_dhcp_msg = Some(dhcp_msg); self.event_pool .add_timer(self.retrans_timeout, DhcpV6Event::TransmitWait) } else { Err(DhcpError::new( ErrorKind::Bug, format!("Got NULL lease for `process_renew()`: {self:?}"), )) } } fn process_rebind(&mut self) -> Result<(), DhcpError> { self.event_pool.del_timer(DhcpV6Event::Rebind)?; self.phase = DhcpV6Phase::Rebind; if let Some(lease) = self.lease.as_ref() { self.retrans_timeout = gen_rebind_wait_time( Instant::now(), 0, Duration::new(0, 0), Duration::from_secs(lease.valid_life.into()), )?; let mut dhcp_msg = DhcpV6Message::new( &self.config, DhcpV6MessageType::REBIND, self.xid, ); dhcp_msg.load_lease(lease.clone())?; self.trans_dhcp_msg = Some(dhcp_msg); self.event_pool .add_timer(self.retrans_timeout, DhcpV6Event::TransmitWait) } else { Err(DhcpError::new( ErrorKind::Bug, format!("Got NULL lease for `process_renew()`: {self:?}"), )) } } } fn recv_dhcp_msg( socket: &DhcpUdpSocket, expected: DhcpV6MessageType, xid: [u8; 3], ) -> Result, DhcpError> { let buffer: Vec = socket.recv()?; let reply_dhcp_msg = DhcpV6Message::from_dhcp_pkg(&buffer)?; if reply_dhcp_msg.xid != xid { log::debug!( "Dropping DHCP message due to xid miss-match. Expecting {:?}, got \ {:?}", xid, reply_dhcp_msg.xid ); return Ok(None); } if reply_dhcp_msg.msg_type != expected { log::debug!( "Dropping DHCP message due to type miss-match. Expecting {}, got {}", expected, reply_dhcp_msg.msg_type ); return Ok(None); } if let Some(lease) = reply_dhcp_msg.lease { Ok(Some(lease)) } else { log::debug!( "No lease found in the reply from DHCP server {reply_dhcp_msg:?}" ); Ok(None) } } mozim-0.2.7/src/dhcpv6/config.rs000064400000000000000000000161041046102023000145630ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::net::Ipv6Addr; use std::time::{Duration, SystemTime}; use rand::RngCore; use crate::{ mac::mac_str_to_u8_array, nispor::{get_ipv6_addr_of_iface, get_nispor_iface}, socket::DEFAULT_SOCKET_TIMEOUT, DhcpError, }; // https://www.iana.org/assignments/arp-parameters/arp-parameters.xhtml const ARP_HW_TYPE_ETHERNET: u16 = 1; const OPTION_IA_NA: u16 = 3; const OPTION_IA_TA: u16 = 4; const OPTION_IA_PD: u16 = 5; // RFC 8415 11.2. DUID Based on Link-Layer Address Plus Time (DUID-LLT) // Indicate the base time is midnight (UTC), January 1, 2000 // This is calculated value by chrono: // chrono::Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap() // - chrono::Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap() const BASE_TIME: Duration = Duration::new(946684800, 0); const DHCPV6_DUID_TYPE_LLT: u16 = 1; const DHCPV6_DUID_TYPE_EN: u16 = 2; const DHCPV6_DUID_TYPE_LL: u16 = 3; const DHCPV6_DUID_TYPE_UUID: u16 = 4; #[derive(Debug, PartialEq, Eq, Clone, Copy)] #[non_exhaustive] pub enum DhcpV6IaType { NonTemporaryAddresses, TemporaryAddresses, PrefixDelegation, } impl Default for DhcpV6IaType { fn default() -> Self { Self::NonTemporaryAddresses } } impl std::fmt::Display for DhcpV6IaType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { Self::NonTemporaryAddresses => "IANA", Self::TemporaryAddresses => "IATA", Self::PrefixDelegation => "IAPD", } ) } } impl From for u16 { fn from(v: DhcpV6IaType) -> Self { match v { DhcpV6IaType::NonTemporaryAddresses => OPTION_IA_NA, DhcpV6IaType::TemporaryAddresses => OPTION_IA_TA, DhcpV6IaType::PrefixDelegation => OPTION_IA_PD, } } } #[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] pub struct DhcpV6Config { pub(crate) iface_name: String, pub(crate) iface_index: u32, pub(crate) duid: Dhcpv6Duid, pub(crate) timeout: u32, pub(crate) ia_type: DhcpV6IaType, pub(crate) src_ip: Ipv6Addr, pub(crate) socket_timeout: u32, } impl Default for DhcpV6Config { fn default() -> Self { Self { iface_name: String::new(), iface_index: 0, duid: Dhcpv6Duid::Other(Vec::new()), timeout: 0, ia_type: DhcpV6IaType::default(), src_ip: Ipv6Addr::UNSPECIFIED, socket_timeout: DEFAULT_SOCKET_TIMEOUT, } } } impl DhcpV6Config { pub fn new(iface_name: &str, ia_type: DhcpV6IaType) -> Self { Self { iface_name: iface_name.to_string(), ia_type, ..Default::default() } } /// Set timeout in seconds pub fn set_timeout(&mut self, timeout: u32) -> &mut Self { self.timeout = timeout; self } /// Set arbitrary DUID pub fn set_duid(&mut self, duid: Dhcpv6Duid) -> &mut Self { self.duid = duid; self } // Check whether interface exists and resolve iface_index and MAC pub(crate) fn init(&mut self) -> Result<(), DhcpError> { let np_iface = get_nispor_iface(self.iface_name.as_str(), true)?; self.iface_index = np_iface.index; self.src_ip = get_ipv6_addr_of_iface(&np_iface)?; self.duid = if np_iface.mac_address.is_empty() { Dhcpv6Duid::default() } else { Dhcpv6Duid::LL(Dhcpv6DuidLl::new( ARP_HW_TYPE_ETHERNET, &mac_str_to_u8_array(np_iface.mac_address.as_str()), )) }; Ok(()) } } #[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] pub enum Dhcpv6Duid { LLT(Dhcpv6DuidLlt), EN(Dhcpv6DuidEn), LL(Dhcpv6DuidLl), UUID(Dhcpv6DuidUuid), Other(Vec), } impl Default for Dhcpv6Duid { fn default() -> Self { let mut rand_data = [0u8; 16]; rand::thread_rng().fill_bytes(&mut rand_data); Self::Other(rand_data.to_vec()) } } impl Dhcpv6Duid { pub fn to_vec(&self) -> Vec { match self { Self::LLT(v) => v.to_vec(), Self::EN(v) => v.to_vec(), Self::LL(v) => v.to_vec(), Self::UUID(v) => v.to_vec(), Self::Other(v) => v.clone(), } } } // Type 1 #[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] pub struct Dhcpv6DuidLlt { pub hardware_type: u16, pub time: u32, pub link_layer_address: Vec, } impl Dhcpv6DuidLlt { pub fn new(hardware_type: u16, link_layer_address: &[u8]) -> Self { let time: u32 = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .ok() .and_then(|s| s.checked_sub(BASE_TIME)) .map(|t| t.as_secs()) .map(|t| t as u32) .unwrap_or_default(); Self { hardware_type, time, link_layer_address: link_layer_address.to_vec(), } } pub fn to_vec(&self) -> Vec { let mut ret: Vec = Vec::new(); ret.extend_from_slice(&DHCPV6_DUID_TYPE_LLT.to_be_bytes()); ret.extend_from_slice(&self.hardware_type.to_be_bytes()); ret.extend_from_slice(&self.time.to_be_bytes()); ret.extend_from_slice(self.link_layer_address.as_slice()); ret } } // Type 2 #[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] pub struct Dhcpv6DuidEn { pub enterprise_number: u32, pub identifier: Vec, } impl Dhcpv6DuidEn { pub fn new(enterprise_number: u32, identifier: &[u8]) -> Self { Self { enterprise_number, identifier: identifier.to_vec(), } } pub fn to_vec(&self) -> Vec { let mut ret: Vec = Vec::new(); ret.extend_from_slice(&DHCPV6_DUID_TYPE_EN.to_be_bytes()); ret.extend_from_slice(&self.enterprise_number.to_be_bytes()); ret.extend_from_slice(self.identifier.as_slice()); ret } } // Type 3 #[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] pub struct Dhcpv6DuidLl { hardware_type: u16, link_layer_address: Vec, } impl Dhcpv6DuidLl { pub fn new(hardware_type: u16, link_layer_address: &[u8]) -> Self { Self { hardware_type, link_layer_address: link_layer_address.to_vec(), } } pub fn to_vec(&self) -> Vec { let mut ret: Vec = Vec::new(); ret.extend_from_slice(&DHCPV6_DUID_TYPE_LL.to_be_bytes()); ret.extend_from_slice(&self.hardware_type.to_be_bytes()); ret.extend_from_slice(self.link_layer_address.as_slice()); ret } } // Type 4 #[derive(Debug, PartialEq, Eq, Clone, Copy)] #[non_exhaustive] pub struct Dhcpv6DuidUuid { uuid: u128, } impl Dhcpv6DuidUuid { pub fn new(uuid: u128) -> Self { Self { uuid } } pub fn to_vec(&self) -> Vec { let mut ret: Vec = Vec::new(); ret.extend_from_slice(&DHCPV6_DUID_TYPE_UUID.to_be_bytes()); ret.extend_from_slice(&self.uuid.to_be_bytes()); ret } } mozim-0.2.7/src/dhcpv6/event.rs000064400000000000000000000033451046102023000144420ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::convert::TryFrom; use crate::{event::DhcpEvent, DhcpError, ErrorKind}; #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] #[non_exhaustive] pub enum DhcpV6Event { UdpPackageIn = 1, TransmitWait, Timeout, Renew, Rebind, LeaseExpired, } impl From for u64 { fn from(v: DhcpV6Event) -> u64 { v as u64 } } impl TryFrom for DhcpV6Event { type Error = DhcpError; fn try_from(v: u64) -> Result { match v { x if x == Self::UdpPackageIn as u64 => Ok(Self::UdpPackageIn), x if x == Self::TransmitWait as u64 => Ok(Self::TransmitWait), x if x == Self::Timeout as u64 => Ok(Self::Timeout), x if x == Self::Renew as u64 => Ok(Self::Renew), x if x == Self::Rebind as u64 => Ok(Self::Rebind), x if x == Self::LeaseExpired as u64 => Ok(Self::LeaseExpired), _ => { let e = DhcpError::new( ErrorKind::Bug, format!("Got unexpected event ID {v}"), ); log::error!("{e}"); Err(e) } } } } impl std::fmt::Display for DhcpV6Event { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { Self::UdpPackageIn => "UdpPackageIn", Self::TransmitWait => "TransmitWait", Self::Timeout => "Timeout", Self::Renew => "Renew", Self::Rebind => "Rebind", Self::LeaseExpired => "LeaseExpired", } ) } } impl DhcpEvent for DhcpV6Event {} mozim-0.2.7/src/dhcpv6/lease.rs000064400000000000000000000100731046102023000144060ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::net::Ipv6Addr; use dhcproto::{ v6, v6::{DhcpOption, DhcpOptions}, }; use crate::{DhcpError, DhcpV6IaType, ErrorKind}; #[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] pub struct DhcpV6Lease { pub t1: u32, pub t2: u32, pub xid: [u8; 3], pub iaid: u32, pub ia_type: DhcpV6IaType, pub addr: Ipv6Addr, pub prefix_len: u8, // TODO: OPTION_UNICAST // For Request, Renew, Information-request, Release, and Decline // messages, it is allowed only if the Server Unicast option is // configured. pub preferred_life: u32, pub valid_life: u32, pub cli_duid: Vec, pub srv_duid: Vec, pub dhcp_opts: Vec, pub srv_ip: Ipv6Addr, } impl Default for DhcpV6Lease { fn default() -> Self { Self { t1: 0, t2: 0, xid: [0; 3], iaid: 0, ia_type: DhcpV6IaType::TemporaryAddresses, addr: Ipv6Addr::UNSPECIFIED, prefix_len: 128, preferred_life: 0, valid_life: 0, cli_duid: Vec::new(), srv_duid: Vec::new(), dhcp_opts: Vec::new(), srv_ip: Ipv6Addr::UNSPECIFIED, } } } impl std::convert::TryFrom<&v6::Message> for DhcpV6Lease { type Error = DhcpError; fn try_from(v6_dhcp_msg: &v6::Message) -> Result { let mut ret = Self { xid: v6_dhcp_msg.xid(), ..Default::default() }; for dhcp_opt in v6_dhcp_msg.opts().iter() { match dhcp_opt { DhcpOption::ClientId(v) => ret.cli_duid = v.clone(), DhcpOption::ServerId(v) => ret.srv_duid = v.clone(), DhcpOption::IANA(v) => { ret.ia_type = DhcpV6IaType::NonTemporaryAddresses; ret.iaid = v.id; ret.t1 = v.t1; ret.t2 = v.t2; parse_dhcp_opt_iaadr(&v.opts, &mut ret); } DhcpOption::IATA(v) => { ret.ia_type = DhcpV6IaType::TemporaryAddresses; ret.iaid = v.id; parse_dhcp_opt_iaadr(&v.opts, &mut ret); } DhcpOption::IAPD(v) => { ret.ia_type = DhcpV6IaType::PrefixDelegation; ret.iaid = v.id; ret.t1 = v.t1; ret.t2 = v.t2; parse_dhcp_opt_iaadr(&v.opts, &mut ret); } DhcpOption::ServerUnicast(srv_ip) => { ret.srv_ip = *srv_ip; } DhcpOption::StatusCode(v) => { if v.status != v6::Status::Success { return Err(DhcpError::new( ErrorKind::NoLease, format!( "DHCP server reply status code {}({:?}), \ message {}", u16::from(v.status), v.status, v.msg ), )); } } v => { log::debug!("Unsupported DHCPv6 opt {v:?}"); } } } ret.dhcp_opts = v6_dhcp_msg.opts().iter().cloned().collect(); // TODO: Validate T1 < T2 < lease_time. Ok(ret) } } fn parse_dhcp_opt_iaadr(opts: &DhcpOptions, lease: &mut DhcpV6Lease) { if let Some(DhcpOption::IAPrefix(a)) = opts.get(v6::OptionCode::IAPrefix) { lease.addr = a.prefix_ip; lease.prefix_len = a.prefix_len; lease.preferred_life = a.preferred_lifetime; lease.valid_life = a.valid_lifetime; } if let Some(DhcpOption::IAAddr(a)) = opts.get(v6::OptionCode::IAAddr) { lease.addr = a.addr; lease.preferred_life = a.preferred_life; lease.valid_life = a.valid_life; lease.prefix_len = 128 } } mozim-0.2.7/src/dhcpv6/mod.rs000064400000000000000000000005721046102023000140770ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 mod client; mod config; mod event; mod lease; mod msg; mod time; pub use self::client::DhcpV6Client; pub use self::config::{ DhcpV6Config, DhcpV6IaType, Dhcpv6Duid, Dhcpv6DuidEn, Dhcpv6DuidLl, Dhcpv6DuidLlt, Dhcpv6DuidUuid, }; pub use self::event::DhcpV6Event; pub use self::lease::DhcpV6Lease; pub use self::msg::DhcpV6Message; mozim-0.2.7/src/dhcpv6/msg.rs000064400000000000000000000232131046102023000141030ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::net::Ipv6Addr; use std::time::Instant; use dhcproto::{ v6, v6::{DhcpOption, DhcpOptions}, Decodable, Decoder, Encodable, }; use crate::{DhcpError, DhcpV6Config, DhcpV6IaType, DhcpV6Lease, ErrorKind}; const DEFAULT_IAID: u32 = 0; #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub(crate) struct DhcpV6MessageType(v6::MessageType); impl DhcpV6MessageType { pub(crate) const SOLICIT: Self = DhcpV6MessageType(v6::MessageType::Solicit); pub(crate) const ADVERTISE: Self = DhcpV6MessageType(v6::MessageType::Advertise); pub(crate) const REQUEST: Self = DhcpV6MessageType(v6::MessageType::Request); pub(crate) const REPLY: Self = DhcpV6MessageType(v6::MessageType::Reply); pub(crate) const RENEW: Self = DhcpV6MessageType(v6::MessageType::Renew); pub(crate) const REBIND: Self = DhcpV6MessageType(v6::MessageType::Rebind); pub(crate) const RELEASE: Self = DhcpV6MessageType(v6::MessageType::Release); } impl Default for DhcpV6MessageType { fn default() -> Self { Self(v6::MessageType::Unknown(0)) } } impl std::fmt::Display for DhcpV6MessageType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{:?}", match self.0 { v6::MessageType::Solicit => "solicit", v6::MessageType::Advertise => "advertise", v6::MessageType::Request => "request", v6::MessageType::Confirm => "confirm", v6::MessageType::Decline => "decline", v6::MessageType::Renew => "renew", v6::MessageType::Rebind => "rebind", v6::MessageType::Release => "release", v6::MessageType::Reply => "reply", _ => { log::warn!("Got unknown message type {:?}", self.0); "unknown" } } ) } } impl From for v6::MessageType { fn from(v: DhcpV6MessageType) -> Self { v.0 } } impl From for DhcpV6MessageType { fn from(v: v6::MessageType) -> Self { Self(v) } } #[derive(Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct DhcpV6Message { pub(crate) msg_type: DhcpV6MessageType, pub(crate) lease: Option, pub(crate) config: DhcpV6Config, pub(crate) xid: [u8; 3], elapsed_time: u16, } impl DhcpV6Message { pub(crate) fn new( config: &DhcpV6Config, msg_type: DhcpV6MessageType, xid: [u8; 3], ) -> Self { Self { msg_type, config: config.clone(), lease: None, xid, elapsed_time: 0, } } pub(crate) fn load_lease( &mut self, lease: DhcpV6Lease, ) -> Result<(), DhcpError> { validate_lease(&self.config, &lease)?; self.lease = Some(lease); Ok(()) } pub(crate) fn to_dhcp_pkg(&self) -> Result, DhcpError> { let mut dhcp_msg = v6::Message::new_with_id(self.msg_type.into(), self.xid); dhcp_msg .opts_mut() .insert(DhcpOption::ClientId(self.config.duid.to_vec())); match self.config.ia_type { DhcpV6IaType::NonTemporaryAddresses => { dhcp_msg.opts_mut().insert(DhcpOption::IANA(v6::IANA { id: self .lease .as_ref() .map(|l| l.iaid) .unwrap_or(DEFAULT_IAID), // Required by RFC 8415 section 21.4 t1: 0, // Required by RFC 8415 section 21.4 t2: 0, opts: self .lease .as_ref() .map(gen_iaadr_dhcp_opt) .unwrap_or_default(), })) } DhcpV6IaType::TemporaryAddresses => { dhcp_msg.opts_mut().insert(DhcpOption::IATA(v6::IATA { id: self .lease .as_ref() .map(|l| l.iaid) .unwrap_or(DEFAULT_IAID), opts: self .lease .as_ref() .map(gen_iaadr_dhcp_opt) .unwrap_or_default(), })) } DhcpV6IaType::PrefixDelegation => { dhcp_msg.opts_mut().insert(DhcpOption::IAPD(v6::IAPD { id: self .lease .as_ref() .map(|l| l.iaid) .unwrap_or(DEFAULT_IAID), // Required by RFC 8415 section 21.21 t1: 0, // Required by RFC 8415 section 21.21 t2: 0, opts: self .lease .as_ref() .map(gen_iaadr_dhcp_opt) .unwrap_or_default(), })) } } match self.msg_type { DhcpV6MessageType::SOLICIT | DhcpV6MessageType::REBIND => (), DhcpV6MessageType::REQUEST | DhcpV6MessageType::RENEW | DhcpV6MessageType::RELEASE => { if let Some(lease) = self.lease.as_ref() { dhcp_msg .opts_mut() .insert(DhcpOption::ServerId(lease.srv_duid.clone())); } else { return Err(DhcpError::new( ErrorKind::InvalidArgument, "No DHCP lease found for DHCP request, please run \ DhcpV6Message::load_lease() first" .to_string(), )); } } _ => { log::error!( "BUG: Invalid DhcpV6MessageType {:?}", self.msg_type ); } } if self.elapsed_time > 0 { dhcp_msg .opts_mut() .insert(DhcpOption::ElapsedTime(self.elapsed_time)); } log::debug!("DHCP message {dhcp_msg:?}"); let mut dhcp_msg_buff = Vec::new(); let mut e = v6::Encoder::new(&mut dhcp_msg_buff); dhcp_msg.encode(&mut e)?; Ok(dhcp_msg_buff) } pub(crate) fn from_dhcp_pkg(payload: &[u8]) -> Result { let v6_dhcp_msg = v6::Message::decode(&mut Decoder::new(payload)) .map_err(|decode_error| { let e = DhcpError::new( ErrorKind::InvalidDhcpServerReply, format!( "Failed to parse DHCPv6 message from payload of pkg \ {payload:?}: {decode_error}" ), ); log::error!("{e}"); e })?; let ret = Self { lease: Some(DhcpV6Lease::try_from(&v6_dhcp_msg)?), msg_type: v6_dhcp_msg.msg_type().into(), xid: v6_dhcp_msg.xid(), ..Default::default() }; log::debug!("Got reply DHCP message {ret:?}"); Ok(ret) } pub(crate) fn add_elapsed_time(&mut self, trans_begin_time: Instant) { self.elapsed_time = match u16::try_from(trans_begin_time.elapsed().as_secs() / 100) { Ok(i) => i, Err(_) => u16::MAX, }; } } fn validate_lease( config: &DhcpV6Config, lease: &DhcpV6Lease, ) -> Result<(), DhcpError> { if lease.ia_type != config.ia_type { return Err(DhcpError::new( ErrorKind::InvalidArgument, format!( "DHCPv6 lease contains different IA type({}) with config({}) \ DhcpV6Message::load_lease() with correct lease", lease.ia_type, config.ia_type ), )); } if lease.srv_duid.is_empty() { return Err(DhcpError::new( ErrorKind::InvalidArgument, "DHCPv6 lease contains empty server DUID, please run \ DhcpV6Message::load_lease() with correct lease" .to_string(), )); } if lease.addr == Ipv6Addr::UNSPECIFIED { return Err(DhcpError::new( ErrorKind::InvalidArgument, "DHCPv6 lease contains invalid all zero lease IPv6 address, \ please run DhcpV6Message::load_lease() with correct lease" .to_string(), )); } Ok(()) } fn gen_iaadr_dhcp_opt(lease: &DhcpV6Lease) -> DhcpOptions { let mut ret = DhcpOptions::new(); match lease.ia_type { DhcpV6IaType::TemporaryAddresses | DhcpV6IaType::NonTemporaryAddresses => { ret.insert(DhcpOption::IAAddr(v6::IAAddr { addr: lease.addr, // Set to 0 per RFC 8415 section 21.6 preferred_life: 0, // Set to 0 per RFC 8415 section 21.6 valid_life: 0, opts: DhcpOptions::new(), })); } DhcpV6IaType::PrefixDelegation => { ret.insert(DhcpOption::IAPrefix(v6::IAPrefix { prefix_len: lease.prefix_len, prefix_ip: lease.addr, // Set to 0 per RFC 8415 section 21.6 preferred_lifetime: 0, // Set to 0 per RFC 8415 section 21.6 valid_lifetime: 0, opts: DhcpOptions::new(), })); } } ret } mozim-0.2.7/src/dhcpv6/time.rs000064400000000000000000000103251046102023000142530ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::time::{Duration, Instant}; use rand::Rng; use crate::{DhcpError, ErrorKind}; // RFC 8415 section 7.6 Transmission and Retransmission Parameters const SOL_TIMEOUT: Duration = Duration::from_secs(1); const SOL_MAX_RT: Duration = Duration::from_secs(3600); const REQ_TIMEOUT: Duration = Duration::from_secs(1); const REQ_MAX_RT: Duration = Duration::from_secs(30); const REQ_MAX_RC: u32 = 10; const REN_TIMEOUT: Duration = Duration::from_secs(10); const REN_MAX_RT: Duration = Duration::from_secs(600); const REB_TIMEOUT: Duration = Duration::from_secs(10); const REB_MAX_RT: Duration = Duration::from_secs(600); // RFC 8415 section 15. Reliability of Client-Initiated Message Exchanges // RT Retransmission timeout // IRT Initial retransmission time // MRC Maximum retransmission count // MRT Maximum retransmission time // MRD Maximum retransmission duration // RAND Randomization factor fn gen_retransmit_time( trans_begin_time: Instant, retransmit_count: u32, rt: Duration, irt: Duration, mrt: Duration, mrc: u32, mrd: Duration, ) -> Option { if mrc != 0 && mrc < retransmit_count { return None; } if mrd != Duration::new(0, 0) && mrd < trans_begin_time.elapsed() { return None; } let rt = if rt == Duration::new(0, 0) { Duration::from_millis( (irt.as_millis() * rand::thread_rng().gen_range(900..1100) / 1000) .try_into() .unwrap_or(u64::MAX), ) } else { Duration::from_millis( (rt.as_millis() * rand::thread_rng().gen_range(1900..2100) / 1000) .try_into() .unwrap_or(u64::MAX), ) }; if mrt != Duration::new(0, 0) && rt > mrt { Some(Duration::from_millis( (mrt.as_millis() * rand::thread_rng().gen_range(900..1100) / 1000) .try_into() .unwrap_or(u64::MAX), )) } else { Some(rt) } } pub(crate) fn gen_solicit_wait_time( trans_begin_time: Instant, retransmit_count: u32, previous_wait_time: Duration, ) -> Result { match gen_retransmit_time( trans_begin_time, retransmit_count, previous_wait_time, SOL_TIMEOUT, SOL_MAX_RT, 0, Duration::new(0, 0), ) { Some(rt) => Ok(rt), None => Err(DhcpError::new( ErrorKind::Timeout, "Timeout on waiting DHCPv6 reply on SOLICIT message".to_string(), )), } } pub(crate) fn gen_request_wait_time( trans_begin_time: Instant, retransmit_count: u32, previous_wait_time: Duration, ) -> Result { match gen_retransmit_time( trans_begin_time, retransmit_count, previous_wait_time, REQ_TIMEOUT, REQ_MAX_RT, REQ_MAX_RC, Duration::new(0, 0), ) { Some(rt) => Ok(rt), None => Err(DhcpError::new( ErrorKind::Timeout, "Timeout on waiting DHCPv6 reply on REQUEST message".to_string(), )), } } pub(crate) fn gen_renew_wait_time( trans_begin_time: Instant, retransmit_count: u32, previous_wait_time: Duration, t2: Duration, ) -> Result { match gen_retransmit_time( trans_begin_time, retransmit_count, previous_wait_time, REN_TIMEOUT, REN_MAX_RT, 0, t2, ) { Some(rt) => Ok(rt), None => Err(DhcpError::new( ErrorKind::Timeout, "Timeout on waiting DHCPv6 reply on RENEW message".to_string(), )), } } pub(crate) fn gen_rebind_wait_time( trans_begin_time: Instant, retransmit_count: u32, previous_wait_time: Duration, valid_life: Duration, ) -> Result { match gen_retransmit_time( trans_begin_time, retransmit_count, previous_wait_time, REB_TIMEOUT, REB_MAX_RT, 0, valid_life, ) { Some(rt) => Ok(rt), None => Err(DhcpError::new( ErrorKind::Timeout, "Timeout on waiting DHCPv6 reply on REBIND message".to_string(), )), } } mozim-0.2.7/src/error.rs000064400000000000000000000033721046102023000132600ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum ErrorKind { Timeout, InvalidArgument, InvalidDhcpServerReply, NoLease, Bug, LeaseExpired, } #[derive(Debug, PartialEq, Eq, Clone)] pub struct DhcpError { kind: ErrorKind, msg: String, } impl DhcpError { pub fn new(kind: ErrorKind, msg: String) -> Self { Self { kind, msg } } pub fn kind(&self) -> ErrorKind { self.kind } pub fn msg(&self) -> &str { self.msg.as_str() } } impl std::error::Error for DhcpError {} impl std::fmt::Display for ErrorKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{self:?}") } } impl std::fmt::Display for DhcpError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}: {}", self.kind, self.msg) } } impl From for DhcpError { fn from(e: std::io::Error) -> Self { Self::new(ErrorKind::Bug, format!("IO error: {e}")) } } impl From for DhcpError { fn from(e: std::ffi::NulError) -> Self { Self::new(ErrorKind::Bug, format!("CString error: {e}")) } } impl From for DhcpError { fn from(e: dhcproto::v4::EncodeError) -> Self { Self::new(ErrorKind::Bug, format!("DHCP protocol error: {e}")) } } impl From for DhcpError { fn from(e: etherparse::WriteError) -> Self { Self::new(ErrorKind::Bug, format!("etherparse protocol error: {e}")) } } impl From for DhcpError { fn from(e: std::net::AddrParseError) -> Self { Self::new(ErrorKind::Bug, format!("IPv4 address parse error: {e}")) } } mozim-0.2.7/src/event.rs000064400000000000000000000137341046102023000132530ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use std::os::fd::BorrowedFd; use std::os::unix::io::{AsRawFd, RawFd}; use std::time::Duration; use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags}; use crate::{time::DhcpTimerFd, DhcpError, ErrorKind}; const EVENT_BUFFER_COUNT: usize = 64; pub(crate) trait DhcpEvent: std::fmt::Display + Into + Eq + std::hash::Hash + TryFrom + Copy { } #[derive(Debug)] pub(crate) struct DhcpEpoll { pub(crate) fd: Epoll, } impl AsRawFd for DhcpEpoll { fn as_raw_fd(&self) -> RawFd { self.fd.0.as_raw_fd() } } impl DhcpEpoll { pub(crate) fn new() -> Result { Ok(Self { fd: Epoll::new(EpollCreateFlags::empty()).map_err(|e| { let e = DhcpError::new( ErrorKind::Bug, format!("Failed to create Epoll: {e}"), ); log::error!("{e}"); e })?, }) } pub(crate) fn add_fd(&self, fd: RawFd, event: T) -> Result<(), DhcpError> where T: DhcpEvent, { let fd = unsafe { BorrowedFd::borrow_raw(fd) }; log::debug!( "Adding fd {} to Epoll {}, event {}", fd.as_raw_fd(), self.fd.0.as_raw_fd(), event ); let event = EpollEvent::new(EpollFlags::EPOLLIN, event.into()); self.fd.add(fd, event).map_err(|e| { let e = DhcpError::new( ErrorKind::Bug, format!( "Failed to add fd {} with event {} to epoll {}: {e}", fd.as_raw_fd(), event.data(), self.fd.0.as_raw_fd() ), ); log::error!("{e}"); e }) } pub(crate) fn del_fd(&self, fd: RawFd) -> Result<(), DhcpError> { let fd = unsafe { BorrowedFd::borrow_raw(fd) }; log::debug!( "Removing fd {} from Epoll {}", fd.as_raw_fd(), self.fd.0.as_raw_fd() ); self.fd.delete(fd).map_err(|e| { let e = DhcpError::new( ErrorKind::Bug, format!( "Failed to delete fd {} from epoll {}: {e}", fd.as_raw_fd(), self.fd.0.as_raw_fd(), ), ); log::error!("{e}"); e }) } pub(crate) fn poll(&self, wait_time: isize) -> Result, DhcpError> where T: DhcpEvent, { let mut events: [EpollEvent; EVENT_BUFFER_COUNT] = [EpollEvent::empty(); EVENT_BUFFER_COUNT]; loop { match self.fd.wait(&mut events, 1000 * wait_time as u16) { Ok(c) => { let mut ret = Vec::new(); for i in &events[..c] { ret.push(T::try_from(i.data())?); } return Ok(ret); } Err(e) => match e { nix::errno::Errno::EINTR | nix::errno::Errno::EAGAIN => { // retry continue; } _ => { let e = DhcpError::new( ErrorKind::Bug, format!("Failed on epoll_wait(): {e}"), ); return Err(e); } }, } } } } #[derive(Debug)] pub(crate) struct DhcpEventPool { timer_fds: HashMap, socket_fds: HashMap, pub(crate) epoll: DhcpEpoll, } impl Drop for DhcpEventPool { fn drop(&mut self) { self.remove_all_event(); } } impl DhcpEventPool { pub(crate) fn remove_all_event(&mut self) { for (_, timer_fd) in self.timer_fds.drain() { self.epoll.del_fd(timer_fd.as_raw_fd()).ok(); } for (_, fd) in self.socket_fds.drain() { self.epoll.del_fd(fd).ok(); } } pub(crate) fn new() -> Result { Ok(Self { timer_fds: HashMap::new(), socket_fds: HashMap::new(), epoll: DhcpEpoll::new()?, }) } pub(crate) fn add_socket( &mut self, fd: RawFd, event: T, ) -> Result<(), DhcpError> { log::debug!("Adding socket {fd} with event {event} to event pool"); self.socket_fds.insert(event, fd); self.epoll.add_fd(fd, event) } pub(crate) fn del_socket(&mut self, event: T) -> Result<(), DhcpError> { if let Some(fd) = self.socket_fds.remove(&event) { self.epoll.del_fd(fd)?; } Ok(()) } pub(crate) fn add_timer( &mut self, timeout: Duration, event: T, ) -> Result<(), DhcpError> { log::debug!( "Adding timer {} milliseconds with event {} to event pool", timeout.as_millis(), event ); let timer_fd = DhcpTimerFd::new(timeout)?; self.epoll.add_fd(timer_fd.as_raw_fd(), event)?; self.timer_fds.insert(event, timer_fd); Ok(()) } pub(crate) fn del_timer(&mut self, event: T) -> Result<(), DhcpError> { if let Some(timer_fd) = self.timer_fds.remove(&event) { self.epoll.del_fd(timer_fd.as_raw_fd())?; } log::debug!("Deleted timer {event} from event pool"); Ok(()) } pub(crate) fn poll(&self, wait_time: u32) -> Result, DhcpError> { match isize::try_from(wait_time) { Ok(i) => self.epoll.poll(i), Err(_) => Err(DhcpError::new( ErrorKind::InvalidArgument, format!( "Invalid timeout, should be in the range of 0 - {}", isize::MAX ), )), } } } mozim-0.2.7/src/integ_tests/dhcpv4.rs000064400000000000000000000032251046102023000156440ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::{DhcpV4Client, DhcpV4Config, DhcpV4Lease}; use super::env::{ with_dhcp_env, FOO1_CLIENT_ID, FOO1_HOSTNAME, FOO1_STATIC_IP, TEST_NIC_CLI, }; const POLL_WAIT_TIME: u32 = 5; #[test] fn test_dhcpv4_manual_client_id() { with_dhcp_env(|| { let mut config = DhcpV4Config::new(TEST_NIC_CLI); config.set_client_id(0, FOO1_CLIENT_ID.as_bytes()); let mut client_id = vec![0]; client_id.extend_from_slice(FOO1_CLIENT_ID.as_bytes()); assert_eq!(config.client_id, client_id); let mut cli = DhcpV4Client::init(config, None).unwrap(); let lease = get_lease(&mut cli); assert!(lease.is_some()); if let Some(lease) = lease { // Even though we didn't send it in the DHCP request, dnsmasq should // return the hostname since it was set in the --dhcp-host // option assert_eq!( lease.host_name.as_ref(), Some(&FOO1_HOSTNAME.to_string()) ); // If the client id was set correctly to FOO1_CLIENT_ID then the // server should return FOO1_STATIC_IP. assert_eq!(lease.yiaddr, FOO1_STATIC_IP,); } }) } fn get_lease(cli: &mut DhcpV4Client) -> Option { while let Ok(events) = cli.poll(POLL_WAIT_TIME) { for event in events { match cli.process(event) { Ok(Some(lease)) => { return Some(lease); } Ok(None) => (), Err(_) => { return None; } } } } None } mozim-0.2.7/src/integ_tests/dhcpv4_async.rs000064400000000000000000000054531046102023000170460ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use futures::StreamExt; use crate::{ DhcpV4ClasslessRoute, DhcpV4ClientAsync, DhcpV4Config, DhcpV4Lease, }; use super::env::{ with_dhcp_env, FOO1_HOSTNAME, FOO1_STATIC_IP_HOSTNAME_AS_CLIENT_ID, TEST_CLS_DST, TEST_CLS_DST_LEN, TEST_CLS_RT_ADDR, TEST_NIC_CLI, }; const FOO2_HOSTNAME: &str = "foo2"; #[test] fn test_dhcpv4_async() { with_dhcp_env(|| { let mut config = DhcpV4Config::new(TEST_NIC_CLI); // Since hostname hasn't been set yet, client_id should be empty. config.use_host_name_as_client_id(); assert_eq!(config.client_id.len(), 0); config.set_host_name(FOO1_HOSTNAME); config.use_host_name_as_client_id(); // Now client id should be set to 0 + hostname. let mut client_id = vec![0]; client_id.extend_from_slice(FOO1_HOSTNAME.as_bytes()); assert_eq!(config.client_id, client_id); // config.use_host_name_as_client_id() copies the current hostname to // client_id at the time it was called. We should now change the // hostname to something dnsmasq doesn't know about so we're sure we get // the correct ip address based on the client id (original hostname) and // not the hostname we're now sending in option 12. config.set_host_name(FOO2_HOSTNAME); let mut cli = DhcpV4ClientAsync::init(config, None).unwrap(); let rt = tokio::runtime::Builder::new_current_thread() .enable_io() .build() .unwrap(); let lease = rt.block_on(get_lease(&mut cli)); assert!(lease.is_some()); if let Some(lease) = lease { // We should get FOO2_HOSTNAME as the hostname since that's what we // sent in option 12 in the DHCP request. assert_eq!( lease.host_name.as_ref(), Some(&FOO2_HOSTNAME.to_string()) ); // If the client id was set correctly to FOO1_HOSTNAME via the // call to use_host_name_as_client_id(), then the server should // return FOO1_STATIC_IP_HOSTNAME_AS_CLIENT_ID. assert_eq!(lease.yiaddr, FOO1_STATIC_IP_HOSTNAME_AS_CLIENT_ID,); assert_eq!( lease.classless_routes.as_deref().unwrap(), &[DhcpV4ClasslessRoute { destination: TEST_CLS_DST, prefix_length: TEST_CLS_DST_LEN, router: TEST_CLS_RT_ADDR, }] ); assert_eq!( lease.get_option_raw(249).unwrap(), &[249, 8, 24, 203, 0, 113, 192, 0, 2, 40] ); cli.release(&lease).unwrap(); } }) } async fn get_lease(cli: &mut DhcpV4ClientAsync) -> Option { cli.next().await.unwrap().ok() } mozim-0.2.7/src/integ_tests/dhcpv4_proxy.rs000064400000000000000000000020621046102023000171030ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::{DhcpV4Client, DhcpV4Config, DhcpV4Lease}; use super::env::{ with_dhcp_env, TEST_NIC_CLI, TEST_PROXY_IP1, TEST_PROXY_MAC1, }; const POLL_WAIT_TIME: u32 = 5; #[test] fn test_dhcpv4_proxy() { with_dhcp_env(|| { let config = DhcpV4Config::new_proxy(TEST_NIC_CLI, TEST_PROXY_MAC1); let mut cli = DhcpV4Client::init(config, None).unwrap(); let lease = get_lease(&mut cli); assert!(lease.is_some()); if let Some(lease) = lease { assert_eq!(lease.yiaddr, TEST_PROXY_IP1); cli.release(&lease).unwrap(); } }) } fn get_lease(cli: &mut DhcpV4Client) -> Option { while let Ok(events) = cli.poll(POLL_WAIT_TIME) { for event in events { match cli.process(event) { Ok(Some(lease)) => { return Some(lease); } Ok(None) => (), Err(_) => { return None; } } } } None } mozim-0.2.7/src/integ_tests/dhcpv6.rs000064400000000000000000000021541046102023000156460ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::{DhcpV6Client, DhcpV6Config, DhcpV6IaType, DhcpV6Lease}; use super::env::{with_dhcp_env, FOO1_STATIC_IPV6, TEST_NIC_CLI}; const POLL_WAIT_TIME: u32 = 5; #[test] fn test_dhcpv6_use_default_client_id() { with_dhcp_env(|| { let config = DhcpV6Config::new( TEST_NIC_CLI, DhcpV6IaType::NonTemporaryAddresses, ); let mut cli = DhcpV6Client::init(config, None).unwrap(); let lease = get_lease(&mut cli); println!("Got lease {:?}", lease); assert!(lease.is_some()); if let Some(lease) = lease { assert_eq!(lease.addr, FOO1_STATIC_IPV6); } }) } fn get_lease(cli: &mut DhcpV6Client) -> Option { while let Ok(events) = cli.poll(POLL_WAIT_TIME) { for event in events { match cli.process(event) { Ok(Some(lease)) => { return Some(lease); } Ok(None) => (), Err(_) => { return None; } } } } None } mozim-0.2.7/src/integ_tests/dhcpv6_async.rs000064400000000000000000000022021046102023000170350ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use futures::StreamExt; use crate::{DhcpV6ClientAsync, DhcpV6Config, DhcpV6IaType, DhcpV6Lease}; use super::env::{with_dhcp_env, FOO1_STATIC_IPV6, TEST_NIC_CLI}; #[test] fn test_dhcpv6_async() { with_dhcp_env(|| { let config = DhcpV6Config::new( TEST_NIC_CLI, DhcpV6IaType::NonTemporaryAddresses, ); let mut cli = DhcpV6ClientAsync::init(config, None).unwrap(); let rt = tokio::runtime::Builder::new_current_thread() .enable_io() .build() .unwrap(); let lease = rt.block_on(get_lease(&mut cli)); assert!(lease.is_some()); if let Some(lease) = lease { // If the client id was set correctly to FOO1_HOSTNAME via the // call to use_host_name_as_client_id(), then the server should // return FOO1_STATIC_IP_HOSTNAME_AS_CLIENT_ID. assert_eq!(lease.addr, FOO1_STATIC_IPV6); cli.release(&lease).unwrap(); } }) } async fn get_lease(cli: &mut DhcpV6ClientAsync) -> Option { cli.next().await.unwrap().ok() } mozim-0.2.7/src/integ_tests/env.rs000064400000000000000000000134301046102023000152430ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::io::Read; use std::net::{Ipv4Addr, Ipv6Addr}; use std::process::Command; use std::str::FromStr; const PID_FILE_PATH: &str = "/tmp/mozim_test_dnsmasq_pid"; const TEST_DHCPD_NETNS: &str = "mozim_test"; const LOG_FILE: &str = "/tmp/mozim_test_dnsmasq_log"; pub(crate) const TEST_NIC_CLI: &str = "dhcpcli"; const TEST_NIC_CLI_MAC: &str = "00:23:45:67:89:1a"; pub(crate) const TEST_PROXY_MAC1: &str = "00:11:22:33:44:55"; const TEST_NIC_SRV: &str = "dhcpsrv"; const TEST_DHCP_SRV_IP: &str = "192.0.2.1"; const TEST_DHCP_SRV_IPV6: &str = "2001:db8:a::1"; pub(crate) const TEST_CLS_DST: Ipv4Addr = Ipv4Addr::new(203, 0, 113, 0); pub(crate) const TEST_CLS_DST_LEN: u8 = 24; pub(crate) const TEST_CLS_RT_ADDR: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 40); pub(crate) const FOO1_HOSTNAME: &str = "foo1"; pub(crate) const FOO1_CLIENT_ID: &str = "0123456789123456012345678912345601234567891234560123456789123456"; pub(crate) const FOO1_STATIC_IP: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 99); pub(crate) const FOO1_STATIC_IPV6: Ipv6Addr = Ipv6Addr::new(0x2001, 0xdb8, 0xa, 0x0, 0x0, 0x0, 0x0, 0x99); pub(crate) const FOO1_STATIC_IP_HOSTNAME_AS_CLIENT_ID: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 96); pub(crate) const TEST_PROXY_IP1: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 51); fn create_test_net_namespace() { run_cmd(&format!("ip netns add {TEST_DHCPD_NETNS}")); } fn remove_test_net_namespace() { run_cmd_ignore_failure(&format!("ip netns del {TEST_DHCPD_NETNS}")); } fn create_test_veth_nics() { run_cmd(&format!( "ip link add {TEST_NIC_CLI} address {TEST_NIC_CLI_MAC} type veth peer \ name {TEST_NIC_SRV}" )); run_cmd(&format!("ip link set {TEST_NIC_CLI} up")); run_cmd(&format!( "ip link set {TEST_NIC_SRV} netns {TEST_DHCPD_NETNS}" )); run_cmd(&format!( "ip netns exec {TEST_DHCPD_NETNS} ip link set {TEST_NIC_SRV} up", )); run_cmd(&format!( "ip netns exec {TEST_DHCPD_NETNS} ip addr add {TEST_DHCP_SRV_IP}/24 \ dev {TEST_NIC_SRV}", )); run_cmd(&format!( "ip netns exec {TEST_DHCPD_NETNS} ip addr add {TEST_DHCP_SRV_IPV6}/64 \ dev {TEST_NIC_SRV}", )); // Need to wait 2 seconds for IPv6 duplicate address detection std::thread::sleep(std::time::Duration::from_secs(2)); } fn remove_test_veth_nics() { run_cmd_ignore_failure(&format!("ip link del {TEST_NIC_CLI}")); } fn start_dhcp_server() { run_cmd(&format!("rm {LOG_FILE}")); run_cmd(&format!("touch {LOG_FILE}")); run_cmd(&format!("chmod 666 {LOG_FILE}")); let dnsmasq_opts = format!( r#" --pid-file={PID_FILE_PATH} --log-queries --log-dhcp --log-debug --log-facility=/tmp/mozim_test_dnsmasq_log --conf-file=/dev/null --dhcp-leasefile=/tmp/mozim_test_dhcpd_lease --no-hosts --dhcp-host=id:{FOO1_CLIENT_ID},{FOO1_STATIC_IP},{FOO1_HOSTNAME} --dhcp-host=id:00:03:00:01:{TEST_NIC_CLI_MAC},[{FOO1_STATIC_IPV6}],{FOO1_HOSTNAME} --dhcp-host=id:{FOO1_HOSTNAME},{FOO1_STATIC_IP_HOSTNAME_AS_CLIENT_ID} --dhcp-host={TEST_PROXY_MAC1},{TEST_PROXY_IP1} --dhcp-option=option:dns-server,8.8.8.8,1.1.1.1 --dhcp-option=option:mtu,1492 --dhcp-option=option:domain-name,example.com --dhcp-option=option:ntp-server,192.0.2.1 --dhcp-option=121,{TEST_CLS_DST}/{TEST_CLS_DST_LEN},{TEST_CLS_RT_ADDR} --dhcp-option=249,{TEST_CLS_DST}/{TEST_CLS_DST_LEN},{TEST_CLS_RT_ADDR} --bind-interfaces --except-interface=lo --clear-on-reload --interface=dhcpsrv --dhcp-range=192.0.2.2,192.0.2.50,60 --dhcp-range=2001:db8:a::2,2001:db8:a::ff,64,2m --no-ping "# ); let cmd = format!( "ip netns exec {} dnsmasq {}", TEST_DHCPD_NETNS, dnsmasq_opts.replace('\n', " ") ); let cmds: Vec<&str> = cmd.split(' ').collect(); Command::new(cmds[0]) .args(&cmds[1..]) .spawn() .expect("Failed to start DHCP server") .wait() .ok(); // Need to wait 1 seconds for dnsmasq to finish its start std::thread::sleep(std::time::Duration::from_secs(1)); } fn stop_dhcp_server() { if !std::path::Path::new(PID_FILE_PATH).exists() { return; } let mut fd = std::fs::File::open(PID_FILE_PATH) .unwrap_or_else(|_| panic!("Failed to open {PID_FILE_PATH} file")); let mut contents = String::new(); fd.read_to_string(&mut contents) .unwrap_or_else(|_| panic!("Failed to read {PID_FILE_PATH} file")); let pid = u32::from_str(contents.trim()) .unwrap_or_else(|_| panic!("Invalid PID content {contents}")); run_cmd_ignore_failure(&format!("kill {pid}")); } fn run_cmd(cmd: &str) -> String { let cmds: Vec<&str> = cmd.split(' ').collect(); String::from_utf8( Command::new(cmds[0]) .args(&cmds[1..]) .output() .unwrap_or_else(|_| panic!("failed to execute command {cmd}")) .stdout, ) .expect("Failed to convert file command output to String") } fn run_cmd_ignore_failure(cmd: &str) -> String { let cmds: Vec<&str> = cmd.split(' ').collect(); match Command::new(cmds[0]).args(&cmds[1..]).output() { Ok(o) => String::from_utf8(o.stdout).unwrap_or_default(), Err(e) => { eprintln!("Failed to execute command {cmd}: {e}"); "".to_string() } } } pub(crate) fn with_dhcp_env(test: T) where T: FnOnce() + std::panic::UnwindSafe, { create_test_net_namespace(); create_test_veth_nics(); stop_dhcp_server(); start_dhcp_server(); let result = std::panic::catch_unwind(|| { test(); }); stop_dhcp_server(); remove_test_veth_nics(); remove_test_net_namespace(); assert!(result.is_ok()) } mozim-0.2.7/src/integ_tests/mod.rs000064400000000000000000000003011046102023000152230ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 #[cfg(test)] mod dhcpv4; #[cfg(test)] mod dhcpv4_async; #[cfg(test)] mod dhcpv4_proxy; #[cfg(test)] mod dhcpv6; #[cfg(test)] mod dhcpv6_async; mod env; mozim-0.2.7/src/lib.rs000064400000000000000000000012231046102023000126660ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 mod bpf; mod client_async; mod dhcpv4; mod dhcpv6; mod error; mod event; mod mac; mod nispor; mod proiscuous; mod socket; mod time; #[cfg(test)] mod integ_tests; pub use crate::client_async::{DhcpV4ClientAsync, DhcpV6ClientAsync}; pub use crate::dhcpv4::{ DhcpV4ClasslessRoute, DhcpV4Client, DhcpV4Config, DhcpV4Event, DhcpV4Lease, DhcpV4Message, DhcpV4MessageType, }; pub use crate::dhcpv6::{ DhcpV6Client, DhcpV6Config, DhcpV6Event, DhcpV6IaType, DhcpV6Lease, DhcpV6Message, Dhcpv6Duid, Dhcpv6DuidEn, Dhcpv6DuidLl, Dhcpv6DuidLlt, Dhcpv6DuidUuid, }; pub use crate::error::{DhcpError, ErrorKind}; mozim-0.2.7/src/mac.rs000064400000000000000000000022321046102023000126610ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use log::error; use crate::{DhcpError, ErrorKind}; pub(crate) const BROADCAST_MAC_ADDRESS: [u8; 6] = [u8::MAX; 6]; pub(crate) fn mac_str_to_u8_array(mac: &str) -> Vec { let mut mac_bytes = Vec::new(); for item in mac.split(':') { match u8::from_str_radix(item, 16) { Ok(i) => mac_bytes.push(i), Err(e) => { error!( "Failed to convert to MAC address to bytes {mac:?}: {e}" ); return Vec::new(); } } } mac_bytes } pub(crate) fn mac_address_to_eth_mac_bytes( mac_address: &str, ) -> Result<[u8; libc::ETH_ALEN as usize], DhcpError> { let mut ret = [0u8; libc::ETH_ALEN as usize]; let mac_bytes = mac_str_to_u8_array(mac_address); if mac_bytes.len() > libc::ETH_ALEN as usize { Err(DhcpError::new( ErrorKind::Bug, format!( "MAC address {} exceeded the max length {}", mac_address, libc::ETH_ALEN ), )) } else { ret.clone_from_slice(&mac_bytes); Ok(ret) } } mozim-0.2.7/src/nispor.rs000064400000000000000000000074241046102023000134430ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::net::Ipv6Addr; use std::str::FromStr; use nispor::{Ipv6AddrFlag, NetState, NetStateFilter, NetStateIfaceFilter}; use crate::{DhcpError, ErrorKind}; // We use thread to invoke nispor which has `tokio::block_on` which // stop our async usage pub(crate) fn get_nispor_iface( iface_name: &str, with_ip: bool, ) -> Result { let iface_name = iface_name.to_string(); match std::thread::spawn(move || { if iface_name.is_empty() { let e = DhcpError::new( ErrorKind::InvalidArgument, "Interface name not defined".to_string(), ); log::error!("{e}"); return Err(e); } let mut filter = NetStateFilter::minimum(); let mut iface_filter = NetStateIfaceFilter::minimum(); iface_filter.iface_name = Some(iface_name.to_string()); iface_filter.include_ip_address = with_ip; filter.iface = Some(iface_filter); let net_state = match NetState::retrieve_with_filter(&filter) { Ok(s) => s, Err(e) => { return Err(DhcpError::new( ErrorKind::Bug, format!("Failed to retrieve network state: {e}"), )) } }; if let Some(iface) = net_state.ifaces.get(iface_name.as_str()) { Ok(iface.clone()) } else { Err(DhcpError::new( ErrorKind::InvalidArgument, format!("Interface {iface_name} not found"), )) } }) .join() { Ok(n) => Ok(n?), Err(e) => Err(DhcpError::new( ErrorKind::Bug, format!("Failed to invoke nispor thread: {e:?}"), )), } } // Search link-local address or global address: // * prefer link-local address over global // * Not allow address with tentative flag. pub(crate) fn get_ipv6_addr_of_iface( iface: &nispor::Iface, ) -> Result { if let Some(addrs) = iface.ipv6.as_ref().map(|i| i.addresses.as_slice()) { if let Some(addr) = addrs .iter() .filter_map(|a| { if !a.flags.contains(&Ipv6AddrFlag::Tentative) { Ipv6Addr::from_str(a.address.as_str()).ok() } else { None } }) .find(is_ipv6_unicast_link_local) .or_else(|| { addrs .iter() .filter_map(|a| { if !a.flags.contains(&Ipv6AddrFlag::Tentative) { Ipv6Addr::from_str(a.address.as_str()).ok() } else { None } }) .find(is_ipv6_unicast) }) { Ok(addr) } else { Err(DhcpError::new( ErrorKind::InvalidArgument, format!( "Failed to find unicast IPv6 address on interface {} \ which is required for DHCPv6", iface.name ), )) } } else { Err(DhcpError::new( ErrorKind::InvalidArgument, format!( "Interface {} has no IPv6 address to start DHCPv6", iface.name ), )) } } // Copy from Rust official std::net::Ipv6Addr::is_unicast_link_local() which // is experimental. fn is_ipv6_unicast_link_local(ip: &Ipv6Addr) -> bool { (ip.segments()[0] & 0xffc0) == 0xfe80 } // Copy from Rust official std::net::Ipv6Addr::is_multicast() which is // experimental. fn is_ipv6_unicast(ip: &Ipv6Addr) -> bool { (ip.segments()[0] & 0xff00) != 0xff00 } mozim-0.2.7/src/proiscuous.rs000064400000000000000000000016431046102023000143410ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::{DhcpError, ErrorKind}; pub(crate) fn enable_promiscuous_mode( fd: libc::c_int, iface_index: libc::c_int, ) -> Result<(), DhcpError> { let mreq = libc::packet_mreq { mr_ifindex: iface_index, mr_type: libc::PACKET_MR_PROMISC as libc::c_ushort, mr_alen: 0, mr_address: [0; 8], }; unsafe { let rc = libc::setsockopt( fd, libc::SOL_PACKET, libc::PACKET_ADD_MEMBERSHIP, (&mreq as *const libc::packet_mreq) as *const libc::c_void, std::mem::size_of::() as libc::socklen_t, ); if rc != 0 { return Err(DhcpError::new( ErrorKind::Bug, format!( "Failed to set socket to promiscuous mode with error: {rc}" ), )); } } Ok(()) } mozim-0.2.7/src/socket.rs000064400000000000000000000274011046102023000134160ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::ffi::CString; use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV6, UdpSocket}; use std::os::unix::io::AsRawFd; use std::os::unix::io::RawFd; use nix::errno::Errno; use crate::{ bpf::apply_dhcp_bpf, mac::{mac_address_to_eth_mac_bytes, BROADCAST_MAC_ADDRESS}, proiscuous::enable_promiscuous_mode, DhcpError, DhcpV4Config, ErrorKind, }; pub(crate) const DEFAULT_SOCKET_TIMEOUT: u32 = 5; const PACKET_HOST: u8 = 0; // a packet addressed to the local host pub(crate) trait DhcpSocket { fn recv(&self) -> Result, DhcpError>; fn send(&self, eth_pkg: &[u8]) -> Result<(), DhcpError>; fn is_raw(&self) -> bool; } #[derive(Debug, PartialEq, Clone, Default)] pub(crate) struct DhcpRawSocket { config: DhcpV4Config, raw_fd: libc::c_int, } impl std::os::unix::io::AsRawFd for DhcpRawSocket { fn as_raw_fd(&self) -> std::os::unix::io::RawFd { self.raw_fd as std::os::unix::io::RawFd } } impl Drop for DhcpRawSocket { fn drop(&mut self) { if self.raw_fd >= 0 { unsafe { libc::close(self.raw_fd); } } } } impl DhcpRawSocket { pub(crate) fn new(config: &DhcpV4Config) -> Result { let iface_index = config.iface_index as libc::c_int; let eth_protocol = libc::ETH_P_ALL; let raw_fd = create_raw_socket(eth_protocol)?; apply_dhcp_bpf(raw_fd)?; bind_raw_socket(raw_fd, eth_protocol, iface_index, &config.src_mac)?; if config.is_proxy { enable_promiscuous_mode(raw_fd, iface_index)?; } set_socket_timeout(raw_fd, config.socket_timeout)?; log::debug!("Raw socket created {raw_fd}"); Ok(DhcpRawSocket { raw_fd, config: config.clone(), }) } } impl DhcpSocket for DhcpRawSocket { fn is_raw(&self) -> bool { true } fn send(&self, eth_pkg: &[u8]) -> Result<(), DhcpError> { if self.raw_fd < 0 { let e = DhcpError::new( ErrorKind::Bug, "Please run DhcpSocket::open_raw() first".to_string(), ); log::error!("{e}"); return Err(e); } let mut dst_addr: libc::sockaddr_ll = unsafe { std::mem::zeroed() }; dst_addr.sll_halen = libc::ETH_ALEN as u8; dst_addr.sll_addr[..libc::ETH_ALEN as usize] .clone_from_slice(&BROADCAST_MAC_ADDRESS); dst_addr.sll_ifindex = self.config.iface_index as i32; let addr_buffer_size: libc::socklen_t = std::mem::size_of::() as libc::socklen_t; let addr_ptr = unsafe { std::mem::transmute::<*mut libc::sockaddr_ll, *mut libc::sockaddr>( &mut dst_addr, ) }; unsafe { log::debug!("Sending raw ethernet package: {eth_pkg:?}"); let sent_bytes = libc::sendto( self.raw_fd, eth_pkg.as_ptr() as *mut libc::c_void, eth_pkg.len(), 0, // flags addr_ptr, addr_buffer_size, ); log::debug!("Raw socket sent: {sent_bytes} bytes"); if sent_bytes <= 0 { let e = DhcpError::new( ErrorKind::Bug, format!( "Failed to send data to socket {}: {}, data: {:?}", self.raw_fd, Errno::last(), eth_pkg, ), ); log::error!("{e}"); return Err(e); } } Ok(()) } fn recv(&self) -> Result, DhcpError> { let mut src_addr: libc::sockaddr_ll = unsafe { std::mem::zeroed() }; // TODO: Add support of `Maximum DHCP Message Size` option let mut buffer = [0u8; 1500]; let mut addr_buffer_size: libc::socklen_t = std::mem::size_of::() as libc::socklen_t; let addr_ptr = unsafe { std::mem::transmute::<*mut libc::sockaddr_ll, *mut libc::sockaddr>( &mut src_addr, ) }; unsafe { log::debug!("Raw socket receiving"); let rc = libc::recvfrom( self.raw_fd, buffer.as_mut_ptr() as *mut libc::c_void, buffer.len(), 0, // flags addr_ptr, &mut addr_buffer_size, ); if rc <= 0 { let errno = Errno::last(); let e = if errno == Errno::EAGAIN { DhcpError::new( ErrorKind::Timeout, "Timeout on receiving data from socket".to_string(), ) } else { DhcpError::new( ErrorKind::Bug, format!( "Failed to recv from socket {}: {}", self.raw_fd, errno ), ) }; log::error!("{e}"); return Err(e); } log::debug!("Raw socket received {:?}", &buffer[..rc as usize]); Ok(buffer[..rc as usize].to_vec()) } } } fn create_raw_socket( eth_protocol: libc::c_int, ) -> Result { unsafe { match libc::socket( libc::AF_PACKET, libc::SOCK_RAW, eth_protocol.to_be() as libc::c_int, ) { -1 => Err(DhcpError::new( ErrorKind::Bug, "libc::socket() failed with -1".to_string(), )), fd => Ok(fd), } } } fn bind_raw_socket( fd: libc::c_int, eth_protocol: libc::c_int, iface_index: libc::c_int, mac_address: &str, ) -> Result<(), DhcpError> { let mut sll_addr: [libc::c_uchar; 8] = [0; 8]; sll_addr[..libc::ETH_ALEN as usize] .clone_from_slice(&mac_address_to_eth_mac_bytes(mac_address)?); let mut socket_addr = libc::sockaddr_ll { sll_family: libc::AF_PACKET as libc::c_ushort, sll_protocol: (eth_protocol as libc::c_ushort).to_be(), sll_ifindex: iface_index, sll_hatype: libc::ARPHRD_ETHER as libc::c_ushort, sll_pkttype: PACKET_HOST as libc::c_uchar, sll_halen: libc::ETH_ALEN as libc::c_uchar, sll_addr, }; unsafe { let addr_ptr = std::mem::transmute::< *mut libc::sockaddr_ll, *mut libc::sockaddr, >(&mut socket_addr); match libc::bind( fd, addr_ptr, std::mem::size_of::() as libc::socklen_t, ) { 0 => Ok(()), rc => { libc::close(fd); Err(DhcpError::new( ErrorKind::Bug, format!("Failed to bind socket: {rc}"), )) } } } } #[derive(Debug)] pub(crate) struct DhcpUdpSocket { socket: UdpSocket, } impl std::os::unix::io::AsRawFd for DhcpUdpSocket { fn as_raw_fd(&self) -> RawFd { self.socket.as_raw_fd() } } impl DhcpUdpSocket { pub(crate) fn new( iface_name: &str, src_ip: &Ipv4Addr, dst_ip: &Ipv4Addr, socket_timeout: u32, ) -> Result { let socket = UdpSocket::bind(format!( "{}:{}", src_ip, 0 // Use random source port ))?; log::debug!("UDP socket bind to {socket:?}"); bind_socket_to_iface(socket.as_raw_fd(), iface_name)?; socket.set_read_timeout(Some(std::time::Duration::from_secs( socket_timeout.into(), )))?; socket.set_write_timeout(Some(std::time::Duration::from_secs( socket_timeout.into(), )))?; socket.connect(format!("{}:{}", dst_ip, dhcproto::v4::SERVER_PORT))?; Ok(Self { socket }) } pub(crate) fn new_v6( iface_index: u32, src_ip: &Ipv6Addr, socket_timeout: u32, ) -> Result { let socket = UdpSocket::bind(SocketAddrV6::new( *src_ip, dhcproto::v6::CLIENT_PORT, 0, iface_index, ))?; log::debug!("UDP socket bind to {socket:?}"); socket.set_read_timeout(Some(std::time::Duration::from_secs( socket_timeout.into(), )))?; socket.set_write_timeout(Some(std::time::Duration::from_secs( socket_timeout.into(), )))?; Ok(Self { socket }) } pub(crate) fn send_to_v6( &self, dst_ip: &Ipv6Addr, buff: &[u8], ) -> Result<(), DhcpError> { self.socket.send_to( buff, SocketAddrV6::new(*dst_ip, dhcproto::v6::SERVER_PORT, 0, 0), )?; Ok(()) } } impl DhcpSocket for DhcpUdpSocket { fn is_raw(&self) -> bool { false } fn send(&self, pkg: &[u8]) -> Result<(), DhcpError> { self.socket.send(pkg)?; Ok(()) } fn recv(&self) -> Result, DhcpError> { // TODO: Add support of `Maximum DHCP Message Size` option let mut buffer = [0u8; 1500]; let received = self.socket.recv(&mut buffer)?; Ok(buffer[..received].to_vec()) } } fn set_socket_timeout(fd: libc::c_int, timeout: u32) -> Result<(), DhcpError> { // suppress clippy warning when compiling on 64bit system, but this // `try_into()` is require on i686 system. #[allow(clippy::unnecessary_fallible_conversions)] let tv_sec: libc::time_t = match timeout.try_into() { Ok(t) => t, Err(e) => { return Err(DhcpError::new( ErrorKind::InvalidArgument, format!("Invalid timeout value {timeout}, error: {e}"), )); } }; let tmo = libc::timeval { tv_sec, tv_usec: 0 }; unsafe { let rc = libc::setsockopt( fd, libc::SOL_SOCKET, libc::SO_SNDTIMEO, (&tmo as *const libc::timeval) as *const libc::c_void, std::mem::size_of::() as libc::socklen_t, ); if rc < 0 { return Err(DhcpError::new( ErrorKind::Bug, format!( "Failed to set the send timeout SO_SNDTIMEO to socket \ {fd}: {rc}" ), )); } let rc = libc::setsockopt( fd, libc::SOL_SOCKET, libc::SO_RCVTIMEO, (&tmo as *const libc::timeval) as *const libc::c_void, std::mem::size_of::() as libc::socklen_t, ); if rc < 0 { let e = DhcpError::new( ErrorKind::Bug, format!( "Failed to set the recv timeout SO_RCVTIMEO to socket \ {fd}: {rc}" ), ); log::error!("{e}"); return Err(e); } } Ok(()) } fn bind_socket_to_iface(fd: RawFd, iface_name: &str) -> Result<(), DhcpError> { let iface_name_cstr = CString::new(iface_name)?; unsafe { let rc = libc::setsockopt( fd, libc::SOL_SOCKET, libc::SO_BINDTODEVICE, iface_name_cstr.as_ptr() as *const libc::c_void, std::mem::size_of::() as libc::socklen_t, ); if rc != 0 { let e = DhcpError::new( ErrorKind::Bug, format!( "Failed to bind socket to interface {} with error: {}", iface_name, Errno::last(), ), ); log::error!("{e}"); return Err(e); } } Ok(()) } mozim-0.2.7/src/time.rs000064400000000000000000000026121046102023000130610ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::os::fd::AsFd; use std::os::unix::io::{AsRawFd, RawFd}; use std::time::Duration; use nix::sys::time::TimeSpec; use nix::sys::timerfd::{ ClockId::CLOCK_BOOTTIME, Expiration, TimerFd, TimerFlags, TimerSetTimeFlags, }; use crate::{DhcpError, ErrorKind}; #[derive(Debug)] pub(crate) struct DhcpTimerFd { pub(crate) fd: TimerFd, } impl AsRawFd for DhcpTimerFd { fn as_raw_fd(&self) -> RawFd { self.fd.as_fd().as_raw_fd() } } impl DhcpTimerFd { pub(crate) fn new(time: Duration) -> Result { let fd = TimerFd::new(CLOCK_BOOTTIME, TimerFlags::empty()).map_err(|e| { let e = DhcpError::new( ErrorKind::Bug, format!("Failed to create timerfd {e}"), ); log::error!("{e}"); e })?; fd.set( Expiration::OneShot(TimeSpec::from_duration(time)), TimerSetTimeFlags::empty(), ) .map_err(|e| { let e = DhcpError::new( ErrorKind::Bug, format!("Failed to set timerfd {e}"), ); log::error!("{e}"); e })?; log::debug!( "TimerFd created {:?} with {} milliseconds", fd, time.as_millis() ); Ok(Self { fd }) } } mozim-0.2.7/utils/test_env_mozim000075500000000000000000000030301046102023000151110ustar 00000000000000#!/bin/bash -x PID_FILE="/tmp/test_dnsmasq.pid" LEASE_FILE="/tmp/test_dnsmasq.lease" IPV4_BLOCK="192.0.2" IPV6_BLOCK="2001:db8:a::" DHCP_SRV_IP="${IPV4_BLOCK}.1" DHCP_SRV_IP6="${IPV6_BLOCK}1" if [ -e $PID_FILE ];then sudo kill `cat $PID_FILE` fi if [ "CHK$1" == "CHKrm" ]; then sudo ip link del dhcpcli sudo ip netns del mozim exit fi sudo ip netns add mozim sudo ip link add dhcpcli type veth peer name dhcpsrv sudo ip link set dhcpcli up sudo ip link set dhcpsrv netns mozim sudo ip netns exec mozim ip link set dhcpsrv up sudo ip netns exec mozim ip addr add ${DHCP_SRV_IP}/24 dev dhcpsrv sudo ip netns exec mozim ip -6 addr add ${DHCP_SRV_IP6}/64 dev dhcpsrv sudo rm $LEASE_FILE -f sudo ip netns exec mozim dnsmasq \ --log-dhcp \ --keep-in-foreground \ --no-daemon \ --conf-file=/dev/null \ --dhcp-leasefile=$LEASE_FILE \ --no-hosts \ --dhcp-host=dummy-host,${IPV4_BLOCK}.99 \ --dhcp-option=option:dns-server,8.8.8.8,1.1.1.1 \ --dhcp-option=option:mtu,1492 \ --dhcp-option=option:domain-name,example.com\ --dhcp-option=option:ntp-server,${DHCP_SRV_IP} \ --dhcp-option=121,203.0.113.0/24,${IPV4_BLOCK}.40 \ --dhcp-option=249,203.0.113.0/24,${IPV4_BLOCK}.40 \ --keep-in-foreground \ --clear-on-reload \ --interface=dhcpsrv \ --enable-ra \ --dhcp-range=${IPV6_BLOCK}2,${IPV6_BLOCK}fff,ra-names,slaac,64,2m \ --dhcp-range=${IPV4_BLOCK}.2,${IPV4_BLOCK}.50,2m \ --no-ping if [ "CHK$1" == "CHK" ];then sudo ip link del dhcpcli sudo ip netns del mozim fi