diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f48e57..3c59af5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,6 @@ -# This file was autogenerated by dist: https://github.com/astral-sh/cargo-dist +# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist # # Copyright 2022-2024, axodotdev -# Copyright 2025 Astral Software Inc. # SPDX-License-Identifier: MIT or Apache-2.0 # # CI that: @@ -65,7 +64,7 @@ jobs: # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.7/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.3/cargo-dist-installer.sh | sh" - name: Cache dist uses: actions/upload-artifact@v4 with: @@ -218,8 +217,8 @@ jobs: - plan - build-local-artifacts - build-global-artifacts - # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) - if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} + # Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine) + if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} runs-on: "ubuntu-22.04" diff --git a/Cargo.lock b/Cargo.lock index 2a28e3f..a29f3bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,21 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -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.4" @@ -73,35 +58,29 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "atomic-waker" -version = "1.1.2" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "autocfg" @@ -109,21 +88,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link 0.2.1", -] - [[package]] name = "base64" version = "0.22.1" @@ -156,16 +120,16 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -176,15 +140,24 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block2" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -194,9 +167,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cassowary" @@ -206,9 +179,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.2.41" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "shlex", @@ -225,9 +198,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -237,15 +210,15 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -261,9 +234,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.48" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -271,9 +244,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.48" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -283,21 +256,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.47" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "colorchoice" @@ -307,35 +280,15 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "comfy-table" -version = "7.2.1" +version = "7.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03b7db8e0b4b2fdad6c551e634134e99ec000e5c8c3b6856c65e8bbaded7a3b" +checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" dependencies = [ "crossterm 0.29.0", "unicode-segmentation", "unicode-width 0.2.2", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -348,7 +301,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "crossterm_winapi", "libc", "mio 0.8.11", @@ -364,7 +317,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "crossterm_winapi", "document-features", "parking_lot", @@ -383,43 +336,44 @@ dependencies = [ [[package]] name = "csv" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" dependencies = [ "csv-core", "itoa", "ryu", - "serde", + "serde_core", ] [[package]] name = "csv-core" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" dependencies = [ "memchr", ] [[package]] name = "deranged" -version = "0.5.4" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] [[package]] -name = "displaydoc" -version = "0.2.5" +name = "dispatch2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", + "bitflags 2.11.0", + "block2", + "libc", + "objc2", ] [[package]] @@ -466,15 +420,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" - -[[package]] -name = "fnv" -version = "1.0.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "foldhash" @@ -482,20 +430,11 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -508,9 +447,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -518,15 +457,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -535,38 +474,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -576,43 +515,9 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasi 0.14.7+wasi-0.2.4", - "wasm-bindgen", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - [[package]] name = "glob" version = "0.3.3" @@ -632,9 +537,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -650,55 +555,15 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hostname" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" dependencies = [ "cfg-if", "libc", - "windows-link 0.1.3", -] - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", + "windows-link", ] -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - [[package]] name = "humansize" version = "2.1.3" @@ -708,74 +573,11 @@ dependencies = [ "libm", ] -[[package]] -name = "hyper" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-native-certs", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] - -[[package]] -name = "hyper-util" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -795,138 +597,23 @@ dependencies = [ "cc", ] -[[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 = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -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 = "indexmap" -version = "2.11.4" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", ] [[package]] name = "indoc" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" - -[[package]] -name = "io-uring" -version = "0.7.10" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", + "rustversion", ] [[package]] @@ -938,21 +625,11 @@ dependencies = [ "serde", ] -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -963,17 +640,26 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -987,9 +673,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libloading" @@ -998,14 +684,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link 0.2.1", + "windows-link", ] [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libproc" @@ -1020,15 +706,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.8.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litrs" @@ -1047,9 +727,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" @@ -1060,12 +740,6 @@ dependencies = [ "hashbrown 0.15.5", ] -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "mac-addr" version = "0.3.0" @@ -1077,9 +751,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "minimal-lexical" @@ -1087,15 +761,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[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 = "0.8.11" @@ -1104,26 +769,26 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.48.0", ] [[package]] name = "mio" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] name = "ndb-oui" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed688164b72fd09394f077dda902f086280565e6370f74364112f43b822a8f35" +checksum = "0fc97af0c84b8b5e392c808d5c557446561fa74f4da21edbcdaddd858da107d9" dependencies = [ "anyhow", "bincode", @@ -1135,10 +800,12 @@ dependencies = [ [[package]] name = "netdev" -version = "0.39.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a703aa1a87cd885b9f674922445a42dbb0c0f4f1b28fef21b227ae32375d21" +checksum = "dc9815643a243856e7bd84524e1ff739e901e846cfb06ad9627cd2b6d59bd737" dependencies = [ + "block2", + "dispatch2", "dlopen2", "ipnet", "libc", @@ -1146,9 +813,11 @@ dependencies = [ "netlink-packet-core 0.8.1", "netlink-packet-route", "netlink-sys", + "objc2-core-foundation", + "objc2-system-configuration", "once_cell", + "plist", "serde", - "system-configuration", "windows-sys 0.59.0", ] @@ -1178,7 +847,7 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ec2f5b6839be2a19d7fa5aab5bc444380f6311c2b693551cb80f45caaa7b5ef" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "libc", "log", "netlink-packet-core 0.8.1", @@ -1222,17 +891,17 @@ dependencies = [ "log", "netlink-packet-core 0.8.1", "netlink-sys", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "netlink-sys" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" dependencies = [ "bytes", - "futures", + "futures-util", "libc", "log", "tokio", @@ -1240,26 +909,25 @@ dependencies = [ [[package]] name = "netroute" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59eb911513f79dbbd83c7cb5ec059f1c4631c1b1f52f1703f52538d50a9aa200" +checksum = "b4c848584afd406dd1ab12e12c5d6df01313a20beeda2eba5738923ff4e0cc38" dependencies = [ "libc", "netlink-packet-core 0.8.1", "netlink-packet-route", "netlink-sys", "serde", - "thiserror 1.0.69", "windows-sys 0.59.0", ] [[package]] name = "netsock" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e58f2fae01a0d3809c0918aeb86db4e99b16b6ee1fbbe7e9daff8992a969cf" +checksum = "41e6a43de4034ef4b62e99c90081a371f9050cf6e2aa1651dfa4ee87bc818574" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "libproc", "log", "netlink-packet-core 0.7.0", @@ -1269,13 +937,13 @@ dependencies = [ "num-derive", "num-traits", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows-sys 0.59.0", ] [[package]] name = "nifa" -version = "0.4.0" +version = "0.5.0" dependencies = [ "anyhow", "chrono", @@ -1295,16 +963,13 @@ dependencies = [ "netsock", "os_info", "ratatui", - "reqwest", "rtnetlink", "serde", "serde_json", "serde_yaml", "termtree", - "tokio", "tracing", "tracing-subscriber", - "url", "windows-sys 0.59.0", ] @@ -1314,7 +979,19 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "libc", @@ -1341,9 +1018,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-derive" @@ -1353,7 +1030,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -1366,250 +1043,305 @@ dependencies = [ ] [[package]] -name = "object" -version = "0.37.3" +name = "objc2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ - "memchr", + "objc2-encode", ] [[package]] -name = "once_cell" -version = "1.21.3" +name = "objc2-cloud-kit" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-foundation", +] [[package]] -name = "once_cell_polyfill" -version = "1.70.1" +name = "objc2-core-data" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] [[package]] -name = "openssl-probe" -version = "0.1.6" +name = "objc2-core-foundation" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", + "block2", + "dispatch2", + "libc", + "objc2", +] [[package]] -name = "os_info" -version = "3.12.0" +name = "objc2-core-graphics" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0e1ac5fde8d43c34139135df8ea9ee9465394b2d8d20f032d38998f64afffc3" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "log", - "plist", - "serde", - "windows-sys 0.52.0", + "bitflags 2.11.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", ] [[package]] -name = "parking_lot" -version = "0.12.5" +name = "objc2-core-image" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" dependencies = [ - "lock_api", - "parking_lot_core", + "objc2", + "objc2-foundation", ] [[package]] -name = "parking_lot_core" -version = "0.9.12" +name = "objc2-core-location" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link 0.2.1", + "objc2", + "objc2-foundation", ] [[package]] -name = "paste" -version = "1.0.15" +name = "objc2-core-text" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] [[package]] -name = "percent-encoding" -version = "2.3.2" +name = "objc2-encode" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] -name = "pin-project-lite" -version = "0.2.16" +name = "objc2-foundation" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] [[package]] -name = "pin-utils" -version = "0.1.0" +name = "objc2-io-surface" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] [[package]] -name = "plist" -version = "1.8.0" +name = "objc2-quartz-core" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "base64", - "indexmap", - "quick-xml", - "serde", - "time", + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", ] [[package]] -name = "potential_utf" -version = "0.1.3" +name = "objc2-security" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" dependencies = [ - "zerovec", + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", ] [[package]] -name = "powerfmt" -version = "0.2.0" +name = "objc2-system-configuration" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "libc", + "objc2", + "objc2-core-foundation", + "objc2-security", +] [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "objc2-ui-kit" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "zerocopy", + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", ] [[package]] -name = "proc-macro2" -version = "1.0.101" +name = "objc2-user-notifications" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" dependencies = [ - "unicode-ident", + "objc2", + "objc2-foundation", ] [[package]] -name = "quick-xml" -version = "0.38.3" +name = "once_cell" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" -dependencies = [ - "memchr", -] +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "quinn" -version = "0.11.9" +name = "os_info" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.17", - "tokio", - "tracing", - "web-time", + "android_system_properties", + "log", + "nix 0.30.1", + "objc2", + "objc2-foundation", + "objc2-ui-kit", + "serde", + "windows-sys 0.61.2", ] [[package]] -name = "quinn-proto" -version = "0.11.13" +name = "parking_lot" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ - "bytes", - "getrandom 0.3.3", - "lru-slab", - "rand", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.17", - "tinyvec", - "tracing", - "web-time", + "lock_api", + "parking_lot_core", ] [[package]] -name = "quinn-udp" -version = "0.5.14" +name = "parking_lot_core" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ - "cfg_aliases", + "cfg-if", "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", + "redox_syscall", + "smallvec", + "windows-link", ] [[package]] -name = "quote" -version = "1.0.41" +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ - "proc-macro2", + "base64", + "indexmap", + "quick-xml", + "serde", + "time", ] [[package]] -name = "r-efi" -version = "5.3.0" +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] -name = "rand" -version = "0.9.2" +name = "proc-macro2" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ - "rand_chacha", - "rand_core", + "unicode-ident", ] [[package]] -name = "rand_chacha" -version = "0.9.0" +name = "quick-xml" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ - "ppv-lite86", - "rand_core", + "memchr", ] [[package]] -name = "rand_core" -version = "0.9.3" +name = "quote" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ - "getrandom 0.3.3", + "proc-macro2", ] [[package]] name = "rangemap" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" [[package]] name = "ratatui" @@ -1617,11 +1349,11 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5659e52e4ba6e07b2dad9f1158f578ef84a73762625ddb51536019f34d180eb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "cassowary", "crossterm 0.27.0", "indoc", - "itertools", + "itertools 0.12.1", "lru", "paste", "stability", @@ -1636,14 +1368,14 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -1653,9 +1385,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1664,62 +1396,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "reqwest" -version = "0.12.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" -dependencies = [ - "base64", - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-native-certs", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted", - "windows-sys 0.52.0", -] +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rtnetlink" @@ -1733,17 +1412,11 @@ dependencies = [ "netlink-packet-route", "netlink-proto", "netlink-sys", - "nix", + "nix 0.29.0", "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 = "rustc-hash" version = "2.1.1" @@ -1752,64 +1425,17 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", "windows-sys 0.61.2", ] -[[package]] -name = "rustls" -version = "0.23.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pki-types" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustversion" version = "1.0.22" @@ -1818,18 +1444,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "scopeguard" @@ -1837,29 +1454,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "security-framework" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" -dependencies = [ - "bitflags 2.9.4", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "serde" version = "1.0.228" @@ -1887,32 +1481,20 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", + "zmij", ] [[package]] @@ -1955,9 +1537,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio 0.8.11", @@ -1966,18 +1548,19 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -1987,12 +1570,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2005,12 +1588,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - [[package]] name = "strsim" version = "0.11.1" @@ -2036,15 +1613,9 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.106", + "syn 2.0.117", ] -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "syn" version = "1.0.109" @@ -2058,56 +1629,15 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[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.106", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags 2.9.4", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "termtree" version = "0.5.1" @@ -2125,11 +1655,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -2140,18 +1670,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -2165,149 +1695,53 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", ] -[[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.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -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" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ - "backtrace", - "bytes", - "io-uring", "libc", - "mio 1.0.4", + "mio 1.1.1", "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.106", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags 2.9.4", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", + "windows-sys 0.61.2", ] -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -2316,20 +1750,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -2348,9 +1782,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "chrono", "nu-ansi-term", @@ -2362,17 +1796,11 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" @@ -2398,36 +1826,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - [[package]] name = "unty" version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[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" @@ -2446,44 +1850,17 @@ version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -2492,38 +1869,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2531,55 +1881,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] -[[package]] -name = "web-sys" -version = "0.3.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "winapi" version = "0.3.9" @@ -2610,7 +1931,7 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.1", + "windows-link", "windows-result", "windows-strings", ] @@ -2623,7 +1944,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -2634,15 +1955,9 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" @@ -2655,7 +1970,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -2664,7 +1979,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -2676,15 +1991,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.59.0" @@ -2709,7 +2015,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -2749,7 +2055,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.2.1", + "windows-link", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -2899,117 +2205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[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" +name = "zmij" +version = "1.0.21" 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.106", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[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.106", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - -[[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.106", -] +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 10c61d7..b42bef6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nifa" -version = "0.4.0" +version = "0.5.0" edition = "2024" authors = ["shellrow "] description = "Cross-platform network inspection tool" @@ -21,11 +21,9 @@ serde = { version = "1", features = ["derive"] } serde_json = { version = "1" } serde_yaml = { version = "0.9" } mac-addr = { version = "0.3.0" } -netdev = { version = "0.39", features = ["serde"] } -netroute = { version = "0.3", features = ["serde"] } -netsock = { version = "0.5", features = ["serde"] } -tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] } -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "rustls-tls-native-roots"] } +netdev = { version = "0.40", features = ["serde"] } +netroute = { version = "0.4", features = ["serde"] } +netsock = { version = "0.6", features = ["serde"] } clap = { version = "4.5", features = ["derive", "cargo"] } termtree = { version = "0.5" } hostname = { version = "0.4" } @@ -34,7 +32,6 @@ ndb-oui = { version = "0.4", features = ["bundled"] } ratatui = "0.25" crossterm = "0.27" humansize = "2.1" -url = "2.5" comfy-table = "7.2" #home = { version = "0.5" } diff --git a/README.md b/README.md index aab3e26..77b40a2 100644 --- a/README.md +++ b/README.md @@ -3,18 +3,22 @@ [license-badge]: https://img.shields.io/crates/l/nifa.svg # nifa [![Crates.io][crates-badge]][crates-url] ![License][license-badge] -Cross-platform network inspection tool - a modern, read-only alternative to classic network commands. - -## Features -- List all network interfaces with detailed information -- Inspect a specific interface with full metadata (addresses, DNS, gateway, speeds, flags, stats) -- View routing tables (IPv4/IPv6) -- View neighbor table (ARP/NDP) with optional vendor (OUI) lookup -- Inspect open TCP/UDP sockets with process association -- Monitor live per-interface traffic statistics in a TUI -- Fetch your public IPv4/IPv6 -- Display OS, kernel, proxy, permission capabilities, and default interface info -- Export view as JSON/YAML for automation + +Cross-platform network inspection tool. + +It is designed for Linux, macOS, and Windows with a consistent command surface: + +```bash +nifa [options] +``` + +With no sub-command (`nifa`), it shows the primary network interface summary. + +## Principles + +- Read-only only: no mutation commands +- Tree-first output (`tree` default) +- Structured output for automation (`json` / `yaml`) ## Supported Platforms - **Linux** @@ -44,29 +48,67 @@ You can download precompiled binaries from the [releases](https://github.com/she cargo install nifa ``` -## Usage +## Commands + +```text +if Network interfaces +addr IP addresses +link Layer 2 information +route Routing table +neigh ARP / NDP entries +sock Sockets (ss/netstat equivalent) +sys Network/system summary +mon TUI monitor ``` -Usage: nifa [OPTIONS] [COMMAND] - -Commands: - ifaces Show all interfaces - iface Show details for specified interface - monitor Monitor traffic statistics for interfaces in TUI - route Show routing tables (IPv4/IPv6) - neigh Show neighbor table (ARP/NDP) - socket Show open TCP/UDP sockets and associated processes - public Show public IP information - system Show OS / kernel / proxy / default interface - help Print this message or the help of the given subcommand(s) - -Options: - -l, --log-level Set log level [default: error] [possible values: error, warn, info, debug, trace] - -h, --help Print help - -V, --version Print version + +## Output Options + +All non-TUI commands support: + +```text +--format tree|table|json|yaml +--wide +--no-color +--no-truncate ``` -See `nifa -h` for more detail. +### Format behavior + +- Default: `tree` +- Use `--format table` for tabular output -## Note for Developers -If you are looking for a Rust library for network interface, -consider using [netdev](https://github.com/shellrow/netdev). +## Examples + +```bash +# Interfaces (summary) +nifa if + +# Interface details +nifa if show en0 + +# Addresses +nifa addr +nifa addr --iface en0 --ipv6 + +# Link layer +nifa link --up + +# Routes +nifa route --default +nifa route --ipv4 --detail + +# Neighbors +nifa neigh --ipv4 --vendor + +# Sockets +nifa sock --proto tcp --listen +nifa sock --proto tcp --established --pid + +# System summary +nifa sys +nifa sys --dns +nifa sys --proxy + +# TUI monitor +nifa mon --iface en0 --interval 1 --sort total +``` diff --git a/dist-workspace.toml b/dist-workspace.toml index 5a17490..f54d589 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -4,7 +4,7 @@ members = ["cargo:."] # Config for 'dist' [dist] # The preferred dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.28.7" +cargo-dist-version = "0.30.3" # CI backends to support ci = "github" # The installers to generate for each app diff --git a/src/cli.rs b/src/cli.rs index 544e65e..bcce525 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,12 +1,15 @@ -use std::path::PathBuf; - use clap::{Args, Parser, Subcommand, ValueEnum}; use crate::cmd::monitor::{SortKey, Unit}; /// nifa - Cross-platform network inspection tool #[derive(Debug, Parser)] -#[command(name = "nifa", author, version, about = "nifa - Cross-platform network inspection tool", long_about = None)] +#[command( + name = "nifa", + author, + version, + about = "Cross-platform network inspection tool" +)] pub struct Cli { /// Set log level #[arg(short = 'l', long, value_enum, default_value_t = LogLevel::Error)] @@ -45,38 +48,53 @@ pub enum OutputFormat { Yaml, } -#[derive(Debug, Clone, Copy, ValueEnum)] -pub enum ExportFormat { - Json, - Yaml, +#[derive(Args, Debug, Clone)] +pub struct OutputArgs { + /// Output format + #[arg(long, value_enum, default_value_t = OutputFormat::Tree)] + pub format: OutputFormat, + /// Prefer wider table layout + #[arg(long, default_value_t = false)] + pub wide: bool, + /// Disable colored output + #[arg(long, default_value_t = false)] + pub no_color: bool, + /// Disable truncation in compact output + #[arg(long, default_value_t = false)] + pub no_truncate: bool, } #[derive(Debug, Subcommand)] pub enum Command { - /// Show all interfaces - Ifaces(IfacesArgs), - /// Show details for specified interface - Iface(IfaceArgs), - /// Monitor traffic statistics for interfaces in TUI - Monitor(MonitorArgs), - /// Show routing tables (IPv4/IPv6) + /// Network interfaces + #[command(name = "if")] + If(IfArgs), + /// IP addresses + Addr(AddrArgs), + /// Layer 2 information + Link(LinkArgs), + /// Routing table Route(RouteArgs), - /// Show neighbor table (ARP/NDP) + /// ARP/NDP entries Neigh(NeighArgs), - /// Show open TCP/UDP sockets and associated processes - Socket(SocketArgs), - /// Show public IP information - Public(PublicArgs), - /// Show OS / kernel / proxy / default interface - System(SystemArgs), + /// Sockets (ss/netstat equivalent) + Sock(SockArgs), + /// Network/system summary + Sys(SysArgs), + /// TUI monitor + Mon(MonArgs), +} + +#[derive(Debug, Subcommand)] +pub enum ShowAction { + /// Show details for a specific target + Show { target: String }, } -/// Ifaces command arguments #[derive(Args, Debug)] -pub struct IfacesArgs { - /// Filter by name (supports partial match) - #[arg(long)] - pub name_like: Option, +pub struct IfArgs { + #[command(subcommand)] + pub action: Option, /// Show UP status interfaces only #[arg(long, conflicts_with = "down")] pub up: bool, @@ -95,126 +113,79 @@ pub struct IfacesArgs { /// Show interfaces with IPv6 address only #[arg(long)] pub ipv6: bool, - /// Output format - #[arg(short='f', long, value_enum, default_value_t = OutputFormat::Tree)] - pub format: OutputFormat, - /// Export data instead of printing to stdout - #[arg(long, value_enum)] - pub export: Option, - /// Output file for export - #[arg(short = 'o', long)] - pub output: Option, - /// With vendor info (OUI lookup) - #[arg(long, default_value_t = false)] - pub vendor: bool, -} - -/// Iface command arguments -#[derive(Args, Debug)] -pub struct IfaceArgs { - /// Show details for specified interface - pub iface: String, - /// Output format - #[arg(short='f', long, value_enum, default_value_t = OutputFormat::Tree)] - pub format: OutputFormat, - /// Export data instead of printing to stdout - #[arg(long, value_enum)] - pub export: Option, - /// Output file for export - #[arg(short = 'o', long)] - pub output: Option, - /// With vendor info (OUI lookup) + /// Resolve vendor info using OUI DB #[arg(long, default_value_t = false)] pub vendor: bool, + #[command(flatten)] + pub out: OutputArgs, } -/// Monitor command arguments #[derive(Args, Debug)] -pub struct MonitorArgs { - /// Target interface (default: all) - #[arg(short, long)] +pub struct AddrArgs { + #[command(subcommand)] + pub action: Option, + /// Filter by interface name + #[arg(long)] pub iface: Option, - /// Sort key - #[arg(short='s', long, value_enum, default_value_t=SortKey::Total)] - pub sort: SortKey, - /// Monitor interval in seconds - #[arg(short = 'd', long, default_value = "1")] - pub interval: u64, - /// Display unit (bytes or bits) - #[arg(long, value_enum, default_value_t=Unit::default())] - pub unit: Unit, -} - -/// System command arguments -#[derive(Args, Debug)] -pub struct SystemArgs { - /// Output format - #[arg(short='f', long, value_enum, default_value_t = OutputFormat::Tree)] - pub format: OutputFormat, - /// Export data instead of printing to stdout - #[arg(long, value_enum)] - pub export: Option, - /// Output file for export - #[arg(short = 'o', long)] - pub output: Option, + /// Show IPv4 only + #[arg(long, conflicts_with = "ipv6")] + pub ipv4: bool, + /// Show IPv6 only + #[arg(long)] + pub ipv6: bool, + #[command(flatten)] + pub out: OutputArgs, } #[derive(Args, Debug)] -pub struct PublicArgs { - /// IPv4 only +pub struct LinkArgs { + /// Filter by interface name #[arg(long)] - pub ipv4: bool, - /// Timeout seconds - #[arg(long, default_value_t = 3)] - pub timeout: u64, - /// Output format - #[arg(short='f', long, value_enum, default_value_t = OutputFormat::Tree)] - pub format: OutputFormat, - /// Export data instead of printing to stdout - #[arg(long, value_enum)] - pub export: Option, - /// Output file for export - #[arg(short = 'o', long)] - pub output: Option, -} - -#[derive(clap::ValueEnum, Clone, Copy, Debug)] -pub enum RouteFamilyOpt { - All, - Ipv4, - Ipv6, + pub iface: Option, + /// Show UP status interfaces only + #[arg(long, conflicts_with = "down")] + pub up: bool, + /// Show DOWN status interfaces only + #[arg(long)] + pub down: bool, + #[command(flatten)] + pub out: OutputArgs, } #[derive(Args, Debug)] pub struct RouteArgs { - /// Family filter - #[arg(long, value_enum, default_value_t = RouteFamilyOpt::All)] - pub family: RouteFamilyOpt, - /// Output format - #[arg(short='f', long, value_enum, default_value_t = OutputFormat::Tree)] - pub format: OutputFormat, - /// Export data instead of printing to stdout - #[arg(long, value_enum)] - pub export: Option, - /// Output file for export - #[arg(short = 'o', long)] - pub output: Option, + /// Show IPv4 routes only + #[arg(long, conflicts_with = "ipv6")] + pub ipv4: bool, + /// Show IPv6 routes only + #[arg(long)] + pub ipv6: bool, + /// Show default routes only + #[arg(long)] + pub default: bool, + /// Show detailed route metadata + #[arg(long, default_value_t = false)] + pub detail: bool, + #[command(flatten)] + pub out: OutputArgs, } #[derive(Args, Debug)] pub struct NeighArgs { - /// Output format - #[arg(short='f', long, value_enum, default_value_t = OutputFormat::Tree)] - pub format: OutputFormat, - /// Export data instead of printing to stdout - #[arg(long, value_enum)] - pub export: Option, - /// Output file for export - #[arg(short = 'o', long)] - pub output: Option, - /// With vendor info (OUI lookup) + /// Filter by interface name (best-effort by platform support) + #[arg(long)] + pub iface: Option, + /// Show IPv4 only + #[arg(long, conflicts_with = "ipv6")] + pub ipv4: bool, + /// Show IPv6 only + #[arg(long)] + pub ipv6: bool, + /// Resolve vendor info using OUI DB #[arg(long, default_value_t = false)] pub vendor: bool, + #[command(flatten)] + pub out: OutputArgs, } #[derive(Debug, Clone, Copy, ValueEnum)] @@ -232,29 +203,56 @@ pub enum SocketFamily { } #[derive(Args, Debug)] -pub struct SocketArgs { +pub struct SockArgs { /// Protocol filter #[arg(long, value_enum, default_value = "all")] pub proto: SocketProto, /// Address family filter #[arg(long, value_enum, default_value = "all")] pub family: SocketFamily, - /// TCP state filter (established, listen, time_wait, all) + /// Show listening sockets only + #[arg(long, conflicts_with = "established")] + pub listen: bool, + /// Show established sockets only #[arg(long)] - pub state: Option, + pub established: bool, /// Filter by local or remote port #[arg(long)] pub port: Option, - /// Filter by PID + /// Include process ownership details + #[arg(long, default_value_t = false)] + pub pid: bool, + #[command(flatten)] + pub out: OutputArgs, +} + +#[derive(Args, Debug)] +pub struct SysArgs { + /// Emphasize DNS details + #[arg(long, default_value_t = false)] + pub dns: bool, + /// Emphasize proxy details + #[arg(long, default_value_t = false)] + pub proxy: bool, + /// Show summary-only output + #[arg(long, default_value_t = false)] + pub summary: bool, + #[command(flatten)] + pub out: OutputArgs, +} + +#[derive(Args, Debug)] +pub struct MonArgs { + /// Target interface (default: all) #[arg(long)] - pub pid: Option, - /// Output format - #[arg(short='f', long, value_enum, default_value_t = OutputFormat::Tree)] - pub format: OutputFormat, - /// Export data instead of printing to stdout - #[arg(long, value_enum)] - pub export: Option, - /// Output file for export - #[arg(short = 'o', long)] - pub output: Option, + pub iface: Option, + /// Monitor interval in seconds + #[arg(long, default_value = "1")] + pub interval: u64, + /// Sort key + #[arg(long, value_enum, default_value_t = SortKey::Total)] + pub sort: SortKey, + /// Display unit (bytes or bits) + #[arg(long, value_enum, default_value_t = Unit::default())] + pub unit: Unit, } diff --git a/src/cmd/addr.rs b/src/cmd/addr.rs new file mode 100644 index 0000000..18431bd --- /dev/null +++ b/src/cmd/addr.rs @@ -0,0 +1,35 @@ +use anyhow::Result; + +use crate::cli::{AddrArgs, ShowAction}; +use crate::model::IpAddrEntry; + +pub fn run(args: &AddrArgs) -> Result<()> { + let mut interfaces = crate::net::iface::get_all_interfaces(); + if let Some(iface_name) = &args.iface { + interfaces.retain(|i| &i.name == iface_name); + } + if let Some(ShowAction::Show { target }) = &args.action { + interfaces.retain(|i| &i.name == target); + } + + let mut entries: Vec = interfaces + .iter() + .flat_map(super::common::interface_to_ip_entries) + .collect(); + + if args.ipv4 { + entries.retain(|e| e.family == "ipv4"); + } + if args.ipv6 { + entries.retain(|e| e.family == "ipv6"); + } + + entries.sort_by(|a, b| { + a.iface + .cmp(&b.iface) + .then(a.family.cmp(&b.family)) + .then(a.address.cmp(&b.address)) + }); + + crate::renderer::render_ip_entries(&entries, &args.out) +} diff --git a/src/cmd/common.rs b/src/cmd/common.rs new file mode 100644 index 0000000..92a412a --- /dev/null +++ b/src/cmd/common.rs @@ -0,0 +1,136 @@ +use netdev::Interface; + +use crate::model::{IfDetail, IfSummary, IpAddrEntry}; +use crate::renderer::{fmt_bps, fmt_flags}; + +pub fn vendor_name_for(mac: &Option) -> Option { + let mac = mac.as_ref()?; + if !crate::db::oui::is_oui_db_initialized() || *mac == mac_addr::MacAddr::zero() { + return None; + } + let oui_db = crate::db::oui::oui_db(); + oui_db + .lookup_mac(mac) + .map(|v| v.vendor_detail.as_deref().unwrap_or(&v.vendor).to_string()) +} + +pub fn interface_to_summary(iface: &Interface, with_vendor: bool) -> IfSummary { + let (gateway_mac, gateway_ipv4, gateway_ipv6) = if let Some(gw) = &iface.gateway { + let mac = if gw.mac_addr == mac_addr::MacAddr::zero() { + None + } else { + Some(gw.mac_addr.to_string()) + }; + let ipv4: Vec = gw + .ipv4 + .iter() + .filter(|ip| !ip.is_unspecified()) + .map(|ip| ip.to_string()) + .collect(); + let ipv6: Vec = gw + .ipv6 + .iter() + .filter(|ip| { + !ip.is_unspecified() && **ip != std::net::Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0) + }) + .map(|ip| ip.to_string()) + .collect(); + if mac.is_some() || !ipv4.is_empty() || !ipv6.is_empty() { + (mac, ipv4, ipv6) + } else { + (None, Vec::new(), Vec::new()) + } + } else { + (None, Vec::new(), Vec::new()) + }; + + IfSummary { + index: iface.index, + name: iface.name.clone(), + if_type: iface.if_type.name().to_string(), + state: iface.oper_state.to_string(), + is_default: iface.default, + mac: iface.mac_addr.map(|m| m.to_string()), + mtu: iface.mtu, + ipv4_addrs: iface.ipv4.iter().map(|n| n.to_string()).collect(), + ipv6_addrs: iface.ipv6.iter().map(|n| n.to_string()).collect(), + gateway_mac, + gateway_ipv4, + gateway_ipv6, + vendor: if with_vendor { + vendor_name_for(&iface.mac_addr) + } else { + None + }, + } +} + +pub fn interface_to_detail(iface: &Interface, with_vendor: bool) -> IfDetail { + let gateway_mac = iface.gateway.as_ref().map(|g| g.mac_addr.to_string()); + let gateway_ipv4 = iface + .gateway + .as_ref() + .map(|g| g.ipv4.iter().map(|ip| ip.to_string()).collect::>()) + .unwrap_or_default(); + let gateway_ipv6 = iface + .gateway + .as_ref() + .map(|g| g.ipv6.iter().map(|ip| ip.to_string()).collect::>()) + .unwrap_or_default(); + let vpn_heuristic = crate::net::iface::detect_vpn_like(iface); + + IfDetail { + summary: interface_to_summary(iface, with_vendor), + friendly_name: iface.friendly_name.clone(), + description: iface.description.clone(), + flags: fmt_flags(iface.flags), + tx_speed: iface.transmit_speed.map(fmt_bps), + rx_speed: iface.receive_speed.map(fmt_bps), + ipv4: iface.ipv4.iter().map(|n| n.to_string()).collect(), + ipv6: iface.ipv6.iter().map(|n| n.to_string()).collect(), + ipv6_scoped: iface + .ipv6 + .iter() + .enumerate() + .map(|(idx, n)| { + if let Some(scope) = iface.ipv6_scope_ids.get(idx) { + format!("{} (scope_id={scope})", n) + } else { + n.to_string() + } + }) + .collect(), + dns_servers: iface.dns_servers.iter().map(|d| d.to_string()).collect(), + gateway_ipv4, + gateway_ipv6, + gateway_mac, + stats_rx_bytes: iface.stats.as_ref().map(|s| s.rx_bytes), + stats_tx_bytes: iface.stats.as_ref().map(|s| s.tx_bytes), + vpn_like: vpn_heuristic.is_vpn_like, + } +} + +pub fn interface_to_ip_entries(iface: &Interface) -> Vec { + let mut out = Vec::with_capacity(iface.ipv4.len() + iface.ipv6.len()); + for net in &iface.ipv4 { + out.push(IpAddrEntry { + iface: iface.name.clone(), + if_type: iface.if_type.name().to_string(), + family: "ipv4".to_string(), + address: net.addr().to_string(), + prefix_len: net.prefix_len(), + scope_id: None, + }); + } + for (idx, net) in iface.ipv6.iter().enumerate() { + out.push(IpAddrEntry { + iface: iface.name.clone(), + if_type: iface.if_type.name().to_string(), + family: "ipv6".to_string(), + address: net.addr().to_string(), + prefix_len: net.prefix_len(), + scope_id: iface.ipv6_scope_ids.get(idx).copied(), + }); + } + out +} diff --git a/src/cmd/iface.rs b/src/cmd/iface.rs deleted file mode 100644 index 72b4d7a..0000000 --- a/src/cmd/iface.rs +++ /dev/null @@ -1,315 +0,0 @@ -use crate::cli::Cli; -use crate::cli::IfaceArgs; -use crate::db::oui::is_oui_db_initialized; -use crate::net; -use crate::renderer; -use crate::renderer::fmt_bps; -use crate::renderer::fmt_flags; -use crate::renderer::tree::tree_label; -use anyhow::Result; -use mac_addr::MacAddr; -use netdev::Interface; -use termtree::Tree; - -/// Default action with no subcommand -pub fn show_default_interface(_cli: &Cli) -> Result<()> { - let iface: Interface = net::iface::get_default_interface() - .ok_or_else(|| anyhow::anyhow!("No default interface found"))?; - // Render output - print_default_interface_tree(&iface); - - // Print note about nifa help - println!(); - println!("Tip: Run 'nifa --help' to see available commands and options."); - Ok(()) -} - -/// Show specified interface details -pub fn show_interface(_cli: &Cli, args: &IfaceArgs) -> Result<()> { - if args.vendor { - crate::db::oui::init_oui_db()?; - } - match net::iface::get_interface_by_name(&args.iface) { - Some(iface) => { - match args.export { - Some(export_format) => { - // Export to file in specified format - crate::fs::export(export_format, args.output.as_deref(), &iface)?; - return Ok(()); - } - None => { - // Render output - match args.format { - crate::cli::OutputFormat::Tree => print_interface_detail_tree(&iface), - crate::cli::OutputFormat::Json => { - renderer::json::print_interface_json(&[iface]) - } - crate::cli::OutputFormat::Yaml => { - renderer::yaml::print_interface_yaml(&[iface]) - } - _ => { - tracing::error!( - "Unsupported format for show interface: {:?}", - args.format - ); - } - } - } - } - } - None => { - tracing::error!("Interface '{}' not found", args.iface); - } - } - Ok(()) -} - -/// Print detailed information of a single interface in a tree structure. -fn print_default_interface_tree(iface: &Interface) { - let host = crate::net::sys::hostname(); - let title = format!("Default Network Interface on {}", host); - let mut root = Tree::new(tree_label(title)); - - // flat fields (no General section) - root.push(Tree::new(format!("Index: {}", iface.index))); - root.push(Tree::new(format!("Name: {}", iface.name))); - - if let Some(fn_name) = &iface.friendly_name { - root.push(Tree::new(format!("Friendly Name: {}", fn_name))); - } - if let Some(desc) = &iface.description { - root.push(Tree::new(format!("Description: {}", desc))); - } - - root.push(Tree::new(format!("Type: {:?}", iface.if_type))); - root.push(Tree::new(format!("State: {:?}", iface.oper_state))); - - if let Some(mac) = &iface.mac_addr { - root.push(Tree::new(format!("MAC: {}", mac))); - - if is_oui_db_initialized() && *mac != MacAddr::zero() { - let oui_db = crate::db::oui::oui_db(); - if let Some(vendor) = oui_db.lookup_mac(mac) { - let vendor_name = vendor.vendor_detail.as_deref().unwrap_or(&vendor.vendor); - root.push(Tree::new(format!("Vendor: {}", vendor_name))); - } - } - } - - if let Some(mtu) = iface.mtu { - root.push(Tree::new(format!("MTU: {}", mtu))); - } - - // link speeds (humanized bps) - if iface.transmit_speed.is_some() || iface.receive_speed.is_some() { - let mut speed = Tree::new(tree_label("Link Speed")); - if let Some(tx) = iface.transmit_speed { - speed.push(Tree::new(format!("TX: {}", fmt_bps(tx)))); - } - if let Some(rx) = iface.receive_speed { - speed.push(Tree::new(format!("RX: {}", fmt_bps(rx)))); - } - root.push(speed); - } - - // flags - root.push(Tree::new(format!("Flags: {}", fmt_flags(iface.flags)))); - - // ---- Addresses ---- - if !iface.ipv4.is_empty() { - let mut ipv4_tree = Tree::new(tree_label("IPv4")); - for net in &iface.ipv4 { - ipv4_tree.push(Tree::new(net.to_string())); - } - root.push(ipv4_tree); - } - - if !iface.ipv6.is_empty() { - let mut ipv6_tree = Tree::new(tree_label("IPv6")); - for (i, net) in iface.ipv6.iter().enumerate() { - let mut label = net.to_string(); - if let Some(scope) = iface.ipv6_scope_ids.get(i) { - label.push_str(&format!(" (scope_id={})", scope)); - } - ipv6_tree.push(Tree::new(label)); - } - root.push(ipv6_tree); - } - - // ---- DNS ---- - if !iface.dns_servers.is_empty() { - let mut dns_tree = Tree::new(tree_label("DNS")); - for dns in &iface.dns_servers { - dns_tree.push(Tree::new(dns.to_string())); - } - root.push(dns_tree); - } - - // ---- Gateway ---- - if let Some(gw) = &iface.gateway { - let mut gw_node = Tree::new(tree_label("Gateway")); - gw_node.push(Tree::new(format!("MAC: {}", gw.mac_addr))); - if !gw.ipv4.is_empty() { - let mut gw4 = Tree::new(tree_label("IPv4")); - for ip in &gw.ipv4 { - gw4.push(Tree::new(ip.to_string())); - } - gw_node.push(gw4); - } - if !gw.ipv6.is_empty() { - let mut gw6 = Tree::new(tree_label("IPv6")); - for ip in &gw.ipv6 { - gw6.push(Tree::new(ip.to_string())); - } - gw_node.push(gw6); - } - root.push(gw_node); - } - - // ---- Statistics (snapshot) ---- - if let Some(st) = &iface.stats { - let mut stats_node = Tree::new(tree_label("Statistics (snapshot)")); - stats_node.push(Tree::new(format!("RX bytes: {}", st.rx_bytes))); - stats_node.push(Tree::new(format!("TX bytes: {}", st.tx_bytes))); - root.push(stats_node); - } - - let vpn_heuristic = crate::net::iface::detect_vpn_like(&iface); - if vpn_heuristic.is_vpn_like { - let mut heuristic_node = Tree::new(tree_label("Heuristic")); - heuristic_node.push(Tree::new(format!( - "VPN-like: {}", - vpn_heuristic.is_vpn_like - ))); - root.push(heuristic_node); - } - - println!("{}", root); -} - -/// Print detailed information of a single interface in a tree structure. -fn print_interface_detail_tree(iface: &Interface) { - let host = crate::net::sys::hostname(); - let title = format!( - "{}{} on {}", - iface.name, - if iface.default { " (default)" } else { "" }, - host - ); - let mut root = Tree::new(tree_label(title)); - - // flat fields (no General section) - root.push(Tree::new(format!("Index: {}", iface.index))); - - if let Some(fn_name) = &iface.friendly_name { - root.push(Tree::new(format!("Friendly Name: {}", fn_name))); - } - if let Some(desc) = &iface.description { - root.push(Tree::new(format!("Description: {}", desc))); - } - - root.push(Tree::new(format!("Type: {:?}", iface.if_type))); - root.push(Tree::new(format!("State: {:?}", iface.oper_state))); - - if let Some(mac) = &iface.mac_addr { - root.push(Tree::new(format!("MAC: {}", mac))); - - if is_oui_db_initialized() && *mac != MacAddr::zero() { - let oui_db = crate::db::oui::oui_db(); - if let Some(vendor) = oui_db.lookup_mac(mac) { - let vendor_name = vendor.vendor_detail.as_deref().unwrap_or(&vendor.vendor); - root.push(Tree::new(format!("Vendor: {}", vendor_name))); - } - } - } - - if let Some(mtu) = iface.mtu { - root.push(Tree::new(format!("MTU: {}", mtu))); - } - - // link speeds (humanized bps) - if iface.transmit_speed.is_some() || iface.receive_speed.is_some() { - let mut speed = Tree::new(tree_label("Link Speed")); - if let Some(tx) = iface.transmit_speed { - speed.push(Tree::new(format!("TX: {}", fmt_bps(tx)))); - } - if let Some(rx) = iface.receive_speed { - speed.push(Tree::new(format!("RX: {}", fmt_bps(rx)))); - } - root.push(speed); - } - - // flags - root.push(Tree::new(format!("Flags: {}", fmt_flags(iface.flags)))); - - // ---- Addresses ---- - if !iface.ipv4.is_empty() { - let mut ipv4_tree = Tree::new(tree_label("IPv4")); - for net in &iface.ipv4 { - ipv4_tree.push(Tree::new(net.to_string())); - } - root.push(ipv4_tree); - } - - if !iface.ipv6.is_empty() { - let mut ipv6_tree = Tree::new(tree_label("IPv6")); - for (i, net) in iface.ipv6.iter().enumerate() { - let mut label = net.to_string(); - if let Some(scope) = iface.ipv6_scope_ids.get(i) { - label.push_str(&format!(" (scope_id={})", scope)); - } - ipv6_tree.push(Tree::new(label)); - } - root.push(ipv6_tree); - } - - // ---- DNS ---- - if !iface.dns_servers.is_empty() { - let mut dns_tree = Tree::new(tree_label("DNS")); - for dns in &iface.dns_servers { - dns_tree.push(Tree::new(dns.to_string())); - } - root.push(dns_tree); - } - - // ---- Gateway ---- - if let Some(gw) = &iface.gateway { - let mut gw_node = Tree::new(tree_label("Gateway")); - gw_node.push(Tree::new(format!("MAC: {}", gw.mac_addr))); - if !gw.ipv4.is_empty() { - let mut gw4 = Tree::new(tree_label("IPv4")); - for ip in &gw.ipv4 { - gw4.push(Tree::new(ip.to_string())); - } - gw_node.push(gw4); - } - if !gw.ipv6.is_empty() { - let mut gw6 = Tree::new(tree_label("IPv6")); - for ip in &gw.ipv6 { - gw6.push(Tree::new(ip.to_string())); - } - gw_node.push(gw6); - } - root.push(gw_node); - } - - // ---- Statistics (snapshot) ---- - if let Some(st) = &iface.stats { - let mut stats_node = Tree::new(tree_label("Statistics (snapshot)")); - stats_node.push(Tree::new(format!("RX bytes: {}", st.rx_bytes))); - stats_node.push(Tree::new(format!("TX bytes: {}", st.tx_bytes))); - root.push(stats_node); - } - - let vpn_heuristic = crate::net::iface::detect_vpn_like(&iface); - if vpn_heuristic.is_vpn_like { - let mut heuristic_node = Tree::new(tree_label("Heuristic")); - heuristic_node.push(Tree::new(format!( - "VPN-like: {}", - vpn_heuristic.is_vpn_like - ))); - root.push(heuristic_node); - } - - println!("{}", root); -} diff --git a/src/cmd/ifaces.rs b/src/cmd/ifaces.rs deleted file mode 100644 index 02ef798..0000000 --- a/src/cmd/ifaces.rs +++ /dev/null @@ -1,214 +0,0 @@ -use crate::cli::Cli; -use crate::cli::IfacesArgs; -use crate::db::oui::is_oui_db_initialized; -use crate::net; -use crate::renderer; -use crate::renderer::table::make_table; -use crate::renderer::tree::tree_label; -use anyhow::Result; -use mac_addr::MacAddr; -use netdev::Interface; -use netdev::interface::state::OperState; -use termtree::Tree; - -pub fn list_interfaces(_cli: &Cli, args: &IfacesArgs) -> Result<()> { - if args.vendor { - crate::db::oui::init_oui_db()?; - } - - let mut interfaces: Vec = net::iface::get_all_interfaces(); - - // Apply filters - if let Some(name_like) = &args.name_like { - interfaces.retain(|iface| iface.name.contains(name_like)); - } - if args.up { - interfaces.retain(|iface| iface.oper_state == OperState::Up); - } - if args.down { - interfaces.retain(|iface| iface.oper_state == OperState::Down); - } - if args.phy { - interfaces.retain(|iface| iface.is_physical()); - } - if args.virt { - interfaces.retain(|iface| !iface.is_physical()); - } - if args.ipv4 { - interfaces.retain(|iface| !iface.ipv4.is_empty()); - } - if args.ipv6 { - interfaces.retain(|iface| !iface.ipv6.is_empty()); - } - - match args.export { - Some(export_format) => { - // Export to file in specified format - crate::fs::export(export_format, args.output.as_deref(), &interfaces)?; - return Ok(()); - } - None => { - // Render output - match args.format { - crate::cli::OutputFormat::Tree => print_interface_tree(&interfaces), - crate::cli::OutputFormat::Json => renderer::json::print_interface_json(&interfaces), - crate::cli::OutputFormat::Yaml => renderer::yaml::print_interface_yaml(&interfaces), - crate::cli::OutputFormat::Table => print_interface_table(&interfaces), - } - } - } - Ok(()) -} - -/// Print the network interfaces in a tree structure. -fn print_interface_tree(ifaces: &[Interface]) { - let default: bool = if ifaces.len() == 1 { - ifaces[0].default - } else { - false - }; - let host = crate::net::sys::hostname(); - let mut root = if default { - Tree::new(tree_label(format!("Default Interface on {}", host))) - } else { - Tree::new(tree_label(format!("Interfaces on {}", host))) - }; - for iface in ifaces { - let mut node = Tree::new(format!( - "{}{}", - iface.name, - if iface.default { " (default)" } else { "" } - )); - - node.push(Tree::new(format!("Index: {}", iface.index))); - - if let Some(fn_name) = &iface.friendly_name { - node.push(Tree::new(format!("Friendly Name: {}", fn_name))); - } - if let Some(desc) = &iface.description { - node.push(Tree::new(format!("Description: {}", desc))); - } - - node.push(Tree::new(format!("Type: {:?}", iface.if_type))); - node.push(Tree::new(format!("State: {:?}", iface.oper_state))); - if let Some(mac) = &iface.mac_addr { - node.push(Tree::new(format!("MAC: {}", mac))); - - if is_oui_db_initialized() && *mac != MacAddr::zero() { - let oui_db = crate::db::oui::oui_db(); - if let Some(vendor) = oui_db.lookup_mac(mac) { - let vendor_name = vendor.vendor_detail.as_deref().unwrap_or(&vendor.vendor); - node.push(Tree::new(format!("Vendor: {}", vendor_name))); - } - } - } - - if let Some(mtu) = iface.mtu { - node.push(Tree::new(format!("MTU: {}", mtu))); - } - - if !iface.ipv4.is_empty() { - let mut ipv4_tree = Tree::new(tree_label("IPv4")); - for net in &iface.ipv4 { - ipv4_tree.push(Tree::new(net.to_string())); - } - node.push(ipv4_tree); - } - - if !iface.ipv6.is_empty() { - let mut ipv6_tree = Tree::new(tree_label("IPv6")); - for (i, net) in iface.ipv6.iter().enumerate() { - let mut label = net.to_string(); - if let Some(scope) = iface.ipv6_scope_ids.get(i) { - label.push_str(&format!(" (scope_id={})", scope)); - } - ipv6_tree.push(Tree::new(label)); - } - node.push(ipv6_tree); - } - - if !iface.dns_servers.is_empty() { - let mut dns_tree = Tree::new(tree_label("DNS")); - for dns in &iface.dns_servers { - dns_tree.push(Tree::new(dns.to_string())); - } - node.push(dns_tree); - } - - if let Some(gw) = &iface.gateway { - let mut gw_node = Tree::new(tree_label("Gateway")); - // GW MAC - gw_node.push(Tree::new(format!("MAC: {}", gw.mac_addr))); - // GW IPv4/IPv6 - if !gw.ipv4.is_empty() { - let mut gw_tree = Tree::new(tree_label("IPv4")); - for ip in &gw.ipv4 { - gw_tree.push(Tree::new(ip.to_string())); - } - gw_node.push(gw_tree); - } - if !gw.ipv6.is_empty() { - let mut gw_tree = Tree::new(tree_label("IPv6")); - for ip in &gw.ipv6 { - gw_tree.push(Tree::new(ip.to_string())); - } - gw_node.push(gw_tree); - } - node.push(gw_node); - } - - if iface.default { - let vpn_heuristic = crate::net::iface::detect_vpn_like(&iface); - if vpn_heuristic.is_vpn_like { - let mut heuristic_node = Tree::new(tree_label("Heuristic")); - heuristic_node.push(Tree::new(format!( - "VPN-like: {}", - vpn_heuristic.is_vpn_like - ))); - node.push(heuristic_node); - } - } - - root.push(node); - } - println!("{}", root); -} - -fn print_interface_table(ifs: &[Interface]) { - let mut table = make_table(&[ - "INDEX", "NAME", "TYPE", "STATE", "MAC", "IPv4", "IPv6", "MTU", - ]); - for iface in ifs { - let ipv4 = iface - .ipv4 - .iter() - .map(|n| n.addr().to_string()) - .collect::>() - .join(", "); - let ipv6 = iface - .ipv6 - .iter() - .map(|n| n.addr().to_string()) - .collect::>() - .join(", "); - - table.add_row(vec![ - iface.index.to_string(), - iface.name.clone(), - iface.if_type.name(), - iface.oper_state.to_string(), - iface - .mac_addr - .map(|m| m.to_string()) - .unwrap_or_else(|| "-".into()), - ipv4, - ipv6, - iface - .mtu - .map(|m| m.to_string()) - .unwrap_or_else(|| "-".into()), - ]); - } - - println!("{table}"); -} diff --git a/src/cmd/ifc.rs b/src/cmd/ifc.rs new file mode 100644 index 0000000..a7344a5 --- /dev/null +++ b/src/cmd/ifc.rs @@ -0,0 +1,155 @@ +use anyhow::Result; +use netdev::interface::state::OperState; +use termtree::Tree; + +use crate::cli::IfArgs; +use crate::cli::ShowAction; +use crate::model::{IfDetail, IfSummary}; +use crate::renderer::fmt_flags; + +pub fn run(args: &IfArgs) -> Result<()> { + if args.vendor { + crate::db::oui::init_oui_db()?; + } + + match &args.action { + Some(ShowAction::Show { target }) => { + let iface = if let Some(primary) = crate::net::iface::get_primary_interface() { + if primary.name == *target { + primary + } else { + crate::net::iface::get_interface_by_name(target) + .ok_or_else(|| anyhow::anyhow!("Interface '{target}' not found"))? + } + } else { + crate::net::iface::get_interface_by_name(target) + .ok_or_else(|| anyhow::anyhow!("Interface '{target}' not found"))? + }; + let detail: IfDetail = super::common::interface_to_detail(&iface, args.vendor); + crate::renderer::render_if_detail(&detail, &args.out) + } + None => { + let mut interfaces = crate::net::iface::get_all_interfaces(); + if args.up { + interfaces.retain(|iface| iface.oper_state == OperState::Up); + } + if args.down { + interfaces.retain(|iface| iface.oper_state == OperState::Down); + } + if args.phy { + interfaces.retain(|iface| iface.is_physical()); + } + if args.virt { + interfaces.retain(|iface| !iface.is_physical()); + } + if args.ipv4 { + interfaces.retain(|iface| !iface.ipv4.is_empty()); + } + if args.ipv6 { + interfaces.retain(|iface| !iface.ipv6.is_empty()); + } + interfaces.sort_by(|a, b| a.name.cmp(&b.name)); + + let summaries: Vec = interfaces + .iter() + .map(|iface| super::common::interface_to_summary(iface, args.vendor)) + .collect(); + crate::renderer::render_if_summaries(&summaries, &args.out) + } + } +} + +pub fn run_default_interface() -> Result<()> { + let iface = crate::net::iface::get_primary_interface() + .ok_or_else(|| anyhow::anyhow!("No network interface found"))?; + let host = crate::net::sys::hostname(); + + let mut root = Tree::new(format!("Default Network Interface on {}", host)); + root.push(Tree::new(format!("Index: {}", iface.index))); + root.push(Tree::new(format!("Name: {}", iface.name))); + if let Some(name) = &iface.friendly_name { + root.push(Tree::new(format!("Friendly Name: {}", name))); + } + root.push(Tree::new(format!("Type: {}", iface.if_type.name()))); + root.push(Tree::new(format!("State: {}", iface.oper_state))); + if let Some(mac) = &iface.mac_addr { + root.push(Tree::new(format!("MAC: {}", mac))); + } + if let Some(mtu) = iface.mtu { + root.push(Tree::new(format!("MTU: {}", mtu))); + } + root.push(Tree::new(format!("Flags: {}", fmt_flags(iface.flags)))); + + let mut ipv4 = Tree::new("IPv4".to_string()); + if iface.ipv4.is_empty() { + ipv4.push(Tree::new("(none)".to_string())); + } else { + for net in &iface.ipv4 { + ipv4.push(Tree::new(net.to_string())); + } + } + root.push(ipv4); + + let mut ipv6 = Tree::new("IPv6".to_string()); + if iface.ipv6.is_empty() { + ipv6.push(Tree::new("(none)".to_string())); + } else { + for (idx, net) in iface.ipv6.iter().enumerate() { + let mut label = net.to_string(); + if let Some(scope) = iface.ipv6_scope_ids.get(idx) { + label.push_str(&format!(" (scope_id={scope})")); + } + ipv6.push(Tree::new(label)); + } + } + root.push(ipv6); + + let mut dns = Tree::new("DNS".to_string()); + if iface.dns_servers.is_empty() { + dns.push(Tree::new("(none)".to_string())); + } else { + for server in &iface.dns_servers { + dns.push(Tree::new(server.to_string())); + } + } + root.push(dns); + + if let Some(gw) = &iface.gateway { + let mut gateway = Tree::new("Gateway".to_string()); + gateway.push(Tree::new(format!("MAC: {}", gw.mac_addr))); + let mut gw4 = Tree::new("IPv4".to_string()); + if gw.ipv4.is_empty() { + gw4.push(Tree::new("(none)".to_string())); + } else { + for ip in &gw.ipv4 { + gw4.push(Tree::new(ip.to_string())); + } + } + gateway.push(gw4); + let mut gw6 = Tree::new("IPv6".to_string()); + if gw.ipv6.is_empty() { + gw6.push(Tree::new("(none)".to_string())); + } else { + for ip in &gw.ipv6 { + gw6.push(Tree::new(ip.to_string())); + } + } + gateway.push(gw6); + root.push(gateway); + } + + if let Some(st) = &iface.stats { + let mut stats = Tree::new("Statistics (snapshot)".to_string()); + stats.push(Tree::new(format!("RX bytes: {}", st.rx_bytes))); + stats.push(Tree::new(format!("TX bytes: {}", st.tx_bytes))); + root.push(stats); + } + + let vpn = crate::net::iface::detect_vpn_like(&iface); + root.push(Tree::new(format!("VPN-like: {}", vpn.is_vpn_like))); + + println!("{root}"); + println!(); + println!("Tip: Run 'nifa --help' to see available commands and options."); + Ok(()) +} diff --git a/src/cmd/link.rs b/src/cmd/link.rs new file mode 100644 index 0000000..7c56e8f --- /dev/null +++ b/src/cmd/link.rs @@ -0,0 +1,27 @@ +use anyhow::Result; +use netdev::interface::state::OperState; + +use crate::cli::LinkArgs; +use crate::model::IfDetail; + +pub fn run(args: &LinkArgs) -> Result<()> { + let mut interfaces = crate::net::iface::get_all_interfaces(); + + if let Some(iface_name) = &args.iface { + interfaces.retain(|iface| &iface.name == iface_name); + } + if args.up { + interfaces.retain(|iface| iface.oper_state == OperState::Up); + } + if args.down { + interfaces.retain(|iface| iface.oper_state == OperState::Down); + } + + interfaces.sort_by(|a, b| a.name.cmp(&b.name)); + let details: Vec = interfaces + .iter() + .map(|iface| super::common::interface_to_detail(iface, false)) + .collect(); + + crate::renderer::render_link_entries(&details, &args.out) +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 4fe9183..374ced5 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -1,8 +1,9 @@ -pub mod iface; -pub mod ifaces; +pub mod addr; +pub mod common; +pub mod ifc; +pub mod link; pub mod monitor; pub mod neigh; -pub mod public; pub mod route; -pub mod socket; -pub mod system; +pub mod sock; +pub mod sys; diff --git a/src/cmd/monitor.rs b/src/cmd/monitor.rs index eed86e4..7745e28 100644 --- a/src/cmd/monitor.rs +++ b/src/cmd/monitor.rs @@ -23,10 +23,8 @@ use ratatui::{ }; use termtree::Tree; -use crate::cli::Cli; -use crate::cli::MonitorArgs; +use crate::cli::MonArgs; use crate::net::iface::get_all_interfaces; -use crate::renderer::tree::tree_label; use crate::renderer::{fmt_bps, fmt_flags}; #[derive(Clone, Copy, Debug, ValueEnum)] @@ -90,7 +88,7 @@ struct RowData { tx: f64, } -pub fn monitor_interfaces(_cli: &Cli, args: &MonitorArgs) -> Result<()> { +pub fn monitor_interfaces(args: &MonArgs) -> Result<()> { // Settings let mut sort = args.sort; let target_iface = args.iface.clone(); // Option @@ -505,13 +503,11 @@ fn iface_to_text(iface: &netdev::Interface) -> String { if iface.default { " (default)" } else { "" }, host ); - let mut root = Tree::new(tree_label(title)); + let mut root = Tree::new(title); - // flat fields (no General section) root.push(Tree::new(format!("Index: {}", iface.index))); - - if let Some(fn_name) = &iface.friendly_name { - root.push(Tree::new(format!("Friendly Name: {}", fn_name))); + if let Some(name) = &iface.friendly_name { + root.push(Tree::new(format!("Friendly Name: {}", name))); } if let Some(desc) = &iface.description { root.push(Tree::new(format!("Description: {}", desc))); @@ -519,7 +515,6 @@ fn iface_to_text(iface: &netdev::Interface) -> String { root.push(Tree::new(format!("Type: {:?}", iface.if_type))); root.push(Tree::new(format!("State: {:?}", iface.oper_state))); - if let Some(mac) = &iface.mac_addr { root.push(Tree::new(format!("MAC: {}", mac))); } @@ -527,9 +522,8 @@ fn iface_to_text(iface: &netdev::Interface) -> String { root.push(Tree::new(format!("MTU: {}", mtu))); } - // link speeds (humanized bps) if iface.transmit_speed.is_some() || iface.receive_speed.is_some() { - let mut speed = Tree::new(tree_label("Link Speed")); + let mut speed = Tree::new("Link Speed".to_string()); if let Some(tx) = iface.transmit_speed { speed.push(Tree::new(format!("TX: {}", fmt_bps(tx)))); } @@ -539,68 +533,62 @@ fn iface_to_text(iface: &netdev::Interface) -> String { root.push(speed); } - // flags root.push(Tree::new(format!("Flags: {}", fmt_flags(iface.flags)))); - // ---- Addresses ---- if !iface.ipv4.is_empty() { - let mut ipv4_tree = Tree::new(tree_label("IPv4")); + let mut ipv4 = Tree::new("IPv4".to_string()); for net in &iface.ipv4 { - ipv4_tree.push(Tree::new(net.to_string())); + ipv4.push(Tree::new(net.to_string())); } - root.push(ipv4_tree); + root.push(ipv4); } if !iface.ipv6.is_empty() { - let mut ipv6_tree = Tree::new(tree_label("IPv6")); + let mut ipv6 = Tree::new("IPv6".to_string()); for (i, net) in iface.ipv6.iter().enumerate() { let mut label = net.to_string(); if let Some(scope) = iface.ipv6_scope_ids.get(i) { - label.push_str(&format!(" (scope_id={})", scope)); + label.push_str(&format!(" (scope_id={scope})")); } - ipv6_tree.push(Tree::new(label)); + ipv6.push(Tree::new(label)); } - root.push(ipv6_tree); + root.push(ipv6); } - // ---- DNS ---- if !iface.dns_servers.is_empty() { - let mut dns_tree = Tree::new(tree_label("DNS")); - for dns in &iface.dns_servers { - dns_tree.push(Tree::new(dns.to_string())); + let mut dns = Tree::new("DNS".to_string()); + for server in &iface.dns_servers { + dns.push(Tree::new(server.to_string())); } - root.push(dns_tree); + root.push(dns); } - // ---- Gateway ---- if let Some(gw) = &iface.gateway { - let mut gw_node = Tree::new(tree_label("Gateway")); - gw_node.push(Tree::new(format!("MAC: {}", gw.mac_addr))); + let mut gateway = Tree::new("Gateway".to_string()); + gateway.push(Tree::new(format!("MAC: {}", gw.mac_addr))); if !gw.ipv4.is_empty() { - let mut gw4 = Tree::new(tree_label("IPv4")); + let mut v4 = Tree::new("IPv4".to_string()); for ip in &gw.ipv4 { - gw4.push(Tree::new(ip.to_string())); + v4.push(Tree::new(ip.to_string())); } - gw_node.push(gw4); + gateway.push(v4); } if !gw.ipv6.is_empty() { - let mut gw6 = Tree::new(tree_label("IPv6")); + let mut v6 = Tree::new("IPv6".to_string()); for ip in &gw.ipv6 { - gw6.push(Tree::new(ip.to_string())); + v6.push(Tree::new(ip.to_string())); } - gw_node.push(gw6); + gateway.push(v6); } - root.push(gw_node); + root.push(gateway); } - // ---- Statistics (snapshot) ---- if let Some(st) = &iface.stats { - let mut stats_node = Tree::new(tree_label("Statistics (snapshot)")); - stats_node.push(Tree::new(format!("RX bytes: {}", st.rx_bytes))); - stats_node.push(Tree::new(format!("TX bytes: {}", st.tx_bytes))); - root.push(stats_node); + let mut stats = Tree::new("Statistics".to_string()); + stats.push(Tree::new(format!("RX bytes: {}", st.rx_bytes))); + stats.push(Tree::new(format!("TX bytes: {}", st.tx_bytes))); + root.push(stats); } - //println!("{}", root); - format!("{}", root) + format!("{root}") } diff --git a/src/cmd/neigh.rs b/src/cmd/neigh.rs index 4d3ef4e..5ae55e3 100644 --- a/src/cmd/neigh.rs +++ b/src/cmd/neigh.rs @@ -1,185 +1,82 @@ -use std::collections::HashMap; use std::net::IpAddr; -use crate::cli::{Cli, NeighArgs, OutputFormat}; -use crate::db::oui::is_oui_db_initialized; -use crate::net::neigh; -use crate::renderer::table::make_table; -use crate::renderer::tree::tree_label; use anyhow::Result; use mac_addr::MacAddr; -use netdev::NetworkDevice; -use termtree::Tree; -pub fn show_neigh(_cli: &Cli, args: &NeighArgs) -> Result<()> { +use crate::cli::NeighArgs; +use crate::model::NeighEntry; + +pub fn run(args: &NeighArgs) -> Result<()> { if args.vendor { crate::db::oui::init_oui_db()?; } - let table: HashMap = neigh::get_neighbor_table()?; + let table = crate::net::neigh::get_neighbor_table()?; + let default_iface = crate::net::iface::get_primary_interface(); + let self_ips: Vec = default_iface + .as_ref() + .map(|i| i.ip_addrs()) + .unwrap_or_default(); - match args.export { - Some(export_format) => { - let devices = map_to_devices(table); - crate::fs::export(export_format, args.output.as_deref(), &devices)?; - return Ok(()); - } - None => match args.format { - OutputFormat::Tree => print_neigh_tree(&table), - OutputFormat::Table => print_neigh_table(&table), - OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&table)?), - OutputFormat::Yaml => println!("{}", serde_yaml::to_string(&table)?), - }, - } - Ok(()) -} - -fn map_to_devices(map: HashMap) -> Vec { - map.into_iter() + let mut entries: Vec = table + .into_iter() .map(|(ip, mac)| { - let mut device = NetworkDevice::new(); - device.mac_addr = mac; - match ip { - IpAddr::V4(v4) => device.ipv4.push(v4), - IpAddr::V6(v6) => device.ipv6.push(v6), - } - device - }) - .collect() -} - -fn print_neigh_tree(table: &std::collections::HashMap) { - let iface = netdev::get_default_interface().unwrap(); - let self_ips: Vec = iface.ip_addrs(); - let host = crate::net::sys::hostname(); - let mut root = Tree::new(tree_label(format!("Neighbors (ARP/NDP) on {}", host))); - - let mut v4 = Tree::new(tree_label("IPv4")); - let mut v6 = Tree::new(tree_label("IPv6")); - - let mut keys: Vec<_> = table.keys().cloned().collect(); - keys.sort_by(|a, b| a.to_string().cmp(&b.to_string())); + let vendor = if args.vendor && mac != MacAddr::zero() && !mac.is_broadcast() { + let oui_db = crate::db::oui::oui_db(); + oui_db + .lookup_mac(&mac) + .map(|v| v.vendor_detail.as_deref().unwrap_or(&v.vendor).to_string()) + } else { + None + }; - for ip in keys { - let mac = table.get(&ip).unwrap(); - let mut ip_node = Tree::new(tree_label(ip.to_string())); - ip_node.push(Tree::new(format!("MAC: {}", mac))); - // Vendor lookup - if is_oui_db_initialized() && *mac != MacAddr::zero() && !mac.is_broadcast() { - let oui_db = crate::db::oui::oui_db(); - if let Some(vendor) = oui_db.lookup_mac(mac) { - let vendor_name = vendor.vendor_detail.as_deref().unwrap_or(&vendor.vendor); - ip_node.push(Tree::new(format!("Vendor: {}", vendor_name))); + let mut tags = Vec::new(); + if self_ips.contains(&ip) { + tags.push("Self".to_string()); } - } - - // Classify tags - let mut tags = Vec::new(); - if self_ips.contains(&ip) { - tags.push("Self".to_string()); - } - if let Some(gw) = &iface.gateway { - match ip { - IpAddr::V4(ipv4) => { - if gw.ipv4.contains(&ipv4) { - tags.push("Gateway".to_string()); + if let Some(iface) = &default_iface { + if let Some(gw) = &iface.gateway { + match ip { + IpAddr::V4(v4) if gw.ipv4.contains(&v4) => tags.push("Gateway".to_string()), + IpAddr::V6(v6) if gw.ipv6.contains(&v6) => tags.push("Gateway".to_string()), + _ => {} } } - IpAddr::V6(ipv6) => { - if gw.ipv6.contains(&ipv6) { - tags.push("Gateway".to_string()); - } + if iface.dns_servers.contains(&ip) { + tags.push("DNS".to_string()); } } - } - - if iface.dns_servers.contains(&ip) { - tags.push("DNS".to_string()); - } - if !tags.is_empty() { - ip_node.push(Tree::new(format!("Tags: {}", tags.join(", ")))); - } - - match ip { - std::net::IpAddr::V4(_) => { - v4.push(ip_node); - } - std::net::IpAddr::V6(_) => { - v6.push(ip_node); + NeighEntry { + iface: default_iface.as_ref().map(|i| i.name.clone()), + family: if ip.is_ipv4() { + "ipv4".to_string() + } else { + "ipv6".to_string() + }, + ip: ip.to_string(), + mac: mac.to_string(), + vendor, + tags, } - } - } + }) + .collect(); - if !v4.leaves.is_empty() { - root.push(v4); + if args.ipv4 { + entries.retain(|e| e.family == "ipv4"); } - if !v6.leaves.is_empty() { - root.push(v6); + if args.ipv6 { + entries.retain(|e| e.family == "ipv6"); } - println!("{}", root); -} - -fn print_neigh_table(table: &std::collections::HashMap) { - let iface = netdev::get_default_interface().unwrap(); - let self_ips: Vec = iface.ip_addrs(); - - let mut tbl = make_table(&["IP", "MAC", "Vendor", "Tags"]); - - let mut rows: Vec<_> = table.iter().collect(); - rows.sort_by(|(a, _), (b, _)| a.to_string().cmp(&b.to_string())); - - for (ip, mac) in rows { - // Vendor lookup - let vendor = if is_oui_db_initialized() && *mac != MacAddr::zero() && !mac.is_broadcast() { - let oui_db = crate::db::oui::oui_db(); - oui_db - .lookup_mac(mac) - .map(|v| v.vendor_detail.as_deref().unwrap_or(&v.vendor).to_string()) - } else { - None - }; - - // Classify tags - let mut tags = Vec::new(); - - // Self - if self_ips.contains(ip) { - tags.push("Self".to_string()); - } - - // Gateway - if let Some(gw) = &iface.gateway { - match ip { - IpAddr::V4(ipv4) => { - if gw.ipv4.contains(ipv4) { - tags.push("Gateway".into()); - } - } - IpAddr::V6(ipv6) => { - if gw.ipv6.contains(ipv6) { - tags.push("Gateway".into()); - } - } - } - } - - // DNS - if iface.dns_servers.contains(ip) { - tags.push("DNS".to_string()); - } - - tbl.add_row(vec![ - ip.to_string(), - mac.to_string(), - vendor.unwrap_or_else(|| "-".into()), - if tags.is_empty() { - "-".into() - } else { - tags.join(", ") - }, - ]); + if let Some(iface_name) = &args.iface { + entries.retain(|e| e.iface.as_deref() == Some(iface_name.as_str())); } - - println!("{tbl}"); + entries.sort_by(|a, b| { + a.family + .cmp(&b.family) + .then(a.ip.cmp(&b.ip)) + .then(a.mac.cmp(&b.mac)) + }); + + crate::renderer::render_neighbors(&entries, &args.out) } diff --git a/src/cmd/public.rs b/src/cmd/public.rs deleted file mode 100644 index be2b0eb..0000000 --- a/src/cmd/public.rs +++ /dev/null @@ -1,357 +0,0 @@ -use anyhow::{Context, Result}; -use mac_addr::MacAddr; -use netdev::Interface; -use reqwest::Client; -use std::time::Duration; -use termtree::Tree; - -use crate::cli::{Cli, OutputFormat, PublicArgs}; -use crate::db::oui::is_oui_db_initialized; -use crate::model::ipinfo::{CommonInfo, IpInfo, IpSide, PublicOut}; -use crate::renderer::fmt_bps; -use crate::renderer::tree::tree_label; - -const IPSTRUCT_URL: &str = "https://api.ipstruct.com/ip"; -const IPSTRUCT_V4_URL: &str = "https://ipv4.ipstruct.com/ip"; -//const IP_VERSION_4: &str = "v4"; -const IP_VERSION_6: &str = "v6"; - -/// Show public IP information -pub async fn show_public_ip_info(_cli: &Cli, args: &PublicArgs) -> Result<()> { - let client = Client::builder() - .timeout(Duration::from_secs(args.timeout.max(1))) - .build() - .context("build http client")?; - - let v4: Option; - let mut v6: Option = None; - - if args.ipv4 { - v4 = fetch_ip(&client, IPSTRUCT_V4_URL).await?; - } else { - let (any_res, v4_res) = tokio::join!( - fetch_ip(&client, IPSTRUCT_URL), - fetch_ip(&client, IPSTRUCT_V4_URL), - ); - - let any = any_res.unwrap_or(None); - let v4opt = v4_res.unwrap_or(None); - - match any { - Some(info) if is_ipv6(&info) => { - v6 = Some(info); - v4 = v4opt; - } - Some(info) => { - v4 = Some(info); - } - None => { - v4 = v4opt; - } - } - } - - let out = build_public_out(v4, v6); - - let default_iface_opt = crate::net::iface::get_default_interface(); - - match args.export { - Some(export_date) => { - crate::fs::export(export_date, args.output.as_deref(), &out)?; - return Ok(()); - } - None => match args.format { - OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&out)?), - OutputFormat::Yaml => println!("{}", serde_yaml::to_string(&out)?), - _ => print_public_ip_tree(&out, default_iface_opt), - }, - } - Ok(()) -} - -/// Fetch IP information from a given URL -async fn fetch_ip(client: &Client, url: &str) -> Result> { - let resp = client - .get(url) - .send() - .await - .with_context(|| format!("GET {}", url))?; - if !resp.status().is_success() { - anyhow::bail!("{} -> HTTP {}", url, resp.status()); - } - let info: IpInfo = resp.json().await.context("parse json IpInfo")?; - Ok(Some(info)) -} - -fn is_ipv6(info: &IpInfo) -> bool { - info.ip_version == IP_VERSION_6 || info.ip_addr.contains(':') -} - -fn build_public_out(v4: Option, v6: Option) -> PublicOut { - // v4 or v6 is missing, cannot commonize - if v4.is_none() || v6.is_none() { - return PublicOut { - common: None, - ipv4: v4.as_ref().map(|i| IpSide { - ip_addr: i.ip_addr.clone(), - ip_addr_dec: i.ip_addr_dec.clone(), - host_name: i.host_name.clone(), - network: i.network.clone(), - asn: Some(i.asn.clone()), - as_name: Some(i.as_name.clone()), - country_code: Some(i.country_code.clone()), - country_name: Some(i.country_name.clone()), - }), - ipv6: v6.as_ref().map(|i| IpSide { - ip_addr: i.ip_addr.clone(), - ip_addr_dec: i.ip_addr_dec.clone(), - host_name: i.host_name.clone(), - network: i.network.clone(), - asn: Some(i.asn.clone()), - as_name: Some(i.as_name.clone()), - country_code: Some(i.country_code.clone()), - country_name: Some(i.country_name.clone()), - }), - }; - } - - let v4i = v4.as_ref().unwrap(); - let v6i = v6.as_ref().unwrap(); - - let same_asn = v4i.asn == v6i.asn; - let same_as_name = v4i.as_name == v6i.as_name; - let same_cc = v4i.country_code == v6i.country_code; - let same_country = v4i.country_name == v6i.country_name; - - // If all fields are the same, we can commonize - if same_asn && same_as_name && same_cc && same_country { - PublicOut { - common: Some(CommonInfo { - asn: v4i.asn.clone(), - as_name: v4i.as_name.clone(), - country_code: v4i.country_code.clone(), - country_name: v4i.country_name.clone(), - }), - ipv4: Some(IpSide { - ip_addr: v4i.ip_addr.clone(), - ip_addr_dec: v4i.ip_addr_dec.clone(), - host_name: v4i.host_name.clone(), - network: v4i.network.clone(), - asn: None, - as_name: None, - country_code: None, - country_name: None, - }), - ipv6: Some(IpSide { - ip_addr: v6i.ip_addr.clone(), - ip_addr_dec: v6i.ip_addr_dec.clone(), - host_name: v6i.host_name.clone(), - network: v6i.network.clone(), - asn: None, - as_name: None, - country_code: None, - country_name: None, - }), - } - } else { - PublicOut { - common: None, - ipv4: Some(IpSide { - ip_addr: v4i.ip_addr.clone(), - ip_addr_dec: v4i.ip_addr_dec.clone(), - host_name: v4i.host_name.clone(), - network: v4i.network.clone(), - asn: Some(v4i.asn.clone()), - as_name: Some(v4i.as_name.clone()), - country_code: Some(v4i.country_code.clone()), - country_name: Some(v4i.country_name.clone()), - }), - ipv6: Some(IpSide { - ip_addr: v6i.ip_addr.clone(), - ip_addr_dec: v6i.ip_addr_dec.clone(), - host_name: v6i.host_name.clone(), - network: v6i.network.clone(), - asn: Some(v6i.asn.clone()), - as_name: Some(v6i.as_name.clone()), - country_code: Some(v6i.country_code.clone()), - country_name: Some(v6i.country_name.clone()), - }), - } - } -} - -fn print_public_ip_tree(out: &PublicOut, default_iface: Option) { - let host = crate::net::sys::hostname(); - let mut root = Tree::new(tree_label(format!("Public IPs on {}", host))); - - let mut v4node = Tree::new(tree_label("IPv4")); - if let Some(i) = &out.ipv4 { - v4node.push(Tree::new(tree_label(format!("IP: {}", i.ip_addr)))); - //v4node.push(Tree::new(tree_label(format!("Decimal: {}", i.ip_addr_dec)))); - //v4node.push(Tree::new(tree_label(format!("Host: {}", i.host_name)))); - v4node.push(Tree::new(tree_label(format!("Network: {}", i.network)))); - if out.common.is_none() { - if let Some(asn) = &i.asn { - v4node.push(Tree::new(tree_label(format!("ASN: {}", asn)))); - } - if let Some(n) = &i.as_name { - v4node.push(Tree::new(tree_label(format!("AS Name: {}", n)))); - } - if let Some(cc) = &i.country_code { - let cn = i.country_name.as_deref().unwrap_or(""); - v4node.push(Tree::new(tree_label(format!("Country: {} ({})", cn, cc)))); - } - } - } else { - v4node.push(Tree::new(tree_label("(none)"))); - } - root.push(v4node); - - let mut v6node = Tree::new(tree_label("IPv6")); - if let Some(i) = &out.ipv6 { - v6node.push(Tree::new(tree_label(format!("IP: {}", i.ip_addr)))); - //v6node.push(Tree::new(tree_label(format!("Decimal: {}", i.ip_addr_dec)))); - //v6node.push(Tree::new(tree_label(format!("Host: {}", i.host_name)))); - v6node.push(Tree::new(tree_label(format!("Network: {}", i.network)))); - if out.common.is_none() { - if let Some(asn) = &i.asn { - v6node.push(Tree::new(tree_label(format!("ASN: {}", asn)))); - } - if let Some(n) = &i.as_name { - v6node.push(Tree::new(tree_label(format!("AS Name: {}", n)))); - } - if let Some(cc) = &i.country_code { - let cn = i.country_name.as_deref().unwrap_or(""); - v6node.push(Tree::new(tree_label(format!("Country: {} ({})", cn, cc)))); - } - } - } else { - v6node.push(Tree::new(tree_label("(none)"))); - } - root.push(v6node); - - if let Some(c) = &out.common { - let mut country_info = Tree::new(tree_label("Country")); - country_info.push(Tree::new(tree_label(format!("Code: {}", c.country_code)))); - country_info.push(Tree::new(tree_label(format!("Name: {}", c.country_name)))); - root.push(country_info); - - let mut as_info = Tree::new(tree_label("AS Info")); - as_info.push(Tree::new(tree_label(format!("ASN: {}", c.asn)))); - as_info.push(Tree::new(tree_label(format!("AS Name: {}", c.as_name)))); - root.push(as_info); - } - - // ---- Default Interface (optional) ---- - if let Some(iface) = default_iface { - let mut if_node = Tree::new(tree_label(format!("Default Interface: {}", iface.name))); - - if let Some(fn_name) = &iface.friendly_name { - if_node.push(Tree::new(tree_label(format!("Friendly Name: {}", fn_name)))); - } - if let Some(desc) = &iface.description { - if_node.push(Tree::new(tree_label(format!("Description: {}", desc)))); - } - - if_node.push(Tree::new(tree_label(format!("Index: {}", iface.index)))); - if_node.push(Tree::new(tree_label(format!("Type: {:?}", iface.if_type)))); - if_node.push(Tree::new(tree_label(format!( - "State: {:?}", - iface.oper_state - )))); - if let Some(mac) = &iface.mac_addr { - if_node.push(Tree::new(tree_label(format!("MAC: {}", mac)))); - - if is_oui_db_initialized() && *mac != MacAddr::zero() { - let oui_db = crate::db::oui::oui_db(); - if let Some(vendor) = oui_db.lookup_mac(mac) { - let vendor_name = vendor.vendor_detail.as_deref().unwrap_or(&vendor.vendor); - if_node.push(Tree::new(format!("Vendor: {}", vendor_name))); - } - } - } - - if let Some(mtu) = iface.mtu { - if_node.push(Tree::new(tree_label(format!("MTU: {}", mtu)))); - } - - // Speeds - if iface.transmit_speed.is_some() || iface.receive_speed.is_some() { - let mut speed = Tree::new(tree_label("Link Speed")); - if let Some(tx) = iface.transmit_speed { - speed.push(Tree::new(tree_label(format!("TX: {}", fmt_bps(tx))))); - } - if let Some(rx) = iface.receive_speed { - speed.push(Tree::new(tree_label(format!("RX: {}", fmt_bps(rx))))); - } - if_node.push(speed); - } - - // IPv4 - if !iface.ipv4.is_empty() { - let mut ipv4_node = Tree::new(tree_label("IPv4")); - for n in &iface.ipv4 { - ipv4_node.push(Tree::new(tree_label(n.to_string()))); - } - if_node.push(ipv4_node); - } - // IPv6 with scope ID - if !iface.ipv6.is_empty() { - let mut ipv6_node = Tree::new(tree_label("IPv6")); - for (i, n) in iface.ipv6.iter().enumerate() { - let mut label = n.to_string(); - if let Some(sc) = iface.ipv6_scope_ids.get(i) { - label.push_str(&format!(" (scope_id={})", sc)); - } - ipv6_node.push(Tree::new(tree_label(label))); - } - if_node.push(ipv6_node); - } - - // DNS - if !iface.dns_servers.is_empty() { - let mut dns = Tree::new(tree_label("DNS")); - for s in &iface.dns_servers { - dns.push(Tree::new(tree_label(s.to_string()))); - } - if_node.push(dns); - } - - // Gateway (IP + MAC) - if let Some(gw) = &iface.gateway { - let mut gw_node = Tree::new(tree_label("Gateway")); - gw_node.push(Tree::new(tree_label(format!("MAC: {}", gw.mac_addr)))); - if !gw.ipv4.is_empty() { - let mut gw4 = Tree::new(tree_label("IPv4")); - for ip in &gw.ipv4 { - gw4.push(Tree::new(tree_label(ip.to_string()))); - } - gw_node.push(gw4); - } - if !gw.ipv6.is_empty() { - let mut gw6 = Tree::new(tree_label("IPv6")); - for ip in &gw.ipv6 { - gw6.push(Tree::new(tree_label(ip.to_string()))); - } - gw_node.push(gw6); - } - if_node.push(gw_node); - } - - let vpn_heuristic = crate::net::iface::detect_vpn_like(&iface); - if vpn_heuristic.is_vpn_like { - let mut heuristic_node = Tree::new(tree_label("Heuristic")); - heuristic_node.push(Tree::new(format!( - "VPN-like: {}", - vpn_heuristic.is_vpn_like - ))); - if_node.push(heuristic_node); - } - - root.push(if_node); - } else { - root.push(Tree::new(tree_label("Default Interface: (not found)"))); - } - - println!("{}", root); -} diff --git a/src/cmd/route.rs b/src/cmd/route.rs index 412d40f..10e2e2a 100644 --- a/src/cmd/route.rs +++ b/src/cmd/route.rs @@ -1,138 +1,51 @@ -use crate::cli::{Cli, OutputFormat, RouteArgs, RouteFamilyOpt}; -use crate::net::route; -use crate::renderer::table::make_table; -use crate::renderer::tree::tree_label; use anyhow::Result; -use netroute::{RouteEntry, RouteFamily, RouteFlag}; -use termtree::Tree; +use netroute::{RouteFamily, RouteFlag}; -pub fn show_route(_cli: &Cli, args: &RouteArgs) -> Result<()> { - let mut routes = route::list_routes()?; +use crate::cli::RouteArgs; +use crate::model::RouteEntry; - // family filter - routes.retain(|r| match args.family { - RouteFamilyOpt::All => true, - RouteFamilyOpt::Ipv4 => r.family == RouteFamily::Ipv4, - RouteFamilyOpt::Ipv6 => r.family == RouteFamily::Ipv6, - }); +pub fn run(args: &RouteArgs) -> Result<()> { + let mut routes = crate::net::route::list_routes()?; - match args.export { - Some(export_format) => { - crate::fs::export(export_format, args.output.as_deref(), &routes)?; - return Ok(()); - } - None => match args.format { - OutputFormat::Tree => print_route_tree(&routes), - OutputFormat::Table => print_route_table(&routes), - OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&routes)?), - OutputFormat::Yaml => println!("{}", serde_yaml::to_string(&routes)?), - }, + if args.ipv4 { + routes.retain(|r| r.family == RouteFamily::Ipv4); } - Ok(()) -} - -fn print_route_tree(routes: &[RouteEntry]) { - let host = crate::net::sys::hostname(); - let mut root = Tree::new(tree_label(format!("Routing Table on {}", host))); - - let mut v4 = Tree::new(tree_label("IPv4")); - let mut v6 = Tree::new(tree_label("IPv6")); - - for r in routes { - let mut node = Tree::new(tree_label(format!("{}", r.destination))); - if let Some(gw) = r.gateway { - node.push(Tree::new(format!("via {}", gw))); - } else if r.on_link { - node.push(Tree::new(tree_label("via link"))); - } - - if let Some(name) = r.ifname.as_ref() { - node.push(Tree::new(format!("dev {}", name))); - } else if let Some(idx) = r.ifindex { - node.push(Tree::new(format!("ifindex {}", idx))); - } - - if let Some(m) = r.metric { - node.push(Tree::new(format!("metric {}", m))); - } - - if !r.flags.is_empty() { - let short = flags_short(r); - node.push(Tree::new(format!("flags {}", short))); - } - - if let Some(p) = r.protocol.as_ref() { - node.push(Tree::new(format!("proto {:?}", p))); - } - if let Some(s) = r.scope.as_ref() { - node.push(Tree::new(format!("scope {:?}", s))); - } - if let Some(tbl) = r.table { - node.push(Tree::new(format!("table {}", tbl))); - } - if let Some(ms) = r.lifetime_ms { - node.push(Tree::new(format!("lifetime {}ms", ms))); - } - - match r.family { - RouteFamily::Ipv4 => { - v4.push(node); - } - RouteFamily::Ipv6 => { - v6.push(node); - } - } + if args.ipv6 { + routes.retain(|r| r.family == RouteFamily::Ipv6); } - - if !v4.leaves.is_empty() { - root.push(v4); - } - if !v6.leaves.is_empty() { - root.push(v6); + if args.default { + routes.retain(|r| r.destination.addr.is_unspecified()); } - println!("{}", root); -} -fn print_route_table(routes: &[RouteEntry]) { - let mut table = make_table(&["FAMILY", "DESTINATION", "VIA/NH", "DEV", "METRIC", "FLAGS"]); - - for r in routes { - let fam = match r.family { - RouteFamily::Ipv4 => "v4", - RouteFamily::Ipv6 => "v6", - }; - let via = r - .gateway - .map(|g| g.to_string()) - .unwrap_or_else(|| if r.on_link { "link".into() } else { "-".into() }); - let dev = r.ifname.as_deref().unwrap_or("-"); - let metric = r.metric.map(|m| m.to_string()).unwrap_or("-".into()); - let flags = r - .flags - .iter() - .map(RouteFlag::short) - .collect::>() - .join(""); - table.add_row(vec![ - fam, - &r.destination.to_string(), - &via, - dev, - &metric, - &flags, - ]); - } - - println!("{table}"); -} + let mut entries: Vec = routes + .into_iter() + .map(|r| RouteEntry { + family: match r.family { + RouteFamily::Ipv4 => "ipv4".to_string(), + RouteFamily::Ipv6 => "ipv6".to_string(), + }, + destination: r.destination.to_string(), + gateway: r.gateway.map(|g| g.to_string()), + interface: r.ifname, + metric: r.metric, + flags: r.flags.iter().map(RouteFlag::short).collect(), + detail: if args.detail { + Some(format!( + "proto={:?},scope={:?},table={:?},on_link={},ifindex={:?},lifetime_ms={:?}", + r.protocol, r.scope, r.table, r.on_link, r.ifindex, r.lifetime_ms + )) + } else { + None + }, + }) + .collect(); + + entries.sort_by(|a, b| { + a.family + .cmp(&b.family) + .then(a.destination.cmp(&b.destination)) + .then(a.interface.cmp(&b.interface)) + }); -fn flags_short(r: &RouteEntry) -> String { - if r.flags.is_empty() { - return "-".into(); - } - r.flags - .iter() - .map(RouteFlag::short) - .collect::>() - .join("") + crate::renderer::render_routes(&entries, &args.out) } diff --git a/src/cmd/sock.rs b/src/cmd/sock.rs new file mode 100644 index 0000000..2d431fa --- /dev/null +++ b/src/cmd/sock.rs @@ -0,0 +1,102 @@ +use anyhow::Result; +use netsock::{family::AddressFamilyFlags, protocol::ProtocolFlags, socket::ProtocolSocketInfo}; + +use crate::cli::{SockArgs, SocketFamily, SocketProto}; +use crate::model::SockEntry; + +pub fn run(args: &SockArgs) -> Result<()> { + let pf = match args.proto { + SocketProto::Tcp => ProtocolFlags::TCP, + SocketProto::Udp => ProtocolFlags::UDP, + SocketProto::All => ProtocolFlags::TCP | ProtocolFlags::UDP, + }; + + let af = match args.family { + SocketFamily::Ipv4 => AddressFamilyFlags::IPV4, + SocketFamily::Ipv6 => AddressFamilyFlags::IPV6, + SocketFamily::All => AddressFamilyFlags::IPV4 | AddressFamilyFlags::IPV6, + }; + + let mut socks = crate::net::socket::collect_sockets(pf, af)?; + + if let Some(port) = args.port { + socks.retain(|s| s.local_port() == port || s.remote_port() == Some(port)); + } + + if args.listen { + socks.retain(|s| { + matches!( + &s.protocol_socket_info, + ProtocolSocketInfo::Tcp(tcp) if tcp.state.to_string().eq_ignore_ascii_case("listen") + ) + }); + } + + if args.established { + socks.retain(|s| { + matches!( + &s.protocol_socket_info, + ProtocolSocketInfo::Tcp(tcp) if tcp.state.to_string().eq_ignore_ascii_case("established") + ) + }); + } + + let mut entries: Vec = socks + .iter() + .map(|s| { + let (proto, family, local, remote, state) = match &s.protocol_socket_info { + ProtocolSocketInfo::Tcp(info) => ( + "tcp".to_string(), + if info.local_addr.is_ipv4() { + "ipv4".to_string() + } else { + "ipv6".to_string() + }, + fmt_sock_addr(info.local_addr, info.local_port), + Some(fmt_sock_addr(info.remote_addr, info.remote_port)), + Some(info.state.to_string().to_lowercase()), + ), + ProtocolSocketInfo::Udp(info) => ( + "udp".to_string(), + if info.local_addr.is_ipv4() { + "ipv4".to_string() + } else { + "ipv6".to_string() + }, + fmt_sock_addr(info.local_addr, info.local_port), + None, + None, + ), + }; + + let pid = s.processes.first().map(|p| p.pid); + let process = s.processes.first().map(|p| p.name.clone()); + + SockEntry { + proto, + family, + local, + remote, + state, + pid, + process, + } + }) + .collect(); + + entries.sort_by(|a, b| { + a.proto + .cmp(&b.proto) + .then(a.family.cmp(&b.family)) + .then(a.local.cmp(&b.local)) + }); + + crate::renderer::render_sockets(&entries, &args.out, args.pid) +} + +fn fmt_sock_addr(ip: std::net::IpAddr, port: u16) -> String { + match ip { + std::net::IpAddr::V4(v4) => format!("{v4}:{port}"), + std::net::IpAddr::V6(v6) => format!("[{v6}]:{port}"), + } +} diff --git a/src/cmd/socket.rs b/src/cmd/socket.rs deleted file mode 100644 index 2fff929..0000000 --- a/src/cmd/socket.rs +++ /dev/null @@ -1,200 +0,0 @@ -use crate::cli::{Cli, OutputFormat, SocketArgs, SocketFamily, SocketProto}; -use crate::fs::export; -use crate::net::addr::AddressFamily; -use crate::net::socket::collect_sockets; -use crate::renderer::tree::tree_label; -use anyhow::Result; -use comfy_table::{Cell, Row}; -use netsock::socket::ProtocolSocketInfo; -use std::net::IpAddr; -use termtree::Tree; - -use netsock::{family::AddressFamilyFlags, protocol::ProtocolFlags, socket::SocketInfo}; - -pub fn show_sockets(_cli: &Cli, args: &SocketArgs) -> Result<()> { - let pf = match args.proto { - SocketProto::Tcp => ProtocolFlags::TCP, - SocketProto::Udp => ProtocolFlags::UDP, - SocketProto::All => ProtocolFlags::TCP | ProtocolFlags::UDP, - }; - - let af = match args.family { - SocketFamily::Ipv4 => AddressFamilyFlags::IPV4, - SocketFamily::Ipv6 => AddressFamilyFlags::IPV6, - SocketFamily::All => AddressFamilyFlags::IPV4 | AddressFamilyFlags::IPV6, - }; - - let mut socks = collect_sockets(pf, af)?; - - if let Some(pid) = args.pid { - socks.retain(|s| s.is_owned_by_pid(pid)); - } - if let Some(port) = args.port { - socks.retain(|s| s.local_port() == port || s.remote_port() == Some(port)); - } - if let Some(ref state) = args.state { - let want = state.to_lowercase(); - socks.retain(|s| match &s.protocol_socket_info { - ProtocolSocketInfo::Tcp(tcp_sock) => { - let sock_state = tcp_sock.state.to_string().to_lowercase(); - want == "all" || sock_state == want - } - _ => false, - }); - } - - match args.export { - Some(export_format) => { - export(export_format, args.output.as_deref(), &socks)?; - return Ok(()); - } - None => match args.format { - OutputFormat::Tree => print_socket_tree(&socks), - OutputFormat::Table => print_socket_table(&socks), - OutputFormat::Json => crate::renderer::json::pretty_print_json(&socks)?, - OutputFormat::Yaml => crate::renderer::yaml::print_yaml(&socks)?, - }, - } - Ok(()) -} - -fn fmt_sock_addr(ip: IpAddr, port: u16) -> String { - match ip { - IpAddr::V4(v4) => format!("{}:{}", v4, port), - IpAddr::V6(v6) => format!("[{}]:{}", v6, port), - } -} - -fn fmt_processes(s: &SocketInfo) -> (String, String) { - if s.processes.is_empty() { - return ("-".to_string(), "-".to_string()); - } - - let first = &s.processes[0]; - let pid = if s.processes.len() == 1 { - format!("{}", first.pid) - } else { - format!("{} (+{})", first.pid, s.processes.len() - 1) - }; - - let name = if s.processes.len() == 1 { - first.name.clone() - } else { - format!("{} (+{})", first.name, s.processes.len() - 1) - }; - - (pid, name) -} - -pub fn print_socket_table(socks: &[SocketInfo]) { - if socks.is_empty() { - println!("(no sockets)"); - return; - } - - let mut table = crate::renderer::table::make_table(&[ - "Proto", - "Fam", - "Local Address", - "Remote Address", - "State", - "PID", - "Process", - ]); - - for s in socks { - let fam = AddressFamily::from_ip_addr(&s.local_addr()); - let (proto, fam, local, remote, state) = match &s.protocol_socket_info { - ProtocolSocketInfo::Tcp(info) => ( - "TCP".to_string(), - fam.to_string(), - fmt_sock_addr(info.local_addr, info.local_port), - fmt_sock_addr(info.remote_addr, info.remote_port), - format!("{:?}", info.state), - ), - ProtocolSocketInfo::Udp(info) => ( - "UDP".to_string(), - fam.to_string(), - fmt_sock_addr(info.local_addr, info.local_port), - "-".to_string(), - "-".to_string(), - ), - }; - - let (pid, pname) = fmt_processes(s); - - table.add_row(Row::from(vec![ - Cell::new(proto), - Cell::new(fam), - Cell::new(local), - Cell::new(remote), - Cell::new(state), - Cell::new(pid), - Cell::new(pname), - ])); - } - - println!("{table}"); -} - -pub fn print_socket_tree(socks: &[SocketInfo]) { - let host = crate::net::sys::hostname(); - let mut root = Tree::new(tree_label(format!("Sockets on {}", host))); - - if socks.is_empty() { - root.push(Tree::new(tree_label("(no sockets)"))); - println!("{root}"); - return; - } - - for s in socks { - let mut node = match &s.protocol_socket_info { - ProtocolSocketInfo::Tcp(info) => { - let fam = match info.local_addr { - IpAddr::V4(_) => "IPv4", - IpAddr::V6(_) => "IPv6", - }; - let title = format!( - "TCP [{}] {} -> {} ({:?})", - fam, - fmt_sock_addr(info.local_addr, info.local_port), - fmt_sock_addr(info.remote_addr, info.remote_port), - info.state, - ); - Tree::new(tree_label(title)) - } - ProtocolSocketInfo::Udp(info) => { - let fam = match info.local_addr { - IpAddr::V4(_) => "IPv4", - IpAddr::V6(_) => "IPv6", - }; - let title = format!( - "UDP [{}] {}", - fam, - fmt_sock_addr(info.local_addr, info.local_port), - ); - Tree::new(tree_label(title)) - } - }; - - // Processes - if s.processes.is_empty() { - node.push(Tree::new(tree_label("Process: (unknown)"))); - } else { - let mut procs = Tree::new(tree_label("Processes")); - for p in &s.processes { - procs.push(Tree::new(tree_label(format!("{} ({})", p.pid, p.name)))); - } - node.push(procs); - } - - #[cfg(target_os = "linux")] - { - node.push(Tree::new(tree_label(format!("UID: {}", s.uid)))); - node.push(Tree::new(tree_label(format!("Inode: {}", s.inode)))); - } - root.push(node); - } - - println!("{root}"); -} diff --git a/src/cmd/sys.rs b/src/cmd/sys.rs new file mode 100644 index 0000000..284c86d --- /dev/null +++ b/src/cmd/sys.rs @@ -0,0 +1,55 @@ +use anyhow::Result; + +use crate::cli::SysArgs; +use crate::model::SystemSummary; + +pub fn run(args: &SysArgs) -> Result<()> { + let sys_info = crate::net::sys::system_info(); + let default_iface = crate::net::iface::get_primary_interface(); + + let dns_servers = if args.summary || args.proxy { + Vec::new() + } else { + default_iface + .as_ref() + .map(|iface| iface.dns_servers.iter().map(|d| d.to_string()).collect()) + .unwrap_or_default() + }; + + let (proxy_http, proxy_https, proxy_all, proxy_no_proxy) = if args.summary || args.dns { + (None, None, None, None) + } else { + ( + sys_info.proxy.http.clone(), + sys_info.proxy.https.clone(), + sys_info.proxy.all.clone(), + sys_info.proxy.no_proxy.clone(), + ) + }; + + let summary = SystemSummary { + hostname: sys_info.hostname, + os: format!("{} {}", sys_info.os_type, sys_info.os_version), + kernel_version: sys_info.kernel_version, + default_interface: default_iface.as_ref().map(|iface| iface.name.clone()), + default_gateway: default_iface.as_ref().and_then(|iface| { + iface + .gateway + .as_ref() + .and_then(|gw| gw.ipv4.first().map(|ip| ip.to_string())) + .or_else(|| { + iface + .gateway + .as_ref() + .and_then(|gw| gw.ipv6.first().map(|ip| ip.to_string())) + }) + }), + dns_servers, + proxy_http, + proxy_https, + proxy_all, + proxy_no_proxy, + }; + + crate::renderer::render_system_summary(&summary, &args.out) +} diff --git a/src/cmd/system.rs b/src/cmd/system.rs deleted file mode 100644 index 5787c20..0000000 --- a/src/cmd/system.rs +++ /dev/null @@ -1,245 +0,0 @@ -use anyhow::Result; -use mac_addr::MacAddr; -use netdev::Interface; -use termtree::Tree; -use url::Url; - -use crate::{ - cli::{Cli, SystemArgs}, - db::oui::is_oui_db_initialized, - net::sys::SysInfo, - renderer::{fmt_bps, tree::tree_label}, -}; - -/// Show system network stack details -pub fn show_system_net_stack(_cli: &Cli, args: &SystemArgs) -> Result<()> { - let sys_info = crate::net::sys::system_info(); - let default_iface_opt = crate::net::iface::get_default_interface(); - match args.export { - Some(export_format) => { - crate::fs::export(export_format, args.output.as_deref(), &sys_info)?; - return Ok(()); - } - None => match args.format { - crate::cli::OutputFormat::Tree => { - print_system_with_default_iface(&sys_info, default_iface_opt) - } - crate::cli::OutputFormat::Json => { - crate::renderer::json::print_snapshot_json(&sys_info, default_iface_opt) - } - crate::cli::OutputFormat::Yaml => { - crate::renderer::yaml::print_snapshot_yaml(&sys_info, default_iface_opt) - } - _ => { - tracing::error!( - "Unsupported format for show system network stack: {:?}", - args.format - ); - } - }, - } - Ok(()) -} - -/// Mask username/password in proxy URL for privacy -fn mask_proxy_url(raw: &str) -> String { - if let Ok(mut url) = Url::parse(raw) { - if url.password().is_some() || !url.username().is_empty() { - let user = url.username().to_string(); - let _ = url.set_username(&user).ok(); - let _ = url.set_password(Some("*****")).ok(); - } - return url.to_string(); - } - raw.to_string() -} - -fn print_system_with_default_iface(sys: &SysInfo, default_iface: Option) { - let mut root = Tree::new(tree_label(format!( - "System Information on {}", - sys.hostname - ))); - - // ---- System ---- - let mut sys_node = Tree::new(tree_label("System")); - sys_node.push(Tree::new(tree_label(format!("OS Type: {}", sys.os_type)))); - sys_node.push(Tree::new(tree_label(format!( - "Version: {}", - sys.os_version - )))); - if let Some(kv) = &sys.kernel_version { - sys_node.push(Tree::new(tree_label(format!("Kernel: {}", kv)))); - } - sys_node.push(Tree::new(tree_label(format!("Edition: {}", sys.edition)))); - sys_node.push(Tree::new(tree_label(format!("Codename: {}", sys.codename)))); - sys_node.push(Tree::new(tree_label(format!("Bitness: {}", sys.bitness)))); - sys_node.push(Tree::new(tree_label(format!( - "Architecture: {}", - sys.architecture - )))); - - // ---- Proxy (env) ---- - let px = crate::net::sys::collect_proxy_env(); - let mut px_node = Tree::new(tree_label("Proxy (env)")); - px_node.push(Tree::new(format!( - "HTTP_PROXY: {}", - px.http - .as_deref() - .map(mask_proxy_url) - .unwrap_or_else(|| "(none)".into()) - ))); - px_node.push(Tree::new(format!( - "HTTPS_PROXY: {}", - px.https - .as_deref() - .map(mask_proxy_url) - .unwrap_or_else(|| "(none)".into()) - ))); - px_node.push(Tree::new(format!( - "ALL_PROXY: {}", - px.all - .as_deref() - .map(mask_proxy_url) - .unwrap_or_else(|| "(none)".into()) - ))); - if let Some(np) = px.no_proxy.as_deref() { - let mut np_node = Tree::new(tree_label("NO_PROXY")); - // Split by comma and trim spaces, ignore empty parts - for (i, part) in np - .split(',') - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .enumerate() - { - // Limit to first 20 entries - if i < 20 { - np_node.push(Tree::new(part.to_string())); - } else { - np_node.push(Tree::new(format!( - "(+{} more)", - np.split(',').count().saturating_sub(20) - ))); - break; - } - } - px_node.push(np_node); - } else { - px_node.push(Tree::new(tree_label("NO_PROXY: (none)"))); - } - sys_node.push(px_node); - - root.push(sys_node); - - // ---- Default Interface (optional) ---- - if let Some(iface) = default_iface { - let mut if_node = Tree::new(tree_label(format!("Default Interface: {}", iface.name))); - - if let Some(fn_name) = &iface.friendly_name { - if_node.push(Tree::new(tree_label(format!("Friendly Name: {}", fn_name)))); - } - if let Some(desc) = &iface.description { - if_node.push(Tree::new(tree_label(format!("Description: {}", desc)))); - } - - if_node.push(Tree::new(tree_label(format!("Index: {}", iface.index)))); - if_node.push(Tree::new(tree_label(format!("Type: {:?}", iface.if_type)))); - if_node.push(Tree::new(tree_label(format!( - "State: {:?}", - iface.oper_state - )))); - if let Some(mac) = &iface.mac_addr { - if_node.push(Tree::new(tree_label(format!("MAC: {}", mac)))); - - if is_oui_db_initialized() && *mac != MacAddr::zero() { - let oui_db = crate::db::oui::oui_db(); - if let Some(vendor) = oui_db.lookup_mac(mac) { - let vendor_name = vendor.vendor_detail.as_deref().unwrap_or(&vendor.vendor); - if_node.push(Tree::new(format!("Vendor: {}", vendor_name))); - } - } - } - - if let Some(mtu) = iface.mtu { - if_node.push(Tree::new(tree_label(format!("MTU: {}", mtu)))); - } - - // Speeds - if iface.transmit_speed.is_some() || iface.receive_speed.is_some() { - let mut speed = Tree::new(tree_label("Link Speed")); - if let Some(tx) = iface.transmit_speed { - speed.push(Tree::new(tree_label(format!("TX: {}", fmt_bps(tx))))); - } - if let Some(rx) = iface.receive_speed { - speed.push(Tree::new(tree_label(format!("RX: {}", fmt_bps(rx))))); - } - if_node.push(speed); - } - - // IPv4 - if !iface.ipv4.is_empty() { - let mut ipv4_node = Tree::new(tree_label("IPv4")); - for n in &iface.ipv4 { - ipv4_node.push(Tree::new(tree_label(n.to_string()))); - } - if_node.push(ipv4_node); - } - // IPv6 with scope ID - if !iface.ipv6.is_empty() { - let mut ipv6_node = Tree::new(tree_label("IPv6")); - for (i, n) in iface.ipv6.iter().enumerate() { - let mut label = n.to_string(); - if let Some(sc) = iface.ipv6_scope_ids.get(i) { - label.push_str(&format!(" (scope_id={})", sc)); - } - ipv6_node.push(Tree::new(tree_label(label))); - } - if_node.push(ipv6_node); - } - - // DNS - if !iface.dns_servers.is_empty() { - let mut dns = Tree::new(tree_label("DNS")); - for s in &iface.dns_servers { - dns.push(Tree::new(tree_label(s.to_string()))); - } - if_node.push(dns); - } - - // Gateway (IP + MAC) - if let Some(gw) = &iface.gateway { - let mut gw_node = Tree::new(tree_label("Gateway")); - gw_node.push(Tree::new(tree_label(format!("MAC: {}", gw.mac_addr)))); - if !gw.ipv4.is_empty() { - let mut gw4 = Tree::new(tree_label("IPv4")); - for ip in &gw.ipv4 { - gw4.push(Tree::new(tree_label(ip.to_string()))); - } - gw_node.push(gw4); - } - if !gw.ipv6.is_empty() { - let mut gw6 = Tree::new(tree_label("IPv6")); - for ip in &gw.ipv6 { - gw6.push(Tree::new(tree_label(ip.to_string()))); - } - gw_node.push(gw6); - } - if_node.push(gw_node); - } - - let vpn_heuristic = crate::net::iface::detect_vpn_like(&iface); - if vpn_heuristic.is_vpn_like { - let mut heuristic_node = Tree::new(tree_label("Heuristic")); - heuristic_node.push(Tree::new(format!( - "VPN-like: {}", - vpn_heuristic.is_vpn_like - ))); - if_node.push(heuristic_node); - } - - root.push(if_node); - } else { - root.push(Tree::new(tree_label("Default Interface: (not found)"))); - } - - println!("{}", root); -} diff --git a/src/fs.rs b/src/fs.rs deleted file mode 100644 index 258c17d..0000000 --- a/src/fs.rs +++ /dev/null @@ -1,35 +0,0 @@ -use anyhow::{Context, Result}; -use serde::Serialize; -use std::{fs, io::Write, path::Path}; - -use crate::cli::ExportFormat; - -pub fn export(format: ExportFormat, output: Option<&Path>, data: &T) -> Result<()> { - let (bytes, ext_default) = match format { - ExportFormat::Json => (serde_json::to_vec_pretty(data)?, "json"), - ExportFormat::Yaml => (serde_yaml::to_string(data)?.into_bytes(), "yaml"), - }; - - if let Some(path) = output { - atomic_write(path, &bytes, ext_default)?; - tracing::info!("Exported {} bytes to {}", bytes.len(), path.display()); - } else { - std::io::stdout() - .write_all(&bytes) - .context("write stdout")?; - } - - Ok(()) -} - -fn atomic_write(path: &Path, data: &[u8], ext_default: &str) -> Result<()> { - let target = if path.extension().is_none() { - path.with_extension(ext_default) - } else { - path.to_path_buf() - }; - let tmp = target.with_extension("tmp"); - fs::write(&tmp, data).with_context(|| format!("write temp {}", tmp.display()))?; - fs::rename(&tmp, &target).with_context(|| format!("rename to {}", target.display()))?; - Ok(()) -} diff --git a/src/main.rs b/src/main.rs index 6f224c8..99acb2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,9 @@ use anyhow::Result; use clap::Parser; + mod cli; mod cmd; mod db; -mod fs; mod log; mod model; mod net; @@ -12,40 +12,23 @@ mod time; use cli::{Cli, Command}; -#[tokio::main] -async fn main() -> Result<()> { +fn main() -> Result<()> { let cli = Cli::parse(); - log::init_logger(&cli.log_level)?; match &cli.command { + Some(Command::If(args)) => cmd::ifc::run(args)?, + Some(Command::Addr(args)) => cmd::addr::run(args)?, + Some(Command::Link(args)) => cmd::link::run(args)?, + Some(Command::Route(args)) => cmd::route::run(args)?, + Some(Command::Neigh(args)) => cmd::neigh::run(args)?, + Some(Command::Sock(args)) => cmd::sock::run(args)?, + Some(Command::Sys(args)) => cmd::sys::run(args)?, + Some(Command::Mon(args)) => cmd::monitor::monitor_interfaces(args)?, None => { - cmd::iface::show_default_interface(&cli)?; - } - Some(Command::Ifaces(args)) => { - cmd::ifaces::list_interfaces(&cli, args)?; - } - Some(Command::Iface(args)) => { - cmd::iface::show_interface(&cli, args)?; - } - Some(Command::System(args)) => { - cmd::system::show_system_net_stack(&cli, args)?; - } - Some(Command::Monitor(args)) => { - cmd::monitor::monitor_interfaces(&cli, args)?; + cmd::ifc::run_default_interface()?; } - Some(Command::Public(args)) => { - cmd::public::show_public_ip_info(&cli, args).await?; - } - Some(Command::Route(args)) => { - cmd::route::show_route(&cli, args)?; - } - Some(Command::Neigh(args)) => { - cmd::neigh::show_neigh(&cli, args)?; - } - Some(Command::Socket(args)) => { - cmd::socket::show_sockets(&cli, args)?; - } - }; + } + Ok(()) } diff --git a/src/model/ipinfo.rs b/src/model/ipinfo.rs deleted file mode 100644 index daae67c..0000000 --- a/src/model/ipinfo.rs +++ /dev/null @@ -1,45 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize)] -pub struct IpInfo { - pub ip_version: String, - pub ip_addr_dec: String, - pub ip_addr: String, - pub host_name: String, - pub network: String, - pub asn: String, - pub as_name: String, - pub country_code: String, - pub country_name: String, -} - -#[derive(Debug, Serialize)] -pub struct PublicOut { - pub common: Option, - pub ipv4: Option, - pub ipv6: Option, -} - -#[derive(Debug, Serialize)] -pub struct CommonInfo { - pub asn: String, - pub as_name: String, - pub country_code: String, - pub country_name: String, -} - -#[derive(Debug, Serialize)] -pub struct IpSide { - pub ip_addr: String, - pub ip_addr_dec: String, - pub host_name: String, - pub network: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub asn: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub as_name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub country_code: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub country_name: Option, -} diff --git a/src/model/mod.rs b/src/model/mod.rs index 2236c1d..8e1c5fe 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,2 +1,94 @@ -pub mod ipinfo; -pub mod snapshot; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IfSummary { + pub index: u32, + pub name: String, + pub if_type: String, + pub state: String, + pub is_default: bool, + pub mac: Option, + pub mtu: Option, + pub ipv4_addrs: Vec, + pub ipv6_addrs: Vec, + pub gateway_mac: Option, + pub gateway_ipv4: Vec, + pub gateway_ipv6: Vec, + pub vendor: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IfDetail { + pub summary: IfSummary, + pub friendly_name: Option, + pub description: Option, + pub flags: String, + pub tx_speed: Option, + pub rx_speed: Option, + pub ipv4: Vec, + pub ipv6: Vec, + pub ipv6_scoped: Vec, + pub dns_servers: Vec, + pub gateway_ipv4: Vec, + pub gateway_ipv6: Vec, + pub gateway_mac: Option, + pub stats_rx_bytes: Option, + pub stats_tx_bytes: Option, + pub vpn_like: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IpAddrEntry { + pub iface: String, + pub if_type: String, + pub family: String, + pub address: String, + pub prefix_len: u8, + pub scope_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RouteEntry { + pub family: String, + pub destination: String, + pub gateway: Option, + pub interface: Option, + pub metric: Option, + pub flags: Vec, + pub detail: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NeighEntry { + pub iface: Option, + pub family: String, + pub ip: String, + pub mac: String, + pub vendor: Option, + pub tags: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SockEntry { + pub proto: String, + pub family: String, + pub local: String, + pub remote: Option, + pub state: Option, + pub pid: Option, + pub process: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SystemSummary { + pub hostname: String, + pub os: String, + pub kernel_version: Option, + pub default_interface: Option, + pub default_gateway: Option, + pub dns_servers: Vec, + pub proxy_http: Option, + pub proxy_https: Option, + pub proxy_all: Option, + pub proxy_no_proxy: Option, +} diff --git a/src/model/snapshot.rs b/src/model/snapshot.rs deleted file mode 100644 index b0cbfa2..0000000 --- a/src/model/snapshot.rs +++ /dev/null @@ -1,10 +0,0 @@ -use netdev::Interface; -use serde::{Deserialize, Serialize}; - -use crate::net::sys::SysInfo; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Snapshot { - pub sys: SysInfo, - pub interfaces: Vec, -} diff --git a/src/net/addr.rs b/src/net/addr.rs deleted file mode 100644 index 00f69e0..0000000 --- a/src/net/addr.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; - -pub enum AddressFamily { - Ipv4, - Ipv6, -} - -impl AddressFamily { - pub fn from_ip_addr(ip: &IpAddr) -> Self { - match ip { - IpAddr::V4(_) => AddressFamily::Ipv4, - IpAddr::V6(_) => AddressFamily::Ipv6, - } - } - - #[allow(dead_code)] - pub fn from_socket_addr(sock: &SocketAddr) -> Self { - match sock { - SocketAddr::V4(_) => AddressFamily::Ipv4, - SocketAddr::V6(_) => AddressFamily::Ipv6, - } - } - - pub fn unspecified_ip(&self) -> IpAddr { - match self { - AddressFamily::Ipv4 => IpAddr::V4(Ipv4Addr::UNSPECIFIED), - AddressFamily::Ipv6 => IpAddr::V6(Ipv6Addr::UNSPECIFIED), - } - } - - pub fn unspecified_sock(&self) -> SocketAddr { - SocketAddr::new(self.unspecified_ip(), 0) - } -} - -impl ToString for AddressFamily { - fn to_string(&self) -> String { - match self { - AddressFamily::Ipv4 => "IPv4".to_string(), - AddressFamily::Ipv6 => "IPv6".to_string(), - } - } -} - -#[allow(dead_code)] -pub fn unwrap_or_unspecified_ip(ip: Option, family: AddressFamily) -> IpAddr { - ip.unwrap_or_else(|| family.unspecified_ip()) -} - -#[allow(dead_code)] -pub fn unwrap_or_unspecified_sock(ip: Option, family: AddressFamily) -> SocketAddr { - ip.unwrap_or_else(|| family.unspecified_sock()) -} diff --git a/src/net/iface.rs b/src/net/iface.rs index 3804d1c..65fa8a0 100644 --- a/src/net/iface.rs +++ b/src/net/iface.rs @@ -25,10 +25,58 @@ pub fn get_all_interfaces() -> Vec { } pub fn get_default_interface() -> Option { - match netdev::get_default_interface() { - Ok(iface) => Some(iface), - Err(_) => None, + netdev::get_default_interface().ok() +} + +/// Return a practical primary interface with fallback. +/// Priority: OS default -> default-flagged non-loopback -> UP non-loopback -> first non-loopback -> first any. +pub fn get_primary_interface() -> Option { + if let Some(iface) = get_default_interface() { + if iface.if_type != InterfaceType::Loopback { + return Some(iface); + } } + + let mut all = get_all_interfaces(); + all.sort_by(|a, b| a.index.cmp(&b.index)); + + all.iter() + .find(|i| i.default && i.if_type != InterfaceType::Loopback) + .cloned() + .or_else(|| { + all.iter() + .find(|i| { + i.oper_state == netdev::interface::state::OperState::Up + && i.if_type != InterfaceType::Loopback + && i.gateway + .as_ref() + .is_some_and(|gw| !gw.ipv4.is_empty() || !gw.ipv6.is_empty()) + }) + .cloned() + }) + .or_else(|| { + all.iter() + .find(|i| { + i.oper_state == netdev::interface::state::OperState::Up + && i.if_type != InterfaceType::Loopback + && (!i.dns_servers.is_empty() || !i.ipv4.is_empty()) + }) + .cloned() + }) + .or_else(|| { + all.iter() + .find(|i| { + i.oper_state == netdev::interface::state::OperState::Up + && i.if_type != InterfaceType::Loopback + }) + .cloned() + }) + .or_else(|| { + all.iter() + .find(|i| i.if_type != InterfaceType::Loopback) + .cloned() + }) + .or_else(|| all.into_iter().next()) } pub fn get_interface_by_name(name: &str) -> Option { diff --git a/src/net/mod.rs b/src/net/mod.rs index 2c9c5cc..6de0ea0 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -1,4 +1,3 @@ -pub mod addr; pub mod iface; pub mod neigh; pub mod route; diff --git a/src/renderer/json.rs b/src/renderer/json.rs deleted file mode 100644 index be47575..0000000 --- a/src/renderer/json.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::{model::snapshot::Snapshot, net::sys::SysInfo}; -use anyhow::Result; -use netdev::Interface; -use serde::Serialize; - -pub fn print_interface_json(ifaces: &[Interface]) { - let json = serde_json::to_string_pretty(ifaces).unwrap(); - println!("{}", json); -} - -pub fn print_snapshot_json(sys: &SysInfo, default_iface: Option) { - let snapshot = Snapshot { - sys: sys.clone(), - interfaces: default_iface.into_iter().collect(), - }; - let json = serde_json::to_string_pretty(&snapshot).unwrap(); - println!("{}", json); -} - -pub fn pretty_print_json(data: &T) -> Result<()> { - let json = serde_json::to_string_pretty(data)?; - println!("{}", json); - Ok(()) -} diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 9a16455..978d7db 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -1,7 +1,39 @@ -pub mod json; -pub mod table; -pub mod tree; -pub mod yaml; +use anyhow::Result; +use comfy_table::{ + Cell, ContentArrangement, Table, modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL, +}; +use serde::Serialize; +use termtree::Tree; + +use crate::cli::{OutputArgs, OutputFormat}; +use crate::model::{ + IfDetail, IfSummary, IpAddrEntry, NeighEntry, RouteEntry, SockEntry, SystemSummary, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResolvedFormat { + Tree, + Table, + Json, + Yaml, +} + +#[derive(Debug, Serialize)] +struct AddrByIface { + iface: String, + if_type: String, + ipv4: Vec, + ipv6: Vec, +} + +pub fn resolve_output_format(out: &OutputArgs) -> ResolvedFormat { + match out.format { + OutputFormat::Tree => ResolvedFormat::Tree, + OutputFormat::Table => ResolvedFormat::Table, + OutputFormat::Json => ResolvedFormat::Json, + OutputFormat::Yaml => ResolvedFormat::Yaml, + } +} pub fn fmt_bps(bps: u64) -> String { const K: f64 = 1_000.0; @@ -18,5 +50,746 @@ pub fn fmt_bps(bps: u64) -> String { } pub fn fmt_flags(flags: u32) -> String { - format!("0x{:08X}", flags) + format!("0x{flags:08X}") +} + +fn make_table(headers: &[&str]) -> Table { + let mut table = Table::new(); + table + .load_preset(UTF8_FULL) + .apply_modifier(UTF8_ROUND_CORNERS) + .set_content_arrangement(ContentArrangement::Dynamic) + .set_header(headers.iter().map(|h| Cell::new(*h)).collect::>()); + table +} + +fn print_json(data: &T) -> Result<()> { + println!("{}", serde_json::to_string_pretty(data)?); + Ok(()) +} + +fn print_yaml(data: &T) -> Result<()> { + println!("{}", serde_yaml::to_string(data)?); + Ok(()) +} + +pub fn render_if_summaries(data: &[IfSummary], out: &OutputArgs) -> Result<()> { + match resolve_output_format(out) { + ResolvedFormat::Json => print_json(data), + ResolvedFormat::Yaml => print_yaml(data), + ResolvedFormat::Tree => { + let mut root = Tree::new("Interfaces".to_string()); + for item in data { + let mut node = Tree::new(format!( + "{}{}", + item.name, + if item.is_default { " (default)" } else { "" } + )); + node.push(Tree::new(format!("Index: {}", item.index))); + node.push(Tree::new(format!("Type: {}", item.if_type))); + node.push(Tree::new(format!("State: {}", item.state))); + node.push(Tree::new(format!( + "MAC: {}", + item.mac.as_deref().unwrap_or("-") + ))); + node.push(Tree::new(format!( + "MTU: {}", + item.mtu + .map(|v| v.to_string()) + .unwrap_or_else(|| "-".into()) + ))); + let mut ipv4 = Tree::new("IPv4".to_string()); + if item.ipv4_addrs.is_empty() { + ipv4.push(Tree::new("(none)".to_string())); + } else { + for addr in &item.ipv4_addrs { + ipv4.push(Tree::new(addr.clone())); + } + } + node.push(ipv4); + + let mut ipv6 = Tree::new("IPv6".to_string()); + if item.ipv6_addrs.is_empty() { + ipv6.push(Tree::new("(none)".to_string())); + } else { + for addr in &item.ipv6_addrs { + ipv6.push(Tree::new(addr.clone())); + } + } + node.push(ipv6); + + if item.gateway_mac.is_some() + || !item.gateway_ipv4.is_empty() + || !item.gateway_ipv6.is_empty() + { + let mut gateway = Tree::new("Gateway".to_string()); + if let Some(mac) = &item.gateway_mac { + gateway.push(Tree::new(format!("MAC: {}", mac))); + } + + let mut gw4 = Tree::new("IPv4".to_string()); + if item.gateway_ipv4.is_empty() { + gw4.push(Tree::new("(none)".to_string())); + } else { + for ip in &item.gateway_ipv4 { + gw4.push(Tree::new(ip.clone())); + } + } + gateway.push(gw4); + + let mut gw6 = Tree::new("IPv6".to_string()); + if item.gateway_ipv6.is_empty() { + gw6.push(Tree::new("(none)".to_string())); + } else { + for ip in &item.gateway_ipv6 { + gw6.push(Tree::new(ip.clone())); + } + } + gateway.push(gw6); + + node.push(gateway); + } + if let Some(vendor) = &item.vendor { + node.push(Tree::new(format!("Vendor: {vendor}"))); + } + root.push(node); + } + println!("{root}"); + Ok(()) + } + ResolvedFormat::Table => { + let mut table = make_table(&[ + "INDEX", "NAME", "TYPE", "STATE", "MAC", "MTU", "IPv4", "IPv6", "VENDOR", + ]); + for item in data { + table.add_row(vec![ + item.index.to_string(), + item.name.clone(), + item.if_type.clone(), + item.state.clone(), + item.mac.clone().unwrap_or_else(|| "-".into()), + item.mtu + .map(|v| v.to_string()) + .unwrap_or_else(|| "-".into()), + item.ipv4_addrs.len().to_string(), + item.ipv6_addrs.len().to_string(), + item.vendor.clone().unwrap_or_else(|| "-".into()), + ]); + } + println!("{table}"); + Ok(()) + } + } +} + +pub fn render_if_detail(data: &IfDetail, out: &OutputArgs) -> Result<()> { + match resolve_output_format(out) { + ResolvedFormat::Json => print_json(data), + ResolvedFormat::Yaml => print_yaml(data), + ResolvedFormat::Table => { + let mut table = make_table(&["FIELD", "VALUE"]); + table.add_row(vec!["name".into(), data.summary.name.clone()]); + table.add_row(vec!["index".into(), data.summary.index.to_string()]); + table.add_row(vec![ + "friendly_name".into(), + data.friendly_name.clone().unwrap_or_else(|| "-".into()), + ]); + table.add_row(vec!["type".into(), data.summary.if_type.clone()]); + table.add_row(vec!["state".into(), data.summary.state.clone()]); + table.add_row(vec![ + "mac".into(), + data.summary.mac.clone().unwrap_or_else(|| "-".into()), + ]); + table.add_row(vec![ + "mtu".into(), + data.summary + .mtu + .map(|m| m.to_string()) + .unwrap_or_else(|| "-".into()), + ]); + table.add_row(vec!["flags".into(), data.flags.clone()]); + table.add_row(vec![ + "tx_speed".into(), + data.tx_speed.clone().unwrap_or_else(|| "-".into()), + ]); + table.add_row(vec![ + "rx_speed".into(), + data.rx_speed.clone().unwrap_or_else(|| "-".into()), + ]); + table.add_row(vec![ + "gateway_mac".into(), + data.gateway_mac.clone().unwrap_or_else(|| "-".into()), + ]); + table.add_row(vec![ + "gateway_ipv4".into(), + if data.gateway_ipv4.is_empty() { + "-".into() + } else { + data.gateway_ipv4.join(", ") + }, + ]); + table.add_row(vec![ + "gateway_ipv6".into(), + if data.gateway_ipv6.is_empty() { + "-".into() + } else { + data.gateway_ipv6.join(", ") + }, + ]); + table.add_row(vec![ + "dns_servers".into(), + if data.dns_servers.is_empty() { + "-".into() + } else { + data.dns_servers.join(", ") + }, + ]); + table.add_row(vec![ + "stats_rx_bytes".into(), + data.stats_rx_bytes + .map(|v| v.to_string()) + .unwrap_or_else(|| "-".into()), + ]); + table.add_row(vec![ + "stats_tx_bytes".into(), + data.stats_tx_bytes + .map(|v| v.to_string()) + .unwrap_or_else(|| "-".into()), + ]); + table.add_row(vec!["vpn_like".into(), data.vpn_like.to_string()]); + println!("{table}"); + Ok(()) + } + ResolvedFormat::Tree => { + let mut root = Tree::new(data.summary.name.clone()); + root.push(Tree::new(format!("Index: {}", data.summary.index))); + if let Some(v) = &data.friendly_name { + root.push(Tree::new(format!("Friendly Name: {v}"))); + } + root.push(Tree::new(format!("Type: {}", data.summary.if_type))); + root.push(Tree::new(format!("State: {}", data.summary.state))); + root.push(Tree::new(format!( + "MAC: {}", + data.summary.mac.as_deref().unwrap_or("-") + ))); + root.push(Tree::new(format!( + "MTU: {}", + data.summary + .mtu + .map(|m| m.to_string()) + .unwrap_or_else(|| "-".into()) + ))); + root.push(Tree::new(format!("Flags: {}", data.flags))); + + if let Some(v) = &data.summary.vendor { + root.push(Tree::new(format!("Vendor: {v}"))); + } + if let Some(v) = &data.description { + root.push(Tree::new(format!("Description: {v}"))); + } + if let Some(v) = &data.tx_speed { + root.push(Tree::new(format!("TX Speed: {v}"))); + } + if let Some(v) = &data.rx_speed { + root.push(Tree::new(format!("RX Speed: {v}"))); + } + + let mut v4 = Tree::new("IPv4".to_string()); + for a in &data.ipv4 { + v4.push(Tree::new(a.clone())); + } + if !data.ipv4.is_empty() { + root.push(v4); + } + + let mut v6 = Tree::new("IPv6".to_string()); + for a in &data.ipv6_scoped { + v6.push(Tree::new(a.clone())); + } + if !data.ipv6_scoped.is_empty() { + root.push(v6); + } + + let mut dns = Tree::new("DNS".to_string()); + if data.dns_servers.is_empty() { + dns.push(Tree::new("(none)".to_string())); + } else { + for d in &data.dns_servers { + dns.push(Tree::new(d.clone())); + } + } + root.push(dns); + + if data.gateway_mac.is_some() + || !data.gateway_ipv4.is_empty() + || !data.gateway_ipv6.is_empty() + { + let mut gateway = Tree::new("Gateway".to_string()); + if let Some(mac) = &data.gateway_mac { + gateway.push(Tree::new(format!("MAC: {}", mac))); + } + + if !data.gateway_ipv4.is_empty() { + let mut gw4 = Tree::new("IPv4".to_string()); + for ip in &data.gateway_ipv4 { + gw4.push(Tree::new(ip.clone())); + } + gateway.push(gw4); + } + if !data.gateway_ipv6.is_empty() { + let mut gw6 = Tree::new("IPv6".to_string()); + for ip in &data.gateway_ipv6 { + gw6.push(Tree::new(ip.clone())); + } + gateway.push(gw6); + } + root.push(gateway); + } + + if data.stats_rx_bytes.is_some() || data.stats_tx_bytes.is_some() { + let mut stats = Tree::new("Statistics (snapshot)".to_string()); + if let Some(rx) = data.stats_rx_bytes { + stats.push(Tree::new(format!("RX bytes: {}", rx))); + } + if let Some(tx) = data.stats_tx_bytes { + stats.push(Tree::new(format!("TX bytes: {}", tx))); + } + root.push(stats); + } + + root.push(Tree::new(format!("VPN-like: {}", data.vpn_like))); + + println!("{root}"); + Ok(()) + } + } +} + +pub fn render_ip_entries(data: &[IpAddrEntry], out: &OutputArgs) -> Result<()> { + match resolve_output_format(out) { + ResolvedFormat::Json => { + let grouped = group_ip_entries_by_iface(data); + print_json(&grouped) + } + ResolvedFormat::Yaml => { + let grouped = group_ip_entries_by_iface(data); + print_yaml(&grouped) + } + ResolvedFormat::Tree => { + let mut root = Tree::new("IP Addresses".to_string()); + let mut ifaces: std::collections::BTreeMap, Vec)> = + std::collections::BTreeMap::new(); + + for e in data { + let entry = ifaces + .entry(e.iface.clone()) + .or_insert_with(|| (e.if_type.clone(), Vec::new(), Vec::new())); + let addr_label = if let Some(scope) = e.scope_id { + format!("{}/{} (scope_id={scope})", e.address, e.prefix_len) + } else { + format!("{}/{}", e.address, e.prefix_len) + }; + if e.family == "ipv4" { + entry.1.push(addr_label); + } else { + entry.2.push(addr_label); + } + } + + for (iface, (if_type, v4_addrs, v6_addrs)) in ifaces { + let mut iface_node = Tree::new(iface); + iface_node.push(Tree::new(format!("Type: {}", if_type))); + + let mut v4 = Tree::new("IPv4".to_string()); + if v4_addrs.is_empty() { + v4.push(Tree::new("(none)".to_string())); + } else { + for addr in v4_addrs { + v4.push(Tree::new(addr)); + } + } + iface_node.push(v4); + + let mut v6 = Tree::new("IPv6".to_string()); + if v6_addrs.is_empty() { + v6.push(Tree::new("(none)".to_string())); + } else { + for addr in v6_addrs { + v6.push(Tree::new(addr)); + } + } + iface_node.push(v6); + + root.push(iface_node); + } + println!("{root}"); + Ok(()) + } + ResolvedFormat::Table => { + let mut table = make_table(&["IFACE", "TYPE", "FAMILY", "ADDRESS", "PREFIX", "SCOPE"]); + for e in data { + table.add_row(vec![ + e.iface.clone(), + e.if_type.clone(), + e.family.clone(), + e.address.clone(), + e.prefix_len.to_string(), + e.scope_id + .map(|v| v.to_string()) + .unwrap_or_else(|| "-".into()), + ]); + } + println!("{table}"); + Ok(()) + } + } +} + +fn group_ip_entries_by_iface(data: &[IpAddrEntry]) -> Vec { + let mut grouped: std::collections::BTreeMap, Vec)> = + std::collections::BTreeMap::new(); + + for e in data { + let entry = grouped + .entry(e.iface.clone()) + .or_insert_with(|| (e.if_type.clone(), Vec::new(), Vec::new())); + let addr = format!("{}/{}", e.address, e.prefix_len); + if e.family == "ipv4" { + entry.1.push(addr); + } else { + entry.2.push(addr); + } + } + + grouped + .into_iter() + .map(|(iface, (if_type, mut ipv4, mut ipv6))| { + ipv4.sort(); + ipv6.sort(); + AddrByIface { + iface, + if_type, + ipv4, + ipv6, + } + }) + .collect() +} + +pub fn render_link_entries(data: &[IfDetail], out: &OutputArgs) -> Result<()> { + match resolve_output_format(out) { + ResolvedFormat::Json => print_json(data), + ResolvedFormat::Yaml => print_yaml(data), + ResolvedFormat::Tree => { + let mut root = Tree::new("Link Layer".to_string()); + for item in data { + let mut node = Tree::new(item.summary.name.clone()); + node.push(Tree::new(format!("Index: {}", item.summary.index))); + node.push(Tree::new(format!("Type: {}", item.summary.if_type))); + node.push(Tree::new(format!( + "MAC: {}", + item.summary.mac.as_deref().unwrap_or("-") + ))); + node.push(Tree::new(format!( + "MTU: {}", + item.summary + .mtu + .map(|m| m.to_string()) + .unwrap_or_else(|| "-".into()) + ))); + node.push(Tree::new(format!("State: {}", item.summary.state))); + node.push(Tree::new(format!("Flags: {}", item.flags))); + node.push(Tree::new(format!( + "Speed(TX/RX): {}/{}", + item.tx_speed.as_deref().unwrap_or("-"), + item.rx_speed.as_deref().unwrap_or("-") + ))); + root.push(node); + } + println!("{root}"); + Ok(()) + } + ResolvedFormat::Table => { + let mut table = make_table(&[ + "INDEX", "IFACE", "TYPE", "MAC", "MTU", "STATE", "FLAGS", "SPEED", + ]); + for item in data { + table.add_row(vec![ + item.summary.index.to_string(), + item.summary.name.clone(), + item.summary.if_type.clone(), + item.summary.mac.clone().unwrap_or_else(|| "-".into()), + item.summary + .mtu + .map(|m| m.to_string()) + .unwrap_or_else(|| "-".into()), + item.summary.state.clone(), + item.flags.clone(), + format!( + "{}/{}", + item.tx_speed.as_deref().unwrap_or("-"), + item.rx_speed.as_deref().unwrap_or("-") + ), + ]); + } + println!("{table}"); + Ok(()) + } + } +} + +pub fn render_routes(data: &[RouteEntry], out: &OutputArgs) -> Result<()> { + match resolve_output_format(out) { + ResolvedFormat::Json => print_json(data), + ResolvedFormat::Yaml => print_yaml(data), + ResolvedFormat::Tree => { + let mut root = Tree::new("Routes".to_string()); + for r in data { + let mut node = Tree::new(format!("{} {}", r.family, r.destination)); + node.push(Tree::new(format!( + "Gateway: {}", + r.gateway.as_deref().unwrap_or("-") + ))); + node.push(Tree::new(format!( + "Interface: {}", + r.interface.as_deref().unwrap_or("-") + ))); + node.push(Tree::new(format!( + "Metric: {}", + r.metric + .map(|v| v.to_string()) + .unwrap_or_else(|| "-".into()) + ))); + if !r.flags.is_empty() { + node.push(Tree::new(format!("Flags: {}", r.flags.join(",")))); + } + if let Some(d) = &r.detail { + node.push(Tree::new(format!("Detail: {d}"))); + } + root.push(node); + } + println!("{root}"); + Ok(()) + } + ResolvedFormat::Table => { + let mut table = + make_table(&["FAMILY", "DESTINATION", "GATEWAY", "DEV", "METRIC", "FLAGS"]); + for r in data { + table.add_row(vec![ + r.family.clone(), + r.destination.clone(), + r.gateway.clone().unwrap_or_else(|| "-".into()), + r.interface.clone().unwrap_or_else(|| "-".into()), + r.metric + .map(|m| m.to_string()) + .unwrap_or_else(|| "-".into()), + if r.flags.is_empty() { + "-".into() + } else { + r.flags.join("") + }, + ]); + } + println!("{table}"); + Ok(()) + } + } +} + +pub fn render_neighbors(data: &[NeighEntry], out: &OutputArgs) -> Result<()> { + match resolve_output_format(out) { + ResolvedFormat::Json => print_json(data), + ResolvedFormat::Yaml => print_yaml(data), + ResolvedFormat::Tree => { + let mut root = Tree::new("Neighbors".to_string()); + for n in data { + let mut node = Tree::new(format!("{} {}", n.family, n.ip)); + node.push(Tree::new(format!("MAC: {}", n.mac))); + if let Some(v) = &n.vendor { + node.push(Tree::new(format!("Vendor: {v}"))); + } + if !n.tags.is_empty() { + node.push(Tree::new(format!("Tags: {}", n.tags.join(", ")))); + } + root.push(node); + } + println!("{root}"); + Ok(()) + } + ResolvedFormat::Table => { + let mut table = make_table(&["FAMILY", "IP", "MAC", "VENDOR", "TAGS"]); + for n in data { + table.add_row(vec![ + n.family.clone(), + n.ip.clone(), + n.mac.clone(), + n.vendor.clone().unwrap_or_else(|| "-".into()), + if n.tags.is_empty() { + "-".into() + } else { + n.tags.join(", ") + }, + ]); + } + println!("{table}"); + Ok(()) + } + } +} + +pub fn render_sockets(data: &[SockEntry], out: &OutputArgs, include_pid: bool) -> Result<()> { + match resolve_output_format(out) { + ResolvedFormat::Json => print_json(data), + ResolvedFormat::Yaml => print_yaml(data), + ResolvedFormat::Tree => { + let mut root = Tree::new("Sockets".to_string()); + for s in data { + let mut node = Tree::new(format!( + "{} [{}] {} -> {}", + s.proto, + s.family, + s.local, + s.remote.as_deref().unwrap_or("-") + )); + if let Some(state) = &s.state { + node.push(Tree::new(format!("State: {state}"))); + } + if include_pid { + node.push(Tree::new(format!( + "PID: {}", + s.pid.map(|v| v.to_string()).unwrap_or_else(|| "-".into()) + ))); + node.push(Tree::new(format!( + "Process: {}", + s.process.as_deref().unwrap_or("-") + ))); + } + root.push(node); + } + println!("{root}"); + Ok(()) + } + ResolvedFormat::Table => { + let mut headers = vec!["PROTO", "FAMILY", "LOCAL", "REMOTE", "STATE"]; + if include_pid { + headers.push("PID"); + headers.push("PROCESS"); + } + let mut table = make_table(&headers); + for s in data { + let mut row = vec![ + s.proto.clone(), + s.family.clone(), + s.local.clone(), + s.remote.clone().unwrap_or_else(|| "-".into()), + s.state.clone().unwrap_or_else(|| "-".into()), + ]; + if include_pid { + row.push(s.pid.map(|v| v.to_string()).unwrap_or_else(|| "-".into())); + row.push(s.process.clone().unwrap_or_else(|| "-".into())); + } + table.add_row(row); + } + println!("{table}"); + Ok(()) + } + } +} + +pub fn render_system_summary(data: &SystemSummary, out: &OutputArgs) -> Result<()> { + match resolve_output_format(out) { + ResolvedFormat::Json => print_json(data), + ResolvedFormat::Yaml => print_yaml(data), + ResolvedFormat::Table => { + let mut table = make_table(&["FIELD", "VALUE"]); + table.add_row(vec!["hostname".into(), data.hostname.clone()]); + table.add_row(vec!["os".into(), data.os.clone()]); + table.add_row(vec![ + "kernel".into(), + data.kernel_version.clone().unwrap_or_else(|| "-".into()), + ]); + table.add_row(vec![ + "default_interface".into(), + data.default_interface.clone().unwrap_or_else(|| "-".into()), + ]); + table.add_row(vec![ + "default_gateway".into(), + data.default_gateway.clone().unwrap_or_else(|| "-".into()), + ]); + table.add_row(vec![ + "dns_servers".into(), + if data.dns_servers.is_empty() { + "-".into() + } else { + data.dns_servers.join(", ") + }, + ]); + table.add_row(vec![ + "http_proxy".into(), + data.proxy_http.clone().unwrap_or_else(|| "-".into()), + ]); + table.add_row(vec![ + "https_proxy".into(), + data.proxy_https.clone().unwrap_or_else(|| "-".into()), + ]); + table.add_row(vec![ + "all_proxy".into(), + data.proxy_all.clone().unwrap_or_else(|| "-".into()), + ]); + table.add_row(vec![ + "no_proxy".into(), + data.proxy_no_proxy.clone().unwrap_or_else(|| "-".into()), + ]); + println!("{table}"); + Ok(()) + } + ResolvedFormat::Tree => { + let mut root = Tree::new(format!("System {}", data.hostname)); + root.push(Tree::new(format!("OS: {}", data.os))); + root.push(Tree::new(format!( + "Kernel: {}", + data.kernel_version.as_deref().unwrap_or("-") + ))); + root.push(Tree::new(format!( + "Default Interface: {}", + data.default_interface.as_deref().unwrap_or("-") + ))); + root.push(Tree::new(format!( + "Default Gateway: {}", + data.default_gateway.as_deref().unwrap_or("-") + ))); + + let mut dns = Tree::new("DNS".to_string()); + if data.dns_servers.is_empty() { + dns.push(Tree::new("-".to_string())); + } else { + for item in &data.dns_servers { + dns.push(Tree::new(item.clone())); + } + } + root.push(dns); + + let mut proxy = Tree::new("Proxy".to_string()); + proxy.push(Tree::new(format!( + "HTTP: {}", + data.proxy_http.as_deref().unwrap_or("-") + ))); + proxy.push(Tree::new(format!( + "HTTPS: {}", + data.proxy_https.as_deref().unwrap_or("-") + ))); + proxy.push(Tree::new(format!( + "ALL: {}", + data.proxy_all.as_deref().unwrap_or("-") + ))); + proxy.push(Tree::new(format!( + "NO_PROXY: {}", + data.proxy_no_proxy.as_deref().unwrap_or("-") + ))); + root.push(proxy); + + println!("{root}"); + Ok(()) + } + } } diff --git a/src/renderer/table.rs b/src/renderer/table.rs deleted file mode 100644 index fa76c26..0000000 --- a/src/renderer/table.rs +++ /dev/null @@ -1,25 +0,0 @@ -use comfy_table::{ - Cell, Color, ContentArrangement, Table, modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL, -}; - -/// Create a preconfigured comfy-table instance with consistent styling. -pub fn make_table(headers: &[&str]) -> Table { - let mut table = Table::new(); - table - .load_preset(UTF8_FULL) - .apply_modifier(UTF8_ROUND_CORNERS) - .set_content_arrangement(ContentArrangement::Dynamic); - - // cyan header - let header_cells = headers - .iter() - .map(|h| Cell::new(*h).fg(Color::Cyan)) - .collect::>(); - table.set_header(header_cells); - table -} - -/* /// Helper for colored header text -pub fn header_cell(text: &str) -> Cell { - Cell::new(text).fg(Color::Cyan) -} */ diff --git a/src/renderer/tree.rs b/src/renderer/tree.rs deleted file mode 100644 index da408ad..0000000 --- a/src/renderer/tree.rs +++ /dev/null @@ -1,4 +0,0 @@ -/// Convert a string into a tree label. -pub fn tree_label>(s: S) -> String { - s.into() -} diff --git a/src/renderer/yaml.rs b/src/renderer/yaml.rs deleted file mode 100644 index 4cc8091..0000000 --- a/src/renderer/yaml.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::{model::snapshot::Snapshot, net::sys::SysInfo}; -use anyhow::Result; -use netdev::Interface; -use serde::Serialize; - -pub fn print_interface_yaml(ifaces: &[Interface]) { - let yaml = serde_yaml::to_string(ifaces).unwrap(); - println!("{}", yaml); -} - -pub fn print_snapshot_yaml(sys: &SysInfo, default_iface: Option) { - let snapshot = Snapshot { - sys: sys.clone(), - interfaces: default_iface.into_iter().collect(), - }; - let yaml = serde_yaml::to_string(&snapshot).unwrap(); - println!("{}", yaml); -} - -pub fn print_yaml(data: &T) -> Result<()> { - let yaml = serde_yaml::to_string(data)?; - println!("{}", yaml); - Ok(()) -}