├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── ci.yaml │ ├── publish-crate.yaml │ ├── release.yaml │ └── require-changelog-for-PRs.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Cross.toml ├── INSTALL.md ├── LICENSE.md ├── README.md ├── build.rs ├── res ├── bandwhich-inkscape.svg ├── bandwhich.svg └── demo.gif ├── rustfmt.toml └── src ├── cli.rs ├── display ├── components │ ├── display_bandwidth.rs │ ├── header_details.rs │ ├── help_text.rs │ ├── layout.rs │ ├── mod.rs │ ├── snapshots │ │ └── bandwhich__display__components__display_bandwidth__tests__bandwidth_formatting.snap │ └── table.rs ├── mod.rs ├── raw_terminal_backend.rs ├── ui.rs └── ui_state.rs ├── main.rs ├── network ├── connection.rs ├── dns │ ├── client.rs │ ├── mod.rs │ └── resolver.rs ├── mod.rs ├── sniffer.rs └── utilization.rs ├── os ├── errors.rs ├── linux.rs ├── lsof.rs ├── lsof_utils.rs ├── mod.rs ├── shared.rs └── windows.rs └── tests ├── cases ├── mod.rs ├── raw_mode.rs ├── snapshots │ ├── bandwhich__tests__cases__raw_mode__bi_directional_traffic.snap │ ├── bandwhich__tests__cases__raw_mode__multiple_connections_from_remote_address.snap │ ├── bandwhich__tests__cases__raw_mode__multiple_packets_of_traffic_from_different_connections.snap │ ├── bandwhich__tests__cases__raw_mode__multiple_packets_of_traffic_from_single_connection.snap │ ├── bandwhich__tests__cases__raw_mode__multiple_processes_with_multiple_connections.snap │ ├── bandwhich__tests__cases__raw_mode__no_resolve_mode.snap │ ├── bandwhich__tests__cases__raw_mode__one_ip_packet_of_traffic.snap │ ├── bandwhich__tests__cases__raw_mode__one_packet_of_traffic.snap │ ├── bandwhich__tests__cases__raw_mode__one_process_with_multiple_connections.snap │ ├── bandwhich__tests__cases__raw_mode__sustained_traffic_from_multiple_processes.snap │ ├── bandwhich__tests__cases__raw_mode__sustained_traffic_from_multiple_processes_bi_directional.snap │ ├── bandwhich__tests__cases__raw_mode__sustained_traffic_from_one_process.snap │ ├── bandwhich__tests__cases__raw_mode__traffic_with_host_names.snap │ ├── bandwhich__tests__cases__ui__basic_only_addresses.snap │ ├── bandwhich__tests__cases__ui__basic_only_connections.snap │ ├── bandwhich__tests__cases__ui__basic_only_processes.snap │ ├── bandwhich__tests__cases__ui__basic_processes_with_dns_queries.snap │ ├── bandwhich__tests__cases__ui__basic_startup-2.snap │ ├── bandwhich__tests__cases__ui__basic_startup.snap │ ├── bandwhich__tests__cases__ui__bi_directional_traffic-2.snap │ ├── bandwhich__tests__cases__ui__bi_directional_traffic.snap │ ├── bandwhich__tests__cases__ui__layout-full-width-under-30-height-draw_events.snap │ ├── bandwhich__tests__cases__ui__layout-full-width-under-30-height-events.snap │ ├── bandwhich__tests__cases__ui__layout-under-120-width-full-height-draw_events.snap │ ├── bandwhich__tests__cases__ui__layout-under-120-width-full-height-events.snap │ ├── bandwhich__tests__cases__ui__layout-under-120-width-under-30-height-draw_events.snap │ ├── bandwhich__tests__cases__ui__layout-under-120-width-under-30-height-events.snap │ ├── bandwhich__tests__cases__ui__layout-under-50-width-under-50-height-draw_events.snap │ ├── bandwhich__tests__cases__ui__layout-under-50-width-under-50-height-events.snap │ ├── bandwhich__tests__cases__ui__layout-under-70-width-under-30-height-draw_events.snap │ ├── bandwhich__tests__cases__ui__layout-under-70-width-under-30-height-events.snap │ ├── bandwhich__tests__cases__ui__multiple_connections_from_remote_address-2.snap │ ├── bandwhich__tests__cases__ui__multiple_connections_from_remote_address.snap │ ├── bandwhich__tests__cases__ui__multiple_packets_of_traffic_from_different_connections-2.snap │ ├── bandwhich__tests__cases__ui__multiple_packets_of_traffic_from_different_connections.snap │ ├── bandwhich__tests__cases__ui__multiple_packets_of_traffic_from_single_connection-2.snap │ ├── bandwhich__tests__cases__ui__multiple_packets_of_traffic_from_single_connection.snap │ ├── bandwhich__tests__cases__ui__multiple_processes_with_multiple_connections-2.snap │ ├── bandwhich__tests__cases__ui__multiple_processes_with_multiple_connections.snap │ ├── bandwhich__tests__cases__ui__no_resolve_mode-2.snap │ ├── bandwhich__tests__cases__ui__no_resolve_mode.snap │ ├── bandwhich__tests__cases__ui__one_packet_of_traffic-2.snap │ ├── bandwhich__tests__cases__ui__one_packet_of_traffic.snap │ ├── bandwhich__tests__cases__ui__one_process_with_multiple_connections-2.snap │ ├── bandwhich__tests__cases__ui__one_process_with_multiple_connections.snap │ ├── bandwhich__tests__cases__ui__pause_by_space-2.snap │ ├── bandwhich__tests__cases__ui__pause_by_space.snap │ ├── bandwhich__tests__cases__ui__rearranged_by_tab-2.snap │ ├── bandwhich__tests__cases__ui__rearranged_by_tab.snap │ ├── bandwhich__tests__cases__ui__sustained_traffic_from_multiple_processes-2.snap │ ├── bandwhich__tests__cases__ui__sustained_traffic_from_multiple_processes.snap │ ├── bandwhich__tests__cases__ui__sustained_traffic_from_multiple_processes_bi_directional-2.snap │ ├── bandwhich__tests__cases__ui__sustained_traffic_from_multiple_processes_bi_directional.snap │ ├── bandwhich__tests__cases__ui__sustained_traffic_from_multiple_processes_bi_directional_total-2.snap │ ├── bandwhich__tests__cases__ui__sustained_traffic_from_multiple_processes_bi_directional_total.snap │ ├── bandwhich__tests__cases__ui__sustained_traffic_from_multiple_processes_total-2.snap │ ├── bandwhich__tests__cases__ui__sustained_traffic_from_multiple_processes_total.snap │ ├── bandwhich__tests__cases__ui__sustained_traffic_from_one_process-2.snap │ ├── bandwhich__tests__cases__ui__sustained_traffic_from_one_process.snap │ ├── bandwhich__tests__cases__ui__sustained_traffic_from_one_process_total-2.snap │ ├── bandwhich__tests__cases__ui__sustained_traffic_from_one_process_total.snap │ ├── bandwhich__tests__cases__ui__traffic_with_host_names-2.snap │ ├── bandwhich__tests__cases__ui__traffic_with_host_names.snap │ ├── bandwhich__tests__cases__ui__traffic_with_winch_event-2.snap │ ├── bandwhich__tests__cases__ui__traffic_with_winch_event.snap │ ├── bandwhich__tests__cases__ui__truncate_long_hostnames-2.snap │ ├── bandwhich__tests__cases__ui__truncate_long_hostnames.snap │ ├── bandwhich__tests__cases__ui__two_packets_only_addresses.snap │ ├── bandwhich__tests__cases__ui__two_packets_only_connections.snap │ ├── bandwhich__tests__cases__ui__two_packets_only_processes.snap │ ├── bandwhich__tests__cases__ui__two_windows_split_horizontally.snap │ └── bandwhich__tests__cases__ui__two_windows_split_vertically.snap ├── test_utils.rs └── ui.rs ├── fakes ├── fake_input.rs ├── fake_output.rs └── mod.rs └── mod.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [imsnif] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 30 8 | groups: 9 | dependencies: 10 | patterns: 11 | - "*" 12 | - package-ecosystem: github-actions 13 | directory: "/" 14 | schedule: 15 | interval: monthly 16 | open-pull-requests-limit: 30 17 | groups: 18 | github-actions: 19 | patterns: 20 | - "*" 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | jobs: 9 | get-msrv: 10 | name: Get declared MSRV from Cargo.toml 11 | runs-on: ubuntu-latest 12 | outputs: 13 | msrv: ${{ steps.get_msrv.outputs.msrv }} 14 | steps: 15 | - name: Install ripgrep 16 | run: sudo apt-get install -y ripgrep 17 | 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Get MSRV 22 | id: get_msrv 23 | run: rg '^\s*rust-version\s*=\s*"(\d+(\.\d+){0,2})"' --replace 'msrv=$1' Cargo.toml >> "$GITHUB_OUTPUT" 24 | 25 | check-fmt: 26 | name: Check code formatting 27 | runs-on: ubuntu-latest 28 | needs: get-msrv 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | rust: 33 | - ${{ needs.get-msrv.outputs.msrv }} 34 | - stable 35 | - nightly 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | 40 | - name: Install Rust 41 | uses: dtolnay/rust-toolchain@master 42 | with: 43 | toolchain: ${{ matrix.rust }} 44 | components: rustfmt 45 | 46 | - name: Check formatting 47 | run: cargo fmt --all -- --check 48 | 49 | test: 50 | name: Test on each target 51 | needs: get-msrv 52 | env: 53 | # use sccache 54 | # It's too much of a hassle to set up sccache in cross. 55 | # See https://github.com/cross-rs/cross/wiki/Recipes#sccache. 56 | SCCACHE_GHA_ENABLED: ${{ matrix.cargo == 'cargo' && 'true' || 'false'}} 57 | RUSTC_WRAPPER: ${{ matrix.cargo == 'cargo' && 'sccache' || '' }} 58 | # Emit backtraces on panics. 59 | RUST_BACKTRACE: 1 60 | runs-on: ${{ matrix.os }} 61 | strategy: 62 | fail-fast: false 63 | matrix: 64 | build: 65 | - android-aarch64 66 | - linux-aarch64-gnu 67 | - linux-aarch64-musl 68 | - linux-armv7-gnueabihf 69 | - linux-armv7-musleabihf 70 | - linux-x64-gnu 71 | - linux-x64-musl 72 | - macos-aarch64 73 | - macos-x64 74 | - windows-x64-msvc 75 | rust: 76 | - ${{ needs.get-msrv.outputs.msrv }} 77 | - stable 78 | - nightly 79 | include: 80 | - os: ubuntu-latest # default 81 | - cargo: cargo # default; overwrite with `cross` if necessary 82 | - build: android-aarch64 83 | target: aarch64-linux-android 84 | cargo: cross 85 | - build: linux-aarch64-gnu 86 | target: aarch64-unknown-linux-gnu 87 | cargo: cross 88 | - build: linux-aarch64-musl 89 | target: aarch64-unknown-linux-musl 90 | cargo: cross 91 | - build: linux-armv7-gnueabihf 92 | target: armv7-unknown-linux-gnueabihf 93 | cargo: cross 94 | - build: linux-armv7-musleabihf 95 | target: armv7-unknown-linux-musleabihf 96 | cargo: cross 97 | - build: linux-x64-gnu 98 | target: x86_64-unknown-linux-gnu 99 | - build: linux-x64-musl 100 | target: x86_64-unknown-linux-musl 101 | - build: macos-aarch64 102 | # Go back ot `macos-latest` after migration is complete 103 | # See https://github.blog/changelog/2024-04-01-macos-14-sonoma-is-generally-available-and-the-latest-macos-runner-image/. 104 | os: macos-14 105 | target: aarch64-apple-darwin 106 | - build: macos-x64 107 | os: macos-14 108 | target: x86_64-apple-darwin 109 | - build: windows-x64-msvc 110 | os: windows-latest 111 | target: x86_64-pc-windows-msvc 112 | steps: 113 | - name: Checkout repository 114 | uses: actions/checkout@v4 115 | 116 | - name: Install Rust 117 | uses: dtolnay/rust-toolchain@master 118 | with: 119 | toolchain: ${{ matrix.rust }} 120 | targets: ${{ matrix.target }} 121 | components: clippy 122 | 123 | - name: Set up sccache 124 | # It's too much of a hassle to set up sccache in cross. 125 | # See https://github.com/cross-rs/cross/wiki/Recipes#sccache. 126 | if: matrix.cargo == 'cargo' 127 | uses: mozilla-actions/sccache-action@v0.0.7 128 | 129 | - name: Install cross 130 | if: matrix.cargo == 'cross' 131 | # The latest release of `cross` is not able to build/link for `aarch64-linux-android` 132 | # See: https://github.com/cross-rs/cross/issues/1222 133 | # This is fixed on `main` but not yet released. To avoid a breakage somewhen in the future 134 | # pin the cross revision used to the latest HEAD at 04/2024. 135 | # Go back to taiki-e/install-action once cross 0.3 is released. 136 | uses: taiki-e/cache-cargo-install-action@v2 137 | with: 138 | tool: cross 139 | git: https://github.com/cross-rs/cross.git 140 | rev: 085092c 141 | 142 | - name: Build 143 | id: build 144 | run: ${{ matrix.cargo }} build --verbose --target ${{ matrix.target }} 145 | 146 | # This is useful for debugging problems when the expected build artifacts 147 | # (like shell completions and man pages) aren't generated. 148 | - name: Show build.rs stderr 149 | shell: bash 150 | run: | 151 | # it's probably okay to assume no spaces? 152 | STDERR_FILES=$(find "./target/debug" -name stderr | grep bandwhich || true) 153 | for FILE in $STDERR_FILES; do 154 | echo "::group::$FILE" 155 | cat "$FILE" 156 | echo "::endgroup::" 157 | done 158 | 159 | - name: Run clippy 160 | run: ${{ matrix.cargo }} clippy --all-targets --target ${{ matrix.target }} -- -D warnings 161 | 162 | - name: Install npcap on Windows 163 | # PRs from other repositories cannot be trusted with repository secrets 164 | if: matrix.os == 'windows-latest' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) 165 | env: 166 | NPCAP_OEM_URL: ${{ secrets.NPCAP_OEM_URL }} 167 | run: | 168 | Invoke-WebRequest -Uri "$env:NPCAP_OEM_URL" -OutFile "$env:TEMP/npcap-oem.exe" 169 | # for this ridiculous `&` syntax alone, I'd rather use COBOL than Powershell 170 | # see https://stackoverflow.com/a/1674950/5637701 171 | & "$env:TEMP/npcap-oem.exe" /S 172 | 173 | - name: Run tests 174 | id: run_tests 175 | # npcap is needed to run tests on Windows, so unfortunately we cannot run tests 176 | # on PRs from other repositories 177 | if: matrix.os != 'windows-latest' || github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository 178 | env: 179 | # make insta generate new snapshots in CI 180 | INSTA_UPDATE: new 181 | run: ${{ matrix.cargo }} test --all-targets --target ${{ matrix.target }} 182 | 183 | - name: Upload snapshots of failed tests 184 | if: ${{ failure() && steps.run_tests.outcome == 'failure' }} 185 | uses: actions/upload-artifact@v4 186 | with: 187 | name: ${{ matrix.os }}-${{ matrix.rust }}-failed_snapshots 188 | path: '**/*.snap.new' 189 | 190 | - name: Upload binaries 191 | if: ${{ success() || steps.build.outcome == 'success' }} 192 | uses: actions/upload-artifact@v4 193 | with: 194 | name: ${{ matrix.target }}-${{ matrix.rust }} 195 | path: | 196 | target/${{ matrix.target }}/debug/bandwhich 197 | target/${{ matrix.target }}/debug/bandwhich.exe 198 | target/${{ matrix.target }}/debug/bandwhich.pdb 199 | -------------------------------------------------------------------------------- /.github/workflows/publish-crate.yaml: -------------------------------------------------------------------------------- 1 | # This workflow triggers when a stable release is published on GitHub. 2 | # 3 | # The crates.io token used for publishing was created under the account of 4 | # cyqsimon <28627918+cyqsimon@users.noreply.github.com> and was added to this 5 | # repository's secrets by Aram Drevekenin . 6 | 7 | name: publish-crate 8 | on: 9 | release: 10 | types: 11 | - released 12 | workflow_dispatch: 13 | 14 | jobs: 15 | publish-to-crates-io: 16 | name: Publish to crates.io 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Install Rust 23 | uses: dtolnay/rust-toolchain@master 24 | with: 25 | toolchain: stable 26 | 27 | - name: Run cargo publish 28 | uses: katyo/publish-crates@v2 29 | with: 30 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | # The way this works is the following: 2 | # 3 | # - create-release job runs purely to initialize the GitHub release itself 4 | # and to output upload_url for the following job. 5 | # 6 | # - build-release job runs only once create-release is finished. It gets 7 | # the release upload URL from create-release job outputs, then builds 8 | # the release executables for each supported platform and attaches them 9 | # as release assets to the previously created release. 10 | # 11 | # Reference: 12 | # - https://eugene-babichenko.github.io/blog/2020/05/09/github-actions-cross-platform-auto-releases/ 13 | # 14 | # Currently this workflow only ever creates drafts; the draft should be checked 15 | # and then released manually. 16 | 17 | name: release 18 | on: 19 | push: 20 | tags: 21 | - "v[0-9]+.[0-9]+.[0-9]+" 22 | workflow_dispatch: 23 | 24 | jobs: 25 | create-release: 26 | name: create-release 27 | runs-on: ubuntu-latest 28 | outputs: 29 | upload_url: ${{ steps.create_release.outputs.upload_url }} 30 | steps: 31 | - name: create_release 32 | id: create_release 33 | uses: actions/create-release@v1 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | with: 37 | tag_name: ${{ github.ref_name }} 38 | release_name: Release ${{ github.ref_name }} 39 | # draft: ${{ github.event_name == 'workflow_dispatch' }} 40 | draft: true 41 | prerelease: false 42 | 43 | build-release: 44 | name: build-release 45 | needs: create-release 46 | runs-on: ${{ matrix.os }} 47 | env: 48 | # Emit backtraces on panics. 49 | RUST_BACKTRACE: 1 50 | BANDWHICH_GEN_DIR: assets 51 | PKGDIR: github-actions-pkg 52 | strategy: 53 | matrix: 54 | build: 55 | - android-aarch64 56 | - linux-aarch64-gnu 57 | - linux-aarch64-musl 58 | - linux-armv7-gnueabihf 59 | - linux-armv7-musleabihf 60 | - linux-x64-gnu 61 | - linux-x64-musl 62 | - macos-aarch64 63 | - macos-x64 64 | - windows-x64-msvc 65 | include: 66 | - os: ubuntu-latest # default 67 | - cargo: cargo # default; overwrite with `cross` if necessary 68 | - build: android-aarch64 69 | target: aarch64-linux-android 70 | cargo: cross 71 | - build: linux-aarch64-gnu 72 | target: aarch64-unknown-linux-gnu 73 | cargo: cross 74 | - build: linux-aarch64-musl 75 | target: aarch64-unknown-linux-musl 76 | cargo: cross 77 | - build: linux-armv7-gnueabihf 78 | target: armv7-unknown-linux-gnueabihf 79 | cargo: cross 80 | - build: linux-armv7-musleabihf 81 | target: armv7-unknown-linux-musleabihf 82 | cargo: cross 83 | - build: linux-x64-gnu 84 | target: x86_64-unknown-linux-gnu 85 | - build: linux-x64-musl 86 | target: x86_64-unknown-linux-musl 87 | - build: macos-aarch64 88 | # Go back ot `macos-latest` after migration is complete 89 | # See https://github.blog/changelog/2024-04-01-macos-14-sonoma-is-generally-available-and-the-latest-macos-runner-image/. 90 | os: macos-14 91 | target: aarch64-apple-darwin 92 | - build: macos-x64 93 | os: macos-14 94 | target: x86_64-apple-darwin 95 | - build: windows-x64-msvc 96 | os: windows-latest 97 | target: x86_64-pc-windows-msvc 98 | 99 | steps: 100 | - name: Checkout repository 101 | uses: actions/checkout@v4 102 | 103 | - name: Install Rust 104 | uses: dtolnay/rust-toolchain@master 105 | with: 106 | toolchain: stable 107 | targets: ${{ matrix.target }} 108 | 109 | - name: Install cross 110 | if: matrix.cargo == 'cross' 111 | # The latest release of `cross` is not able to build/link for `aarch64-linux-android` 112 | # See: https://github.com/cross-rs/cross/issues/1222 113 | # This is fixed on `main` but not yet released. To avoid a breakage somewhen in the future 114 | # pin the cross revision used to the latest HEAD at 04/2024. 115 | # Go back to taiki-e/install-action once cross 0.3 is released. 116 | uses: taiki-e/cache-cargo-install-action@v2 117 | with: 118 | tool: cross 119 | git: https://github.com/cross-rs/cross.git 120 | rev: 085092c 121 | 122 | - name: Build release binary 123 | shell: bash 124 | env: 125 | RUSTFLAGS: "-C strip=symbols" 126 | run: | 127 | mkdir -p "$BANDWHICH_GEN_DIR" 128 | ${{ matrix.cargo }} build --verbose --release --target ${{ matrix.target }} 129 | 130 | - name: Collect build artifacts 131 | shell: bash 132 | env: 133 | BANDWHICH_BIN: ${{ contains(matrix.os, 'windows') && 'bandwhich.exe' || 'bandwhich' }} 134 | run: | 135 | mkdir "$PKGDIR" 136 | mv "target/${{ matrix.target }}/release/$BANDWHICH_BIN" "$PKGDIR" 137 | mv "$BANDWHICH_GEN_DIR" "$PKGDIR" 138 | 139 | - name: Tar release (Unix) 140 | if: ${{ !contains(matrix.os, 'windows') }} 141 | working-directory: ${{ env.PKGDIR }} 142 | run: tar cvfz bandwhich-${{ github.ref_name }}-${{ matrix.target }}.tar.gz * 143 | 144 | - name: Zip release (Windows) 145 | if: contains(matrix.os, 'windows') 146 | working-directory: ${{ env.PKGDIR }} 147 | run: Compress-Archive -Path * -DestinationPath bandwhich-${{ github.ref_name }}-${{ matrix.target }}.zip 148 | 149 | - name: Upload release archive 150 | uses: actions/upload-release-asset@v1 151 | env: 152 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 153 | ARCHIVE_EXT: ${{ contains(matrix.os, 'windows') && 'zip' || 'tar.gz' }} 154 | with: 155 | upload_url: ${{ needs.create-release.outputs.upload_url }} 156 | asset_path: ${{ env.PKGDIR }}/bandwhich-${{ github.ref_name }}-${{ matrix.target }}.${{ env.ARCHIVE_EXT }} 157 | asset_name: bandwhich-${{ github.ref_name }}-${{ matrix.target }}.${{ env.ARCHIVE_EXT }} 158 | asset_content_type: application/octet-stream 159 | -------------------------------------------------------------------------------- /.github/workflows/require-changelog-for-PRs.yml: -------------------------------------------------------------------------------- 1 | name: Changelog 2 | 3 | on: 4 | pull_request: 5 | 6 | env: 7 | PR_NUMBER: ${{ github.event.number }} 8 | PR_BASE: ${{ github.base_ref }} 9 | 10 | jobs: 11 | get-submitter: 12 | name: Get the username of the PR submitter 13 | runs-on: ubuntu-latest 14 | outputs: 15 | submitter: ${{ steps.get-submitter.outputs.submitter }} 16 | steps: 17 | # cannot use `github.actor`: the triggering commit may be authored by a maintainer 18 | - name: Get PR submitter 19 | id: get-submitter 20 | run: curl -sSfL https://api.github.com/repos/imsnif/bandwhich/pulls/${PR_NUMBER} | jq -r '"submitter=" + .user.login' | tee -a $GITHUB_OUTPUT 21 | 22 | check-changelog: 23 | name: Check for changelog entry 24 | needs: get-submitter 25 | env: 26 | PR_SUBMITTER: ${{ needs.get-submitter.outputs.submitter }} 27 | runs-on: ubuntu-latest 28 | # allow dependabot PRs to have no changelog 29 | if: ${{ needs.get-submitter.outputs.submitter != 'dependabot[bot]' }} 30 | steps: 31 | - uses: actions/checkout@v4 32 | 33 | - name: Fetch PR base 34 | run: git fetch --no-tags --prune --depth=1 origin 35 | 36 | - name: Search for added line in changelog 37 | run: | 38 | ADDED=$(git diff -U0 "origin/${PR_BASE}" HEAD -- CHANGELOG.md | grep -P '^\+[^\+].+$') 39 | echo "Added lines in CHANGELOG.md:" 40 | echo "$ADDED" 41 | echo "Grepping for PR info:" 42 | grep -P "(#|pull/)${PR_NUMBER}\\b.*@${PR_SUBMITTER}\\b" <<< "$ADDED" 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at aram@poor.dev. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributions of any kind are very welcome. If you'd like a new feature (or found a bug), please open an issue or a PR. 2 | 3 | To set up your development environment: 4 | 1. Clone the project 5 | 2. `cargo run`, or if you prefer `cargo run -- -i ` (you can often find out the name with `ifconfig` or `iwconfig`). You might need root privileges to run this application, so be sure to use (for example) sudo. 6 | 7 | To run tests: `cargo test` 8 | 9 | After tests, check the formatting: `cargo fmt -- --check` 10 | 11 | Note that at the moment the tests do not test the os layer (anything in the `os` folder). 12 | 13 | If you are stuck, unsure about how to approach an issue or would like some guidance, 14 | you are welcome to [open an issue](https://github.com/imsnif/bandwhich/issues/new); 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bandwhich" 3 | version = "0.23.1" 4 | authors = [ 5 | "Aram Drevekenin ", 6 | "Eduardo Toledo ", 7 | "Eduardo Broto ", 8 | "Kelvin Zhang ", 9 | "Brooks Rady ", 10 | "cyqsimon <28627918+cyqsimon@users.noreply.github.com>", 11 | ] 12 | categories = ["network-programming", "command-line-utilities"] 13 | edition = "2021" 14 | exclude = ["src/tests/*", "demo.gif"] 15 | homepage = "https://github.com/imsnif/bandwhich" 16 | keywords = ["networking", "utilization", "cli"] 17 | license = "MIT" 18 | readme = "README.md" 19 | repository = "https://github.com/imsnif/bandwhich" 20 | rust-version = "1.75.0" 21 | description = "Display current network utilization by process, connection and remote IP/hostname" 22 | 23 | [features] 24 | default = [] 25 | # UI tests temporarily disabled by default, until big refactor is done 26 | ui_test = [] 27 | 28 | [dependencies] 29 | async-trait = "0.1.86" 30 | chrono = "0.4" 31 | clap-verbosity-flag = "3.0.2" 32 | clap = { version = "4.5.28", features = ["derive"] } 33 | crossterm = "0.28.1" 34 | derive_more = { version = "2.0.1", features = ["debug"] } 35 | eyre = "0.6.12" 36 | itertools = "0.14.0" 37 | log = "0.4.25" 38 | once_cell = "1.20.2" 39 | pnet = "0.35.0" 40 | pnet_macros_support = "0.35.0" 41 | ratatui = "0.29.0" 42 | resolv-conf = "0.7.0" 43 | simplelog = "0.12.2" 44 | thiserror = "2.0.11" 45 | tokio = { version = "1.43", features = ["rt", "sync"] } 46 | trust-dns-resolver = "0.23.2" 47 | unicode-width = "0.2.0" 48 | strum = { version = "0.26.3", features = ["derive"] } 49 | 50 | 51 | [target.'cfg(any(target_os = "android", target_os = "linux"))'.dependencies] 52 | procfs = "0.17.0" 53 | 54 | [target.'cfg(any(target_os = "macos", target_os = "freebsd"))'.dependencies] 55 | regex = "1.11.1" 56 | 57 | [target.'cfg(target_os = "windows")'.dependencies] 58 | netstat2 = "0.11.1" 59 | sysinfo = "0.33.1" 60 | 61 | [dev-dependencies] 62 | insta = "1.42.1" 63 | packet-builder = { version = "0.7.0", git = "https://github.com/cyqsimon/packet_builder.git", branch = "patch-pnet-0.35" } 64 | pnet_base = "0.35.0" 65 | regex = "1.11.1" 66 | rstest = "0.24.0" 67 | 68 | [build-dependencies] 69 | clap = { version = "4.5.28", features = ["derive"] } 70 | clap-verbosity-flag = "3.0.2" 71 | clap_complete = "4.5.44" 72 | clap_mangen = "0.2.26" 73 | derive_more = { version = "2.0.1", features = ["debug"] } 74 | eyre = "0.6.12" 75 | strum = { version = "0.26.3", features = ["derive"] } 76 | 77 | [target.'cfg(target_os = "windows")'.build-dependencies] 78 | http_req = "0.13.1" 79 | zip = "2.2.2" 80 | 81 | [profile.release] 82 | codegen-units = 1 83 | opt-level = 3 84 | lto = "fat" 85 | panic = "abort" 86 | strip = "symbols" 87 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [build.env] 2 | passthrough = ["BANDWHICH_GEN_DIR"] 3 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | - [Installation](#installation) 4 | - [Arch Linux](#arch-linux) 5 | - [Exherbo Linux](#exherbo-linux) 6 | - [Nix/NixOS](#nixnixos) 7 | - [Void Linux](#void-linux) 8 | - [Fedora](#fedora) 9 | - [macOS/Linux (using Homebrew)](#macoslinux-using-homebrew) 10 | - [macOS (using MacPorts)](#macos-using-macports) 11 | - [FreeBSD](#freebsd) 12 | - [Cargo](#cargo) 13 | 14 | ## Arch Linux 15 | 16 | ``` 17 | pacman -S bandwhich 18 | ``` 19 | 20 | ## Exherbo Linux 21 | 22 | `bandwhich` is available in [rust repository](https://gitlab.exherbo.org/exherbo/rust/-/tree/master/packages/sys-apps/bandwhich), and can be installed via `cave`: 23 | 24 | ``` 25 | cave resolve -x repository/rust 26 | cave resolve -x bandwhich 27 | ``` 28 | 29 | ## Nix/NixOS 30 | 31 | `bandwhich` is available in [`nixpkgs`](https://github.com/nixos/nixpkgs/blob/master/pkgs/tools/networking/bandwhich/default.nix), and can be installed, for example, with `nix-env`: 32 | 33 | ``` 34 | nix-env -iA nixpkgs.bandwhich 35 | ``` 36 | 37 | ## Void Linux 38 | 39 | ``` 40 | xbps-install -S bandwhich 41 | ``` 42 | 43 | ## Fedora 44 | 45 | `bandwhich` is available in [COPR](https://copr.fedorainfracloud.org/coprs/atim/bandwhich/), and can be installed via DNF: 46 | 47 | ``` 48 | sudo dnf copr enable atim/bandwhich -y && sudo dnf install bandwhich 49 | ``` 50 | 51 | ## macOS/Linux (using Homebrew) 52 | 53 | ``` 54 | brew install bandwhich 55 | ``` 56 | 57 | ## macOS (using MacPorts) 58 | 59 | ``` 60 | sudo port selfupdate 61 | sudo port install bandwhich 62 | ``` 63 | 64 | ## FreeBSD 65 | 66 | ``` 67 | pkg install bandwhich 68 | ``` 69 | 70 | or 71 | 72 | ``` 73 | cd /usr/ports/net-mgmt/bandwhich && make install clean 74 | ``` 75 | 76 | ## Cargo 77 | 78 | Regardless of OS, you can always fallback to the Rust package manager, `cargo`. 79 | For installation instructions of the Rust toolchain, see [here](https://www.rust-lang.org/tools/install). 80 | 81 | ``` 82 | cargo install bandwhich 83 | ``` 84 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Aram Drevekenin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bandwhich 2 | 3 | ![demo](res/demo.gif) 4 | 5 | This is a CLI utility for displaying current network utilization by process, connection and remote IP/hostname 6 | 7 | ## Table of contents 8 | 9 | - [bandwhich](#bandwhich) 10 | - [Table of contents](#table-of-contents) 11 | - [Project status](#project-status) 12 | - [How does it work?](#how-does-it-work) 13 | - [Installation](#installation) 14 | - [Downstream packaging status](#downstream-packaging-status) 15 | - [Download a prebuilt binary](#download-a-prebuilt-binary) 16 | - [Building from source](#building-from-source) 17 | - [Cross-compiling](#cross-compiling) 18 | - [Android](#android) 19 | - [Post install (Linux)](#post-install-linux) 20 | - [1. `setcap`](#1-setcap) 21 | - [Capabilities explained](#capabilities-explained) 22 | - [2. `sudo` (or alternative)](#2-sudo-or-alternative) 23 | - [Post install (Windows)](#post-install-windows) 24 | - [Usage](#usage) 25 | - [Contributing](#contributing) 26 | - [License](#license) 27 | 28 | ## Project status 29 | 30 | This project is in passive maintenance. Critical issues will be addressed, but 31 | no new features are being worked on. However, this is due to a lack of funding 32 | and/or manpower more than anything else, so pull requests are more than welcome. 33 | In addition, if you are able and willing to contribute to this project long-term, 34 | we would like to invite you to apply for co-maintainership. 35 | 36 | For more details, see [The Future of Bandwhich #275](https://github.com/imsnif/bandwhich/issues/275). 37 | 38 | ## How does it work? 39 | 40 | `bandwhich` sniffs a given network interface and records IP packet size, cross referencing it with the `/proc` filesystem on linux, `lsof` on macOS, or using WinApi on windows. It is responsive to the terminal window size, displaying less info if there is no room for it. It will also attempt to resolve ips to their host name in the background using reverse DNS on a best effort basis. 41 | 42 | ## Installation 43 | 44 | ### Downstream packaging status 45 | 46 | For detailed instructions for each platform, see [INSTALL.md](INSTALL.md). 47 | 48 | 49 | Packaging status 50 | 51 | 52 | ### Download a prebuilt binary 53 | 54 | We offer several generic binaries in [releases](https://github.com/imsnif/bandwhich/releases) for various OSes. 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 |
OSArchitectureSupportUsage
Androidaarch64Best effort 64 |

All modern Android devices.

65 |

Note that this is a pure binary file, not an APK suitable for general usage.

66 |
Linuxaarch64Full64-bit ARMv8+ (servers, some modern routers, RPi-4+).
armv7hfBest effort32-bit ARMv7 (older routers, pre-RPi-4).
x64FullMost Linux desktops & servers.
MacOSaarch64FullApple silicon Macs (2021+).
x64Intel Macs (pre-2021).
Windowsx64FullMost Windows desktops & servers.
93 | 94 | ## Building from source 95 | 96 | ```sh 97 | git clone https://github.com/imsnif/bandwhich.git 98 | cd bandwhich 99 | cargo build --release 100 | ``` 101 | 102 | For the up-to-date minimum supported Rust version, please refer to the `rust-version` field in [Cargo.toml](Cargo.toml). 103 | 104 | ### Cross-compiling 105 | 106 | Cross-compiling for alternate targets is supported via [cross](https://github.com/cross-rs/cross). Here's the rough procedure: 107 | 108 | 1. Check the target architecture. If on Linux, you can use `uname -m`. 109 | 2. Lookup [rustc platform support](https://doc.rust-lang.org/rustc/platform-support.html) for the corresponding target triple. 110 | 3. [Install `cross`](https://github.com/cross-rs/cross#installation). 111 | 4. Run `cross build --release --target `. 112 | 113 | #### Android 114 | 115 | Until [cross-rs/cross#1222](https://github.com/cross-rs/cross/issues/1222) is solved, use the latest HEAD: 116 | 117 | ```sh 118 | cargo install --git https://github.com/cross-rs/cross.git cross 119 | cross build --release --target aarch64-linux-android 120 | ``` 121 | 122 | ## Post install (Linux) 123 | 124 | Since `bandwhich` sniffs network packets, it requires elevated privileges. 125 | On Linux, there are two main ways to accomplish this: 126 | 127 | ### 1. `setcap` 128 | 129 | - Permanently allow the `bandwhich` binary its required privileges (called "capabilities" in Linux). 130 | - Do this if you want to give all unprivileged users full access to bandwhich's monitoring capabilities. 131 | - This is the **recommended** setup **for single user machines**, or **if all users are trusted**. 132 | - This is **not recommended** if you want to **ensure users cannot see others' traffic**. 133 | 134 | ```sh 135 | # assign capabilities 136 | sudo setcap cap_sys_ptrace,cap_dac_read_search,cap_net_raw,cap_net_admin+ep $(command -v bandwhich) 137 | # run as unprivileged user 138 | bandwhich 139 | ``` 140 | 141 | #### Capabilities explained 142 | - `cap_sys_ptrace,cap_dac_read_search`: allow access to `/proc//fd/`, so that `bandwhich` can determine which open port belongs to which process. 143 | - `cap_net_raw,cap_net_admin`: allow capturing packets on your system. 144 | 145 | ### 2. `sudo` (or alternative) 146 | 147 | - Require privilege escalation every time. 148 | - Do this if you are an administrator of a multi-user environment. 149 | 150 | ```sh 151 | sudo bandwhich 152 | ``` 153 | 154 | Note that if your installation method installed `bandwhich` to somewhere in 155 | your home directory (you can check with `command -v bandwhich`), you may get a 156 | `command not found` error. This is because in many distributions, `sudo` by 157 | default does not keep your user's `$PATH` for safety concerns. 158 | 159 | To overcome this, you can do any one of the following: 160 | 1. [make `sudo` preserve your `$PATH` environment variable](https://unix.stackexchange.com/q/83191/375550); 161 | 2. explicitly set `$PATH` while running `bandwhich`: `sudo env "PATH=$PATH" bandwhich`; 162 | 3. pass the full path to `sudo`: `sudo $(command -v bandwhich)`. 163 | 164 | ## Post install (Windows) 165 | 166 | You might need to first install [npcap](https://npcap.com/#download) for capturing packets on Windows. 167 | 168 | ## Usage 169 | 170 | ``` 171 | Usage: bandwhich [OPTIONS] 172 | 173 | Options: 174 | -i, --interface The network interface to listen on, eg. eth0 175 | -r, --raw Machine friendlier output 176 | -n, --no-resolve Do not attempt to resolve IPs to their hostnames 177 | -s, --show-dns Show DNS queries 178 | -d, --dns-server A dns server ip to use instead of the system default 179 | --log-to Enable debug logging to a file 180 | -v, --verbose... Increase logging verbosity 181 | -q, --quiet... Decrease logging verbosity 182 | -p, --processes Show processes table only 183 | -c, --connections Show connections table only 184 | -a, --addresses Show remote addresses table only 185 | -u, --unit-family Choose a specific family of units [default: bin-bytes] [possible values: bin-bytes, bin-bits, si-bytes, si-bits] 186 | -t, --total-utilization Show total (cumulative) usages 187 | -h, --help Print help (see more with '--help') 188 | -V, --version Print version 189 | ``` 190 | 191 | ## Contributing 192 | 193 | See [CONTRIBUTING.md](CONTRIBUTING.md). 194 | 195 | ## License 196 | 197 | MIT 198 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs::File}; 2 | 3 | use clap::CommandFactory; 4 | use clap_complete::Shell; 5 | use clap_mangen::Man; 6 | use eyre::eyre; 7 | 8 | fn main() { 9 | build_completion_manpage().unwrap(); 10 | 11 | #[cfg(target_os = "windows")] 12 | download_windows_npcap_sdk().unwrap(); 13 | } 14 | 15 | include!("src/cli.rs"); 16 | 17 | fn build_completion_manpage() -> eyre::Result<()> { 18 | let mut cmd = Opt::command(); 19 | 20 | // build into `BANDWHICH_GEN_DIR` with a fallback to `OUT_DIR` 21 | let gen_dir: PathBuf = env::var_os("BANDWHICH_GEN_DIR") 22 | .or_else(|| env::var_os("OUT_DIR")) 23 | .ok_or(eyre!("OUT_DIR is unset"))? 24 | .into(); 25 | 26 | // completion 27 | for &shell in Shell::value_variants() { 28 | clap_complete::generate_to(shell, &mut cmd, "bandwhich", &gen_dir)?; 29 | } 30 | 31 | // manpage 32 | let mut manpage_out = File::create(gen_dir.join("bandwhich.1"))?; 33 | let manpage = Man::new(cmd); 34 | manpage.render(&mut manpage_out)?; 35 | 36 | Ok(()) 37 | } 38 | 39 | #[cfg(target_os = "windows")] 40 | fn download_windows_npcap_sdk() -> eyre::Result<()> { 41 | use std::{ 42 | fs, 43 | io::{self, Write}, 44 | }; 45 | 46 | use http_req::request; 47 | use zip::ZipArchive; 48 | 49 | println!("cargo:rerun-if-changed=build.rs"); 50 | 51 | // get npcap SDK 52 | const NPCAP_SDK: &str = "npcap-sdk-1.13.zip"; 53 | 54 | let npcap_sdk_download_url = format!("https://npcap.com/dist/{NPCAP_SDK}"); 55 | let cache_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?).join("target"); 56 | let npcap_sdk_cache_path = cache_dir.join(NPCAP_SDK); 57 | 58 | let npcap_zip = match fs::read(&npcap_sdk_cache_path) { 59 | // use cached 60 | Ok(zip_data) => { 61 | eprintln!("Found cached npcap SDK"); 62 | zip_data 63 | } 64 | // download SDK 65 | Err(_) => { 66 | eprintln!("Downloading npcap SDK"); 67 | 68 | // download 69 | let mut zip_data = vec![]; 70 | let _res = request::get(npcap_sdk_download_url, &mut zip_data)?; 71 | 72 | // write cache 73 | fs::create_dir_all(cache_dir)?; 74 | let mut cache = fs::File::create(npcap_sdk_cache_path)?; 75 | cache.write_all(&zip_data)?; 76 | 77 | zip_data 78 | } 79 | }; 80 | 81 | // extract DLL 82 | let lib_path = if cfg!(target_arch = "aarch64") { 83 | "Lib/ARM64/Packet.lib" 84 | } else if cfg!(target_arch = "x86_64") { 85 | "Lib/x64/Packet.lib" 86 | } else if cfg!(target_arch = "x86") { 87 | "Lib/Packet.lib" 88 | } else { 89 | panic!("Unsupported target!") 90 | }; 91 | let mut archive = ZipArchive::new(io::Cursor::new(npcap_zip))?; 92 | let mut npcap_lib = archive.by_name(lib_path)?; 93 | 94 | // write DLL 95 | let lib_dir = PathBuf::from(env::var("OUT_DIR")?).join("npcap_sdk"); 96 | let lib_path = lib_dir.join("Packet.lib"); 97 | fs::create_dir_all(&lib_dir)?; 98 | let mut lib_file = fs::File::create(lib_path)?; 99 | io::copy(&mut npcap_lib, &mut lib_file)?; 100 | 101 | println!( 102 | "cargo:rustc-link-search=native={}", 103 | lib_dir 104 | .to_str() 105 | .ok_or(eyre!("{lib_dir:?} is not valid UTF-8"))? 106 | ); 107 | 108 | Ok(()) 109 | } 110 | -------------------------------------------------------------------------------- /res/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imsnif/bandwhich/362aa89427df102615a9f92be2f4431498cc1253/res/demo.gif -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imsnif/bandwhich/362aa89427df102615a9f92be2f4431498cc1253/rustfmt.toml -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::{net::Ipv4Addr, path::PathBuf}; 2 | 3 | use clap::{Args, Parser, ValueEnum, ValueHint}; 4 | use clap_verbosity_flag::{InfoLevel, Verbosity}; 5 | use derive_more::Debug; 6 | use strum::EnumIter; 7 | 8 | #[derive(Clone, Debug, Parser, Default)] 9 | #[command(name = "bandwhich", version)] 10 | pub struct Opt { 11 | #[arg(short, long)] 12 | /// The network interface to listen on, eg. eth0 13 | pub interface: Option, 14 | 15 | #[arg(short, long)] 16 | /// Machine friendlier output 17 | pub raw: bool, 18 | 19 | #[arg(short, long)] 20 | /// Do not attempt to resolve IPs to their hostnames 21 | pub no_resolve: bool, 22 | 23 | #[arg(short, long)] 24 | /// Show DNS queries 25 | pub show_dns: bool, 26 | 27 | #[arg(short, long)] 28 | /// A dns server ip to use instead of the system default 29 | pub dns_server: Option, 30 | 31 | #[arg(long, value_hint = ValueHint::FilePath)] 32 | /// Enable debug logging to a file 33 | pub log_to: Option, 34 | 35 | #[command(flatten)] 36 | pub verbosity: Verbosity, 37 | 38 | #[command(flatten)] 39 | pub render_opts: RenderOpts, 40 | } 41 | 42 | #[derive(Copy, Clone, Debug, Default, Args)] 43 | pub struct RenderOpts { 44 | #[arg(short, long)] 45 | /// Show processes table only 46 | pub processes: bool, 47 | 48 | #[arg(short, long)] 49 | /// Show connections table only 50 | pub connections: bool, 51 | 52 | #[arg(short, long)] 53 | /// Show remote addresses table only 54 | pub addresses: bool, 55 | 56 | #[arg(short, long, value_enum, default_value_t)] 57 | /// Choose a specific family of units 58 | pub unit_family: UnitFamily, 59 | 60 | #[arg(short, long)] 61 | /// Show total (cumulative) usages 62 | pub total_utilization: bool, 63 | } 64 | 65 | // IMPRV: it would be nice if we can `#[cfg_attr(not(build), derive(strum::EnumIter))]` this 66 | // unfortunately there is no configuration option for build script detection 67 | #[derive(Copy, Clone, Debug, Default, Eq, PartialEq, ValueEnum, EnumIter)] 68 | pub enum UnitFamily { 69 | #[default] 70 | /// bytes, in powers of 2^10 71 | BinBytes, 72 | /// bits, in powers of 2^10 73 | BinBits, 74 | /// bytes, in powers of 10^3 75 | SiBytes, 76 | /// bits, in powers of 10^3 77 | SiBits, 78 | } 79 | -------------------------------------------------------------------------------- /src/display/components/display_bandwidth.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use derive_more::Debug; 4 | 5 | use crate::cli::UnitFamily; 6 | 7 | #[derive(Copy, Clone, Debug)] 8 | pub struct DisplayBandwidth { 9 | // Custom format for reduced precision. 10 | // Workaround for FP calculation discrepancy between Unix and Windows. 11 | // See https://github.com/rust-lang/rust/issues/111405#issuecomment-2055964223. 12 | #[debug("{bandwidth:.10e}")] 13 | pub bandwidth: f64, 14 | pub unit_family: BandwidthUnitFamily, 15 | } 16 | 17 | impl fmt::Display for DisplayBandwidth { 18 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 19 | let (div, suffix) = self.unit_family.get_unit_for(self.bandwidth); 20 | write!(f, "{:.2}{suffix}", self.bandwidth / div) 21 | } 22 | } 23 | 24 | /// Type wrapper around [`UnitFamily`] to provide extra functionality. 25 | #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] 26 | #[debug("{_0:?}")] 27 | pub struct BandwidthUnitFamily(UnitFamily); 28 | impl From for BandwidthUnitFamily { 29 | fn from(value: UnitFamily) -> Self { 30 | Self(value) 31 | } 32 | } 33 | impl BandwidthUnitFamily { 34 | #[inline] 35 | /// Returns an array of tuples, corresponding to the steps of this unit family. 36 | /// 37 | /// Each step contains a divisor, an upper bound, and a unit suffix. 38 | fn steps(&self) -> [(f64, f64, &'static str); 6] { 39 | /// The fraction of the next unit the value has to meet to step up. 40 | const STEP_UP_FRAC: f64 = 0.95; 41 | /// Binary base: 2^10. 42 | const BB: f64 = 1024.0; 43 | 44 | use UnitFamily as F; 45 | // probably could macro this stuff, but I'm too lazy 46 | match self.0 { 47 | F::BinBytes => [ 48 | (1.0, BB * STEP_UP_FRAC, "B"), 49 | (BB, BB.powi(2) * STEP_UP_FRAC, "KiB"), 50 | (BB.powi(2), BB.powi(3) * STEP_UP_FRAC, "MiB"), 51 | (BB.powi(3), BB.powi(4) * STEP_UP_FRAC, "GiB"), 52 | (BB.powi(4), BB.powi(5) * STEP_UP_FRAC, "TiB"), 53 | (BB.powi(5), f64::MAX, "PiB"), 54 | ], 55 | F::BinBits => [ 56 | (1.0 / 8.0, BB / 8.0 * STEP_UP_FRAC, "b"), 57 | (BB / 8.0, BB.powi(2) / 8.0 * STEP_UP_FRAC, "Kib"), 58 | (BB.powi(2) / 8.0, BB.powi(3) / 8.0 * STEP_UP_FRAC, "Mib"), 59 | (BB.powi(3) / 8.0, BB.powi(4) / 8.0 * STEP_UP_FRAC, "Gib"), 60 | (BB.powi(4) / 8.0, BB.powi(5) / 8.0 * STEP_UP_FRAC, "Tib"), 61 | (BB.powi(5) / 8.0, f64::MAX, "Pib"), 62 | ], 63 | F::SiBytes => [ 64 | (1.0, 1e3 * STEP_UP_FRAC, "B"), 65 | (1e3, 1e6 * STEP_UP_FRAC, "kB"), 66 | (1e6, 1e9 * STEP_UP_FRAC, "MB"), 67 | (1e9, 1e12 * STEP_UP_FRAC, "GB"), 68 | (1e12, 1e15 * STEP_UP_FRAC, "TB"), 69 | (1e15, f64::MAX, "PB"), 70 | ], 71 | F::SiBits => [ 72 | (1.0 / 8.0, 1e3 / 8.0 * STEP_UP_FRAC, "b"), 73 | (1e3 / 8.0, 1e6 / 8.0 * STEP_UP_FRAC, "kb"), 74 | (1e6 / 8.0, 1e9 / 8.0 * STEP_UP_FRAC, "Mb"), 75 | (1e9 / 8.0, 1e12 / 8.0 * STEP_UP_FRAC, "Gb"), 76 | (1e12 / 8.0, 1e15 / 8.0 * STEP_UP_FRAC, "Tb"), 77 | (1e15 / 8.0, f64::MAX, "Pb"), 78 | ], 79 | } 80 | } 81 | 82 | /// Select a unit for a given value, returning its divisor and suffix. 83 | fn get_unit_for(&self, bytes: f64) -> (f64, &'static str) { 84 | let Some((div, _, suffix)) = self 85 | .steps() 86 | .into_iter() 87 | .find(|&(_, bound, _)| bound >= bytes) 88 | else { 89 | panic!("Cannot select an appropriate unit for {bytes:.2}B.") 90 | }; 91 | 92 | (div, suffix) 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use std::fmt::Write; 99 | 100 | use insta::assert_snapshot; 101 | use itertools::Itertools; 102 | use strum::IntoEnumIterator; 103 | 104 | use crate::{cli::UnitFamily, display::DisplayBandwidth}; 105 | 106 | #[test] 107 | fn bandwidth_formatting() { 108 | let test_bandwidths_formatted = UnitFamily::iter() 109 | .map_into() 110 | .cartesian_product( 111 | // I feel like this is a decent selection of values 112 | (-6..60) 113 | .map(|exp| 2f64.powi(exp)) 114 | .chain((-5..45).map(|exp| 2.5f64.powi(exp))) 115 | .chain((-4..38).map(|exp| 3f64.powi(exp))) 116 | .chain((-3..26).map(|exp| 5f64.powi(exp))), 117 | ) 118 | .map(|(unit_family, bandwidth)| DisplayBandwidth { 119 | bandwidth, 120 | unit_family, 121 | }) 122 | .fold(String::new(), |mut buf, b| { 123 | let _ = writeln!(buf, "{b:?}: {b}"); 124 | buf 125 | }); 126 | 127 | assert_snapshot!(test_bandwidths_formatted); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/display/components/header_details.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | use ratatui::{ 4 | layout::{Alignment, Rect}, 5 | style::{Color, Modifier, Style}, 6 | text::Span, 7 | widgets::Paragraph, 8 | Frame, 9 | }; 10 | use unicode_width::UnicodeWidthStr; 11 | 12 | use crate::display::{DisplayBandwidth, UIState}; 13 | 14 | pub fn elapsed_time(last_start_time: Instant, cumulative_time: Duration, paused: bool) -> Duration { 15 | if paused { 16 | cumulative_time 17 | } else { 18 | cumulative_time + last_start_time.elapsed() 19 | } 20 | } 21 | 22 | fn format_duration(d: Duration) -> String { 23 | let s = d.as_secs(); 24 | let days = match s / 86400 { 25 | 0 => "".to_string(), 26 | 1 => "1 day, ".to_string(), 27 | n => format!("{n} days, "), 28 | }; 29 | format!( 30 | "{days}{:02}:{:02}:{:02}", 31 | (s / 3600) % 24, 32 | (s / 60) % 60, 33 | s % 60, 34 | ) 35 | } 36 | 37 | pub struct HeaderDetails<'a> { 38 | pub state: &'a UIState, 39 | pub elapsed_time: Duration, 40 | pub paused: bool, 41 | } 42 | 43 | impl HeaderDetails<'_> { 44 | pub fn render(&self, frame: &mut Frame, rect: Rect) { 45 | let bandwidth = self.bandwidth_string(); 46 | let color = if self.paused { 47 | Color::Yellow 48 | } else { 49 | Color::Green 50 | }; 51 | 52 | // do not render time in tests, otherwise the output becomes non-deterministic 53 | // see: https://github.com/imsnif/bandwhich/issues/303 54 | if cfg!(not(test)) && self.state.cumulative_mode { 55 | let elapsed_time = format_duration(self.elapsed_time); 56 | // only render if there is enough width 57 | if bandwidth.width() + 1 + elapsed_time.width() <= rect.width as usize { 58 | self.render_elapsed_time(frame, rect, &elapsed_time, color); 59 | } 60 | } 61 | 62 | self.render_bandwidth(frame, rect, &bandwidth, color); 63 | } 64 | 65 | fn render_bandwidth(&self, frame: &mut Frame, rect: Rect, bandwidth: &str, color: Color) { 66 | let bandwidth_text = Span::styled( 67 | bandwidth, 68 | Style::default().fg(color).add_modifier(Modifier::BOLD), 69 | ); 70 | 71 | let paragraph = Paragraph::new(bandwidth_text).alignment(Alignment::Left); 72 | frame.render_widget(paragraph, rect); 73 | } 74 | 75 | fn bandwidth_string(&self) -> String { 76 | let intrf = self.state.interface_name.as_deref().unwrap_or("all"); 77 | let t = if self.state.cumulative_mode { 78 | "Data" 79 | } else { 80 | "Rate" 81 | }; 82 | let unit_family = self.state.unit_family; 83 | let up = DisplayBandwidth { 84 | bandwidth: self.state.total_bytes_uploaded as f64, 85 | unit_family, 86 | }; 87 | let down = DisplayBandwidth { 88 | bandwidth: self.state.total_bytes_downloaded as f64, 89 | unit_family, 90 | }; 91 | let paused = if self.paused { " [PAUSED]" } else { "" }; 92 | format!("IF: {intrf} | Total {t} (Up / Down): {up} / {down}{paused}") 93 | } 94 | 95 | fn render_elapsed_time(&self, frame: &mut Frame, rect: Rect, elapsed_time: &str, color: Color) { 96 | let elapsed_time_text = Span::styled( 97 | elapsed_time, 98 | Style::default().fg(color).add_modifier(Modifier::BOLD), 99 | ); 100 | let paragraph = Paragraph::new(elapsed_time_text).alignment(Alignment::Right); 101 | frame.render_widget(paragraph, rect); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/display/components/help_text.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | layout::{Alignment, Rect}, 3 | style::{Modifier, Style}, 4 | text::Span, 5 | widgets::Paragraph, 6 | Frame, 7 | }; 8 | 9 | pub struct HelpText { 10 | pub paused: bool, 11 | pub show_dns: bool, 12 | } 13 | 14 | const FIRST_WIDTH_BREAKPOINT: u16 = 76; 15 | const SECOND_WIDTH_BREAKPOINT: u16 = 54; 16 | 17 | const TEXT_WHEN_PAUSED: &str = " Press to resume."; 18 | const TEXT_WHEN_NOT_PAUSED: &str = " Press to pause."; 19 | const TEXT_WHEN_DNS_NOT_SHOWN: &str = " (DNS queries hidden)."; 20 | const TEXT_WHEN_DNS_SHOWN: &str = " (DNS queries shown)."; 21 | const TEXT_TAB_TIP: &str = " Use to rearrange tables."; 22 | 23 | impl HelpText { 24 | pub fn render(&self, frame: &mut Frame, rect: Rect) { 25 | let pause_content = if self.paused { 26 | TEXT_WHEN_PAUSED 27 | } else { 28 | TEXT_WHEN_NOT_PAUSED 29 | }; 30 | 31 | let dns_content = if rect.width <= FIRST_WIDTH_BREAKPOINT { 32 | "" 33 | } else if self.show_dns { 34 | TEXT_WHEN_DNS_SHOWN 35 | } else { 36 | TEXT_WHEN_DNS_NOT_SHOWN 37 | }; 38 | 39 | let tab_text = if rect.width <= SECOND_WIDTH_BREAKPOINT { 40 | "" 41 | } else { 42 | TEXT_TAB_TIP 43 | }; 44 | 45 | let text = Span::styled( 46 | [pause_content, tab_text, dns_content].concat(), 47 | Style::default().add_modifier(Modifier::BOLD), 48 | ); 49 | let paragraph = Paragraph::new(text).alignment(Alignment::Left); 50 | frame.render_widget(paragraph, rect); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/display/components/layout.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | layout::{Constraint, Direction, Rect}, 3 | Frame, 4 | }; 5 | 6 | use crate::display::{HeaderDetails, HelpText, Table}; 7 | 8 | const FIRST_HEIGHT_BREAKPOINT: u16 = 30; 9 | const FIRST_WIDTH_BREAKPOINT: u16 = 120; 10 | 11 | fn top_app_and_bottom_split(rect: Rect) -> (Rect, Rect, Rect) { 12 | let parts = ratatui::layout::Layout::default() 13 | .direction(Direction::Vertical) 14 | .margin(0) 15 | .constraints( 16 | [ 17 | Constraint::Length(1), 18 | Constraint::Length(rect.height - 2), 19 | Constraint::Length(1), 20 | ] 21 | .as_ref(), 22 | ) 23 | .split(rect); 24 | (parts[0], parts[1], parts[2]) 25 | } 26 | 27 | pub struct Layout<'a> { 28 | pub header: HeaderDetails<'a>, 29 | pub children: Vec, 30 | pub footer: HelpText, 31 | } 32 | 33 | impl Layout<'_> { 34 | fn progressive_split(&self, rect: Rect, splits: Vec) -> Vec { 35 | splits 36 | .into_iter() 37 | .fold(vec![rect], |mut layout, direction| { 38 | let last_rect = layout.pop().unwrap(); 39 | let halves = ratatui::layout::Layout::default() 40 | .direction(direction) 41 | .margin(0) 42 | .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) 43 | .split(last_rect); 44 | layout.append(&mut halves.to_vec()); 45 | layout 46 | }) 47 | } 48 | 49 | fn build_two_children_layout(&self, rect: Rect) -> Vec { 50 | // if there are two elements 51 | if rect.height < FIRST_HEIGHT_BREAKPOINT && rect.width < FIRST_WIDTH_BREAKPOINT { 52 | // if the space is not enough, we drop one element 53 | vec![rect] 54 | } else if rect.width < FIRST_WIDTH_BREAKPOINT { 55 | // if the horizontal space is not enough, we drop one element and we split horizontally 56 | self.progressive_split(rect, vec![Direction::Vertical]) 57 | } else { 58 | // by default we display two elements splitting vertically 59 | self.progressive_split(rect, vec![Direction::Horizontal]) 60 | } 61 | } 62 | 63 | fn build_three_children_layout(&self, rect: Rect) -> Vec { 64 | // if there are three elements 65 | if rect.height < FIRST_HEIGHT_BREAKPOINT && rect.width < FIRST_WIDTH_BREAKPOINT { 66 | //if the space is not enough, we drop two elements 67 | vec![rect] 68 | } else if rect.height < FIRST_HEIGHT_BREAKPOINT { 69 | // if the vertical space is not enough, we drop one element and we split vertically 70 | self.progressive_split(rect, vec![Direction::Horizontal]) 71 | } else if rect.width < FIRST_WIDTH_BREAKPOINT { 72 | // if the horizontal space is not enough, we drop one element and we split horizontally 73 | self.progressive_split(rect, vec![Direction::Vertical]) 74 | } else { 75 | // default layout 76 | let halves = ratatui::layout::Layout::default() 77 | .direction(Direction::Vertical) 78 | .margin(0) 79 | .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) 80 | .split(rect); 81 | let top_quarters = ratatui::layout::Layout::default() 82 | .direction(Direction::Horizontal) 83 | .margin(0) 84 | .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) 85 | .split(halves[0]); 86 | 87 | vec![top_quarters[0], top_quarters[1], halves[1]] 88 | } 89 | } 90 | 91 | fn build_layout(&self, rect: Rect) -> Vec { 92 | if self.children.len() == 1 { 93 | // if there's only one element to render, it can take the whole frame 94 | vec![rect] 95 | } else if self.children.len() == 2 { 96 | self.build_two_children_layout(rect) 97 | } else { 98 | self.build_three_children_layout(rect) 99 | } 100 | } 101 | 102 | pub fn render(&self, frame: &mut Frame, rect: Rect, table_cycle_offset: usize) { 103 | let (top, app, bottom) = top_app_and_bottom_split(rect); 104 | let layout_slots = self.build_layout(app); 105 | for i in 0..layout_slots.len() { 106 | if let Some(rect) = layout_slots.get(i) { 107 | if let Some(child) = self 108 | .children 109 | .get((i + table_cycle_offset) % self.children.len()) 110 | { 111 | child.render(frame, *rect); 112 | } 113 | } 114 | } 115 | self.header.render(frame, top); 116 | self.footer.render(frame, bottom); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/display/components/mod.rs: -------------------------------------------------------------------------------- 1 | mod display_bandwidth; 2 | mod header_details; 3 | mod help_text; 4 | mod layout; 5 | mod table; 6 | 7 | pub use display_bandwidth::*; 8 | pub use header_details::*; 9 | pub use help_text::*; 10 | pub use layout::*; 11 | pub use table::*; 12 | -------------------------------------------------------------------------------- /src/display/mod.rs: -------------------------------------------------------------------------------- 1 | mod components; 2 | mod raw_terminal_backend; 3 | mod ui; 4 | mod ui_state; 5 | 6 | pub use components::*; 7 | pub use raw_terminal_backend::*; 8 | pub use ui::*; 9 | pub use ui_state::*; 10 | -------------------------------------------------------------------------------- /src/display/raw_terminal_backend.rs: -------------------------------------------------------------------------------- 1 | // this is a bit of a hack: 2 | // the TUI backend used by this app changes stdout to raw byte mode. 3 | // this is not desired when we do not use it (in our --raw mode), 4 | // since it makes writing to stdout overly complex 5 | // 6 | // so what we do here is provide a fake backend (RawTerminalBackend) 7 | // that implements the Backend TUI trait, but does nothing 8 | // this way, we don't need to create the TermionBackend 9 | // and thus skew our stdout when we don't need it 10 | 11 | use std::io; 12 | 13 | use ratatui::{ 14 | backend::{Backend, WindowSize}, 15 | buffer::Cell, 16 | layout::{Position, Size}, 17 | }; 18 | 19 | pub struct RawTerminalBackend {} 20 | 21 | impl Backend for RawTerminalBackend { 22 | fn clear(&mut self) -> io::Result<()> { 23 | Ok(()) 24 | } 25 | 26 | fn hide_cursor(&mut self) -> io::Result<()> { 27 | Ok(()) 28 | } 29 | 30 | fn show_cursor(&mut self) -> io::Result<()> { 31 | Ok(()) 32 | } 33 | 34 | fn get_cursor_position(&mut self) -> io::Result { 35 | Ok(Position::new(0, 0)) 36 | } 37 | 38 | fn set_cursor_position>(&mut self, _position: P) -> io::Result<()> { 39 | Ok(()) 40 | } 41 | 42 | fn draw<'a, I>(&mut self, _content: I) -> io::Result<()> 43 | where 44 | I: Iterator, 45 | { 46 | Ok(()) 47 | } 48 | 49 | fn size(&self) -> io::Result { 50 | Ok(Size::new(0, 0)) 51 | } 52 | 53 | fn window_size(&mut self) -> io::Result { 54 | Ok(WindowSize { 55 | columns_rows: Size::default(), 56 | pixels: Size::default(), 57 | }) 58 | } 59 | 60 | fn flush(&mut self) -> io::Result<()> { 61 | Ok(()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/display/ui.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, net::IpAddr, time::Duration}; 2 | 3 | use chrono::prelude::*; 4 | use ratatui::{backend::Backend, Terminal}; 5 | 6 | use crate::{ 7 | cli::{Opt, RenderOpts}, 8 | display::{ 9 | components::{HeaderDetails, HelpText, Layout, Table}, 10 | UIState, 11 | }, 12 | network::{display_connection_string, display_ip_or_host, LocalSocket, Utilization}, 13 | os::ProcessInfo, 14 | }; 15 | 16 | pub struct Ui 17 | where 18 | B: Backend, 19 | { 20 | terminal: Terminal, 21 | state: UIState, 22 | ip_to_host: HashMap, 23 | opts: RenderOpts, 24 | } 25 | 26 | impl Ui 27 | where 28 | B: Backend, 29 | { 30 | pub fn new(terminal_backend: B, opts: &Opt) -> Self { 31 | let mut terminal = Terminal::new(terminal_backend).unwrap(); 32 | terminal.clear().unwrap(); 33 | terminal.hide_cursor().unwrap(); 34 | let state = { 35 | let mut state = UIState::default(); 36 | state.interface_name.clone_from(&opts.interface); 37 | state.unit_family = opts.render_opts.unit_family.into(); 38 | state.cumulative_mode = opts.render_opts.total_utilization; 39 | state.show_dns = opts.show_dns; 40 | state 41 | }; 42 | Ui { 43 | terminal, 44 | state, 45 | ip_to_host: Default::default(), 46 | opts: opts.render_opts, 47 | } 48 | } 49 | pub fn output_text(&mut self, write_to_stdout: &mut (dyn FnMut(&str) + Send)) { 50 | let state = &self.state; 51 | let ip_to_host = &self.ip_to_host; 52 | let local_time: DateTime = Local::now(); 53 | let timestamp = local_time.timestamp(); 54 | let mut no_traffic = true; 55 | 56 | let output_process_data = |write_to_stdout: &mut (dyn FnMut(&str) + Send), 57 | no_traffic: &mut bool| { 58 | for (proc_info, process_network_data) in &state.processes { 59 | write_to_stdout(&format!( 60 | "process: <{timestamp}> \"{}\" up/down Bps: {}/{} connections: {}", 61 | proc_info.name, 62 | process_network_data.total_bytes_uploaded, 63 | process_network_data.total_bytes_downloaded, 64 | process_network_data.connection_count 65 | )); 66 | *no_traffic = false; 67 | } 68 | }; 69 | 70 | let output_connections_data = 71 | |write_to_stdout: &mut (dyn FnMut(&str) + Send), no_traffic: &mut bool| { 72 | for (connection, connection_network_data) in &state.connections { 73 | write_to_stdout(&format!( 74 | "connection: <{timestamp}> {} up/down Bps: {}/{} process: \"{}\"", 75 | display_connection_string( 76 | connection, 77 | ip_to_host, 78 | &connection_network_data.interface_name, 79 | ), 80 | connection_network_data.total_bytes_uploaded, 81 | connection_network_data.total_bytes_downloaded, 82 | connection_network_data.process_name 83 | )); 84 | *no_traffic = false; 85 | } 86 | }; 87 | 88 | let output_adressess_data = |write_to_stdout: &mut (dyn FnMut(&str) + Send), 89 | no_traffic: &mut bool| { 90 | for (remote_address, remote_address_network_data) in &state.remote_addresses { 91 | write_to_stdout(&format!( 92 | "remote_address: <{timestamp}> {} up/down Bps: {}/{} connections: {}", 93 | display_ip_or_host(*remote_address, ip_to_host), 94 | remote_address_network_data.total_bytes_uploaded, 95 | remote_address_network_data.total_bytes_downloaded, 96 | remote_address_network_data.connection_count 97 | )); 98 | *no_traffic = false; 99 | } 100 | }; 101 | 102 | // header 103 | write_to_stdout("Refreshing:"); 104 | 105 | // body1 106 | if self.opts.processes { 107 | output_process_data(write_to_stdout, &mut no_traffic); 108 | } 109 | if self.opts.connections { 110 | output_connections_data(write_to_stdout, &mut no_traffic); 111 | } 112 | if self.opts.addresses { 113 | output_adressess_data(write_to_stdout, &mut no_traffic); 114 | } 115 | if !(self.opts.processes || self.opts.connections || self.opts.addresses) { 116 | output_process_data(write_to_stdout, &mut no_traffic); 117 | output_connections_data(write_to_stdout, &mut no_traffic); 118 | output_adressess_data(write_to_stdout, &mut no_traffic); 119 | } 120 | 121 | // body2: In case no traffic is detected 122 | if no_traffic { 123 | write_to_stdout(""); 124 | } 125 | 126 | // footer 127 | write_to_stdout(""); 128 | } 129 | 130 | pub fn draw(&mut self, paused: bool, elapsed_time: Duration, table_cycle_offset: usize) { 131 | let layout = Layout { 132 | header: HeaderDetails { 133 | state: &self.state, 134 | elapsed_time, 135 | paused, 136 | }, 137 | children: self.get_tables_to_display(), 138 | footer: HelpText { 139 | paused, 140 | show_dns: self.state.show_dns, 141 | }, 142 | }; 143 | self.terminal 144 | .draw(|frame| layout.render(frame, frame.area(), table_cycle_offset)) 145 | .unwrap(); 146 | } 147 | 148 | fn get_tables_to_display(&self) -> Vec
{ 149 | let opts = &self.opts; 150 | let mut children: Vec
= Vec::new(); 151 | if opts.processes { 152 | children.push(Table::create_processes_table(&self.state)); 153 | } 154 | if opts.addresses { 155 | children.push(Table::create_remote_addresses_table( 156 | &self.state, 157 | &self.ip_to_host, 158 | )); 159 | } 160 | if opts.connections { 161 | children.push(Table::create_connections_table( 162 | &self.state, 163 | &self.ip_to_host, 164 | )); 165 | } 166 | if !(opts.processes || opts.addresses || opts.connections) { 167 | children = vec![ 168 | Table::create_processes_table(&self.state), 169 | Table::create_remote_addresses_table(&self.state, &self.ip_to_host), 170 | Table::create_connections_table(&self.state, &self.ip_to_host), 171 | ]; 172 | } 173 | children 174 | } 175 | 176 | pub fn get_table_count(&self) -> usize { 177 | self.get_tables_to_display().len() 178 | } 179 | 180 | pub fn update_state( 181 | &mut self, 182 | connections_to_procs: HashMap, 183 | utilization: Utilization, 184 | ip_to_host: HashMap, 185 | ) { 186 | self.state.update(connections_to_procs, utilization); 187 | self.ip_to_host.extend(ip_to_host); 188 | } 189 | pub fn end(&mut self) { 190 | self.terminal.show_cursor().unwrap(); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/network/connection.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | fmt, 4 | net::{IpAddr, SocketAddr}, 5 | }; 6 | 7 | #[derive(PartialEq, Hash, Eq, Clone, PartialOrd, Ord, Debug, Copy)] 8 | pub enum Protocol { 9 | Tcp, 10 | Udp, 11 | } 12 | 13 | impl Protocol { 14 | #[allow(dead_code)] 15 | pub fn from_str(string: &str) -> Option { 16 | match string { 17 | "TCP" => Some(Protocol::Tcp), 18 | "UDP" => Some(Protocol::Udp), 19 | _ => None, 20 | } 21 | } 22 | } 23 | 24 | impl fmt::Display for Protocol { 25 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 26 | match *self { 27 | Protocol::Tcp => write!(f, "tcp"), 28 | Protocol::Udp => write!(f, "udp"), 29 | } 30 | } 31 | } 32 | 33 | #[derive(Clone, Ord, PartialOrd, PartialEq, Eq, Hash, Copy)] 34 | pub struct Socket { 35 | pub ip: IpAddr, 36 | pub port: u16, 37 | } 38 | 39 | impl fmt::Debug for Socket { 40 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 41 | let Socket { ip, port } = self; 42 | match ip { 43 | IpAddr::V4(v4) => write!(f, "{v4}:{port}"), 44 | IpAddr::V6(v6) => write!(f, "[{v6}]:{port}"), 45 | } 46 | } 47 | } 48 | 49 | #[derive(PartialEq, Hash, Eq, Clone, PartialOrd, Ord, Copy)] 50 | pub struct LocalSocket { 51 | pub ip: IpAddr, 52 | pub port: u16, 53 | pub protocol: Protocol, 54 | } 55 | 56 | impl fmt::Debug for LocalSocket { 57 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 58 | let LocalSocket { ip, port, protocol } = self; 59 | match ip { 60 | IpAddr::V4(v4) => write!(f, "{protocol}://{v4}:{port}"), 61 | IpAddr::V6(v6) => write!(f, "{protocol}://[{v6}]:{port}"), 62 | } 63 | } 64 | } 65 | 66 | #[derive(PartialEq, Hash, Eq, Clone, PartialOrd, Ord, Copy)] 67 | pub struct Connection { 68 | pub remote_socket: Socket, 69 | pub local_socket: LocalSocket, 70 | } 71 | 72 | impl fmt::Debug for Connection { 73 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 74 | let Connection { 75 | remote_socket, 76 | local_socket, 77 | } = self; 78 | write!(f, "{local_socket:?} => {remote_socket:?}") 79 | } 80 | } 81 | 82 | pub fn display_ip_or_host(ip: IpAddr, ip_to_host: &HashMap) -> String { 83 | match ip_to_host.get(&ip) { 84 | Some(host) => host.clone(), 85 | None => ip.to_string(), 86 | } 87 | } 88 | 89 | pub fn display_connection_string( 90 | connection: &Connection, 91 | ip_to_host: &HashMap, 92 | interface_name: &str, 93 | ) -> String { 94 | format!( 95 | "<{interface_name}>:{} => {}:{} ({})", 96 | connection.local_socket.port, 97 | display_ip_or_host(connection.remote_socket.ip, ip_to_host), 98 | connection.remote_socket.port, 99 | connection.local_socket.protocol, 100 | ) 101 | } 102 | 103 | impl Connection { 104 | pub fn new( 105 | remote_socket: SocketAddr, 106 | local_ip: IpAddr, 107 | local_port: u16, 108 | protocol: Protocol, 109 | ) -> Self { 110 | Connection { 111 | remote_socket: Socket { 112 | ip: remote_socket.ip(), 113 | port: remote_socket.port(), 114 | }, 115 | local_socket: LocalSocket { 116 | ip: local_ip, 117 | port: local_port, 118 | protocol, 119 | }, 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/network/dns/client.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashSet, 3 | net::IpAddr, 4 | sync::{Arc, Mutex}, 5 | thread::{Builder, JoinHandle}, 6 | }; 7 | 8 | use tokio::{ 9 | runtime::Runtime, 10 | sync::mpsc::{self, Sender}, 11 | }; 12 | 13 | use crate::network::dns::{resolver::Lookup, IpTable}; 14 | 15 | type PendingAddrs = HashSet; 16 | 17 | const CHANNEL_SIZE: usize = 1_000; 18 | 19 | pub struct Client { 20 | cache: Arc>, 21 | pending: Arc>, 22 | tx: Option>>, 23 | handle: Option>, 24 | } 25 | 26 | impl Client { 27 | pub fn new(resolver: R, runtime: Runtime) -> eyre::Result 28 | where 29 | R: Lookup + Send + Sync + 'static, 30 | { 31 | let cache = Arc::new(Mutex::new(IpTable::new())); 32 | let pending = Arc::new(Mutex::new(PendingAddrs::new())); 33 | let (tx, mut rx) = mpsc::channel::>(CHANNEL_SIZE); 34 | 35 | let handle = Builder::new().name("resolver".into()).spawn({ 36 | let cache = cache.clone(); 37 | let pending = pending.clone(); 38 | move || { 39 | runtime.block_on(async { 40 | let resolver = Arc::new(resolver); 41 | 42 | while let Some(ips) = rx.recv().await { 43 | for ip in ips { 44 | tokio::spawn({ 45 | let resolver = resolver.clone(); 46 | let cache = cache.clone(); 47 | let pending = pending.clone(); 48 | 49 | async move { 50 | if let Some(name) = resolver.lookup(ip).await { 51 | cache.lock().unwrap().insert(ip, name); 52 | } 53 | pending.lock().unwrap().remove(&ip); 54 | } 55 | }); 56 | } 57 | } 58 | }); 59 | } 60 | })?; 61 | 62 | Ok(Self { 63 | cache, 64 | pending, 65 | tx: Some(tx), 66 | handle: Some(handle), 67 | }) 68 | } 69 | 70 | pub fn resolve(&mut self, ips: Vec) { 71 | // Remove ips that are already being resolved 72 | let ips = ips 73 | .into_iter() 74 | .filter(|ip| self.pending.lock().unwrap().insert(*ip)) 75 | .collect::>(); 76 | 77 | if !ips.is_empty() { 78 | // Discard the message if the channel is full; it will be retried eventually 79 | let _ = self.tx.as_mut().unwrap().try_send(ips); 80 | } 81 | } 82 | 83 | pub fn cache(&mut self) -> IpTable { 84 | let cache = self.cache.lock().unwrap(); 85 | cache.clone() 86 | } 87 | } 88 | 89 | impl Drop for Client { 90 | fn drop(&mut self) { 91 | // Do the Option dance to be able to drop the sender so that the receiver finishes and the thread can be joined 92 | drop(self.tx.take().unwrap()); 93 | self.handle.take().unwrap().join().unwrap(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/network/dns/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, net::IpAddr}; 2 | 3 | mod client; 4 | mod resolver; 5 | 6 | pub use client::*; 7 | pub use resolver::*; 8 | 9 | pub type IpTable = HashMap; 10 | -------------------------------------------------------------------------------- /src/network/dns/resolver.rs: -------------------------------------------------------------------------------- 1 | use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}; 2 | 3 | use async_trait::async_trait; 4 | use trust_dns_resolver::{ 5 | config::{NameServerConfig, Protocol, ResolverConfig, ResolverOpts}, 6 | error::ResolveErrorKind, 7 | TokioAsyncResolver, 8 | }; 9 | 10 | #[async_trait] 11 | pub trait Lookup { 12 | async fn lookup(&self, ip: IpAddr) -> Option; 13 | } 14 | 15 | pub struct Resolver(TokioAsyncResolver); 16 | 17 | impl Resolver { 18 | pub async fn new(dns_server: Option) -> eyre::Result { 19 | let resolver = match dns_server { 20 | Some(dns_server_address) => { 21 | let mut config = ResolverConfig::new(); 22 | let options = ResolverOpts::default(); 23 | let socket = SocketAddr::V4(SocketAddrV4::new(dns_server_address, 53)); 24 | let nameserver_config = NameServerConfig { 25 | socket_addr: socket, 26 | protocol: Protocol::Udp, 27 | tls_dns_name: None, 28 | trust_negative_responses: false, 29 | bind_addr: None, 30 | }; 31 | config.add_name_server(nameserver_config); 32 | TokioAsyncResolver::tokio(config, options) 33 | } 34 | None => TokioAsyncResolver::tokio_from_system_conf()?, 35 | }; 36 | Ok(Self(resolver)) 37 | } 38 | } 39 | 40 | #[async_trait] 41 | impl Lookup for Resolver { 42 | async fn lookup(&self, ip: IpAddr) -> Option { 43 | let lookup_future = self.0.reverse_lookup(ip); 44 | match lookup_future.await { 45 | Ok(names) => { 46 | // Take the first result and convert it to a string 47 | names.into_iter().next().map(|name| name.to_string()) 48 | } 49 | Err(e) => match e.kind() { 50 | // If the IP is not associated with a hostname, store the IP 51 | // so that we don't retry indefinitely 52 | ResolveErrorKind::NoRecordsFound { .. } => Some(ip.to_string()), 53 | _ => None, 54 | }, 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/network/mod.rs: -------------------------------------------------------------------------------- 1 | mod connection; 2 | pub mod dns; 3 | mod sniffer; 4 | mod utilization; 5 | 6 | pub use connection::*; 7 | pub use sniffer::*; 8 | pub use utilization::*; 9 | -------------------------------------------------------------------------------- /src/network/sniffer.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{self, Result}, 3 | net::{IpAddr, SocketAddr}, 4 | thread::park_timeout, 5 | time::Duration, 6 | }; 7 | 8 | use pnet::{ 9 | datalink::{DataLinkReceiver, NetworkInterface}, 10 | ipnetwork::IpNetwork, 11 | packet::{ 12 | ethernet::{EtherTypes, EthernetPacket}, 13 | ip::{IpNextHeaderProtocol, IpNextHeaderProtocols}, 14 | ipv4::Ipv4Packet, 15 | ipv6::Ipv6Packet, 16 | tcp::TcpPacket, 17 | udp::UdpPacket, 18 | Packet, 19 | }, 20 | }; 21 | 22 | use crate::{ 23 | network::{Connection, Protocol}, 24 | os::shared::get_datalink_channel, 25 | }; 26 | 27 | const PACKET_WAIT_TIMEOUT: Duration = Duration::from_millis(10); 28 | const CHANNEL_RESET_DELAY: Duration = Duration::from_millis(1000); 29 | 30 | #[derive(Debug)] 31 | pub struct Segment { 32 | pub interface_name: String, 33 | pub connection: Connection, 34 | pub direction: Direction, 35 | pub data_length: u128, 36 | } 37 | 38 | #[derive(PartialEq, Hash, Eq, Debug, Clone, PartialOrd)] 39 | pub enum Direction { 40 | Download, 41 | Upload, 42 | } 43 | 44 | impl Direction { 45 | pub fn new(network_interface_ips: &[IpNetwork], source: IpAddr) -> Self { 46 | if network_interface_ips 47 | .iter() 48 | .any(|ip_network| ip_network.ip() == source) 49 | { 50 | Direction::Upload 51 | } else { 52 | Direction::Download 53 | } 54 | } 55 | } 56 | 57 | trait NextLevelProtocol { 58 | fn get_next_level_protocol(&self) -> IpNextHeaderProtocol; 59 | } 60 | 61 | impl NextLevelProtocol for Ipv6Packet<'_> { 62 | fn get_next_level_protocol(&self) -> IpNextHeaderProtocol { 63 | self.get_next_header() 64 | } 65 | } 66 | 67 | macro_rules! extract_transport_protocol { 68 | ( $ip_packet: ident ) => {{ 69 | match $ip_packet.get_next_level_protocol() { 70 | IpNextHeaderProtocols::Tcp => { 71 | let message = TcpPacket::new($ip_packet.payload())?; 72 | ( 73 | Protocol::Tcp, 74 | message.get_source(), 75 | message.get_destination(), 76 | $ip_packet.payload().len() as u128, 77 | ) 78 | } 79 | IpNextHeaderProtocols::Udp => { 80 | let datagram = UdpPacket::new($ip_packet.payload())?; 81 | ( 82 | Protocol::Udp, 83 | datagram.get_source(), 84 | datagram.get_destination(), 85 | $ip_packet.payload().len() as u128, 86 | ) 87 | } 88 | _ => return None, 89 | } 90 | }}; 91 | } 92 | 93 | pub struct Sniffer { 94 | network_interface: NetworkInterface, 95 | network_frames: Box, 96 | show_dns: bool, 97 | } 98 | 99 | impl Sniffer { 100 | pub fn new( 101 | network_interface: NetworkInterface, 102 | network_frames: Box, 103 | show_dns: bool, 104 | ) -> Self { 105 | Sniffer { 106 | network_interface, 107 | network_frames, 108 | show_dns, 109 | } 110 | } 111 | pub fn next(&mut self) -> Option { 112 | let bytes = match self.network_frames.next() { 113 | Ok(bytes) => bytes, 114 | Err(err) => match err.kind() { 115 | std::io::ErrorKind::TimedOut => { 116 | park_timeout(PACKET_WAIT_TIMEOUT); 117 | return None; 118 | } 119 | _ => { 120 | park_timeout(CHANNEL_RESET_DELAY); 121 | self.reset_channel().ok(); 122 | return None; 123 | } 124 | }, 125 | }; 126 | // See https://github.com/libpnet/libpnet/blob/master/examples/packetdump.rs 127 | // VPN interfaces (such as utun0, utun1, etc) have POINT_TO_POINT bit set to 1 128 | let payload_offset = if (self.network_interface.is_loopback() 129 | || self.network_interface.is_point_to_point()) 130 | && cfg!(target_os = "macos") 131 | { 132 | // The pnet code for BPF loopback adds a zero'd out Ethernet header 133 | 14 134 | } else { 135 | 0 136 | }; 137 | let ip_packet = Ipv4Packet::new(&bytes[payload_offset..])?; 138 | let version = ip_packet.get_version(); 139 | 140 | match version { 141 | 4 => Self::handle_v4(ip_packet, &self.network_interface, self.show_dns), 142 | 6 => Self::handle_v6( 143 | Ipv6Packet::new(&bytes[payload_offset..])?, 144 | &self.network_interface, 145 | ), 146 | _ => { 147 | let pkg = EthernetPacket::new(bytes)?; 148 | match pkg.get_ethertype() { 149 | EtherTypes::Ipv4 => Self::handle_v4( 150 | Ipv4Packet::new(pkg.payload())?, 151 | &self.network_interface, 152 | self.show_dns, 153 | ), 154 | EtherTypes::Ipv6 => { 155 | Self::handle_v6(Ipv6Packet::new(pkg.payload())?, &self.network_interface) 156 | } 157 | _ => None, 158 | } 159 | } 160 | } 161 | } 162 | pub fn reset_channel(&mut self) -> Result<()> { 163 | self.network_frames = get_datalink_channel(&self.network_interface) 164 | .map_err(|_| io::Error::new(io::ErrorKind::Other, "Interface not available"))?; 165 | Ok(()) 166 | } 167 | fn handle_v6(ip_packet: Ipv6Packet, network_interface: &NetworkInterface) -> Option { 168 | let (protocol, source_port, destination_port, data_length) = 169 | extract_transport_protocol!(ip_packet); 170 | 171 | let interface_name = network_interface.name.clone(); 172 | let direction = Direction::new(&network_interface.ips, ip_packet.get_source().into()); 173 | let from = SocketAddr::new(ip_packet.get_source().into(), source_port); 174 | let to = SocketAddr::new(ip_packet.get_destination().into(), destination_port); 175 | 176 | let connection = match direction { 177 | Direction::Download => Connection::new(from, to.ip(), destination_port, protocol), 178 | Direction::Upload => Connection::new(to, from.ip(), source_port, protocol), 179 | }; 180 | Some(Segment { 181 | interface_name, 182 | connection, 183 | data_length, 184 | direction, 185 | }) 186 | } 187 | fn handle_v4( 188 | ip_packet: Ipv4Packet, 189 | network_interface: &NetworkInterface, 190 | show_dns: bool, 191 | ) -> Option { 192 | let (protocol, source_port, destination_port, data_length) = 193 | extract_transport_protocol!(ip_packet); 194 | 195 | let interface_name = network_interface.name.clone(); 196 | let direction = Direction::new(&network_interface.ips, ip_packet.get_source().into()); 197 | let from = SocketAddr::new(ip_packet.get_source().into(), source_port); 198 | let to = SocketAddr::new(ip_packet.get_destination().into(), destination_port); 199 | 200 | let connection = match direction { 201 | Direction::Download => Connection::new(from, to.ip(), destination_port, protocol), 202 | Direction::Upload => Connection::new(to, from.ip(), source_port, protocol), 203 | }; 204 | 205 | if !show_dns && connection.remote_socket.port == 53 { 206 | return None; 207 | } 208 | Some(Segment { 209 | interface_name, 210 | connection, 211 | data_length, 212 | direction, 213 | }) 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/network/utilization.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::network::{Connection, Direction, Segment}; 4 | 5 | #[derive(Clone)] 6 | pub struct ConnectionInfo { 7 | pub interface_name: String, 8 | pub total_bytes_downloaded: u128, 9 | pub total_bytes_uploaded: u128, 10 | } 11 | 12 | #[derive(Clone)] 13 | pub struct Utilization { 14 | pub connections: HashMap, 15 | } 16 | 17 | impl Utilization { 18 | pub fn new() -> Self { 19 | let connections = HashMap::new(); 20 | Utilization { connections } 21 | } 22 | pub fn clone_and_reset(&mut self) -> Self { 23 | let clone = self.clone(); 24 | self.connections.clear(); 25 | clone 26 | } 27 | pub fn ingest(&mut self, seg: Segment) { 28 | let total_bandwidth = self 29 | .connections 30 | .entry(seg.connection) 31 | .or_insert(ConnectionInfo { 32 | interface_name: seg.interface_name, 33 | total_bytes_downloaded: 0, 34 | total_bytes_uploaded: 0, 35 | }); 36 | match seg.direction { 37 | Direction::Download => { 38 | total_bandwidth.total_bytes_downloaded += seg.data_length; 39 | } 40 | Direction::Upload => { 41 | total_bandwidth.total_bytes_uploaded += seg.data_length; 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/os/errors.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Eq, PartialEq, Debug, thiserror::Error)] 2 | pub enum GetInterfaceError { 3 | #[error("Permission error: {0}")] 4 | PermissionError(String), 5 | #[error("Other error: {0}")] 6 | OtherError(String), 7 | } 8 | -------------------------------------------------------------------------------- /src/os/linux.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use procfs::process::FDTarget; 4 | 5 | use crate::{ 6 | network::{LocalSocket, Protocol}, 7 | os::ProcessInfo, 8 | OpenSockets, 9 | }; 10 | 11 | pub(crate) fn get_open_sockets() -> OpenSockets { 12 | let mut open_sockets = HashMap::new(); 13 | let mut inode_to_proc = HashMap::new(); 14 | 15 | if let Ok(all_procs) = procfs::process::all_processes() { 16 | for process in all_procs.filter_map(|res| res.ok()) { 17 | let Ok(fds) = process.fd() else { continue }; 18 | let Ok(stat) = process.stat() else { continue }; 19 | let proc_name = stat.comm; 20 | let proc_info = ProcessInfo::new(&proc_name, stat.pid as u32); 21 | for fd in fds.filter_map(|res| res.ok()) { 22 | if let FDTarget::Socket(inode) = fd.target { 23 | inode_to_proc.insert(inode, proc_info.clone()); 24 | } 25 | } 26 | } 27 | } 28 | 29 | macro_rules! insert_proto { 30 | ($source: expr, $proto: expr) => { 31 | let entries = $source.into_iter().filter_map(|res| res.ok()).flatten(); 32 | for entry in entries { 33 | if let Some(proc_info) = inode_to_proc.get(&entry.inode) { 34 | let socket = LocalSocket { 35 | ip: entry.local_address.ip(), 36 | port: entry.local_address.port(), 37 | protocol: $proto, 38 | }; 39 | open_sockets.insert(socket, proc_info.clone()); 40 | } 41 | } 42 | }; 43 | } 44 | 45 | insert_proto!([procfs::net::tcp(), procfs::net::tcp6()], Protocol::Tcp); 46 | insert_proto!([procfs::net::udp(), procfs::net::udp6()], Protocol::Udp); 47 | 48 | OpenSockets { 49 | sockets_to_procs: open_sockets, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/os/lsof.rs: -------------------------------------------------------------------------------- 1 | use crate::{os::lsof_utils::get_connections, OpenSockets}; 2 | 3 | pub(crate) fn get_open_sockets() -> OpenSockets { 4 | let sockets_to_procs = get_connections() 5 | .filter_map(|raw| raw.as_local_socket().map(|s| (s, raw.proc_info))) 6 | .collect(); 7 | 8 | OpenSockets { sockets_to_procs } 9 | } 10 | -------------------------------------------------------------------------------- /src/os/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(any(target_os = "android", target_os = "linux"))] 2 | mod linux; 3 | 4 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 5 | mod lsof; 6 | 7 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 8 | mod lsof_utils; 9 | 10 | #[cfg(target_os = "windows")] 11 | mod windows; 12 | 13 | mod errors; 14 | pub(crate) mod shared; 15 | 16 | pub use shared::*; 17 | -------------------------------------------------------------------------------- /src/os/shared.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{self, ErrorKind, Write}, 3 | net::Ipv4Addr, 4 | time, 5 | }; 6 | 7 | use crossterm::event::{read, Event}; 8 | use eyre::{bail, eyre}; 9 | use itertools::Itertools; 10 | use log::{debug, warn}; 11 | use pnet::datalink::{self, Channel::Ethernet, Config, DataLinkReceiver, NetworkInterface}; 12 | use tokio::runtime::Runtime; 13 | 14 | use crate::{network::dns, os::errors::GetInterfaceError, OsInputOutput}; 15 | 16 | #[cfg(any(target_os = "android", target_os = "linux"))] 17 | use crate::os::linux::get_open_sockets; 18 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 19 | use crate::os::lsof::get_open_sockets; 20 | #[cfg(target_os = "windows")] 21 | use crate::os::windows::get_open_sockets; 22 | 23 | #[derive(Clone, Debug, Default, Hash, PartialEq, Eq)] 24 | pub struct ProcessInfo { 25 | pub name: String, 26 | pub pid: u32, 27 | } 28 | 29 | impl ProcessInfo { 30 | pub fn new(name: &str, pid: u32) -> Self { 31 | Self { 32 | name: name.to_string(), 33 | pid, 34 | } 35 | } 36 | } 37 | 38 | pub struct TerminalEvents; 39 | 40 | impl Iterator for TerminalEvents { 41 | type Item = Event; 42 | fn next(&mut self) -> Option { 43 | read().ok() 44 | } 45 | } 46 | 47 | pub(crate) fn get_datalink_channel( 48 | interface: &NetworkInterface, 49 | ) -> Result, GetInterfaceError> { 50 | let config = Config { 51 | read_timeout: Some(time::Duration::new(1, 0)), 52 | read_buffer_size: 65536, 53 | ..Default::default() 54 | }; 55 | 56 | match datalink::channel(interface, config) { 57 | Ok(Ethernet(_tx, rx)) => Ok(rx), 58 | Ok(_) => Err(GetInterfaceError::OtherError(format!( 59 | "{}: Unsupported interface type", 60 | interface.name 61 | ))), 62 | Err(e) => match e.kind() { 63 | ErrorKind::PermissionDenied => Err(GetInterfaceError::PermissionError( 64 | interface.name.to_owned(), 65 | )), 66 | _ => Err(GetInterfaceError::OtherError(format!( 67 | "{}: {e}", 68 | &interface.name 69 | ))), 70 | }, 71 | } 72 | } 73 | 74 | fn get_interface(interface_name: &str) -> Option { 75 | datalink::interfaces() 76 | .into_iter() 77 | .find(|iface| iface.name == interface_name) 78 | } 79 | 80 | fn create_write_to_stdout() -> Box { 81 | let mut stdout = io::stdout(); 82 | Box::new({ 83 | move |output| match writeln!(stdout, "{}", output) { 84 | Ok(_) => (), 85 | Err(e) if e.kind() == ErrorKind::BrokenPipe => { 86 | // A process that was listening to bandwhich stdout has exited 87 | // We can't do much here, lets just exit as well 88 | std::process::exit(0) 89 | } 90 | Err(e) => panic!("Failed to write to stdout: {e}"), 91 | } 92 | }) 93 | } 94 | 95 | pub fn get_input( 96 | interface_name: Option<&str>, 97 | resolve: bool, 98 | dns_server: Option, 99 | ) -> eyre::Result { 100 | // get the user's requested interface, if any 101 | // IDEA: allow requesting multiple interfaces 102 | let requested_interfaces = interface_name 103 | .map(|name| get_interface(name).ok_or_else(|| eyre!("Cannot find interface {name}"))) 104 | .transpose()? 105 | .map(|interface| vec![interface]); 106 | 107 | // take the user's requested interfaces (or all interfaces), and filter for up ones 108 | let available_interfaces = requested_interfaces 109 | .unwrap_or_else(datalink::interfaces) 110 | .into_iter() 111 | .filter(|interface| { 112 | // see https://github.com/libpnet/libpnet/issues/564 113 | let keep = if cfg!(target_os = "windows") { 114 | !interface.ips.is_empty() 115 | } else { 116 | interface.is_up() && !interface.ips.is_empty() 117 | }; 118 | if !keep { 119 | debug!("{} is down. Skipping it.", interface.name); 120 | } 121 | keep 122 | }) 123 | .collect_vec(); 124 | 125 | // bail if no interfaces are up 126 | if available_interfaces.is_empty() { 127 | bail!("Failed to find any network interface to listen on."); 128 | } 129 | 130 | // try to get a frame receiver for each interface 131 | let interfaces_with_frames_res = available_interfaces 132 | .into_iter() 133 | .map(|interface| { 134 | let frames_res = get_datalink_channel(&interface); 135 | (interface, frames_res) 136 | }) 137 | .collect_vec(); 138 | 139 | // warn for all frame receivers we failed to acquire 140 | interfaces_with_frames_res 141 | .iter() 142 | .filter_map(|(interface, frames_res)| frames_res.as_ref().err().map(|err| (interface, err))) 143 | .for_each(|(interface, err)| { 144 | warn!( 145 | "Failed to acquire a frame receiver for {}: {err}", 146 | interface.name 147 | ) 148 | }); 149 | 150 | // bail if all of them fail 151 | // note that `Iterator::all` returns `true` for an empty iterator, so it is important to handle 152 | // that failure mode separately, which we already have 153 | if interfaces_with_frames_res 154 | .iter() 155 | .all(|(_, frames)| frames.is_err()) 156 | { 157 | let (permission_err_interfaces, other_errs) = interfaces_with_frames_res.iter().fold( 158 | (vec![], vec![]), 159 | |(mut perms, mut others), (_, res)| { 160 | match res { 161 | Ok(_) => (), 162 | Err(GetInterfaceError::PermissionError(interface)) => { 163 | perms.push(interface.as_str()) 164 | } 165 | Err(GetInterfaceError::OtherError(err)) => others.push(err.as_str()), 166 | } 167 | (perms, others) 168 | }, 169 | ); 170 | 171 | let err_msg = match (permission_err_interfaces.is_empty(), other_errs.is_empty()) { 172 | (false, false) => format!( 173 | "\n\n{}: {}\nAdditional errors:\n{}", 174 | permission_err_interfaces.join(", "), 175 | eperm_message(), 176 | other_errs.join("\n") 177 | ), 178 | (false, true) => format!( 179 | "\n\n{}: {}", 180 | permission_err_interfaces.join(", "), 181 | eperm_message() 182 | ), 183 | (true, false) => format!("\n\n{}", other_errs.join("\n")), 184 | (true, true) => unreachable!("Found no errors in error handling code path."), 185 | }; 186 | bail!(err_msg); 187 | } 188 | 189 | // filter out interfaces for which we failed to acquire a frame receiver 190 | let interfaces_with_frames = interfaces_with_frames_res 191 | .into_iter() 192 | .filter_map(|(interface, res)| res.ok().map(|frames| (interface, frames))) 193 | .collect(); 194 | 195 | let dns_client = if resolve { 196 | let runtime = Runtime::new()?; 197 | let resolver = runtime 198 | .block_on(dns::Resolver::new(dns_server)) 199 | .map_err(|err| { 200 | eyre!("Could not initialize the DNS resolver. Are you offline?\n\nReason: {err}") 201 | })?; 202 | let dns_client = dns::Client::new(resolver, runtime)?; 203 | Some(dns_client) 204 | } else { 205 | None 206 | }; 207 | 208 | let write_to_stdout = create_write_to_stdout(); 209 | 210 | Ok(OsInputOutput { 211 | interfaces_with_frames, 212 | get_open_sockets, 213 | terminal_events: Box::new(TerminalEvents), 214 | dns_client, 215 | write_to_stdout, 216 | }) 217 | } 218 | 219 | #[inline] 220 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 221 | fn eperm_message() -> &'static str { 222 | "Insufficient permissions to listen on network interface(s). Try running with sudo." 223 | } 224 | 225 | #[inline] 226 | #[cfg(any(target_os = "android", target_os = "linux"))] 227 | fn eperm_message() -> &'static str { 228 | r#" 229 | Insufficient permissions to listen on network interface(s). You can work around 230 | this issue like this: 231 | 232 | * Try running `bandwhich` with `sudo` 233 | 234 | * Build a `setcap(8)` wrapper for `bandwhich` with the following rules: 235 | `cap_sys_ptrace,cap_dac_read_search,cap_net_raw,cap_net_admin+ep` 236 | "# 237 | } 238 | 239 | #[inline] 240 | #[cfg(target_os = "windows")] 241 | fn eperm_message() -> &'static str { 242 | "Insufficient permissions to listen on network interface(s). Try running with administrator rights." 243 | } 244 | -------------------------------------------------------------------------------- /src/os/windows.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use netstat2::*; 4 | use sysinfo::{Pid, ProcessesToUpdate, System}; 5 | 6 | use crate::{ 7 | network::{LocalSocket, Protocol}, 8 | os::ProcessInfo, 9 | OpenSockets, 10 | }; 11 | 12 | pub(crate) fn get_open_sockets() -> OpenSockets { 13 | let mut open_sockets = HashMap::new(); 14 | 15 | let mut sysinfo = System::new_all(); 16 | sysinfo.refresh_processes(ProcessesToUpdate::All, true); 17 | 18 | let af_flags = AddressFamilyFlags::IPV4 | AddressFamilyFlags::IPV6; 19 | let proto_flags = ProtocolFlags::TCP | ProtocolFlags::UDP; 20 | let sockets_info = get_sockets_info(af_flags, proto_flags); 21 | 22 | if let Ok(sockets_info) = sockets_info { 23 | for si in sockets_info { 24 | let proc_info = si 25 | .associated_pids 26 | .into_iter() 27 | .find_map(|pid| sysinfo.process(Pid::from_u32(pid))) 28 | .map(|p| ProcessInfo::new(&p.name().to_string_lossy(), p.pid().as_u32())) 29 | .unwrap_or_default(); 30 | 31 | match si.protocol_socket_info { 32 | ProtocolSocketInfo::Tcp(tcp_si) => { 33 | open_sockets.insert( 34 | LocalSocket { 35 | ip: tcp_si.local_addr, 36 | port: tcp_si.local_port, 37 | protocol: Protocol::Tcp, 38 | }, 39 | proc_info, 40 | ); 41 | } 42 | ProtocolSocketInfo::Udp(udp_si) => { 43 | open_sockets.insert( 44 | LocalSocket { 45 | ip: udp_si.local_addr, 46 | port: udp_si.local_port, 47 | protocol: Protocol::Udp, 48 | }, 49 | proc_info, 50 | ); 51 | } 52 | } 53 | } 54 | } 55 | 56 | OpenSockets { 57 | sockets_to_procs: open_sockets, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/tests/cases/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod raw_mode; 2 | pub mod test_utils; 3 | #[cfg(feature = "ui_test")] 4 | pub mod ui; 5 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__raw_mode__bi_directional_traffic.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/raw_mode.rs 3 | expression: formatted 4 | --- 5 | Refreshing: 6 | 7 | 8 | Refreshing: 9 | process: "1" up/down Bps: 24/25 connections: 1 10 | connection: :443 => 1.1.1.1:12345 (tcp) up/down Bps: 24/25 process: "1" 11 | remote_address: 1.1.1.1 up/down Bps: 24/25 connections: 1 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__raw_mode__multiple_connections_from_remote_address.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/raw_mode.rs 3 | expression: formatted 4 | --- 5 | Refreshing: 6 | 7 | 8 | Refreshing: 9 | process: "1" up/down Bps: 0/47 connections: 2 10 | connection: :443 => 1.1.1.1:12346 (tcp) up/down Bps: 0/25 process: "1" 11 | connection: :443 => 1.1.1.1:12345 (tcp) up/down Bps: 0/22 process: "1" 12 | remote_address: 1.1.1.1 up/down Bps: 0/47 connections: 2 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__raw_mode__multiple_packets_of_traffic_from_different_connections.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/raw_mode.rs 3 | expression: formatted 4 | --- 5 | Refreshing: 6 | 7 | 8 | Refreshing: 9 | process: "1" up/down Bps: 0/22 connections: 1 10 | process: "4" up/down Bps: 0/19 connections: 1 11 | connection: :443 => 2.2.2.2:12345 (tcp) up/down Bps: 0/22 process: "1" 12 | connection: :4434 => 2.2.2.2:54321 (tcp) up/down Bps: 0/19 process: "4" 13 | remote_address: 2.2.2.2 up/down Bps: 0/41 connections: 2 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__raw_mode__multiple_packets_of_traffic_from_single_connection.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/raw_mode.rs 3 | expression: formatted 4 | --- 5 | Refreshing: 6 | 7 | 8 | Refreshing: 9 | process: "1" up/down Bps: 0/45 connections: 1 10 | connection: :443 => 1.1.1.1:12345 (tcp) up/down Bps: 0/45 process: "1" 11 | remote_address: 1.1.1.1 up/down Bps: 0/45 connections: 1 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__raw_mode__multiple_processes_with_multiple_connections.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/raw_mode.rs 3 | expression: formatted 4 | --- 5 | Refreshing: 6 | 7 | 8 | Refreshing: 9 | process: "5" up/down Bps: 0/28 connections: 1 10 | process: "4" up/down Bps: 0/26 connections: 1 11 | process: "1" up/down Bps: 0/22 connections: 1 12 | process: "2" up/down Bps: 0/21 connections: 1 13 | connection: :4435 => 3.3.3.3:1337 (tcp) up/down Bps: 0/28 process: "5" 14 | connection: :4434 => 2.2.2.2:54321 (tcp) up/down Bps: 0/26 process: "4" 15 | connection: :443 => 1.1.1.1:12345 (tcp) up/down Bps: 0/22 process: "1" 16 | connection: :4432 => 4.4.4.4:1337 (tcp) up/down Bps: 0/21 process: "2" 17 | remote_address: 3.3.3.3 up/down Bps: 0/28 connections: 1 18 | remote_address: 2.2.2.2 up/down Bps: 0/26 connections: 1 19 | remote_address: 1.1.1.1 up/down Bps: 0/22 connections: 1 20 | remote_address: 4.4.4.4 up/down Bps: 0/21 connections: 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__raw_mode__no_resolve_mode.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/raw_mode.rs 3 | expression: formatted 4 | --- 5 | Refreshing: 6 | 7 | 8 | Refreshing: 9 | process: "1" up/down Bps: 28/30 connections: 1 10 | process: "5" up/down Bps: 17/18 connections: 1 11 | connection: :443 => 1.1.1.1:12345 (tcp) up/down Bps: 28/30 process: "1" 12 | connection: :4435 => 3.3.3.3:1337 (tcp) up/down Bps: 17/18 process: "5" 13 | remote_address: 1.1.1.1 up/down Bps: 28/30 connections: 1 14 | remote_address: 3.3.3.3 up/down Bps: 17/18 connections: 1 15 | 16 | Refreshing: 17 | process: "1" up/down Bps: 31/32 connections: 1 18 | process: "5" up/down Bps: 22/27 connections: 1 19 | connection: :443 => 1.1.1.1:12345 (tcp) up/down Bps: 31/32 process: "1" 20 | connection: :4435 => 3.3.3.3:1337 (tcp) up/down Bps: 22/27 process: "5" 21 | remote_address: 1.1.1.1 up/down Bps: 31/32 connections: 1 22 | remote_address: 3.3.3.3 up/down Bps: 22/27 connections: 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__raw_mode__one_ip_packet_of_traffic.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/raw_mode.rs 3 | expression: formatted 4 | --- 5 | Refreshing: 6 | 7 | 8 | Refreshing: 9 | process: "1" up/down Bps: 21/0 connections: 1 10 | connection: :443 => 1.1.1.1:12345 (tcp) up/down Bps: 21/0 process: "1" 11 | remote_address: 1.1.1.1 up/down Bps: 21/0 connections: 1 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__raw_mode__one_packet_of_traffic.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/raw_mode.rs 3 | expression: formatted 4 | --- 5 | Refreshing: 6 | 7 | 8 | Refreshing: 9 | process: "1" up/down Bps: 21/0 connections: 1 10 | connection: :443 => 1.1.1.1:12345 (tcp) up/down Bps: 21/0 process: "1" 11 | remote_address: 1.1.1.1 up/down Bps: 21/0 connections: 1 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__raw_mode__one_process_with_multiple_connections.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/raw_mode.rs 3 | expression: formatted 4 | --- 5 | Refreshing: 6 | 7 | 8 | Refreshing: 9 | process: "1" up/down Bps: 0/46 connections: 2 10 | connection: :443 => 1.1.1.1:12346 (tcp) up/down Bps: 0/24 process: "1" 11 | connection: :443 => 1.1.1.1:12345 (tcp) up/down Bps: 0/22 process: "1" 12 | remote_address: 1.1.1.1 up/down Bps: 0/46 connections: 2 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__raw_mode__sustained_traffic_from_multiple_processes.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/raw_mode.rs 3 | expression: formatted 4 | --- 5 | Refreshing: 6 | 7 | 8 | Refreshing: 9 | process: "1" up/down Bps: 0/22 connections: 1 10 | process: "5" up/down Bps: 0/19 connections: 1 11 | connection: :443 => 1.1.1.1:12345 (tcp) up/down Bps: 0/22 process: "1" 12 | connection: :4435 => 3.3.3.3:1337 (tcp) up/down Bps: 0/19 process: "5" 13 | remote_address: 1.1.1.1 up/down Bps: 0/22 connections: 1 14 | remote_address: 3.3.3.3 up/down Bps: 0/19 connections: 1 15 | 16 | Refreshing: 17 | process: "1" up/down Bps: 0/35 connections: 1 18 | process: "5" up/down Bps: 0/30 connections: 1 19 | connection: :443 => 1.1.1.1:12345 (tcp) up/down Bps: 0/35 process: "1" 20 | connection: :4435 => 3.3.3.3:1337 (tcp) up/down Bps: 0/30 process: "5" 21 | remote_address: 1.1.1.1 up/down Bps: 0/35 connections: 1 22 | remote_address: 3.3.3.3 up/down Bps: 0/30 connections: 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__raw_mode__sustained_traffic_from_multiple_processes_bi_directional.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/raw_mode.rs 3 | expression: formatted 4 | --- 5 | Refreshing: 6 | 7 | 8 | Refreshing: 9 | process: "1" up/down Bps: 28/30 connections: 1 10 | process: "5" up/down Bps: 17/18 connections: 1 11 | connection: :443 => 1.1.1.1:12345 (tcp) up/down Bps: 28/30 process: "1" 12 | connection: :4435 => 3.3.3.3:1337 (tcp) up/down Bps: 17/18 process: "5" 13 | remote_address: 1.1.1.1 up/down Bps: 28/30 connections: 1 14 | remote_address: 3.3.3.3 up/down Bps: 17/18 connections: 1 15 | 16 | Refreshing: 17 | process: "1" up/down Bps: 31/32 connections: 1 18 | process: "5" up/down Bps: 22/27 connections: 1 19 | connection: :443 => 1.1.1.1:12345 (tcp) up/down Bps: 31/32 process: "1" 20 | connection: :4435 => 3.3.3.3:1337 (tcp) up/down Bps: 22/27 process: "5" 21 | remote_address: 1.1.1.1 up/down Bps: 31/32 connections: 1 22 | remote_address: 3.3.3.3 up/down Bps: 22/27 connections: 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__raw_mode__sustained_traffic_from_one_process.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/raw_mode.rs 3 | expression: formatted 4 | --- 5 | Refreshing: 6 | 7 | 8 | Refreshing: 9 | process: "1" up/down Bps: 0/22 connections: 1 10 | connection: :443 => 1.1.1.1:12345 (tcp) up/down Bps: 0/22 process: "1" 11 | remote_address: 1.1.1.1 up/down Bps: 0/22 connections: 1 12 | 13 | Refreshing: 14 | process: "1" up/down Bps: 0/31 connections: 1 15 | connection: :443 => 1.1.1.1:12345 (tcp) up/down Bps: 0/31 process: "1" 16 | remote_address: 1.1.1.1 up/down Bps: 0/31 connections: 1 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__raw_mode__traffic_with_host_names.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/raw_mode.rs 3 | expression: formatted 4 | --- 5 | Refreshing: 6 | 7 | 8 | Refreshing: 9 | process: "1" up/down Bps: 28/30 connections: 1 10 | process: "5" up/down Bps: 17/18 connections: 1 11 | connection: :443 => 1.1.1.1:12345 (tcp) up/down Bps: 28/30 process: "1" 12 | connection: :4435 => 3.3.3.3:1337 (tcp) up/down Bps: 17/18 process: "5" 13 | remote_address: 1.1.1.1 up/down Bps: 28/30 connections: 1 14 | remote_address: 3.3.3.3 up/down Bps: 17/18 connections: 1 15 | 16 | Refreshing: 17 | process: "1" up/down Bps: 31/32 connections: 1 18 | process: "5" up/down Bps: 22/27 connections: 1 19 | connection: :443 => one.one.one.one:12345 (tcp) up/down Bps: 31/32 process: "1" 20 | connection: :4435 => three.three.three.three:1337 (tcp) up/down Bps: 22/27 process: "5" 21 | remote_address: one.one.one.one up/down Bps: 31/32 connections: 1 22 | remote_address: three.three.three.three up/down Bps: 22/27 connections: 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__basic_only_addresses.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_draw_events.lock().unwrap().join(SNAPSHOT_SECTION_SEPARATOR) 4 | --- 5 | IF: interface_name | Total Rate (Up / Down): 0.00B / 0.00B 6 | ┌Utilization by remote address───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 7 | │Remote Address Connections Rate (Up / Down) │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ │ 15 | │ │ 16 | │ │ 17 | │ │ 18 | │ │ 19 | │ │ 20 | │ │ 21 | │ │ 22 | │ │ 23 | │ │ 24 | │ │ 25 | │ │ 26 | │ │ 27 | │ │ 28 | │ │ 29 | │ │ 30 | │ │ 31 | │ │ 32 | │ │ 33 | │ │ 34 | │ │ 35 | │ │ 36 | │ │ 37 | │ │ 38 | │ │ 39 | │ │ 40 | │ │ 41 | │ │ 42 | │ │ 43 | │ │ 44 | │ │ 45 | │ │ 46 | │ │ 47 | │ │ 48 | │ │ 49 | │ │ 50 | │ │ 51 | │ │ 52 | │ │ 53 | └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 54 | Press to pause. Use to rearrange tables. (DNS queries hidden). 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__basic_only_connections.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_draw_events.lock().unwrap().join(SNAPSHOT_SECTION_SEPARATOR) 4 | --- 5 | IF: interface_name | Total Rate (Up / Down): 0.00B / 0.00B 6 | ┌Utilization by connection───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 7 | │Connection Process Rate (Up / Down) │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ │ 15 | │ │ 16 | │ │ 17 | │ │ 18 | │ │ 19 | │ │ 20 | │ │ 21 | │ │ 22 | │ │ 23 | │ │ 24 | │ │ 25 | │ │ 26 | │ │ 27 | │ │ 28 | │ │ 29 | │ │ 30 | │ │ 31 | │ │ 32 | │ │ 33 | │ │ 34 | │ │ 35 | │ │ 36 | │ │ 37 | │ │ 38 | │ │ 39 | │ │ 40 | │ │ 41 | │ │ 42 | │ │ 43 | │ │ 44 | │ │ 45 | │ │ 46 | │ │ 47 | │ │ 48 | │ │ 49 | │ │ 50 | │ │ 51 | │ │ 52 | │ │ 53 | └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 54 | Press to pause. Use to rearrange tables. (DNS queries hidden). 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__basic_only_processes.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_draw_events.lock().unwrap().join(SNAPSHOT_SECTION_SEPARATOR) 4 | --- 5 | IF: interface_name | Total Rate (Up / Down): 0.00B / 0.00B 6 | ┌Utilization by process name─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 7 | │Process PID Connections Rate (Up / Down) │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ │ 15 | │ │ 16 | │ │ 17 | │ │ 18 | │ │ 19 | │ │ 20 | │ │ 21 | │ │ 22 | │ │ 23 | │ │ 24 | │ │ 25 | │ │ 26 | │ │ 27 | │ │ 28 | │ │ 29 | │ │ 30 | │ │ 31 | │ │ 32 | │ │ 33 | │ │ 34 | │ │ 35 | │ │ 36 | │ │ 37 | │ │ 38 | │ │ 39 | │ │ 40 | │ │ 41 | │ │ 42 | │ │ 43 | │ │ 44 | │ │ 45 | │ │ 46 | │ │ 47 | │ │ 48 | │ │ 49 | │ │ 50 | │ │ 51 | │ │ 52 | │ │ 53 | └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 54 | Press to pause. Use to rearrange tables. (DNS queries hidden). 55 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__basic_processes_with_dns_queries.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_draw_events.lock().unwrap().join(SNAPSHOT_SECTION_SEPARATOR) 4 | --- 5 | IF: interface_name | Total Rate (Up / Down): 0.00B / 0.00B 6 | ┌Utilization by process name─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 7 | │Process PID Connections Rate (Up / Down) │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ │ 15 | │ │ 16 | │ │ 17 | │ │ 18 | │ │ 19 | │ │ 20 | │ │ 21 | │ │ 22 | │ │ 23 | │ │ 24 | │ │ 25 | │ │ 26 | │ │ 27 | │ │ 28 | │ │ 29 | │ │ 30 | │ │ 31 | │ │ 32 | │ │ 33 | │ │ 34 | │ │ 35 | │ │ 36 | │ │ 37 | │ │ 38 | │ │ 39 | │ │ 40 | │ │ 41 | │ │ 42 | │ │ 43 | │ │ 44 | │ │ 45 | │ │ 46 | │ │ 47 | │ │ 48 | │ │ 49 | │ │ 50 | │ │ 51 | │ │ 52 | │ │ 53 | └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 54 | Press to pause. Use to rearrange tables. (DNS queries shown). 55 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__basic_startup-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | ShowCursor, 12 | ] 13 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__basic_startup.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_draw_events.lock().unwrap().join(SNAPSHOT_SECTION_SEPARATOR) 4 | --- 5 | IF: interface_name | Total Rate (Up / Down): 0.00B / 0.00B 6 | ┌Utilization by process name──────────────────────────────────────────────────────────────────┐┌Utilization by remote address────────────────────────────────────────────────────────────────┐ 7 | │Process PID Connections Rate (Up / Down) ││Remote Address Connections Rate (Up / Down) │ 8 | │ ││ │ 9 | │ ││ │ 10 | │ ││ │ 11 | │ ││ │ 12 | │ ││ │ 13 | │ ││ │ 14 | │ ││ │ 15 | │ ││ │ 16 | │ ││ │ 17 | │ ││ │ 18 | │ ││ │ 19 | │ ││ │ 20 | │ ││ │ 21 | │ ││ │ 22 | │ ││ │ 23 | │ ││ │ 24 | │ ││ │ 25 | │ ││ │ 26 | │ ││ │ 27 | │ ││ │ 28 | │ ││ │ 29 | └─────────────────────────────────────────────────────────────────────────────────────────────┘└─────────────────────────────────────────────────────────────────────────────────────────────┘ 30 | ┌Utilization by connection───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 31 | │Connection Process Rate (Up / Down) │ 32 | │ │ 33 | │ │ 34 | │ │ 35 | │ │ 36 | │ │ 37 | │ │ 38 | │ │ 39 | │ │ 40 | │ │ 41 | │ │ 42 | │ │ 43 | │ │ 44 | │ │ 45 | │ │ 46 | │ │ 47 | │ │ 48 | │ │ 49 | │ │ 50 | │ │ 51 | │ │ 52 | │ │ 53 | └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 54 | Press to pause. Use to rearrange tables. (DNS queries hidden). 55 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__bi_directional_traffic-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | ShowCursor, 15 | ] 16 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__layout-full-width-under-30-height-draw_events.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_draw_events.lock().unwrap().join(SNAPSHOT_SECTION_SEPARATOR) 4 | --- 5 | IF: interface_name | Total Rate (Up / Down): 0.00B / 0.00B 6 | ┌Utilization by process name──────────────────────────────────────────────────────────────────┐┌Utilization by remote address────────────────────────────────────────────────────────────────┐ 7 | │Process PID Connections Rate (Up / Down) ││Remote Address Connections Rate (Up / Down) │ 8 | │ ││ │ 9 | │ ││ │ 10 | │ ││ │ 11 | │ ││ │ 12 | │ ││ │ 13 | │ ││ │ 14 | │ ││ │ 15 | │ ││ │ 16 | │ ││ │ 17 | │ ││ │ 18 | │ ││ │ 19 | │ ││ │ 20 | │ ││ │ 21 | │ ││ │ 22 | │ ││ │ 23 | │ ││ │ 24 | │ ││ │ 25 | │ ││ │ 26 | │ ││ │ 27 | │ ││ │ 28 | │ ││ │ 29 | │ ││ │ 30 | │ ││ │ 31 | │ ││ │ 32 | └─────────────────────────────────────────────────────────────────────────────────────────────┘└─────────────────────────────────────────────────────────────────────────────────────────────┘ 33 | Press to pause. Use to rearrange tables. (DNS queries hidden). 34 | 35 | --- SECTION SEPARATOR --- 36 | 98. 0B 37 | 38 | 39 | 5 5 1 0.00B / 28.00B 3.3.3.3 1 0.00B / 28.00B 40 | 4 4 1 0.00B / 26.00B 2.2.2.2 1 0.00B / 26.00B 41 | 1 1 1 0.00B / 22.00B 1.1.1.1 1 0.00B / 22.00B 42 | 2 2 1 0.00B / 21.00B 4.4.4.4 1 0.00B / 21.00B 43 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__layout-full-width-under-30-height-events.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | ShowCursor, 15 | ] 16 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__layout-under-120-width-full-height-events.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | ShowCursor, 15 | ] 16 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__layout-under-120-width-under-30-height-draw_events.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_draw_events.lock().unwrap().join(SNAPSHOT_SECTION_SEPARATOR) 4 | --- 5 | IF: interface_name | Total Rate (Up / Down): 0.00B / 0.00B 6 | ┌Utilization by process name──────────────────────────────────────────────────────────────────────────────────────────┐ 7 | │Process PID Connections Rate (Up / Down) │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ │ 15 | │ │ 16 | │ │ 17 | │ │ 18 | │ │ 19 | │ │ 20 | │ │ 21 | │ │ 22 | │ │ 23 | │ │ 24 | │ │ 25 | │ │ 26 | │ │ 27 | │ │ 28 | │ │ 29 | │ │ 30 | │ │ 31 | │ │ 32 | └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 33 | Press to pause. Use to rearrange tables. (DNS queries hidden). 34 | 35 | --- SECTION SEPARATOR --- 36 | 98. 0B 37 | 38 | 39 | 5 5 1 0.00B / 28.00B 40 | 4 4 1 0.00B / 26.00B 41 | 1 1 1 0.00B / 22.00B 42 | 2 2 1 0.00B / 21.00B 43 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__layout-under-120-width-under-30-height-events.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | ShowCursor, 15 | ] 16 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__layout-under-50-width-under-50-height-draw_events.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_draw_events.lock().unwrap().join(SNAPSHOT_SECTION_SEPARATOR) 4 | --- 5 | IF: interface_name | Total Rate (Up / Down): 0.00B 6 | ┌Utilization by process name─────────────────────┐ 7 | │Process Rate (Up / Down) │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ │ 15 | │ │ 16 | │ │ 17 | │ │ 18 | │ │ 19 | │ │ 20 | │ │ 21 | │ │ 22 | │ │ 23 | │ │ 24 | │ │ 25 | │ │ 26 | │ │ 27 | │ │ 28 | │ │ 29 | └────────────────────────────────────────────────┘ 30 | ┌Utilization by remote address───────────────────┐ 31 | │Remote Address Rate (Up / Down) │ 32 | │ │ 33 | │ │ 34 | │ │ 35 | │ │ 36 | │ │ 37 | │ │ 38 | │ │ 39 | │ │ 40 | │ │ 41 | │ │ 42 | │ │ 43 | │ │ 44 | │ │ 45 | │ │ 46 | │ │ 47 | │ │ 48 | │ │ 49 | │ │ 50 | │ │ 51 | │ │ 52 | │ │ 53 | └────────────────────────────────────────────────┘ 54 | Press to pause. 55 | 56 | --- SECTION SEPARATOR --- 57 | 58 | 59 | 60 | 5 0.00B / 28.00B 61 | 4 0.00B / 26.00B 62 | 1 0.00B / 22.00B 63 | 2 0.00B / 21.00B 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 3.3.3.3 0.00B / 28.00B 85 | 2.2.2.2 0.00B / 26.00B 86 | 1.1.1.1 0.00B / 22.00B 87 | 4.4.4.4 0.00B / 21.00B 88 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__layout-under-50-width-under-50-height-events.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | ShowCursor, 15 | ] 16 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__layout-under-70-width-under-30-height-draw_events.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_draw_events.lock().unwrap().join(SNAPSHOT_SECTION_SEPARATOR) 4 | --- 5 | IF: interface_name | Total Rate (Up / Down): 0.00B / 0.00B 6 | ┌Utilization by process name────────────────────────────────────────┐ 7 | │Process Connections Rate (Up / Down) │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ │ 15 | │ │ 16 | │ │ 17 | │ │ 18 | │ │ 19 | │ │ 20 | │ │ 21 | │ │ 22 | │ │ 23 | │ │ 24 | │ │ 25 | │ │ 26 | │ │ 27 | │ │ 28 | │ │ 29 | │ │ 30 | │ │ 31 | │ │ 32 | └───────────────────────────────────────────────────────────────────┘ 33 | Press to pause. Use to rearrange tables. 34 | 35 | --- SECTION SEPARATOR --- 36 | 98. 0B 37 | 38 | 39 | 5 1 0.00B / 28.00B 40 | 4 1 0.00B / 26.00B 41 | 1 1 0.00B / 22.00B 42 | 2 1 0.00B / 21.00B 43 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__layout-under-70-width-under-30-height-events.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | ShowCursor, 15 | ] 16 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__multiple_connections_from_remote_address-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | ShowCursor, 15 | ] 16 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__multiple_packets_of_traffic_from_different_connections-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | ShowCursor, 15 | ] 16 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__multiple_packets_of_traffic_from_single_connection-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | ShowCursor, 15 | ] 16 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__multiple_processes_with_multiple_connections-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | ShowCursor, 15 | ] 16 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__no_resolve_mode-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | Draw, 15 | HideCursor, 16 | Flush, 17 | ShowCursor, 18 | ] 19 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__one_packet_of_traffic-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | ShowCursor, 15 | ] 16 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__one_process_with_multiple_connections-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | ShowCursor, 15 | ] 16 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__pause_by_space-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | Draw, 15 | HideCursor, 16 | Flush, 17 | ShowCursor, 18 | ] 19 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__rearranged_by_tab-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | Draw, 15 | HideCursor, 16 | Flush, 17 | Draw, 18 | HideCursor, 19 | Flush, 20 | Draw, 21 | HideCursor, 22 | Flush, 23 | ShowCursor, 24 | ] 25 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__sustained_traffic_from_multiple_processes-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | Draw, 15 | HideCursor, 16 | Flush, 17 | ShowCursor, 18 | ] 19 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__sustained_traffic_from_multiple_processes_bi_directional-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | Draw, 15 | HideCursor, 16 | Flush, 17 | ShowCursor, 18 | ] 19 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__sustained_traffic_from_multiple_processes_bi_directional_total-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | Draw, 15 | HideCursor, 16 | Flush, 17 | ShowCursor, 18 | ] 19 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__sustained_traffic_from_multiple_processes_total-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | Draw, 15 | HideCursor, 16 | Flush, 17 | ShowCursor, 18 | ] 19 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__sustained_traffic_from_one_process-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | Draw, 15 | HideCursor, 16 | Flush, 17 | ShowCursor, 18 | ] 19 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__sustained_traffic_from_one_process_total-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | Draw, 15 | HideCursor, 16 | Flush, 17 | ShowCursor, 18 | ] 19 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__traffic_with_host_names-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | Draw, 15 | HideCursor, 16 | Flush, 17 | ShowCursor, 18 | ] 19 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__traffic_with_winch_event-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | Draw, 15 | HideCursor, 16 | Flush, 17 | ShowCursor, 18 | ] 19 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__truncate_long_hostnames-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_events.lock().unwrap().as_slice() 4 | --- 5 | [ 6 | Clear, 7 | HideCursor, 8 | Draw, 9 | HideCursor, 10 | Flush, 11 | Draw, 12 | HideCursor, 13 | Flush, 14 | Draw, 15 | HideCursor, 16 | Flush, 17 | ShowCursor, 18 | ] 19 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__two_windows_split_horizontally.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_draw_events.lock().unwrap().join(SNAPSHOT_SECTION_SEPARATOR) 4 | --- 5 | IF: interface_name | Total Rate (Up / Down): 0.00B / 0.00B 6 | ┌Utilization by remote address─────────────────────────────┐ 7 | │Remote Address Rate (Up / Down) │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ │ 15 | │ │ 16 | │ │ 17 | │ │ 18 | │ │ 19 | │ │ 20 | │ │ 21 | │ │ 22 | │ │ 23 | │ │ 24 | │ │ 25 | │ │ 26 | │ │ 27 | │ │ 28 | │ │ 29 | └──────────────────────────────────────────────────────────┘ 30 | ┌Utilization by connection─────────────────────────────────┐ 31 | │Connection Rate (Up / Down) │ 32 | │ │ 33 | │ │ 34 | │ │ 35 | │ │ 36 | │ │ 37 | │ │ 38 | │ │ 39 | │ │ 40 | │ │ 41 | │ │ 42 | │ │ 43 | │ │ 44 | │ │ 45 | │ │ 46 | │ │ 47 | │ │ 48 | │ │ 49 | │ │ 50 | │ │ 51 | │ │ 52 | │ │ 53 | └──────────────────────────────────────────────────────────┘ 54 | Press to pause. Use to rearrange tables. 55 | 56 | --- SECTION SEPARATOR --- 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/bandwhich__tests__cases__ui__two_windows_split_vertically.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: terminal_draw_events.lock().unwrap().join(SNAPSHOT_SECTION_SEPARATOR) 4 | --- 5 | IF: interface_name | Total Rate (Up / Down): 0.00B / 0.00B 6 | ┌Utilization by remote address────────────────────────────────────────────────────────────────┐┌Utilization by connection────────────────────────────────────────────────────────────────────┐ 7 | │Remote Address Connections Rate (Up / Down) ││Connection Process Rate (Up / Down) │ 8 | │ ││ │ 9 | │ ││ │ 10 | │ ││ │ 11 | │ ││ │ 12 | │ ││ │ 13 | │ ││ │ 14 | │ ││ │ 15 | │ ││ │ 16 | │ ││ │ 17 | │ ││ │ 18 | │ ││ │ 19 | │ ││ │ 20 | │ ││ │ 21 | │ ││ │ 22 | │ ││ │ 23 | │ ││ │ 24 | │ ││ │ 25 | │ ││ │ 26 | │ ││ │ 27 | │ ││ │ 28 | │ ││ │ 29 | │ ││ │ 30 | │ ││ │ 31 | │ ││ │ 32 | │ ││ │ 33 | │ ││ │ 34 | │ ││ │ 35 | │ ││ │ 36 | │ ││ │ 37 | │ ││ │ 38 | │ ││ │ 39 | │ ││ │ 40 | │ ││ │ 41 | │ ││ │ 42 | │ ││ │ 43 | │ ││ │ 44 | │ ││ │ 45 | │ ││ │ 46 | │ ││ │ 47 | │ ││ │ 48 | │ ││ │ 49 | │ ││ │ 50 | │ ││ │ 51 | │ ││ │ 52 | │ ││ │ 53 | └─────────────────────────────────────────────────────────────────────────────────────────────┘└─────────────────────────────────────────────────────────────────────────────────────────────┘ 54 | Press to pause. Use to rearrange tables. (DNS queries hidden). 55 | 56 | -------------------------------------------------------------------------------- /src/tests/fakes/fake_input.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | net::{IpAddr, Ipv4Addr, SocketAddr}, 4 | thread, time, 5 | }; 6 | 7 | use async_trait::async_trait; 8 | use crossterm::event::Event; 9 | use itertools::Itertools; 10 | use pnet::{ 11 | datalink::{DataLinkReceiver, NetworkInterface}, 12 | ipnetwork::IpNetwork, 13 | }; 14 | use tokio::runtime::Runtime; 15 | 16 | use crate::{ 17 | network::{ 18 | dns::{self, Lookup}, 19 | Connection, Protocol, 20 | }, 21 | os::ProcessInfo, 22 | OpenSockets, 23 | }; 24 | 25 | pub struct TerminalEvents { 26 | pub events: Vec>, 27 | } 28 | 29 | impl TerminalEvents { 30 | pub fn new(mut events: Vec>) -> Self { 31 | events.reverse(); // this is so that we do not have to shift the array 32 | TerminalEvents { events } 33 | } 34 | } 35 | impl Iterator for TerminalEvents { 36 | type Item = Event; 37 | fn next(&mut self) -> Option { 38 | match self.events.pop() { 39 | Some(ev) => match ev { 40 | Some(ev) => Some(ev), 41 | None => { 42 | thread::sleep(time::Duration::from_millis(900)); 43 | self.next() 44 | } 45 | }, 46 | None => None, 47 | } 48 | } 49 | } 50 | 51 | pub struct NetworkFrames { 52 | pub packets: Vec>>, 53 | pub current_index: usize, 54 | } 55 | 56 | impl NetworkFrames { 57 | pub fn new(packets: Vec>>) -> Box { 58 | Box::new(NetworkFrames { 59 | packets, 60 | current_index: 0, 61 | }) 62 | } 63 | fn next_packet(&mut self) -> Option<&[u8]> { 64 | let next_index = self.current_index; 65 | self.current_index += 1; 66 | self.packets.get(next_index).and_then(|p| p.as_deref()) 67 | } 68 | } 69 | impl DataLinkReceiver for NetworkFrames { 70 | fn next(&mut self) -> Result<&[u8], std::io::Error> { 71 | if self.current_index == 0 { 72 | // make it less likely to have a race condition with the display loop 73 | // this is so the tests pass consistently 74 | thread::sleep(time::Duration::from_millis(500)); 75 | } 76 | if self.current_index < self.packets.len() { 77 | let action = self.next_packet(); 78 | match action { 79 | Some(packet) => Ok(packet), 80 | None => { 81 | thread::sleep(time::Duration::from_secs(1)); 82 | Ok(&[]) 83 | } 84 | } 85 | } else { 86 | thread::sleep(time::Duration::from_secs(1)); 87 | Ok(&[]) 88 | } 89 | } 90 | } 91 | 92 | pub fn get_open_sockets() -> OpenSockets { 93 | let mut open_sockets = HashMap::new(); 94 | let local_ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)); 95 | open_sockets.insert( 96 | Connection::new( 97 | SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), 12345), 98 | local_ip, 99 | 443, 100 | Protocol::Tcp, 101 | ), 102 | ProcessInfo::new("1", 1), 103 | ); 104 | open_sockets.insert( 105 | Connection::new( 106 | SocketAddr::new(IpAddr::V4(Ipv4Addr::new(2, 2, 2, 2)), 54321), 107 | local_ip, 108 | 4434, 109 | Protocol::Tcp, 110 | ), 111 | ProcessInfo::new("4", 4), 112 | ); 113 | open_sockets.insert( 114 | Connection::new( 115 | SocketAddr::new(IpAddr::V4(Ipv4Addr::new(3, 3, 3, 3)), 1337), 116 | local_ip, 117 | 4435, 118 | Protocol::Tcp, 119 | ), 120 | ProcessInfo::new("5", 5), 121 | ); 122 | open_sockets.insert( 123 | Connection::new( 124 | SocketAddr::new(IpAddr::V4(Ipv4Addr::new(4, 4, 4, 4)), 1337), 125 | local_ip, 126 | 4432, 127 | Protocol::Tcp, 128 | ), 129 | ProcessInfo::new("2", 2), 130 | ); 131 | open_sockets.insert( 132 | Connection::new( 133 | SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), 12346), 134 | local_ip, 135 | 443, 136 | Protocol::Tcp, 137 | ), 138 | ProcessInfo::new("1", 1), 139 | ); 140 | let mut local_socket_to_procs = HashMap::new(); 141 | let mut connections = std::vec::Vec::new(); 142 | for (connection, proc_info) in open_sockets { 143 | local_socket_to_procs.insert(connection.local_socket, proc_info); 144 | connections.push(connection); 145 | } 146 | 147 | OpenSockets { 148 | sockets_to_procs: local_socket_to_procs, 149 | } 150 | } 151 | 152 | pub fn get_interfaces() -> Vec { 153 | vec![NetworkInterface { 154 | name: String::from("interface_name"), 155 | description: String::from("Fake interface"), 156 | index: 42, 157 | mac: None, 158 | ips: vec![IpNetwork::V4("10.0.0.2".parse().unwrap())], 159 | // It's important that the IFF_LOOPBACK bit is set to 0. 160 | // Otherwise sniffer will attempt to start parse packets 161 | // at offset 14 162 | flags: 0, 163 | }] 164 | } 165 | 166 | pub fn get_interfaces_with_frames( 167 | frames: impl IntoIterator>, 168 | ) -> Vec<(NetworkInterface, Box)> { 169 | get_interfaces().into_iter().zip_eq(frames).collect() 170 | } 171 | 172 | pub fn create_fake_dns_client(ips_to_hosts: HashMap) -> Option { 173 | let runtime = Runtime::new().unwrap(); 174 | let dns_client = dns::Client::new(FakeResolver(ips_to_hosts), runtime).unwrap(); 175 | Some(dns_client) 176 | } 177 | 178 | struct FakeResolver(HashMap); 179 | 180 | #[async_trait] 181 | impl Lookup for FakeResolver { 182 | async fn lookup(&self, ip: IpAddr) -> Option { 183 | self.0.get(&ip).cloned() 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/tests/fakes/fake_output.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | io, 4 | sync::{Arc, Mutex}, 5 | }; 6 | 7 | use ratatui::{ 8 | backend::{Backend, WindowSize}, 9 | buffer::Cell, 10 | layout::{Position, Size}, 11 | }; 12 | 13 | #[derive(Hash, Debug, PartialEq)] 14 | pub enum TerminalEvent { 15 | Clear, 16 | HideCursor, 17 | ShowCursor, 18 | GetCursor, 19 | Flush, 20 | Draw, 21 | } 22 | 23 | pub struct TestBackend { 24 | pub events: Arc>>, 25 | pub draw_events: Arc>>, 26 | terminal_width: Arc>, 27 | terminal_height: Arc>, 28 | } 29 | 30 | impl TestBackend { 31 | pub fn new( 32 | log: Arc>>, 33 | draw_log: Arc>>, 34 | terminal_width: Arc>, 35 | terminal_height: Arc>, 36 | ) -> TestBackend { 37 | TestBackend { 38 | events: log, 39 | draw_events: draw_log, 40 | terminal_width, 41 | terminal_height, 42 | } 43 | } 44 | } 45 | 46 | #[derive(Hash, Eq, PartialEq)] 47 | struct Point { 48 | x: u16, 49 | y: u16, 50 | } 51 | 52 | impl Backend for TestBackend { 53 | fn clear(&mut self) -> io::Result<()> { 54 | self.events.lock().unwrap().push(TerminalEvent::Clear); 55 | Ok(()) 56 | } 57 | 58 | fn hide_cursor(&mut self) -> io::Result<()> { 59 | self.events.lock().unwrap().push(TerminalEvent::HideCursor); 60 | Ok(()) 61 | } 62 | 63 | fn show_cursor(&mut self) -> io::Result<()> { 64 | self.events.lock().unwrap().push(TerminalEvent::ShowCursor); 65 | Ok(()) 66 | } 67 | 68 | fn get_cursor_position(&mut self) -> io::Result { 69 | self.events.lock().unwrap().push(TerminalEvent::GetCursor); 70 | Ok(Position::new(0, 0)) 71 | } 72 | 73 | fn set_cursor_position>(&mut self, _position: P) -> io::Result<()> { 74 | Ok(()) 75 | } 76 | 77 | fn draw<'a, I>(&mut self, content: I) -> io::Result<()> 78 | where 79 | I: Iterator, 80 | { 81 | // use std::fmt::Write; 82 | self.events.lock().unwrap().push(TerminalEvent::Draw); 83 | let mut string = String::with_capacity(content.size_hint().0 * 3); 84 | let mut coordinates = HashMap::new(); 85 | for (x, y, cell) in content { 86 | coordinates.insert(Point { x, y }, cell); 87 | } 88 | let terminal_height = self.terminal_height.lock().unwrap(); 89 | let terminal_width = self.terminal_width.lock().unwrap(); 90 | for y in 0..*terminal_height { 91 | for x in 0..*terminal_width { 92 | match coordinates.get(&Point { x, y }) { 93 | Some(cell) => { 94 | // this will contain no style information at all 95 | // should be good enough for testing 96 | string.push_str(cell.symbol()); 97 | } 98 | None => { 99 | string.push(' '); 100 | } 101 | } 102 | } 103 | string.push('\n'); 104 | } 105 | self.draw_events.lock().unwrap().push(string); 106 | Ok(()) 107 | } 108 | 109 | fn size(&self) -> io::Result { 110 | let terminal_height = self.terminal_height.lock().unwrap(); 111 | let terminal_width = self.terminal_width.lock().unwrap(); 112 | 113 | Ok(Size::new(*terminal_width, *terminal_height)) 114 | } 115 | 116 | fn window_size(&mut self) -> io::Result { 117 | let width = *self.terminal_width.lock().unwrap(); 118 | let height = *self.terminal_height.lock().unwrap(); 119 | 120 | Ok(WindowSize { 121 | columns_rows: Size { width, height }, 122 | pixels: Size::default(), 123 | }) 124 | } 125 | 126 | fn flush(&mut self) -> io::Result<()> { 127 | self.events.lock().unwrap().push(TerminalEvent::Flush); 128 | Ok(()) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/tests/fakes/mod.rs: -------------------------------------------------------------------------------- 1 | mod fake_input; 2 | mod fake_output; 3 | 4 | pub use fake_input::*; 5 | pub use fake_output::*; 6 | -------------------------------------------------------------------------------- /src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cases; 2 | pub mod fakes; 3 | --------------------------------------------------------------------------------